Cargo.lock π
@@ -5367,6 +5367,7 @@ dependencies = [
"db",
"edit_prediction",
"emojis",
+ "feature_flags",
"file_icons",
"fs",
"futures 0.3.31",
Cole Miller , cameron , and Cameron created
Release Notes:
- N/A
---------
Co-authored-by: cameron <cameron.studdstreet@gmail.com>
Co-authored-by: Cameron <cameron@zed.dev>
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 | 540 ++++++++++---
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, 1,758 insertions(+), 469 deletions(-)
@@ -5367,6 +5367,7 @@ dependencies = [
"db",
"edit_prediction",
"emojis",
+ "feature_flags",
"file_icons",
"fs",
"futures 0.3.31",
@@ -493,7 +493,7 @@ impl Item for AgentDiffPane {
Some("Assistant Diff Opened")
}
- fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.editor.clone()))
}
@@ -2556,7 +2556,11 @@ impl Item for TextThreadEditor {
Some(self.title(cx).to_string().into())
}
- fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ fn as_searchable(
+ &self,
+ handle: &Entity<Self>,
+ _: &App,
+ ) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(handle.clone()))
}
@@ -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<TaskLabel> = LazyLock::new(TaskLabel::new);
@@ -88,6 +88,7 @@ struct PendingHunk {
#[derive(Debug, Clone)]
pub struct DiffHunkSummary {
buffer_range: Range<Anchor>,
+ diff_base_byte_range: Range<usize>,
}
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::<DiffHunkSummary>(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}"
+ );
+ }
+ }
}
@@ -541,7 +541,7 @@ impl Item for ChannelView {
})
}
- fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.editor.clone()))
}
@@ -998,7 +998,11 @@ impl Item for DapLogView {
None
}
- fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ fn as_searchable(
+ &self,
+ handle: &Entity<Self>,
+ _: &App,
+ ) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(handle.clone()))
}
}
@@ -428,7 +428,7 @@ impl Item for StackTraceView {
}
}
- fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.editor.clone()))
}
@@ -890,7 +890,7 @@ impl Item for ProjectDiagnosticsEditor {
}
}
- fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.editor.clone()))
}
@@ -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
@@ -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,
@@ -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<Option<LanguageName>, LanguageSettings>,
accent_overrides: Vec<SharedString>,
fetched_tree_sitter_chunks: HashMap<ExcerptId, HashSet<Range<BufferRow>>>,
+ use_base_text_line_numbers: bool,
}
fn debounce_value(debounce_ms: u64) -> Option<Duration> {
@@ -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>) {
+ 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);
@@ -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, |_| {});
@@ -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;
}
@@ -929,7 +929,11 @@ impl Item for Editor {
})
}
- fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ fn as_searchable(
+ &self,
+ handle: &Entity<Self>,
+ _: &App,
+ ) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(handle.clone()))
}
@@ -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<Editor>,
+ secondary: Option<SecondaryEditor>,
+ panes: PaneGroup,
+ workspace: WeakEntity<Workspace>,
+ _subscriptions: Vec<Subscription>,
+}
+
+struct SecondaryEditor {
+ editor: Entity<Editor>,
+ pane: Entity<Pane>,
+ has_latest_selection: bool,
+ _subscriptions: Vec<Subscription>,
+}
+
+impl SplittableEditor {
+ pub fn primary_editor(&self) -> &Entity<Editor> {
+ &self.primary_editor
+ }
+
+ pub fn last_selected_editor(&self) -> &Entity<Editor> {
+ if let Some(secondary) = &self.secondary
+ && secondary.has_latest_selection
+ {
+ &secondary.editor
+ } else {
+ &self.primary_editor
+ }
+ }
+
+ pub fn new_unsplit(
+ buffer: Entity<MultiBuffer>,
+ project: Entity<Project>,
+ workspace: Entity<Workspace>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> 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<Self>) {
+ if !cx.has_flag::<SplitDiffFeatureFlag>() {
+ 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<Self>) {
+ 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>,
+ ) {
+ 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<EditorEvent> 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<Self>,
+ ) -> 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()
+ }
+}
@@ -513,7 +513,7 @@ impl Item for CommitView {
}
}
- fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.editor.clone()))
}
@@ -278,7 +278,7 @@ impl Item for FileDiffView {
}
}
- fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.editor.clone()))
}
@@ -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<Project>,
multibuffer: Entity<MultiBuffer>,
branch_diff: Entity<branch_diff::BranchDiff>,
- editor: Entity<Editor>,
+ editor: Entity<SplittableEditor>,
buffer_diff_subscriptions: HashMap<Arc<RelPath>, (Entity<BufferDiff>, Subscription)>,
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
@@ -172,7 +173,9 @@ impl ProjectDiff {
pub fn autoscroll(&self, cx: &mut Context<Self>) {
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>,
) -> 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<ProjectPath> {
- 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<Self>) {
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::<Vec<_>>();
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>,
+ editor: &Entity<SplittableEditor>,
event: &EditorEvent,
window: &mut Window,
cx: &mut Context<Self>,
@@ -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::<ConflictAddon>()
.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>) {
- 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<Self>,
) -> 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<SharedString> {
@@ -689,8 +716,9 @@ impl Item for ProjectDiff {
Some("Project Diff Opened")
}
- fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
- Some(Box::new(self.editor.clone()))
+ fn as_searchable(&self, _: &Entity<Self>, cx: &App) -> Option<Box<dyn SearchableItemHandle>> {
+ // 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>,
) {
- 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<Self>,
) -> Task<Result<()>> {
- 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<Self>,
) -> Task<Result<()>> {
- 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<Self>,
- _: &'a App,
+ cx: &'a App,
) -> Option<gpui::AnyEntity> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.clone().into())
} else if type_id == TypeId::of::<Editor>() {
- 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<Vec<BreadcrumbText>> {
- 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::<ProjectDiff>(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::<ProjectDiff>(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::<ConflictAddon>()
.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::<ProjectDiff>(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")),
@@ -339,7 +339,7 @@ impl Item for TextDiffView {
}
}
- fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.diff_editor.clone()))
}
@@ -744,7 +744,11 @@ impl Item for LspLogView {
None
}
- fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ fn as_searchable(
+ &self,
+ handle: &Entity<Self>,
+ _: &App,
+ ) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(handle.clone()))
}
@@ -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 <https://zed.dev/features#multi-buffers>
@@ -87,6 +92,14 @@ pub struct MultiBuffer {
/// The writing capability of the multi-buffer.
capability: Capability,
buffer_changed_since_sync: Rc<Cell<bool>>,
+ follower: Option<Entity<MultiBuffer>>,
+ filter_mode: Option<MultiBufferFilterMode>,
+}
+
+#[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<DiffTransformHunkInfo>,
+ },
+ 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<usize>,
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<usize>,
}
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::<Self>())
@@ -632,6 +661,7 @@ pub struct ExpandInfo {
pub struct RowInfo {
pub buffer_id: Option<BufferId>,
pub buffer_row: Option<u32>,
+ pub base_text_row: Option<BaseTextRow>,
pub multibuffer_row: Option<MultiBufferRow>,
pub diff_status: Option<buffer_diff::DiffHunkStatus>,
pub expand_info: Option<ExpandInfo>,
@@ -927,7 +957,7 @@ impl<'a, MBD: MultiBufferDimension> Dimension<'a, DiffTransformSummary> for Diff
struct MultiBufferCursor<'a, MBD, BD> {
excerpts: Cursor<'a, 'static, Excerpt, ExcerptDimension<MBD>>,
diff_transforms: Cursor<'a, 'static, DiffTransform, DiffTransforms<MBD>>,
- diffs: &'a TreeMap<BufferId, BufferDiffSnapshot>,
+ snapshot: &'a MultiBufferSnapshot,
cached_region: Option<MultiBufferRegion<'a, MBD, BD>>,
}
@@ -938,10 +968,21 @@ struct MultiBufferRegion<'a, MBD, BD> {
diff_hunk_status: Option<DiffHunkStatus>,
excerpt: &'a Excerpt,
buffer_range: Range<BD>,
+ diff_base_byte_range: Option<Range<usize>>,
range: Range<MBD>,
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<Self>) -> Entity<MultiBuffer> {
+ 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<MultiBufferFilterMode>) {
+ 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<Self>,
) -> Vec<ExcerptId>
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<Self>,
) -> Vec<ExcerptId>
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<Item = (ExcerptId, ExcerptRange<O>)>,
cx: &mut Context<Self>,
) 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<Entity<BufferDiff>> {
@@ -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::<Dimensions<Option<&Locator>, ExcerptOffset>>(());
- let mut edits = Vec::<Edit<ExcerptOffset>>::new();
+ let mut excerpt_edits = Vec::<Edit<ExcerptOffset>>::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<BufferId, BufferState>,
diffs: &HashMap<BufferId, DiffState>,
+ filter_mode: Option<MultiBufferFilterMode>,
cx: &App,
) -> Vec<Edit<MultiBufferOffset>> {
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<text::Edit<ExcerptOffset>>,
change_kind: DiffChangeKind,
+ filter_mode: Option<MultiBufferFilterMode>,
) -> Vec<Edit<MultiBufferOffset>> {
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<ExcerptOffset, MultiBufferOffset>,
>,
new_diff_transforms: &mut SumTree<DiffTransform>,
- end_of_current_insert: &mut Option<(ExcerptOffset, DiffTransformHunkInfo)>,
+ end_of_current_insert: &mut Option<CurrentInsertedHunk>,
old_expanded_hunks: &mut HashSet<DiffTransformHunkInfo>,
snapshot: &MultiBufferSnapshot,
change_kind: DiffChangeKind,
+ filter_mode: Option<MultiBufferFilterMode>,
) -> 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<DiffTransform>,
subtree: SumTree<DiffTransform>,
) {
- 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<DiffTransform>, 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<DiffTransform>,
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::<MBTextSummary>(
+ 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::<MBTextSummary>(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<DiffTransform>,
- new_inserted_hunk_info: Option<DiffTransformHunkInfo>,
- 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<Self> {
+ 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<Item = Range<Point>>,
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::<MBD>(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::<MBD::TextDimension, _>(
- 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(&<MBD::TextDimension>::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::<MBD::TextDimension, _>(
- 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"
+ );
}
}
}
@@ -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<BufferId>,
range: Range<usize>,
- buffer_start: Option<Point>,
+ buffer_range: Option<Range<Point>>,
status: Option<DiffHunkStatus>,
excerpt_id: Option<ExcerptId>,
}
@@ -2353,9 +2354,15 @@ impl ReferenceMultibuffer {
}
}
- fn expected_content(&self, cx: &App) -> (String, Vec<RowInfo>, HashSet<MultiBufferRow>) {
+ fn expected_content(
+ &self,
+ filter_mode: Option<MultiBufferFilterMode>,
+ all_diff_hunks_expanded: bool,
+ cx: &App,
+ ) -> (String, Vec<RowInfo>, HashSet<MultiBufferRow>) {
let mut text = String::new();
let mut regions = Vec::<ReferenceRegion>::new();
+ let mut filtered_regions = Vec::<ReferenceRegion>::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::<String>();
@@ -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<MultiBuffer>,
+ 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<Entity<Buffer>> = Vec::new();
let mut base_texts: HashMap<BufferId, String> = 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::<HashSet<_>>();
- let actual_row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
+ 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::<HashSet<_>>();
+ let actual_row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
+
+ 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::<Vec<_>>(),
+ &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::<Vec<_>>(),
- &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::<HashMap<_, _>>();
+ 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::<HashMap<_, _>>()
- });
- 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::<String>();
- 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::<String>();
+ 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::<MBTextSummary, _>(
- 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::<MBTextSummary, _>(
+ MultiBufferOffset(start_ix)..MultiBufferOffset(end_ix)
+ ),
+ expected_summary,
+ "incorrect summary for range {:?}",
+ start_ix..end_ix
+ );
+ }
- // Anchor resolution
- let summaries = snapshot.summaries_for_anchors::<MultiBufferOffset, _>(&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::<MultiBufferOffset>(anchor),
- resolved_offset,
- "anchor: {:?}",
- anchor
- );
- }
+ // Anchor resolution
+ let summaries = snapshot.summaries_for_anchors::<MultiBufferOffset, _>(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::<MultiBufferOffset>(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::<String>(),
- expected_text[..end_ix].chars().rev().collect::<String>(),
- );
- }
+ 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::<String>(),
+ expected_text[..end_ix].chars().rev().collect::<String>(),
+ );
+ }
- 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::<Vec<_>>(),
- 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::<Vec<_>>(),
+ 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<MultiBufferOffset>,
+) {
+ 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::<String>()
+ );
}
+ 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::<Vec<_>>()
.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
+ Β«<inserted>Β»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::<Vec<_>>();
+ 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<MultiBuffer>,
@@ -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<Anchor> {
@@ -756,7 +756,7 @@ impl Item for NotebookEditor {
}
// TODO
- fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
None
}
@@ -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,
@@ -505,7 +505,7 @@ impl Item for ProjectSearchView {
None
}
}
- fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.results_editor.clone()))
}
@@ -1269,7 +1269,11 @@ impl Item for TerminalView {
false
}
- fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ fn as_searchable(
+ &self,
+ handle: &Entity<Self>,
+ _: &App,
+ ) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(handle.clone()))
}
@@ -287,7 +287,7 @@ pub trait Item: Focusable + EventEmitter<Self::Event> + Render + Sized {
}
}
- fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
None
}
@@ -981,7 +981,7 @@ impl<T: Item> ItemHandle for Entity<T> {
}
fn to_searchable_item_handle(&self, cx: &App) -> Option<Box<dyn SearchableItemHandle>> {
- self.read(cx).as_searchable(self)
+ self.read(cx).as_searchable(self, cx)
}
fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation {
@@ -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]