From 2e00f40c54e1e0c23af06587cefcfd06eae49a0e Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Sun, 30 Nov 2025 22:45:01 -0500 Subject: [PATCH] Basic side-by-side diff implementation (#43586) Release Notes: - N/A --------- Co-authored-by: cameron Co-authored-by: Cameron --- Cargo.lock | 1 + crates/agent_ui/src/agent_diff.rs | 2 +- crates/agent_ui/src/text_thread_editor.rs | 6 +- crates/buffer_diff/src/buffer_diff.rs | 124 ++- crates/collab_ui/src/channel_view.rs | 2 +- crates/debugger_tools/src/dap_log.rs | 6 +- crates/debugger_ui/src/stack_trace_view.rs | 2 +- crates/diagnostics/src/diagnostics.rs | 2 +- crates/editor/Cargo.toml | 1 + crates/editor/src/display_map/wrap_map.rs | 1 + crates/editor/src/editor.rs | 10 +- crates/editor/src/editor_tests.rs | 206 ++++- crates/editor/src/element.rs | 3 + crates/editor/src/items.rs | 6 +- crates/editor/src/split.rs | 262 ++++++ crates/git_ui/src/commit_view.rs | 2 +- crates/git_ui/src/file_diff_view.rs | 2 +- crates/git_ui/src/project_diff.rs | 225 +++-- crates/git_ui/src/text_diff_view.rs | 2 +- crates/language_tools/src/lsp_log_view.rs | 6 +- crates/multi_buffer/src/multi_buffer.rs | 720 ++++++++++++---- crates/multi_buffer/src/multi_buffer_tests.rs | 786 +++++++++++++----- crates/multi_buffer/src/path_key.rs | 5 + crates/repl/src/notebook/notebook_ui.rs | 2 +- crates/rope/src/point.rs | 9 +- crates/search/src/project_search.rs | 2 +- crates/terminal_view/src/terminal_view.rs | 6 +- crates/workspace/src/item.rs | 4 +- typos.toml | 2 + 29 files changed, 1902 insertions(+), 505 deletions(-) create mode 100644 crates/editor/src/split.rs diff --git a/Cargo.lock b/Cargo.lock index 4844d9574e0abe0c80a710bd299fd909807172b2..80840bf3d3c631f6469f18811f1f3b79979003e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5367,6 +5367,7 @@ dependencies = [ "db", "edit_prediction", "emojis", + "feature_flags", "file_icons", "fs", "futures 0.3.31", diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 8aece1984ad597e629cd966c0e61d6a5681d7020..11acd649ef9df500edf99926e754228e4c41e7bc 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -493,7 +493,7 @@ impl Item for AgentDiffPane { Some("Assistant Diff Opened") } - fn as_searchable(&self, _: &Entity) -> Option> { + fn as_searchable(&self, _: &Entity, _: &App) -> Option> { Some(Box::new(self.editor.clone())) } diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 8c245a0675a03e65efeaf3e92bc3b7a5062fdd53..6d5e226b6a5f1ae441314d45f2546a57c84ca664 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -2556,7 +2556,11 @@ impl Item for TextThreadEditor { Some(self.title(cx).to_string().into()) } - fn as_searchable(&self, handle: &Entity) -> Option> { + fn as_searchable( + &self, + handle: &Entity, + _: &App, + ) -> Option> { Some(Box::new(handle.clone())) } diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index 6dea38fe6819eb4b70271cb31e15b033ed0e565c..5f1736e450556f2943618b49eee926eb3bbb4338 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -1,7 +1,7 @@ use futures::channel::oneshot; use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch}; use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, TaskLabel}; -use language::{Language, LanguageRegistry}; +use language::{BufferRow, Language, LanguageRegistry}; use rope::Rope; use std::{ cmp::Ordering, @@ -11,7 +11,7 @@ use std::{ sync::{Arc, LazyLock}, }; use sum_tree::SumTree; -use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point, ToOffset as _}; +use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point, ToOffset as _, ToPoint as _}; use util::ResultExt; pub static CALCULATE_DIFF_TASK: LazyLock = LazyLock::new(TaskLabel::new); @@ -88,6 +88,7 @@ struct PendingHunk { #[derive(Debug, Clone)] pub struct DiffHunkSummary { buffer_range: Range, + diff_base_byte_range: Range, } impl sum_tree::Item for InternalDiffHunk { @@ -96,6 +97,7 @@ impl sum_tree::Item for InternalDiffHunk { fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary { DiffHunkSummary { buffer_range: self.buffer_range.clone(), + diff_base_byte_range: self.diff_base_byte_range.clone(), } } } @@ -106,6 +108,7 @@ impl sum_tree::Item for PendingHunk { fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary { DiffHunkSummary { buffer_range: self.buffer_range.clone(), + diff_base_byte_range: self.diff_base_byte_range.clone(), } } } @@ -116,6 +119,7 @@ impl sum_tree::Summary for DiffHunkSummary { fn zero(_cx: Self::Context<'_>) -> Self { DiffHunkSummary { buffer_range: Anchor::MIN..Anchor::MIN, + diff_base_byte_range: 0..0, } } @@ -125,6 +129,15 @@ impl sum_tree::Summary for DiffHunkSummary { .start .min(&other.buffer_range.start, buffer); self.buffer_range.end = *self.buffer_range.end.max(&other.buffer_range.end, buffer); + + self.diff_base_byte_range.start = self + .diff_base_byte_range + .start + .min(other.diff_base_byte_range.start); + self.diff_base_byte_range.end = self + .diff_base_byte_range + .end + .max(other.diff_base_byte_range.end); } } @@ -305,6 +318,54 @@ impl BufferDiffSnapshot { let (new_id, new_empty) = (right.remote_id(), right.is_empty()); new_id == old_id || (new_empty && old_empty) } + + pub fn row_to_base_text_row(&self, row: BufferRow, buffer: &text::BufferSnapshot) -> u32 { + // TODO(split-diff) expose a parameter to reuse a cursor to avoid repeatedly seeking from the start + + // Find the last hunk that starts before this position. + let mut cursor = self.inner.hunks.cursor::(buffer); + let position = buffer.anchor_before(Point::new(row, 0)); + cursor.seek(&position, Bias::Left); + if cursor + .item() + .is_none_or(|hunk| hunk.buffer_range.start.cmp(&position, buffer).is_gt()) + { + cursor.prev(); + } + + let unclipped_point = if let Some(hunk) = cursor.item() + && hunk.buffer_range.start.cmp(&position, buffer).is_le() + { + let mut unclipped_point = cursor + .end() + .diff_base_byte_range + .end + .to_point(self.base_text()); + if position.cmp(&cursor.end().buffer_range.end, buffer).is_ge() { + unclipped_point += + Point::new(row, 0) - cursor.end().buffer_range.end.to_point(buffer); + } + // Move the cursor so that at the next step we can clip with the start of the next hunk. + cursor.next(); + unclipped_point + } else { + // Position is before the added region for the first hunk. + debug_assert!(self.inner.hunks.first().is_none_or(|first_hunk| { + position.cmp(&first_hunk.buffer_range.start, buffer).is_le() + })); + Point::new(row, 0) + }; + + let max_point = if let Some(next_hunk) = cursor.item() { + next_hunk + .diff_base_byte_range + .start + .to_point(self.base_text()) + } else { + self.base_text().max_point() + }; + unclipped_point.min(max_point).row + } } impl BufferDiffInner { @@ -946,6 +1007,7 @@ impl BufferDiff { if self.secondary_diff.is_some() { self.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary { buffer_range: Anchor::min_min_range_for_buffer(self.buffer_id), + diff_base_byte_range: 0..0, }); cx.emit(BufferDiffEvent::DiffChanged { changed_range: Some(Anchor::min_max_range_for_buffer(self.buffer_id)), @@ -2240,4 +2302,62 @@ mod tests { hunks = found_hunks; } } + + #[gpui::test] + async fn test_row_to_base_text_row(cx: &mut TestAppContext) { + let base_text = " + zero + one + two + three + four + five + six + seven + eight + " + .unindent(); + let buffer_text = " + zero + ONE + two + NINE + five + seven + " + .unindent(); + + // zero + // - one + // + ONE + // two + // - three + // - four + // + NINE + // five + // - six + // seven + // + eight + + let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text); + let buffer_snapshot = buffer.snapshot(); + let diff = BufferDiffSnapshot::new_sync(buffer_snapshot.clone(), base_text, cx); + let expected_results = [ + // don't format me + (0, 0), + (1, 2), + (2, 2), + (3, 5), + (4, 5), + (5, 7), + (6, 9), + ]; + for (buffer_row, expected) in expected_results { + assert_eq!( + diff.row_to_base_text_row(buffer_row, &buffer_snapshot), + expected, + "{buffer_row}" + ); + } + } } diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 483597359e091eb166df51bbe3c9fa9448ee3d4f..8959c6ccbe88d1f3f78fb29009904244624d9999 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -541,7 +541,7 @@ impl Item for ChannelView { }) } - fn as_searchable(&self, _: &Entity) -> Option> { + fn as_searchable(&self, _: &Entity, _: &App) -> Option> { Some(Box::new(self.editor.clone())) } diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 4c994ad7eb749dcb5828daa83bad34a579f9f14c..8841a3744a4452355e2b02c9dca969cab493796e 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -998,7 +998,11 @@ impl Item for DapLogView { None } - fn as_searchable(&self, handle: &Entity) -> Option> { + fn as_searchable( + &self, + handle: &Entity, + _: &App, + ) -> Option> { Some(Box::new(handle.clone())) } } diff --git a/crates/debugger_ui/src/stack_trace_view.rs b/crates/debugger_ui/src/stack_trace_view.rs index cdbd9aaff0cff250fdc3e5091ffa7dcabc70861a..70b88d203e4ff8017127eee2ad6ff0a81df74c69 100644 --- a/crates/debugger_ui/src/stack_trace_view.rs +++ b/crates/debugger_ui/src/stack_trace_view.rs @@ -428,7 +428,7 @@ impl Item for StackTraceView { } } - fn as_searchable(&self, _: &Entity) -> Option> { + fn as_searchable(&self, _: &Entity, _: &App) -> Option> { Some(Box::new(self.editor.clone())) } diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 413b73d1b6f679fa464d378760e37c773e1583e7..58babbd251416118947362fae0a47a80cc277695 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -890,7 +890,7 @@ impl Item for ProjectDiagnosticsEditor { } } - fn as_searchable(&self, _: &Entity) -> Option> { + fn as_searchable(&self, _: &Entity, _: &App) -> Option> { Some(Box::new(self.editor.clone())) } diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index bd3ac75649ce705f98f7fa1c2616dc2bcf152642..2aa02e293dd44d5fdd920ac8cd98da48b9c1a912 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -41,6 +41,7 @@ dap.workspace = true db.workspace = true buffer_diff.workspace = true emojis.workspace = true +feature_flags.workspace = true file_icons.workspace = true futures.workspace = true fuzzy.workspace = true diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 30e0e652195af38dbe25f0b7b71c5c5fff1c7179..20ef9391888e6a824b87fe5de2607500049904ff 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1056,6 +1056,7 @@ impl Iterator for WrapRows<'_> { RowInfo { buffer_id: None, buffer_row: None, + base_text_row: None, multibuffer_row: None, diff_status, expand_info: None, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2577591d3f5acfa2c351cd6aa89c9320e90db6c3..1f23f08b101c56cde25ac6cd2eaffbc2c7f32469 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -36,6 +36,7 @@ mod persistence; mod rust_analyzer_ext; pub mod scroll; mod selections_collection; +mod split; pub mod tasks; #[cfg(test)] @@ -69,6 +70,7 @@ pub use multi_buffer::{ MultiBufferOffset, MultiBufferOffsetUtf16, MultiBufferSnapshot, PathKey, RowInfo, ToOffset, ToPoint, }; +pub use split::SplittableEditor; pub use text::Bias; use ::git::{ @@ -1198,6 +1200,7 @@ pub struct Editor { applicable_language_settings: HashMap, LanguageSettings>, accent_overrides: Vec, fetched_tree_sitter_chunks: HashMap>>, + use_base_text_line_numbers: bool, } fn debounce_value(debounce_ms: u64) -> Option { @@ -1637,7 +1640,7 @@ pub(crate) struct FocusedBlock { focus_handle: WeakFocusHandle, } -#[derive(Clone)] +#[derive(Clone, Debug)] enum JumpData { MultiBufferRow { row: MultiBufferRow, @@ -2344,6 +2347,7 @@ impl Editor { applicable_language_settings: HashMap::default(), accent_overrides: Vec::new(), fetched_tree_sitter_chunks: HashMap::default(), + use_base_text_line_numbers: false, }; if is_minimap { @@ -19204,6 +19208,10 @@ impl Editor { self.display_map.read(cx).fold_placeholder.clone() } + pub fn set_use_base_text_line_numbers(&mut self, show: bool, _cx: &mut Context) { + self.use_base_text_line_numbers = show; + } + pub fn set_expand_all_diff_hunks(&mut self, cx: &mut App) { self.buffer.update(cx, |buffer, cx| { buffer.set_all_diff_hunks_expanded(cx); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 09c9083f29a57addbdd5ca01b162f4abc023d0d7..0bcfad7b881f4d90a2ffe0aa5c1d330d89470e98 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -35,7 +35,9 @@ use language_settings::Formatter; use languages::markdown_lang; use languages::rust_lang; use lsp::CompletionParams; -use multi_buffer::{IndentGuide, MultiBufferOffset, MultiBufferOffsetUtf16, PathKey}; +use multi_buffer::{ + IndentGuide, MultiBufferFilterMode, MultiBufferOffset, MultiBufferOffsetUtf16, PathKey, +}; use parking_lot::Mutex; use pretty_assertions::{assert_eq, assert_ne}; use project::{ @@ -28199,6 +28201,208 @@ async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) { "}); } +#[gpui::test] +async fn test_filtered_editor_pair(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut leader_cx = EditorTestContext::new(cx).await; + + let diff_base = indoc!( + r#" + one + two + three + four + five + six + "# + ); + + let initial_state = indoc!( + r#" + ˇone + two + THREE + four + five + six + "# + ); + + leader_cx.set_state(initial_state); + + leader_cx.set_head_text(&diff_base); + leader_cx.run_until_parked(); + + let follower = leader_cx.update_multibuffer(|leader, cx| { + leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions)); + leader.set_all_diff_hunks_expanded(cx); + leader.get_or_create_follower(cx) + }); + follower.update(cx, |follower, cx| { + follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions)); + follower.set_all_diff_hunks_expanded(cx); + }); + + let follower_editor = + leader_cx.new_window_entity(|window, cx| build_editor(follower, window, cx)); + // leader_cx.window.focus(&follower_editor.focus_handle(cx)); + + let mut follower_cx = EditorTestContext::for_editor_in(follower_editor, &mut leader_cx).await; + cx.run_until_parked(); + + leader_cx.assert_editor_state(initial_state); + follower_cx.assert_editor_state(indoc! { + r#" + ˇone + two + three + four + five + six + "# + }); + + follower_cx.editor(|editor, _window, cx| { + assert!(editor.read_only(cx)); + }); + + leader_cx.update_editor(|editor, _window, cx| { + editor.edit([(Point::new(4, 0)..Point::new(5, 0), "FIVE\n")], cx); + }); + cx.run_until_parked(); + + leader_cx.assert_editor_state(indoc! { + r#" + ˇone + two + THREE + four + FIVE + six + "# + }); + + follower_cx.assert_editor_state(indoc! { + r#" + ˇone + two + three + four + five + six + "# + }); + + leader_cx.update_editor(|editor, _window, cx| { + editor.edit([(Point::new(6, 0)..Point::new(6, 0), "SEVEN")], cx); + }); + cx.run_until_parked(); + + leader_cx.assert_editor_state(indoc! { + r#" + ˇone + two + THREE + four + FIVE + six + SEVEN"# + }); + + follower_cx.assert_editor_state(indoc! { + r#" + ˇone + two + three + four + five + six + "# + }); + + leader_cx.update_editor(|editor, window, cx| { + editor.move_down(&MoveDown, window, cx); + editor.refresh_selected_text_highlights(true, window, cx); + }); + leader_cx.run_until_parked(); +} + +#[gpui::test] +async fn test_filtered_editor_pair_complex(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let base_text = "base\n"; + let buffer_text = "buffer\n"; + + let buffer1 = cx.new(|cx| Buffer::local(buffer_text, cx)); + let diff1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer1, cx)); + + let extra_buffer_1 = cx.new(|cx| Buffer::local("dummy text 1\n", cx)); + let extra_diff_1 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_1, cx)); + let extra_buffer_2 = cx.new(|cx| Buffer::local("dummy text 2\n", cx)); + let extra_diff_2 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_2, cx)); + + let leader = cx.new(|cx| { + let mut leader = MultiBuffer::new(Capability::ReadWrite); + leader.set_all_diff_hunks_expanded(cx); + leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions)); + leader + }); + let follower = leader.update(cx, |leader, cx| leader.get_or_create_follower(cx)); + follower.update(cx, |follower, _| { + follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions)); + }); + + leader.update(cx, |leader, cx| { + leader.insert_excerpts_after( + ExcerptId::min(), + extra_buffer_2.clone(), + vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)], + cx, + ); + leader.add_diff(extra_diff_2.clone(), cx); + + leader.insert_excerpts_after( + ExcerptId::min(), + extra_buffer_1.clone(), + vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)], + cx, + ); + leader.add_diff(extra_diff_1.clone(), cx); + + leader.insert_excerpts_after( + ExcerptId::min(), + buffer1.clone(), + vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)], + cx, + ); + leader.add_diff(diff1.clone(), cx); + }); + + cx.run_until_parked(); + let mut cx = cx.add_empty_window(); + + let leader_editor = cx + .new_window_entity(|window, cx| Editor::for_multibuffer(leader.clone(), None, window, cx)); + let follower_editor = cx.new_window_entity(|window, cx| { + Editor::for_multibuffer(follower.clone(), None, window, cx) + }); + + let mut leader_cx = EditorTestContext::for_editor_in(leader_editor.clone(), &mut cx).await; + leader_cx.assert_editor_state(indoc! {" + ˇbuffer + + dummy text 1 + + dummy text 2 + "}); + let mut follower_cx = EditorTestContext::for_editor_in(follower_editor.clone(), &mut cx).await; + follower_cx.assert_editor_state(indoc! {" + ˇbase + + + "}); +} + #[gpui::test] async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index eabc0fd496407e5f02797a9721a9811ac4d39752..a629d45408825c061c417821c12ab260e2ed2f77 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -3274,6 +3274,8 @@ impl EditorElement { line_number.clear(); let non_relative_number = if relative.wrapped() { row_info.buffer_row.or(row_info.wrapped_buffer_row)? + 1 + } else if self.editor.read(cx).use_base_text_line_numbers { + row_info.base_text_row?.0 + 1 } else { row_info.buffer_row? + 1 }; @@ -3282,6 +3284,7 @@ impl EditorElement { && row_info .diff_status .is_some_and(|status| status.is_deleted()) + && !self.editor.read(cx).use_base_text_line_numbers { return None; } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 7e82336b4403cc8142983ef3802a9cdb9ca9cf2b..8111c837e2ee5c35fdfb120999c2be49b09c468c 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -929,7 +929,11 @@ impl Item for Editor { }) } - fn as_searchable(&self, handle: &Entity) -> Option> { + fn as_searchable( + &self, + handle: &Entity, + _: &App, + ) -> Option> { Some(Box::new(handle.clone())) } diff --git a/crates/editor/src/split.rs b/crates/editor/src/split.rs new file mode 100644 index 0000000000000000000000000000000000000000..ff821de287ecf61a651393f437a13334ddf1f85e --- /dev/null +++ b/crates/editor/src/split.rs @@ -0,0 +1,262 @@ +use feature_flags::{FeatureFlag, FeatureFlagAppExt as _}; +use gpui::{ + Action, AppContext as _, Entity, EventEmitter, Focusable, NoAction, Subscription, WeakEntity, +}; +use multi_buffer::{MultiBuffer, MultiBufferFilterMode}; +use project::Project; +use ui::{ + App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render, + Styled as _, Window, div, +}; +use workspace::{ + ActivePaneDecorator, Item, ItemHandle, Pane, PaneGroup, SplitDirection, Workspace, +}; + +use crate::{Editor, EditorEvent}; + +struct SplitDiffFeatureFlag; + +impl FeatureFlag for SplitDiffFeatureFlag { + const NAME: &'static str = "split-diff"; + + fn enabled_for_staff() -> bool { + true + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Action, Default)] +#[action(namespace = editor)] +struct SplitDiff; + +#[derive(Clone, Copy, PartialEq, Eq, Action, Default)] +#[action(namespace = editor)] +struct UnsplitDiff; + +pub struct SplittableEditor { + primary_editor: Entity, + secondary: Option, + panes: PaneGroup, + workspace: WeakEntity, + _subscriptions: Vec, +} + +struct SecondaryEditor { + editor: Entity, + pane: Entity, + has_latest_selection: bool, + _subscriptions: Vec, +} + +impl SplittableEditor { + pub fn primary_editor(&self) -> &Entity { + &self.primary_editor + } + + pub fn last_selected_editor(&self) -> &Entity { + if let Some(secondary) = &self.secondary + && secondary.has_latest_selection + { + &secondary.editor + } else { + &self.primary_editor + } + } + + pub fn new_unsplit( + buffer: Entity, + project: Entity, + workspace: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let primary_editor = + cx.new(|cx| Editor::for_multibuffer(buffer, Some(project.clone()), window, cx)); + let pane = cx.new(|cx| { + let mut pane = Pane::new( + workspace.downgrade(), + project, + Default::default(), + None, + NoAction.boxed_clone(), + true, + window, + cx, + ); + pane.set_should_display_tab_bar(|_, _| false); + pane.add_item(primary_editor.boxed_clone(), true, true, None, window, cx); + pane + }); + let panes = PaneGroup::new(pane); + // TODO(split-diff) we might want to tag editor events with whether they came from primary/secondary + let subscriptions = + vec![ + cx.subscribe(&primary_editor, |this, _, event: &EditorEvent, cx| { + if let EditorEvent::SelectionsChanged { .. } = event + && let Some(secondary) = &mut this.secondary + { + secondary.has_latest_selection = false; + } + cx.emit(event.clone()) + }), + ]; + + window.defer(cx, { + let workspace = workspace.downgrade(); + let primary_editor = primary_editor.downgrade(); + move |window, cx| { + workspace + .update(cx, |workspace, cx| { + primary_editor.update(cx, |editor, cx| { + editor.added_to_workspace(workspace, window, cx); + }) + }) + .ok(); + } + }); + Self { + primary_editor, + secondary: None, + panes, + workspace: workspace.downgrade(), + _subscriptions: subscriptions, + } + } + + fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context) { + if !cx.has_flag::() { + return; + } + if self.secondary.is_some() { + return; + } + let Some(workspace) = self.workspace.upgrade() else { + return; + }; + let project = workspace.read(cx).project().clone(); + let follower = self.primary_editor.update(cx, |primary, cx| { + primary.buffer().update(cx, |buffer, cx| { + let follower = buffer.get_or_create_follower(cx); + buffer.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions)); + follower + }) + }); + follower.update(cx, |follower, _| { + follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions)); + }); + let secondary_editor = workspace.update(cx, |workspace, cx| { + cx.new(|cx| { + let mut editor = Editor::for_multibuffer(follower, Some(project), window, cx); + // TODO(split-diff) this should be at the multibuffer level + editor.set_use_base_text_line_numbers(true, cx); + editor.added_to_workspace(workspace, window, cx); + editor + }) + }); + let secondary_pane = cx.new(|cx| { + let mut pane = Pane::new( + workspace.downgrade(), + workspace.read(cx).project().clone(), + Default::default(), + None, + NoAction.boxed_clone(), + true, + window, + cx, + ); + pane.set_should_display_tab_bar(|_, _| false); + pane.add_item( + ItemHandle::boxed_clone(&secondary_editor), + false, + false, + None, + window, + cx, + ); + pane + }); + + let subscriptions = + vec![ + cx.subscribe(&secondary_editor, |this, _, event: &EditorEvent, cx| { + if let EditorEvent::SelectionsChanged { .. } = event + && let Some(secondary) = &mut this.secondary + { + secondary.has_latest_selection = true; + } + cx.emit(event.clone()) + }), + ]; + self.secondary = Some(SecondaryEditor { + editor: secondary_editor, + pane: secondary_pane.clone(), + has_latest_selection: false, + _subscriptions: subscriptions, + }); + let primary_pane = self.panes.first_pane(); + self.panes + .split(&primary_pane, &secondary_pane, SplitDirection::Left) + .unwrap(); + cx.notify(); + } + + fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context) { + let Some(secondary) = self.secondary.take() else { + return; + }; + self.panes.remove(&secondary.pane).unwrap(); + self.primary_editor.update(cx, |primary, cx| { + primary.buffer().update(cx, |buffer, _| { + buffer.set_filter_mode(None); + }); + }); + cx.notify(); + } + + pub fn added_to_workspace( + &mut self, + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, + ) { + self.workspace = workspace.weak_handle(); + self.primary_editor.update(cx, |primary_editor, cx| { + primary_editor.added_to_workspace(workspace, window, cx); + }); + if let Some(secondary) = &self.secondary { + secondary.editor.update(cx, |secondary_editor, cx| { + secondary_editor.added_to_workspace(workspace, window, cx); + }); + } + } +} + +impl EventEmitter for SplittableEditor {} +impl Focusable for SplittableEditor { + fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { + self.primary_editor.read(cx).focus_handle(cx) + } +} + +impl Render for SplittableEditor { + fn render( + &mut self, + window: &mut ui::Window, + cx: &mut ui::Context, + ) -> impl ui::IntoElement { + let Some(active) = self.panes.panes().into_iter().next() else { + return div().into_any_element(); + }; + div() + .id("splittable-editor") + .on_action(cx.listener(Self::split)) + .on_action(cx.listener(Self::unsplit)) + .size_full() + .child(self.panes.render( + None, + &ActivePaneDecorator::new(active, &self.workspace), + window, + cx, + )) + .into_any_element() + } +} diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index 41fd99982c97967c016d9a59199f22ea7ba6115c..e9cfa5f719e5435d9e13343028f6397aba6587f3 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/crates/git_ui/src/commit_view.rs @@ -513,7 +513,7 @@ impl Item for CommitView { } } - fn as_searchable(&self, _: &Entity) -> Option> { + fn as_searchable(&self, _: &Entity, _: &App) -> Option> { Some(Box::new(self.editor.clone())) } diff --git a/crates/git_ui/src/file_diff_view.rs b/crates/git_ui/src/file_diff_view.rs index 1599ccd12415e366adcf6a42d67a5c5d77a52151..e6ed8feb7f69493d3731d9d382cf9b955059fcc4 100644 --- a/crates/git_ui/src/file_diff_view.rs +++ b/crates/git_ui/src/file_diff_view.rs @@ -278,7 +278,7 @@ impl Item for FileDiffView { } } - fn as_searchable(&self, _: &Entity) -> Option> { + fn as_searchable(&self, _: &Entity, _: &App) -> Option> { Some(Box::new(self.editor.clone())) } diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 515056bef48889ecc4da5c3f3f1a8983f9d56841..0a8667ba6c753f9b7925948f212388f0668c1c92 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -8,7 +8,7 @@ use anyhow::{Context as _, Result, anyhow}; use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus}; use collections::{HashMap, HashSet}; use editor::{ - Addon, Editor, EditorEvent, SelectionEffects, + Addon, Editor, EditorEvent, SelectionEffects, SplittableEditor, actions::{GoToHunk, GoToPreviousHunk}, multibuffer_context_lines, scroll::Autoscroll, @@ -56,7 +56,8 @@ actions!( Add, /// Shows the diff between the working directory and your default /// branch (typically main or master). - BranchDiff + BranchDiff, + LeaderAndFollower, ] ); @@ -64,7 +65,7 @@ pub struct ProjectDiff { project: Entity, multibuffer: Entity, branch_diff: Entity, - editor: Entity, + editor: Entity, buffer_diff_subscriptions: HashMap, (Entity, Subscription)>, workspace: WeakEntity, focus_handle: FocusHandle, @@ -172,7 +173,9 @@ impl ProjectDiff { pub fn autoscroll(&self, cx: &mut Context) { self.editor.update(cx, |editor, cx| { - editor.request_autoscroll(Autoscroll::fit(), cx); + editor.primary_editor().update(cx, |editor, cx| { + editor.request_autoscroll(Autoscroll::fit(), cx); + }) }) } @@ -226,44 +229,44 @@ impl ProjectDiff { cx: &mut Context, ) -> Self { let focus_handle = cx.focus_handle(); - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); + let multibuffer = cx.new(|cx| { + let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); + multibuffer.set_all_diff_hunks_expanded(cx); + multibuffer + }); let editor = cx.new(|cx| { - let mut diff_display_editor = - Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx); - diff_display_editor.disable_diagnostics(cx); - diff_display_editor.set_expand_all_diff_hunks(cx); - - match branch_diff.read(cx).diff_base() { - DiffBase::Head => { - diff_display_editor.register_addon(GitPanelAddon { - workspace: workspace.downgrade(), - }); - } - DiffBase::Merge { .. } => { - diff_display_editor.register_addon(BranchDiffAddon { - branch_diff: branch_diff.clone(), - }); - diff_display_editor.start_temporary_diff_override(); - diff_display_editor.set_render_diff_hunk_controls( - Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()), - cx, - ); - // - } - } + let diff_display_editor = SplittableEditor::new_unsplit( + multibuffer.clone(), + project.clone(), + workspace.clone(), + window, + cx, + ); diff_display_editor - }); - window.defer(cx, { - let workspace = workspace.clone(); - let editor = editor.clone(); - move |window, cx| { - workspace.update(cx, |workspace, cx| { - editor.update(cx, |editor, cx| { - editor.added_to_workspace(workspace, window, cx); - }) + .primary_editor() + .update(cx, |editor, cx| { + editor.disable_diagnostics(cx); + + match branch_diff.read(cx).diff_base() { + DiffBase::Head => { + editor.register_addon(GitPanelAddon { + workspace: workspace.downgrade(), + }); + } + DiffBase::Merge { .. } => { + editor.register_addon(BranchDiffAddon { + branch_diff: branch_diff.clone(), + }); + editor.start_temporary_diff_override(); + editor.set_render_diff_hunk_controls( + Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()), + cx, + ); + } + } }); - } + diff_display_editor }); cx.subscribe_in(&editor, window, Self::handle_editor_event) .detach(); @@ -343,7 +346,7 @@ impl ProjectDiff { } pub fn active_path(&self, cx: &App) -> Option { - let editor = self.editor.read(cx); + let editor = self.editor.read(cx).last_selected_editor().read(cx); let position = editor.selections.newest_anchor().head(); let multi_buffer = editor.buffer().read(cx); let (_, buffer, _) = multi_buffer.excerpt_containing(position, cx)?; @@ -358,14 +361,16 @@ impl ProjectDiff { fn move_to_path(&mut self, path_key: PathKey, window: &mut Window, cx: &mut Context) { if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) { self.editor.update(cx, |editor, cx| { - editor.change_selections( - SelectionEffects::scroll(Autoscroll::focused()), - window, - cx, - |s| { - s.select_ranges([position..position]); - }, - ) + editor.primary_editor().update(cx, |editor, cx| { + editor.change_selections( + SelectionEffects::scroll(Autoscroll::focused()), + window, + cx, + |s| { + s.select_ranges([position..position]); + }, + ) + }) }); } else { self.pending_scroll = Some(path_key); @@ -373,7 +378,7 @@ impl ProjectDiff { } fn button_states(&self, cx: &App) -> ButtonStates { - let editor = self.editor.read(cx); + let editor = self.editor.read(cx).primary_editor().read(cx); let snapshot = self.multibuffer.read(cx).snapshot(cx); let prev_next = snapshot.diff_hunks().nth(1).is_some(); let mut selection = true; @@ -384,7 +389,13 @@ impl ProjectDiff { .collect::>(); if !ranges.iter().any(|range| range.start != range.end) { selection = false; - if let Some((excerpt_id, _, range)) = self.editor.read(cx).active_excerpt(cx) { + if let Some((excerpt_id, _, range)) = self + .editor + .read(cx) + .primary_editor() + .read(cx) + .active_excerpt(cx) + { ranges = vec![multi_buffer::Anchor::range_in_buffer(excerpt_id, range)]; } else { ranges = Vec::default(); @@ -432,7 +443,7 @@ impl ProjectDiff { fn handle_editor_event( &mut self, - editor: &Entity, + editor: &Entity, event: &EditorEvent, window: &mut Window, cx: &mut Context, @@ -476,9 +487,12 @@ impl ProjectDiff { self.buffer_diff_subscriptions .insert(path_key.path.clone(), (diff.clone(), subscription)); + // TODO(split-diff) we shouldn't have a conflict addon when split let conflict_addon = self .editor .read(cx) + .primary_editor() + .read(cx) .addon::() .expect("project diff editor should have a conflict addon"); @@ -518,20 +532,27 @@ impl ProjectDiff { }); self.editor.update(cx, |editor, cx| { - if was_empty { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { - // TODO select the very beginning (possibly inside a deletion) - selections - .select_ranges([multi_buffer::Anchor::min()..multi_buffer::Anchor::min()]) - }); - } - if is_excerpt_newly_added - && (file_status.is_deleted() - || (file_status.is_untracked() - && GitPanelSettings::get_global(cx).collapse_untracked_diff)) - { - editor.fold_buffer(snapshot.text.remote_id(), cx) - } + editor.primary_editor().update(cx, |editor, cx| { + if was_empty { + editor.change_selections( + SelectionEffects::no_scroll(), + window, + cx, + |selections| { + selections.select_ranges([ + multi_buffer::Anchor::min()..multi_buffer::Anchor::min() + ]) + }, + ); + } + if is_excerpt_newly_added + && (file_status.is_deleted() + || (file_status.is_untracked() + && GitPanelSettings::get_global(cx).collapse_untracked_diff)) + { + editor.fold_buffer(snapshot.text.remote_id(), cx) + } + }) }); if self.multibuffer.read(cx).is_empty() @@ -650,8 +671,11 @@ impl Item for ProjectDiff { } fn deactivated(&mut self, window: &mut Window, cx: &mut Context) { - self.editor - .update(cx, |editor, cx| editor.deactivated(window, cx)); + self.editor.update(cx, |editor, cx| { + editor.primary_editor().update(cx, |primary_editor, cx| { + primary_editor.deactivated(window, cx); + }) + }); } fn navigate( @@ -660,8 +684,11 @@ impl Item for ProjectDiff { window: &mut Window, cx: &mut Context, ) -> bool { - self.editor - .update(cx, |editor, cx| editor.navigate(data, window, cx)) + self.editor.update(cx, |editor, cx| { + editor.primary_editor().update(cx, |primary_editor, cx| { + primary_editor.navigate(data, window, cx) + }) + }) } fn tab_tooltip_text(&self, _: &App) -> Option { @@ -689,8 +716,9 @@ impl Item for ProjectDiff { Some("Project Diff Opened") } - fn as_searchable(&self, _: &Entity) -> Option> { - Some(Box::new(self.editor.clone())) + fn as_searchable(&self, _: &Entity, cx: &App) -> Option> { + // TODO(split-diff) SplitEditor should be searchable + Some(Box::new(self.editor.read(cx).primary_editor().clone())) } fn for_each_project_item( @@ -698,7 +726,11 @@ impl Item for ProjectDiff { cx: &App, f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem), ) { - self.editor.for_each_project_item(cx, f) + self.editor + .read(cx) + .primary_editor() + .read(cx) + .for_each_project_item(cx, f) } fn set_nav_history( @@ -707,8 +739,10 @@ impl Item for ProjectDiff { _: &mut Window, cx: &mut Context, ) { - self.editor.update(cx, |editor, _| { - editor.set_nav_history(Some(nav_history)); + self.editor.update(cx, |editor, cx| { + editor.primary_editor().update(cx, |primary_editor, _| { + primary_editor.set_nav_history(Some(nav_history)); + }) }); } @@ -752,7 +786,11 @@ impl Item for ProjectDiff { window: &mut Window, cx: &mut Context, ) -> Task> { - self.editor.save(options, project, window, cx) + self.editor.update(cx, |editor, cx| { + editor.primary_editor().update(cx, |primary_editor, cx| { + primary_editor.save(options, project, window, cx) + }) + }) } fn save_as( @@ -771,19 +809,23 @@ impl Item for ProjectDiff { window: &mut Window, cx: &mut Context, ) -> Task> { - self.editor.reload(project, window, cx) + self.editor.update(cx, |editor, cx| { + editor.primary_editor().update(cx, |primary_editor, cx| { + primary_editor.reload(project, window, cx) + }) + }) } fn act_as_type<'a>( &'a self, type_id: TypeId, self_handle: &'a Entity, - _: &'a App, + cx: &'a App, ) -> Option { if type_id == TypeId::of::() { Some(self_handle.clone().into()) } else if type_id == TypeId::of::() { - Some(self.editor.clone().into()) + Some(self.editor.read(cx).primary_editor().clone().into()) } else { None } @@ -794,7 +836,11 @@ impl Item for ProjectDiff { } fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option> { - self.editor.breadcrumbs(theme, cx) + self.editor + .read(cx) + .last_selected_editor() + .read(cx) + .breadcrumbs(theme, cx) } fn added_to_workspace( @@ -1629,7 +1675,7 @@ mod tests { ); cx.run_until_parked(); - let editor = diff.read_with(cx, |diff, _| diff.editor.clone()); + let editor = diff.read_with(cx, |diff, cx| diff.editor.read(cx).primary_editor().clone()); assert_state_with_diff( &editor, cx, @@ -1685,7 +1731,7 @@ mod tests { window, cx, ); - diff.editor.clone() + diff.editor.read(cx).primary_editor().clone() }); assert_state_with_diff( &editor, @@ -1706,7 +1752,7 @@ mod tests { window, cx, ); - diff.editor.clone() + diff.editor.read(cx).primary_editor().clone() }); assert_state_with_diff( &editor, @@ -1759,7 +1805,8 @@ mod tests { ); cx.run_until_parked(); - let diff_editor = diff.read_with(cx, |diff, _| diff.editor.clone()); + let diff_editor = + diff.read_with(cx, |diff, cx| diff.editor.read(cx).primary_editor().clone()); assert_state_with_diff( &diff_editor, @@ -1883,7 +1930,7 @@ mod tests { workspace.active_item_as::(cx).unwrap() }); cx.focus(&item); - let editor = item.read_with(cx, |item, _| item.editor.clone()); + let editor = item.read_with(cx, |item, cx| item.editor.read(cx).primary_editor().clone()); let mut cx = EditorTestContext::for_editor_in(editor, cx).await; @@ -1997,7 +2044,7 @@ mod tests { workspace.active_item_as::(cx).unwrap() }); cx.focus(&item); - let editor = item.read_with(cx, |item, _| item.editor.clone()); + let editor = item.read_with(cx, |item, cx| item.editor.read(cx).primary_editor().clone()); let mut cx = EditorTestContext::for_editor_in(editor, cx).await; @@ -2044,7 +2091,7 @@ mod tests { cx.run_until_parked(); cx.update(|window, cx| { - let editor = diff.read(cx).editor.clone(); + let editor = diff.read(cx).editor.read(cx).primary_editor().clone(); let excerpt_ids = editor.read(cx).buffer().read(cx).excerpt_ids(); assert_eq!(excerpt_ids.len(), 1); let excerpt_id = excerpt_ids[0]; @@ -2061,6 +2108,8 @@ mod tests { .read(cx) .editor .read(cx) + .primary_editor() + .read(cx) .addon::() .unwrap() .conflict_set(buffer_id) @@ -2144,7 +2193,7 @@ mod tests { ); cx.run_until_parked(); - let editor = diff.read_with(cx, |diff, _| diff.editor.clone()); + let editor = diff.read_with(cx, |diff, cx| diff.editor.read(cx).primary_editor().clone()); assert_state_with_diff( &editor, @@ -2255,7 +2304,7 @@ mod tests { ); cx.run_until_parked(); - let editor = diff.read_with(cx, |diff, _| diff.editor.clone()); + let editor = diff.read_with(cx, |diff, cx| diff.editor.read(cx).primary_editor().clone()); assert_state_with_diff( &editor, @@ -2349,7 +2398,7 @@ mod tests { workspace.active_item_as::(cx).unwrap() }); cx.focus(&item); - let editor = item.read_with(cx, |item, _| item.editor.clone()); + let editor = item.read_with(cx, |item, cx| item.editor.read(cx).primary_editor().clone()); fs.set_head_and_index_for_repo( Path::new(path!("/project/.git")), diff --git a/crates/git_ui/src/text_diff_view.rs b/crates/git_ui/src/text_diff_view.rs index f95c2626f6c45fd50348daea599ee114231e9426..5a8f0f79592f9161ae9c7ed7f0dc2814eacc2e53 100644 --- a/crates/git_ui/src/text_diff_view.rs +++ b/crates/git_ui/src/text_diff_view.rs @@ -339,7 +339,7 @@ impl Item for TextDiffView { } } - fn as_searchable(&self, _: &Entity) -> Option> { + fn as_searchable(&self, _: &Entity, _: &App) -> Option> { Some(Box::new(self.diff_editor.clone())) } diff --git a/crates/language_tools/src/lsp_log_view.rs b/crates/language_tools/src/lsp_log_view.rs index 4cf47cab079617d55aeeb959dcad116919a55609..df24f469495a2396410408a68f7310d1546eefde 100644 --- a/crates/language_tools/src/lsp_log_view.rs +++ b/crates/language_tools/src/lsp_log_view.rs @@ -744,7 +744,11 @@ impl Item for LspLogView { None } - fn as_searchable(&self, handle: &Entity) -> Option> { + fn as_searchable( + &self, + handle: &Entity, + _: &App, + ) -> Option> { Some(Box::new(handle.clone())) } diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 3e9d113a300fad6f8c29221d0f886497793fafc9..680e7b1c48a9d858180da95203ed0fc8a9299af2 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -36,7 +36,9 @@ use std::{ any::type_name, borrow::Cow, cell::{Cell, Ref, RefCell}, - cmp, fmt, + cmp, + collections::VecDeque, + fmt::{self, Debug}, future::Future, io, iter::{self, FromIterator}, @@ -61,6 +63,9 @@ pub use self::path_key::PathKey; #[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct ExcerptId(u32); +#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct BaseTextRow(pub u32); + /// One or more [`Buffers`](Buffer) being edited in a single view. /// /// See @@ -87,6 +92,14 @@ pub struct MultiBuffer { /// The writing capability of the multi-buffer. capability: Capability, buffer_changed_since_sync: Rc>, + follower: Option>, + filter_mode: Option, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MultiBufferFilterMode { + KeepInsertions, + KeepDeletions, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -549,24 +562,31 @@ pub struct MultiBufferSnapshot { #[derive(Debug, Clone)] enum DiffTransform { - BufferContent { + Unmodified { summary: MBTextSummary, - inserted_hunk_info: Option, + }, + InsertedHunk { + summary: MBTextSummary, + hunk_info: DiffTransformHunkInfo, + }, + FilteredInsertedHunk { + summary: MBTextSummary, + hunk_info: DiffTransformHunkInfo, }, DeletedHunk { summary: TextSummary, buffer_id: BufferId, hunk_info: DiffTransformHunkInfo, - base_text_byte_range: Range, has_trailing_newline: bool, }, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] struct DiffTransformHunkInfo { excerpt_id: ExcerptId, hunk_start_anchor: text::Anchor, hunk_secondary_status: DiffHunkSecondaryStatus, + base_text_byte_range: Range, } impl Eq for DiffTransformHunkInfo {} @@ -593,6 +613,15 @@ pub struct ExcerptInfo { pub end_row: MultiBufferRow, } +/// Used with [`MultiBuffer::push_buffer_content_transform`] +#[derive(Clone, Debug)] +struct CurrentInsertedHunk { + hunk_excerpt_start: ExcerptOffset, + insertion_end_offset: ExcerptOffset, + hunk_info: DiffTransformHunkInfo, + is_filtered: bool, +} + impl std::fmt::Debug for ExcerptInfo { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct(type_name::()) @@ -632,6 +661,7 @@ pub struct ExpandInfo { pub struct RowInfo { pub buffer_id: Option, pub buffer_row: Option, + pub base_text_row: Option, pub multibuffer_row: Option, pub diff_status: Option, pub expand_info: Option, @@ -927,7 +957,7 @@ impl<'a, MBD: MultiBufferDimension> Dimension<'a, DiffTransformSummary> for Diff struct MultiBufferCursor<'a, MBD, BD> { excerpts: Cursor<'a, 'static, Excerpt, ExcerptDimension>, diff_transforms: Cursor<'a, 'static, DiffTransform, DiffTransforms>, - diffs: &'a TreeMap, + snapshot: &'a MultiBufferSnapshot, cached_region: Option>, } @@ -938,10 +968,21 @@ struct MultiBufferRegion<'a, MBD, BD> { diff_hunk_status: Option, excerpt: &'a Excerpt, buffer_range: Range, + diff_base_byte_range: Option>, range: Range, has_trailing_newline: bool, } +impl<'a, MBD, BD> MultiBufferRegion<'a, MBD, BD> +where + MBD: Ord, + BD: Ord, +{ + fn is_filtered(&self) -> bool { + self.range.is_empty() && self.buffer_range.is_empty() && self.diff_hunk_status == None + } +} + struct ExcerptChunks<'a> { excerpt_id: ExcerptId, content_chunks: BufferChunks<'a>, @@ -1054,6 +1095,8 @@ impl MultiBuffer { paths_by_excerpt: Default::default(), buffer_changed_since_sync: Default::default(), history: History::default(), + follower: None, + filter_mode: None, } } @@ -1087,8 +1130,8 @@ impl MultiBuffer { Self { snapshot: RefCell::new(self.snapshot.borrow().clone()), buffers: buffers, - excerpts_by_path: Default::default(), - paths_by_excerpt: Default::default(), + excerpts_by_path: self.excerpts_by_path.clone(), + paths_by_excerpt: self.paths_by_excerpt.clone(), diffs: diff_bases, subscriptions: Default::default(), singleton: self.singleton, @@ -1096,6 +1139,46 @@ impl MultiBuffer { history: self.history.clone(), title: self.title.clone(), buffer_changed_since_sync, + follower: None, + filter_mode: None, + } + } + + pub fn get_or_create_follower(&mut self, cx: &mut Context) -> Entity { + use gpui::AppContext as _; + + if let Some(follower) = &self.follower { + return follower.clone(); + } + + let follower = cx.new(|cx| self.clone(cx)); + follower.update(cx, |follower, _cx| { + follower.capability = Capability::ReadOnly; + }); + self.follower = Some(follower.clone()); + follower + } + + pub fn set_filter_mode(&mut self, new_mode: Option) { + self.filter_mode = new_mode; + let excerpt_len = self + .snapshot + .get_mut() + .diff_transforms + .summary() + .excerpt_len(); + let edits = Self::sync_diff_transforms( + self.snapshot.get_mut(), + vec![Edit { + old: ExcerptDimension(MultiBufferOffset(0))..excerpt_len, + new: ExcerptDimension(MultiBufferOffset(0))..excerpt_len, + }], + // TODO(split-diff) is this right? + DiffChangeKind::BufferEdited, + new_mode, + ); + if !edits.is_empty() { + self.subscriptions.publish(edits); } } @@ -1578,7 +1661,7 @@ impl MultiBuffer { cx: &mut Context, ) -> Vec where - O: text::ToOffset, + O: text::ToOffset + Clone, { self.insert_excerpts_after(ExcerptId::max(), buffer, ranges, cx) } @@ -1616,7 +1699,7 @@ impl MultiBuffer { cx: &mut Context, ) -> Vec where - O: text::ToOffset, + O: text::ToOffset + Clone, { let mut ids = Vec::new(); let mut next_excerpt_id = @@ -1645,10 +1728,13 @@ impl MultiBuffer { ranges: impl IntoIterator)>, cx: &mut Context, ) where - O: text::ToOffset, + O: text::ToOffset + Clone, { + // TODO(split-diff) see if it's worth time avoiding collecting here later + let collected_ranges: Vec<_> = ranges.into_iter().collect(); + assert_eq!(self.history.transaction_depth(), 0); - let mut ranges = ranges.into_iter().peekable(); + let mut ranges = collected_ranges.iter().cloned().peekable(); if ranges.peek().is_none() { return Default::default(); } @@ -1748,11 +1834,23 @@ impl MultiBuffer { new: edit_start..edit_end, }], DiffChangeKind::BufferEdited, + self.filter_mode, ); if !edits.is_empty() { self.subscriptions.publish(edits); } + if let Some(follower) = &self.follower { + follower.update(cx, |follower, cx| { + follower.insert_excerpts_with_ids_after( + prev_excerpt_id, + buffer.clone(), + collected_ranges, + cx, + ); + }) + } + cx.emit(Event::Edited { edited_buffer: None, }); @@ -1802,10 +1900,16 @@ impl MultiBuffer { new: start..start, }], DiffChangeKind::BufferEdited, + self.filter_mode, ); if !edits.is_empty() { self.subscriptions.publish(edits); } + if let Some(follower) = &self.follower { + follower.update(cx, |follower, cx| { + follower.clear(cx); + }) + } cx.emit(Event::Edited { edited_buffer: None, }); @@ -2094,10 +2198,22 @@ impl MultiBuffer { snapshot.trailing_excerpt_update_count += 1; } - let edits = Self::sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited); + let edits = Self::sync_diff_transforms( + &mut snapshot, + edits, + DiffChangeKind::BufferEdited, + self.filter_mode, + ); if !edits.is_empty() { self.subscriptions.publish(edits); } + + if let Some(follower) = &self.follower { + follower.update(cx, |follower, cx| { + follower.remove_excerpts(ids.clone(), cx); + }) + } + cx.emit(Event::Edited { edited_buffer: None, }); @@ -2253,6 +2369,7 @@ impl MultiBuffer { DiffChangeKind::DiffUpdated { base_changed: base_text_changed, }, + self.filter_mode, ); if !edits.is_empty() { self.subscriptions.publish(edits); @@ -2415,7 +2532,14 @@ impl MultiBuffer { text::Anchor::min_max_range_for_buffer(buffer_id), cx, ); - self.diffs.insert(buffer_id, DiffState::new(diff, cx)); + self.diffs + .insert(buffer_id, DiffState::new(diff.clone(), cx)); + + if let Some(follower) = &self.follower { + follower.update(cx, |follower, cx| { + follower.add_diff(diff, cx); + }) + } } pub fn diff_for(&self, buffer_id: BufferId) -> Option> { @@ -2528,6 +2652,7 @@ impl MultiBuffer { &mut snapshot, excerpt_edits, DiffChangeKind::ExpandOrCollapseHunks { expand }, + self.filter_mode, ); if !edits.is_empty() { self.subscriptions.publish(edits); @@ -2615,7 +2740,16 @@ impl MultiBuffer { drop(cursor); snapshot.excerpts = new_excerpts; - let edits = Self::sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited); + if let Some(follower) = &self.follower { + follower.update(cx, |follower, cx| follower.resize_excerpt(id, range, cx)); + } + + let edits = Self::sync_diff_transforms( + &mut snapshot, + edits, + DiffChangeKind::BufferEdited, + self.filter_mode, + ); if !edits.is_empty() { self.subscriptions.publish(edits); } @@ -2649,7 +2783,7 @@ impl MultiBuffer { let mut cursor = snapshot .excerpts .cursor::, ExcerptOffset>>(()); - let mut edits = Vec::>::new(); + let mut excerpt_edits = Vec::>::new(); for locator in &locators { let prefix = cursor.slice(&Some(locator), Bias::Left); @@ -2701,15 +2835,15 @@ impl MultiBuffer { new: new_start_offset..new_start_offset + new_text_len, }; - if let Some(last_edit) = edits.last_mut() { + if let Some(last_edit) = excerpt_edits.last_mut() { if last_edit.old.end == edit.old.start { last_edit.old.end = edit.old.end; last_edit.new.end = edit.new.end; } else { - edits.push(edit); + excerpt_edits.push(edit); } } else { - edits.push(edit); + excerpt_edits.push(edit); } new_excerpts.push(excerpt, ()); @@ -2720,12 +2854,22 @@ impl MultiBuffer { new_excerpts.append(cursor.suffix(), ()); drop(cursor); - snapshot.excerpts = new_excerpts; + snapshot.excerpts = new_excerpts.clone(); - let edits = Self::sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited); + let edits = Self::sync_diff_transforms( + &mut snapshot, + excerpt_edits.clone(), + DiffChangeKind::BufferEdited, + self.filter_mode, + ); if !edits.is_empty() { self.subscriptions.publish(edits); } + if let Some(follower) = &self.follower { + follower.update(cx, |follower, cx| { + follower.expand_excerpts(ids.clone(), line_count, direction, cx); + }) + } cx.emit(Event::Edited { edited_buffer: None, }); @@ -2738,10 +2882,11 @@ impl MultiBuffer { if !changed { return; } - let edits = Self::sync_( + let edits = Self::sync_from_buffer_changes( &mut self.snapshot.borrow_mut(), &self.buffers, &self.diffs, + self.filter_mode, cx, ); if !edits.is_empty() { @@ -2754,17 +2899,24 @@ impl MultiBuffer { if !changed { return; } - let edits = Self::sync_(self.snapshot.get_mut(), &self.buffers, &self.diffs, cx); + let edits = Self::sync_from_buffer_changes( + self.snapshot.get_mut(), + &self.buffers, + &self.diffs, + self.filter_mode, + cx, + ); if !edits.is_empty() { self.subscriptions.publish(edits); } } - fn sync_( + fn sync_from_buffer_changes( snapshot: &mut MultiBufferSnapshot, buffers: &HashMap, diffs: &HashMap, + filter_mode: Option, cx: &App, ) -> Vec> { let MultiBufferSnapshot { @@ -2887,13 +3039,14 @@ impl MultiBuffer { drop(cursor); *excerpts = new_excerpts; - Self::sync_diff_transforms(snapshot, edits, DiffChangeKind::BufferEdited) + Self::sync_diff_transforms(snapshot, edits, DiffChangeKind::BufferEdited, filter_mode) } fn sync_diff_transforms( snapshot: &mut MultiBufferSnapshot, excerpt_edits: Vec>, change_kind: DiffChangeKind, + filter_mode: Option, ) -> Vec> { if excerpt_edits.is_empty() { return vec![]; @@ -2910,8 +3063,8 @@ impl MultiBuffer { let mut at_transform_boundary = true; let mut end_of_current_insert = None; - let mut excerpt_edits = excerpt_edits.into_iter().peekable(); - while let Some(edit) = excerpt_edits.next() { + let mut excerpt_edits: VecDeque<_> = excerpt_edits.into_iter().collect(); + while let Some(edit) = excerpt_edits.pop_front() { excerpts.seek_forward(&edit.new.start, Bias::Right); if excerpts.item().is_none() && *excerpts.start() == edit.new.start { excerpts.prev(); @@ -2932,7 +3085,13 @@ impl MultiBuffer { } // Compute the start of the edit in output coordinates. - let edit_start_overshoot = edit.old.start - old_diff_transforms.start().0; + let edit_start_overshoot = if let Some(DiffTransform::FilteredInsertedHunk { .. }) = + old_diff_transforms.item() + { + 0 + } else { + edit.old.start - old_diff_transforms.start().0 + }; let edit_old_start = old_diff_transforms.start().1 + edit_start_overshoot; let edit_new_start = MultiBufferOffset((edit_old_start.0 as isize + output_delta) as usize); @@ -2946,12 +3105,56 @@ impl MultiBuffer { &mut old_expanded_hunks, snapshot, change_kind, + filter_mode, ); + // When the added range of a hunk is edited, the end anchor of the hunk may be moved later + // in response by hunks_intersecting_range to keep it at a row boundary. In KeepDeletions + // mode, we need to make sure that the whole added range is still filtered out in this situation. + // We do that by adding an additional edit that covers the rest of the hunk added range. + if let Some(current_inserted_hunk) = &end_of_current_insert + && current_inserted_hunk.is_filtered + // No additional edit needed if we've already covered the whole added range. + && current_inserted_hunk.insertion_end_offset > edit.new.end + // No additional edit needed if this edit just touched the start of the hunk + // (this also prevents pushing the deleted region for the hunk twice). + && edit.new.end > current_inserted_hunk.hunk_excerpt_start + // No additional edit needed if there is a subsequent edit that intersects + // the same hunk (the last such edit will take care of it). + && excerpt_edits.front().is_none_or(|next_edit| { + next_edit.new.start >= current_inserted_hunk.insertion_end_offset + }) + { + let overshoot = current_inserted_hunk.insertion_end_offset - edit.new.end; + let additional_edit = Edit { + old: edit.old.end..edit.old.end + overshoot, + new: edit.new.end..current_inserted_hunk.insertion_end_offset, + }; + excerpt_edits.push_front(additional_edit); + } + // Compute the end of the edit in output coordinates. - let edit_old_end_overshoot = edit.old.end - old_diff_transforms.start().0; - let edit_new_end_overshoot = edit.new.end - new_diff_transforms.summary().excerpt_len(); - let edit_old_end = old_diff_transforms.start().1 + edit_old_end_overshoot; + let edit_old_end_overshoot = if let Some(DiffTransform::FilteredInsertedHunk { + .. + }) = old_diff_transforms.item() + { + ExcerptDimension(MultiBufferOffset(0)) + } else { + ExcerptDimension(MultiBufferOffset( + edit.old.end - old_diff_transforms.start().0, + )) + }; + let edit_new_end_overshoot = if let Some(current_inserted_hunk) = &end_of_current_insert + && current_inserted_hunk.is_filtered + { + let insertion_end_offset = current_inserted_hunk.insertion_end_offset; + let excerpt_len = new_diff_transforms.summary().excerpt_len(); + let base = insertion_end_offset.max(excerpt_len); + edit.new.end.saturating_sub(base) + } else { + edit.new.end - new_diff_transforms.summary().excerpt_len() + }; + let edit_old_end = old_diff_transforms.start().1 + edit_old_end_overshoot.0; let edit_new_end = new_diff_transforms.summary().output.len + edit_new_end_overshoot; let output_edit = Edit { old: edit_old_start..edit_old_end, @@ -2968,16 +3171,16 @@ impl MultiBuffer { // then recreate the content up to the end of this transform, to prepare // for reusing additional slices of the old transforms. if excerpt_edits - .peek() + .front() .is_none_or(|next_edit| next_edit.old.start >= old_diff_transforms.end().0) { let keep_next_old_transform = (old_diff_transforms.start().0 >= edit.old.end) && match old_diff_transforms.item() { - Some(DiffTransform::BufferContent { - inserted_hunk_info: Some(hunk), - .. - }) => excerpts.item().is_some_and(|excerpt| { - hunk.hunk_start_anchor.is_valid(&excerpt.buffer) + Some( + DiffTransform::InsertedHunk { hunk_info, .. } + | DiffTransform::FilteredInsertedHunk { hunk_info, .. }, + ) => excerpts.item().is_some_and(|excerpt| { + hunk_info.hunk_start_anchor.is_valid(&excerpt.buffer) }), _ => true, }; @@ -2993,7 +3196,7 @@ impl MultiBuffer { snapshot, &mut new_diff_transforms, excerpt_offset, - end_of_current_insert, + end_of_current_insert.as_ref(), ); at_transform_boundary = true; } @@ -3005,9 +3208,8 @@ impl MultiBuffer { // Ensure there's always at least one buffer content transform. if new_diff_transforms.is_empty() { new_diff_transforms.push( - DiffTransform::BufferContent { + DiffTransform::Unmodified { summary: Default::default(), - inserted_hunk_info: None, }, (), ); @@ -3031,10 +3233,11 @@ impl MultiBuffer { Dimensions, >, new_diff_transforms: &mut SumTree, - end_of_current_insert: &mut Option<(ExcerptOffset, DiffTransformHunkInfo)>, + end_of_current_insert: &mut Option, old_expanded_hunks: &mut HashSet, snapshot: &MultiBufferSnapshot, change_kind: DiffChangeKind, + filter_mode: Option, ) -> bool { log::trace!( "recomputing diff transform for edit {:?} => {:?}", @@ -3100,6 +3303,7 @@ impl MultiBuffer { excerpt_id: excerpt.id, hunk_start_anchor: hunk.buffer_range.start, hunk_secondary_status: hunk.secondary_status, + base_text_byte_range: hunk.diff_base_byte_range.clone(), }; let hunk_excerpt_start = excerpt_start @@ -3111,7 +3315,7 @@ impl MultiBuffer { snapshot, new_diff_transforms, hunk_excerpt_start, - *end_of_current_insert, + end_of_current_insert.as_ref(), ); // For every existing hunk, determine if it was previously expanded @@ -3144,6 +3348,7 @@ impl MultiBuffer { if !hunk.diff_base_byte_range.is_empty() && hunk_buffer_range.start >= edit_buffer_start && hunk_buffer_range.start <= excerpt_buffer_end + && filter_mode != Some(MultiBufferFilterMode::KeepInsertions) { let base_text = diff.base_text(); let mut text_cursor = @@ -3159,10 +3364,9 @@ impl MultiBuffer { new_diff_transforms.push( DiffTransform::DeletedHunk { - base_text_byte_range: hunk.diff_base_byte_range.clone(), summary: base_text_summary, buffer_id: excerpt.buffer_id, - hunk_info, + hunk_info: hunk_info.clone(), has_trailing_newline, }, (), @@ -3170,8 +3374,15 @@ impl MultiBuffer { } if !hunk_buffer_range.is_empty() { - *end_of_current_insert = - Some((hunk_excerpt_end.min(excerpt_end), hunk_info)); + let is_filtered = + filter_mode == Some(MultiBufferFilterMode::KeepDeletions); + let insertion_end_offset = hunk_excerpt_end.min(excerpt_end); + *end_of_current_insert = Some(CurrentInsertedHunk { + hunk_excerpt_start, + insertion_end_offset, + hunk_info, + is_filtered, + }); } } } @@ -3191,15 +3402,8 @@ impl MultiBuffer { new_transforms: &mut SumTree, subtree: SumTree, ) { - if let Some(DiffTransform::BufferContent { - inserted_hunk_info, - summary, - }) = subtree.first() - && Self::extend_last_buffer_content_transform( - new_transforms, - *inserted_hunk_info, - *summary, - ) + if let Some(transform) = subtree.first() + && Self::extend_last_buffer_content_transform(new_transforms, transform) { let mut cursor = subtree.cursor::<()>(()); cursor.next(); @@ -3211,16 +3415,7 @@ impl MultiBuffer { } fn push_diff_transform(new_transforms: &mut SumTree, transform: DiffTransform) { - if let DiffTransform::BufferContent { - inserted_hunk_info: inserted_hunk_anchor, - summary, - } = transform - && Self::extend_last_buffer_content_transform( - new_transforms, - inserted_hunk_anchor, - summary, - ) - { + if Self::extend_last_buffer_content_transform(new_transforms, &transform) { return; } new_transforms.push(transform, ()); @@ -3230,55 +3425,56 @@ impl MultiBuffer { old_snapshot: &MultiBufferSnapshot, new_transforms: &mut SumTree, end_offset: ExcerptOffset, - current_inserted_hunk: Option<(ExcerptOffset, DiffTransformHunkInfo)>, + current_inserted_hunk: Option<&CurrentInsertedHunk>, ) { - let inserted_region = current_inserted_hunk.map(|(insertion_end_offset, hunk_info)| { - (end_offset.min(insertion_end_offset), Some(hunk_info)) - }); - let unchanged_region = [(end_offset, None)]; - - for (end_offset, inserted_hunk_info) in inserted_region.into_iter().chain(unchanged_region) - { + if let Some(current_inserted_hunk) = current_inserted_hunk { let start_offset = new_transforms.summary().excerpt_len(); - if end_offset <= start_offset { - continue; + let end_offset = current_inserted_hunk.insertion_end_offset.min(end_offset); + if end_offset > start_offset { + let summary_to_add = old_snapshot + .text_summary_for_excerpt_offset_range::( + start_offset..end_offset, + ); + + let transform = if current_inserted_hunk.is_filtered { + DiffTransform::FilteredInsertedHunk { + summary: summary_to_add, + hunk_info: current_inserted_hunk.hunk_info.clone(), + } + } else { + DiffTransform::InsertedHunk { + summary: summary_to_add, + hunk_info: current_inserted_hunk.hunk_info.clone(), + } + }; + if !Self::extend_last_buffer_content_transform(new_transforms, &transform) { + new_transforms.push(transform, ()) + } } + } + + let start_offset = new_transforms.summary().excerpt_len(); + if end_offset > start_offset { let summary_to_add = old_snapshot .text_summary_for_excerpt_offset_range::(start_offset..end_offset); - if !Self::extend_last_buffer_content_transform( - new_transforms, - inserted_hunk_info, - summary_to_add, - ) { - new_transforms.push( - DiffTransform::BufferContent { - summary: summary_to_add, - inserted_hunk_info, - }, - (), - ) + let transform = DiffTransform::Unmodified { + summary: summary_to_add, + }; + if !Self::extend_last_buffer_content_transform(new_transforms, &transform) { + new_transforms.push(transform, ()) } } } fn extend_last_buffer_content_transform( new_transforms: &mut SumTree, - new_inserted_hunk_info: Option, - summary_to_add: MBTextSummary, + transform: &DiffTransform, ) -> bool { let mut did_extend = false; new_transforms.update_last( |last_transform| { - if let DiffTransform::BufferContent { - summary, - inserted_hunk_info: inserted_hunk_anchor, - } = last_transform - && *inserted_hunk_anchor == new_inserted_hunk_info - { - *summary += summary_to_add; - did_extend = true; - } + did_extend = last_transform.merge_with(&transform); }, (), ); @@ -3286,6 +3482,72 @@ impl MultiBuffer { } } +impl DiffTransform { + /// Ergonomic wrapper for [`DiffTransform::merged_with`] that applies the + /// merging in-place. Returns `true` if merging was possible. + #[must_use = "check whether merging actually succeeded"] + fn merge_with(&mut self, other: &Self) -> bool { + match self.to_owned().merged_with(other) { + Some(merged) => { + *self = merged; + true + } + None => false, + } + } + + /// Attempt to merge `self` with `other`, and return the merged transform. + /// + /// This will succeed if all of the following are true: + /// - both transforms are the same variant + /// - neither transform is [`DiffTransform::DeletedHunk`] + /// - if both transform are either [`DiffTransform::InsertedHunk`] or + /// [`DiffTransform::FilteredInsertedHunk`], then their + /// `hunk_info.hunk_start_anchor`s match + #[must_use = "check whether merging actually succeeded"] + #[rustfmt::skip] + fn merged_with(self, other: &Self) -> Option { + match (self, other) { + ( + DiffTransform::Unmodified { mut summary }, + DiffTransform::Unmodified { summary: other_summary }, + ) => { + summary += *other_summary; + Some(DiffTransform::Unmodified { summary }) + } + ( + DiffTransform::FilteredInsertedHunk { mut summary, hunk_info }, + DiffTransform::FilteredInsertedHunk { + hunk_info: other_hunk_info, + summary: other_summary, + }, + ) => { + if hunk_info.hunk_start_anchor == other_hunk_info.hunk_start_anchor { + summary += *other_summary; + Some(DiffTransform::FilteredInsertedHunk { summary, hunk_info }) + } else { + None + } + } + ( + DiffTransform::InsertedHunk { mut summary, hunk_info }, + DiffTransform::InsertedHunk { + hunk_info: other_hunk_info, + summary: other_summary, + }, + ) => { + if hunk_info.hunk_start_anchor == other_hunk_info.hunk_start_anchor { + summary += *other_summary; + Some(DiffTransform::InsertedHunk { summary, hunk_info }) + } else { + None + } + } + _ => return None, + } + } +} + fn build_excerpt_ranges( ranges: impl IntoIterator>, context_line_count: u32, @@ -4517,19 +4779,20 @@ impl MultiBufferSnapshot { let end_overshoot = std::cmp::min(range.end, diff_transform_end) - diff_transform_start; let mut result = match first_transform { - DiffTransform::BufferContent { .. } => { + DiffTransform::Unmodified { .. } | DiffTransform::InsertedHunk { .. } => { let excerpt_start = cursor.start().1 + start_overshoot; let excerpt_end = cursor.start().1 + end_overshoot; self.text_summary_for_excerpt_offset_range(excerpt_start..excerpt_end) } + DiffTransform::FilteredInsertedHunk { .. } => MBD::default(), DiffTransform::DeletedHunk { buffer_id, - base_text_byte_range, has_trailing_newline, + hunk_info, .. } => { - let buffer_start = base_text_byte_range.start + start_overshoot; - let mut buffer_end = base_text_byte_range.start + end_overshoot; + let buffer_start = hunk_info.base_text_byte_range.start + start_overshoot; + let mut buffer_end = hunk_info.base_text_byte_range.start + end_overshoot; let Some(base_text) = self.diffs.get(buffer_id).map(|diff| diff.base_text()) else { panic!("{:?} is in non-existent deleted hunk", range.start) }; @@ -4571,25 +4834,26 @@ impl MultiBufferSnapshot { let overshoot = range.end - cursor.start().0; let suffix = match last_transform { - DiffTransform::BufferContent { .. } => { + DiffTransform::Unmodified { .. } | DiffTransform::InsertedHunk { .. } => { let end = cursor.start().1 + overshoot; self.text_summary_for_excerpt_offset_range::(cursor.start().1..end) } + DiffTransform::FilteredInsertedHunk { .. } => MBD::default(), DiffTransform::DeletedHunk { - base_text_byte_range, buffer_id, has_trailing_newline, + hunk_info, .. } => { - let buffer_end = base_text_byte_range.start + overshoot; + let buffer_end = hunk_info.base_text_byte_range.start + overshoot; let Some(base_text) = self.diffs.get(buffer_id).map(|diff| diff.base_text()) else { panic!("{:?} is in non-existent deleted hunk", range.end) }; let mut suffix = base_text.text_summary_for_range::( - base_text_byte_range.start..buffer_end, + hunk_info.base_text_byte_range.start..buffer_end, ); - if *has_trailing_newline && buffer_end == base_text_byte_range.end + 1 { + if *has_trailing_newline && buffer_end == hunk_info.base_text_byte_range.end + 1 { suffix.add_assign(&::from_text_summary( &TextSummary::from("\n"), )) @@ -4695,7 +4959,7 @@ impl MultiBufferSnapshot { match diff_transforms.item() { Some(DiffTransform::DeletedHunk { buffer_id, - base_text_byte_range, + hunk_info, .. }) => { if let Some(diff_base_anchor) = &anchor.diff_base_anchor @@ -4704,12 +4968,12 @@ impl MultiBufferSnapshot { && base_text.can_resolve(diff_base_anchor) { let base_text_offset = diff_base_anchor.to_offset(base_text); - if base_text_offset >= base_text_byte_range.start - && base_text_offset <= base_text_byte_range.end + if base_text_offset >= hunk_info.base_text_byte_range.start + && base_text_offset <= hunk_info.base_text_byte_range.end { let position_in_hunk = base_text .text_summary_for_range::( - base_text_byte_range.start..base_text_offset, + hunk_info.base_text_byte_range.start..base_text_offset, ); position.0.add_text_dim(&position_in_hunk); } else if at_transform_end { @@ -4723,8 +4987,14 @@ impl MultiBufferSnapshot { diff_transforms.next(); continue; } - let overshoot = excerpt_position - diff_transforms.start().0; - position += overshoot; + + if !matches!( + diff_transforms.item(), + Some(DiffTransform::FilteredInsertedHunk { .. }) + ) { + let overshoot = excerpt_position - diff_transforms.start().0; + position += overshoot; + } } } @@ -5014,11 +5284,12 @@ impl MultiBufferSnapshot { let mut diff_base_anchor = None; if let Some(DiffTransform::DeletedHunk { buffer_id, - base_text_byte_range, has_trailing_newline, + hunk_info, .. }) = diff_transforms.item() { + let base_text_byte_range = &hunk_info.base_text_byte_range; let diff = self.diffs.get(buffer_id).expect("missing diff"); if offset_in_transform > base_text_byte_range.len() { debug_assert!(*has_trailing_newline); @@ -5167,7 +5438,7 @@ impl MultiBufferSnapshot { MultiBufferCursor { excerpts, diff_transforms, - diffs: &self.diffs, + snapshot: &self, cached_region: None, } } @@ -6313,6 +6584,11 @@ impl MultiBufferSnapshot { let excerpts = self.excerpts.items(()); let excerpt_ids = self.excerpt_ids.items(()); + assert!( + self.excerpts.is_empty() || !self.diff_transforms.is_empty(), + "must be at least one diff transform if excerpts exist" + ); + for (ix, excerpt) in excerpts.iter().enumerate() { if ix == 0 { if excerpt.locator <= Locator::min() { @@ -6335,36 +6611,26 @@ impl MultiBufferSnapshot { if self.diff_transforms.summary().input != self.excerpts.summary().text { panic!( - "incorrect input summary. expected {:?}, got {:?}. transforms: {:+?}", + "incorrect input summary. expected {:#?}, got {:#?}. transforms: {:#?}", self.excerpts.summary().text, self.diff_transforms.summary().input, self.diff_transforms.items(()), ); } - let mut prev_transform: Option<&DiffTransform> = None; - for item in self.diff_transforms.iter() { - if let DiffTransform::BufferContent { - summary, - inserted_hunk_info, - } = item + for (left, right) in self.diff_transforms.iter().tuple_windows() { + use sum_tree::Item; + + if left.is_buffer_content() + && left.summary(()).input.len == MultiBufferOffset(0) + && !self.is_empty() { - if let Some(DiffTransform::BufferContent { - inserted_hunk_info: prev_inserted_hunk_info, - .. - }) = prev_transform - && *inserted_hunk_info == *prev_inserted_hunk_info - { - panic!( - "multiple adjacent buffer content transforms with is_inserted_hunk = {inserted_hunk_info:?}. transforms: {:+?}", - self.diff_transforms.items(()) - ); - } - if summary.len == MultiBufferOffset(0) && !self.is_empty() { - panic!("empty buffer content transform"); - } + panic!("empty buffer content transform in non-empty snapshot"); } - prev_transform = Some(item); + assert!( + left.clone().merged_with(right).is_none(), + "two consecutive diff transforms could have been merged, but weren't" + ); } } } @@ -6385,7 +6651,9 @@ where } let mut excerpt_position = self.diff_transforms.start().excerpt_dimension; - if let Some(DiffTransform::BufferContent { .. }) = self.diff_transforms.item() { + if let Some(item) = self.diff_transforms.item() + && item.is_buffer_content() + { let overshoot = position - self.diff_transforms.start().output_dimension; excerpt_position += overshoot; } @@ -6408,7 +6676,9 @@ where let overshoot = position - self.diff_transforms.start().output_dimension; let mut excerpt_position = self.diff_transforms.start().excerpt_dimension; - if let Some(DiffTransform::BufferContent { .. }) = self.diff_transforms.item() { + if let Some(item) = self.diff_transforms.item() + && item.is_buffer_content() + { excerpt_position += overshoot; } @@ -6447,8 +6717,12 @@ where .excerpt_dimension .cmp(&self.excerpts.end()) { - cmp::Ordering::Less => self.diff_transforms.next(), - cmp::Ordering::Greater => self.excerpts.next(), + cmp::Ordering::Less => { + self.diff_transforms.next(); + } + cmp::Ordering::Greater => { + self.excerpts.next(); + } cmp::Ordering::Equal => { self.diff_transforms.next(); if self.diff_transforms.end().excerpt_dimension > self.excerpts.end() @@ -6507,9 +6781,7 @@ where let prev_transform = self.diff_transforms.item(); self.diff_transforms.next(); - prev_transform.is_none_or(|next_transform| { - matches!(next_transform, DiffTransform::BufferContent { .. }) - }) + prev_transform.is_none_or(|prev_transform| prev_transform.is_buffer_content()) } fn is_at_end_of_excerpt(&mut self) -> bool { @@ -6523,7 +6795,9 @@ where let next_transform = self.diff_transforms.next_item(); next_transform.is_none_or(|next_transform| match next_transform { - DiffTransform::BufferContent { .. } => true, + DiffTransform::Unmodified { .. } + | DiffTransform::InsertedHunk { .. } + | DiffTransform::FilteredInsertedHunk { .. } => true, DiffTransform::DeletedHunk { hunk_info, .. } => self .excerpts .item() @@ -6546,16 +6820,16 @@ where match self.diff_transforms.item()? { DiffTransform::DeletedHunk { buffer_id, - base_text_byte_range, has_trailing_newline, hunk_info, .. } => { - let diff = self.diffs.get(buffer_id)?; + let diff = self.snapshot.diffs.get(buffer_id)?; let buffer = diff.base_text(); let mut rope_cursor = buffer.as_rope().cursor(0); - let buffer_start = rope_cursor.summary::(base_text_byte_range.start); - let buffer_range_len = rope_cursor.summary::(base_text_byte_range.end); + let buffer_start = rope_cursor.summary::(hunk_info.base_text_byte_range.start); + let buffer_range_len = + rope_cursor.summary::(hunk_info.base_text_byte_range.end); let mut buffer_end = buffer_start; TextDimension::add_assign(&mut buffer_end, &buffer_range_len); let start = self.diff_transforms.start().output_dimension.0; @@ -6570,11 +6844,20 @@ where )), buffer_range: buffer_start..buffer_end, range: start..end, + diff_base_byte_range: Some(hunk_info.base_text_byte_range.clone()), }) } - DiffTransform::BufferContent { - inserted_hunk_info, .. - } => { + transform @ (DiffTransform::Unmodified { .. } + | DiffTransform::InsertedHunk { .. } + | DiffTransform::FilteredInsertedHunk { .. }) => { + let mut diff_hunk_status = transform + .hunk_info() + .map(|hunk_info| DiffHunkStatus::added(hunk_info.hunk_secondary_status)); + + let diff_base_byte_range = transform + .hunk_info() + .map(|hunk_info| hunk_info.base_text_byte_range); + let buffer = &excerpt.buffer; let buffer_context_start = excerpt.range.context.start.summary::(buffer); @@ -6609,13 +6892,19 @@ where has_trailing_newline = excerpt.has_trailing_newline; }; + if matches!(transform, DiffTransform::FilteredInsertedHunk { .. }) { + buffer_end = buffer_start; + end = start; + diff_hunk_status = None; + } + Some(MultiBufferRegion { buffer, excerpt, has_trailing_newline, is_main_buffer: true, - diff_hunk_status: inserted_hunk_info - .map(|info| DiffHunkStatus::added(info.hunk_secondary_status)), + diff_hunk_status, + diff_base_byte_range, buffer_range: buffer_start..buffer_end, range: start..end, }) @@ -6780,7 +7069,11 @@ impl<'a> MultiBufferExcerpt<'a> { fn map_offset_to_buffer_internal(&self, offset: MultiBufferOffset) -> BufferOffset { let mut excerpt_offset = self.diff_transforms.start().excerpt_dimension; - if let Some(DiffTransform::BufferContent { .. }) = self.diff_transforms.item() { + if self + .diff_transforms + .item() + .is_some_and(|t| t.is_buffer_content()) + { excerpt_offset += offset - self.diff_transforms.start().output_dimension.0; }; let offset_in_excerpt = excerpt_offset.saturating_sub(self.excerpt_offset); @@ -6923,10 +7216,19 @@ impl sum_tree::KeyedItem for ExcerptIdMapping { impl DiffTransform { fn hunk_info(&self) -> Option { match self { - DiffTransform::DeletedHunk { hunk_info, .. } => Some(*hunk_info), - DiffTransform::BufferContent { - inserted_hunk_info, .. - } => *inserted_hunk_info, + DiffTransform::DeletedHunk { hunk_info, .. } + | DiffTransform::InsertedHunk { hunk_info, .. } + | DiffTransform::FilteredInsertedHunk { hunk_info, .. } => Some(hunk_info.clone()), + DiffTransform::Unmodified { .. } => None, + } + } + + fn is_buffer_content(&self) -> bool { + match self { + Self::Unmodified { .. } + | Self::InsertedHunk { .. } + | Self::FilteredInsertedHunk { .. } => true, + Self::DeletedHunk { .. } => false, } } } @@ -6936,7 +7238,8 @@ impl sum_tree::Item for DiffTransform { fn summary(&self, _: ::Context<'_>) -> Self::Summary { match self { - DiffTransform::BufferContent { summary, .. } => DiffTransformSummary { + DiffTransform::InsertedHunk { summary, .. } + | DiffTransform::Unmodified { summary, .. } => DiffTransformSummary { input: *summary, output: *summary, }, @@ -6944,6 +7247,10 @@ impl sum_tree::Item for DiffTransform { input: MBTextSummary::default(), output: summary.into(), }, + DiffTransform::FilteredInsertedHunk { summary, .. } => DiffTransformSummary { + input: *summary, + output: MBTextSummary::default(), + }, } } } @@ -7232,6 +7539,7 @@ impl Iterator for MultiBufferRows<'_> { return Some(RowInfo { buffer_id: None, buffer_row: Some(0), + base_text_row: Some(BaseTextRow(0)), multibuffer_row: Some(MultiBufferRow(0)), diff_status: None, expand_info: None, @@ -7257,6 +7565,14 @@ impl Iterator for MultiBufferRows<'_> { .end .to_point(&last_excerpt.buffer) .row; + // TODO(split-diff) perf + let base_text_row = self + .cursor + .snapshot + .diffs + .get(&last_excerpt.buffer_id) + .map(|diff| diff.row_to_base_text_row(last_row, &last_excerpt.buffer)) + .map(BaseTextRow); let first_row = last_excerpt .range @@ -7269,8 +7585,12 @@ impl Iterator for MultiBufferRows<'_> { None } else { let needs_expand_up = first_row == last_row - && last_row > 0 - && !region.diff_hunk_status.is_some_and(|d| d.is_deleted()); + && (last_row > 0) + && !region.diff_hunk_status.is_some_and(|d| d.is_deleted()) + && !(region.is_filtered() + && region + .diff_base_byte_range + .is_some_and(|range| !range.is_empty())); let needs_expand_down = last_row < last_excerpt.buffer.max_point().row; if needs_expand_up && needs_expand_down { @@ -7291,6 +7611,7 @@ impl Iterator for MultiBufferRows<'_> { return Some(RowInfo { buffer_id: Some(last_excerpt.buffer_id), buffer_row: Some(last_row), + base_text_row, multibuffer_row: Some(multibuffer_row), diff_status: None, wrapped_buffer_row: None, @@ -7303,6 +7624,31 @@ impl Iterator for MultiBufferRows<'_> { let overshoot = self.point - region.range.start; let buffer_point = region.buffer_range.start + overshoot; + let diff_status = region + .diff_hunk_status + .filter(|_| self.point < region.range.end); + let base_text_row = match diff_status { + // TODO(split-diff) perf + None => self + .cursor + .snapshot + .diffs + .get(®ion.excerpt.buffer_id) + .map(|diff| diff.row_to_base_text_row(buffer_point.row, ®ion.buffer)) + .map(BaseTextRow), + Some(DiffHunkStatus { + kind: DiffHunkStatusKind::Added, + .. + }) => None, + Some(DiffHunkStatus { + kind: DiffHunkStatusKind::Deleted, + .. + }) => Some(BaseTextRow(buffer_point.row)), + Some(DiffHunkStatus { + kind: DiffHunkStatusKind::Modified, + .. + }) => unreachable!(), + }; let expand_info = if self.is_singleton { None } else { @@ -7333,10 +7679,9 @@ impl Iterator for MultiBufferRows<'_> { let result = Some(RowInfo { buffer_id: Some(region.buffer.remote_id()), buffer_row: Some(buffer_point.row), + base_text_row, multibuffer_row: Some(MultiBufferRow(self.point.row)), - diff_status: region - .diff_hunk_status - .filter(|_| self.point < region.range.end), + diff_status, expand_info, wrapped_buffer_row: None, }); @@ -7353,14 +7698,22 @@ impl<'a> MultiBufferChunks<'a> { pub fn seek(&mut self, range: Range) { self.diff_transforms.seek(&range.end, Bias::Right); let mut excerpt_end = self.diff_transforms.start().1; - if let Some(DiffTransform::BufferContent { .. }) = self.diff_transforms.item() { + if self + .diff_transforms + .item() + .is_some_and(|t| t.is_buffer_content()) + { let overshoot = range.end - self.diff_transforms.start().0; excerpt_end += overshoot; } self.diff_transforms.seek(&range.start, Bias::Right); let mut excerpt_start = self.diff_transforms.start().1; - if let Some(DiffTransform::BufferContent { .. }) = self.diff_transforms.item() { + if self + .diff_transforms + .item() + .is_some_and(|t| t.is_buffer_content()) + { let overshoot = range.start - self.diff_transforms.start().0; excerpt_start += overshoot; } @@ -7423,6 +7776,12 @@ impl<'a> Iterator for ReversedMultiBufferChunks<'a> { let mut region = self.cursor.region()?; if self.offset == region.range.start { self.cursor.prev(); + while let Some(region) = self.cursor.region() + && region.buffer_range.is_empty() + && !region.has_trailing_newline + { + self.cursor.prev(); + } region = self.cursor.region()?; let start_overshoot = self.start.saturating_sub(region.range.start); self.current_chunks = Some(region.buffer.reversed_chunks_in_range( @@ -7451,6 +7810,13 @@ impl<'a> Iterator for MultiBufferChunks<'a> { if self.range.start == self.diff_transforms.end().0 { self.diff_transforms.next(); } + while let Some(DiffTransform::FilteredInsertedHunk { .. }) = self.diff_transforms.item() { + self.diff_transforms.next(); + let mut range = self.excerpt_offset_range.clone(); + range.start = self.diff_transforms.start().1; + self.seek_to_excerpt_offset_range(range); + self.buffer_chunk.take(); + } let diff_transform_start = self.diff_transforms.start().0; let diff_transform_end = self.diff_transforms.end().0; @@ -7464,7 +7830,9 @@ impl<'a> Iterator for MultiBufferChunks<'a> { let diff_transform = self.diff_transforms.item()?; match diff_transform { - DiffTransform::BufferContent { .. } => { + DiffTransform::Unmodified { .. } + | DiffTransform::InsertedHunk { .. } + | DiffTransform::FilteredInsertedHunk { .. } => { let chunk = if let Some(chunk) = &mut self.buffer_chunk { chunk } else { @@ -7500,15 +7868,15 @@ impl<'a> Iterator for MultiBufferChunks<'a> { } DiffTransform::DeletedHunk { buffer_id, - base_text_byte_range, + hunk_info, has_trailing_newline, .. } => { - let base_text_start = - base_text_byte_range.start + (self.range.start - diff_transform_start); + let base_text_start = hunk_info.base_text_byte_range.start + + (self.range.start - diff_transform_start); let base_text_end = - base_text_byte_range.start + (self.range.end - diff_transform_start); - let base_text_end = base_text_end.min(base_text_byte_range.end); + hunk_info.base_text_byte_range.start + (self.range.end - diff_transform_start); + let base_text_end = base_text_end.min(hunk_info.base_text_byte_range.end); let mut chunks = if let Some((_, mut chunks)) = self .diff_base_chunks @@ -7557,6 +7925,12 @@ impl MultiBufferBytes<'_> { self.chunk = b"\n"; } else { self.cursor.next(); + while let Some(region) = self.cursor.region() + && region.buffer_range.is_empty() + && !region.has_trailing_newline + { + self.cursor.next(); + } if let Some(region) = self.cursor.region() { let mut excerpt_bytes = region.buffer.bytes_in_range( region.buffer_range.start diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index e95d222c651999645a6966195be2da31347f1409..286ed9537d1ec0293b92ae47c3bc548a47f35232 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -29,6 +29,7 @@ fn test_empty_singleton(cx: &mut App) { [RowInfo { buffer_id: Some(buffer_id), buffer_row: Some(0), + base_text_row: None, multibuffer_row: Some(MultiBufferRow(0)), diff_status: None, expand_info: None, @@ -2242,7 +2243,7 @@ struct ReferenceExcerpt { struct ReferenceRegion { buffer_id: Option, range: Range, - buffer_start: Option, + buffer_range: Option>, status: Option, excerpt_id: Option, } @@ -2353,9 +2354,15 @@ impl ReferenceMultibuffer { } } - fn expected_content(&self, cx: &App) -> (String, Vec, HashSet) { + fn expected_content( + &self, + filter_mode: Option, + all_diff_hunks_expanded: bool, + cx: &App, + ) -> (String, Vec, HashSet) { let mut text = String::new(); let mut regions = Vec::::new(); + let mut filtered_regions = Vec::::new(); let mut excerpt_boundary_rows = HashSet::default(); for excerpt in &self.excerpts { excerpt_boundary_rows.insert(MultiBufferRow(text.matches('\n').count() as u32)); @@ -2379,10 +2386,12 @@ impl ReferenceMultibuffer { continue; } - if !excerpt.expanded_diff_hunks.iter().any(|expanded_anchor| { - expanded_anchor.to_offset(buffer).max(buffer_range.start) - == hunk_range.start.max(buffer_range.start) - }) { + if !all_diff_hunks_expanded + && !excerpt.expanded_diff_hunks.iter().any(|expanded_anchor| { + expanded_anchor.to_offset(buffer).max(buffer_range.start) + == hunk_range.start.max(buffer_range.start) + }) + { log::trace!("skipping a hunk that's not marked as expanded"); continue; } @@ -2396,16 +2405,20 @@ impl ReferenceMultibuffer { // Add the buffer text before the hunk let len = text.len(); text.extend(buffer.text_for_range(offset..hunk_range.start)); - regions.push(ReferenceRegion { - buffer_id: Some(buffer.remote_id()), - range: len..text.len(), - buffer_start: Some(buffer.offset_to_point(offset)), - status: None, - excerpt_id: Some(excerpt.id), - }); + if text.len() > len { + regions.push(ReferenceRegion { + buffer_id: Some(buffer.remote_id()), + range: len..text.len(), + buffer_range: Some((offset..hunk_range.start).to_point(&buffer)), + status: None, + excerpt_id: Some(excerpt.id), + }); + } // Add the deleted text for the hunk. - if !hunk.diff_base_byte_range.is_empty() { + if !hunk.diff_base_byte_range.is_empty() + && filter_mode != Some(MultiBufferFilterMode::KeepInsertions) + { let mut base_text = base_buffer .text_for_range(hunk.diff_base_byte_range.clone()) .collect::(); @@ -2417,9 +2430,7 @@ impl ReferenceMultibuffer { regions.push(ReferenceRegion { buffer_id: Some(base_buffer.remote_id()), range: len..text.len(), - buffer_start: Some( - base_buffer.offset_to_point(hunk.diff_base_byte_range.start), - ), + buffer_range: Some(hunk.diff_base_byte_range.to_point(&base_buffer)), status: Some(DiffHunkStatus::deleted(hunk.secondary_status)), excerpt_id: Some(excerpt.id), }); @@ -2430,16 +2441,27 @@ impl ReferenceMultibuffer { // Add the inserted text for the hunk. if hunk_range.end > offset { - let len = text.len(); - text.extend(buffer.text_for_range(offset..hunk_range.end)); - regions.push(ReferenceRegion { + let is_filtered = filter_mode == Some(MultiBufferFilterMode::KeepDeletions); + let range = if is_filtered { + text.len()..text.len() + } else { + let len = text.len(); + text.extend(buffer.text_for_range(offset..hunk_range.end)); + len..text.len() + }; + let region = ReferenceRegion { buffer_id: Some(buffer.remote_id()), - range: len..text.len(), - buffer_start: Some(buffer.offset_to_point(offset)), + range, + buffer_range: Some((offset..hunk_range.end).to_point(&buffer)), status: Some(DiffHunkStatus::added(hunk.secondary_status)), excerpt_id: Some(excerpt.id), - }); + }; offset = hunk_range.end; + if is_filtered { + filtered_regions.push(region); + } else { + regions.push(region); + } } } @@ -2450,7 +2472,7 @@ impl ReferenceMultibuffer { regions.push(ReferenceRegion { buffer_id: Some(buffer.remote_id()), range: len..text.len(), - buffer_start: Some(buffer.offset_to_point(offset)), + buffer_range: Some((offset..buffer_range.end).to_point(&buffer)), status: None, excerpt_id: Some(excerpt.id), }); @@ -2461,7 +2483,7 @@ impl ReferenceMultibuffer { regions.push(ReferenceRegion { buffer_id: None, range: 0..1, - buffer_start: Some(Point::new(0, 0)), + buffer_range: Some(Point::new(0, 0)..Point::new(0, 1)), status: None, excerpt_id: None, }); @@ -2480,10 +2502,47 @@ impl ReferenceMultibuffer { .position(|region| region.range.contains(&ix)) .map_or(RowInfo::default(), |region_ix| { let region = ®ions[region_ix]; - let buffer_row = region.buffer_start.map(|start_point| { - start_point.row + let buffer_row = region.buffer_range.as_ref().map(|buffer_range| { + buffer_range.start.row + text[region.range.start..ix].matches('\n').count() as u32 }); + let main_buffer = self + .excerpts + .iter() + .find(|e| e.id == region.excerpt_id.unwrap()) + .map(|e| e.buffer.clone()); + let base_text_row = match region.status { + None => Some( + main_buffer + .as_ref() + .map(|main_buffer| { + let diff = self + .diffs + .get(&main_buffer.read(cx).remote_id()) + .unwrap(); + let buffer_row = buffer_row.unwrap(); + BaseTextRow( + diff.read(cx).snapshot(cx).row_to_base_text_row( + buffer_row, + &main_buffer.read(cx).snapshot(), + ), + ) + }) + .unwrap_or_default(), + ), + Some(DiffHunkStatus { + kind: DiffHunkStatusKind::Added, + .. + }) => None, + Some(DiffHunkStatus { + kind: DiffHunkStatusKind::Deleted, + .. + }) => Some(BaseTextRow(buffer_row.unwrap())), + Some(DiffHunkStatus { + kind: DiffHunkStatusKind::Modified, + .. + }) => unreachable!(), + }; let is_excerpt_start = region_ix == 0 || ®ions[region_ix - 1].excerpt_id != ®ion.excerpt_id || regions[region_ix - 1].range.is_empty(); @@ -2507,18 +2566,15 @@ impl ReferenceMultibuffer { is_end = true; is_excerpt_end = true; } + let multibuffer_row = + MultiBufferRow(text[..ix].matches('\n').count() as u32); let mut expand_direction = None; - if let Some(buffer) = &self - .excerpts - .iter() - .find(|e| e.id == region.excerpt_id.unwrap()) - .map(|e| e.buffer.clone()) - { - let needs_expand_up = - is_excerpt_start && is_start && buffer_row.unwrap() > 0; + if let Some(buffer) = &main_buffer { + let buffer_row = buffer_row.unwrap(); + let needs_expand_up = is_excerpt_start && is_start && buffer_row > 0; let needs_expand_down = is_excerpt_end && is_end - && buffer.read(cx).max_point().row > buffer_row.unwrap(); + && buffer.read(cx).max_point().row > buffer_row; expand_direction = if needs_expand_up && needs_expand_down { Some(ExpandExcerptDirection::UpAndDown) } else if needs_expand_up { @@ -2533,11 +2589,10 @@ impl ReferenceMultibuffer { buffer_id: region.buffer_id, diff_status: region.status, buffer_row, + base_text_row, wrapped_buffer_row: None, - multibuffer_row: Some(MultiBufferRow( - text[..ix].matches('\n').count() as u32 - )), + multibuffer_row: Some(multibuffer_row), expand_info: expand_direction.zip(region.excerpt_id).map( |(direction, excerpt_id)| ExpandInfo { direction, @@ -2664,18 +2719,48 @@ async fn test_random_set_ranges(cx: &mut TestAppContext, mut rng: StdRng) { } } +// TODO(split-diff) bump up iterations +// #[gpui::test(iterations = 100)] +#[gpui::test] +async fn test_random_filtered_multibuffer(cx: &mut TestAppContext, rng: StdRng) { + let multibuffer = cx.new(|cx| { + let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); + multibuffer.set_all_diff_hunks_expanded(cx); + multibuffer.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions)); + multibuffer + }); + let follower = multibuffer.update(cx, |multibuffer, cx| multibuffer.get_or_create_follower(cx)); + follower.update(cx, |follower, _| { + assert!(follower.all_diff_hunks_expanded()); + follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions)); + }); + test_random_multibuffer_impl(multibuffer, cx, rng).await; +} + #[gpui::test(iterations = 100)] -async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { +async fn test_random_multibuffer(cx: &mut TestAppContext, rng: StdRng) { + let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); + test_random_multibuffer_impl(multibuffer, cx, rng).await; +} + +async fn test_random_multibuffer_impl( + multibuffer: Entity, + cx: &mut TestAppContext, + mut rng: StdRng, +) { let operations = env::var("OPERATIONS") .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); + multibuffer.read_with(cx, |multibuffer, _| assert!(multibuffer.is_empty())); + let all_diff_hunks_expanded = + multibuffer.read_with(cx, |multibuffer, _| multibuffer.all_diff_hunks_expanded()); let mut buffers: Vec> = Vec::new(); let mut base_texts: HashMap = HashMap::default(); - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); let mut reference = ReferenceMultibuffer::default(); let mut anchors = Vec::new(); let mut old_versions = Vec::new(); + let mut old_follower_versions = Vec::new(); let mut needs_diff_calculation = false; for _ in 0..operations { @@ -2774,7 +2859,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { assert!(excerpt.contains(anchor)); } } - 45..=55 if !reference.excerpts.is_empty() => { + 45..=55 if !reference.excerpts.is_empty() && !all_diff_hunks_expanded => { multibuffer.update(cx, |multibuffer, cx| { let snapshot = multibuffer.snapshot(cx); let excerpt_ix = rng.random_range(0..reference.excerpts.len()); @@ -2858,17 +2943,6 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { (start_ix..end_ix, anchor_range) }); - multibuffer.update(cx, |multibuffer, cx| { - let id = buffer_handle.read(cx).remote_id(); - if multibuffer.diff_for(id).is_none() { - let base_text = base_texts.get(&id).unwrap(); - let diff = cx - .new(|cx| BufferDiff::new_with_base_text(base_text, buffer_handle, cx)); - reference.add_diff(diff.clone(), cx); - multibuffer.add_diff(diff, cx) - } - }); - let excerpt_id = multibuffer.update(cx, |multibuffer, cx| { multibuffer .insert_excerpts_after( @@ -2886,208 +2960,276 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { excerpt_id, (buffer_handle.clone(), anchor_range), ); + + multibuffer.update(cx, |multibuffer, cx| { + let id = buffer_handle.read(cx).remote_id(); + if multibuffer.diff_for(id).is_none() { + let base_text = base_texts.get(&id).unwrap(); + let diff = cx + .new(|cx| BufferDiff::new_with_base_text(base_text, buffer_handle, cx)); + reference.add_diff(diff.clone(), cx); + multibuffer.add_diff(diff, cx) + } + }); } } if rng.random_bool(0.3) { multibuffer.update(cx, |multibuffer, cx| { old_versions.push((multibuffer.snapshot(cx), multibuffer.subscribe())); + + if let Some(follower) = &multibuffer.follower { + follower.update(cx, |follower, cx| { + old_follower_versions.push((follower.snapshot(cx), follower.subscribe())); + }) + } }) } - let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - let actual_text = snapshot.text(); - let actual_boundary_rows = snapshot - .excerpt_boundaries_in_range(MultiBufferOffset(0)..) - .map(|b| b.row) - .collect::>(); - let actual_row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::>(); + multibuffer.read_with(cx, |multibuffer, cx| { + check_multibuffer(multibuffer, &reference, &anchors, cx, &mut rng); + + if let Some(follower) = &multibuffer.follower { + check_multibuffer(follower.read(cx), &reference, &anchors, cx, &mut rng); + } + }); + } + + let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); + for (old_snapshot, subscription) in old_versions { + check_multibuffer_edits(&snapshot, &old_snapshot, subscription); + } + if let Some(follower) = multibuffer.read_with(cx, |multibuffer, _| multibuffer.follower.clone()) + { + let snapshot = follower.read_with(cx, |follower, cx| follower.snapshot(cx)); + for (old_snapshot, subscription) in old_follower_versions { + check_multibuffer_edits(&snapshot, &old_snapshot, subscription); + } + } +} + +fn check_multibuffer( + multibuffer: &MultiBuffer, + reference: &ReferenceMultibuffer, + anchors: &[Anchor], + cx: &App, + rng: &mut StdRng, +) { + let snapshot = multibuffer.snapshot(cx); + let filter_mode = multibuffer.filter_mode; + assert!(filter_mode.is_some() == snapshot.all_diff_hunks_expanded); + let actual_text = snapshot.text(); + let actual_boundary_rows = snapshot + .excerpt_boundaries_in_range(MultiBufferOffset(0)..) + .map(|b| b.row) + .collect::>(); + let actual_row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::>(); + + let (expected_text, expected_row_infos, expected_boundary_rows) = + reference.expected_content(filter_mode, snapshot.all_diff_hunks_expanded, cx); - let (expected_text, expected_row_infos, expected_boundary_rows) = - cx.update(|cx| reference.expected_content(cx)); + let (unfiltered_text, unfiltered_row_infos, unfiltered_boundary_rows) = + reference.expected_content(None, snapshot.all_diff_hunks_expanded, cx); - let has_diff = actual_row_infos + let has_diff = actual_row_infos + .iter() + .any(|info| info.diff_status.is_some()) + || unfiltered_row_infos .iter() - .any(|info| info.diff_status.is_some()) - || expected_row_infos - .iter() - .any(|info| info.diff_status.is_some()); - let actual_diff = format_diff( - &actual_text, - &actual_row_infos, - &actual_boundary_rows, - Some(has_diff), - ); - let expected_diff = format_diff( - &expected_text, - &expected_row_infos, - &expected_boundary_rows, - Some(has_diff), + .any(|info| info.diff_status.is_some()); + let actual_diff = format_diff( + &actual_text, + &actual_row_infos, + &actual_boundary_rows, + Some(has_diff), + ); + let expected_diff = format_diff( + &expected_text, + &expected_row_infos, + &expected_boundary_rows, + Some(has_diff), + ); + + log::info!("Multibuffer content:\n{}", actual_diff); + if filter_mode.is_some() { + log::info!( + "Unfiltered multibuffer content:\n{}", + format_diff( + &unfiltered_text, + &unfiltered_row_infos, + &unfiltered_boundary_rows, + None, + ), ); + } - log::info!("Multibuffer content:\n{}", actual_diff); + assert_eq!( + actual_row_infos.len(), + actual_text.split('\n').count(), + "line count: {}", + actual_text.split('\n').count() + ); + pretty_assertions::assert_eq!(actual_diff, expected_diff); + pretty_assertions::assert_eq!(actual_text, expected_text); + pretty_assertions::assert_eq!(actual_row_infos, expected_row_infos); + for _ in 0..5 { + let start_row = rng.random_range(0..=expected_row_infos.len()); assert_eq!( - actual_row_infos.len(), - actual_text.split('\n').count(), - "line count: {}", - actual_text.split('\n').count() + snapshot + .row_infos(MultiBufferRow(start_row as u32)) + .collect::>(), + &expected_row_infos[start_row..], + "buffer_rows({})", + start_row ); - pretty_assertions::assert_eq!(actual_diff, expected_diff); - pretty_assertions::assert_eq!(actual_text, expected_text); - pretty_assertions::assert_eq!(actual_row_infos, expected_row_infos); - - for _ in 0..5 { - let start_row = rng.random_range(0..=expected_row_infos.len()); - assert_eq!( - snapshot - .row_infos(MultiBufferRow(start_row as u32)) - .collect::>(), - &expected_row_infos[start_row..], - "buffer_rows({})", - start_row - ); - } + } + assert_eq!( + snapshot.widest_line_number(), + expected_row_infos + .into_iter() + .filter_map(|info| { + if info.diff_status.is_some_and(|status| status.is_deleted()) { + None + } else { + info.buffer_row + } + }) + .max() + .unwrap() + + 1 + ); + let reference_ranges = reference + .excerpts + .iter() + .map(|excerpt| { + ( + excerpt.id, + excerpt.range.to_offset(&excerpt.buffer.read(cx).snapshot()), + ) + }) + .collect::>(); + for i in 0..snapshot.len().0 { + let excerpt = snapshot + .excerpt_containing(MultiBufferOffset(i)..MultiBufferOffset(i)) + .unwrap(); assert_eq!( - snapshot.widest_line_number(), - expected_row_infos - .into_iter() - .filter_map(|info| { - if info.diff_status.is_some_and(|status| status.is_deleted()) { - None - } else { - info.buffer_row - } - }) - .max() - .unwrap() - + 1 + excerpt.buffer_range().start.0..excerpt.buffer_range().end.0, + reference_ranges[&excerpt.id()] ); - let reference_ranges = cx.update(|cx| { - reference - .excerpts - .iter() - .map(|excerpt| { - ( - excerpt.id, - excerpt.range.to_offset(&excerpt.buffer.read(cx).snapshot()), - ) - }) - .collect::>() - }); - for i in 0..snapshot.len().0 { - let excerpt = snapshot - .excerpt_containing(MultiBufferOffset(i)..MultiBufferOffset(i)) - .unwrap(); - assert_eq!( - excerpt.buffer_range().start.0..excerpt.buffer_range().end.0, - reference_ranges[&excerpt.id()] - ); - } + } - assert_consistent_line_numbers(&snapshot); - assert_position_translation(&snapshot); + assert_consistent_line_numbers(&snapshot); + assert_position_translation(&snapshot); - for (row, line) in expected_text.split('\n').enumerate() { - assert_eq!( - snapshot.line_len(MultiBufferRow(row as u32)), - line.len() as u32, - "line_len({}).", - row - ); - } + for (row, line) in expected_text.split('\n').enumerate() { + assert_eq!( + snapshot.line_len(MultiBufferRow(row as u32)), + line.len() as u32, + "line_len({}).", + row + ); + } - let text_rope = Rope::from(expected_text.as_str()); - for _ in 0..10 { - let end_ix = text_rope.clip_offset(rng.random_range(0..=text_rope.len()), Bias::Right); - let start_ix = text_rope.clip_offset(rng.random_range(0..=end_ix), Bias::Left); + let text_rope = Rope::from(expected_text.as_str()); + for _ in 0..10 { + let end_ix = text_rope.clip_offset(rng.random_range(0..=text_rope.len()), Bias::Right); + let start_ix = text_rope.clip_offset(rng.random_range(0..=end_ix), Bias::Left); - let text_for_range = snapshot - .text_for_range(MultiBufferOffset(start_ix)..MultiBufferOffset(end_ix)) - .collect::(); - assert_eq!( - text_for_range, - &expected_text[start_ix..end_ix], - "incorrect text for range {:?}", - start_ix..end_ix - ); + let text_for_range = snapshot + .text_for_range(MultiBufferOffset(start_ix)..MultiBufferOffset(end_ix)) + .collect::(); + assert_eq!( + text_for_range, + &expected_text[start_ix..end_ix], + "incorrect text for range {:?}", + start_ix..end_ix + ); - let expected_summary = - MBTextSummary::from(TextSummary::from(&expected_text[start_ix..end_ix])); - assert_eq!( - snapshot.text_summary_for_range::( - MultiBufferOffset(start_ix)..MultiBufferOffset(end_ix) - ), - expected_summary, - "incorrect summary for range {:?}", - start_ix..end_ix - ); - } + let expected_summary = + MBTextSummary::from(TextSummary::from(&expected_text[start_ix..end_ix])); + assert_eq!( + snapshot.text_summary_for_range::( + MultiBufferOffset(start_ix)..MultiBufferOffset(end_ix) + ), + expected_summary, + "incorrect summary for range {:?}", + start_ix..end_ix + ); + } - // Anchor resolution - let summaries = snapshot.summaries_for_anchors::(&anchors); - assert_eq!(anchors.len(), summaries.len()); - for (anchor, resolved_offset) in anchors.iter().zip(summaries) { - assert!(resolved_offset <= snapshot.len()); - assert_eq!( - snapshot.summary_for_anchor::(anchor), - resolved_offset, - "anchor: {:?}", - anchor - ); - } + // Anchor resolution + let summaries = snapshot.summaries_for_anchors::(anchors); + assert_eq!(anchors.len(), summaries.len()); + for (anchor, resolved_offset) in anchors.iter().zip(summaries) { + assert!(resolved_offset <= snapshot.len()); + assert_eq!( + snapshot.summary_for_anchor::(anchor), + resolved_offset, + "anchor: {:?}", + anchor + ); + } - for _ in 0..10 { - let end_ix = text_rope.clip_offset(rng.random_range(0..=text_rope.len()), Bias::Right); - assert_eq!( - snapshot - .reversed_chars_at(MultiBufferOffset(end_ix)) - .collect::(), - expected_text[..end_ix].chars().rev().collect::(), - ); - } + for _ in 0..10 { + let end_ix = text_rope.clip_offset(rng.random_range(0..=text_rope.len()), Bias::Right); + assert_eq!( + snapshot + .reversed_chars_at(MultiBufferOffset(end_ix)) + .collect::(), + expected_text[..end_ix].chars().rev().collect::(), + ); + } - for _ in 0..10 { - let end_ix = rng.random_range(0..=text_rope.len()); - let end_ix = text_rope.floor_char_boundary(end_ix); - let start_ix = rng.random_range(0..=end_ix); - let start_ix = text_rope.floor_char_boundary(start_ix); - assert_eq!( - snapshot - .bytes_in_range(MultiBufferOffset(start_ix)..MultiBufferOffset(end_ix)) - .flatten() - .copied() - .collect::>(), - expected_text.as_bytes()[start_ix..end_ix].to_vec(), - "bytes_in_range({:?})", - start_ix..end_ix, - ); - } + for _ in 0..10 { + let end_ix = rng.random_range(0..=text_rope.len()); + let end_ix = text_rope.floor_char_boundary(end_ix); + let start_ix = rng.random_range(0..=end_ix); + let start_ix = text_rope.floor_char_boundary(start_ix); + assert_eq!( + snapshot + .bytes_in_range(MultiBufferOffset(start_ix)..MultiBufferOffset(end_ix)) + .flatten() + .copied() + .collect::>(), + expected_text.as_bytes()[start_ix..end_ix].to_vec(), + "bytes_in_range({:?})", + start_ix..end_ix, + ); } +} - let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - for (old_snapshot, subscription) in old_versions { - let edits = subscription.consume().into_inner(); +fn check_multibuffer_edits( + snapshot: &MultiBufferSnapshot, + old_snapshot: &MultiBufferSnapshot, + subscription: Subscription, +) { + let edits = subscription.consume().into_inner(); - log::info!( - "applying subscription edits to old text: {:?}: {:?}", - old_snapshot.text(), - edits, - ); + log::info!( + "applying subscription edits to old text: {:?}: {:#?}", + old_snapshot.text(), + edits, + ); - let mut text = old_snapshot.text(); - for edit in edits { - let new_text: String = snapshot - .text_for_range(edit.new.start..edit.new.end) - .collect(); - text.replace_range( - edit.new.start.0..edit.new.start.0 + (edit.old.end.0 - edit.old.start.0), - &new_text, - ); - } - assert_eq!(text.to_string(), snapshot.text()); + let mut text = old_snapshot.text(); + for edit in edits { + let new_text: String = snapshot + .text_for_range(edit.new.start..edit.new.end) + .collect(); + text.replace_range( + (edit.new.start.0..edit.new.start.0 + (edit.old.end.0 - edit.old.start.0)).clone(), + &new_text, + ); + pretty_assertions::assert_eq!( + &text[0..edit.new.end.0], + snapshot + .text_for_range(MultiBufferOffset(0)..edit.new.end) + .collect::() + ); } + pretty_assertions::assert_eq!(text, snapshot.text()); } #[gpui::test] @@ -3534,12 +3676,210 @@ fn format_diff( } else { "" }; - format!("{boundary_row}{marker}{line}") + let expand = info + .expand_info + .map(|expand_info| match expand_info.direction { + ExpandExcerptDirection::Up => " [↑]", + ExpandExcerptDirection::Down => " [↓]", + ExpandExcerptDirection::UpAndDown => " [↕]", + }) + .unwrap_or_default(); + + format!("{boundary_row}{marker}{line}{expand}") + // let mbr = info + // .multibuffer_row + // .map(|row| format!("{:0>3}", row.0)) + // .unwrap_or_else(|| "???".to_string()); + // let byte_range = format!("{byte_range_start:0>3}..{byte_range_end:0>3}"); + // format!("{boundary_row}Row: {mbr}, Bytes: {byte_range} | {marker}{line}{expand}") }) .collect::>() .join("\n") } +// fn format_transforms(snapshot: &MultiBufferSnapshot) -> String { +// snapshot +// .diff_transforms +// .iter() +// .map(|transform| { +// let (kind, summary) = match transform { +// DiffTransform::DeletedHunk { summary, .. } => (" Deleted", (*summary).into()), +// DiffTransform::FilteredInsertedHunk { summary, .. } => (" Filtered", *summary), +// DiffTransform::InsertedHunk { summary, .. } => (" Inserted", *summary), +// DiffTransform::Unmodified { summary, .. } => ("Unmodified", *summary), +// }; +// format!("{kind}(len: {}, lines: {:?})", summary.len, summary.lines) +// }) +// .join("\n") +// } + +// fn format_excerpts(snapshot: &MultiBufferSnapshot) -> String { +// snapshot +// .excerpts +// .iter() +// .map(|excerpt| { +// format!( +// "Excerpt(buffer_range = {:?}, lines = {:?}, has_trailing_newline = {:?})", +// excerpt.range.context.to_point(&excerpt.buffer), +// excerpt.text_summary.lines, +// excerpt.has_trailing_newline +// ) +// }) +// .join("\n") +// } + +#[gpui::test] +async fn test_basic_filtering(cx: &mut TestAppContext) { + let text = indoc!( + " + ZERO + one + TWO + three + six + " + ); + let base_text = indoc!( + " + one + two + three + four + five + six + " + ); + + let buffer = cx.new(|cx| Buffer::local(text, cx)); + let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); + cx.run_until_parked(); + + let multibuffer = cx.new(|cx| { + let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx); + multibuffer.add_diff(diff.clone(), cx); + multibuffer.set_all_diff_hunks_expanded(cx); + multibuffer.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions)); + multibuffer + }); + + let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { + (multibuffer.snapshot(cx), multibuffer.subscribe()) + }); + + assert_eq!(snapshot.text(), base_text); + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc!( + " + one + - two + three + - four + - five + six + " + ), + ); + + buffer.update(cx, |buffer, cx| { + buffer.edit_via_marked_text( + indoc!( + " + ZERO + one + «»W«O + T»hree + six + " + ), + None, + cx, + ); + }); + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc! { + " + one + - two + - four + - five + six + " + }, + ); +} + +#[gpui::test] +async fn test_base_text_line_numbers(cx: &mut TestAppContext) { + let base_text = indoc! {" + one + two + three + four + five + six + "}; + let buffer_text = indoc! {" + two + THREE + five + six + SEVEN + "}; + let multibuffer = cx.update(|cx| MultiBuffer::build_simple(buffer_text, cx)); + multibuffer.update(cx, |multibuffer, cx| { + let buffer = multibuffer.all_buffers().into_iter().next().unwrap(); + let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); + multibuffer.set_all_diff_hunks_expanded(cx); + multibuffer.add_diff(diff, cx); + }); + let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { + (multibuffer.snapshot(cx), multibuffer.subscribe()) + }); + + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc! {" + - one + two + - three + - four + + THREE + five + six + + SEVEN + "}, + ); + let base_text_rows = snapshot + .row_infos(MultiBufferRow(0)) + .map(|row_info| row_info.base_text_row) + .collect::>(); + pretty_assertions::assert_eq!( + base_text_rows, + vec![ + Some(BaseTextRow(0)), + Some(BaseTextRow(1)), + Some(BaseTextRow(2)), + Some(BaseTextRow(3)), + None, + Some(BaseTextRow(4)), + Some(BaseTextRow(5)), + None, + Some(BaseTextRow(6)), + ] + ) +} + #[track_caller] fn assert_excerpts_match( multibuffer: &Entity, diff --git a/crates/multi_buffer/src/path_key.rs b/crates/multi_buffer/src/path_key.rs index 5d9b653a2b8c9df8c854ca01c47c57b42c159f1e..1685e7a27329b1beea5f0d2c9563acfab07d8d8b 100644 --- a/crates/multi_buffer/src/path_key.rs +++ b/crates/multi_buffer/src/path_key.rs @@ -50,6 +50,11 @@ impl MultiBuffer { if let Some(to_remove) = self.excerpts_by_path.remove(&path) { self.remove_excerpts(to_remove, cx) } + if let Some(follower) = &self.follower { + follower.update(cx, |follower, cx| { + follower.remove_excerpts_for_path(path, cx); + }); + } } pub fn location_for_path(&self, path: &PathKey, cx: &App) -> Option { diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index eaeff234bc1a8e21471cee74f98636dfdd995ca4..07c6e9c8aa8c116cf5b4ec46ca07817cb8b4c36f 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -756,7 +756,7 @@ impl Item for NotebookEditor { } // TODO - fn as_searchable(&self, _: &Entity) -> Option> { + fn as_searchable(&self, _: &Entity, _: &App) -> Option> { None } diff --git a/crates/rope/src/point.rs b/crates/rope/src/point.rs index a5a0a442c3209a299fc299526418d792b7f7fd12..a2491f6b0ec9152f775aba037d3fdc0603bbdc9d 100644 --- a/crates/rope/src/point.rs +++ b/crates/rope/src/point.rs @@ -1,15 +1,22 @@ use std::{ cmp::Ordering, + fmt::{self, Debug}, ops::{Add, AddAssign, Range, Sub}, }; /// A zero-indexed point in a text buffer consisting of a row and column. -#[derive(Clone, Copy, Default, Eq, PartialEq, Debug, Hash)] +#[derive(Clone, Copy, Default, Eq, PartialEq, Hash)] pub struct Point { pub row: u32, pub column: u32, } +impl Debug for Point { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Point({}:{})", self.row, self.column) + } +} + impl Point { pub const MAX: Self = Self { row: u32::MAX, diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 85656f179946393c3b15d8d57921ffa3847365cf..911e9ec34e2dcd93d76fcd3365539fbe02173064 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -505,7 +505,7 @@ impl Item for ProjectSearchView { None } } - fn as_searchable(&self, _: &Entity) -> Option> { + fn as_searchable(&self, _: &Entity, _: &App) -> Option> { Some(Box::new(self.results_editor.clone())) } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 64336886a4b430f780db1126b8d677e51cff066b..2a5213ce7ebc3326c7f4a0b5a8291e098e65cd78 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1269,7 +1269,11 @@ impl Item for TerminalView { false } - fn as_searchable(&self, handle: &Entity) -> Option> { + fn as_searchable( + &self, + handle: &Entity, + _: &App, + ) -> Option> { Some(Box::new(handle.clone())) } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 1f37c961159b8adeb89c38a4063bc682724fbee5..8f459557270e7b4595e26e15f2aad3c33aea4cd8 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -287,7 +287,7 @@ pub trait Item: Focusable + EventEmitter + Render + Sized { } } - fn as_searchable(&self, _: &Entity) -> Option> { + fn as_searchable(&self, _: &Entity, _: &App) -> Option> { None } @@ -981,7 +981,7 @@ impl ItemHandle for Entity { } fn to_searchable_item_handle(&self, cx: &App) -> Option> { - self.read(cx).as_searchable(self) + self.read(cx).as_searchable(self, cx) } fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation { diff --git a/typos.toml b/typos.toml index f185a25790da5edd56edbf9a4884d305ee4fa4bc..6b1df15b40da0bded6d1f14bdf8b28e0b0ec649f 100644 --- a/typos.toml +++ b/typos.toml @@ -54,6 +54,8 @@ extend-exclude = [ "crates/editor/src/code_completion_tests.rs", # Linux repository structure is not a valid text, hence we should not check it for typos "crates/project_panel/benches/linux_repo_snapshot.txt", + # Some multibuffer test cases have word fragments that register as typos + "crates/multi_buffer/src/multi_buffer_tests.rs", ] [default]