diff --git a/crates/diagnostics/src/grouped_diagnostics.rs b/crates/diagnostics/src/grouped_diagnostics.rs index 99c6651399..ac8d9ecea9 100644 --- a/crates/diagnostics/src/grouped_diagnostics.rs +++ b/crates/diagnostics/src/grouped_diagnostics.rs @@ -319,7 +319,7 @@ impl GroupedDiagnosticsEditor { || server_to_update.map_or(false, |to_update| *server_id != to_update) }); - // TODO kb change selections as in the old panel, to the next primary diagnostics + // TODO change selections as in the old panel, to the next primary diagnostics // TODO make [shift-]f8 to work, jump to the next block group let _was_empty = self.path_states.is_empty(); let path_ix = match self.path_states.binary_search_by(|probe| { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d6bfa95b86..577ac9daf4 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -78,7 +78,7 @@ use gpui::{ use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; use hunk_diff::ExpandedHunks; -pub(crate) use hunk_diff::HunkToExpand; +pub(crate) use hunk_diff::HoveredHunk; use indent_guides::ActiveIndentGuidesState; use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; pub use inline_completion_provider::*; @@ -2873,7 +2873,10 @@ impl Editor { } pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - self.clear_expanded_diff_hunks(cx); + if self.clear_clicked_diff_hunks(cx) { + cx.notify(); + return; + } if self.dismiss_menus_and_popups(true, cx) { return; } @@ -2908,6 +2911,10 @@ impl Editor { return true; } + if self.mouse_context_menu.take().is_some() { + return true; + } + if self.discard_inline_completion(should_report_inline_completion_event, cx) { return true; } @@ -5125,6 +5132,23 @@ impl Editor { })) } + fn render_close_hunk_diff_button( + &self, + hunk: HoveredHunk, + row: DisplayRow, + cx: &mut ViewContext, + ) -> IconButton { + IconButton::new( + ("close_hunk_diff_indicator", row.0 as usize), + ui::IconName::Close, + ) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .tooltip(|cx| Tooltip::for_action("Close hunk diff", &ToggleHunkDiff, cx)) + .on_click(cx.listener(move |editor, _e, cx| editor.toggle_hovered_hunk(&hunk, cx))) + } + pub fn context_menu_visible(&self) -> bool { self.context_menu .read() @@ -5879,22 +5903,7 @@ impl Editor { let revert_changes = self.gather_revert_changes(&self.selections.disjoint_anchors(), cx); if !revert_changes.is_empty() { self.transact(cx, |editor, cx| { - editor.buffer().update(cx, |multi_buffer, cx| { - for (buffer_id, changes) in revert_changes { - if let Some(buffer) = multi_buffer.buffer(buffer_id) { - buffer.update(cx, |buffer, cx| { - buffer.edit( - changes.into_iter().map(|(range, text)| { - (range, text.to_string().map(Arc::::from)) - }), - None, - cx, - ); - }); - } - } - }); - editor.change_selections(None, cx, |selections| selections.refresh()); + editor.revert(revert_changes, cx); }); } } @@ -5924,22 +5933,20 @@ impl Editor { cx: &mut ViewContext<'_, Editor>, ) -> HashMap, Rope)>> { let mut revert_changes = HashMap::default(); - self.buffer.update(cx, |multi_buffer, cx| { - let multi_buffer_snapshot = multi_buffer.snapshot(cx); - for hunk in hunks_for_selections(&multi_buffer_snapshot, selections) { - Self::prepare_revert_change(&mut revert_changes, &multi_buffer, &hunk, cx); - } - }); + let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); + for hunk in hunks_for_selections(&multi_buffer_snapshot, selections) { + Self::prepare_revert_change(&mut revert_changes, self.buffer(), &hunk, cx); + } revert_changes } - fn prepare_revert_change( + pub fn prepare_revert_change( revert_changes: &mut HashMap, Rope)>>, - multi_buffer: &MultiBuffer, + multi_buffer: &Model, hunk: &DiffHunk, - cx: &mut AppContext, + cx: &AppContext, ) -> Option<()> { - let buffer = multi_buffer.buffer(hunk.buffer_id)?; + let buffer = multi_buffer.read(cx).buffer(hunk.buffer_id)?; let buffer = buffer.read(cx); let original_text = buffer.diff_base()?.slice(hunk.diff_base_byte_range.clone()); let buffer_snapshot = buffer.snapshot(); @@ -11737,15 +11744,81 @@ impl Editor { pub fn file_header_size(&self) -> u8 { self.file_header_size } + + pub fn revert( + &mut self, + revert_changes: HashMap, Rope)>>, + cx: &mut ViewContext, + ) { + self.buffer().update(cx, |multi_buffer, cx| { + for (buffer_id, changes) in revert_changes { + if let Some(buffer) = multi_buffer.buffer(buffer_id) { + buffer.update(cx, |buffer, cx| { + buffer.edit( + changes.into_iter().map(|(range, text)| { + (range, text.to_string().map(Arc::::from)) + }), + None, + cx, + ); + }); + } + } + }); + self.change_selections(None, cx, |selections| selections.refresh()); + } + + pub fn to_pixel_point( + &mut self, + source: multi_buffer::Anchor, + editor_snapshot: &EditorSnapshot, + cx: &mut ViewContext, + ) -> Option> { + let text_layout_details = self.text_layout_details(cx); + let line_height = text_layout_details + .editor_style + .text + .line_height_in_pixels(cx.rem_size()); + let source_point = source.to_display_point(editor_snapshot); + let first_visible_line = text_layout_details + .scroll_anchor + .anchor + .to_display_point(editor_snapshot); + if first_visible_line > source_point { + return None; + } + let source_x = editor_snapshot.x_for_display_point(source_point, &text_layout_details); + let source_y = line_height + * ((source_point.row() - first_visible_line.row()).0 as f32 + - text_layout_details.scroll_anchor.offset.y); + Some(gpui::Point::new(source_x, source_y)) + } + + pub fn display_to_pixel_point( + &mut self, + source: DisplayPoint, + editor_snapshot: &EditorSnapshot, + cx: &mut ViewContext, + ) -> Option> { + let line_height = self.style()?.text.line_height_in_pixels(cx.rem_size()); + let text_layout_details = self.text_layout_details(cx); + let first_visible_line = text_layout_details + .scroll_anchor + .anchor + .to_display_point(editor_snapshot); + if first_visible_line > source { + return None; + } + let source_x = editor_snapshot.x_for_display_point(source, &text_layout_details); + let source_y = line_height * (source.row() - first_visible_line.row()).0 as f32; + Some(gpui::Point::new(source_x, source_y)) + } } fn hunks_for_selections( multi_buffer_snapshot: &MultiBufferSnapshot, selections: &[Selection], ) -> Vec> { - let mut hunks = Vec::with_capacity(selections.len()); - let mut processed_buffer_rows: HashMap>> = - HashMap::default(); let buffer_rows_for_selections = selections.iter().map(|selection| { let head = selection.head(); let tail = selection.tail(); @@ -11758,7 +11831,17 @@ fn hunks_for_selections( } }); - for selected_multi_buffer_rows in buffer_rows_for_selections { + hunks_for_rows(buffer_rows_for_selections, multi_buffer_snapshot) +} + +pub fn hunks_for_rows( + rows: impl Iterator>, + multi_buffer_snapshot: &MultiBufferSnapshot, +) -> Vec> { + let mut hunks = Vec::new(); + let mut processed_buffer_rows: HashMap>> = + HashMap::default(); + for selected_multi_buffer_rows in rows { let query_rows = selected_multi_buffer_rows.start..selected_multi_buffer_rows.end.next_row(); for hunk in multi_buffer_snapshot.git_diff_hunks_in_range(query_rows.clone()) { @@ -12968,8 +13051,13 @@ pub(crate) fn split_words(text: &str) -> impl std::iter::Iterator + }) } -pub trait RangeToAnchorExt { +pub trait RangeToAnchorExt: Sized { fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range; + + fn to_display_points(self, snapshot: &EditorSnapshot) -> Range { + let anchor_range = self.to_anchors(&snapshot.buffer_snapshot); + anchor_range.start.to_display_point(&snapshot)..anchor_range.end.to_display_point(&snapshot) + } } impl RangeToAnchorExt for Range { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index decbe56994..fdea62b229 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1,4 +1,7 @@ use crate::editor_settings::ScrollBeyondLastLine; +use crate::hunk_diff::ExpandedHunk; +use crate::mouse_context_menu::MenuPosition; +use crate::RangeToAnchorExt; use crate::TransformBlockId; use crate::{ blame_entry_tooltip::{blame_entry_relative_timestamp, BlameEntryTooltip}, @@ -21,13 +24,14 @@ use crate::{ scroll::scroll_amount::ScrollAmount, CodeActionsMenu, CursorShape, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, - ExpandExcerpts, GutterDimensions, HalfPageDown, HalfPageUp, HoveredCursor, HunkToExpand, + ExpandExcerpts, GutterDimensions, HalfPageDown, HalfPageUp, HoveredCursor, HoveredHunk, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN, }; use client::ParticipantIndex; use collections::{BTreeMap, HashMap}; use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid}; +use gpui::Subscription; use gpui::{ anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg, transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClipboardItem, @@ -63,6 +67,7 @@ use sum_tree::Bias; use theme::{ActiveTheme, PlayerColor}; use ui::prelude::*; use ui::{h_flex, ButtonLike, ButtonStyle, ContextMenu, Tooltip}; +use util::RangeExt; use util::ResultExt; use workspace::{item::Item, Workspace}; @@ -442,7 +447,7 @@ impl EditorElement { fn mouse_left_down( editor: &mut Editor, event: &MouseDownEvent, - hovered_hunk: Option<&HunkToExpand>, + hovered_hunk: Option, position_map: &PositionMap, text_hitbox: &Hitbox, gutter_hitbox: &Hitbox, @@ -456,7 +461,28 @@ impl EditorElement { let mut modifiers = event.modifiers; if let Some(hovered_hunk) = hovered_hunk { - editor.expand_diff_hunk(None, hovered_hunk, cx); + if modifiers.control || modifiers.platform { + editor.toggle_hovered_hunk(&hovered_hunk, cx); + } else { + let display_range = hovered_hunk + .multi_buffer_range + .clone() + .to_display_points(&position_map.snapshot); + let hunk_bounds = Self::diff_hunk_bounds( + &position_map.snapshot, + position_map.line_height, + gutter_hitbox.bounds, + &DisplayDiffHunk::Unfolded { + diff_base_byte_range: hovered_hunk.diff_base_byte_range.clone(), + display_row_range: display_range.start.row()..display_range.end.row(), + multi_buffer_range: hovered_hunk.multi_buffer_range.clone(), + status: hovered_hunk.status, + }, + ); + if hunk_bounds.contains(&event.position) { + editor.open_hunk_context_menu(hovered_hunk, event.position, cx); + } + } cx.notify(); return; } else if gutter_hitbox.is_hovered(cx) { @@ -1245,47 +1271,18 @@ impl EditorElement { .row, ); - let expanded_hunk_display_rows = self.editor.update(cx, |editor, _| { - editor - .expanded_hunks - .hunks(false) - .map(|expanded_hunk| { - let start_row = expanded_hunk - .hunk_range - .start - .to_display_point(snapshot) - .row(); - let end_row = expanded_hunk - .hunk_range - .end - .to_display_point(snapshot) - .row(); - (start_row, end_row) - }) - .collect::>() - }); - let git_gutter_setting = ProjectSettings::get_global(cx) .git .git_gutter .unwrap_or_default(); - buffer_snapshot + let display_hunks = buffer_snapshot .git_diff_hunks_in_range(buffer_start_row..buffer_end_row) .map(|hunk| diff_hunk_to_display(&hunk, snapshot)) .dedup() .map(|hunk| match git_gutter_setting { GitGutterSetting::TrackedFiles => { - let hitbox = if let DisplayDiffHunk::Unfolded { - display_row_range, .. - } = &hunk - { - let was_expanded = expanded_hunk_display_rows - .get(&display_row_range.start) - .map(|expanded_end_row| expanded_end_row == &display_row_range.end) - .unwrap_or(false); - if was_expanded { - None - } else { + let hitbox = match hunk { + DisplayDiffHunk::Unfolded { .. } => { let hunk_bounds = Self::diff_hunk_bounds( &snapshot, line_height, @@ -1294,14 +1291,14 @@ impl EditorElement { ); Some(cx.insert_hitbox(hunk_bounds, true)) } - } else { - None + DisplayDiffHunk::Folded { .. } => None, }; (hunk, hitbox) } GitGutterSetting::Hide => (hunk, None), }) - .collect() + .collect(); + display_hunks } #[allow(clippy::too_many_arguments)] @@ -1369,9 +1366,7 @@ impl EditorElement { }; let absolute_offset = point(start_x, start_y); - let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); - - element.prepaint_as_root(absolute_offset, available_space, cx); + element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx); Some(element) } @@ -2472,8 +2467,7 @@ impl EditorElement { return false; }; - let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); - let context_menu_size = context_menu.layout_as_root(available_space, cx); + let context_menu_size = context_menu.layout_as_root(AvailableSpace::min_size(), cx); let (x, y) = match position { crate::ContextMenuOrigin::EditorPoint(point) => { @@ -2510,19 +2504,72 @@ impl EditorElement { true } - fn layout_mouse_context_menu(&self, cx: &mut WindowContext) -> Option { - let mouse_context_menu = self.editor.read(cx).mouse_context_menu.as_ref()?; - let mut element = deferred( - anchored() - .position(mouse_context_menu.position) - .child(mouse_context_menu.context_menu.clone()) - .anchor(AnchorCorner::TopLeft) - .snap_to_window(), - ) - .with_priority(1) - .into_any(); + fn layout_mouse_context_menu( + &self, + editor_snapshot: &EditorSnapshot, + visible_range: Range, + cx: &mut WindowContext, + ) -> 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, + cx, + )?; + let visible_end_point = editor.display_to_pixel_point( + DisplayPoint::new(visible_range.end, 0), + editor_snapshot, + cx, + )?; - element.prepaint_as_root(gpui::Point::default(), AvailableSpace::min_size(), 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_x, + offset_y, + } => { + let source_display_point = source.to_display_point(editor_snapshot); + let mut source_point = editor.to_pixel_point(source, editor_snapshot, cx)?; + source_point.x += offset_x; + source_point.y += offset_y; + (Some(source_display_point), source_point) + } + }; + + let source_included = source_display_point.map_or(true, |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 mut element = self.editor.update(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(AnchorCorner::TopLeft) + .snap_to_window(), + ) + .with_priority(1) + .into_any(), + ) + })?; + + element.prepaint_as_root(position, AvailableSpace::min_size(), cx); Some(element) } @@ -2569,8 +2616,6 @@ impl EditorElement { return; }; - let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); - // This is safe because we check on layout whether the required row is available let hovered_row_layout = &line_layouts[position.row().minus(visible_display_row_range.start) as usize]; @@ -2584,7 +2629,7 @@ impl EditorElement { let mut overall_height = Pixels::ZERO; let mut measured_hover_popovers = Vec::new(); for mut hover_popover in hover_popovers { - let size = hover_popover.layout_as_root(available_space, cx); + let size = hover_popover.layout_as_root(AvailableSpace::min_size(), cx); let horizontal_offset = (text_hitbox.upper_right().x - (hovered_point.x + size.width)).min(Pixels::ZERO); @@ -2953,7 +2998,7 @@ impl EditorElement { } } - fn paint_diff_hunks(layout: &EditorLayout, cx: &mut WindowContext) { + fn paint_diff_hunks(layout: &mut EditorLayout, cx: &mut WindowContext) { if layout.display_hunks.is_empty() { return; } @@ -3018,7 +3063,7 @@ impl EditorElement { fn diff_hunk_bounds( snapshot: &EditorSnapshot, line_height: Pixels, - bounds: Bounds, + gutter_bounds: Bounds, hunk: &DisplayDiffHunk, ) -> Bounds { let scroll_position = snapshot.scroll_position(); @@ -3030,7 +3075,7 @@ impl EditorElement { let end_y = start_y + line_height; let width = 0.275 * line_height; - let highlight_origin = bounds.origin + point(px(0.), start_y); + let highlight_origin = gutter_bounds.origin + point(px(0.), start_y); let highlight_size = size(width, end_y - start_y); Bounds::new(highlight_origin, highlight_size) } @@ -3063,7 +3108,7 @@ impl EditorElement { let end_y = end_row_in_current_excerpt.as_f32() * line_height - scroll_top; let width = 0.275 * line_height; - let highlight_origin = bounds.origin + point(px(0.), start_y); + let highlight_origin = gutter_bounds.origin + point(px(0.), start_y); let highlight_size = size(width, end_y - start_y); Bounds::new(highlight_origin, highlight_size) } @@ -3075,7 +3120,7 @@ impl EditorElement { let end_y = start_y + line_height; let width = 0.35 * line_height; - let highlight_origin = bounds.origin + point(px(0.), start_y); + let highlight_origin = gutter_bounds.origin + point(px(0.), start_y); let highlight_size = size(width, end_y - start_y); Bounds::new(highlight_origin, highlight_size) } @@ -3091,8 +3136,11 @@ impl EditorElement { } }); - for test_indicators in layout.test_indicators.iter_mut() { - test_indicators.paint(cx); + for test_indicator in layout.test_indicators.iter_mut() { + test_indicator.paint(cx); + } + for close_indicator in layout.close_indicators.iter_mut() { + close_indicator.paint(cx); } if let Some(indicator) = layout.code_actions_indicator.as_mut() { @@ -3101,7 +3149,7 @@ impl EditorElement { }); } - fn paint_gutter_highlights(&self, layout: &EditorLayout, cx: &mut WindowContext) { + fn paint_gutter_highlights(&self, layout: &mut EditorLayout, cx: &mut WindowContext) { for (_, hunk_hitbox) in &layout.display_hunks { if let Some(hunk_hitbox) = hunk_hitbox { cx.set_cursor_style(CursorStyle::PointingHand, hunk_hitbox); @@ -3757,7 +3805,7 @@ impl EditorElement { fn paint_mouse_listeners( &mut self, layout: &EditorLayout, - hovered_hunk: Option, + hovered_hunk: Option, cx: &mut WindowContext, ) { self.paint_scroll_wheel_listener(layout, cx); @@ -3775,7 +3823,7 @@ impl EditorElement { Self::mouse_left_down( editor, event, - hovered_hunk.as_ref(), + hovered_hunk.clone(), &position_map, &text_hitbox, &gutter_hitbox, @@ -3881,6 +3929,43 @@ impl EditorElement { + 1; self.column_pixels(digit_count, cx) } + + fn layout_hunk_diff_close_indicators( + &self, + expanded_hunks_by_rows: HashMap, + line_height: Pixels, + scroll_pixel_position: gpui::Point, + gutter_dimensions: &GutterDimensions, + gutter_hitbox: &Hitbox, + cx: &mut WindowContext, + ) -> Vec { + self.editor.update(cx, |editor, cx| { + expanded_hunks_by_rows + .into_iter() + .map(|(display_row, hunk)| { + let button = editor.render_close_hunk_diff_button( + HoveredHunk { + multi_buffer_range: hunk.hunk_range, + status: hunk.status, + diff_base_byte_range: hunk.diff_base_byte_range, + }, + display_row, + cx, + ); + + prepaint_gutter_button( + button, + display_row, + line_height, + gutter_dimensions, + scroll_pixel_position, + gutter_hitbox, + cx, + ) + }) + .collect() + }) + } } fn prepaint_gutter_button( @@ -4037,19 +4122,24 @@ fn deploy_blame_entry_context_menu( position: gpui::Point, cx: &mut WindowContext<'_>, ) { - let context_menu = ContextMenu::build(cx, move |this, _| { + let context_menu = ContextMenu::build(cx, move |menu, _| { let sha = format!("{}", blame_entry.sha); - this.entry("Copy commit SHA", None, move |cx| { - cx.write_to_clipboard(ClipboardItem::new(sha.clone())); - }) - .when_some( - details.and_then(|details| details.permalink.clone()), - |this, url| this.entry("Open permalink", None, move |cx| cx.open_url(url.as_str())), - ) + menu.on_blur_subscription(Subscription::new(|| {})) + .entry("Copy commit SHA", None, move |cx| { + cx.write_to_clipboard(ClipboardItem::new(sha.clone())); + }) + .when_some( + details.and_then(|details| details.permalink.clone()), + |this, url| this.entry("Open permalink", None, move |cx| cx.open_url(url.as_str())), + ) }); editor.update(cx, move |editor, cx| { - editor.mouse_context_menu = Some(MouseContextMenu::new(position, context_menu, cx)); + editor.mouse_context_menu = Some(MouseContextMenu::pinned_to_screen( + position, + context_menu, + cx, + )); cx.notify(); }); } @@ -5087,6 +5177,22 @@ impl Element for EditorElement { let gutter_settings = EditorSettings::get_global(cx).gutter; + let expanded_add_hunks_by_rows = self.editor.update(cx, |editor, _| { + editor + .expanded_hunks + .hunks(false) + .filter(|hunk| hunk.status == DiffHunkStatus::Added) + .map(|expanded_hunk| { + let start_row = expanded_hunk + .hunk_range + .start + .to_display_point(&snapshot) + .row(); + (start_row, expanded_hunk.clone()) + }) + .collect::>() + }); + let mut _context_menu_visible = false; let mut code_actions_indicator = None; if let Some(newest_selection_head) = newest_selection_head { @@ -5110,25 +5216,34 @@ impl Element for EditorElement { if show_code_actions { let newest_selection_point = newest_selection_head.to_point(&snapshot.display_snapshot); - let buffer = snapshot.buffer_snapshot.buffer_line_for_row( - MultiBufferRow(newest_selection_point.row), - ); - if let Some((buffer, range)) = buffer { - let buffer_id = buffer.remote_id(); - let row = range.start.row; - let has_test_indicator = - self.editor.read(cx).tasks.contains_key(&(buffer_id, row)); + let newest_selection_display_row = + newest_selection_point.to_display_point(&snapshot).row(); + if !expanded_add_hunks_by_rows + .contains_key(&newest_selection_display_row) + { + let buffer = snapshot.buffer_snapshot.buffer_line_for_row( + MultiBufferRow(newest_selection_point.row), + ); + if let Some((buffer, range)) = buffer { + let buffer_id = buffer.remote_id(); + let row = range.start.row; + let has_test_indicator = self + .editor + .read(cx) + .tasks + .contains_key(&(buffer_id, row)); - if !has_test_indicator { - code_actions_indicator = self - .layout_code_actions_indicator( - line_height, - newest_selection_head, - scroll_pixel_position, - &gutter_dimensions, - &gutter_hitbox, - cx, - ); + if !has_test_indicator { + code_actions_indicator = self + .layout_code_actions_indicator( + line_height, + newest_selection_head, + scroll_pixel_position, + &gutter_dimensions, + &gutter_hitbox, + cx, + ); + } } } } @@ -5145,9 +5260,18 @@ impl Element for EditorElement { cx, ) } else { - vec![] + Vec::new() }; + let close_indicators = self.layout_hunk_diff_close_indicators( + expanded_add_hunks_by_rows, + line_height, + scroll_pixel_position, + &gutter_dimensions, + &gutter_hitbox, + cx, + ); + self.layout_signature_help( &hitbox, content_origin, @@ -5175,7 +5299,8 @@ impl Element for EditorElement { ); } - let mouse_context_menu = self.layout_mouse_context_menu(cx); + let mouse_context_menu = + self.layout_mouse_context_menu(&snapshot, start_row..end_row, cx); cx.with_element_namespace("gutter_fold_toggles", |cx| { self.prepaint_gutter_fold_toggles( @@ -5240,6 +5365,7 @@ impl Element for EditorElement { text_hitbox, gutter_hitbox, gutter_dimensions, + display_hunks, content_origin, scrollbar_layout, active_rows, @@ -5249,7 +5375,6 @@ impl Element for EditorElement { redacted_ranges, line_elements, line_numbers, - display_hunks, blamed_display_rows, inline_blame, blocks, @@ -5258,6 +5383,7 @@ impl Element for EditorElement { selections, mouse_context_menu, test_indicators, + close_indicators, code_actions_indicator, gutter_fold_toggles, crease_trailers, @@ -5310,7 +5436,7 @@ impl Element for EditorElement { .map(|hitbox| hitbox.contains(&mouse_position)) .unwrap_or(false) { - Some(HunkToExpand { + Some(HoveredHunk { status: *status, multi_buffer_range: multi_buffer_range.clone(), diff_base_byte_range: diff_base_byte_range.clone(), @@ -5390,6 +5516,7 @@ pub struct EditorLayout { selections: Vec<(PlayerColor, Vec)>, code_actions_indicator: Option, test_indicators: Vec, + close_indicators: Vec, gutter_fold_toggles: Vec>, crease_trailers: Vec>, mouse_context_menu: Option, @@ -5705,11 +5832,7 @@ impl CursorLayout { .child(cursor_name.string.clone()) .into_any_element(); - name_element.prepaint_as_root( - name_origin, - size(AvailableSpace::MinContent, AvailableSpace::MinContent), - cx, - ); + name_element.prepaint_as_root(name_origin, AvailableSpace::min_size(), cx); self.cursor_name = Some(name_element); } diff --git a/crates/editor/src/hunk_diff.rs b/crates/editor/src/hunk_diff.rs index 1fe0804793..4edcf20241 100644 --- a/crates/editor/src/hunk_diff.rs +++ b/crates/editor/src/hunk_diff.rs @@ -5,28 +5,31 @@ use std::{ use collections::{hash_map, HashMap, HashSet}; use git::diff::{DiffHunk, DiffHunkStatus}; -use gpui::{AppContext, Hsla, Model, Task, View}; +use gpui::{Action, AppContext, Hsla, Model, MouseButton, Subscription, Task, View}; use language::Buffer; use multi_buffer::{ - Anchor, ExcerptRange, MultiBuffer, MultiBufferRow, MultiBufferSnapshot, ToPoint, + Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferRow, MultiBufferSnapshot, ToPoint, }; use settings::SettingsStore; use text::{BufferId, Point}; use ui::{ - div, ActiveTheme, Context as _, IntoElement, ParentElement, Styled, ViewContext, VisualContext, + h_flex, v_flex, ActiveTheme, Context as _, ContextMenu, InteractiveElement, IntoElement, + ParentElement, Pixels, Styled, ViewContext, VisualContext, }; use util::{debug_panic, RangeExt}; use crate::{ editor_settings::CurrentLineHighlight, git::{diff_hunk_to_display, DisplayDiffHunk}, - hunk_status, hunks_for_selections, BlockDisposition, BlockId, BlockProperties, BlockStyle, - DiffRowHighlight, Editor, EditorSnapshot, ExpandAllHunkDiffs, RangeToAnchorExt, - RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff, + hunk_status, hunks_for_selections, + mouse_context_menu::MouseContextMenu, + BlockDisposition, BlockId, BlockProperties, BlockStyle, DiffRowHighlight, Editor, + EditorSnapshot, ExpandAllHunkDiffs, RangeToAnchorExt, RevertSelectedHunks, ToDisplayPoint, + ToggleHunkDiff, }; #[derive(Debug, Clone)] -pub(super) struct HunkToExpand { +pub(super) struct HoveredHunk { pub multi_buffer_range: Range, pub status: DiffHunkStatus, pub diff_base_byte_range: Range, @@ -63,6 +66,123 @@ pub(super) struct ExpandedHunk { } impl Editor { + pub(super) fn open_hunk_context_menu( + &mut self, + hovered_hunk: HoveredHunk, + clicked_point: gpui::Point, + cx: &mut ViewContext, + ) { + let focus_handle = self.focus_handle.clone(); + let expanded = self + .expanded_hunks + .hunks(false) + .any(|expanded_hunk| expanded_hunk.hunk_range == hovered_hunk.multi_buffer_range); + let editor_handle = cx.view().clone(); + let editor_snapshot = self.snapshot(cx); + let start_point = self + .to_pixel_point(hovered_hunk.multi_buffer_range.start, &editor_snapshot, cx) + .unwrap_or(clicked_point); + let end_point = self + .to_pixel_point(hovered_hunk.multi_buffer_range.start, &editor_snapshot, cx) + .unwrap_or(clicked_point); + let norm = + |a: gpui::Point, b: gpui::Point| (a.x - b.x).abs() + (a.y - b.y).abs(); + let closest_source = if norm(start_point, clicked_point) < norm(end_point, clicked_point) { + hovered_hunk.multi_buffer_range.start + } else { + hovered_hunk.multi_buffer_range.end + }; + + self.mouse_context_menu = MouseContextMenu::pinned_to_editor( + self, + closest_source, + clicked_point, + ContextMenu::build(cx, move |menu, _| { + menu.on_blur_subscription(Subscription::new(|| {})) + .context(focus_handle) + .entry( + if expanded { + "Collapse Hunk" + } else { + "Expand Hunk" + }, + Some(ToggleHunkDiff.boxed_clone()), + { + let editor = editor_handle.clone(); + let hunk = hovered_hunk.clone(); + move |cx| { + editor.update(cx, |editor, cx| { + editor.toggle_hovered_hunk(&hunk, cx); + }); + } + }, + ) + .entry("Revert Hunk", Some(RevertSelectedHunks.boxed_clone()), { + let editor = editor_handle.clone(); + let hunk = hovered_hunk.clone(); + move |cx| { + let multi_buffer = editor.read(cx).buffer().clone(); + let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx); + let mut revert_changes = HashMap::default(); + if let Some(hunk) = + crate::hunk_diff::to_diff_hunk(&hunk, &multi_buffer_snapshot) + { + Editor::prepare_revert_change( + &mut revert_changes, + &multi_buffer, + &hunk, + cx, + ); + } + if !revert_changes.is_empty() { + editor.update(cx, |editor, cx| editor.revert(revert_changes, cx)); + } + } + }) + .entry("Revert File", None, { + let editor = editor_handle.clone(); + move |cx| { + let mut revert_changes = HashMap::default(); + let multi_buffer = editor.read(cx).buffer().clone(); + let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx); + for hunk in crate::hunks_for_rows( + Some(MultiBufferRow(0)..multi_buffer_snapshot.max_buffer_row()) + .into_iter(), + &multi_buffer_snapshot, + ) { + Editor::prepare_revert_change( + &mut revert_changes, + &multi_buffer, + &hunk, + cx, + ); + } + if !revert_changes.is_empty() { + editor.update(cx, |editor, cx| { + editor.transact(cx, |editor, cx| { + editor.revert(revert_changes, cx); + }); + }); + } + } + }) + }), + cx, + ) + } + + pub(super) fn toggle_hovered_hunk( + &mut self, + hovered_hunk: &HoveredHunk, + cx: &mut ViewContext, + ) { + let editor_snapshot = self.snapshot(cx); + if let Some(diff_hunk) = to_diff_hunk(hovered_hunk, &editor_snapshot.buffer_snapshot) { + self.toggle_hunks_expanded(vec![diff_hunk], cx); + self.change_selections(None, cx, |selections| selections.refresh()); + } + } + pub fn toggle_hunk_diff(&mut self, _: &ToggleHunkDiff, cx: &mut ViewContext) { let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx); let selections = self.selections.disjoint_anchors(); @@ -164,7 +284,7 @@ impl Editor { retain = false; break; } else { - hunks_to_expand.push(HunkToExpand { + hunks_to_expand.push(HoveredHunk { status, multi_buffer_range, diff_base_byte_range, @@ -182,7 +302,7 @@ impl Editor { let remaining_hunk_point_range = Point::new(remaining_hunk.associated_range.start.0, 0) ..Point::new(remaining_hunk.associated_range.end.0, 0); - hunks_to_expand.push(HunkToExpand { + hunks_to_expand.push(HoveredHunk { status: hunk_status(&remaining_hunk), multi_buffer_range: remaining_hunk_point_range .to_anchors(&snapshot.buffer_snapshot), @@ -215,7 +335,7 @@ impl Editor { pub(super) fn expand_diff_hunk( &mut self, diff_base_buffer: Option>, - hunk: &HunkToExpand, + hunk: &HoveredHunk, cx: &mut ViewContext<'_, Editor>, ) -> Option<()> { let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx); @@ -303,28 +423,58 @@ impl Editor { &mut self, diff_base_buffer: Model, deleted_text_height: u8, - hunk: &HunkToExpand, + hunk: &HoveredHunk, cx: &mut ViewContext<'_, Self>, ) -> Option { let deleted_hunk_color = deleted_hunk_color(cx); let (editor_height, editor_with_deleted_text) = editor_with_deleted_text(diff_base_buffer, deleted_hunk_color, hunk, cx); + let editor = cx.view().clone(); let editor_model = cx.model().clone(); + let hunk = hunk.clone(); let mut new_block_ids = self.insert_blocks( Some(BlockProperties { position: hunk.multi_buffer_range.start, height: editor_height.max(deleted_text_height), style: BlockStyle::Flex, + disposition: BlockDisposition::Above, render: Box::new(move |cx| { + let close_button = editor.update(cx.context, |editor, cx| { + let editor_snapshot = editor.snapshot(cx); + let hunk_start_row = hunk + .multi_buffer_range + .start + .to_display_point(&editor_snapshot) + .row(); + editor.render_close_hunk_diff_button(hunk.clone(), hunk_start_row, cx) + }); let gutter_dimensions = editor_model.read(cx).gutter_dimensions; - div() + let click_editor = editor.clone(); + h_flex() .bg(deleted_hunk_color) .size_full() - .pl(gutter_dimensions.full_width()) + .child( + v_flex() + .justify_center() + .max_w(gutter_dimensions.full_width()) + .min_w(gutter_dimensions.full_width()) + .size_full() + .on_mouse_down(MouseButton::Left, { + let click_hunk = hunk.clone(); + move |e, cx| { + let modifiers = e.modifiers; + if modifiers.control || modifiers.platform { + click_editor.update(cx, |editor, cx| { + editor.toggle_hovered_hunk(&click_hunk, cx); + }); + } + } + }) + .child(close_button), + ) .child(editor_with_deleted_text.clone()) .into_any_element() }), - disposition: BlockDisposition::Above, }), None, cx, @@ -339,16 +489,21 @@ impl Editor { } } - pub(super) fn clear_expanded_diff_hunks(&mut self, cx: &mut ViewContext<'_, Editor>) { + pub(super) fn clear_clicked_diff_hunks(&mut self, cx: &mut ViewContext<'_, Editor>) -> bool { self.expanded_hunks.hunk_update_tasks.clear(); + self.clear_row_highlights::(); let to_remove = self .expanded_hunks .hunks .drain(..) .filter_map(|expanded_hunk| expanded_hunk.block) - .collect(); - self.clear_row_highlights::(); - self.remove_blocks(to_remove, None, cx); + .collect::>(); + if to_remove.is_empty() { + false + } else { + self.remove_blocks(to_remove, None, cx); + true + } } pub(super) fn sync_expanded_diff_hunks( @@ -457,7 +612,7 @@ impl Editor { recalculated_hunks.next(); retain = true; } else { - hunks_to_reexpand.push(HunkToExpand { + hunks_to_reexpand.push(HoveredHunk { status, multi_buffer_range, diff_base_byte_range, @@ -522,6 +677,29 @@ impl Editor { } } +fn to_diff_hunk( + hovered_hunk: &HoveredHunk, + multi_buffer_snapshot: &MultiBufferSnapshot, +) -> Option> { + let buffer_id = hovered_hunk + .multi_buffer_range + .start + .buffer_id + .or_else(|| hovered_hunk.multi_buffer_range.end.buffer_id)?; + let buffer_range = hovered_hunk.multi_buffer_range.start.text_anchor + ..hovered_hunk.multi_buffer_range.end.text_anchor; + let point_range = hovered_hunk + .multi_buffer_range + .to_point(&multi_buffer_snapshot); + Some(DiffHunk { + associated_range: MultiBufferRow(point_range.start.row) + ..MultiBufferRow(point_range.end.row), + buffer_id, + buffer_range, + diff_base_byte_range: hovered_hunk.diff_base_byte_range.clone(), + }) +} + fn create_diff_base_buffer(buffer: &Model, cx: &mut AppContext) -> Option> { buffer .update(cx, |buffer, _| { @@ -555,7 +733,7 @@ fn deleted_hunk_color(cx: &AppContext) -> Hsla { fn editor_with_deleted_text( diff_base_buffer: Model, deleted_color: Hsla, - hunk: &HunkToExpand, + hunk: &HoveredHunk, cx: &mut ViewContext<'_, Editor>, ) -> (u8, View) { let parent_editor = cx.view().downgrade(); @@ -613,11 +791,12 @@ fn editor_with_deleted_text( } }), ]); + let parent_editor_for_reverts = parent_editor.clone(); let original_multi_buffer_range = hunk.multi_buffer_range.clone(); let diff_base_range = hunk.diff_base_byte_range.clone(); editor .register_action::(move |_, cx| { - parent_editor + parent_editor_for_reverts .update(cx, |editor, cx| { let Some((buffer, original_text)) = editor.buffer().update(cx, |buffer, cx| { @@ -645,6 +824,16 @@ fn editor_with_deleted_text( .ok(); }) .detach(); + let hunk = hunk.clone(); + editor + .register_action::(move |_, cx| { + parent_editor + .update(cx, |editor, cx| { + editor.toggle_hovered_hunk(&hunk, cx); + }) + .ok(); + }) + .detach(); editor }); diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index 1ad8040624..8b3f0cafc5 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -10,14 +10,62 @@ use gpui::prelude::FluentBuilder; use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext}; use workspace::OpenInTerminal; +pub enum MenuPosition { + /// When the editor is scrolled, the context menu stays on the exact + /// same position on the screen, never disappearing. + PinnedToScreen(Point), + /// When the editor is scrolled, the context menu follows the position it is associated with. + /// Disappears when the position is no longer visible. + PinnedToEditor { + source: multi_buffer::Anchor, + offset_x: Pixels, + offset_y: Pixels, + }, +} + pub struct MouseContextMenu { - pub(crate) position: Point, + pub(crate) position: MenuPosition, pub(crate) context_menu: View, _subscription: Subscription, } impl MouseContextMenu { - pub(crate) fn new( + pub(crate) fn pinned_to_editor( + editor: &mut Editor, + source: multi_buffer::Anchor, + position: Point, + context_menu: View, + cx: &mut ViewContext, + ) -> Option { + let context_menu_focus = context_menu.focus_handle(cx); + cx.focus(&context_menu_focus); + + let _subscription = cx.subscribe( + &context_menu, + move |editor, _, _event: &DismissEvent, cx| { + editor.mouse_context_menu.take(); + if context_menu_focus.contains_focused(cx) { + editor.focus(cx); + } + }, + ); + + let editor_snapshot = editor.snapshot(cx); + let source_point = editor.to_pixel_point(source, &editor_snapshot, cx)?; + let offset = position - source_point; + + Some(Self { + position: MenuPosition::PinnedToEditor { + source, + offset_x: offset.x, + offset_y: offset.y, + }, + context_menu, + _subscription, + }) + } + + pub(crate) fn pinned_to_screen( position: Point, context_menu: View, cx: &mut ViewContext, @@ -25,16 +73,18 @@ impl MouseContextMenu { let context_menu_focus = context_menu.focus_handle(cx); cx.focus(&context_menu_focus); - let _subscription = - cx.subscribe(&context_menu, move |this, _, _event: &DismissEvent, cx| { - this.mouse_context_menu.take(); + let _subscription = cx.subscribe( + &context_menu, + move |editor, _, _event: &DismissEvent, cx| { + editor.mouse_context_menu.take(); if context_menu_focus.contains_focused(cx) { - this.focus(cx); + editor.focus(cx); } - }); + }, + ); Self { - position, + position: MenuPosition::PinnedToScreen(position), context_menu, _subscription, } @@ -71,6 +121,8 @@ pub fn deploy_context_menu( return; } + let display_map = editor.selections.display_map(cx); + let source_anchor = display_map.display_point_to_anchor(point, text::Bias::Right); let context_menu = if let Some(custom) = editor.custom_context_menu.take() { let menu = custom(editor, point, cx); editor.custom_context_menu = Some(custom); @@ -98,6 +150,7 @@ pub fn deploy_context_menu( let focus = cx.focused(); ui::ContextMenu::build(cx, |menu, _cx| { let builder = menu + .on_blur_subscription(Subscription::new(|| {})) .action("Rename Symbol", Box::new(Rename)) .action("Go to Definition", Box::new(GoToDefinition)) .action("Go to Type Definition", Box::new(GoToTypeDefinition)) @@ -128,8 +181,9 @@ pub fn deploy_context_menu( } }) }; - let mouse_context_menu = MouseContextMenu::new(position, context_menu, cx); - editor.mouse_context_menu = Some(mouse_context_menu); + + editor.mouse_context_menu = + MouseContextMenu::pinned_to_editor(editor, source_anchor, position, context_menu, cx); cx.notify(); } diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 0c9b61f1f9..0ba5a79766 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -285,6 +285,11 @@ impl ContextMenu { cx.propagate() } } + + pub fn on_blur_subscription(mut self, new_subscription: Subscription) -> Self { + self._on_blur_subscription = new_subscription; + self + } } impl ContextMenuItem {