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

@ -2,17 +2,16 @@ use std::{ops::Range, time::Duration};
use collections::HashSet;
use gpui::{AppContext, Task};
use language::{language_settings::language_settings, BufferRow};
use multi_buffer::{MultiBufferIndentGuide, MultiBufferRow};
use text::{BufferId, LineIndent, Point};
use language::language_settings::language_settings;
use multi_buffer::{IndentGuide, MultiBufferRow};
use text::{LineIndent, Point};
use ui::ViewContext;
use util::ResultExt;
use crate::{DisplaySnapshot, Editor};
struct ActiveIndentedRange {
buffer_id: BufferId,
row_range: Range<BufferRow>,
row_range: Range<MultiBufferRow>,
indent: LineIndent,
}
@ -36,7 +35,7 @@ impl Editor {
visible_buffer_range: Range<MultiBufferRow>,
snapshot: &DisplaySnapshot,
cx: &mut ViewContext<Editor>,
) -> Option<Vec<MultiBufferIndentGuide>> {
) -> Option<Vec<IndentGuide>> {
let show_indent_guides = self.should_show_indent_guides().unwrap_or_else(|| {
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
language_settings(
@ -66,7 +65,7 @@ impl Editor {
pub fn find_active_indent_guide_indices(
&mut self,
indent_guides: &[MultiBufferIndentGuide],
indent_guides: &[IndentGuide],
snapshot: &DisplaySnapshot,
cx: &mut ViewContext<Editor>,
) -> Option<HashSet<usize>> {
@ -134,9 +133,7 @@ impl Editor {
.iter()
.enumerate()
.filter(|(_, indent_guide)| {
indent_guide.buffer_id == active_indent_range.buffer_id
&& indent_guide.indent_level()
== active_indent_range.indent.len(indent_guide.tab_size)
indent_guide.indent_level() == active_indent_range.indent.len(indent_guide.tab_size)
});
let mut matches = HashSet::default();
@ -158,7 +155,7 @@ pub fn indent_guides_in_range(
ignore_disabled_for_language: bool,
snapshot: &DisplaySnapshot,
cx: &AppContext,
) -> Vec<MultiBufferIndentGuide> {
) -> Vec<IndentGuide> {
let start_anchor = snapshot
.buffer_snapshot
.anchor_before(Point::new(visible_buffer_range.start.0, 0));
@ -169,14 +166,12 @@ pub fn indent_guides_in_range(
snapshot
.buffer_snapshot
.indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx)
.into_iter()
.filter(|indent_guide| {
if editor.buffer_folded(indent_guide.buffer_id, cx) {
if editor.is_buffer_folded(indent_guide.buffer_id, cx) {
return false;
}
let start =
MultiBufferRow(indent_guide.multibuffer_row_range.start.0.saturating_sub(1));
let start = MultiBufferRow(indent_guide.start_row.0.saturating_sub(1));
// Filter out indent guides that are inside a fold
// All indent guides that are starting "offscreen" have a start value of the first visible row minus one
// Therefore checking if a line is folded at first visible row minus one causes the other indent guides that are not related to the fold to disappear as well
@ -193,24 +188,11 @@ async fn resolve_indented_range(
snapshot: DisplaySnapshot,
buffer_row: MultiBufferRow,
) -> Option<ActiveIndentedRange> {
let (buffer_row, buffer_snapshot, buffer_id) =
if let Some((_, buffer_id, snapshot)) = snapshot.buffer_snapshot.as_singleton() {
(buffer_row.0, snapshot, buffer_id)
} else {
let (snapshot, point) = snapshot.buffer_snapshot.buffer_line_for_row(buffer_row)?;
let buffer_id = snapshot.remote_id();
(point.start.row, snapshot, buffer_id)
};
buffer_snapshot
snapshot
.buffer_snapshot
.enclosing_indent(buffer_row)
.await
.map(|(row_range, indent)| ActiveIndentedRange {
row_range,
indent,
buffer_id,
})
.map(|(row_range, indent)| ActiveIndentedRange { row_range, indent })
}
fn should_recalculate_indented_range(
@ -222,23 +204,23 @@ fn should_recalculate_indented_range(
if prev_row.0 == new_row.0 {
return false;
}
if let Some((_, _, snapshot)) = snapshot.buffer_snapshot.as_singleton() {
if !current_indent_range.row_range.contains(&new_row.0) {
if snapshot.buffer_snapshot.is_singleton() {
if !current_indent_range.row_range.contains(&new_row) {
return true;
}
let old_line_indent = snapshot.line_indent_for_row(prev_row.0);
let new_line_indent = snapshot.line_indent_for_row(new_row.0);
let old_line_indent = snapshot.buffer_snapshot.line_indent_for_row(prev_row);
let new_line_indent = snapshot.buffer_snapshot.line_indent_for_row(new_row);
if old_line_indent.is_line_empty()
|| new_line_indent.is_line_empty()
|| old_line_indent != new_line_indent
|| snapshot.max_point().row == new_row.0
|| snapshot.buffer_snapshot.max_point().row == new_row.0
{
return true;
}
let next_line_indent = snapshot.line_indent_for_row(new_row.0 + 1);
let next_line_indent = snapshot.buffer_snapshot.line_indent_for_row(new_row + 1);
next_line_indent.is_line_empty() || next_line_indent != old_line_indent
} else {
true