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:
parent
1fdae4bae0
commit
d2c55cbe3d
64 changed files with 7653 additions and 5495 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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)),
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue