Detailed changes
@@ -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| {
@@ -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>) {
- 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<Self>,
+ ) -> 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::<str>::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<BufferId, Vec<(Range<text::Anchor>, 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<BufferId, Vec<(Range<text::Anchor>, Rope)>>,
- multi_buffer: &MultiBuffer,
+ multi_buffer: &Model<MultiBuffer>,
hunk: &DiffHunk<MultiBufferRow>,
- 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<BufferId, Vec<(Range<text::Anchor>, Rope)>>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ 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::<str>::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<Self>,
+ ) -> Option<gpui::Point<Pixels>> {
+ 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<Self>,
+ ) -> Option<gpui::Point<Pixels>> {
+ 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<Anchor>],
) -> Vec<DiffHunk<MultiBufferRow>> {
- let mut hunks = Vec::with_capacity(selections.len());
- let mut processed_buffer_rows: HashMap<BufferId, HashSet<Range<text::Anchor>>> =
- 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<Item = Range<MultiBufferRow>>,
+ multi_buffer_snapshot: &MultiBufferSnapshot,
+) -> Vec<DiffHunk<MultiBufferRow>> {
+ let mut hunks = Vec::new();
+ let mut processed_buffer_rows: HashMap<BufferId, HashSet<Range<text::Anchor>>> =
+ 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<Item = &str> +
})
}
-pub trait RangeToAnchorExt {
+pub trait RangeToAnchorExt: Sized {
fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range<Anchor>;
+
+ fn to_display_points(self, snapshot: &EditorSnapshot) -> Range<DisplayPoint> {
+ let anchor_range = self.to_anchors(&snapshot.buffer_snapshot);
+ anchor_range.start.to_display_point(&snapshot)..anchor_range.end.to_display_point(&snapshot)
+ }
}
impl<T: ToOffset> RangeToAnchorExt for Range<T> {
@@ -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<HoveredHunk>,
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::<HashMap<_, _>>()
- });
-
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<AnyElement> {
- 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<DisplayRow>,
+ cx: &mut WindowContext,
+ ) -> Option<AnyElement> {
+ 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,
+ )?;
+
+ 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(gpui::Point::default(), AvailableSpace::min_size(), cx);
+ 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<Pixels>,
+ gutter_bounds: Bounds<Pixels>,
hunk: &DisplayDiffHunk,
) -> Bounds<Pixels> {
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<HunkToExpand>,
+ hovered_hunk: Option<HoveredHunk>,
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<DisplayRow, ExpandedHunk>,
+ line_height: Pixels,
+ scroll_pixel_position: gpui::Point<Pixels>,
+ gutter_dimensions: &GutterDimensions,
+ gutter_hitbox: &Hitbox,
+ cx: &mut WindowContext,
+ ) -> Vec<AnyElement> {
+ 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<Pixels>,
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::<HashMap<_, _>>()
+ });
+
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));
-
- 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,
- );
+ 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,
+ );
+ }
}
}
}
@@ -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<SelectionLayout>)>,
code_actions_indicator: Option<AnyElement>,
test_indicators: Vec<AnyElement>,
+ close_indicators: Vec<AnyElement>,
gutter_fold_toggles: Vec<Option<AnyElement>>,
crease_trailers: Vec<Option<CreaseTrailerLayout>>,
mouse_context_menu: Option<AnyElement>,
@@ -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);
}
@@ -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<Anchor>,
pub status: DiffHunkStatus,
pub diff_base_byte_range: Range<usize>,
@@ -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<Pixels>,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ 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<Pixels>, b: gpui::Point<Pixels>| (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<Editor>,
+ ) {
+ 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<Self>) {
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<Model<Buffer>>,
- 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<Buffer>,
deleted_text_height: u8,
- hunk: &HunkToExpand,
+ hunk: &HoveredHunk,
cx: &mut ViewContext<'_, Self>,
) -> Option<BlockId> {
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::<DiffRowHighlight>();
let to_remove = self
.expanded_hunks
.hunks
.drain(..)
.filter_map(|expanded_hunk| expanded_hunk.block)
- .collect();
- self.clear_row_highlights::<DiffRowHighlight>();
- self.remove_blocks(to_remove, None, cx);
+ .collect::<HashSet<_>>();
+ 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<DiffHunk<MultiBufferRow>> {
+ 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<Buffer>, cx: &mut AppContext) -> Option<Model<Buffer>> {
buffer
.update(cx, |buffer, _| {
@@ -555,7 +733,7 @@ fn deleted_hunk_color(cx: &AppContext) -> Hsla {
fn editor_with_deleted_text(
diff_base_buffer: Model<Buffer>,
deleted_color: Hsla,
- hunk: &HunkToExpand,
+ hunk: &HoveredHunk,
cx: &mut ViewContext<'_, Editor>,
) -> (u8, View<Editor>) {
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::<RevertSelectedHunks>(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::<ToggleHunkDiff>(move |_, cx| {
+ parent_editor
+ .update(cx, |editor, cx| {
+ editor.toggle_hovered_hunk(&hunk, cx);
+ })
+ .ok();
+ })
+ .detach();
editor
});
@@ -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<Pixels>),
+ /// 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<Pixels>,
+ pub(crate) position: MenuPosition,
pub(crate) context_menu: View<ui::ContextMenu>,
_subscription: Subscription,
}
impl MouseContextMenu {
- pub(crate) fn new(
+ pub(crate) fn pinned_to_editor(
+ editor: &mut Editor,
+ source: multi_buffer::Anchor,
+ position: Point<Pixels>,
+ context_menu: View<ui::ContextMenu>,
+ cx: &mut ViewContext<Editor>,
+ ) -> Option<Self> {
+ 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<Pixels>,
context_menu: View<ui::ContextMenu>,
cx: &mut ViewContext<Editor>,
@@ -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();
}
@@ -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 {