diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 7e03201b27887d..026a3032a1c684 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1,15 +1,19 @@ +mod header; +mod mouse; + +#[cfg(test)] +pub(crate) use header::StickyHeader; +pub(crate) use header::{header_jump_data, render_buffer_header}; + use crate::{ - BUFFER_HEADER_PADDING, BlockId, CURSORS_VISIBLE_FOR, ChunkRendererContext, ChunkReplacement, - CodeActionSource, ColumnarMode, ConflictsOurs, ConflictsOursMarker, ConflictsOuter, - ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId, - DisplayDiffHunk, DisplayPoint, DisplayRow, EditDisplayMode, EditPrediction, Editor, EditorMode, - EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT, FocusedBlock, - GutterDimensions, GutterHoverButton, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, - InlayHintRefreshReason, JumpData, LineDown, LineHighlight, LineUp, MAX_LINE_LEN, - MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown, PageUp, - PhantomDiffReviewIndicator, Point, RowExt, RowRangeExt, SelectPhase, Selection, - SelectionDragState, SelectionEffects, SizingBehavior, SoftWrap, StickyHeaderExcerpt, ToPoint, - ToggleFold, ToggleFoldAll, + BUFFER_HEADER_PADDING, BlockId, ChunkRendererContext, ChunkReplacement, CodeActionSource, + ConflictsOurs, ConflictsOursMarker, ConflictsOuter, ConflictsTheirs, ConflictsTheirsMarker, + ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk, DisplayPoint, DisplayRow, + EditDisplayMode, EditPrediction, Editor, EditorMode, EditorSettings, EditorSnapshot, + EditorStyle, FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, + HandleInput, HoveredCursor, InlayHintRefreshReason, LineDown, LineHighlight, LineUp, + MAX_LINE_LEN, MINIMAP_FONT_SIZE, PageDown, PageUp, Point, RowExt, RowRangeExt, Selection, + SelectionDragState, SizingBehavior, SoftWrap, ToPoint, code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP}, column_pixels, display_map::{ @@ -17,38 +21,34 @@ use crate::{ HighlightKey, HighlightedChunk, ToDisplayPoint, }, editor_settings::{ - CurrentLineHighlight, DocumentColorsRenderMode, DoubleClickInMultibuffer, Minimap, - MinimapThumb, MinimapThumbBorder, ScrollBeyondLastLine, ScrollbarAxes, - ScrollbarDiagnostics, ShowMinimap, + CurrentLineHighlight, DocumentColorsRenderMode, Minimap, MinimapThumb, MinimapThumbBorder, + ScrollBeyondLastLine, ScrollbarAxes, ScrollbarDiagnostics, ShowMinimap, }, git::blame::{BlameRenderer, GitBlame, GlobalBlameRenderer}, hover_popover::{ self, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, - POPOVER_RIGHT_OFFSET, hover_at, + POPOVER_RIGHT_OFFSET, }, inlay_hint_settings, - mouse_context_menu::{self, MenuPosition}, scroll::{ - ActiveScrollbarState, Autoscroll, ScrollOffset, ScrollPixelOffset, ScrollbarThumbState, + ActiveScrollbarState, ScrollOffset, ScrollPixelOffset, ScrollbarThumbState, scroll_amount::ScrollAmount, }, }; use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; use collections::{BTreeMap, HashMap, HashSet}; use feature_flags::{DiffReviewFeatureFlag, FeatureFlagAppExt as _}; -use file_icons::FileIcons; -use git::{Oid, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus}; +use git::{Oid, blame::BlameEntry, commit::ParsedCommitMessage}; use gpui::{ Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle, - Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corners, CursorStyle, DispatchPhase, - Edges, Element, ElementInputHandler, Entity, Focusable as _, Font, FontId, FontWeight, - GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, Length, - Modifiers, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent, - MousePressureEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, PressureStage, ScrollDelta, - ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, - Style, Styled, StyledText, TaskExt, TextAlign, TextRun, TextStyleRefinement, WeakEntity, - Window, anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, - pattern_slash, point, px, quad, relative, size, solid_background, transparent_black, + Bounds, ClipboardItem, ContentMask, Context, Corners, CursorStyle, DispatchPhase, Edges, + Element, ElementInputHandler, Entity, Focusable as _, Font, FontId, FontWeight, + GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, + ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, + ParentElement, Pixels, ScrollHandle, ShapedLine, SharedString, Size, + StatefulInteractiveElement, Style, Styled, StyledText, TaskExt, TextAlign, TextRun, + TextStyleRefinement, WeakEntity, Window, div, fill, outline, pattern_slash, point, px, quad, + relative, size, solid_background, transparent_black, }; use itertools::Itertools; use language::{ @@ -57,18 +57,16 @@ use language::{ }; use markdown::Markdown; use multi_buffer::{ - Anchor, ExcerptBoundaryInfo, ExpandExcerptDirection, ExpandInfo, MultiBufferPoint, - MultiBufferRow, RowInfo, + Anchor, ExpandExcerptDirection, ExpandInfo, MultiBufferPoint, MultiBufferRow, RowInfo, }; use project::{ - DisableAiSettings, Entry, debugger::breakpoint_store::{Breakpoint, BreakpointSessionState}, project_settings::ProjectSettings, }; use settings::{ GitGutterSetting, GitHunkStyleSetting, IndentGuideBackgroundColoring, IndentGuideColoring, - RelativeLineNumbers, Settings, + Settings, }; use smallvec::{SmallVec, smallvec}; use std::{ @@ -79,26 +77,20 @@ use std::{ fmt::{self, Write}, iter, mem, ops::{Deref, Range}, - path::{self, Path}, rc::Rc, sync::Arc, - time::{Duration, Instant}, + time::Duration, }; use sum_tree::Bias; -use text::{BufferId, SelectionGoal}; +use text::BufferId; use theme::{ActiveTheme, Appearance, PlayerColor}; use theme_settings::BufferLineHeight; use ui::utils::ensure_minimum_contrast; -use ui::{ - ButtonLike, ContextMenu, Indicator, KeyBinding, POPOVER_Y_PADDING, Tooltip, prelude::*, - right_click_menu, scrollbars::ShowScrollbar, text_for_keystroke, -}; +use ui::{ButtonLike, POPOVER_Y_PADDING, Tooltip, prelude::*, scrollbars::ShowScrollbar}; use unicode_segmentation::UnicodeSegmentation; -use util::post_inc; -use util::{RangeExt, ResultExt, debug_panic}; +use util::{ResultExt, debug_panic}; use workspace::{ - CollaboratorId, ItemHandle, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel, - Workspace, + CollaboratorId, ItemHandle, Workspace, item::{Item, ItemBufferKind}, }; @@ -222,10 +214,6 @@ impl EditorElement { self.split_side = Some(side); } - fn should_show_buffer_headers(&self) -> bool { - self.split_side.is_none() - } - fn register_actions(&self, window: &mut Window, cx: &mut App) { let editor = &self.editor; editor.update(cx, |editor, cx| { @@ -713,840 +701,6 @@ impl EditorElement { }); } - fn mouse_left_down( - editor: &mut Editor, - event: &MouseDownEvent, - position_map: &PositionMap, - line_numbers: &HashMap, - window: &mut Window, - cx: &mut Context, - ) { - if window.default_prevented() { - return; - } - - let text_hitbox = &position_map.text_hitbox; - let gutter_hitbox = &position_map.gutter_hitbox; - let point_for_position = position_map.point_for_position(event.position); - let mut click_count = event.click_count; - let mut modifiers = event.modifiers; - - if let Some(hovered_hunk) = - position_map - .display_hunks - .iter() - .find_map(|(hunk, hunk_hitbox)| match hunk { - DisplayDiffHunk::Folded { .. } => None, - DisplayDiffHunk::Unfolded { - multi_buffer_range, .. - } => hunk_hitbox - .as_ref() - .is_some_and(|hitbox| hitbox.is_hovered(window)) - .then(|| multi_buffer_range.clone()), - }) - { - editor.toggle_single_diff_hunk(hovered_hunk, cx); - cx.notify(); - return; - } else if gutter_hitbox.is_hovered(window) { - click_count = 3; // Simulate triple-click when clicking the gutter to select lines - } else if !text_hitbox.is_hovered(window) { - return; - } - - if EditorSettings::get_global(cx) - .drag_and_drop_selection - .enabled - && click_count == 1 - && !modifiers.shift - { - let newest_anchor = editor.selections.newest_anchor(); - let snapshot = editor.snapshot(window, cx); - let selection = newest_anchor.map(|anchor| anchor.to_display_point(&snapshot)); - if point_for_position.intersects_selection(&selection) { - editor.selection_drag_state = SelectionDragState::ReadyToDrag { - selection: newest_anchor.clone(), - click_position: event.position, - mouse_down_time: Instant::now(), - }; - cx.stop_propagation(); - return; - } - } - - let is_singleton = editor.buffer().read(cx).is_singleton(); - - if click_count == 2 && !is_singleton { - match EditorSettings::get_global(cx).double_click_in_multibuffer { - DoubleClickInMultibuffer::Select => { - // do nothing special on double click, all selection logic is below - } - DoubleClickInMultibuffer::Open => { - if modifiers.alt { - // if double click is made with alt, pretend it's a regular double click without opening and alt, - // and run the selection logic. - modifiers.alt = false; - } else { - let scroll_position_row = position_map.scroll_position.y; - let display_row = (((event.position - gutter_hitbox.bounds.origin).y - / position_map.line_height) - as f64 - + position_map.scroll_position.y) - as u32; - let multi_buffer_row = position_map - .snapshot - .display_point_to_point( - DisplayPoint::new(DisplayRow(display_row), 0), - Bias::Right, - ) - .row; - let line_offset_from_top = display_row - scroll_position_row as u32; - // if double click is made without alt, open the corresponding excerp - editor.open_excerpts_common( - Some(JumpData::MultiBufferRow { - row: MultiBufferRow(multi_buffer_row), - line_offset_from_top, - }), - false, - window, - cx, - ); - return; - } - } - } - } - - if !is_singleton { - let display_row = (ScrollPixelOffset::from( - (event.position - gutter_hitbox.bounds.origin).y / position_map.line_height, - ) + position_map.scroll_position.y) as u32; - let multi_buffer_row = position_map - .snapshot - .display_point_to_point(DisplayPoint::new(DisplayRow(display_row), 0), Bias::Right) - .row; - if line_numbers - .get(&MultiBufferRow(multi_buffer_row)) - .is_some_and(|line_layout| { - line_layout.segments.iter().any(|segment| { - segment - .hitbox - .as_ref() - .is_some_and(|hitbox| hitbox.contains(&event.position)) - }) - }) - { - let line_offset_from_top = display_row - position_map.scroll_position.y as u32; - - editor.open_excerpts_common( - Some(JumpData::MultiBufferRow { - row: MultiBufferRow(multi_buffer_row), - line_offset_from_top, - }), - modifiers.alt, - window, - cx, - ); - cx.stop_propagation(); - return; - } - } - - let position = point_for_position.nearest_valid; - if let Some(mode) = Editor::columnar_selection_mode(&modifiers, cx) { - editor.select( - SelectPhase::BeginColumnar { - position, - reset: match mode { - ColumnarMode::FromMouse => true, - ColumnarMode::FromSelection => false, - }, - mode, - goal_column: point_for_position.exact_unclipped.column(), - }, - window, - cx, - ); - } else if modifiers.shift && !modifiers.control && !modifiers.alt && !modifiers.secondary() - { - editor.select( - SelectPhase::Extend { - position, - click_count, - }, - window, - cx, - ); - } else { - editor.select( - SelectPhase::Begin { - position, - add: Editor::is_alt_pressed(&modifiers, cx), - click_count, - }, - window, - cx, - ); - } - cx.stop_propagation(); - } - - fn mouse_right_down( - editor: &mut Editor, - event: &MouseDownEvent, - position_map: &PositionMap, - window: &mut Window, - cx: &mut Context, - ) { - if position_map.gutter_hitbox.is_hovered(window) { - let gutter_right_padding = editor.gutter_dimensions.right_padding; - let hitbox = &position_map.gutter_hitbox; - - if event.position.x <= hitbox.bounds.right() - gutter_right_padding - // Don't show the gutter_context_menu in collab notes - && editor.project.is_some() - { - let point_for_position = position_map.point_for_position(event.position); - editor.set_gutter_context_menu( - point_for_position.nearest_valid.row(), - None, - event.position, - window, - cx, - ); - } - return; - } - - if !position_map.text_hitbox.is_hovered(window) { - return; - } - - let point_for_position = position_map.point_for_position(event.position); - mouse_context_menu::deploy_context_menu( - editor, - Some(event.position), - point_for_position.nearest_valid, - window, - cx, - ); - cx.stop_propagation(); - } - - fn mouse_middle_down( - editor: &mut Editor, - event: &MouseDownEvent, - position_map: &PositionMap, - window: &mut Window, - cx: &mut Context, - ) { - if !position_map.text_hitbox.is_hovered(window) || window.default_prevented() { - return; - } - - let point_for_position = position_map.point_for_position(event.position); - let position = point_for_position.nearest_valid; - - editor.select( - SelectPhase::BeginColumnar { - position, - reset: true, - mode: ColumnarMode::FromMouse, - goal_column: point_for_position.exact_unclipped.column(), - }, - window, - cx, - ); - } - - fn mouse_up( - editor: &mut Editor, - event: &MouseUpEvent, - position_map: &PositionMap, - window: &mut Window, - cx: &mut Context, - ) { - // Handle diff review drag completion - if editor.diff_review_drag_state.is_some() { - editor.end_diff_review_drag(window, cx); - cx.stop_propagation(); - return; - } - - let text_hitbox = &position_map.text_hitbox; - let end_selection = editor.has_pending_selection(); - let pending_nonempty_selections = editor.has_pending_nonempty_selection(); - let point_for_position = position_map.point_for_position(event.position); - - match editor.selection_drag_state { - SelectionDragState::ReadyToDrag { - selection: _, - ref click_position, - mouse_down_time: _, - } => { - if event.position == *click_position { - editor.select( - SelectPhase::Begin { - position: point_for_position.nearest_valid, - add: false, - click_count: 1, // ready to drag state only occurs on click count 1 - }, - window, - cx, - ); - editor.selection_drag_state = SelectionDragState::None; - cx.stop_propagation(); - return; - } else { - debug_panic!("drag state can never be in ready state after drag") - } - } - SelectionDragState::Dragging { ref selection, .. } => { - let snapshot = editor.snapshot(window, cx); - let selection_display = selection.map(|anchor| anchor.to_display_point(&snapshot)); - if !point_for_position.intersects_selection(&selection_display) - && text_hitbox.is_hovered(window) - { - let is_cut = !(cfg!(target_os = "macos") && event.modifiers.alt - || cfg!(not(target_os = "macos")) && event.modifiers.control); - editor.move_selection_on_drop( - &selection.clone(), - point_for_position.nearest_valid, - is_cut, - window, - cx, - ); - } - editor.selection_drag_state = SelectionDragState::None; - cx.stop_propagation(); - cx.notify(); - return; - } - _ => {} - } - - if end_selection { - editor.select(SelectPhase::End, window, cx); - } - - if end_selection && pending_nonempty_selections { - cx.stop_propagation(); - } else if cfg!(any(target_os = "linux", target_os = "freebsd")) - && event.button == MouseButton::Middle - { - #[allow( - clippy::collapsible_if, - clippy::needless_return, - reason = "The cfg-block below makes this a false positive" - )] - if !text_hitbox.is_hovered(window) || editor.read_only(cx) { - return; - } - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - if EditorSettings::get_global(cx).middle_click_paste { - if let Some(text) = cx.read_from_primary().and_then(|item| item.text()) { - let point_for_position = position_map.point_for_position(event.position); - let position = point_for_position.nearest_valid; - - editor.select( - SelectPhase::Begin { - position, - add: false, - click_count: 1, - }, - window, - cx, - ); - editor.insert(&text, window, cx); - } - cx.stop_propagation() - } - } - } - - fn click( - editor: &mut Editor, - event: &ClickEvent, - position_map: &PositionMap, - window: &mut Window, - cx: &mut Context, - ) { - let text_hitbox = &position_map.text_hitbox; - let pending_nonempty_selections = editor.has_pending_nonempty_selection(); - - let hovered_link_modifier = Editor::is_cmd_or_ctrl_pressed(&event.modifiers(), cx); - let mouse_down_hovered_link_modifier = if let ClickEvent::Mouse(mouse_event) = event { - Editor::is_cmd_or_ctrl_pressed(&mouse_event.down.modifiers, cx) - } else { - true - }; - - if let Some(mouse_position) = event.mouse_position() - && !pending_nonempty_selections - && hovered_link_modifier - && mouse_down_hovered_link_modifier - && text_hitbox.is_hovered(window) - && !matches!( - editor.selection_drag_state, - SelectionDragState::Dragging { .. } - ) - { - let point = position_map.point_for_position(mouse_position); - editor.handle_click_hovered_link(point, event.modifiers(), window, cx); - editor.selection_drag_state = SelectionDragState::None; - - cx.stop_propagation(); - } - } - - fn pressure_click( - editor: &mut Editor, - event: &MousePressureEvent, - position_map: &PositionMap, - window: &mut Window, - cx: &mut Context, - ) { - let text_hitbox = &position_map.text_hitbox; - let force_click_possible = - matches!(editor.prev_pressure_stage, Some(PressureStage::Normal)) - && event.stage == PressureStage::Force; - - editor.prev_pressure_stage = Some(event.stage); - - if force_click_possible && text_hitbox.is_hovered(window) { - let point = position_map.point_for_position(event.position); - editor.handle_click_hovered_link(point, event.modifiers, window, cx); - editor.selection_drag_state = SelectionDragState::None; - cx.stop_propagation(); - } - } - - fn mouse_dragged( - editor: &mut Editor, - event: &MouseMoveEvent, - position_map: &PositionMap, - window: &mut Window, - cx: &mut Context, - ) { - if !editor.has_pending_selection() - && matches!(editor.selection_drag_state, SelectionDragState::None) - { - return; - } - - let point_for_position = position_map.point_for_position(event.position); - let text_hitbox = &position_map.text_hitbox; - - let scroll_delta = { - let text_bounds = text_hitbox.bounds; - let mut scroll_delta = gpui::Point::::default(); - let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0); - let top = text_bounds.origin.y + vertical_margin; - let bottom = text_bounds.bottom_left().y - vertical_margin; - if event.position.y < top { - scroll_delta.y = -scale_vertical_mouse_autoscroll_delta(top - event.position.y); - } - if event.position.y > bottom { - scroll_delta.y = scale_vertical_mouse_autoscroll_delta(event.position.y - bottom); - } - - // We need horizontal width of text - let style = editor.style.clone().unwrap_or_default(); - let font_id = window.text_system().resolve_font(&style.text.font()); - let font_size = style.text.font_size.to_pixels(window.rem_size()); - let em_width = window.text_system().em_width(font_id, font_size).unwrap(); - - let scroll_margin_x = EditorSettings::get_global(cx).horizontal_scroll_margin; - - let scroll_space: Pixels = scroll_margin_x * em_width; - - let left = text_bounds.origin.x + scroll_space; - let right = text_bounds.top_right().x - scroll_space; - - if event.position.x < left { - scroll_delta.x = -scale_horizontal_mouse_autoscroll_delta(left - event.position.x); - } - if event.position.x > right { - scroll_delta.x = scale_horizontal_mouse_autoscroll_delta(event.position.x - right); - } - scroll_delta - }; - - if !editor.has_pending_selection() { - let drop_anchor = position_map - .snapshot - .display_point_to_anchor(point_for_position.nearest_valid, Bias::Left); - match editor.selection_drag_state { - SelectionDragState::Dragging { - ref mut drop_cursor, - ref mut hide_drop_cursor, - .. - } => { - drop_cursor.start = drop_anchor; - drop_cursor.end = drop_anchor; - *hide_drop_cursor = !text_hitbox.is_hovered(window); - editor.apply_scroll_delta(scroll_delta, window, cx); - cx.notify(); - } - SelectionDragState::ReadyToDrag { - ref selection, - ref click_position, - ref mouse_down_time, - } => { - let drag_and_drop_delay = Duration::from_millis( - EditorSettings::get_global(cx) - .drag_and_drop_selection - .delay - .0, - ); - if mouse_down_time.elapsed() >= drag_and_drop_delay { - let drop_cursor = Selection { - id: post_inc(&mut editor.selections.next_selection_id()), - start: drop_anchor, - end: drop_anchor, - reversed: false, - goal: SelectionGoal::None, - }; - editor.selection_drag_state = SelectionDragState::Dragging { - selection: selection.clone(), - drop_cursor, - hide_drop_cursor: false, - }; - editor.apply_scroll_delta(scroll_delta, window, cx); - cx.notify(); - } else { - let click_point = position_map.point_for_position(*click_position); - editor.selection_drag_state = SelectionDragState::None; - editor.select( - SelectPhase::Begin { - position: click_point.nearest_valid, - add: false, - click_count: 1, - }, - window, - cx, - ); - editor.select( - SelectPhase::Update { - position: point_for_position.nearest_valid, - goal_column: point_for_position.exact_unclipped.column(), - scroll_delta, - }, - window, - cx, - ); - } - } - _ => {} - } - } else { - editor.select( - SelectPhase::Update { - position: point_for_position.nearest_valid, - goal_column: point_for_position.exact_unclipped.column(), - scroll_delta, - }, - window, - cx, - ); - } - } - - pub(crate) fn mouse_moved( - editor: &mut Editor, - event: &MouseMoveEvent, - position_map: &PositionMap, - split_side: Option, - window: &mut Window, - cx: &mut Context, - ) { - let text_hitbox = &position_map.text_hitbox; - let gutter_hitbox = &position_map.gutter_hitbox; - let modifiers = event.modifiers; - let text_hovered = text_hitbox.is_hovered(window); - let gutter_hovered = gutter_hitbox.is_hovered(window); - editor.set_gutter_hovered(gutter_hovered, cx); - - let point_for_position = position_map.point_for_position(event.position); - let valid_point = point_for_position.nearest_valid; - - // Update diff review drag state if we're dragging - if editor.diff_review_drag_state.is_some() { - editor.update_diff_review_drag(valid_point.row(), window, cx); - } - - let hovered_diff_control = position_map - .diff_hunk_control_bounds - .iter() - .find(|(_, bounds)| bounds.contains(&event.position)) - .map(|(row, _)| *row); - - let hovered_diff_hunk_row = if let Some(control_row) = hovered_diff_control { - Some(control_row) - } else if text_hovered { - let current_row = valid_point.row(); - position_map.display_hunks.iter().find_map(|(hunk, _)| { - if let DisplayDiffHunk::Unfolded { - display_row_range, .. - } = hunk - { - if display_row_range.contains(¤t_row) { - Some(display_row_range.start) - } else { - None - } - } else { - None - } - }) - } else { - None - }; - - if hovered_diff_hunk_row != editor.hovered_diff_hunk_row { - editor.hovered_diff_hunk_row = hovered_diff_hunk_row; - cx.notify(); - } - - if text_hovered - && let Some((bounds, buffer_id, blame_entry)) = &position_map.inline_blame_bounds - { - let mouse_over_inline_blame = bounds.contains(&event.position); - let mouse_over_popover = editor - .inline_blame_popover - .as_ref() - .and_then(|state| state.popover_bounds) - .is_some_and(|bounds| bounds.contains(&event.position)); - let keyboard_grace = editor - .inline_blame_popover - .as_ref() - .is_some_and(|state| state.keyboard_grace); - - if mouse_over_inline_blame || mouse_over_popover { - editor.show_blame_popover(*buffer_id, blame_entry, event.position, false, cx); - } else if !keyboard_grace { - editor.hide_blame_popover(false, cx); - } - } else { - let keyboard_grace = editor - .inline_blame_popover - .as_ref() - .is_some_and(|state| state.keyboard_grace); - if !keyboard_grace { - editor.hide_blame_popover(false, cx); - } - } - - // Handle diff review indicator when gutter is hovered in diff mode with AI enabled - let show_diff_review = editor.show_diff_review_button() - && cx.has_flag::() - && !DisableAiSettings::is_ai_disabled_for_buffer( - editor.buffer.read(cx).as_singleton().as_ref(), - cx, - ); - - let diff_review_indicator = if gutter_hovered && show_diff_review { - let is_visible = editor - .gutter_diff_review_indicator - .0 - .is_some_and(|indicator| indicator.is_active); - - if !is_visible { - editor - .gutter_diff_review_indicator - .1 - .get_or_insert_with(|| { - cx.spawn(async move |this, cx| { - cx.background_executor() - .timer(Duration::from_millis(200)) - .await; - - this.update(cx, |this, cx| { - if let Some(indicator) = - this.gutter_diff_review_indicator.0.as_mut() - { - indicator.is_active = true; - cx.notify(); - } - }) - .ok(); - }) - }); - } - - let anchor = position_map - .snapshot - .display_point_to_anchor(valid_point, Bias::Left); - Some(PhantomDiffReviewIndicator { - start: anchor, - end: anchor, - is_active: is_visible, - }) - } else { - editor.gutter_diff_review_indicator.1 = None; - None - }; - - if diff_review_indicator != editor.gutter_diff_review_indicator.0 { - editor.gutter_diff_review_indicator.0 = diff_review_indicator; - cx.notify(); - } - - // Don't show breakpoint indicator when diff review indicator is active on this row - let is_on_diff_review_button_row = diff_review_indicator.is_some_and(|indicator| { - let start_row = indicator - .start - .to_display_point(&position_map.snapshot.display_snapshot) - .row(); - indicator.is_active && start_row == valid_point.row() - }); - - let gutter_hover_button = if gutter_hovered - && !is_on_diff_review_button_row - && split_side != Some(SplitSide::Left) - { - let buffer_anchor = position_map - .snapshot - .display_point_to_anchor(valid_point, Bias::Left); - - if position_map - .snapshot - .buffer_snapshot() - .anchor_to_buffer_anchor(buffer_anchor) - .is_some() - { - let is_visible = editor - .gutter_hover_button - .0 - .is_some_and(|indicator| indicator.is_active); - - if !is_visible { - editor.gutter_hover_button.1.get_or_insert_with(|| { - cx.spawn(async move |this, cx| { - cx.background_executor() - .timer(Duration::from_millis(200)) - .await; - - this.update(cx, |this, cx| { - if let Some(indicator) = this.gutter_hover_button.0.as_mut() { - indicator.is_active = true; - cx.notify(); - } - }) - .ok(); - }) - }); - } - - Some(GutterHoverButton { - display_row: valid_point.row(), - is_active: is_visible, - }) - } else { - editor.gutter_hover_button.1 = None; - None - } - } else if editor.has_mouse_context_menu() { - editor.gutter_hover_button.1 = None; - editor.gutter_hover_button.0 - } else { - editor.gutter_hover_button.1 = None; - None - }; - - if &gutter_hover_button != &editor.gutter_hover_button.0 { - editor.gutter_hover_button.0 = gutter_hover_button; - cx.notify(); - } - - // Don't trigger hover popover if mouse is hovering over context menu - if text_hovered { - editor.update_hovered_link( - point_for_position, - Some(event.position), - &position_map.snapshot, - modifiers, - window, - cx, - ); - - if let Some(point) = point_for_position.as_valid() { - let anchor = position_map - .snapshot - .buffer_snapshot() - .anchor_before(point.to_offset(&position_map.snapshot, Bias::Left)); - hover_at(editor, Some(anchor), Some(event.position), window, cx); - Self::update_visible_cursor(editor, point, position_map, window, cx); - } else { - editor.update_inlay_link_and_hover_points( - &position_map.snapshot, - point_for_position, - Some(event.position), - modifiers.secondary(), - modifiers.shift, - window, - cx, - ); - } - } else { - editor.hide_hovered_link(cx); - hover_at(editor, None, Some(event.position), window, cx); - } - } - - fn update_visible_cursor( - editor: &mut Editor, - point: DisplayPoint, - position_map: &PositionMap, - window: &mut Window, - cx: &mut Context, - ) { - let snapshot = &position_map.snapshot; - let Some(hub) = editor.collaboration_hub() else { - return; - }; - let start = snapshot.display_snapshot.clip_point( - DisplayPoint::new(point.row(), point.column().saturating_sub(1)), - Bias::Left, - ); - let end = snapshot.display_snapshot.clip_point( - DisplayPoint::new( - point.row(), - (point.column() + 1).min(snapshot.line_len(point.row())), - ), - Bias::Right, - ); - - let range = snapshot - .buffer_snapshot() - .anchor_before(start.to_point(&snapshot.display_snapshot)) - ..snapshot - .buffer_snapshot() - .anchor_after(end.to_point(&snapshot.display_snapshot)); - - let Some(selection) = snapshot.remote_selections_in_range(&range, hub, cx).next() else { - return; - }; - let key = crate::HoveredCursor { - replica_id: selection.replica_id, - selection_id: selection.selection.id, - }; - editor.hovered_cursors.insert( - key.clone(), - cx.spawn_in(window, async move |editor, cx| { - cx.background_executor().timer(CURSORS_VISIBLE_FOR).await; - editor - .update(cx, |editor, cx| { - editor.hovered_cursors.remove(&key); - cx.notify(); - }) - .ok(); - }), - ); - cx.notify() - } - fn layout_selections( &self, start_anchor: Anchor, @@ -4047,14 +3201,15 @@ impl EditorElement { if self.should_show_buffer_headers() { let selected = selected_buffer_ids.contains(&first_excerpt.buffer_id()); - let jump_data = header_jump_data( + let jump_data = header::header_jump_data( snapshot, block_row_start, *height, first_excerpt, latest_selection_anchors, ); - result = result.child(self.render_buffer_header( + result = result.child(header::render_buffer_header( + &self.editor, first_excerpt, true, selected, @@ -4093,7 +3248,7 @@ impl EditorElement { let mut result = v_flex().id(block_id).w_full(); if self.should_show_buffer_headers() { - let jump_data = header_jump_data( + let jump_data = header::header_jump_data( snapshot, block_row_start, *height, @@ -4105,8 +3260,15 @@ impl EditorElement { let selected = selected_buffer_ids.contains(&excerpt.buffer_id()); result = result.child(div().pr(editor_margins.right).child( - self.render_buffer_header( - excerpt, false, selected, false, jump_data, window, cx, + header::render_buffer_header( + &self.editor, + excerpt, + false, + selected, + false, + jump_data, + window, + cx, ), )); } else { @@ -4261,28 +3423,6 @@ impl EditorElement { .into_any() } - fn render_buffer_header( - &self, - for_excerpt: &ExcerptBoundaryInfo, - is_folded: bool, - is_selected: bool, - is_sticky: bool, - jump_data: JumpData, - window: &mut Window, - cx: &mut App, - ) -> impl IntoElement { - render_buffer_header( - &self.editor, - for_excerpt, - is_folded, - is_selected, - is_sticky, - jump_data, - window, - cx, - ) - } - fn render_blocks( &self, rows: Range, @@ -4512,288 +3652,61 @@ impl EditorElement { spacer_blocks, row_block_types, resized_blocks: (!resized_blocks.is_empty()).then_some(resized_blocks), - } - } - - fn layout_blocks( - &self, - blocks: &mut Vec, - hitbox: &Hitbox, - gutter_hitbox: &Hitbox, - line_height: Pixels, - scroll_position: gpui::Point, - scroll_pixel_position: gpui::Point, - editor_margins: &EditorMargins, - window: &mut Window, - cx: &mut App, - ) { - for block in blocks { - let mut origin = if let Some(row) = block.row { - hitbox.origin - + point( - block.x_offset, - Pixels::from( - (row.as_f64() - scroll_position.y) - * ScrollPixelOffset::from(line_height), - ), - ) - } else { - // Position the block outside the visible area - hitbox.origin + point(Pixels::ZERO, hitbox.size.height) - }; - - if block.style == BlockStyle::Spacer { - origin += point( - gutter_hitbox.size.width + editor_margins.gutter.margin, - Pixels::ZERO, - ); - } - - if !matches!(block.style, BlockStyle::Sticky) { - origin += point(Pixels::from(-scroll_pixel_position.x), Pixels::ZERO); - } - - let focus_handle = - block - .element - .prepaint_as_root(origin, block.available_space, window, cx); - - if let Some(focus_handle) = focus_handle { - self.editor.update(cx, |editor, _cx| { - editor.set_focused_block(FocusedBlock { - id: block.id, - focus_handle: focus_handle.downgrade(), - }); - }); - } - } - } - - fn layout_sticky_buffer_header( - &self, - StickyHeaderExcerpt { excerpt }: StickyHeaderExcerpt<'_>, - scroll_position: gpui::Point, - line_height: Pixels, - right_margin: Pixels, - snapshot: &EditorSnapshot, - hitbox: &Hitbox, - selected_buffer_ids: &Vec, - blocks: &[BlockLayout], - latest_selection_anchors: &HashMap, - window: &mut Window, - cx: &mut App, - ) -> AnyElement { - let jump_data = header_jump_data( - snapshot, - DisplayRow(scroll_position.y as u32), - FILE_HEADER_HEIGHT + MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, - excerpt, - latest_selection_anchors, - ); - - let editor_bg_color = cx.theme().colors().editor_background; - - let selected = selected_buffer_ids.contains(&excerpt.buffer_id()); - - let available_width = hitbox.bounds.size.width - right_margin; - - let mut header = v_flex() - .w_full() - .relative() - .child( - div() - .w(available_width) - .h(FILE_HEADER_HEIGHT as f32 * line_height) - .bg(linear_gradient( - 0., - linear_color_stop(editor_bg_color.opacity(0.), 0.), - linear_color_stop(editor_bg_color, 0.6), - )) - .absolute() - .top_0(), - ) - .child( - self.render_buffer_header(excerpt, false, selected, true, jump_data, window, cx) - .into_any_element(), - ) - .into_any_element(); - - let mut origin = hitbox.origin; - // Move floating header up to avoid colliding with the next buffer header. - for block in blocks.iter() { - if !block.is_buffer_header { - continue; - } - - let Some(display_row) = block.row.filter(|row| row.0 > scroll_position.y as u32) else { - continue; - }; - - let max_row = display_row.0.saturating_sub(FILE_HEADER_HEIGHT); - let offset = scroll_position.y - max_row as f64; - - if offset > 0.0 { - origin.y -= Pixels::from(offset * ScrollPixelOffset::from(line_height)); - } - break; - } - - let size = size( - AvailableSpace::Definite(available_width), - AvailableSpace::MinContent, - ); - - header.prepaint_as_root(origin, size, window, cx); - - header - } - - fn layout_sticky_headers( - &self, - snapshot: &EditorSnapshot, - editor_width: Pixels, - is_row_soft_wrapped: impl Copy + Fn(usize) -> bool, - line_height: Pixels, - scroll_pixel_position: gpui::Point, - content_origin: gpui::Point, - gutter_dimensions: &GutterDimensions, - gutter_hitbox: &Hitbox, - text_hitbox: &Hitbox, - relative_line_numbers: RelativeLineNumbers, - relative_to: Option, - window: &mut Window, - cx: &mut App, - ) -> Option { - let show_line_numbers = snapshot - .show_line_numbers - .unwrap_or_else(|| EditorSettings::get_global(cx).gutter.line_numbers); - - let rows = Self::sticky_headers(self.editor.read(cx), snapshot); - - let mut lines = Vec::::new(); - - for StickyHeader { - sticky_row, - start_point, - offset, - } in rows.into_iter().rev() - { - let line = layout_line( - sticky_row, - snapshot, - &self.style, - editor_width, - is_row_soft_wrapped, - window, - cx, - ); - - let line_number = show_line_numbers.then(|| { - let start_display_row = start_point.to_display_point(snapshot).row(); - let relative_number = relative_to - .filter(|_| relative_line_numbers != RelativeLineNumbers::Disabled) - .map(|base| { - snapshot.relative_line_delta( - base, - start_display_row, - relative_line_numbers == RelativeLineNumbers::Wrapped, - ) - }); - let number = relative_number - .filter(|&delta| delta != 0) - .map(|delta| delta.unsigned_abs() as u32) - .unwrap_or(start_point.row + 1); - let color = cx.theme().colors().editor_line_number; - self.shape_line_number(SharedString::from(number.to_string()), color, window) - }); - - lines.push(StickyHeaderLine::new( - sticky_row, - line_height * offset as f32, - line, - line_number, - line_height, - scroll_pixel_position, - content_origin, - gutter_hitbox, - text_hitbox, - window, - cx, - )); - } - - lines.reverse(); - if lines.is_empty() { - return None; - } - - Some(StickyHeaders { - lines, - gutter_background: cx.theme().colors().editor_gutter_background, - content_background: self.style.background, - gutter_right_padding: gutter_dimensions.right_padding, - }) - } - - pub(crate) fn sticky_headers(editor: &Editor, snapshot: &EditorSnapshot) -> Vec { - let scroll_top = snapshot.scroll_position().y; - - let mut end_rows = Vec::::new(); - let mut rows = Vec::::new(); - - for item in editor.sticky_headers.iter().flatten() { - let start_point = item - .source_range_for_text - .start - .to_point(snapshot.buffer_snapshot()); - let end_point = item.range.end.to_point(snapshot.buffer_snapshot()); - - let sticky_row = snapshot - .display_snapshot - .point_to_display_point(start_point, Bias::Left) - .row(); - if rows - .last() - .is_some_and(|last| last.sticky_row == sticky_row) - { - continue; - } + } + } - let end_row = snapshot - .display_snapshot - .point_to_display_point(end_point, Bias::Left) - .row(); - let max_sticky_row = end_row.previous_row(); - if max_sticky_row <= sticky_row { - continue; - } + fn layout_blocks( + &self, + blocks: &mut Vec, + hitbox: &Hitbox, + gutter_hitbox: &Hitbox, + line_height: Pixels, + scroll_position: gpui::Point, + scroll_pixel_position: gpui::Point, + editor_margins: &EditorMargins, + window: &mut Window, + cx: &mut App, + ) { + for block in blocks { + let mut origin = if let Some(row) = block.row { + hitbox.origin + + point( + block.x_offset, + Pixels::from( + (row.as_f64() - scroll_position.y) + * ScrollPixelOffset::from(line_height), + ), + ) + } else { + // Position the block outside the visible area + hitbox.origin + point(Pixels::ZERO, hitbox.size.height) + }; - while end_rows - .last() - .is_some_and(|&last_end| last_end <= sticky_row) - { - end_rows.pop(); + if block.style == BlockStyle::Spacer { + origin += point( + gutter_hitbox.size.width + editor_margins.gutter.margin, + Pixels::ZERO, + ); } - let depth = end_rows.len(); - let adjusted_scroll_top = scroll_top + depth as f64; - if sticky_row.as_f64() >= adjusted_scroll_top || end_row.as_f64() <= adjusted_scroll_top - { - continue; + if !matches!(block.style, BlockStyle::Sticky) { + origin += point(Pixels::from(-scroll_pixel_position.x), Pixels::ZERO); } - let max_scroll_offset = max_sticky_row.as_f64() - scroll_top; - let offset = (depth as f64).min(max_scroll_offset); + let focus_handle = + block + .element + .prepaint_as_root(origin, block.available_space, window, cx); - end_rows.push(end_row); - rows.push(StickyHeader { - sticky_row, - start_point, - offset, - }); + if let Some(focus_handle) = focus_handle { + self.editor.update(cx, |editor, _cx| { + editor.set_focused_block(FocusedBlock { + id: block.id, + focus_handle: focus_handle.downgrade(), + }); + }); + } } - - rows } fn layout_cursor_popovers( @@ -5327,83 +4240,6 @@ impl EditorElement { } } - fn layout_mouse_context_menu( - &self, - editor_snapshot: &EditorSnapshot, - visible_range: Range, - content_origin: gpui::Point, - window: &mut Window, - cx: &mut App, - ) -> Option { - let position = self.editor.update(cx, |editor, cx| { - let visible_start_point = editor.display_to_pixel_point( - DisplayPoint::new(visible_range.start, 0), - editor_snapshot, - window, - cx, - )?; - let visible_end_point = editor.display_to_pixel_point( - DisplayPoint::new(visible_range.end, 0), - editor_snapshot, - window, - cx, - )?; - - let mouse_context_menu = editor.mouse_context_menu.as_ref()?; - let (source_display_point, position) = match mouse_context_menu.position { - MenuPosition::PinnedToScreen(point) => (None, point), - MenuPosition::PinnedToEditor { source, offset } => { - let source_display_point = source.to_display_point(editor_snapshot); - let source_point = - editor.to_pixel_point(source, editor_snapshot, window, cx)?; - let position = content_origin + source_point + offset; - (Some(source_display_point), position) - } - }; - - let source_included = source_display_point.is_none_or(|source_display_point| { - visible_range - .to_inclusive() - .contains(&source_display_point.row()) - }); - let position_included = - visible_start_point.y <= position.y && position.y <= visible_end_point.y; - if !source_included && !position_included { - None - } else { - Some(position) - } - })?; - - let text_style = TextStyleRefinement { - line_height: Some(DefiniteLength::Fraction( - BufferLineHeight::Comfortable.value(), - )), - ..Default::default() - }; - window.with_text_style(Some(text_style), |window| { - let mut element = self.editor.read_with(cx, |editor, _| { - let mouse_context_menu = editor.mouse_context_menu.as_ref()?; - let context_menu = mouse_context_menu.context_menu.clone(); - - Some( - deferred( - anchored() - .position(position) - .child(context_menu) - .anchor(gpui::Anchor::TopLeft) - .snap_to_window_with_margin(px(8.)), - ) - .with_priority(1) - .into_any(), - ) - })?; - - element.prepaint_as_root(position, AvailableSpace::min_size(), window, cx); - Some(element) - }) - } - fn layout_hover_popovers( &self, snapshot: &EditorSnapshot, @@ -6742,108 +5578,6 @@ impl EditorElement { } } - fn paint_sticky_headers( - &mut self, - layout: &mut EditorLayout, - window: &mut Window, - cx: &mut App, - ) { - let Some(mut sticky_headers) = layout.sticky_headers.take() else { - return; - }; - - if sticky_headers.lines.is_empty() { - layout.sticky_headers = Some(sticky_headers); - return; - } - - let whitespace_setting = self - .editor - .read(cx) - .buffer - .read(cx) - .language_settings(cx) - .show_whitespaces; - sticky_headers.paint(layout, whitespace_setting, window, cx); - - let sticky_header_hitboxes: Vec = sticky_headers - .lines - .iter() - .map(|line| line.hitbox.clone()) - .collect(); - let hovered_hitbox = sticky_header_hitboxes - .iter() - .find_map(|hitbox| hitbox.is_hovered(window).then_some(hitbox.id)); - - window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, _cx| { - if !phase.bubble() { - return; - } - - let current_hover = sticky_header_hitboxes - .iter() - .find_map(|hitbox| hitbox.is_hovered(window).then_some(hitbox.id)); - if hovered_hitbox != current_hover { - window.refresh(); - } - }); - - let position_map = layout.position_map.clone(); - - for (line_index, line) in sticky_headers.lines.iter().enumerate() { - let editor = self.editor.clone(); - let hitbox = line.hitbox.clone(); - let row = line.row; - let line_layout = line.line.clone(); - let position_map = position_map.clone(); - window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| { - if !phase.bubble() { - return; - } - - if event.button == MouseButton::Left && hitbox.is_hovered(window) { - let point_for_position = - position_map.point_for_position_on_line(event.position, row, &line_layout); - - editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(window, cx); - let anchor = snapshot - .display_snapshot - .display_point_to_anchor(point_for_position.nearest_valid, Bias::Left); - editor.change_selections( - SelectionEffects::scroll(Autoscroll::top_relative( - line_index as ScrollOffset, - )), - window, - cx, - |selections| { - selections.clear_disjoint(); - selections.set_pending_anchor_range( - anchor..anchor, - crate::SelectMode::Character, - ); - }, - ); - cx.stop_propagation(); - }); - } - }); - } - - let text_bounds = layout.position_map.text_hitbox.bounds; - let border_top = text_bounds.top() - + sticky_headers.lines.last().unwrap().offset - + layout.position_map.line_height; - let separator_height = px(1.); - let border_bounds = window.pixel_snap_bounds(Bounds::from_corners( - point(layout.gutter_hitbox.bounds.left(), border_top), - point(text_bounds.right(), border_top + separator_height), - )); - window.paint_quad(fill(border_bounds, cx.theme().colors().border_variant)); - - layout.sticky_headers = Some(sticky_headers); - } - fn paint_lines_background( &mut self, layout: &mut EditorLayout, @@ -7702,271 +6436,40 @@ impl EditorElement { layout: &mut EditorLayout, window: &mut Window, cx: &mut App, - ) { - for mut block in layout.blocks.drain(..) { - if block.overlaps_gutter { - block.element.paint(window, cx); - } else { - let mut bounds = layout.hitbox.bounds; - bounds.origin.x += layout.gutter_hitbox.bounds.size.width; - window.with_content_mask(Some(ContentMask { bounds }), |window| { - block.element.paint(window, cx); - }) - } - } - } - - fn paint_edit_prediction_popover( - &mut self, - layout: &mut EditorLayout, - window: &mut Window, - cx: &mut App, - ) { - if let Some(edit_prediction_popover) = layout.edit_prediction_popover.as_mut() { - edit_prediction_popover.paint(window, cx); - } - } - - fn paint_mouse_context_menu( - &mut self, - layout: &mut EditorLayout, - window: &mut Window, - cx: &mut App, - ) { - if let Some(mouse_context_menu) = layout.mouse_context_menu.as_mut() { - mouse_context_menu.paint(window, cx); - } - } - - fn paint_scroll_wheel_listener( - &mut self, - layout: &EditorLayout, - window: &mut Window, - cx: &mut App, - ) { - window.on_mouse_event({ - let position_map = layout.position_map.clone(); - let editor = self.editor.clone(); - let hitbox = layout.hitbox.clone(); - let mut delta = ScrollDelta::default(); - - // Set a minimum scroll_sensitivity of 0.01 to make sure the user doesn't - // accidentally turn off their scrolling. - let base_scroll_sensitivity = - EditorSettings::get_global(cx).scroll_sensitivity.max(0.01); - - // Use a minimum fast_scroll_sensitivity for same reason above - let fast_scroll_sensitivity = EditorSettings::get_global(cx) - .fast_scroll_sensitivity - .max(0.01); - - move |event: &ScrollWheelEvent, phase, window, cx| { - if phase == DispatchPhase::Bubble && hitbox.should_handle_scroll(window) { - delta = delta.coalesce(event.delta); - - if event.modifiers.secondary() - && editor.read(cx).enable_mouse_wheel_zoom - && EditorSettings::get_global(cx).mouse_wheel_zoom - { - let delta_y = match event.delta { - ScrollDelta::Pixels(pixels) => pixels.y.into(), - ScrollDelta::Lines(lines) => lines.y, - }; - - if delta_y > 0.0 { - theme_settings::increase_buffer_font_size(cx); - } else if delta_y < 0.0 { - theme_settings::decrease_buffer_font_size(cx); - } - - cx.stop_propagation(); - } else { - let scroll_sensitivity = { - if event.modifiers.alt { - fast_scroll_sensitivity - } else { - base_scroll_sensitivity - } - }; - - editor.update(cx, |editor, cx| { - let line_height = position_map.line_height; - let glyph_width = position_map.em_layout_width; - let (delta, axis) = match delta { - gpui::ScrollDelta::Pixels(mut pixels) => { - //Trackpad - let axis = - position_map.snapshot.ongoing_scroll.filter(&mut pixels); - (pixels, axis) - } - - gpui::ScrollDelta::Lines(lines) => { - //Not trackpad - let pixels = - point(lines.x * glyph_width, lines.y * line_height); - (pixels, None) - } - }; - - let current_scroll_position = position_map.snapshot.scroll_position(); - let x = (current_scroll_position.x - * ScrollPixelOffset::from(glyph_width) - - ScrollPixelOffset::from(delta.x * scroll_sensitivity)) - / ScrollPixelOffset::from(glyph_width); - let y = (current_scroll_position.y - * ScrollPixelOffset::from(line_height) - - ScrollPixelOffset::from(delta.y * scroll_sensitivity)) - / ScrollPixelOffset::from(line_height); - let mut scroll_position = - point(x, y).clamp(&point(0., 0.), &position_map.scroll_max); - let forbid_vertical_scroll = - editor.scroll_manager.forbid_vertical_scroll(); - if forbid_vertical_scroll { - scroll_position.y = current_scroll_position.y; - } - - if scroll_position != current_scroll_position { - editor.scroll(scroll_position, axis, window, cx); - cx.stop_propagation(); - } else if y < 0. { - // Due to clamping, we may fail to detect cases of overscroll to the top; - // We want the scroll manager to get an update in such cases and detect the change of direction - // on the next frame. - cx.notify(); - } - }); - } - } - } - }); - } - - fn paint_mouse_listeners(&mut self, layout: &EditorLayout, window: &mut Window, cx: &mut App) { - if layout.mode.is_minimap() { - return; - } - - self.paint_scroll_wheel_listener(layout, window, cx); - - window.on_mouse_event({ - let position_map = layout.position_map.clone(); - let editor = self.editor.clone(); - let line_numbers = layout.line_numbers.clone(); - - move |event: &MouseDownEvent, phase, window, cx| { - if phase == DispatchPhase::Bubble { - match event.button { - MouseButton::Left => editor.update(cx, |editor, cx| { - let pending_mouse_down = editor - .pending_mouse_down - .get_or_insert_with(Default::default) - .clone(); - - *pending_mouse_down.borrow_mut() = Some(event.clone()); - - Self::mouse_left_down( - editor, - event, - &position_map, - line_numbers.as_ref(), - window, - cx, - ); - }), - MouseButton::Right => editor.update(cx, |editor, cx| { - Self::mouse_right_down(editor, event, &position_map, window, cx); - }), - MouseButton::Middle => editor.update(cx, |editor, cx| { - Self::mouse_middle_down(editor, event, &position_map, window, cx); - }), - _ => {} - }; - } - } - }); - - window.on_mouse_event({ - let editor = self.editor.clone(); - let position_map = layout.position_map.clone(); - - move |event: &MouseUpEvent, phase, window, cx| { - if phase == DispatchPhase::Bubble { - editor.update(cx, |editor, cx| { - Self::mouse_up(editor, event, &position_map, window, cx) - }); - } - } - }); - - window.on_mouse_event({ - let editor = self.editor.clone(); - let position_map = layout.position_map.clone(); - let mut captured_mouse_down = None; - - move |event: &MouseUpEvent, phase, window, cx| match phase { - // Clear the pending mouse down during the capture phase, - // so that it happens even if another event handler stops - // propagation. - DispatchPhase::Capture => editor.update(cx, |editor, _cx| { - let pending_mouse_down = editor - .pending_mouse_down - .get_or_insert_with(Default::default) - .clone(); - - let mut pending_mouse_down = pending_mouse_down.borrow_mut(); - if pending_mouse_down.is_some() && position_map.text_hitbox.is_hovered(window) { - captured_mouse_down = pending_mouse_down.take(); - window.refresh(); - } - }), - // Fire click handlers during the bubble phase. - DispatchPhase::Bubble => editor.update(cx, |editor, cx| { - if let Some(mouse_down) = captured_mouse_down.take() { - let event = ClickEvent::Mouse(MouseClickEvent { - down: mouse_down, - up: event.clone(), - }); - Self::click(editor, &event, &position_map, window, cx); - } - }), - } - }); - - window.on_mouse_event({ - let position_map = layout.position_map.clone(); - let editor = self.editor.clone(); - - move |event: &MousePressureEvent, phase, window, cx| { - if phase == DispatchPhase::Bubble { - editor.update(cx, |editor, cx| { - Self::pressure_click(editor, &event, &position_map, window, cx); - }) - } + ) { + for mut block in layout.blocks.drain(..) { + if block.overlaps_gutter { + block.element.paint(window, cx); + } else { + let mut bounds = layout.hitbox.bounds; + bounds.origin.x += layout.gutter_hitbox.bounds.size.width; + window.with_content_mask(Some(ContentMask { bounds }), |window| { + block.element.paint(window, cx); + }) } - }); - - window.on_mouse_event({ - let position_map = layout.position_map.clone(); - let editor = self.editor.clone(); - let split_side = self.split_side; + } + } - move |event: &MouseMoveEvent, phase, window, cx| { - if phase == DispatchPhase::Bubble { - editor.update(cx, |editor, cx| { - if editor.hover_state.focused(window, cx) { - return; - } - if event.pressed_button == Some(MouseButton::Left) - || event.pressed_button == Some(MouseButton::Middle) - { - Self::mouse_dragged(editor, event, &position_map, window, cx) - } + fn paint_edit_prediction_popover( + &mut self, + layout: &mut EditorLayout, + window: &mut Window, + cx: &mut App, + ) { + if let Some(edit_prediction_popover) = layout.edit_prediction_popover.as_mut() { + edit_prediction_popover.paint(window, cx); + } + } - Self::mouse_moved(editor, event, &position_map, split_side, window, cx) - }); - } - } - }); + fn paint_mouse_context_menu( + &mut self, + layout: &mut EditorLayout, + window: &mut Window, + cx: &mut App, + ) { + if let Some(mouse_context_menu) = layout.mouse_context_menu.as_mut() { + mouse_context_menu.paint(window, cx); + } } fn shape_line_number( @@ -8341,511 +6844,6 @@ fn apply_dirty_filename_style( ) } -fn file_status_label_color(file_status: Option) -> Color { - file_status.map_or(Color::Default, |status| { - if status.is_conflicted() { - Color::Conflict - } else if status.is_modified() { - Color::Modified - } else if status.is_deleted() { - Color::Disabled - } else if status.is_created() { - Color::Created - } else { - Color::Default - } - }) -} - -pub(crate) fn header_jump_data( - editor_snapshot: &EditorSnapshot, - block_row_start: DisplayRow, - height: u32, - first_excerpt: &ExcerptBoundaryInfo, - latest_selection_anchors: &HashMap, -) -> JumpData { - let multibuffer_snapshot = editor_snapshot.buffer_snapshot(); - let buffer = first_excerpt.buffer(multibuffer_snapshot); - let (jump_anchor, jump_buffer, excerpt_start) = if let Some(anchor) = - latest_selection_anchors.get(&first_excerpt.buffer_id()) - && let Some((jump_anchor, selection_buffer)) = - multibuffer_snapshot.anchor_to_buffer_anchor(*anchor) - { - let jump_offset = text::ToOffset::to_offset(&jump_anchor, selection_buffer); - let selection_excerpt_start = multibuffer_snapshot - .excerpts_for_buffer(jump_anchor.buffer_id) - .find(|excerpt| { - let start = text::ToOffset::to_offset(&excerpt.context.start, selection_buffer); - let end = text::ToOffset::to_offset(&excerpt.context.end, selection_buffer); - start <= jump_offset && jump_offset <= end - }) - .map(|excerpt| excerpt.context.start) - .unwrap_or(first_excerpt.range.context.start); - (jump_anchor, selection_buffer, selection_excerpt_start) - } else { - ( - first_excerpt.range.primary.start, - buffer, - first_excerpt.range.context.start, - ) - }; - let jump_position = language::ToPoint::to_point(&jump_anchor, jump_buffer); - let rows_from_excerpt_start = if jump_anchor == excerpt_start { - 0 - } else { - let excerpt_start_point = language::ToPoint::to_point(&excerpt_start, jump_buffer); - jump_position.row.saturating_sub(excerpt_start_point.row) - }; - - let line_offset_from_top = (block_row_start.0 + height + rows_from_excerpt_start) - .saturating_sub( - editor_snapshot - .scroll_anchor - .scroll_position(&editor_snapshot.display_snapshot) - .y as u32, - ); - - JumpData::MultiBufferPoint { - anchor: jump_anchor, - position: jump_position, - line_offset_from_top, - } -} - -pub(crate) fn render_buffer_header( - editor: &Entity, - for_excerpt: &ExcerptBoundaryInfo, - is_folded: bool, - is_selected: bool, - is_sticky: bool, - jump_data: JumpData, - window: &mut Window, - cx: &mut App, -) -> impl IntoElement { - let editor_read = editor.read(cx); - let multi_buffer = editor_read.buffer.read(cx); - let is_read_only = editor_read.read_only(cx); - let editor_handle: &dyn ItemHandle = editor; - let multibuffer_snapshot = multi_buffer.snapshot(cx); - let buffer = for_excerpt.buffer(&multibuffer_snapshot); - - let breadcrumbs = if is_selected { - editor_read.breadcrumbs_inner(cx) - } else { - None - }; - - let buffer_id = for_excerpt.buffer_id(); - let file_status = multi_buffer - .all_diff_hunks_expanded() - .then(|| editor_read.status_for_buffer_id(buffer_id, cx)) - .flatten(); - let indicator = multi_buffer.buffer(buffer_id).and_then(|buffer| { - let buffer = buffer.read(cx); - let indicator_color = match (buffer.has_conflict(), buffer.is_dirty()) { - (true, _) => Some(Color::Warning), - (_, true) => Some(Color::Accent), - (false, false) => None, - }; - indicator_color.map(|indicator_color| Indicator::dot().color(indicator_color)) - }); - - let include_root = editor_read - .project - .as_ref() - .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) - .unwrap_or_default(); - let file = buffer.file(); - let can_open_excerpts = file.is_none_or(|file| file.can_open()); - let path_style = file.map(|file| file.path_style(cx)); - let relative_path = buffer.resolve_file_path(include_root, cx); - let (parent_path, filename) = if let Some(path) = &relative_path { - if let Some(path_style) = path_style { - let (dir, file_name) = path_style.split(path); - (dir.map(|dir| dir.to_owned()), Some(file_name.to_owned())) - } else { - (None, Some(path.clone())) - } - } else { - (None, None) - }; - let focus_handle = editor_read.focus_handle(cx); - let colors = cx.theme().colors(); - - let header = div() - .id(("buffer-header", buffer_id.to_proto())) - .p(BUFFER_HEADER_PADDING) - .w_full() - .h(FILE_HEADER_HEIGHT as f32 * window.line_height()) - .child( - h_flex() - .group("buffer-header-group") - .size_full() - .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) - .pl_1() - .pr_2() - .rounded_sm() - .gap_1p5() - .when(is_sticky, |el| el.shadow_md()) - .border_1() - .map(|border| { - let border_color = - if is_selected && is_folded && focus_handle.contains_focused(window, cx) { - colors.border_focused - } else { - colors.border - }; - border.border_color(border_color) - }) - .bg(colors.editor_subheader_background) - .hover(|style| style.bg(colors.element_hover)) - .map(|header| { - let editor = editor.clone(); - let buffer_id = for_excerpt.buffer_id(); - let toggle_chevron_icon = - FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path); - let button_size = rems_from_px(28.); - - header.child( - div() - .hover(|style| style.bg(colors.element_selected)) - .rounded_xs() - .child( - ButtonLike::new("toggle-buffer-fold") - .style(ButtonStyle::Transparent) - .height(button_size.into()) - .width(button_size) - .children(toggle_chevron_icon) - .tooltip({ - let focus_handle = focus_handle.clone(); - let is_folded_for_tooltip = is_folded; - move |_window, cx| { - Tooltip::with_meta_in( - if is_folded_for_tooltip { - "Unfold Excerpt" - } else { - "Fold Excerpt" - }, - Some(&ToggleFold), - format!( - "{} to toggle all", - text_for_keystroke( - &Modifiers::alt(), - "click", - cx - ) - ), - &focus_handle, - cx, - ) - } - }) - .on_click(move |event, window, cx| { - if event.modifiers().alt { - editor.update(cx, |editor, cx| { - editor.toggle_fold_all(&ToggleFoldAll, window, cx); - }); - } else { - if is_folded { - editor.update(cx, |editor, cx| { - editor.unfold_buffer(buffer_id, cx); - }); - } else { - editor.update(cx, |editor, cx| { - editor.fold_buffer(buffer_id, cx); - }); - } - } - }), - ), - ) - }) - .children( - editor_read - .addons - .values() - .filter_map(|addon| { - addon.render_buffer_header_controls(for_excerpt, buffer, window, cx) - }) - .take(1), - ) - .when(!is_read_only, |this| { - this.child( - h_flex() - .size_3() - .justify_center() - .flex_shrink_0() - .children(indicator), - ) - }) - .child( - h_flex() - .cursor_pointer() - .id("path_header_block") - .min_w_0() - .size_full() - .gap_1() - .justify_between() - .overflow_hidden() - .child(h_flex().min_w_0().flex_1().gap_0p5().overflow_hidden().map( - |path_header| { - let filename = filename - .map(SharedString::from) - .unwrap_or_else(|| "untitled".into()); - - let full_path = match parent_path.as_deref() { - Some(parent) if !parent.is_empty() => { - format!("{}{}", parent, filename.as_str()) - } - _ => filename.as_str().to_string(), - }; - - path_header - .child( - ButtonLike::new("filename-button") - .when(ItemSettings::get_global(cx).file_icons, |this| { - let path = path::Path::new(filename.as_str()); - let icon = FileIcons::get_icon(path, cx) - .unwrap_or_default(); - - this.child( - Icon::from_path(icon).color(Color::Muted), - ) - }) - .child( - Label::new(filename) - .single_line() - .color(file_status_label_color(file_status)) - .buffer_font(cx) - .when( - file_status.is_some_and(|s| s.is_deleted()), - |label| label.strikethrough(), - ), - ) - .tooltip(move |_, cx| { - Tooltip::with_meta( - "Open File", - None, - full_path.clone(), - cx, - ) - }) - .on_click(window.listener_for(editor, { - let jump_data = jump_data.clone(); - move |editor, e: &ClickEvent, window, cx| { - editor.open_excerpts_common( - Some(jump_data.clone()), - e.modifiers().secondary(), - window, - cx, - ); - } - })), - ) - .when_some(parent_path, |then, path| { - then.child( - Label::new(path) - .buffer_font(cx) - .truncate_start() - .color( - if file_status - .is_some_and(FileStatus::is_deleted) - { - Color::Custom(colors.text_disabled) - } else { - Color::Custom(colors.text_muted) - }, - ), - ) - }) - .when(!buffer.capability.editable(), |el| { - el.child(Icon::new(IconName::FileLock).color(Color::Muted)) - }) - .when_some(breadcrumbs, |then, breadcrumbs| { - let font = theme_settings::ThemeSettings::get_global(cx) - .buffer_font - .clone(); - then.child(render_breadcrumb_text( - breadcrumbs, - Some(font), - None, - editor_handle, - true, - window, - cx, - )) - }) - }, - )) - .when(can_open_excerpts && relative_path.is_some(), |this| { - this.child( - div() - .when(!is_selected, |this| { - this.visible_on_hover("buffer-header-group") - }) - .child( - Button::new("open-file-button", "Open File") - .style(ButtonStyle::OutlinedGhost) - .when(is_selected, |this| { - this.key_binding(KeyBinding::for_action_in( - &OpenExcerpts, - &focus_handle, - cx, - )) - }) - .on_click(window.listener_for(editor, { - let jump_data = jump_data.clone(); - move |editor, e: &ClickEvent, window, cx| { - editor.open_excerpts_common( - Some(jump_data.clone()), - e.modifiers().secondary(), - window, - cx, - ); - } - })), - ), - ) - }) - .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .on_click(window.listener_for(editor, { - let buffer_id = for_excerpt.buffer_id(); - move |editor, e: &ClickEvent, window, cx| { - if e.modifiers().alt { - editor.open_excerpts_common( - Some(jump_data.clone()), - e.modifiers().secondary(), - window, - cx, - ); - return; - } - - if is_folded { - editor.unfold_buffer(buffer_id, cx); - } else { - editor.fold_buffer(buffer_id, cx); - } - } - })), - ), - ); - - let file = buffer.file().cloned(); - let editor = editor.clone(); - let buffer_snapshot = buffer.clone(); - - right_click_menu("buffer-header-context-menu") - .trigger(move |_, _, _| header) - .menu(move |window, cx| { - let menu_context = focus_handle.clone(); - let editor = editor.clone(); - let file = file.clone(); - let buffer_snapshot = buffer_snapshot.clone(); - ContextMenu::build(window, cx, move |mut menu, window, cx| { - if let Some(file) = file - && let Some(project) = editor.read(cx).project() - && let Some(worktree) = - project.read(cx).worktree_for_id(file.worktree_id(cx), cx) - { - let path_style = file.path_style(cx); - let worktree = worktree.read(cx); - let relative_path = file.path(); - let entry_for_path = worktree.entry_for_path(relative_path); - let abs_path = entry_for_path.map(|e| { - e.canonical_path - .as_deref() - .map_or_else(|| worktree.absolutize(relative_path), Path::to_path_buf) - }); - let has_relative_path = worktree.root_entry().is_some_and(Entry::is_dir); - - let parent_abs_path = abs_path - .as_ref() - .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf())); - let relative_path = has_relative_path - .then_some(relative_path) - .map(ToOwned::to_owned); - - let visible_in_project_panel = relative_path.is_some() && worktree.is_visible(); - let reveal_in_project_panel = entry_for_path - .filter(|_| visible_in_project_panel) - .map(|entry| entry.id); - menu = menu - .when_some(abs_path, |menu, abs_path| { - menu.entry( - "Copy Path", - Some(Box::new(zed_actions::workspace::CopyPath)), - window.handler_for(&editor, move |_, _, cx| { - cx.write_to_clipboard(ClipboardItem::new_string( - abs_path.to_string_lossy().into_owned(), - )); - }), - ) - }) - .when_some(relative_path, |menu, relative_path| { - menu.entry( - "Copy Relative Path", - Some(Box::new(zed_actions::workspace::CopyRelativePath)), - window.handler_for(&editor, move |_, _, cx| { - cx.write_to_clipboard(ClipboardItem::new_string( - relative_path.display(path_style).to_string(), - )); - }), - ) - }) - .when( - reveal_in_project_panel.is_some() || parent_abs_path.is_some(), - |menu| menu.separator(), - ) - .when_some(reveal_in_project_panel, |menu, entry_id| { - menu.entry( - "Reveal In Project Panel", - Some(Box::new(RevealInProjectPanel::default())), - window.handler_for(&editor, move |editor, _, cx| { - if let Some(project) = &mut editor.project { - project.update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel(entry_id)) - }); - } - }), - ) - }) - .when_some(parent_abs_path, |menu, parent_abs_path| { - menu.entry( - "Open in Terminal", - Some(Box::new(OpenInTerminal)), - window.handler_for(&editor, move |_, window, cx| { - window.dispatch_action( - OpenTerminal { - working_directory: parent_abs_path.clone(), - local: false, - } - .boxed_clone(), - cx, - ); - }), - ) - }); - } - - menu = editor.update(cx, |editor, cx| { - let mut menu = menu; - for addon in editor.addons.values() { - menu = addon.extend_buffer_header_context_menu( - menu, - &buffer_snapshot, - window, - cx, - ); - } - menu - }); - - menu.context(menu_context) - }) - }) -} - fn render_inline_blame_entry( blame_entry: BlameEntry, style: &EditorStyle, @@ -11529,181 +9527,18 @@ pub struct EditorLayout { tab_invisible: ShapedLine, space_invisible: ShapedLine, sticky_buffer_header: Option, - sticky_headers: Option, + sticky_headers: Option, document_colors: Option<(DocumentColorsRenderMode, Vec<(Range, Hsla)>)>, text_align: TextAlign, content_width: Pixels, } -struct StickyHeaders { - lines: Vec, - gutter_background: Hsla, - content_background: Hsla, - gutter_right_padding: Pixels, -} - -struct StickyHeaderLine { - row: DisplayRow, - offset: Pixels, - line: Rc, - line_number: Option, - elements: SmallVec<[AnyElement; 1]>, - available_text_width: Pixels, - hitbox: Hitbox, -} - impl EditorLayout { fn line_end_overshoot(&self) -> Pixels { 0.15 * self.position_map.line_height } } -impl StickyHeaders { - fn paint( - &mut self, - layout: &mut EditorLayout, - whitespace_setting: ShowWhitespaceSetting, - window: &mut Window, - cx: &mut App, - ) { - let line_height = layout.position_map.line_height; - - for line in self.lines.iter_mut().rev() { - window.paint_layer( - Bounds::new( - layout.gutter_hitbox.origin + point(Pixels::ZERO, line.offset), - size(line.hitbox.size.width, line_height), - ), - |window| { - let gutter_bounds = Bounds::new( - layout.gutter_hitbox.origin + point(Pixels::ZERO, line.offset), - size(layout.gutter_hitbox.size.width, line_height), - ); - window.paint_quad(fill(gutter_bounds, self.gutter_background)); - - let text_bounds = Bounds::new( - layout.position_map.text_hitbox.origin + point(Pixels::ZERO, line.offset), - size(line.available_text_width, line_height), - ); - window.paint_quad(fill(text_bounds, self.content_background)); - - if line.hitbox.is_hovered(window) { - let hover_overlay = cx.theme().colors().panel_overlay_hover; - window.paint_quad(fill(gutter_bounds, hover_overlay)); - window.paint_quad(fill(text_bounds, hover_overlay)); - } - - line.paint( - layout, - self.gutter_right_padding, - line.available_text_width, - layout.content_origin, - line_height, - whitespace_setting, - window, - cx, - ); - }, - ); - - window.set_cursor_style(CursorStyle::IBeam, &line.hitbox); - } - } -} - -impl StickyHeaderLine { - fn new( - row: DisplayRow, - offset: Pixels, - mut line: LineWithInvisibles, - line_number: Option, - line_height: Pixels, - scroll_pixel_position: gpui::Point, - content_origin: gpui::Point, - gutter_hitbox: &Hitbox, - text_hitbox: &Hitbox, - window: &mut Window, - cx: &mut App, - ) -> Self { - let mut elements = SmallVec::<[AnyElement; 1]>::new(); - line.prepaint_with_custom_offset( - line_height, - scroll_pixel_position, - content_origin, - offset, - &mut elements, - window, - cx, - ); - - let hitbox_bounds = Bounds::new( - gutter_hitbox.origin + point(Pixels::ZERO, offset), - size(text_hitbox.right() - gutter_hitbox.left(), line_height), - ); - let available_text_width = - (hitbox_bounds.size.width - gutter_hitbox.size.width).max(Pixels::ZERO); - - Self { - row, - offset, - line: Rc::new(line), - line_number, - elements, - available_text_width, - hitbox: window.insert_hitbox(hitbox_bounds, HitboxBehavior::BlockMouseExceptScroll), - } - } - - fn paint( - &mut self, - layout: &EditorLayout, - gutter_right_padding: Pixels, - available_text_width: Pixels, - content_origin: gpui::Point, - line_height: Pixels, - whitespace_setting: ShowWhitespaceSetting, - window: &mut Window, - cx: &mut App, - ) { - window.with_content_mask( - Some(ContentMask { - bounds: Bounds::new( - layout.position_map.text_hitbox.bounds.origin - + point(Pixels::ZERO, self.offset), - size(available_text_width, line_height), - ), - }), - |window| { - self.line.draw_with_custom_offset( - layout, - self.row, - content_origin, - self.offset, - whitespace_setting, - &[], - window, - cx, - ); - for element in &mut self.elements { - element.paint(window, cx); - } - }, - ); - - if let Some(line_number) = &self.line_number { - let gutter_origin = layout.gutter_hitbox.origin + point(Pixels::ZERO, self.offset); - let gutter_width = layout.gutter_hitbox.size.width; - let origin = point( - gutter_origin.x + gutter_width - gutter_right_padding - line_number.width, - gutter_origin.y, - ); - line_number - .paint(origin, line_height, TextAlign::Left, None, window, cx) - .log_err(); - } - } -} - #[derive(Debug)] struct LineNumberSegment { shaped_line: ShapedLine, @@ -12653,25 +10488,11 @@ impl HighlightedRange { } } -pub(crate) struct StickyHeader { - pub sticky_row: DisplayRow, - pub start_point: Point, - pub offset: ScrollOffset, -} - enum CursorPopoverType { CodeContextMenu, EditPrediction, } -pub fn scale_vertical_mouse_autoscroll_delta(delta: Pixels) -> f32 { - (delta.pow(1.2) / 100.0).min(px(3.0)).into() -} - -fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 { - (delta.pow(1.2) / 300.0).into() -} - pub fn register_action( editor: &Entity, window: &mut Window, diff --git a/crates/editor/src/element/header.rs b/crates/editor/src/element/header.rs new file mode 100644 index 00000000000000..02dfebdfc313f1 --- /dev/null +++ b/crates/editor/src/element/header.rs @@ -0,0 +1,1055 @@ +use std::path::Path; +use std::rc::Rc; + +use collections::HashMap; +use file_icons::FileIcons; +use git::status::FileStatus; +use gpui::{ + Action, AnyElement, App, AvailableSpace, Bounds, ClickEvent, ClipboardItem, ContentMask, + CursorStyle, DefiniteLength, Entity, Focusable as _, Hitbox, HitboxBehavior, Hsla, IntoElement, + Length, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Pixels, + ShapedLine, SharedString, Styled, TextAlign, Window, div, fill, linear_color_stop, + linear_gradient, point, px, size, +}; +use language::language_settings::ShowWhitespaceSetting; +use multi_buffer::{Anchor, ExcerptBoundaryInfo}; +use project::Entry; +use settings::{RelativeLineNumbers, Settings}; +use smallvec::SmallVec; +use sum_tree::Bias; +use text::BufferId; +use theme::ActiveTheme; +use ui::{ + ButtonLike, ContextMenu, Indicator, KeyBinding, Tooltip, prelude::*, right_click_menu, + text_for_keystroke, +}; +use util::ResultExt; +use workspace::{ItemHandle, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel}; + +use super::{ + BlockLayout, EditorElement, EditorLayout, LineWithInvisibles, layout_line, + render_breadcrumb_text, +}; +use crate::{ + BUFFER_HEADER_PADDING, DisplayRow, Editor, EditorSettings, EditorSnapshot, FILE_HEADER_HEIGHT, + GutterDimensions, JumpData, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, Point, RowExt, + SelectionEffects, StickyHeaderExcerpt, ToPoint, ToggleFold, ToggleFoldAll, + display_map::ToDisplayPoint, + scroll::{Autoscroll, ScrollOffset, ScrollPixelOffset}, +}; + +pub(crate) struct StickyHeader { + sticky_row: DisplayRow, + pub(crate) start_point: Point, + pub(crate) offset: ScrollOffset, +} + +pub(super) struct StickyHeaders { + pub(super) lines: Vec, + gutter_background: Hsla, + content_background: Hsla, + gutter_right_padding: Pixels, +} + +pub(super) struct StickyHeaderLine { + row: DisplayRow, + pub(super) offset: Pixels, + line: Rc, + line_number: Option, + elements: SmallVec<[AnyElement; 1]>, + available_text_width: Pixels, + hitbox: Hitbox, +} + +impl EditorElement { + pub(crate) fn sticky_headers(editor: &Editor, snapshot: &EditorSnapshot) -> Vec { + let scroll_top = snapshot.scroll_position().y; + + let mut end_rows = Vec::::new(); + let mut rows = Vec::::new(); + + for item in editor.sticky_headers.iter().flatten() { + let start_point = item + .source_range_for_text + .start + .to_point(snapshot.buffer_snapshot()); + let end_point = item.range.end.to_point(snapshot.buffer_snapshot()); + + let sticky_row = snapshot + .display_snapshot + .point_to_display_point(start_point, Bias::Left) + .row(); + if rows + .last() + .is_some_and(|last| last.sticky_row == sticky_row) + { + continue; + } + + let end_row = snapshot + .display_snapshot + .point_to_display_point(end_point, Bias::Left) + .row(); + let max_sticky_row = end_row.previous_row(); + if max_sticky_row <= sticky_row { + continue; + } + + while end_rows + .last() + .is_some_and(|&last_end| last_end <= sticky_row) + { + end_rows.pop(); + } + let depth = end_rows.len(); + let adjusted_scroll_top = scroll_top + depth as f64; + + if sticky_row.as_f64() >= adjusted_scroll_top || end_row.as_f64() <= adjusted_scroll_top + { + continue; + } + + let max_scroll_offset = max_sticky_row.as_f64() - scroll_top; + let offset = (depth as f64).min(max_scroll_offset); + + end_rows.push(end_row); + rows.push(StickyHeader { + sticky_row, + start_point, + offset, + }); + } + + rows + } + + pub(super) fn should_show_buffer_headers(&self) -> bool { + self.split_side.is_none() + } + + pub(super) fn layout_sticky_buffer_header( + &self, + StickyHeaderExcerpt { excerpt }: StickyHeaderExcerpt<'_>, + scroll_position: gpui::Point, + line_height: Pixels, + right_margin: Pixels, + snapshot: &EditorSnapshot, + hitbox: &Hitbox, + selected_buffer_ids: &Vec, + blocks: &[BlockLayout], + latest_selection_anchors: &HashMap, + window: &mut Window, + cx: &mut App, + ) -> AnyElement { + let jump_data = header_jump_data( + snapshot, + DisplayRow(scroll_position.y as u32), + FILE_HEADER_HEIGHT + MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, + excerpt, + latest_selection_anchors, + ); + + let editor_bg_color = cx.theme().colors().editor_background; + + let selected = selected_buffer_ids.contains(&excerpt.buffer_id()); + + let available_width = hitbox.bounds.size.width - right_margin; + + let mut header = v_flex() + .w_full() + .relative() + .child( + div() + .w(available_width) + .h(FILE_HEADER_HEIGHT as f32 * line_height) + .bg(linear_gradient( + 0., + linear_color_stop(editor_bg_color.opacity(0.), 0.), + linear_color_stop(editor_bg_color, 0.6), + )) + .absolute() + .top_0(), + ) + .child( + render_buffer_header( + &self.editor, + excerpt, + false, + selected, + true, + jump_data, + window, + cx, + ) + .into_any_element(), + ) + .into_any_element(); + + let mut origin = hitbox.origin; + // Move floating header up to avoid colliding with the next buffer header. + for block in blocks.iter() { + if !block.is_buffer_header { + continue; + } + + let Some(display_row) = block.row.filter(|row| row.0 > scroll_position.y as u32) else { + continue; + }; + + let max_row = display_row.0.saturating_sub(FILE_HEADER_HEIGHT); + let offset = scroll_position.y - max_row as f64; + + if offset > 0.0 { + origin.y -= Pixels::from(offset * ScrollPixelOffset::from(line_height)); + } + break; + } + + let size = size( + AvailableSpace::Definite(available_width), + AvailableSpace::MinContent, + ); + + header.prepaint_as_root(origin, size, window, cx); + + header + } + + pub(super) fn layout_sticky_headers( + &self, + snapshot: &EditorSnapshot, + editor_width: Pixels, + is_row_soft_wrapped: impl Copy + Fn(usize) -> bool, + line_height: Pixels, + scroll_pixel_position: gpui::Point, + content_origin: gpui::Point, + gutter_dimensions: &GutterDimensions, + gutter_hitbox: &Hitbox, + text_hitbox: &Hitbox, + relative_line_numbers: RelativeLineNumbers, + relative_to: Option, + window: &mut Window, + cx: &mut App, + ) -> Option { + let show_line_numbers = snapshot + .show_line_numbers + .unwrap_or_else(|| EditorSettings::get_global(cx).gutter.line_numbers); + + let rows = Self::sticky_headers(self.editor.read(cx), snapshot); + + let mut lines = Vec::::new(); + + for StickyHeader { + sticky_row, + start_point, + offset, + } in rows.into_iter().rev() + { + let line = layout_line( + sticky_row, + snapshot, + &self.style, + editor_width, + is_row_soft_wrapped, + window, + cx, + ); + + let line_number = show_line_numbers.then(|| { + let start_display_row = start_point.to_display_point(snapshot).row(); + let relative_number = relative_to + .filter(|_| relative_line_numbers != RelativeLineNumbers::Disabled) + .map(|base| { + snapshot.relative_line_delta( + base, + start_display_row, + relative_line_numbers == RelativeLineNumbers::Wrapped, + ) + }); + let number = relative_number + .filter(|&delta| delta != 0) + .map(|delta| delta.unsigned_abs() as u32) + .unwrap_or(start_point.row + 1); + let color = cx.theme().colors().editor_line_number; + self.shape_line_number(SharedString::from(number.to_string()), color, window) + }); + + lines.push(StickyHeaderLine::new( + sticky_row, + line_height * offset as f32, + line, + line_number, + line_height, + scroll_pixel_position, + content_origin, + gutter_hitbox, + text_hitbox, + window, + cx, + )); + } + + lines.reverse(); + if lines.is_empty() { + return None; + } + + Some(StickyHeaders { + lines, + gutter_background: cx.theme().colors().editor_gutter_background, + content_background: self.style.background, + gutter_right_padding: gutter_dimensions.right_padding, + }) + } + + pub(super) fn paint_sticky_headers( + &mut self, + layout: &mut EditorLayout, + window: &mut Window, + cx: &mut App, + ) { + let Some(mut sticky_headers) = layout.sticky_headers.take() else { + return; + }; + + let Some(last_line_offset) = sticky_headers.lines.last().map(|line| line.offset) else { + layout.sticky_headers = Some(sticky_headers); + return; + }; + + let whitespace_setting = self + .editor + .read(cx) + .buffer + .read(cx) + .language_settings(cx) + .show_whitespaces; + sticky_headers.paint(layout, whitespace_setting, window, cx); + + let sticky_header_hitboxes: Vec = sticky_headers + .lines + .iter() + .map(|line| line.hitbox.clone()) + .collect(); + let hovered_hitbox = sticky_header_hitboxes + .iter() + .find_map(|hitbox| hitbox.is_hovered(window).then_some(hitbox.id)); + + window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, _cx| { + if !phase.bubble() { + return; + } + + let current_hover = sticky_header_hitboxes + .iter() + .find_map(|hitbox| hitbox.is_hovered(window).then_some(hitbox.id)); + if hovered_hitbox != current_hover { + window.refresh(); + } + }); + + let position_map = layout.position_map.clone(); + + for (line_index, line) in sticky_headers.lines.iter().enumerate() { + let editor = self.editor.clone(); + let hitbox = line.hitbox.clone(); + let row = line.row; + let line_layout = line.line.clone(); + let position_map = position_map.clone(); + window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| { + if !phase.bubble() { + return; + } + + if event.button == MouseButton::Left && hitbox.is_hovered(window) { + let point_for_position = + position_map.point_for_position_on_line(event.position, row, &line_layout); + + editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(window, cx); + let anchor = snapshot + .display_snapshot + .display_point_to_anchor(point_for_position.nearest_valid, Bias::Left); + editor.change_selections( + SelectionEffects::scroll(Autoscroll::top_relative( + line_index as ScrollOffset, + )), + window, + cx, + |selections| { + selections.clear_disjoint(); + selections.set_pending_anchor_range( + anchor..anchor, + crate::SelectMode::Character, + ); + }, + ); + cx.stop_propagation(); + }); + } + }); + } + + let text_bounds = layout.position_map.text_hitbox.bounds; + let border_top = text_bounds.top() + last_line_offset + layout.position_map.line_height; + let separator_height = px(1.); + let border_bounds = window.pixel_snap_bounds(Bounds::from_corners( + point(layout.gutter_hitbox.bounds.left(), border_top), + point(text_bounds.right(), border_top + separator_height), + )); + window.paint_quad(fill(border_bounds, cx.theme().colors().border_variant)); + + layout.sticky_headers = Some(sticky_headers); + } +} + +impl StickyHeaders { + fn paint( + &mut self, + layout: &mut EditorLayout, + whitespace_setting: ShowWhitespaceSetting, + window: &mut Window, + cx: &mut App, + ) { + let line_height = layout.position_map.line_height; + + for line in self.lines.iter_mut().rev() { + window.paint_layer( + Bounds::new( + layout.gutter_hitbox.origin + point(Pixels::ZERO, line.offset), + size(line.hitbox.size.width, line_height), + ), + |window| { + let gutter_bounds = Bounds::new( + layout.gutter_hitbox.origin + point(Pixels::ZERO, line.offset), + size(layout.gutter_hitbox.size.width, line_height), + ); + window.paint_quad(fill(gutter_bounds, self.gutter_background)); + + let text_bounds = Bounds::new( + layout.position_map.text_hitbox.origin + point(Pixels::ZERO, line.offset), + size(line.available_text_width, line_height), + ); + window.paint_quad(fill(text_bounds, self.content_background)); + + if line.hitbox.is_hovered(window) { + let hover_overlay = cx.theme().colors().panel_overlay_hover; + window.paint_quad(fill(gutter_bounds, hover_overlay)); + window.paint_quad(fill(text_bounds, hover_overlay)); + } + + line.paint( + layout, + self.gutter_right_padding, + line.available_text_width, + layout.content_origin, + line_height, + whitespace_setting, + window, + cx, + ); + }, + ); + + window.set_cursor_style(CursorStyle::IBeam, &line.hitbox); + } + } +} + +impl StickyHeaderLine { + fn new( + row: DisplayRow, + offset: Pixels, + mut line: LineWithInvisibles, + line_number: Option, + line_height: Pixels, + scroll_pixel_position: gpui::Point, + content_origin: gpui::Point, + gutter_hitbox: &Hitbox, + text_hitbox: &Hitbox, + window: &mut Window, + cx: &mut App, + ) -> Self { + let mut elements = SmallVec::<[AnyElement; 1]>::new(); + line.prepaint_with_custom_offset( + line_height, + scroll_pixel_position, + content_origin, + offset, + &mut elements, + window, + cx, + ); + + let hitbox_bounds = Bounds::new( + gutter_hitbox.origin + point(Pixels::ZERO, offset), + size(text_hitbox.right() - gutter_hitbox.left(), line_height), + ); + let available_text_width = + (hitbox_bounds.size.width - gutter_hitbox.size.width).max(Pixels::ZERO); + + Self { + row, + offset, + line: Rc::new(line), + line_number, + elements, + available_text_width, + hitbox: window.insert_hitbox(hitbox_bounds, HitboxBehavior::BlockMouseExceptScroll), + } + } + + fn paint( + &mut self, + layout: &EditorLayout, + gutter_right_padding: Pixels, + available_text_width: Pixels, + content_origin: gpui::Point, + line_height: Pixels, + whitespace_setting: ShowWhitespaceSetting, + window: &mut Window, + cx: &mut App, + ) { + window.with_content_mask( + Some(ContentMask { + bounds: Bounds::new( + layout.position_map.text_hitbox.bounds.origin + + point(Pixels::ZERO, self.offset), + size(available_text_width, line_height), + ), + }), + |window| { + self.line.draw_with_custom_offset( + layout, + self.row, + content_origin, + self.offset, + whitespace_setting, + &[], + window, + cx, + ); + for element in &mut self.elements { + element.paint(window, cx); + } + }, + ); + + if let Some(line_number) = &self.line_number { + let gutter_origin = layout.gutter_hitbox.origin + point(Pixels::ZERO, self.offset); + let gutter_width = layout.gutter_hitbox.size.width; + let origin = point( + gutter_origin.x + gutter_width - gutter_right_padding - line_number.width, + gutter_origin.y, + ); + line_number + .paint(origin, line_height, TextAlign::Left, None, window, cx) + .log_err(); + } + } +} + +pub(crate) fn header_jump_data( + editor_snapshot: &EditorSnapshot, + block_row_start: DisplayRow, + height: u32, + first_excerpt: &ExcerptBoundaryInfo, + latest_selection_anchors: &HashMap, +) -> JumpData { + let multibuffer_snapshot = editor_snapshot.buffer_snapshot(); + let buffer = first_excerpt.buffer(multibuffer_snapshot); + let (jump_anchor, jump_buffer, excerpt_start) = if let Some(anchor) = + latest_selection_anchors.get(&first_excerpt.buffer_id()) + && let Some((jump_anchor, selection_buffer)) = + multibuffer_snapshot.anchor_to_buffer_anchor(*anchor) + { + let jump_offset = text::ToOffset::to_offset(&jump_anchor, selection_buffer); + let selection_excerpt_start = multibuffer_snapshot + .excerpts_for_buffer(jump_anchor.buffer_id) + .find(|excerpt| { + let start = text::ToOffset::to_offset(&excerpt.context.start, selection_buffer); + let end = text::ToOffset::to_offset(&excerpt.context.end, selection_buffer); + start <= jump_offset && jump_offset <= end + }) + .map(|excerpt| excerpt.context.start) + .unwrap_or(first_excerpt.range.context.start); + (jump_anchor, selection_buffer, selection_excerpt_start) + } else { + ( + first_excerpt.range.primary.start, + buffer, + first_excerpt.range.context.start, + ) + }; + let jump_position = language::ToPoint::to_point(&jump_anchor, jump_buffer); + let rows_from_excerpt_start = if jump_anchor == excerpt_start { + 0 + } else { + let excerpt_start_point = language::ToPoint::to_point(&excerpt_start, jump_buffer); + jump_position.row.saturating_sub(excerpt_start_point.row) + }; + + let line_offset_from_top = (block_row_start.0 + height + rows_from_excerpt_start) + .saturating_sub( + editor_snapshot + .scroll_anchor + .scroll_position(&editor_snapshot.display_snapshot) + .y as u32, + ); + + JumpData::MultiBufferPoint { + anchor: jump_anchor, + position: jump_position, + line_offset_from_top, + } +} + +pub(crate) fn render_buffer_header( + editor: &Entity, + for_excerpt: &ExcerptBoundaryInfo, + is_folded: bool, + is_selected: bool, + is_sticky: bool, + jump_data: JumpData, + window: &mut Window, + cx: &mut App, +) -> impl IntoElement { + let editor_read = editor.read(cx); + let multi_buffer = editor_read.buffer.read(cx); + let is_read_only = editor_read.read_only(cx); + let editor_handle: &dyn ItemHandle = editor; + let multibuffer_snapshot = multi_buffer.snapshot(cx); + let buffer = for_excerpt.buffer(&multibuffer_snapshot); + + let breadcrumbs = if is_selected { + editor_read.breadcrumbs_inner(cx) + } else { + None + }; + + let buffer_id = for_excerpt.buffer_id(); + let file_status = multi_buffer + .all_diff_hunks_expanded() + .then(|| editor_read.status_for_buffer_id(buffer_id, cx)) + .flatten(); + let indicator = multi_buffer.buffer(buffer_id).and_then(|buffer| { + let buffer = buffer.read(cx); + let indicator_color = match (buffer.has_conflict(), buffer.is_dirty()) { + (true, _) => Some(Color::Warning), + (_, true) => Some(Color::Accent), + (false, false) => None, + }; + indicator_color.map(|indicator_color| Indicator::dot().color(indicator_color)) + }); + + let include_root = editor_read + .project + .as_ref() + .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) + .unwrap_or_default(); + let file = buffer.file(); + let can_open_excerpts = file.is_none_or(|file| file.can_open()); + let path_style = file.map(|file| file.path_style(cx)); + let relative_path = buffer.resolve_file_path(include_root, cx); + let (parent_path, filename) = if let Some(path) = &relative_path { + if let Some(path_style) = path_style { + let (dir, file_name) = path_style.split(path); + (dir.map(|dir| dir.to_owned()), Some(file_name.to_owned())) + } else { + (None, Some(path.clone())) + } + } else { + (None, None) + }; + let focus_handle = editor_read.focus_handle(cx); + let colors = cx.theme().colors(); + + let header = div() + .id(("buffer-header", buffer_id.to_proto())) + .p(BUFFER_HEADER_PADDING) + .w_full() + .h(FILE_HEADER_HEIGHT as f32 * window.line_height()) + .child( + h_flex() + .group("buffer-header-group") + .size_full() + .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) + .pl_1() + .pr_2() + .rounded_sm() + .gap_1p5() + .when(is_sticky, |el| el.shadow_md()) + .border_1() + .map(|border| { + let border_color = + if is_selected && is_folded && focus_handle.contains_focused(window, cx) { + colors.border_focused + } else { + colors.border + }; + border.border_color(border_color) + }) + .bg(colors.editor_subheader_background) + .hover(|style| style.bg(colors.element_hover)) + .map(|header| { + let editor = editor.clone(); + let buffer_id = for_excerpt.buffer_id(); + let toggle_chevron_icon = + FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path); + let button_size = rems_from_px(28.); + + header.child( + div() + .hover(|style| style.bg(colors.element_selected)) + .rounded_xs() + .child( + ButtonLike::new("toggle-buffer-fold") + .style(ButtonStyle::Transparent) + .height(button_size.into()) + .width(button_size) + .children(toggle_chevron_icon) + .tooltip({ + let focus_handle = focus_handle.clone(); + let is_folded_for_tooltip = is_folded; + move |_window, cx| { + Tooltip::with_meta_in( + if is_folded_for_tooltip { + "Unfold Excerpt" + } else { + "Fold Excerpt" + }, + Some(&ToggleFold), + format!( + "{} to toggle all", + text_for_keystroke( + &Modifiers::alt(), + "click", + cx + ) + ), + &focus_handle, + cx, + ) + } + }) + .on_click(move |event, window, cx| { + if event.modifiers().alt { + editor.update(cx, |editor, cx| { + editor.toggle_fold_all(&ToggleFoldAll, window, cx); + }); + } else { + if is_folded { + editor.update(cx, |editor, cx| { + editor.unfold_buffer(buffer_id, cx); + }); + } else { + editor.update(cx, |editor, cx| { + editor.fold_buffer(buffer_id, cx); + }); + } + } + }), + ), + ) + }) + .children( + editor_read + .addons + .values() + .filter_map(|addon| { + addon.render_buffer_header_controls(for_excerpt, buffer, window, cx) + }) + .take(1), + ) + .when(!is_read_only, |this| { + this.child( + h_flex() + .size_3() + .justify_center() + .flex_shrink_0() + .children(indicator), + ) + }) + .child( + h_flex() + .cursor_pointer() + .id("path_header_block") + .min_w_0() + .size_full() + .gap_1() + .justify_between() + .overflow_hidden() + .child(h_flex().min_w_0().flex_1().gap_0p5().overflow_hidden().map( + |path_header| { + let filename = filename + .map(SharedString::from) + .unwrap_or_else(|| "untitled".into()); + + let full_path = match parent_path.as_deref() { + Some(parent) if !parent.is_empty() => { + format!("{}{}", parent, filename.as_str()) + } + _ => filename.as_str().to_string(), + }; + + path_header + .child( + ButtonLike::new("filename-button") + .when(ItemSettings::get_global(cx).file_icons, |this| { + let path = std::path::Path::new(filename.as_str()); + let icon = FileIcons::get_icon(path, cx) + .unwrap_or_default(); + + this.child( + Icon::from_path(icon).color(Color::Muted), + ) + }) + .child( + Label::new(filename) + .single_line() + .color(file_status_label_color(file_status)) + .buffer_font(cx) + .when( + file_status.is_some_and(|s| s.is_deleted()), + |label| label.strikethrough(), + ), + ) + .tooltip(move |_, cx| { + Tooltip::with_meta( + "Open File", + None, + full_path.clone(), + cx, + ) + }) + .on_click(window.listener_for(editor, { + let jump_data = jump_data.clone(); + move |editor, e: &ClickEvent, window, cx| { + editor.open_excerpts_common( + Some(jump_data.clone()), + e.modifiers().secondary(), + window, + cx, + ); + } + })), + ) + .when_some(parent_path, |then, path| { + then.child( + Label::new(path) + .buffer_font(cx) + .truncate_start() + .color( + if file_status + .is_some_and(FileStatus::is_deleted) + { + Color::Custom(colors.text_disabled) + } else { + Color::Custom(colors.text_muted) + }, + ), + ) + }) + .when(!buffer.capability.editable(), |el| { + el.child(Icon::new(IconName::FileLock).color(Color::Muted)) + }) + .when_some(breadcrumbs, |then, breadcrumbs| { + let font = theme_settings::ThemeSettings::get_global(cx) + .buffer_font + .clone(); + then.child(render_breadcrumb_text( + breadcrumbs, + Some(font), + None, + editor_handle, + true, + window, + cx, + )) + }) + }, + )) + .when(can_open_excerpts && relative_path.is_some(), |this| { + this.child( + div() + .when(!is_selected, |this| { + this.visible_on_hover("buffer-header-group") + }) + .child( + Button::new("open-file-button", "Open File") + .style(ButtonStyle::OutlinedGhost) + .when(is_selected, |this| { + this.key_binding(KeyBinding::for_action_in( + &OpenExcerpts, + &focus_handle, + cx, + )) + }) + .on_click(window.listener_for(editor, { + let jump_data = jump_data.clone(); + move |editor, e: &ClickEvent, window, cx| { + editor.open_excerpts_common( + Some(jump_data.clone()), + e.modifiers().secondary(), + window, + cx, + ); + } + })), + ), + ) + }) + .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .on_click(window.listener_for(editor, { + let buffer_id = for_excerpt.buffer_id(); + move |editor, e: &ClickEvent, window, cx| { + if e.modifiers().alt { + editor.open_excerpts_common( + Some(jump_data.clone()), + e.modifiers().secondary(), + window, + cx, + ); + return; + } + + if is_folded { + editor.unfold_buffer(buffer_id, cx); + } else { + editor.fold_buffer(buffer_id, cx); + } + } + })), + ), + ); + + let file = buffer.file().cloned(); + let editor = editor.clone(); + let buffer_snapshot = buffer.clone(); + + right_click_menu("buffer-header-context-menu") + .trigger(move |_, _, _| header) + .menu(move |window, cx| { + let menu_context = focus_handle.clone(); + let editor = editor.clone(); + let file = file.clone(); + let buffer_snapshot = buffer_snapshot.clone(); + ContextMenu::build(window, cx, move |mut menu, window, cx| { + if let Some(file) = file + && let Some(project) = editor.read(cx).project() + && let Some(worktree) = + project.read(cx).worktree_for_id(file.worktree_id(cx), cx) + { + let path_style = file.path_style(cx); + let worktree = worktree.read(cx); + let relative_path = file.path(); + let entry_for_path = worktree.entry_for_path(relative_path); + let abs_path = entry_for_path.map(|e| { + e.canonical_path + .as_deref() + .map_or_else(|| worktree.absolutize(relative_path), Path::to_path_buf) + }); + let has_relative_path = worktree.root_entry().is_some_and(Entry::is_dir); + + let parent_abs_path = abs_path + .as_ref() + .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf())); + let relative_path = has_relative_path + .then_some(relative_path) + .map(ToOwned::to_owned); + + let visible_in_project_panel = relative_path.is_some() && worktree.is_visible(); + let reveal_in_project_panel = entry_for_path + .filter(|_| visible_in_project_panel) + .map(|entry| entry.id); + menu = menu + .when_some(abs_path, |menu, abs_path| { + menu.entry( + "Copy Path", + Some(Box::new(zed_actions::workspace::CopyPath)), + window.handler_for(&editor, move |_, _, cx| { + cx.write_to_clipboard(ClipboardItem::new_string( + abs_path.to_string_lossy().into_owned(), + )); + }), + ) + }) + .when_some(relative_path, |menu, relative_path| { + menu.entry( + "Copy Relative Path", + Some(Box::new(zed_actions::workspace::CopyRelativePath)), + window.handler_for(&editor, move |_, _, cx| { + cx.write_to_clipboard(ClipboardItem::new_string( + relative_path.display(path_style).to_string(), + )); + }), + ) + }) + .when( + reveal_in_project_panel.is_some() || parent_abs_path.is_some(), + |menu| menu.separator(), + ) + .when_some(reveal_in_project_panel, |menu, entry_id| { + menu.entry( + "Reveal In Project Panel", + Some(Box::new(RevealInProjectPanel::default())), + window.handler_for(&editor, move |editor, _, cx| { + if let Some(project) = &mut editor.project { + project.update(cx, |_, cx| { + cx.emit(project::Event::RevealInProjectPanel(entry_id)) + }); + } + }), + ) + }) + .when_some(parent_abs_path, |menu, parent_abs_path| { + menu.entry( + "Open in Terminal", + Some(Box::new(OpenInTerminal)), + window.handler_for(&editor, move |_, window, cx| { + window.dispatch_action( + OpenTerminal { + working_directory: parent_abs_path.clone(), + local: false, + } + .boxed_clone(), + cx, + ); + }), + ) + }); + } + + menu = editor.update(cx, |editor, cx| { + let mut menu = menu; + for addon in editor.addons.values() { + menu = addon.extend_buffer_header_context_menu( + menu, + &buffer_snapshot, + window, + cx, + ); + } + menu + }); + + menu.context(menu_context) + }) + }) +} + +fn file_status_label_color(file_status: Option) -> Color { + file_status.map_or(Color::Default, |status| { + if status.is_conflicted() { + Color::Conflict + } else if status.is_modified() { + Color::Modified + } else if status.is_deleted() { + Color::Disabled + } else if status.is_created() { + Color::Created + } else { + Color::Default + } + }) +} diff --git a/crates/editor/src/element/mouse.rs b/crates/editor/src/element/mouse.rs new file mode 100644 index 00000000000000..b3fc095dbbc00d --- /dev/null +++ b/crates/editor/src/element/mouse.rs @@ -0,0 +1,1187 @@ +use std::ops::Range; +use std::time::{Duration, Instant}; + +use collections::HashMap; +use feature_flags::{DiffReviewFeatureFlag, FeatureFlagAppExt as _}; +use gpui::{ + AnyElement, App, AvailableSpace, ClickEvent, Context, DefiniteLength, DispatchPhase, Element, + MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent, + ParentElement, Pixels, PressureStage, ScrollDelta, ScrollWheelEvent, TextStyleRefinement, + Window, anchored, deferred, point, px, +}; +use multi_buffer::MultiBufferRow; +use project::DisableAiSettings; +use settings::Settings; +use sum_tree::Bias; +use text::SelectionGoal; +use theme_settings::BufferLineHeight; +use util::{RangeExt, debug_panic, post_inc}; + +use super::{EditorElement, EditorLayout, LineNumberLayout, PositionMap, SplitSide}; +use crate::{ + CURSORS_VISIBLE_FOR, ColumnarMode, DisplayDiffHunk, DisplayPoint, DisplayRow, Editor, + EditorSettings, EditorSnapshot, GutterHoverButton, HoveredCursor, JumpData, + PhantomDiffReviewIndicator, SelectPhase, Selection, SelectionDragState, + display_map::ToDisplayPoint, editor_settings::DoubleClickInMultibuffer, + hover_popover::hover_at, mouse_context_menu, scroll::ScrollPixelOffset, +}; + +impl EditorElement { + pub(crate) fn mouse_moved( + editor: &mut Editor, + event: &MouseMoveEvent, + position_map: &PositionMap, + split_side: Option, + window: &mut Window, + cx: &mut Context, + ) { + let text_hitbox = &position_map.text_hitbox; + let gutter_hitbox = &position_map.gutter_hitbox; + let modifiers = event.modifiers; + let text_hovered = text_hitbox.is_hovered(window); + let gutter_hovered = gutter_hitbox.is_hovered(window); + editor.set_gutter_hovered(gutter_hovered, cx); + + let point_for_position = position_map.point_for_position(event.position); + let valid_point = point_for_position.nearest_valid; + + // Update diff review drag state if we're dragging + if editor.diff_review_drag_state.is_some() { + editor.update_diff_review_drag(valid_point.row(), window, cx); + } + + let hovered_diff_control = position_map + .diff_hunk_control_bounds + .iter() + .find(|(_, bounds)| bounds.contains(&event.position)) + .map(|(row, _)| *row); + + let hovered_diff_hunk_row = if let Some(control_row) = hovered_diff_control { + Some(control_row) + } else if text_hovered { + let current_row = valid_point.row(); + position_map.display_hunks.iter().find_map(|(hunk, _)| { + if let DisplayDiffHunk::Unfolded { + display_row_range, .. + } = hunk + { + if display_row_range.contains(¤t_row) { + Some(display_row_range.start) + } else { + None + } + } else { + None + } + }) + } else { + None + }; + + if hovered_diff_hunk_row != editor.hovered_diff_hunk_row { + editor.hovered_diff_hunk_row = hovered_diff_hunk_row; + cx.notify(); + } + + if text_hovered + && let Some((bounds, buffer_id, blame_entry)) = &position_map.inline_blame_bounds + { + let mouse_over_inline_blame = bounds.contains(&event.position); + let mouse_over_popover = editor + .inline_blame_popover + .as_ref() + .and_then(|state| state.popover_bounds) + .is_some_and(|bounds| bounds.contains(&event.position)); + let keyboard_grace = editor + .inline_blame_popover + .as_ref() + .is_some_and(|state| state.keyboard_grace); + + if mouse_over_inline_blame || mouse_over_popover { + editor.show_blame_popover(*buffer_id, blame_entry, event.position, false, cx); + } else if !keyboard_grace { + editor.hide_blame_popover(false, cx); + } + } else { + let keyboard_grace = editor + .inline_blame_popover + .as_ref() + .is_some_and(|state| state.keyboard_grace); + if !keyboard_grace { + editor.hide_blame_popover(false, cx); + } + } + + // Handle diff review indicator when gutter is hovered in diff mode with AI enabled + let show_diff_review = editor.show_diff_review_button() + && cx.has_flag::() + && !DisableAiSettings::is_ai_disabled_for_buffer( + editor.buffer.read(cx).as_singleton().as_ref(), + cx, + ); + + let diff_review_indicator = if gutter_hovered && show_diff_review { + let is_visible = editor + .gutter_diff_review_indicator + .0 + .is_some_and(|indicator| indicator.is_active); + + if !is_visible { + editor + .gutter_diff_review_indicator + .1 + .get_or_insert_with(|| { + cx.spawn(async move |this, cx| { + cx.background_executor() + .timer(Duration::from_millis(200)) + .await; + + this.update(cx, |this, cx| { + if let Some(indicator) = + this.gutter_diff_review_indicator.0.as_mut() + { + indicator.is_active = true; + cx.notify(); + } + }) + .ok(); + }) + }); + } + + let anchor = position_map + .snapshot + .display_point_to_anchor(valid_point, Bias::Left); + Some(PhantomDiffReviewIndicator { + start: anchor, + end: anchor, + is_active: is_visible, + }) + } else { + editor.gutter_diff_review_indicator.1 = None; + None + }; + + if diff_review_indicator != editor.gutter_diff_review_indicator.0 { + editor.gutter_diff_review_indicator.0 = diff_review_indicator; + cx.notify(); + } + + // Don't show breakpoint indicator when diff review indicator is active on this row + let is_on_diff_review_button_row = diff_review_indicator.is_some_and(|indicator| { + let start_row = indicator + .start + .to_display_point(&position_map.snapshot.display_snapshot) + .row(); + indicator.is_active && start_row == valid_point.row() + }); + + let gutter_hover_button = if gutter_hovered + && !is_on_diff_review_button_row + && split_side != Some(SplitSide::Left) + { + let buffer_anchor = position_map + .snapshot + .display_point_to_anchor(valid_point, Bias::Left); + + if position_map + .snapshot + .buffer_snapshot() + .anchor_to_buffer_anchor(buffer_anchor) + .is_some() + { + let is_visible = editor + .gutter_hover_button + .0 + .is_some_and(|indicator| indicator.is_active); + + if !is_visible { + editor.gutter_hover_button.1.get_or_insert_with(|| { + cx.spawn(async move |this, cx| { + cx.background_executor() + .timer(Duration::from_millis(200)) + .await; + + this.update(cx, |this, cx| { + if let Some(indicator) = this.gutter_hover_button.0.as_mut() { + indicator.is_active = true; + cx.notify(); + } + }) + .ok(); + }) + }); + } + + Some(GutterHoverButton { + display_row: valid_point.row(), + is_active: is_visible, + }) + } else { + editor.gutter_hover_button.1 = None; + None + } + } else if editor.has_mouse_context_menu() { + editor.gutter_hover_button.1 = None; + editor.gutter_hover_button.0 + } else { + editor.gutter_hover_button.1 = None; + None + }; + + if &gutter_hover_button != &editor.gutter_hover_button.0 { + editor.gutter_hover_button.0 = gutter_hover_button; + cx.notify(); + } + + // Don't trigger hover popover if mouse is hovering over context menu + if text_hovered { + editor.update_hovered_link( + point_for_position, + Some(event.position), + &position_map.snapshot, + modifiers, + window, + cx, + ); + + if let Some(point) = point_for_position.as_valid() { + let anchor = position_map + .snapshot + .buffer_snapshot() + .anchor_before(point.to_offset(&position_map.snapshot, Bias::Left)); + hover_at(editor, Some(anchor), Some(event.position), window, cx); + Self::update_visible_cursor(editor, point, position_map, window, cx); + } else { + editor.update_inlay_link_and_hover_points( + &position_map.snapshot, + point_for_position, + Some(event.position), + modifiers.secondary(), + modifiers.shift, + window, + cx, + ); + } + } else { + editor.hide_hovered_link(cx); + hover_at(editor, None, Some(event.position), window, cx); + } + } + + pub(super) fn layout_mouse_context_menu( + &self, + editor_snapshot: &EditorSnapshot, + visible_range: Range, + content_origin: gpui::Point, + window: &mut Window, + cx: &mut App, + ) -> Option { + let position = self.editor.update(cx, |editor, cx| { + let visible_start_point = editor.display_to_pixel_point( + DisplayPoint::new(visible_range.start, 0), + editor_snapshot, + window, + cx, + )?; + let visible_end_point = editor.display_to_pixel_point( + DisplayPoint::new(visible_range.end, 0), + editor_snapshot, + window, + cx, + )?; + + let mouse_context_menu = editor.mouse_context_menu.as_ref()?; + let (source_display_point, position) = match mouse_context_menu.position { + mouse_context_menu::MenuPosition::PinnedToScreen(point) => (None, point), + mouse_context_menu::MenuPosition::PinnedToEditor { source, offset } => { + let source_display_point = source.to_display_point(editor_snapshot); + let source_point = + editor.to_pixel_point(source, editor_snapshot, window, cx)?; + let position = content_origin + source_point + offset; + (Some(source_display_point), position) + } + }; + + let source_included = source_display_point.is_none_or(|source_display_point| { + visible_range + .to_inclusive() + .contains(&source_display_point.row()) + }); + let position_included = + visible_start_point.y <= position.y && position.y <= visible_end_point.y; + if !source_included && !position_included { + None + } else { + Some(position) + } + })?; + + let text_style = TextStyleRefinement { + line_height: Some(DefiniteLength::Fraction( + BufferLineHeight::Comfortable.value(), + )), + ..Default::default() + }; + window.with_text_style(Some(text_style), |window| { + let mut element = self.editor.read_with(cx, |editor, _| { + let mouse_context_menu = editor.mouse_context_menu.as_ref()?; + let context_menu = mouse_context_menu.context_menu.clone(); + + Some( + deferred( + anchored() + .position(position) + .child(context_menu) + .anchor(gpui::Anchor::TopLeft) + .snap_to_window_with_margin(px(8.)), + ) + .with_priority(1) + .into_any(), + ) + })?; + + element.prepaint_as_root(position, AvailableSpace::min_size(), window, cx); + Some(element) + }) + } + + pub(super) fn paint_mouse_listeners( + &mut self, + layout: &EditorLayout, + window: &mut Window, + cx: &mut App, + ) { + if layout.mode.is_minimap() { + return; + } + + self.paint_scroll_wheel_listener(layout, window, cx); + + window.on_mouse_event({ + let position_map = layout.position_map.clone(); + let editor = self.editor.clone(); + let line_numbers = layout.line_numbers.clone(); + + move |event: &MouseDownEvent, phase, window, cx| { + if phase == DispatchPhase::Bubble { + match event.button { + MouseButton::Left => editor.update(cx, |editor, cx| { + let pending_mouse_down = editor + .pending_mouse_down + .get_or_insert_with(Default::default) + .clone(); + + *pending_mouse_down.borrow_mut() = Some(event.clone()); + + Self::mouse_left_down( + editor, + event, + &position_map, + line_numbers.as_ref(), + window, + cx, + ); + }), + MouseButton::Right => editor.update(cx, |editor, cx| { + Self::mouse_right_down(editor, event, &position_map, window, cx); + }), + MouseButton::Middle => editor.update(cx, |editor, cx| { + Self::mouse_middle_down(editor, event, &position_map, window, cx); + }), + _ => {} + }; + } + } + }); + + window.on_mouse_event({ + let editor = self.editor.clone(); + let position_map = layout.position_map.clone(); + + move |event: &MouseUpEvent, phase, window, cx| { + if phase == DispatchPhase::Bubble { + editor.update(cx, |editor, cx| { + Self::mouse_up(editor, event, &position_map, window, cx) + }); + } + } + }); + + window.on_mouse_event({ + let editor = self.editor.clone(); + let position_map = layout.position_map.clone(); + let mut captured_mouse_down = None; + + move |event: &MouseUpEvent, phase, window, cx| match phase { + // Clear the pending mouse down during the capture phase, + // so that it happens even if another event handler stops + // propagation. + DispatchPhase::Capture => editor.update(cx, |editor, _cx| { + let pending_mouse_down = editor + .pending_mouse_down + .get_or_insert_with(Default::default) + .clone(); + + let mut pending_mouse_down = pending_mouse_down.borrow_mut(); + if pending_mouse_down.is_some() && position_map.text_hitbox.is_hovered(window) { + captured_mouse_down = pending_mouse_down.take(); + window.refresh(); + } + }), + // Fire click handlers during the bubble phase. + DispatchPhase::Bubble => editor.update(cx, |editor, cx| { + if let Some(mouse_down) = captured_mouse_down.take() { + let event = ClickEvent::Mouse(MouseClickEvent { + down: mouse_down, + up: event.clone(), + }); + Self::click(editor, &event, &position_map, window, cx); + } + }), + } + }); + + window.on_mouse_event({ + let position_map = layout.position_map.clone(); + let editor = self.editor.clone(); + + move |event: &MousePressureEvent, phase, window, cx| { + if phase == DispatchPhase::Bubble { + editor.update(cx, |editor, cx| { + Self::pressure_click(editor, &event, &position_map, window, cx); + }) + } + } + }); + + window.on_mouse_event({ + let position_map = layout.position_map.clone(); + let editor = self.editor.clone(); + let split_side = self.split_side; + + move |event: &MouseMoveEvent, phase, window, cx| { + if phase == DispatchPhase::Bubble { + editor.update(cx, |editor, cx| { + if editor.hover_state.focused(window, cx) { + return; + } + if event.pressed_button == Some(MouseButton::Left) + || event.pressed_button == Some(MouseButton::Middle) + { + Self::mouse_dragged(editor, event, &position_map, window, cx) + } + + Self::mouse_moved(editor, event, &position_map, split_side, window, cx) + }); + } + } + }); + } + + fn paint_scroll_wheel_listener( + &mut self, + layout: &EditorLayout, + window: &mut Window, + cx: &mut App, + ) { + window.on_mouse_event({ + let position_map = layout.position_map.clone(); + let editor = self.editor.clone(); + let hitbox = layout.hitbox.clone(); + let mut delta = ScrollDelta::default(); + + // Set a minimum scroll_sensitivity of 0.01 to make sure the user doesn't + // accidentally turn off their scrolling. + let base_scroll_sensitivity = + EditorSettings::get_global(cx).scroll_sensitivity.max(0.01); + + // Use a minimum fast_scroll_sensitivity for same reason above + let fast_scroll_sensitivity = EditorSettings::get_global(cx) + .fast_scroll_sensitivity + .max(0.01); + + move |event: &ScrollWheelEvent, phase, window, cx| { + if phase == DispatchPhase::Bubble && hitbox.should_handle_scroll(window) { + delta = delta.coalesce(event.delta); + + if event.modifiers.secondary() + && editor.read(cx).enable_mouse_wheel_zoom + && EditorSettings::get_global(cx).mouse_wheel_zoom + { + let delta_y = match event.delta { + ScrollDelta::Pixels(pixels) => pixels.y.into(), + ScrollDelta::Lines(lines) => lines.y, + }; + + if delta_y > 0.0 { + theme_settings::increase_buffer_font_size(cx); + } else if delta_y < 0.0 { + theme_settings::decrease_buffer_font_size(cx); + } + + cx.stop_propagation(); + } else { + let scroll_sensitivity = { + if event.modifiers.alt { + fast_scroll_sensitivity + } else { + base_scroll_sensitivity + } + }; + + editor.update(cx, |editor, cx| { + let line_height = position_map.line_height; + let glyph_width = position_map.em_layout_width; + let (delta, axis) = match delta { + gpui::ScrollDelta::Pixels(mut pixels) => { + //Trackpad + let axis = + position_map.snapshot.ongoing_scroll.filter(&mut pixels); + (pixels, axis) + } + + gpui::ScrollDelta::Lines(lines) => { + //Not trackpad + let pixels = + point(lines.x * glyph_width, lines.y * line_height); + (pixels, None) + } + }; + + let current_scroll_position = position_map.snapshot.scroll_position(); + let x = (current_scroll_position.x + * ScrollPixelOffset::from(glyph_width) + - ScrollPixelOffset::from(delta.x * scroll_sensitivity)) + / ScrollPixelOffset::from(glyph_width); + let y = (current_scroll_position.y + * ScrollPixelOffset::from(line_height) + - ScrollPixelOffset::from(delta.y * scroll_sensitivity)) + / ScrollPixelOffset::from(line_height); + let mut scroll_position = + point(x, y).clamp(&point(0., 0.), &position_map.scroll_max); + let forbid_vertical_scroll = + editor.scroll_manager.forbid_vertical_scroll(); + if forbid_vertical_scroll { + scroll_position.y = current_scroll_position.y; + } + + if scroll_position != current_scroll_position { + editor.scroll(scroll_position, axis, window, cx); + cx.stop_propagation(); + } else if y < 0. { + // Due to clamping, we may fail to detect cases of overscroll to the top; + // We want the scroll manager to get an update in such cases and detect the change of direction + // on the next frame. + cx.notify(); + } + }); + } + } + } + }); + } + + fn mouse_left_down( + editor: &mut Editor, + event: &MouseDownEvent, + position_map: &PositionMap, + line_numbers: &HashMap, + window: &mut Window, + cx: &mut Context, + ) { + if window.default_prevented() { + return; + } + + let text_hitbox = &position_map.text_hitbox; + let gutter_hitbox = &position_map.gutter_hitbox; + let point_for_position = position_map.point_for_position(event.position); + let mut click_count = event.click_count; + let mut modifiers = event.modifiers; + + if let Some(hovered_hunk) = + position_map + .display_hunks + .iter() + .find_map(|(hunk, hunk_hitbox)| match hunk { + DisplayDiffHunk::Folded { .. } => None, + DisplayDiffHunk::Unfolded { + multi_buffer_range, .. + } => hunk_hitbox + .as_ref() + .is_some_and(|hitbox| hitbox.is_hovered(window)) + .then(|| multi_buffer_range.clone()), + }) + { + editor.toggle_single_diff_hunk(hovered_hunk, cx); + cx.notify(); + return; + } else if gutter_hitbox.is_hovered(window) { + click_count = 3; // Simulate triple-click when clicking the gutter to select lines + } else if !text_hitbox.is_hovered(window) { + return; + } + + if EditorSettings::get_global(cx) + .drag_and_drop_selection + .enabled + && click_count == 1 + && !modifiers.shift + { + let newest_anchor = editor.selections.newest_anchor(); + let snapshot = editor.snapshot(window, cx); + let selection = newest_anchor.map(|anchor| anchor.to_display_point(&snapshot)); + if point_for_position.intersects_selection(&selection) { + editor.selection_drag_state = SelectionDragState::ReadyToDrag { + selection: newest_anchor.clone(), + click_position: event.position, + mouse_down_time: Instant::now(), + }; + cx.stop_propagation(); + return; + } + } + + let is_singleton = editor.buffer().read(cx).is_singleton(); + + if click_count == 2 && !is_singleton { + match EditorSettings::get_global(cx).double_click_in_multibuffer { + DoubleClickInMultibuffer::Select => { + // do nothing special on double click, all selection logic is below + } + DoubleClickInMultibuffer::Open => { + if modifiers.alt { + // if double click is made with alt, pretend it's a regular double click without opening and alt, + // and run the selection logic. + modifiers.alt = false; + } else { + let scroll_position_row = position_map.scroll_position.y; + let display_row = (((event.position - gutter_hitbox.bounds.origin).y + / position_map.line_height) + as f64 + + position_map.scroll_position.y) + as u32; + let multi_buffer_row = position_map + .snapshot + .display_point_to_point( + DisplayPoint::new(DisplayRow(display_row), 0), + Bias::Right, + ) + .row; + let line_offset_from_top = display_row - scroll_position_row as u32; + // if double click is made without alt, open the corresponding excerp + editor.open_excerpts_common( + Some(JumpData::MultiBufferRow { + row: MultiBufferRow(multi_buffer_row), + line_offset_from_top, + }), + false, + window, + cx, + ); + return; + } + } + } + } + + if !is_singleton { + let display_row = (ScrollPixelOffset::from( + (event.position - gutter_hitbox.bounds.origin).y / position_map.line_height, + ) + position_map.scroll_position.y) as u32; + let multi_buffer_row = position_map + .snapshot + .display_point_to_point(DisplayPoint::new(DisplayRow(display_row), 0), Bias::Right) + .row; + if line_numbers + .get(&MultiBufferRow(multi_buffer_row)) + .is_some_and(|line_layout| { + line_layout.segments.iter().any(|segment| { + segment + .hitbox + .as_ref() + .is_some_and(|hitbox| hitbox.contains(&event.position)) + }) + }) + { + let line_offset_from_top = display_row - position_map.scroll_position.y as u32; + + editor.open_excerpts_common( + Some(JumpData::MultiBufferRow { + row: MultiBufferRow(multi_buffer_row), + line_offset_from_top, + }), + modifiers.alt, + window, + cx, + ); + cx.stop_propagation(); + return; + } + } + + let position = point_for_position.nearest_valid; + if let Some(mode) = Editor::columnar_selection_mode(&modifiers, cx) { + editor.select( + SelectPhase::BeginColumnar { + position, + reset: match mode { + ColumnarMode::FromMouse => true, + ColumnarMode::FromSelection => false, + }, + mode, + goal_column: point_for_position.exact_unclipped.column(), + }, + window, + cx, + ); + } else if modifiers.shift && !modifiers.control && !modifiers.alt && !modifiers.secondary() + { + editor.select( + SelectPhase::Extend { + position, + click_count, + }, + window, + cx, + ); + } else { + editor.select( + SelectPhase::Begin { + position, + add: Editor::is_alt_pressed(&modifiers, cx), + click_count, + }, + window, + cx, + ); + } + cx.stop_propagation(); + } + + fn mouse_right_down( + editor: &mut Editor, + event: &MouseDownEvent, + position_map: &PositionMap, + window: &mut Window, + cx: &mut Context, + ) { + if position_map.gutter_hitbox.is_hovered(window) { + let gutter_right_padding = editor.gutter_dimensions.right_padding; + let hitbox = &position_map.gutter_hitbox; + + if event.position.x <= hitbox.bounds.right() - gutter_right_padding + // Don't show the gutter_context_menu in collab notes + && editor.project.is_some() + { + let point_for_position = position_map.point_for_position(event.position); + editor.set_gutter_context_menu( + point_for_position.nearest_valid.row(), + None, + event.position, + window, + cx, + ); + } + return; + } + + if !position_map.text_hitbox.is_hovered(window) { + return; + } + + let point_for_position = position_map.point_for_position(event.position); + mouse_context_menu::deploy_context_menu( + editor, + Some(event.position), + point_for_position.nearest_valid, + window, + cx, + ); + cx.stop_propagation(); + } + + fn mouse_middle_down( + editor: &mut Editor, + event: &MouseDownEvent, + position_map: &PositionMap, + window: &mut Window, + cx: &mut Context, + ) { + if !position_map.text_hitbox.is_hovered(window) || window.default_prevented() { + return; + } + + let point_for_position = position_map.point_for_position(event.position); + let position = point_for_position.nearest_valid; + + editor.select( + SelectPhase::BeginColumnar { + position, + reset: true, + mode: ColumnarMode::FromMouse, + goal_column: point_for_position.exact_unclipped.column(), + }, + window, + cx, + ); + } + + fn mouse_up( + editor: &mut Editor, + event: &MouseUpEvent, + position_map: &PositionMap, + window: &mut Window, + cx: &mut Context, + ) { + // Handle diff review drag completion + if editor.diff_review_drag_state.is_some() { + editor.end_diff_review_drag(window, cx); + cx.stop_propagation(); + return; + } + + let text_hitbox = &position_map.text_hitbox; + let end_selection = editor.has_pending_selection(); + let pending_nonempty_selections = editor.has_pending_nonempty_selection(); + let point_for_position = position_map.point_for_position(event.position); + + match editor.selection_drag_state { + SelectionDragState::ReadyToDrag { + selection: _, + ref click_position, + mouse_down_time: _, + } => { + if event.position == *click_position { + editor.select( + SelectPhase::Begin { + position: point_for_position.nearest_valid, + add: false, + click_count: 1, // ready to drag state only occurs on click count 1 + }, + window, + cx, + ); + editor.selection_drag_state = SelectionDragState::None; + cx.stop_propagation(); + return; + } else { + debug_panic!("drag state can never be in ready state after drag") + } + } + SelectionDragState::Dragging { ref selection, .. } => { + let snapshot = editor.snapshot(window, cx); + let selection_display = selection.map(|anchor| anchor.to_display_point(&snapshot)); + if !point_for_position.intersects_selection(&selection_display) + && text_hitbox.is_hovered(window) + { + let is_cut = !(cfg!(target_os = "macos") && event.modifiers.alt + || cfg!(not(target_os = "macos")) && event.modifiers.control); + editor.move_selection_on_drop( + &selection.clone(), + point_for_position.nearest_valid, + is_cut, + window, + cx, + ); + } + editor.selection_drag_state = SelectionDragState::None; + cx.stop_propagation(); + cx.notify(); + return; + } + _ => {} + } + + if end_selection { + editor.select(SelectPhase::End, window, cx); + } + + if end_selection && pending_nonempty_selections { + cx.stop_propagation(); + } else if cfg!(any(target_os = "linux", target_os = "freebsd")) + && event.button == MouseButton::Middle + { + #[allow( + clippy::collapsible_if, + clippy::needless_return, + reason = "The cfg-block below makes this a false positive" + )] + if !text_hitbox.is_hovered(window) || editor.read_only(cx) { + return; + } + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + if EditorSettings::get_global(cx).middle_click_paste { + if let Some(text) = cx.read_from_primary().and_then(|item| item.text()) { + let point_for_position = position_map.point_for_position(event.position); + let position = point_for_position.nearest_valid; + + editor.select( + SelectPhase::Begin { + position, + add: false, + click_count: 1, + }, + window, + cx, + ); + editor.insert(&text, window, cx); + } + cx.stop_propagation() + } + } + } + + fn click( + editor: &mut Editor, + event: &ClickEvent, + position_map: &PositionMap, + window: &mut Window, + cx: &mut Context, + ) { + let text_hitbox = &position_map.text_hitbox; + let pending_nonempty_selections = editor.has_pending_nonempty_selection(); + + let hovered_link_modifier = Editor::is_cmd_or_ctrl_pressed(&event.modifiers(), cx); + let mouse_down_hovered_link_modifier = if let ClickEvent::Mouse(mouse_event) = event { + Editor::is_cmd_or_ctrl_pressed(&mouse_event.down.modifiers, cx) + } else { + true + }; + + if let Some(mouse_position) = event.mouse_position() + && !pending_nonempty_selections + && hovered_link_modifier + && mouse_down_hovered_link_modifier + && text_hitbox.is_hovered(window) + && !matches!( + editor.selection_drag_state, + SelectionDragState::Dragging { .. } + ) + { + let point = position_map.point_for_position(mouse_position); + editor.handle_click_hovered_link(point, event.modifiers(), window, cx); + editor.selection_drag_state = SelectionDragState::None; + + cx.stop_propagation(); + } + } + + fn pressure_click( + editor: &mut Editor, + event: &MousePressureEvent, + position_map: &PositionMap, + window: &mut Window, + cx: &mut Context, + ) { + let text_hitbox = &position_map.text_hitbox; + let force_click_possible = + matches!(editor.prev_pressure_stage, Some(PressureStage::Normal)) + && event.stage == PressureStage::Force; + + editor.prev_pressure_stage = Some(event.stage); + + if force_click_possible && text_hitbox.is_hovered(window) { + let point = position_map.point_for_position(event.position); + editor.handle_click_hovered_link(point, event.modifiers, window, cx); + editor.selection_drag_state = SelectionDragState::None; + cx.stop_propagation(); + } + } + + fn mouse_dragged( + editor: &mut Editor, + event: &MouseMoveEvent, + position_map: &PositionMap, + window: &mut Window, + cx: &mut Context, + ) { + if !editor.has_pending_selection() + && matches!(editor.selection_drag_state, SelectionDragState::None) + { + return; + } + + let point_for_position = position_map.point_for_position(event.position); + let text_hitbox = &position_map.text_hitbox; + + let scroll_delta = { + let text_bounds = text_hitbox.bounds; + let mut scroll_delta = gpui::Point::::default(); + let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0); + let top = text_bounds.origin.y + vertical_margin; + let bottom = text_bounds.bottom_left().y - vertical_margin; + if event.position.y < top { + scroll_delta.y = -scale_vertical_mouse_autoscroll_delta(top - event.position.y); + } + if event.position.y > bottom { + scroll_delta.y = scale_vertical_mouse_autoscroll_delta(event.position.y - bottom); + } + + // We need horizontal width of text + let style = editor.style.clone().unwrap_or_default(); + let font_id = window.text_system().resolve_font(&style.text.font()); + let font_size = style.text.font_size.to_pixels(window.rem_size()); + let em_width = window + .text_system() + .em_width(font_id, font_size) + .unwrap_or(font_size); + + let scroll_margin_x = EditorSettings::get_global(cx).horizontal_scroll_margin; + + let scroll_space: Pixels = scroll_margin_x * em_width; + + let left = text_bounds.origin.x + scroll_space; + let right = text_bounds.top_right().x - scroll_space; + + if event.position.x < left { + scroll_delta.x = -scale_horizontal_mouse_autoscroll_delta(left - event.position.x); + } + if event.position.x > right { + scroll_delta.x = scale_horizontal_mouse_autoscroll_delta(event.position.x - right); + } + scroll_delta + }; + + if !editor.has_pending_selection() { + let drop_anchor = position_map + .snapshot + .display_point_to_anchor(point_for_position.nearest_valid, Bias::Left); + match editor.selection_drag_state { + SelectionDragState::Dragging { + ref mut drop_cursor, + ref mut hide_drop_cursor, + .. + } => { + drop_cursor.start = drop_anchor; + drop_cursor.end = drop_anchor; + *hide_drop_cursor = !text_hitbox.is_hovered(window); + editor.apply_scroll_delta(scroll_delta, window, cx); + cx.notify(); + } + SelectionDragState::ReadyToDrag { + ref selection, + ref click_position, + ref mouse_down_time, + } => { + let drag_and_drop_delay = Duration::from_millis( + EditorSettings::get_global(cx) + .drag_and_drop_selection + .delay + .0, + ); + if mouse_down_time.elapsed() >= drag_and_drop_delay { + let drop_cursor = Selection { + id: post_inc(&mut editor.selections.next_selection_id()), + start: drop_anchor, + end: drop_anchor, + reversed: false, + goal: SelectionGoal::None, + }; + editor.selection_drag_state = SelectionDragState::Dragging { + selection: selection.clone(), + drop_cursor, + hide_drop_cursor: false, + }; + editor.apply_scroll_delta(scroll_delta, window, cx); + cx.notify(); + } else { + let click_point = position_map.point_for_position(*click_position); + editor.selection_drag_state = SelectionDragState::None; + editor.select( + SelectPhase::Begin { + position: click_point.nearest_valid, + add: false, + click_count: 1, + }, + window, + cx, + ); + editor.select( + SelectPhase::Update { + position: point_for_position.nearest_valid, + goal_column: point_for_position.exact_unclipped.column(), + scroll_delta, + }, + window, + cx, + ); + } + } + _ => {} + } + } else { + editor.select( + SelectPhase::Update { + position: point_for_position.nearest_valid, + goal_column: point_for_position.exact_unclipped.column(), + scroll_delta, + }, + window, + cx, + ); + } + } + + fn update_visible_cursor( + editor: &mut Editor, + point: DisplayPoint, + position_map: &PositionMap, + window: &mut Window, + cx: &mut Context, + ) { + let snapshot = &position_map.snapshot; + let Some(hub) = editor.collaboration_hub() else { + return; + }; + let start = snapshot.display_snapshot.clip_point( + DisplayPoint::new(point.row(), point.column().saturating_sub(1)), + Bias::Left, + ); + let end = snapshot.display_snapshot.clip_point( + DisplayPoint::new( + point.row(), + (point.column() + 1).min(snapshot.line_len(point.row())), + ), + Bias::Right, + ); + + let range = snapshot + .buffer_snapshot() + .anchor_before(start.to_point(&snapshot.display_snapshot)) + ..snapshot + .buffer_snapshot() + .anchor_after(end.to_point(&snapshot.display_snapshot)); + + let Some(selection) = snapshot.remote_selections_in_range(&range, hub, cx).next() else { + return; + }; + let key = HoveredCursor { + replica_id: selection.replica_id, + selection_id: selection.selection.id, + }; + editor.hovered_cursors.insert( + key.clone(), + cx.spawn_in(window, async move |editor, cx| { + cx.background_executor().timer(CURSORS_VISIBLE_FOR).await; + editor + .update(cx, |editor, cx| { + editor.hovered_cursors.remove(&key); + cx.notify(); + }) + .ok(); + }), + ); + cx.notify() + } +} + +fn scale_vertical_mouse_autoscroll_delta(delta: Pixels) -> f32 { + (delta.pow(1.2) / 100.0).min(px(3.0)).into() +} + +fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 { + (delta.pow(1.2) / 300.0).into() +}