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

@ -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)),
)
}