Rework diff rendering to allow putting the cursor into deleted text, soft-wrapping and scrolling deleted text correctly (#22994)

Closes #12553

* [x] Fix `diff_hunk_before`
* [x] Fix failure to show deleted text when expanding hunk w/ cursor on
second line of the hunk
* [x] Failure to expand diff hunk below the cursor.
* [x] Delete the whole file, and expand the diff. Backspace over the
deleted hunk, panic!
* [x] Go-to-line now counts the diff hunks, but it should not
* [x] backspace at the beginning of a deleted hunk deletes too much text
* [x] Indent guides are rendered incorrectly 
* [ ] Fix randomized multi buffer tests

Maybe:
* [ ] Buffer search should include deleted text (in vim mode it turns
out I use `/x` all the time to jump to the next x I can see).
* [ ] vim: should refuse to switch into insert mode if selection is
fully within a diff.
* [ ] vim `o` command when cursor is on last line of deleted hunk.
* [ ] vim `shift-o` on first line of deleted hunk moves cursor but
doesn't insert line
* [x] `enter` at end of diff hunk inserts a new line but doesn't move
cursor
* [x] (`shift-enter` at start of diff hunk does nothing)
* [ ] Inserting a line just before an expanded hunk collapses it

Release Notes:


- Improved diff rendering, allowing you to navigate with your cursor
inside of deleted text in diff hunks.

---------

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Cole <cole@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Michael <michael@zed.dev>
Co-authored-by: Agus <agus@zed.dev>
Co-authored-by: João <joao@zed.dev>
This commit is contained in:
Max Brunsfeld 2025-01-24 13:18:22 -08:00 committed by GitHub
parent 1fdae4bae0
commit d2c55cbe3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
64 changed files with 7653 additions and 5495 deletions

View file

@ -16,6 +16,7 @@ doctest = false
anyhow.workspace = true
editor.workspace = true
gpui.workspace = true
language.workspace = true
menu.workspace = true
schemars.workspace = true
serde.workspace = true

View file

@ -20,7 +20,7 @@ pub(crate) struct SelectionStats {
}
pub struct CursorPosition {
position: Option<Point>,
position: Option<(Point, bool)>,
selected_count: SelectionStats,
context: Option<FocusHandle>,
workspace: WeakView<Workspace>,
@ -97,8 +97,11 @@ impl CursorPosition {
}
}
}
cursor_position.position =
last_selection.map(|s| s.head().to_point(&buffer));
cursor_position.position = last_selection.and_then(|s| {
buffer
.point_to_buffer_point(s.head().to_point(&buffer))
.map(|(_, point, is_main_buffer)| (point, is_main_buffer))
});
cursor_position.context = Some(editor.focus_handle(cx));
}
}
@ -163,9 +166,10 @@ impl CursorPosition {
impl Render for CursorPosition {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div().when_some(self.position, |el, position| {
div().when_some(self.position, |el, (position, is_main_buffer)| {
let mut text = format!(
"{}{FILE_ROW_COLUMN_DELIMITER}{}",
"{}{}{FILE_ROW_COLUMN_DELIMITER}{}",
if is_main_buffer { "" } else { "(deleted) " },
position.row + 1,
position.column + 1
);
@ -183,8 +187,12 @@ impl Render for CursorPosition {
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))
{
workspace
.toggle_modal(cx, |cx| crate::GoToLine::new(editor, cx))
if let Some((_, buffer, _)) = editor.read(cx).active_excerpt(cx)
{
workspace.toggle_modal(cx, |cx| {
crate::GoToLine::new(editor, buffer, cx)
})
}
}
});
}

View file

@ -1,13 +1,15 @@
pub mod cursor_position;
use cursor_position::LineIndicatorFormat;
use editor::{scroll::Autoscroll, Editor};
use editor::{scroll::Autoscroll, Anchor, Editor, MultiBuffer, ToPoint};
use gpui::{
div, prelude::*, AnyWindowHandle, AppContext, DismissEvent, EventEmitter, FocusHandle,
FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
FocusableView, Model, Render, SharedString, Styled, Subscription, View, ViewContext,
VisualContext,
};
use language::Buffer;
use settings::Settings;
use text::{Bias, Point};
use text::Point;
use theme::ActiveTheme;
use ui::prelude::*;
use util::paths::FILE_ROW_COLUMN_DELIMITER;
@ -21,6 +23,7 @@ pub fn init(cx: &mut AppContext) {
pub struct GoToLine {
line_editor: View<Editor>,
active_editor: View<Editor>,
active_buffer: Model<Buffer>,
current_text: SharedString,
prev_scroll_position: Option<gpui::Point<f32>>,
_subscriptions: Vec<Subscription>,
@ -42,22 +45,43 @@ impl GoToLine {
let handle = cx.view().downgrade();
editor
.register_action(move |_: &editor::actions::ToggleGoToLine, cx| {
let Some(editor) = handle.upgrade() else {
let Some(editor_handle) = handle.upgrade() else {
return;
};
let Some(workspace) = editor.read(cx).workspace() else {
let Some(workspace) = editor_handle.read(cx).workspace() else {
return;
};
let editor = editor_handle.read(cx);
let Some((_, buffer, _)) = editor.active_excerpt(cx) else {
return;
};
workspace.update(cx, |workspace, cx| {
workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx));
workspace.toggle_modal(cx, move |cx| GoToLine::new(editor_handle, buffer, cx));
})
})
.detach();
}
pub fn new(active_editor: View<Editor>, cx: &mut ViewContext<Self>) -> Self {
let cursor =
active_editor.update(cx, |editor, cx| editor.selections.last::<Point>(cx).head());
pub fn new(
active_editor: View<Editor>,
active_buffer: Model<Buffer>,
cx: &mut ViewContext<Self>,
) -> Self {
let (cursor, last_line, scroll_position) = active_editor.update(cx, |editor, cx| {
let cursor = editor.selections.last::<Point>(cx).head();
let snapshot = active_buffer.read(cx).snapshot();
let last_line = editor
.buffer()
.read(cx)
.excerpts_for_buffer(&active_buffer, cx)
.into_iter()
.map(move |(_, range)| text::ToPoint::to_point(&range.context.end, &snapshot).row)
.max()
.unwrap_or(0);
(cursor, last_line, editor.scroll_position(cx))
});
let line = cursor.row + 1;
let column = cursor.column + 1;
@ -69,15 +93,17 @@ impl GoToLine {
});
let line_editor_change = cx.subscribe(&line_editor, Self::on_line_editor_event);
let editor = active_editor.read(cx);
let last_line = editor.buffer().read(cx).snapshot(cx).max_point().row;
let scroll_position = active_editor.update(cx, |editor, cx| editor.scroll_position(cx));
let current_text = format!("{} of {} (column {})", line, last_line + 1, column);
let current_text = format!(
"Current Line: {} of {} (column {})",
line,
last_line + 1,
column
);
Self {
line_editor,
active_editor,
active_buffer,
current_text: current_text.into(),
prev_scroll_position: Some(scroll_position),
_subscriptions: vec![line_editor_change, cx.on_release(Self::release)],
@ -113,35 +139,40 @@ impl GoToLine {
}
fn highlight_current_line(&mut self, cx: &mut ViewContext<Self>) {
if let Some(point) = self.point_from_query(cx) {
self.active_editor.update(cx, |active_editor, cx| {
let snapshot = active_editor.snapshot(cx).display_snapshot;
let start = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
let end = start + Point::new(1, 0);
let start = snapshot.buffer_snapshot.anchor_before(start);
let end = snapshot.buffer_snapshot.anchor_after(end);
active_editor.clear_row_highlights::<GoToLineRowHighlights>();
active_editor.highlight_rows::<GoToLineRowHighlights>(
start..end,
cx.theme().colors().editor_highlighted_line_background,
true,
cx,
);
active_editor.request_autoscroll(Autoscroll::center(), cx);
});
cx.notify();
}
self.active_editor.update(cx, |editor, cx| {
editor.clear_row_highlights::<GoToLineRowHighlights>();
let multibuffer = editor.buffer().read(cx);
let snapshot = multibuffer.snapshot(cx);
let Some(start) = self.anchor_from_query(&multibuffer, cx) else {
return;
};
let start_point = start.to_point(&snapshot);
let end_point = start_point + Point::new(1, 0);
let end = snapshot.anchor_after(end_point);
editor.highlight_rows::<GoToLineRowHighlights>(
start..end,
cx.theme().colors().editor_highlighted_line_background,
true,
cx,
);
editor.request_autoscroll(Autoscroll::center(), cx);
});
cx.notify();
}
fn point_from_query(&self, cx: &ViewContext<Self>) -> Option<Point> {
let (row, column) = self.line_column_from_query(cx);
Some(Point::new(
row?.saturating_sub(1),
column.unwrap_or(0).saturating_sub(1),
))
fn anchor_from_query(
&self,
multibuffer: &MultiBuffer,
cx: &ViewContext<Editor>,
) -> Option<Anchor> {
let (Some(row), column) = self.line_column_from_query(cx) else {
return None;
};
let point = Point::new(row.saturating_sub(1), column.unwrap_or(0).saturating_sub(1));
multibuffer.buffer_point_to_anchor(&self.active_buffer, point, cx)
}
fn line_column_from_query(&self, cx: &ViewContext<Self>) -> (Option<u32>, Option<u32>) {
fn line_column_from_query(&self, cx: &AppContext) -> (Option<u32>, Option<u32>) {
let input = self.line_editor.read(cx).text(cx);
let mut components = input
.splitn(2, FILE_ROW_COLUMN_DELIMITER)
@ -157,18 +188,18 @@ impl GoToLine {
}
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
if let Some(point) = self.point_from_query(cx) {
self.active_editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx).display_snapshot;
let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
editor.change_selections(Some(Autoscroll::center()), cx, |s| {
s.select_ranges([point..point])
});
editor.focus(cx);
cx.notify();
self.active_editor.update(cx, |editor, cx| {
let multibuffer = editor.buffer().read(cx);
let Some(start) = self.anchor_from_query(&multibuffer, cx) else {
return;
};
editor.change_selections(Some(Autoscroll::center()), cx, |s| {
s.select_anchor_ranges([start..start])
});
self.prev_scroll_position.take();
}
editor.focus(cx);
cx.notify()
});
self.prev_scroll_position.take();
cx.emit(DismissEvent);
}
@ -205,7 +236,6 @@ impl Render for GoToLine {
.px_2()
.py_1()
.gap_1()
.child(Label::new("Current Line:").color(Color::Muted))
.child(Label::new(help_text).color(Color::Muted)),
)
}