diff --git a/Cargo.lock b/Cargo.lock index fcd214f78b..eff6ae575a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7754,15 +7754,21 @@ dependencies = [ "ctor", "env_logger 0.11.6", "futures 0.3.31", + "git", "gpui", + "indoc", "itertools 0.14.0", "language", "log", "parking_lot", + "pretty_assertions", + "project", "rand 0.8.5", + "rope", "serde", "settings", "smallvec", + "smol", "sum_tree", "text", "theme", diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 8993867d6b..5cba4686b8 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -117,7 +117,7 @@ "ctrl-alt-space": "editor::ShowCharacterPalette", "ctrl-;": "editor::ToggleLineNumbers", "ctrl-k ctrl-r": "editor::RevertSelectedHunks", - "ctrl-'": "editor::ToggleHunkDiff", + "ctrl-'": "editor::ToggleSelectedDiffHunks", "ctrl-\"": "editor::ExpandAllHunkDiffs", "ctrl-i": "editor::ShowSignatureHelp", "alt-g b": "editor::ToggleGitBlame", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 62da886bb4..a0cc388f96 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -127,7 +127,7 @@ "ctrl-cmd-space": "editor::ShowCharacterPalette", "cmd-;": "editor::ToggleLineNumbers", "cmd-alt-z": "editor::RevertSelectedHunks", - "cmd-'": "editor::ToggleHunkDiff", + "cmd-'": "editor::ToggleSelectedDiffHunks", "cmd-\"": "editor::ExpandAllHunkDiffs", "cmd-alt-g b": "editor::ToggleGitBlame", "cmd-i": "editor::ShowSignatureHelp", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 6713eb0e6d..e8c4dc1d31 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -436,7 +436,7 @@ "bindings": { "d": "vim::CurrentLine", "s": ["vim::PushOperator", "DeleteSurrounds"], - "o": "editor::ToggleHunkDiff", // "d o" + "o": "editor::ToggleSelectedDiffHunks", // "d o" "p": "editor::RevertSelectedHunks" // "d p" } }, diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 92e6435321..3e6d837e40 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -250,22 +250,19 @@ impl InlineAssistant { let newest_selection = newest_selection.unwrap(); let mut codegen_ranges = Vec::new(); - for (excerpt_id, buffer, buffer_range) in - snapshot.excerpts_in_ranges(selections.iter().map(|selection| { + for (buffer, buffer_range, excerpt_id) in + snapshot.ranges_to_buffer_ranges(selections.iter().map(|selection| { snapshot.anchor_before(selection.start)..snapshot.anchor_after(selection.end) })) { - let start = Anchor { - buffer_id: Some(buffer.remote_id()), + let start = buffer.anchor_before(buffer_range.start); + let end = buffer.anchor_after(buffer_range.end); + + codegen_ranges.push(Anchor::range_in_buffer( excerpt_id, - text_anchor: buffer.anchor_before(buffer_range.start), - }; - let end = Anchor { - buffer_id: Some(buffer.remote_id()), - excerpt_id, - text_anchor: buffer.anchor_after(buffer_range.end), - }; - codegen_ranges.push(start..end); + buffer.remote_id(), + start..end, + )); if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() { self.telemetry.report_assistant_event(AssistantEvent { @@ -823,7 +820,7 @@ impl InlineAssistant { let ranges = multibuffer_snapshot.range_to_buffer_ranges(assist.range.clone()); ranges .first() - .and_then(|(excerpt, _)| excerpt.buffer().language()) + .and_then(|(buffer, _, _)| buffer.language()) .map(|language| language.name()) }); report_assistant_event( @@ -2648,17 +2645,17 @@ impl CodegenAlternative { ) -> Self { let snapshot = multi_buffer.read(cx).snapshot(cx); - let (old_excerpt, _) = snapshot + let (buffer, _, _) = snapshot .range_to_buffer_ranges(range.clone()) .pop() .unwrap(); let old_buffer = cx.new_model(|cx| { - let text = old_excerpt.buffer().as_rope().clone(); - let line_ending = old_excerpt.buffer().line_ending(); - let language = old_excerpt.buffer().language().cloned(); + let text = buffer.as_rope().clone(); + let line_ending = buffer.line_ending(); + let language = buffer.language().cloned(); let language_registry = multi_buffer .read(cx) - .buffer(old_excerpt.buffer_id()) + .buffer(buffer.remote_id()) .unwrap() .read(cx) .language_registry(); @@ -2898,7 +2895,7 @@ impl CodegenAlternative { let ranges = snapshot.range_to_buffer_ranges(self.range.clone()); ranges .first() - .and_then(|(excerpt, _)| excerpt.buffer().language()) + .and_then(|(buffer, _, _)| buffer.language()) .map(|language| language.name()) }; diff --git a/crates/assistant2/src/buffer_codegen.rs b/crates/assistant2/src/buffer_codegen.rs index f5700e46c5..09850ea5f7 100644 --- a/crates/assistant2/src/buffer_codegen.rs +++ b/crates/assistant2/src/buffer_codegen.rs @@ -255,17 +255,17 @@ impl CodegenAlternative { ) -> Self { let snapshot = buffer.read(cx).snapshot(cx); - let (old_excerpt, _) = snapshot + let (old_buffer, _, _) = snapshot .range_to_buffer_ranges(range.clone()) .pop() .unwrap(); let old_buffer = cx.new_model(|cx| { - let text = old_excerpt.buffer().as_rope().clone(); - let line_ending = old_excerpt.buffer().line_ending(); - let language = old_excerpt.buffer().language().cloned(); + let text = old_buffer.as_rope().clone(); + let line_ending = old_buffer.line_ending(); + let language = old_buffer.language().cloned(); let language_registry = buffer .read(cx) - .buffer(old_excerpt.buffer_id()) + .buffer(old_buffer.remote_id()) .unwrap() .read(cx) .language_registry(); @@ -475,7 +475,7 @@ impl CodegenAlternative { let ranges = snapshot.range_to_buffer_ranges(self.range.clone()); ranges .first() - .and_then(|(excerpt, _)| excerpt.buffer().language()) + .and_then(|(buffer, _, _)| buffer.language()) .map(|language| language.name()) }; diff --git a/crates/assistant2/src/inline_assistant.rs b/crates/assistant2/src/inline_assistant.rs index e503f1fe9a..55e6666c77 100644 --- a/crates/assistant2/src/inline_assistant.rs +++ b/crates/assistant2/src/inline_assistant.rs @@ -320,22 +320,18 @@ impl InlineAssistant { let newest_selection = newest_selection.unwrap(); let mut codegen_ranges = Vec::new(); - for (excerpt_id, buffer, buffer_range) in - snapshot.excerpts_in_ranges(selections.iter().map(|selection| { + for (buffer, buffer_range, excerpt_id) in + snapshot.ranges_to_buffer_ranges(selections.iter().map(|selection| { snapshot.anchor_before(selection.start)..snapshot.anchor_after(selection.end) })) { - let start = Anchor { - buffer_id: Some(buffer.remote_id()), + let anchor_range = Anchor::range_in_buffer( excerpt_id, - text_anchor: buffer.anchor_before(buffer_range.start), - }; - let end = Anchor { - buffer_id: Some(buffer.remote_id()), - excerpt_id, - text_anchor: buffer.anchor_after(buffer_range.end), - }; - codegen_ranges.push(start..end); + buffer.remote_id(), + buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end), + ); + + codegen_ranges.push(anchor_range); if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() { self.telemetry.report_assistant_event(AssistantEvent { @@ -901,7 +897,7 @@ impl InlineAssistant { let ranges = snapshot.range_to_buffer_ranges(assist.range.clone()); ranges .first() - .and_then(|(excerpt, _)| excerpt.buffer().language()) + .and_then(|(buffer, _, _)| buffer.language()) .map(|language| language.name()) }); report_assistant_event( diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index a9b8f5b0a7..75e7c6825f 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -10,7 +10,7 @@ use editor::{ ToggleCodeActions, Undo, }, test::editor_test_context::{AssertionContextManager, EditorTestContext}, - Editor, + Editor, RowInfo, }; use fs::Fs; use futures::StreamExt; @@ -20,7 +20,6 @@ use language::{ language_settings::{AllLanguageSettings, InlayHintSettings}, FakeLspAdapter, }; -use multi_buffer::MultiBufferRow; use project::{ project_settings::{InlineBlameSettings, ProjectSettings}, SERVER_PROGRESS_THROTTLE_TIMEOUT, @@ -2019,7 +2018,15 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA let blame = editor_b.blame().expect("editor_b should have blame now"); let entries = blame.update(cx, |blame, cx| { blame - .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx) + .blame_for_rows( + &(0..4) + .map(|row| RowInfo { + buffer_row: Some(row), + ..Default::default() + }) + .collect::>(), + cx, + ) .collect::>() }); @@ -2058,7 +2065,15 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA let blame = editor_b.blame().expect("editor_b should have blame now"); let entries = blame.update(cx, |blame, cx| { blame - .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx) + .blame_for_rows( + &(0..4) + .map(|row| RowInfo { + buffer_row: Some(row), + ..Default::default() + }) + .collect::>(), + cx, + ) .collect::>() }); @@ -2085,7 +2100,15 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA let blame = editor_b.blame().expect("editor_b should have blame now"); let entries = blame.update(cx, |blame, cx| { blame - .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx) + .blame_for_rows( + &(0..4) + .map(|row| RowInfo { + buffer_row: Some(row), + ..Default::default() + }) + .collect::>(), + cx, + ) .collect::>() }); diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index d3fa3dd269..1c151a730c 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -2593,7 +2593,7 @@ async fn test_git_diff_base_change( change_set_local_a.read_with(cx_a, |change_set, cx| { let buffer = buffer_local_a.read(cx); assert_eq!( - change_set.base_text_string(cx).as_deref(), + change_set.base_text_string().as_deref(), Some(diff_base.as_str()) ); git::diff::assert_hunks( @@ -2621,7 +2621,7 @@ async fn test_git_diff_base_change( change_set_remote_a.read_with(cx_b, |change_set, cx| { let buffer = buffer_remote_a.read(cx); assert_eq!( - change_set.base_text_string(cx).as_deref(), + change_set.base_text_string().as_deref(), Some(diff_base.as_str()) ); git::diff::assert_hunks( @@ -2643,7 +2643,7 @@ async fn test_git_diff_base_change( change_set_local_a.read_with(cx_a, |change_set, cx| { let buffer = buffer_local_a.read(cx); assert_eq!( - change_set.base_text_string(cx).as_deref(), + change_set.base_text_string().as_deref(), Some(new_diff_base.as_str()) ); git::diff::assert_hunks( @@ -2657,7 +2657,7 @@ async fn test_git_diff_base_change( change_set_remote_a.read_with(cx_b, |change_set, cx| { let buffer = buffer_remote_a.read(cx); assert_eq!( - change_set.base_text_string(cx).as_deref(), + change_set.base_text_string().as_deref(), Some(new_diff_base.as_str()) ); git::diff::assert_hunks( @@ -2703,7 +2703,7 @@ async fn test_git_diff_base_change( change_set_local_b.read_with(cx_a, |change_set, cx| { let buffer = buffer_local_b.read(cx); assert_eq!( - change_set.base_text_string(cx).as_deref(), + change_set.base_text_string().as_deref(), Some(diff_base.as_str()) ); git::diff::assert_hunks( @@ -2730,7 +2730,7 @@ async fn test_git_diff_base_change( change_set_remote_b.read_with(cx_b, |change_set, cx| { let buffer = buffer_remote_b.read(cx); assert_eq!( - change_set.base_text_string(cx).as_deref(), + change_set.base_text_string().as_deref(), Some(diff_base.as_str()) ); git::diff::assert_hunks( @@ -2752,7 +2752,7 @@ async fn test_git_diff_base_change( change_set_local_b.read_with(cx_a, |change_set, cx| { let buffer = buffer_local_b.read(cx); assert_eq!( - change_set.base_text_string(cx).as_deref(), + change_set.base_text_string().as_deref(), Some(new_diff_base.as_str()) ); git::diff::assert_hunks( @@ -2766,7 +2766,7 @@ async fn test_git_diff_base_change( change_set_remote_b.read_with(cx_b, |change_set, cx| { let buffer = buffer_remote_b.read(cx); assert_eq!( - change_set.base_text_string(cx).as_deref(), + change_set.base_text_string().as_deref(), Some(new_diff_base.as_str()) ); git::diff::assert_hunks( diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index 66bae0e9d4..774c105c8f 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -1342,7 +1342,7 @@ impl RandomizedTest for ProjectCollaborationTest { .get_unstaged_changes(host_buffer.read(cx).remote_id()) .unwrap() .read(cx) - .base_text_string(cx) + .base_text_string() }); let guest_diff_base = guest_project.read_with(client_cx, |project, cx| { project @@ -1351,7 +1351,7 @@ impl RandomizedTest for ProjectCollaborationTest { .get_unstaged_changes(guest_buffer.read(cx).remote_id()) .unwrap() .read(cx) - .base_text_string(cx) + .base_text_string() }); assert_eq!( guest_diff_base, host_diff_base, diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 715cccc02a..a7c916fecc 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -1,11 +1,11 @@ use std::time::Duration; -use editor::{AnchorRangeExt, Editor}; +use editor::Editor; use gpui::{ EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, Task, View, ViewContext, WeakView, }; -use language::{Diagnostic, DiagnosticEntry}; +use language::Diagnostic; use ui::{h_flex, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip}; use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace}; @@ -148,11 +148,7 @@ impl DiagnosticIndicator { (buffer, cursor_position) }); let new_diagnostic = buffer - .diagnostics_in_range(cursor_position..cursor_position, false) - .map(|DiagnosticEntry { diagnostic, range }| DiagnosticEntry { - diagnostic, - range: range.to_offset(&buffer), - }) + .diagnostics_in_range::<_, usize>(cursor_position..cursor_position) .filter(|entry| !entry.range.is_empty()) .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len())) .map(|entry| entry.diagnostic); diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index c71c69f8b0..658483567f 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -372,7 +372,7 @@ gpui::actions!( ToggleAutoSignatureHelp, ToggleGitBlame, ToggleGitBlameInline, - ToggleHunkDiff, + ToggleSelectedDiffHunks, ToggleIndentGuides, ToggleInlayHints, ToggleInlineCompletions, diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 0cd4b07600..34bdaf4b72 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -30,8 +30,8 @@ use crate::{ hover_links::InlayHighlight, movement::TextLayoutDetails, EditorStyle, InlayId, RowExt, }; pub use block_map::{ - Block, BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap, - BlockPlacement, BlockPoint, BlockProperties, BlockStyle, CustomBlockId, RenderBlock, + Block, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap, BlockPlacement, + BlockPoint, BlockProperties, BlockRows, BlockStyle, CustomBlockId, RenderBlock, StickyHeaderExcerpt, }; use block_map::{BlockRow, BlockSnapshot}; @@ -54,7 +54,7 @@ use language::{ use lsp::DiagnosticSeverity; use multi_buffer::{ Anchor, AnchorRangeExt, MultiBuffer, MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot, - ToOffset, ToPoint, + RowInfo, ToOffset, ToPoint, }; use serde::Deserialize; use std::{ @@ -68,7 +68,7 @@ use std::{ }; use sum_tree::{Bias, TreeMap}; use tab_map::{TabMap, TabSnapshot}; -use text::LineIndent; +use text::{BufferId, LineIndent}; use ui::{px, SharedString, WindowContext}; use unicode_segmentation::UnicodeSegmentation; use wrap_map::{WrapMap, WrapSnapshot}; @@ -367,10 +367,14 @@ impl DisplayMap { block_map.unfold_buffer(buffer_id, self.buffer.read(cx), cx) } - pub(crate) fn buffer_folded(&self, buffer_id: language::BufferId) -> bool { + pub(crate) fn is_buffer_folded(&self, buffer_id: language::BufferId) -> bool { self.block_map.folded_buffers.contains(&buffer_id) } + pub(crate) fn folded_buffers(&self) -> &HashSet { + &self.block_map.folded_buffers + } + pub fn insert_creases( &mut self, creases: impl IntoIterator>, @@ -716,13 +720,8 @@ impl DisplaySnapshot { self.buffer_snapshot.len() == 0 } - pub fn buffer_rows( - &self, - start_row: DisplayRow, - ) -> impl Iterator> + '_ { - self.block_snapshot - .buffer_rows(BlockRow(start_row.0)) - .map(|row| row.map(MultiBufferRow)) + pub fn row_infos(&self, start_row: DisplayRow) -> impl Iterator + '_ { + self.block_snapshot.row_infos(BlockRow(start_row.0)) } pub fn widest_line_number(&self) -> u32 { diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index f3b5cef853..54bb647232 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -7,8 +7,8 @@ use collections::{Bound, HashMap, HashSet}; use gpui::{AnyElement, AppContext, EntityId, Pixels, WindowContext}; use language::{Chunk, Patch, Point}; use multi_buffer::{ - Anchor, ExcerptId, ExcerptInfo, MultiBuffer, MultiBufferRow, MultiBufferSnapshot, ToOffset, - ToPoint as _, + Anchor, ExcerptId, ExcerptInfo, MultiBuffer, MultiBufferRow, MultiBufferSnapshot, RowInfo, + ToOffset, ToPoint as _, }; use parking_lot::Mutex; use std::{ @@ -399,9 +399,9 @@ pub struct BlockChunks<'a> { } #[derive(Clone)] -pub struct BlockBufferRows<'a> { +pub struct BlockRows<'a> { transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>, - input_buffer_rows: wrap_map::WrapBufferRows<'a>, + input_rows: wrap_map::WrapRows<'a>, output_row: BlockRow, started: bool, } @@ -777,14 +777,12 @@ impl BlockMap { if let Some(new_buffer_id) = new_buffer_id { let first_excerpt = excerpt_boundary.next.clone().unwrap(); if folded_buffers.contains(&new_buffer_id) { - let mut buffer_end = Point::new(excerpt_boundary.row.0, 0) - + excerpt_boundary.next.as_ref().unwrap().text_summary.lines; + let mut last_excerpt_end_row = first_excerpt.end_row; while let Some(next_boundary) = boundaries.peek() { if let Some(next_excerpt_boundary) = &next_boundary.next { if next_excerpt_boundary.buffer_id == new_buffer_id { - buffer_end = Point::new(next_boundary.row.0, 0) - + next_excerpt_boundary.text_summary.lines; + last_excerpt_end_row = next_excerpt_boundary.end_row; } else { break; } @@ -793,7 +791,15 @@ impl BlockMap { boundaries.next(); } - let wrap_end_row = wrap_snapshot.make_wrap_point(buffer_end, Bias::Right).row(); + let wrap_end_row = wrap_snapshot + .make_wrap_point( + Point::new( + last_excerpt_end_row.0, + buffer.line_len(last_excerpt_end_row), + ), + Bias::Right, + ) + .row(); return Some(( BlockPlacement::Replace(WrapRow(wrap_row)..=WrapRow(wrap_end_row)), @@ -1360,7 +1366,7 @@ impl BlockSnapshot { } } - pub(super) fn buffer_rows(&self, start_row: BlockRow) -> BlockBufferRows { + pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows { let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); cursor.seek(&start_row, Bias::Right, &()); let (output_start, input_start) = cursor.start(); @@ -1373,9 +1379,9 @@ impl BlockSnapshot { 0 }; let input_start_row = input_start.0 + overshoot; - BlockBufferRows { + BlockRows { transforms: cursor, - input_buffer_rows: self.wrap_snapshot.buffer_rows(input_start_row), + input_rows: self.wrap_snapshot.row_infos(input_start_row), output_row: start_row, started: false, } @@ -1480,7 +1486,7 @@ impl BlockSnapshot { } BlockId::ExcerptBoundary(next_excerpt_id) => { if let Some(next_excerpt_id) = next_excerpt_id { - let excerpt_range = buffer.range_for_excerpt::(next_excerpt_id)?; + let excerpt_range = buffer.range_for_excerpt(next_excerpt_id)?; self.wrap_snapshot .make_wrap_point(excerpt_range.start, Bias::Left) } else { @@ -1488,10 +1494,9 @@ impl BlockSnapshot { .make_wrap_point(buffer.max_point(), Bias::Left) } } - BlockId::FoldedBuffer(excerpt_id) => self.wrap_snapshot.make_wrap_point( - buffer.range_for_excerpt::(excerpt_id)?.start, - Bias::Left, - ), + BlockId::FoldedBuffer(excerpt_id) => self + .wrap_snapshot + .make_wrap_point(buffer.range_for_excerpt(excerpt_id)?.start, Bias::Left), }; let wrap_row = WrapRow(wrap_point.row()); @@ -1832,8 +1837,8 @@ impl<'a> Iterator for BlockChunks<'a> { } } -impl<'a> Iterator for BlockBufferRows<'a> { - type Item = Option; +impl<'a> Iterator for BlockRows<'a> { + type Item = RowInfo; fn next(&mut self) -> Option { if self.started { @@ -1862,7 +1867,7 @@ impl<'a> Iterator for BlockBufferRows<'a> { .as_ref() .map_or(true, |block| block.is_replacement()) { - self.input_buffer_rows.seek(self.transforms.start().1 .0); + self.input_rows.seek(self.transforms.start().1 .0); } } @@ -1870,15 +1875,15 @@ impl<'a> Iterator for BlockBufferRows<'a> { if let Some(block) = transform.block.as_ref() { if block.is_replacement() && self.transforms.start().0 == self.output_row { if matches!(block, Block::FoldedBuffer { .. }) { - Some(None) + Some(RowInfo::default()) } else { - Some(self.input_buffer_rows.next().unwrap()) + Some(self.input_rows.next().unwrap()) } } else { - Some(None) + Some(RowInfo::default()) } } else { - Some(self.input_buffer_rows.next().unwrap()) + Some(self.input_rows.next().unwrap()) } } } @@ -2153,7 +2158,10 @@ mod tests { ); assert_eq!( - snapshot.buffer_rows(BlockRow(0)).collect::>(), + snapshot + .row_infos(BlockRow(0)) + .map(|row_info| row_info.buffer_row) + .collect::>(), &[ Some(0), None, @@ -2603,7 +2611,10 @@ mod tests { "\n\n\n111\n\n\n\n\n222\n\n\n333\n\n\n444\n\n\n\n\n555\n\n\n666\n" ); assert_eq!( - blocks_snapshot.buffer_rows(BlockRow(0)).collect::>(), + blocks_snapshot + .row_infos(BlockRow(0)) + .map(|i| i.buffer_row) + .collect::>(), vec![ None, None, @@ -2679,7 +2690,10 @@ mod tests { "\n\n\n111\n\n\n\n\n\n222\n\n\n\n333\n\n\n444\n\n\n\n\n\n\n555\n\n\n666\n\n" ); assert_eq!( - blocks_snapshot.buffer_rows(BlockRow(0)).collect::>(), + blocks_snapshot + .row_infos(BlockRow(0)) + .map(|i| i.buffer_row) + .collect::>(), vec![ None, None, @@ -2754,7 +2768,10 @@ mod tests { "\n\n\n\n\n\n222\n\n\n\n333\n\n\n444\n\n\n\n\n\n\n555\n\n\n666\n\n" ); assert_eq!( - blocks_snapshot.buffer_rows(BlockRow(0)).collect::>(), + blocks_snapshot + .row_infos(BlockRow(0)) + .map(|i| i.buffer_row) + .collect::>(), vec![ None, None, @@ -2819,7 +2836,10 @@ mod tests { ); assert_eq!(blocks_snapshot.text(), "\n\n\n\n\n\n\n\n555\n\n\n666\n\n"); assert_eq!( - blocks_snapshot.buffer_rows(BlockRow(0)).collect::>(), + blocks_snapshot + .row_infos(BlockRow(0)) + .map(|i| i.buffer_row) + .collect::>(), vec![ None, None, @@ -2873,7 +2893,10 @@ mod tests { "Should have extra newline for 111 buffer, due to a new block added when it was folded" ); assert_eq!( - blocks_snapshot.buffer_rows(BlockRow(0)).collect::>(), + blocks_snapshot + .row_infos(BlockRow(0)) + .map(|i| i.buffer_row) + .collect::>(), vec![ None, None, @@ -2927,7 +2950,10 @@ mod tests { "Should have a single, first buffer left after folding" ); assert_eq!( - blocks_snapshot.buffer_rows(BlockRow(0)).collect::>(), + blocks_snapshot + .row_infos(BlockRow(0)) + .map(|i| i.buffer_row) + .collect::>(), vec![ None, None, @@ -2997,7 +3023,10 @@ mod tests { ); assert_eq!(blocks_snapshot.text(), "\n"); assert_eq!( - blocks_snapshot.buffer_rows(BlockRow(0)).collect::>(), + blocks_snapshot + .row_infos(BlockRow(0)) + .map(|i| i.buffer_row) + .collect::>(), vec![None, None], "When fully folded, should be no buffer rows" ); @@ -3295,7 +3324,8 @@ mod tests { let mut sorted_blocks_iter = expected_blocks.into_iter().peekable(); let input_buffer_rows = buffer_snapshot - .buffer_rows(MultiBufferRow(0)) + .row_infos(MultiBufferRow(0)) + .map(|row| row.buffer_row) .collect::>(); let mut expected_buffer_rows = Vec::new(); let mut expected_text = String::new(); @@ -3450,7 +3480,8 @@ mod tests { ); assert_eq!( blocks_snapshot - .buffer_rows(BlockRow(start_row as u32)) + .row_infos(BlockRow(start_row as u32)) + .map(|row_info| row_info.buffer_row) .collect::>(), &expected_buffer_rows[start_row..], "incorrect buffer_rows starting at row {:?}", diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index c4bb4080e2..1f3b985d1f 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -4,7 +4,9 @@ use super::{ }; use gpui::{AnyElement, ElementId, WindowContext}; use language::{Chunk, ChunkRenderer, Edit, Point, TextSummary}; -use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, ToOffset}; +use multi_buffer::{ + Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, RowInfo, ToOffset, +}; use std::{ any::TypeId, cmp::{self, Ordering}, @@ -336,9 +338,7 @@ impl FoldMap { let mut folds = self.snapshot.folds.iter().peekable(); while let Some(fold) = folds.next() { if let Some(next_fold) = folds.peek() { - let comparison = fold - .range - .cmp(&next_fold.range, &self.snapshot.inlay_snapshot.buffer); + let comparison = fold.range.cmp(&next_fold.range, self.snapshot.buffer()); assert!(comparison.is_le()); } } @@ -578,6 +578,10 @@ pub struct FoldSnapshot { } impl FoldSnapshot { + pub fn buffer(&self) -> &MultiBufferSnapshot { + &self.inlay_snapshot.buffer + } + #[cfg(test)] pub fn text(&self) -> String { self.chunks(FoldOffset(0)..self.len(), false, Highlights::default()) @@ -673,7 +677,7 @@ impl FoldSnapshot { (line_end - line_start) as u32 } - pub fn buffer_rows(&self, start_row: u32) -> FoldBufferRows { + pub fn row_infos(&self, start_row: u32) -> FoldRows { if start_row > self.transforms.summary().output.lines.row { panic!("invalid display row {}", start_row); } @@ -684,11 +688,11 @@ impl FoldSnapshot { let overshoot = fold_point.0 - cursor.start().0 .0; let inlay_point = InlayPoint(cursor.start().1 .0 + overshoot); - let input_buffer_rows = self.inlay_snapshot.buffer_rows(inlay_point.row()); + let input_rows = self.inlay_snapshot.row_infos(inlay_point.row()); - FoldBufferRows { + FoldRows { fold_point, - input_buffer_rows, + input_rows, cursor, } } @@ -843,8 +847,8 @@ fn push_isomorphic(transforms: &mut SumTree, summary: TextSummary) { transforms.update_last( |last| { if !last.is_fold() { - last.summary.input += summary.clone(); - last.summary.output += summary.clone(); + last.summary.input += summary; + last.summary.output += summary; did_merge = true; } }, @@ -854,7 +858,7 @@ fn push_isomorphic(transforms: &mut SumTree, summary: TextSummary) { transforms.push( Transform { summary: TransformSummary { - input: summary.clone(), + input: summary, output: summary, }, placeholder: None, @@ -1134,25 +1138,25 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for usize { } #[derive(Clone)] -pub struct FoldBufferRows<'a> { +pub struct FoldRows<'a> { cursor: Cursor<'a, Transform, (FoldPoint, InlayPoint)>, - input_buffer_rows: InlayBufferRows<'a>, + input_rows: InlayBufferRows<'a>, fold_point: FoldPoint, } -impl<'a> FoldBufferRows<'a> { +impl<'a> FoldRows<'a> { pub(crate) fn seek(&mut self, row: u32) { let fold_point = FoldPoint::new(row, 0); self.cursor.seek(&fold_point, Bias::Left, &()); let overshoot = fold_point.0 - self.cursor.start().0 .0; let inlay_point = InlayPoint(self.cursor.start().1 .0 + overshoot); - self.input_buffer_rows.seek(inlay_point.row()); + self.input_rows.seek(inlay_point.row()); self.fold_point = fold_point; } } -impl<'a> Iterator for FoldBufferRows<'a> { - type Item = Option; +impl<'a> Iterator for FoldRows<'a> { + type Item = RowInfo; fn next(&mut self) -> Option { let mut traversed_fold = false; @@ -1166,11 +1170,11 @@ impl<'a> Iterator for FoldBufferRows<'a> { if self.cursor.item().is_some() { if traversed_fold { - self.input_buffer_rows.seek(self.cursor.start().1.row()); - self.input_buffer_rows.next(); + self.input_rows.seek(self.cursor.start().1 .0.row); + self.input_rows.next(); } *self.fold_point.row_mut() += 1; - self.input_buffer_rows.next() + self.input_rows.next() } else { None } @@ -1683,12 +1687,12 @@ mod tests { .row(); expected_buffer_rows.extend( inlay_snapshot - .buffer_rows(prev_row) + .row_infos(prev_row) .take((1 + fold_start - prev_row) as usize), ); prev_row = 1 + fold_end; } - expected_buffer_rows.extend(inlay_snapshot.buffer_rows(prev_row)); + expected_buffer_rows.extend(inlay_snapshot.row_infos(prev_row)); assert_eq!( expected_buffer_rows.len(), @@ -1777,7 +1781,7 @@ mod tests { let mut fold_row = 0; while fold_row < expected_buffer_rows.len() as u32 { assert_eq!( - snapshot.buffer_rows(fold_row).collect::>(), + snapshot.row_infos(fold_row).collect::>(), expected_buffer_rows[(fold_row as usize)..], "wrong buffer rows starting at fold row {}", fold_row, @@ -1892,10 +1896,19 @@ mod tests { let (snapshot, _) = map.read(inlay_snapshot, vec![]); assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee\nffffff\n"); assert_eq!( - snapshot.buffer_rows(0).collect::>(), + snapshot + .row_infos(0) + .map(|info| info.buffer_row) + .collect::>(), [Some(0), Some(3), Some(5), Some(6)] ); - assert_eq!(snapshot.buffer_rows(3).collect::>(), [Some(6)]); + assert_eq!( + snapshot + .row_infos(3) + .map(|info| info.buffer_row) + .collect::>(), + [Some(6)] + ); } fn init_test(cx: &mut gpui::AppContext) { diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index cbf75943ba..9fba4aec25 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1,7 +1,9 @@ use crate::{HighlightStyles, InlayId}; use collections::BTreeSet; use language::{Chunk, Edit, Point, TextSummary}; -use multi_buffer::{Anchor, MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, ToOffset}; +use multi_buffer::{ + Anchor, MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, RowInfo, ToOffset, +}; use std::{ cmp, ops::{Add, AddAssign, Range, Sub, SubAssign}, @@ -67,11 +69,11 @@ impl Inlay { impl sum_tree::Item for Transform { type Summary = TransformSummary; - fn summary(&self, _cx: &()) -> Self::Summary { + fn summary(&self, _: &()) -> Self::Summary { match self { Transform::Isomorphic(summary) => TransformSummary { - input: summary.clone(), - output: summary.clone(), + input: *summary, + output: *summary, }, Transform::Inlay(inlay) => TransformSummary { input: TextSummary::default(), @@ -362,14 +364,14 @@ impl<'a> InlayBufferRows<'a> { } impl<'a> Iterator for InlayBufferRows<'a> { - type Item = Option; + type Item = RowInfo; fn next(&mut self) -> Option { let buffer_row = if self.inlay_row == 0 { self.buffer_rows.next().unwrap() } else { match self.transforms.item()? { - Transform::Inlay(_) => None, + Transform::Inlay(_) => Default::default(), Transform::Isomorphic(_) => self.buffer_rows.next().unwrap(), } }; @@ -448,7 +450,7 @@ impl InlayMap { new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left, &()), &()); if let Some(Transform::Isomorphic(transform)) = cursor.item() { if cursor.end(&()).0 == buffer_edit.old.start { - push_isomorphic(&mut new_transforms, transform.clone()); + push_isomorphic(&mut new_transforms, *transform); cursor.next(&()); } } @@ -892,7 +894,7 @@ impl InlaySnapshot { } pub fn text_summary(&self) -> TextSummary { - self.transforms.summary().output.clone() + self.transforms.summary().output } pub fn text_summary_for_range(&self, range: Range) -> TextSummary { @@ -945,7 +947,7 @@ impl InlaySnapshot { summary } - pub fn buffer_rows(&self, row: u32) -> InlayBufferRows<'_> { + pub fn row_infos(&self, row: u32) -> InlayBufferRows<'_> { let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&()); let inlay_point = InlayPoint::new(row, 0); cursor.seek(&inlay_point, Bias::Left, &()); @@ -967,7 +969,7 @@ impl InlaySnapshot { InlayBufferRows { transforms: cursor, inlay_row: inlay_point.row(), - buffer_rows: self.buffer.buffer_rows(buffer_row), + buffer_rows: self.buffer.row_infos(buffer_row), max_buffer_row, } } @@ -1477,7 +1479,10 @@ mod tests { ); assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi"); assert_eq!( - inlay_snapshot.buffer_rows(0).collect::>(), + inlay_snapshot + .row_infos(0) + .map(|info| info.buffer_row) + .collect::>(), vec![Some(0), None, Some(1), None, None, Some(2)] ); } @@ -1548,7 +1553,7 @@ mod tests { } assert_eq!(inlay_snapshot.text(), expected_text.to_string()); - let expected_buffer_rows = inlay_snapshot.buffer_rows(0).collect::>(); + let expected_buffer_rows = inlay_snapshot.row_infos(0).collect::>(); assert_eq!( expected_buffer_rows.len() as u32, expected_text.max_point().row + 1 @@ -1556,7 +1561,7 @@ mod tests { for row_start in 0..expected_buffer_rows.len() { assert_eq!( inlay_snapshot - .buffer_rows(row_start as u32) + .row_infos(row_start as u32) .collect::>(), &expected_buffer_rows[row_start..], "incorrect buffer rows starting at {}", diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index 86fa492712..69ba4ba8a7 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -272,8 +272,8 @@ impl TabSnapshot { } } - pub fn buffer_rows(&self, row: u32) -> fold_map::FoldBufferRows<'_> { - self.fold_snapshot.buffer_rows(row) + pub fn rows(&self, row: u32) -> fold_map::FoldRows<'_> { + self.fold_snapshot.row_infos(row) } #[cfg(test)] diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index ceb91ce0ab..30f59be767 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1,11 +1,11 @@ use super::{ - fold_map::FoldBufferRows, + fold_map::FoldRows, tab_map::{self, TabEdit, TabPoint, TabSnapshot}, Highlights, }; use gpui::{AppContext, Context, Font, LineWrapper, Model, ModelContext, Pixels, Task}; use language::{Chunk, Point}; -use multi_buffer::MultiBufferSnapshot; +use multi_buffer::{MultiBufferSnapshot, RowInfo}; use smol::future::yield_now; use std::sync::LazyLock; use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration}; @@ -60,16 +60,16 @@ pub struct WrapChunks<'a> { } #[derive(Clone)] -pub struct WrapBufferRows<'a> { - input_buffer_rows: FoldBufferRows<'a>, - input_buffer_row: Option, +pub struct WrapRows<'a> { + input_buffer_rows: FoldRows<'a>, + input_buffer_row: RowInfo, output_row: u32, soft_wrapped: bool, max_output_row: u32, transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>, } -impl<'a> WrapBufferRows<'a> { +impl<'a> WrapRows<'a> { pub(crate) fn seek(&mut self, start_row: u32) { self.transforms .seek(&WrapPoint::new(start_row, 0), Bias::Left, &()); @@ -717,7 +717,7 @@ impl WrapSnapshot { self.transforms.summary().output.longest_row } - pub fn buffer_rows(&self, start_row: u32) -> WrapBufferRows { + pub fn row_infos(&self, start_row: u32) -> WrapRows { let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &()); let mut input_row = transforms.start().1.row(); @@ -725,9 +725,9 @@ impl WrapSnapshot { input_row += start_row - transforms.start().0.row(); } let soft_wrapped = transforms.item().map_or(false, |t| !t.is_isomorphic()); - let mut input_buffer_rows = self.tab_snapshot.buffer_rows(input_row); + let mut input_buffer_rows = self.tab_snapshot.rows(input_row); let input_buffer_row = input_buffer_rows.next().unwrap(); - WrapBufferRows { + WrapRows { transforms, input_buffer_row, input_buffer_rows, @@ -847,7 +847,7 @@ impl WrapSnapshot { } let text = language::Rope::from(self.text().as_str()); - let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0); + let mut input_buffer_rows = self.tab_snapshot.rows(0); let mut expected_buffer_rows = Vec::new(); let mut prev_tab_row = 0; for display_row in 0..=self.max_point().row() { @@ -855,7 +855,7 @@ impl WrapSnapshot { if tab_point.row() == prev_tab_row && display_row != 0 { expected_buffer_rows.push(None); } else { - expected_buffer_rows.push(input_buffer_rows.next().unwrap()); + expected_buffer_rows.push(input_buffer_rows.next().unwrap().buffer_row); } prev_tab_row = tab_point.row(); @@ -864,7 +864,8 @@ impl WrapSnapshot { for start_display_row in 0..expected_buffer_rows.len() { assert_eq!( - self.buffer_rows(start_display_row as u32) + self.row_infos(start_display_row as u32) + .map(|row_info| row_info.buffer_row) .collect::>(), &expected_buffer_rows[start_display_row..], "invalid buffer_rows({}..)", @@ -958,8 +959,8 @@ impl<'a> Iterator for WrapChunks<'a> { } } -impl<'a> Iterator for WrapBufferRows<'a> { - type Item = Option; +impl<'a> Iterator for WrapRows<'a> { + type Item = RowInfo; fn next(&mut self) -> Option { if self.output_row > self.max_output_row { @@ -968,6 +969,7 @@ impl<'a> Iterator for WrapBufferRows<'a> { let buffer_row = self.input_buffer_row; let soft_wrapped = self.soft_wrapped; + let diff_status = self.input_buffer_row.diff_status; self.output_row += 1; self.transforms @@ -979,7 +981,15 @@ impl<'a> Iterator for WrapBufferRows<'a> { self.soft_wrapped = true; } - Some(if soft_wrapped { None } else { buffer_row }) + Some(if soft_wrapped { + RowInfo { + buffer_row: None, + multibuffer_row: None, + diff_status, + } + } else { + buffer_row + }) } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bb03206710..a40d0def33 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -25,7 +25,6 @@ mod git; mod highlight_matching_bracket; mod hover_links; mod hover_popover; -mod hunk_diff; mod indent_guides; mod inlay_hint_cache; pub mod items; @@ -56,7 +55,7 @@ use anyhow::{anyhow, Context as _, Result}; use blink_manager::BlinkManager; use client::{Collaborator, ParticipantIndex}; use clock::ReplicaId; -use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; +use collections::{BTreeMap, HashMap, HashSet, VecDeque}; use convert_case::{Case, Casing}; use display_map::*; pub use display_map::{DisplayPoint, FoldPlaceholder}; @@ -89,8 +88,6 @@ use gpui::{ }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; -pub(crate) use hunk_diff::HoveredHunk; -use hunk_diff::{diff_hunk_to_display, DiffMap, DiffMapSnapshot}; use indent_guides::ActiveIndentGuidesState; use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; pub use inline_completion::Direction; @@ -101,7 +98,8 @@ use language::{ language_settings::{self, all_language_settings, language_settings, InlayHintSettings}, markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel, CursorShape, Diagnostic, Documentation, EditPreview, HighlightedEdits, IndentKind, IndentSize, - Language, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId, + Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject, TransactionId, + TreeSitterOptions, }; use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange}; use linked_editing_ranges::refresh_linked_ranges; @@ -123,14 +121,13 @@ use lsp::{ use language::BufferSnapshot; use movement::TextLayoutDetails; pub use multi_buffer::{ - Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset, - ToPoint, + Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, RowInfo, + ToOffset, ToPoint, }; use multi_buffer::{ ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16, }; use project::{ - buffer_store::BufferChangeSet, lsp_store::{FormatTrigger, LspFormatTarget, OpenLspBufferHandle}, project_settings::{GitGutterSetting, ProjectSettings}, CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink, @@ -165,7 +162,7 @@ use text::{BufferId, OffsetUtf16, Rope}; use theme::{ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, ThemeColors, ThemeSettings}; use ui::{ h_flex, prelude::*, ButtonSize, ButtonStyle, Disclosure, IconButton, IconName, IconSize, - PopoverMenuHandle, Tooltip, + Tooltip, }; use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::item::{ItemHandle, PreviewTabsSettings}; @@ -271,7 +268,6 @@ impl InlayId { } } -enum DiffRowHighlight {} enum DocumentHighlightRead {} enum DocumentHighlightWrite {} enum InputComposition {} @@ -650,7 +646,6 @@ pub struct Editor { nav_history: Option, context_menu: RefCell>, mouse_context_menu: Option, - hunk_controls_menu_handle: PopoverMenuHandle, completion_tasks: Vec<(CompletionId, Task>)>, signature_help_state: SignatureHelpState, auto_signature_help: Option, @@ -685,7 +680,6 @@ pub struct Editor { show_inline_completions_override: Option, menu_inline_completions_policy: MenuInlineCompletionsPolicy, inlay_hint_cache: InlayHintCache, - diff_map: DiffMap, next_inlay_id: usize, _subscriptions: Vec, pixel_position_of_newest_cursor: Option>, @@ -755,7 +749,6 @@ pub struct EditorSnapshot { git_blame_gutter_max_author_length: Option, pub display_snapshot: DisplaySnapshot, pub placeholder_text: Option>, - diff_map: DiffMapSnapshot, is_focused: bool, scroll_anchor: ScrollAnchor, ongoing_scroll: OngoingScroll, @@ -1245,7 +1238,12 @@ impl Editor { let mut code_action_providers = Vec::new(); if let Some(project) = project.clone() { - get_unstaged_changes_for_buffers(&project, buffer.read(cx).all_buffers(), cx); + get_unstaged_changes_for_buffers( + &project, + buffer.read(cx).all_buffers(), + buffer.clone(), + cx, + ); code_action_providers.push(Rc::new(project) as Rc<_>); } @@ -1295,7 +1293,6 @@ impl Editor { nav_history: None, context_menu: RefCell::new(None), mouse_context_menu: None, - hunk_controls_menu_handle: PopoverMenuHandle::default(), completion_tasks: Default::default(), signature_help_state: SignatureHelpState::default(), auto_signature_help: None, @@ -1329,7 +1326,7 @@ impl Editor { inline_completion_provider: None, active_inline_completion: None, inlay_hint_cache: InlayHintCache::new(inlay_hint_settings), - diff_map: DiffMap::default(), + gutter_hovered: false, pixel_position_of_newest_cursor: None, last_bounds: None, @@ -1605,7 +1602,6 @@ impl Editor { scroll_anchor: self.scroll_manager.anchor(), ongoing_scroll: self.scroll_manager.ongoing_scroll(), placeholder_text: self.placeholder_text.clone(), - diff_map: self.diff_map.snapshot(), is_focused: self.focus_handle.is_focused(cx), current_line_highlight: self .current_line_highlight @@ -3602,9 +3598,9 @@ impl Editor { multi_buffer_snapshot .range_to_buffer_ranges(multi_buffer_visible_range) .into_iter() - .filter(|(_, excerpt_visible_range)| !excerpt_visible_range.is_empty()) - .filter_map(|(excerpt, excerpt_visible_range)| { - let buffer_file = project::File::from_dyn(excerpt.buffer().file())?; + .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()) + .filter_map(|(buffer, excerpt_visible_range, excerpt_id)| { + let buffer_file = project::File::from_dyn(buffer.file())?; let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?; let worktree_entry = buffer_worktree .read(cx) @@ -3613,17 +3609,17 @@ impl Editor { return None; } - let language = excerpt.buffer().language()?; + let language = buffer.language()?; if let Some(restrict_to_languages) = restrict_to_languages { if !restrict_to_languages.contains(language) { return None; } } Some(( - excerpt.id(), + excerpt_id, ( - multi_buffer.buffer(excerpt.buffer_id()).unwrap(), - excerpt.buffer().version().clone(), + multi_buffer.buffer(buffer.remote_id()).unwrap(), + buffer.version().clone(), excerpt_visible_range, ), )) @@ -4536,10 +4532,12 @@ impl Editor { buffer_id, excerpt_id, text_anchor: start, + diff_base_anchor: None, }..Anchor { buffer_id, excerpt_id, text_anchor: end, + diff_base_anchor: None, }; if highlight.kind == lsp::DocumentHighlightKind::WRITE { write_ranges.push(range); @@ -5262,7 +5260,7 @@ impl Editor { })) } - #[cfg(any(feature = "test-support", test))] + #[cfg(any(test, feature = "test-support"))] pub fn context_menu_visible(&self) -> bool { self.context_menu .borrow() @@ -6126,10 +6124,9 @@ impl Editor { pub fn revert_file(&mut self, _: &RevertFile, cx: &mut ViewContext) { let mut revert_changes = HashMap::default(); let snapshot = self.snapshot(cx); - for hunk in hunks_for_ranges( - Some(Point::zero()..snapshot.buffer_snapshot.max_point()).into_iter(), - &snapshot, - ) { + for hunk in snapshot + .hunks_for_ranges(Some(Point::zero()..snapshot.buffer_snapshot.max_point()).into_iter()) + { self.prepare_revert_change(&mut revert_changes, &hunk, cx); } if !revert_changes.is_empty() { @@ -6147,7 +6144,20 @@ impl Editor { } pub fn revert_selected_hunks(&mut self, _: &RevertSelectedHunks, cx: &mut ViewContext) { - let revert_changes = self.gather_revert_changes(&self.selections.all(cx), cx); + let selections = self.selections.all(cx).into_iter().map(|s| s.range()); + self.revert_hunks_in_ranges(selections, cx); + } + + fn revert_hunks_in_ranges( + &mut self, + ranges: impl Iterator>, + cx: &mut ViewContext, + ) { + let mut revert_changes = HashMap::default(); + let snapshot = self.snapshot(cx); + for hunk in &snapshot.hunks_for_ranges(ranges) { + self.prepare_revert_change(&mut revert_changes, &hunk, cx); + } if !revert_changes.is_empty() { self.transact(cx, |editor, cx| { editor.revert(revert_changes, cx); @@ -6155,18 +6165,6 @@ impl Editor { } } - fn revert_hunk(&mut self, hunk: HoveredHunk, cx: &mut ViewContext) { - let snapshot = self.buffer.read(cx).read(cx); - if let Some(hunk) = crate::hunk_diff::to_diff_hunk(&hunk, &snapshot) { - drop(snapshot); - let mut revert_changes = HashMap::default(); - self.prepare_revert_change(&mut revert_changes, &hunk, cx); - if !revert_changes.is_empty() { - self.revert(revert_changes, cx) - } - } - } - pub fn open_active_item_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext) { if let Some(working_directory) = self.active_excerpt(cx).and_then(|(_, buffer, _)| { let project_path = buffer.read(cx).project_path(cx)?; @@ -6184,33 +6182,20 @@ impl Editor { } } - fn gather_revert_changes( - &self, - selections: &[Selection], - cx: &mut ViewContext, - ) -> HashMap, Rope)>> { - let mut revert_changes = HashMap::default(); - let snapshot = self.snapshot(cx); - for hunk in hunks_for_selections(&snapshot, selections) { - self.prepare_revert_change(&mut revert_changes, &hunk, cx); - } - revert_changes - } - pub fn prepare_revert_change( &self, revert_changes: &mut HashMap, Rope)>>, hunk: &MultiBufferDiffHunk, - cx: &AppContext, + cx: &mut WindowContext, ) -> Option<()> { - let buffer = self.buffer.read(cx).buffer(hunk.buffer_id)?; + let buffer = self.buffer.read(cx); + let change_set = buffer.change_set_for(hunk.buffer_id)?; + let buffer = buffer.buffer(hunk.buffer_id)?; let buffer = buffer.read(cx); - let change_set = &self.diff_map.diff_bases.get(&hunk.buffer_id)?.change_set; let original_text = change_set .read(cx) .base_text .as_ref()? - .read(cx) .as_rope() .slice(hunk.diff_base_byte_range.clone()); let buffer_snapshot = buffer.snapshot(); @@ -6551,12 +6536,8 @@ impl Editor { // Don't move lines across excerpts if buffer - .excerpt_boundaries_in_range(( - Bound::Excluded(insertion_point), - Bound::Included(range_to_move.end), - )) - .next() - .is_none() + .excerpt_containing(insertion_point..range_to_move.end) + .is_some() { let text = buffer .text_for_range(range_to_move.clone()) @@ -6649,12 +6630,8 @@ impl Editor { // Don't move lines across excerpt boundaries if buffer - .excerpt_boundaries_in_range(( - Bound::Excluded(range_to_move.start), - Bound::Included(insertion_point), - )) - .next() - .is_none() + .excerpt_containing(range_to_move.start..insertion_point) + .is_some() { let mut text = String::from("\n"); text.extend(buffer.text_for_range(range_to_move.clone())); @@ -9282,11 +9259,7 @@ impl Editor { let snapshot = buffer.snapshot(cx); let mut excerpt_ids = selections .iter() - .flat_map(|selection| { - snapshot - .excerpts_for_range(selection.range()) - .map(|excerpt| excerpt.id()) - }) + .flat_map(|selection| snapshot.excerpt_ids_for_range(selection.range())) .collect::>(); excerpt_ids.sort(); excerpt_ids.dedup(); @@ -9306,6 +9279,30 @@ impl Editor { }) } + pub fn go_to_singleton_buffer_point(&mut self, point: Point, cx: &mut ViewContext) { + self.go_to_singleton_buffer_range(point..point, cx); + } + + pub fn go_to_singleton_buffer_range( + &mut self, + range: Range, + cx: &mut ViewContext, + ) { + let multibuffer = self.buffer().read(cx); + let Some(buffer) = multibuffer.as_singleton() else { + return; + }; + let Some(start) = multibuffer.buffer_point_to_anchor(&buffer, range.start, cx) else { + return; + }; + let Some(end) = multibuffer.buffer_point_to_anchor(&buffer, range.end, cx) else { + return; + }; + self.change_selections(Some(Autoscroll::center()), cx, |s| { + s.select_anchor_ranges([start..end]) + }); + } + fn go_to_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext) { self.go_to_diagnostic_impl(Direction::Next, cx) } @@ -9321,7 +9318,14 @@ impl Editor { // If there is an active Diagnostic Popover jump to its diagnostic instead. if direction == Direction::Next { if let Some(popover) = self.hover_state.diagnostic_popover.as_ref() { - self.activate_diagnostics(popover.group_id(), cx); + let Some(buffer_id) = popover.local_diagnostic.range.start.buffer_id else { + return; + }; + self.activate_diagnostics( + buffer_id, + popover.local_diagnostic.diagnostic.group_id, + cx, + ); if let Some(active_diagnostics) = self.active_diagnostics.as_ref() { let primary_range_start = active_diagnostics.primary_range.start; self.change_selections(Some(Autoscroll::fit()), cx, |s| { @@ -9352,25 +9356,27 @@ impl Editor { }; let snapshot = self.snapshot(cx); loop { - let diagnostics = if direction == Direction::Prev { - buffer.diagnostics_in_range(0..search_start, true) + let mut diagnostics; + if direction == Direction::Prev { + diagnostics = buffer + .diagnostics_in_range::<_, usize>(0..search_start) + .collect::>(); + diagnostics.reverse(); } else { - buffer.diagnostics_in_range(search_start..buffer.len(), false) - } - .filter(|diagnostic| !snapshot.intersects_fold(diagnostic.range.start)); - let search_start_anchor = buffer.anchor_after(search_start); + diagnostics = buffer + .diagnostics_in_range::<_, usize>(search_start..buffer.len()) + .collect::>(); + }; let group = diagnostics + .into_iter() + .filter(|diagnostic| !snapshot.intersects_fold(diagnostic.range.start)) // relies on diagnostics_in_range to return diagnostics with the same starting range to // be sorted in a stable way // skip until we are at current active diagnostic, if it exists .skip_while(|entry| { let is_in_range = match direction { - Direction::Prev => { - entry.range.start.cmp(&search_start_anchor, &buffer).is_ge() - } - Direction::Next => { - entry.range.start.cmp(&search_start_anchor, &buffer).is_le() - } + Direction::Prev => entry.range.end > search_start, + Direction::Next => entry.range.start < search_start, }; is_in_range && self @@ -9381,7 +9387,7 @@ impl Editor { .find_map(|entry| { if entry.diagnostic.is_primary && entry.diagnostic.severity <= DiagnosticSeverity::WARNING - && !(entry.range.start == entry.range.end) + && entry.range.start != entry.range.end // if we match with the active diagnostic, skip it && Some(entry.diagnostic.group_id) != self.active_diagnostics.as_ref().map(|d| d.group_id) @@ -9393,8 +9399,10 @@ impl Editor { }); if let Some((primary_range, group_id)) = group { - self.activate_diagnostics(group_id, cx); - let primary_range = primary_range.to_offset(&buffer); + let Some(buffer_id) = buffer.anchor_after(primary_range.start).buffer_id else { + return; + }; + self.activate_diagnostics(buffer_id, group_id, cx); if self.active_diagnostics.is_some() { self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select(vec![Selection { @@ -9439,21 +9447,25 @@ impl Editor { position: Point, cx: &mut ViewContext, ) -> Option { - for (ix, position) in [position, Point::zero()].into_iter().enumerate() { - if let Some(hunk) = self.go_to_next_hunk_in_direction( - snapshot, - position, - ix > 0, - snapshot.diff_map.diff_hunks_in_range( - position + Point::new(1, 0)..snapshot.buffer_snapshot.max_point(), - &snapshot.buffer_snapshot, - ), - cx, - ) { - return Some(hunk); - } + let mut hunk = snapshot + .buffer_snapshot + .diff_hunks_in_range(position..snapshot.buffer_snapshot.max_point()) + .find(|hunk| hunk.row_range.start.0 > position.row); + if hunk.is_none() { + hunk = snapshot + .buffer_snapshot + .diff_hunks_in_range(Point::zero()..position) + .find(|hunk| hunk.row_range.end.0 < position.row) } - None + if let Some(hunk) = &hunk { + let destination = Point::new(hunk.row_range.start.0, 0); + self.unfold_ranges(&[destination..destination], false, false, cx); + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges(vec![destination..destination]); + }); + } + + hunk } fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, cx: &mut ViewContext) { @@ -9468,52 +9480,19 @@ impl Editor { position: Point, cx: &mut ViewContext, ) -> Option { - for (ix, position) in [position, snapshot.buffer_snapshot.max_point()] - .into_iter() - .enumerate() - { - if let Some(hunk) = self.go_to_next_hunk_in_direction( - snapshot, - position, - ix > 0, - snapshot - .diff_map - .diff_hunks_in_range_rev(Point::zero()..position, &snapshot.buffer_snapshot), - cx, - ) { - return Some(hunk); - } + let mut hunk = snapshot.buffer_snapshot.diff_hunk_before(position); + if hunk.is_none() { + hunk = snapshot.buffer_snapshot.diff_hunk_before(Point::MAX); } - None - } - - fn go_to_next_hunk_in_direction( - &mut self, - snapshot: &DisplaySnapshot, - initial_point: Point, - is_wrapped: bool, - hunks: impl Iterator, - cx: &mut ViewContext, - ) -> Option { - let display_point = initial_point.to_display_point(snapshot); - let mut hunks = hunks - .map(|hunk| (diff_hunk_to_display(&hunk, snapshot), hunk)) - .filter(|(display_hunk, _)| { - is_wrapped || !display_hunk.contains_display_row(display_point.row()) - }) - .dedup(); - - if let Some((display_hunk, hunk)) = hunks.next() { + if let Some(hunk) = &hunk { + let destination = Point::new(hunk.row_range.start.0, 0); + self.unfold_ranges(&[destination..destination], false, false, cx); self.change_selections(Some(Autoscroll::fit()), cx, |s| { - let row = display_hunk.start_display_row(); - let point = DisplayPoint::new(row, 0); - s.select_display_ranges([point..point]); + s.select_ranges(vec![destination..destination]); }); - - Some(hunk) - } else { - None } + + hunk } pub fn go_to_definition( @@ -9762,15 +9741,12 @@ impl Editor { }; let pane = workspace.read(cx).active_pane().clone(); - let range = target.range.to_offset(target.buffer.read(cx)); + let range = target.range.to_point(target.buffer.read(cx)); let range = editor.range_for_match(&range); + let range = collapse_multiline_range(range); if Some(&target.buffer) == editor.buffer.read(cx).as_singleton().as_ref() { - let buffer = target.buffer.read(cx); - let range = check_multiline_range(buffer, range); - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_ranges([range]); - }); + editor.go_to_singleton_buffer_range(range.clone(), cx); } else { cx.window_context().defer(move |cx| { let target_editor: View = @@ -9793,15 +9769,7 @@ impl Editor { // When selecting a definition in a different buffer, disable the nav history // to avoid creating a history entry at the previous cursor location. pane.update(cx, |pane, _| pane.disable_history()); - let buffer = target.buffer.read(cx); - let range = check_multiline_range(buffer, range); - target_editor.change_selections( - Some(Autoscroll::focused()), - cx, - |s| { - s.select_ranges([range]); - }, - ); + target_editor.go_to_singleton_buffer_range(range, cx); pane.update(cx, |pane, _| pane.enable_history()); }); }); @@ -10420,11 +10388,12 @@ impl Editor { let mut buffer_id_to_ranges: BTreeMap>> = BTreeMap::new(); for selection_range in selection_ranges { - for (excerpt, buffer_range) in snapshot.range_to_buffer_ranges(selection_range) + for (buffer, buffer_range, _) in + snapshot.range_to_buffer_ranges(selection_range) { - let buffer_id = excerpt.buffer_id(); - let start = excerpt.buffer().anchor_before(buffer_range.start); - let end = excerpt.buffer().anchor_after(buffer_range.end); + let buffer_id = buffer.remote_id(); + let start = buffer.anchor_before(buffer_range.start); + let end = buffer.anchor_after(buffer_range.end); buffers.insert(multi_buffer.buffer(buffer_id).unwrap()); buffer_id_to_ranges .entry(buffer_id) @@ -10499,12 +10468,11 @@ impl Editor { let buffer = self.buffer.read(cx).snapshot(cx); let primary_range_start = active_diagnostics.primary_range.start.to_offset(&buffer); let is_valid = buffer - .diagnostics_in_range(active_diagnostics.primary_range.clone(), false) + .diagnostics_in_range::<_, usize>(active_diagnostics.primary_range.clone()) .any(|entry| { - let range = entry.range.to_offset(&buffer); entry.diagnostic.is_primary - && !range.is_empty() - && range.start == primary_range_start + && !entry.range.is_empty() + && entry.range.start == primary_range_start && entry.diagnostic.message == active_diagnostics.primary_message }); @@ -10524,7 +10492,12 @@ impl Editor { } } - fn activate_diagnostics(&mut self, group_id: usize, cx: &mut ViewContext) { + fn activate_diagnostics( + &mut self, + buffer_id: BufferId, + group_id: usize, + cx: &mut ViewContext, + ) { self.dismiss_diagnostics(cx); let snapshot = self.snapshot(cx); self.active_diagnostics = self.display_map.update(cx, |display_map, cx| { @@ -10532,21 +10505,17 @@ impl Editor { let mut primary_range = None; let mut primary_message = None; - let mut group_end = Point::zero(); let diagnostic_group = buffer - .diagnostic_group(group_id) + .diagnostic_group(buffer_id, group_id) .filter_map(|entry| { - let start = entry.range.start.to_point(&buffer); - let end = entry.range.end.to_point(&buffer); + let start = entry.range.start; + let end = entry.range.end; if snapshot.is_line_folded(MultiBufferRow(start.row)) && (start.row == end.row || snapshot.is_line_folded(MultiBufferRow(end.row))) { return None; } - if end > group_end { - group_end = end; - } if entry.diagnostic.is_primary { primary_range = Some(entry.range.clone()); primary_message = Some(entry.diagnostic.message.clone()); @@ -10579,7 +10548,8 @@ impl Editor { .collect(); Some(ActiveDiagnosticGroup { - primary_range, + primary_range: buffer.anchor_before(primary_range.start) + ..buffer.anchor_after(primary_range.end), primary_message, group_id, blocks, @@ -10721,17 +10691,16 @@ impl Editor { } } else { let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); - let mut toggled_buffers = HashSet::default(); - for (_, buffer_snapshot, _) in - multi_buffer_snapshot.excerpts_in_ranges(self.selections.disjoint_anchor_ranges()) - { - let buffer_id = buffer_snapshot.remote_id(); - if toggled_buffers.insert(buffer_id) { - if self.buffer_folded(buffer_id, cx) { - self.unfold_buffer(buffer_id, cx); - } else { - self.fold_buffer(buffer_id, cx); - } + let buffer_ids: HashSet<_> = multi_buffer_snapshot + .ranges_to_buffer_ranges(self.selections.disjoint_anchor_ranges()) + .map(|(snapshot, _, _)| snapshot.remote_id()) + .collect(); + + for buffer_id in buffer_ids { + if self.is_buffer_folded(buffer_id, cx) { + self.unfold_buffer(buffer_id, cx); + } else { + self.fold_buffer(buffer_id, cx); } } } @@ -10804,14 +10773,13 @@ impl Editor { self.fold_creases(to_fold, true, cx); } else { let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); - let mut folded_buffers = HashSet::default(); - for (_, buffer_snapshot, _) in - multi_buffer_snapshot.excerpts_in_ranges(self.selections.disjoint_anchor_ranges()) - { - let buffer_id = buffer_snapshot.remote_id(); - if folded_buffers.insert(buffer_id) { - self.fold_buffer(buffer_id, cx); - } + + let buffer_ids: HashSet<_> = multi_buffer_snapshot + .ranges_to_buffer_ranges(self.selections.disjoint_anchor_ranges()) + .map(|(snapshot, _, _)| snapshot.remote_id()) + .collect(); + for buffer_id in buffer_ids { + self.fold_buffer(buffer_id, cx); } } } @@ -10885,11 +10853,14 @@ impl Editor { cx: &mut ViewContext, ) { let snapshot = self.buffer.read(cx).snapshot(cx); - let Some((_, _, buffer)) = snapshot.as_singleton() else { - return; - }; - let creases = buffer - .function_body_fold_ranges(0..buffer.len()) + + let ranges = snapshot + .text_object_ranges(0..snapshot.len(), TreeSitterOptions::default()) + .filter_map(|(range, obj)| (obj == TextObject::InsideFunction).then_some(range)) + .collect::>(); + + let creases = ranges + .into_iter() .map(|range| Crease::simple(range, self.display_map.read(cx).fold_placeholder.clone())) .collect(); @@ -10967,14 +10938,12 @@ impl Editor { self.unfold_ranges(&ranges, true, true, cx); } else { let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); - let mut unfolded_buffers = HashSet::default(); - for (_, buffer_snapshot, _) in - multi_buffer_snapshot.excerpts_in_ranges(self.selections.disjoint_anchor_ranges()) - { - let buffer_id = buffer_snapshot.remote_id(); - if unfolded_buffers.insert(buffer_id) { - self.unfold_buffer(buffer_id, cx); - } + let buffer_ids: HashSet<_> = multi_buffer_snapshot + .ranges_to_buffer_ranges(self.selections.disjoint_anchor_ranges()) + .map(|(snapshot, _, _)| snapshot.remote_id()) + .collect(); + for buffer_id in buffer_ids { + self.unfold_buffer(buffer_id, cx); } } } @@ -11096,10 +11065,6 @@ impl Editor { self.request_autoscroll(Autoscroll::fit(), cx); } - for buffer_id in buffers_affected { - Self::sync_expanded_diff_hunks(&mut self.diff_map, buffer_id, cx); - } - cx.notify(); if let Some(active_diagnostics) = self.active_diagnostics.take() { @@ -11131,7 +11096,7 @@ impl Editor { } pub fn fold_buffer(&mut self, buffer_id: BufferId, cx: &mut ViewContext) { - if self.buffer().read(cx).is_singleton() || self.buffer_folded(buffer_id, cx) { + if self.buffer().read(cx).is_singleton() || self.is_buffer_folded(buffer_id, cx) { return; } let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else { @@ -11148,7 +11113,7 @@ impl Editor { } pub fn unfold_buffer(&mut self, buffer_id: BufferId, cx: &mut ViewContext) { - if self.buffer().read(cx).is_singleton() || !self.buffer_folded(buffer_id, cx) { + if self.buffer().read(cx).is_singleton() || !self.is_buffer_folded(buffer_id, cx) { return; } let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else { @@ -11165,8 +11130,12 @@ impl Editor { cx.notify(); } - pub fn buffer_folded(&self, buffer: BufferId, cx: &AppContext) -> bool { - self.display_map.read(cx).buffer_folded(buffer) + pub fn is_buffer_folded(&self, buffer: BufferId, cx: &AppContext) -> bool { + self.display_map.read(cx).is_buffer_folded(buffer) + } + + pub fn folded_buffers<'a>(&self, cx: &'a AppContext) -> &'a HashSet { + self.display_map.read(cx).folded_buffers() } /// Removes any folds with the given ranges. @@ -11207,10 +11176,6 @@ impl Editor { self.request_autoscroll(Autoscroll::fit(), cx); } - for buffer_id in buffers_affected { - Self::sync_expanded_diff_hunks(&mut self.diff_map, buffer_id, cx); - } - cx.notify(); self.scrollbar_marker_state.dirty = true; self.active_indent_guides_state.dirty = true; @@ -11220,6 +11185,108 @@ impl Editor { self.display_map.read(cx).fold_placeholder.clone() } + pub fn set_expand_all_diff_hunks(&mut self, cx: &mut AppContext) { + self.buffer.update(cx, |buffer, cx| { + buffer.set_all_diff_hunks_expanded(cx); + }); + } + + pub fn expand_all_diff_hunks(&mut self, _: &ExpandAllHunkDiffs, cx: &mut ViewContext) { + self.buffer.update(cx, |buffer, cx| { + buffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx) + }); + } + + pub fn toggle_selected_diff_hunks( + &mut self, + _: &ToggleSelectedDiffHunks, + cx: &mut ViewContext, + ) { + let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect(); + self.toggle_diff_hunks_in_ranges(ranges, cx); + } + + pub fn expand_selected_diff_hunks(&mut self, cx: &mut ViewContext) { + let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect(); + self.buffer + .update(cx, |buffer, cx| buffer.expand_diff_hunks(ranges, cx)) + } + + pub fn clear_expanded_diff_hunks(&mut self, cx: &mut ViewContext) -> bool { + self.buffer.update(cx, |buffer, cx| { + let ranges = vec![Anchor::min()..Anchor::max()]; + if !buffer.all_diff_hunks_expanded() + && buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx) + { + buffer.collapse_diff_hunks(ranges, cx); + true + } else { + false + } + }) + } + + fn toggle_diff_hunks_in_ranges( + &mut self, + ranges: Vec>, + cx: &mut ViewContext<'_, Editor>, + ) { + self.buffer.update(cx, |buffer, cx| { + if buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx) { + buffer.collapse_diff_hunks(ranges, cx) + } else { + buffer.expand_diff_hunks(ranges, cx) + } + }) + } + + pub(crate) fn apply_all_diff_hunks( + &mut self, + _: &ApplyAllDiffHunks, + cx: &mut ViewContext, + ) { + let buffers = self.buffer.read(cx).all_buffers(); + for branch_buffer in buffers { + branch_buffer.update(cx, |branch_buffer, cx| { + branch_buffer.merge_into_base(Vec::new(), cx); + }); + } + + if let Some(project) = self.project.clone() { + self.save(true, project, cx).detach_and_log_err(cx); + } + } + + pub(crate) fn apply_selected_diff_hunks( + &mut self, + _: &ApplyDiffHunk, + cx: &mut ViewContext, + ) { + let snapshot = self.snapshot(cx); + let hunks = snapshot.hunks_for_ranges(self.selections.ranges(cx).into_iter()); + let mut ranges_by_buffer = HashMap::default(); + self.transact(cx, |editor, cx| { + for hunk in hunks { + if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) { + ranges_by_buffer + .entry(buffer.clone()) + .or_insert_with(Vec::new) + .push(hunk.buffer_range.to_offset(buffer.read(cx))); + } + } + + for (buffer, ranges) in ranges_by_buffer { + buffer.update(cx, |buffer, cx| { + buffer.merge_into_base(ranges, cx); + }); + } + }); + + if let Some(project) = self.project.clone() { + self.save(true, project, cx).detach_and_log_err(cx); + } + } + pub fn set_gutter_hovered(&mut self, hovered: bool, cx: &mut ViewContext) { if hovered != self.gutter_hovered { self.gutter_hovered = hovered; @@ -11751,29 +11818,22 @@ impl Editor { let selection = self.selections.newest::(cx); let selection_range = selection.range(); - let (buffer, selection) = if let Some(buffer) = self.buffer().read(cx).as_singleton() { - (buffer, selection_range.start.row..selection_range.end.row) + let multi_buffer = self.buffer().read(cx); + let multi_buffer_snapshot = multi_buffer.snapshot(cx); + let buffer_ranges = multi_buffer_snapshot.range_to_buffer_ranges(selection_range); + + let (buffer, range, _) = if selection.reversed { + buffer_ranges.first() } else { - let multi_buffer = self.buffer().read(cx); - let multi_buffer_snapshot = multi_buffer.snapshot(cx); - let buffer_ranges = multi_buffer_snapshot.range_to_buffer_ranges(selection_range); + buffer_ranges.last() + }?; - let (excerpt, range) = if selection.reversed { - buffer_ranges.first() - } else { - buffer_ranges.last() - }?; - - let snapshot = excerpt.buffer(); - let selection = text::ToPoint::to_point(&range.start, &snapshot).row - ..text::ToPoint::to_point(&range.end, &snapshot).row; - ( - multi_buffer.buffer(excerpt.buffer_id()).unwrap().clone(), - selection, - ) - }; - - Some((buffer, selection)) + let selection = text::ToPoint::to_point(&range.start, &buffer).row + ..text::ToPoint::to_point(&range.end, &buffer).row; + Some(( + multi_buffer.buffer(buffer.remote_id()).unwrap().clone(), + selection, + )) }); let Some((buffer, selection)) = buffer_and_selection else { @@ -12543,9 +12603,14 @@ impl Editor { } => { self.tasks_update_task = Some(self.refresh_runnables(cx)); let buffer_id = buffer.read(cx).remote_id(); - if !self.diff_map.diff_bases.contains_key(&buffer_id) { + if self.buffer.read(cx).change_set_for(buffer_id).is_none() { if let Some(project) = &self.project { - get_unstaged_changes_for_buffers(project, [buffer.clone()], cx); + get_unstaged_changes_for_buffers( + project, + [buffer.clone()], + self.buffer.clone(), + cx, + ); } } cx.emit(EditorEvent::ExcerptsAdded { @@ -12664,14 +12729,14 @@ impl Editor { let multi_buffer_snapshot = multi_buffer.snapshot(cx); let mut new_selections_by_buffer = HashMap::default(); for selection in selections { - for (excerpt, range) in + for (buffer, range, _) in multi_buffer_snapshot.range_to_buffer_ranges(selection.start..selection.end) { - let mut range = range.to_point(excerpt.buffer()); + let mut range = range.to_point(buffer); range.start.column = 0; - range.end.column = excerpt.buffer().line_len(range.end.row); + range.end.column = buffer.line_len(range.end.row); new_selections_by_buffer - .entry(multi_buffer.buffer(excerpt.buffer_id()).unwrap()) + .entry(multi_buffer.buffer(buffer.remote_id()).unwrap()) .or_insert(Vec::new()) .push(range) } @@ -12772,13 +12837,13 @@ impl Editor { let selections = self.selections.all::(cx); let multi_buffer = self.buffer.read(cx); for selection in selections { - for (excerpt, mut range) in multi_buffer + for (buffer, mut range, _) in multi_buffer .snapshot(cx) .range_to_buffer_ranges(selection.range()) { // When editing branch buffers, jump to the corresponding location // in their base buffer. - let mut buffer_handle = multi_buffer.buffer(excerpt.buffer_id()).unwrap(); + let mut buffer_handle = multi_buffer.buffer(buffer.remote_id()).unwrap(); let buffer = buffer_handle.read(cx); if let Some(base_buffer) = buffer.base_buffer() { range = buffer.range_to_version(range, &base_buffer.read(cx).version()); @@ -12955,17 +13020,13 @@ impl Editor { /// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines, /// with each line being an array of {text, highlight} objects. fn copy_highlight_json(&mut self, _: &CopyHighlightJson, cx: &mut ViewContext) { - let Some(buffer) = self.buffer.read(cx).as_singleton() else { - return; - }; - #[derive(Serialize)] struct Chunk<'a> { text: String, highlight: Option<&'a str>, } - let snapshot = buffer.read(cx).snapshot(); + let snapshot = self.buffer.read(cx).snapshot(cx); let range = self .selected_text_range(false, cx) .and_then(|selection| { @@ -13253,14 +13314,6 @@ impl Editor { .and_then(|item| item.to_any().downcast_ref::()) } - pub fn add_change_set( - &mut self, - change_set: Model, - cx: &mut ViewContext, - ) { - self.diff_map.add_change_set(change_set, cx); - } - fn character_size(&self, cx: &mut ViewContext) -> gpui::Point { let text_layout_details = self.text_layout_details(cx); let style = &text_layout_details.editor_style; @@ -13282,7 +13335,8 @@ impl Editor { fn get_unstaged_changes_for_buffers( project: &Model, buffers: impl IntoIterator>, - cx: &mut ViewContext, + buffer: Model, + cx: &mut AppContext, ) { let mut tasks = Vec::new(); project.update(cx, |project, cx| { @@ -13290,16 +13344,17 @@ fn get_unstaged_changes_for_buffers( tasks.push(project.open_unstaged_changes(buffer.clone(), cx)) } }); - cx.spawn(|this, mut cx| async move { + cx.spawn(|mut cx| async move { let change_sets = futures::future::join_all(tasks).await; - this.update(&mut cx, |this, cx| { - for change_set in change_sets { - if let Some(change_set) = change_set.log_err() { - this.diff_map.add_change_set(change_set, cx); + buffer + .update(&mut cx, |buffer, cx| { + for change_set in change_sets { + if let Some(change_set) = change_set.log_err() { + buffer.add_change_set(change_set, cx); + } } - } - }) - .ok(); + }) + .ok(); }) .detach(); } @@ -13587,56 +13642,6 @@ fn test_wrap_with_prefix() { ); } -fn hunks_for_selections( - snapshot: &EditorSnapshot, - selections: &[Selection], -) -> Vec { - hunks_for_ranges( - selections.iter().map(|selection| selection.range()), - snapshot, - ) -} - -pub fn hunks_for_ranges( - ranges: impl Iterator>, - snapshot: &EditorSnapshot, -) -> Vec { - let mut hunks = Vec::new(); - let mut processed_buffer_rows: HashMap>> = - HashMap::default(); - for query_range in ranges { - let query_rows = - MultiBufferRow(query_range.start.row)..MultiBufferRow(query_range.end.row + 1); - for hunk in snapshot.diff_map.diff_hunks_in_range( - Point::new(query_rows.start.0, 0)..Point::new(query_rows.end.0, 0), - &snapshot.buffer_snapshot, - ) { - // Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it - // when the caret is just above or just below the deleted hunk. - let allow_adjacent = hunk_status(&hunk) == DiffHunkStatus::Removed; - let related_to_selection = if allow_adjacent { - hunk.row_range.overlaps(&query_rows) - || hunk.row_range.start == query_rows.end - || hunk.row_range.end == query_rows.start - } else { - hunk.row_range.overlaps(&query_rows) - }; - if related_to_selection { - if !processed_buffer_rows - .entry(hunk.buffer_id) - .or_default() - .insert(hunk.buffer_range.start..hunk.buffer_range.end) - { - continue; - } - hunks.push(hunk); - } - } - } - - hunks -} - pub trait CollaborationHub { fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap; fn user_participant_indices<'a>( @@ -14227,6 +14232,45 @@ impl EditorSnapshot { }) } + pub fn hunks_for_ranges( + &self, + ranges: impl Iterator>, + ) -> Vec { + let mut hunks = Vec::new(); + let mut processed_buffer_rows: HashMap>> = + HashMap::default(); + for query_range in ranges { + let query_rows = + MultiBufferRow(query_range.start.row)..MultiBufferRow(query_range.end.row + 1); + for hunk in self.buffer_snapshot.diff_hunks_in_range( + Point::new(query_rows.start.0, 0)..Point::new(query_rows.end.0, 0), + ) { + // Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it + // when the caret is just above or just below the deleted hunk. + let allow_adjacent = hunk.status() == DiffHunkStatus::Removed; + let related_to_selection = if allow_adjacent { + hunk.row_range.overlaps(&query_rows) + || hunk.row_range.start == query_rows.end + || hunk.row_range.end == query_rows.start + } else { + hunk.row_range.overlaps(&query_rows) + }; + if related_to_selection { + if !processed_buffer_rows + .entry(hunk.buffer_id) + .or_default() + .insert(hunk.buffer_range.start..hunk.buffer_range.end) + { + continue; + } + hunks.push(hunk); + } + } + } + + hunks + } + pub fn language_at(&self, position: T) -> Option<&Arc> { self.display_snapshot.buffer_snapshot.language_at(position) } @@ -15239,20 +15283,10 @@ impl RowRangeExt for Range { } } -fn hunk_status(hunk: &MultiBufferDiffHunk) -> DiffHunkStatus { - if hunk.diff_base_byte_range.is_empty() { - DiffHunkStatus::Added - } else if hunk.row_range.is_empty() { - DiffHunkStatus::Removed - } else { - DiffHunkStatus::Modified - } -} - /// If select range has more than one line, we /// just point the cursor to range.start. -fn check_multiline_range(buffer: &Buffer, range: Range) -> Range { - if buffer.offset_to_point(range.start).row == buffer.offset_to_point(range.end).row { +fn collapse_multiline_range(range: Range) -> Range { + if range.start.row == range.end.row { range } else { range.start..range.start diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 6d4804b508..6d89af9e8b 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -19,11 +19,11 @@ use language::{ }, BracketPairConfig, Capability::ReadWrite, - FakeLspAdapter, IndentGuide, LanguageConfig, LanguageConfigOverride, LanguageMatcher, - LanguageName, Override, ParsedMarkdown, Point, + FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName, + Override, ParsedMarkdown, Point, }; use language_settings::{Formatter, FormatterList, IndentGuideSettings}; -use multi_buffer::MultiBufferIndentGuide; +use multi_buffer::IndentGuide; use parking_lot::Mutex; use pretty_assertions::{assert_eq, assert_ne}; use project::{buffer_store::BufferChangeSet, FakeFs}; @@ -3363,8 +3363,8 @@ async fn test_custom_newlines_cause_no_false_positive_diffs( let snapshot = editor.snapshot(cx); assert_eq!( snapshot - .diff_map - .diff_hunks_in_range(0..snapshot.buffer_snapshot.len(), &snapshot.buffer_snapshot) + .buffer_snapshot + .diff_hunks_in_range(0..snapshot.buffer_snapshot.len()) .collect::>(), Vec::new(), "Should not have any diffs for files with custom newlines" @@ -5480,6 +5480,109 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { }); } +#[gpui::test] +async fn test_fold_function_bodies(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let base_text = r#" + impl A { + // this is an unstaged comment + + fn b() { + c(); + } + + // this is another unstaged comment + + fn d() { + // e + // f + } + } + + fn g() { + // h + } + "# + .unindent(); + + let text = r#" + ˇimpl A { + + fn b() { + c(); + } + + fn d() { + // e + // f + } + } + + fn g() { + // h + } + "# + .unindent(); + + let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await; + cx.set_state(&text); + cx.set_diff_base(&base_text); + cx.update_editor(|editor, cx| { + editor.expand_all_diff_hunks(&Default::default(), cx); + }); + + cx.assert_state_with_diff( + " + ˇimpl A { + - // this is an unstaged comment + + fn b() { + c(); + } + + - // this is another unstaged comment + - + fn d() { + // e + // f + } + } + + fn g() { + // h + } + " + .unindent(), + ); + + let expected_display_text = " + impl A { + // this is an unstaged comment + + fn b() { + ⋯ + } + + // this is another unstaged comment + + fn d() { + ⋯ + } + } + + fn g() { + ⋯ + } + " + .unindent(); + + cx.update_editor(|editor, cx| { + editor.fold_function_bodies(&FoldFunctionBodies, cx); + assert_eq!(editor.display_text(cx), expected_display_text); + }); +} + #[gpui::test] async fn test_autoindent(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); @@ -10319,7 +10422,7 @@ async fn test_diagnostics_with_links(cx: &mut TestAppContext) { } #[gpui::test] -async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { +async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; @@ -10420,7 +10523,26 @@ async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) ); cx.update_editor(|editor, cx| { - for _ in 0..3 { + editor.go_to_prev_hunk(&GoToPrevHunk, cx); + }); + + cx.assert_editor_state( + &r#" + ˇuse some::modified; + + + fn main() { + println!("hello there"); + + println!("around the"); + println!("world"); + } + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| { + for _ in 0..2 { editor.go_to_prev_hunk(&GoToPrevHunk, cx); } }); @@ -10442,11 +10564,10 @@ async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) cx.update_editor(|editor, cx| { editor.fold(&Fold, cx); + }); - //Make sure that the fold only gets one hunk - for _ in 0..4 { - editor.go_to_next_hunk(&GoToHunk, cx); - } + cx.update_editor(|editor, cx| { + editor.go_to_next_hunk(&GoToHunk, cx); }); cx.assert_editor_state( @@ -11815,6 +11936,39 @@ async fn test_modification_reverts(cx: &mut gpui::TestAppContext) { ); } +#[gpui::test] +async fn test_deleting_over_diff_hunk(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await; + let base_text = indoc! {r#" + one + + two + three + "#}; + + cx.set_diff_base(base_text); + cx.set_state("\nˇ\n"); + cx.executor().run_until_parked(); + cx.update_editor(|editor, cx| { + editor.expand_selected_diff_hunks(cx); + }); + cx.executor().run_until_parked(); + cx.update_editor(|editor, cx| { + editor.backspace(&Default::default(), cx); + }); + cx.run_until_parked(); + cx.assert_state_with_diff( + indoc! {r#" + + - two + - threeˇ + + + "#} + .to_string(), + ); +} + #[gpui::test] async fn test_deletion_reverts(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); @@ -12019,13 +12173,11 @@ async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) { (buffer_3.clone(), base_text_3), ] { let change_set = cx.new_model(|cx| { - BufferChangeSet::new_with_base_text( - diff_base.to_string(), - buffer.read(cx).text_snapshot(), - cx, - ) + BufferChangeSet::new_with_base_text(diff_base.to_string(), &buffer, cx) }); - editor.diff_map.add_change_set(change_set, cx) + editor + .buffer + .update(cx, |buffer, cx| buffer.add_change_set(change_set, cx)); } }); cx.executor().run_until_parked(); @@ -12385,7 +12537,10 @@ async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { +async fn test_toggle_selected_diff_hunks( + executor: BackgroundExecutor, + cx: &mut gpui::TestAppContext, +) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; @@ -12423,7 +12578,7 @@ async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::Test cx.update_editor(|editor, cx| { editor.go_to_next_hunk(&GoToHunk, cx); - editor.toggle_hunk_diff(&ToggleHunkDiff, cx); + editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, cx); }); executor.run_until_parked(); cx.assert_state_with_diff( @@ -12443,12 +12598,34 @@ async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::Test ); cx.update_editor(|editor, cx| { - for _ in 0..3 { + for _ in 0..2 { editor.go_to_next_hunk(&GoToHunk, cx); - editor.toggle_hunk_diff(&ToggleHunkDiff, cx); + editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, cx); } }); executor.run_until_parked(); + cx.assert_state_with_diff( + r#" + - use some::mod; + + ˇuse some::modified; + + + fn main() { + - println!("hello"); + + println!("hello there"); + + + println!("around the"); + println!("world"); + } + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| { + editor.go_to_next_hunk(&GoToHunk, cx); + editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, cx); + }); + executor.run_until_parked(); cx.assert_state_with_diff( r#" - use some::mod; @@ -12534,7 +12711,7 @@ async fn test_diff_base_change_with_expanded_diff_hunks( executor.run_until_parked(); cx.update_editor(|editor, cx| { - editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx); + editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx); }); executor.run_until_parked(); cx.assert_state_with_diff( @@ -12579,7 +12756,7 @@ async fn test_diff_base_change_with_expanded_diff_hunks( ); cx.update_editor(|editor, cx| { - editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx); + editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx); }); executor.run_until_parked(); cx.assert_state_with_diff( @@ -12602,170 +12779,6 @@ async fn test_diff_base_change_with_expanded_diff_hunks( ); } -#[gpui::test] -async fn test_fold_unfold_diff_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - - let diff_base = r#" - use some::mod1; - use some::mod2; - - const A: u32 = 42; - const B: u32 = 42; - const C: u32 = 42; - - fn main() { - println!("hello"); - - println!("world"); - } - - fn another() { - println!("another"); - } - - fn another2() { - println!("another2"); - } - "# - .unindent(); - - cx.set_state( - &r#" - «use some::mod2; - - const A: u32 = 42; - const C: u32 = 42; - - fn main() { - //println!("hello"); - - println!("world"); - // - //ˇ» - } - - fn another() { - println!("another"); - println!("another"); - } - - println!("another2"); - } - "# - .unindent(), - ); - - cx.set_diff_base(&diff_base); - executor.run_until_parked(); - - cx.update_editor(|editor, cx| { - editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx); - }); - executor.run_until_parked(); - - cx.assert_state_with_diff( - r#" - - use some::mod1; - «use some::mod2; - - const A: u32 = 42; - - const B: u32 = 42; - const C: u32 = 42; - - fn main() { - - println!("hello"); - + //println!("hello"); - - println!("world"); - + // - + //ˇ» - } - - fn another() { - println!("another"); - + println!("another"); - } - - - fn another2() { - println!("another2"); - } - "# - .unindent(), - ); - - // Fold across some of the diff hunks. They should no longer appear expanded. - cx.update_editor(|editor, cx| editor.fold_selected_ranges(&FoldSelectedRanges, cx)); - cx.executor().run_until_parked(); - - // Hunks are not shown if their position is within a fold - cx.assert_state_with_diff( - r#" - «use some::mod2; - - const A: u32 = 42; - const C: u32 = 42; - - fn main() { - //println!("hello"); - - println!("world"); - // - //ˇ» - } - - fn another() { - println!("another"); - + println!("another"); - } - - - fn another2() { - println!("another2"); - } - "# - .unindent(), - ); - - cx.update_editor(|editor, cx| { - editor.select_all(&SelectAll, cx); - editor.unfold_lines(&UnfoldLines, cx); - }); - cx.executor().run_until_parked(); - - // The deletions reappear when unfolding. - cx.assert_state_with_diff( - r#" - - use some::mod1; - «use some::mod2; - - const A: u32 = 42; - - const B: u32 = 42; - const C: u32 = 42; - - fn main() { - - println!("hello"); - + //println!("hello"); - - println!("world"); - + // - + // - } - - fn another() { - println!("another"); - + println!("another"); - } - - - fn another2() { - println!("another2"); - } - ˇ»"# - .unindent(), - ); -} - #[gpui::test] async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); @@ -12849,13 +12862,11 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) (buffer_3.clone(), file_3_old), ] { let change_set = cx.new_model(|cx| { - BufferChangeSet::new_with_base_text( - diff_base.to_string(), - buffer.read(cx).text_snapshot(), - cx, - ) + BufferChangeSet::new_with_base_text(diff_base.to_string(), &buffer, cx) }); - editor.diff_map.add_change_set(change_set, cx) + editor + .buffer + .update(cx, |buffer, cx| buffer.add_change_set(change_set, cx)); } }) .unwrap(); @@ -12895,7 +12906,7 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) cx.update_editor(|editor, cx| { editor.select_all(&SelectAll, cx); - editor.toggle_hunk_diff(&ToggleHunkDiff, cx); + editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, cx); }); cx.executor().run_until_parked(); @@ -12962,17 +12973,18 @@ async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext let editor = cx.add_window(|cx| Editor::new(EditorMode::Full, multi_buffer, None, true, cx)); editor .update(cx, |editor, cx| { - let buffer = buffer.read(cx).text_snapshot(); let change_set = cx - .new_model(|cx| BufferChangeSet::new_with_base_text(base.to_string(), buffer, cx)); - editor.diff_map.add_change_set(change_set, cx) + .new_model(|cx| BufferChangeSet::new_with_base_text(base.to_string(), &buffer, cx)); + editor + .buffer + .update(cx, |buffer, cx| buffer.add_change_set(change_set, cx)) }) .unwrap(); let mut cx = EditorTestContext::for_editor(editor, cx).await; cx.run_until_parked(); - cx.update_editor(|editor, cx| editor.expand_all_hunk_diffs(&Default::default(), cx)); + cx.update_editor(|editor, cx| editor.expand_all_diff_hunks(&Default::default(), cx)); cx.executor().run_until_parked(); cx.assert_state_with_diff( @@ -12981,8 +12993,6 @@ async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext - bbb + BBB - - ddd - - eee + EEE fff " @@ -13036,7 +13046,7 @@ async fn test_edits_around_expanded_insertion_hunks( executor.run_until_parked(); cx.update_editor(|editor, cx| { - editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx); + editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx); }); executor.run_until_parked(); @@ -13055,7 +13065,7 @@ async fn test_edits_around_expanded_insertion_hunks( println!("world"); } - "# + "# .unindent(), ); @@ -13078,7 +13088,7 @@ async fn test_edits_around_expanded_insertion_hunks( println!("world"); } - "# + "# .unindent(), ); @@ -13102,7 +13112,7 @@ async fn test_edits_around_expanded_insertion_hunks( println!("world"); } - "# + "# .unindent(), ); @@ -13127,7 +13137,7 @@ async fn test_edits_around_expanded_insertion_hunks( println!("world"); } - "# + "# .unindent(), ); @@ -13153,7 +13163,7 @@ async fn test_edits_around_expanded_insertion_hunks( println!("world"); } - "# + "# .unindent(), ); @@ -13164,21 +13174,63 @@ async fn test_edits_around_expanded_insertion_hunks( executor.run_until_parked(); cx.assert_state_with_diff( r#" - use some::mod1; - - use some::mod2; - - - - const A: u32 = 42; ˇ fn main() { println!("hello"); println!("world"); } - "# + "# .unindent(), ); } +#[gpui::test] +async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + cx.set_diff_base(indoc! { " + one + two + three + four + five + " + }); + cx.set_state(indoc! { " + one + ˇthree + five + "}); + cx.run_until_parked(); + cx.update_editor(|editor, cx| { + editor.toggle_selected_diff_hunks(&Default::default(), cx); + }); + cx.assert_state_with_diff( + indoc! { " + one + - two + ˇthree + - four + five + "} + .to_string(), + ); + cx.update_editor(|editor, cx| { + editor.toggle_selected_diff_hunks(&Default::default(), cx); + }); + + cx.assert_state_with_diff( + indoc! { " + one + ˇthree + five + "} + .to_string(), + ); +} + #[gpui::test] async fn test_edits_around_expanded_deletion_hunks( executor: BackgroundExecutor, @@ -13227,7 +13279,7 @@ async fn test_edits_around_expanded_deletion_hunks( executor.run_until_parked(); cx.update_editor(|editor, cx| { - editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx); + editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx); }); executor.run_until_parked(); @@ -13246,7 +13298,7 @@ async fn test_edits_around_expanded_deletion_hunks( println!("world"); } - "# + "# .unindent(), ); @@ -13269,7 +13321,7 @@ async fn test_edits_around_expanded_deletion_hunks( println!("world"); } - "# + "# .unindent(), ); @@ -13292,7 +13344,7 @@ async fn test_edits_around_expanded_deletion_hunks( println!("world"); } - "# + "# .unindent(), ); @@ -13316,6 +13368,71 @@ async fn test_edits_around_expanded_deletion_hunks( println!("world"); } + "# + .unindent(), + ); +} + +#[gpui::test] +async fn test_backspace_after_deletion_hunk( + executor: BackgroundExecutor, + cx: &mut gpui::TestAppContext, +) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let base_text = r#" + one + two + three + four + five + "# + .unindent(); + executor.run_until_parked(); + cx.set_state( + &r#" + one + two + fˇour + five + "# + .unindent(), + ); + + cx.set_diff_base(&base_text); + executor.run_until_parked(); + + cx.update_editor(|editor, cx| { + editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx); + }); + executor.run_until_parked(); + + cx.assert_state_with_diff( + r#" + one + two + - three + fˇour + five + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| { + editor.backspace(&Backspace, cx); + editor.backspace(&Backspace, cx); + }); + executor.run_until_parked(); + cx.assert_state_with_diff( + r#" + one + two + - threeˇ + - four + + our + five "# .unindent(), ); @@ -13369,7 +13486,7 @@ async fn test_edit_after_expanded_modification_hunk( cx.set_diff_base(&diff_base); executor.run_until_parked(); cx.update_editor(|editor, cx| { - editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx); + editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx); }); executor.run_until_parked(); @@ -13478,22 +13595,14 @@ fn assert_indent_guides( ); } - let expected: Vec<_> = expected - .into_iter() - .map(|guide| MultiBufferIndentGuide { - multibuffer_row_range: MultiBufferRow(guide.start_row)..MultiBufferRow(guide.end_row), - buffer: guide, - }) - .collect(); - assert_eq!(indent_guides, expected, "Indent guides do not match"); } fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide { IndentGuide { buffer_id, - start_row, - end_row, + start_row: MultiBufferRow(start_row), + end_row: MultiBufferRow(end_row), depth, tab_size: 4, settings: IndentGuideSettings { @@ -13945,6 +14054,105 @@ async fn test_active_indent_guide_non_matching_indent(cx: &mut gpui::TestAppCont ); } +#[gpui::test] +async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + let text = indoc! { + " + impl A { + fn b() { + 0; + 3; + 5; + 6; + 7; + } + } + " + }; + let base_text = indoc! { + " + impl A { + fn b() { + 0; + 1; + 2; + 3; + 4; + } + fn c() { + 5; + 6; + 7; + } + } + " + }; + + cx.update_editor(|editor, cx| { + editor.set_text(text, cx); + + editor.buffer().update(cx, |multibuffer, cx| { + let buffer = multibuffer.as_singleton().unwrap(); + let change_set = cx.new_model(|cx| { + let mut change_set = BufferChangeSet::new(&buffer, cx); + change_set.recalculate_diff_sync( + base_text.into(), + buffer.read(cx).text_snapshot(), + true, + cx, + ); + change_set + }); + + multibuffer.set_all_diff_hunks_expanded(cx); + multibuffer.add_change_set(change_set, cx); + + buffer.read(cx).remote_id() + }) + }); + + cx.assert_state_with_diff( + indoc! { " + impl A { + fn b() { + 0; + - 1; + - 2; + 3; + - 4; + - } + - fn c() { + 5; + 6; + 7; + } + } + ˇ" + } + .to_string(), + ); + + let mut actual_guides = cx.update_editor(|editor, cx| { + editor + .snapshot(cx) + .buffer_snapshot + .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx) + .map(|guide| (guide.start_row..=guide.end_row, guide.depth)) + .collect::>() + }); + actual_guides.sort_by_key(|item| (*item.0.start(), item.1)); + assert_eq!( + actual_guides, + vec![ + (MultiBufferRow(1)..=MultiBufferRow(12), 0), + (MultiBufferRow(2)..=MultiBufferRow(6), 1), + (MultiBufferRow(9)..=MultiBufferRow(11), 1), + ] + ); +} + #[gpui::test] fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -15229,7 +15437,7 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC #[track_caller] fn assert_hunk_revert( not_reverted_text_with_selections: &str, - expected_not_reverted_hunk_statuses: Vec, + expected_hunk_statuses_before: Vec, expected_reverted_text_with_selections: &str, base_text: &str, cx: &mut EditorLspTestContext, @@ -15238,12 +15446,12 @@ fn assert_hunk_revert( cx.set_diff_base(base_text); cx.executor().run_until_parked(); - let reverted_hunk_statuses = cx.update_editor(|editor, cx| { + let actual_hunk_statuses_before = cx.update_editor(|editor, cx| { let snapshot = editor.snapshot(cx); let reverted_hunk_statuses = snapshot - .diff_map - .diff_hunks_in_range(0..snapshot.buffer_snapshot.len(), &snapshot.buffer_snapshot) - .map(|hunk| hunk_status(&hunk)) + .buffer_snapshot + .diff_hunks_in_range(0..snapshot.buffer_snapshot.len()) + .map(|hunk| hunk.status()) .collect::>(); editor.revert_selected_hunks(&RevertSelectedHunks, cx); @@ -15251,5 +15459,5 @@ fn assert_hunk_revert( }); cx.executor().run_until_parked(); cx.assert_editor_state(expected_reverted_text_with_selections); - assert_eq!(reverted_hunk_statuses, expected_not_reverted_hunk_statuses); + assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before); } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 63cc1a254d..d582b9e8a6 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -12,19 +12,17 @@ use crate::{ hover_popover::{ self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, }, - hunk_diff::{diff_hunk_to_display, DisplayDiffHunk}, - hunk_status, items::BufferSearchHighlights, mouse_context_menu::{self, MenuPosition, MouseContextMenu}, scroll::{axis_pair, scroll_amount::ScrollAmount, AxisPair}, BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode, - EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, - HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, InlineCompletion, JumpData, - LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, - Selection, SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, CURSORS_VISIBLE_FOR, - FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, - MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, + EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GoToHunk, + GoToPrevHunk, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, + InlineCompletion, JumpData, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, + RevertSelectedHunks, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, + StickyHeaderExcerpt, ToPoint, ToggleFold, CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, + GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, }; use client::ParticipantIndex; use collections::{BTreeMap, HashMap, HashSet}; @@ -46,12 +44,12 @@ use language::{ IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings, ShowWhitespaceSetting, }, - ChunkRendererContext, DiagnosticEntry, + ChunkRendererContext, }; use lsp::DiagnosticSeverity; use multi_buffer::{ - Anchor, AnchorRangeExt, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, - MultiBufferRow, ToOffset, + Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow, + RowInfo, ToOffset, }; use project::project_settings::{GitGutterSetting, ProjectSettings}; use settings::Settings; @@ -70,12 +68,27 @@ use sum_tree::Bias; use text::BufferId; use theme::{ActiveTheme, Appearance, PlayerColor}; use ui::{ - prelude::*, ButtonLike, ButtonStyle, ContextMenu, KeyBinding, Tooltip, POPOVER_Y_PADDING, + h_flex, prelude::*, ButtonLike, ButtonStyle, ContextMenu, IconButtonShape, KeyBinding, Tooltip, + POPOVER_Y_PADDING, }; use unicode_segmentation::UnicodeSegmentation; use util::{RangeExt, ResultExt}; use workspace::{item::Item, notifications::NotifyTaskExt, Workspace}; +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DisplayDiffHunk { + Folded { + display_row: DisplayRow, + }, + + Unfolded { + diff_base_byte_range: Range, + display_row_range: Range, + multi_buffer_range: Range, + status: DiffHunkStatus, + }, +} + struct SelectionLayout { head: DisplayPoint, cursor_shape: CursorShape, @@ -381,8 +394,8 @@ impl EditorElement { register_action(view, cx, Editor::copy_file_location); register_action(view, cx, Editor::toggle_git_blame); register_action(view, cx, Editor::toggle_git_blame_inline); - register_action(view, cx, Editor::toggle_hunk_diff); - register_action(view, cx, Editor::expand_all_hunk_diffs); + register_action(view, cx, Editor::toggle_selected_diff_hunks); + register_action(view, cx, Editor::expand_all_diff_hunks); register_action(view, cx, |editor, action, cx| { if let Some(task) = editor.format(action, cx) { task.detach_and_notify_err(cx); @@ -509,7 +522,7 @@ impl EditorElement { fn mouse_left_down( editor: &mut Editor, event: &MouseDownEvent, - hovered_hunk: Option, + hovered_hunk: Option>, position_map: &PositionMap, text_hitbox: &Hitbox, gutter_hitbox: &Hitbox, @@ -524,7 +537,7 @@ impl EditorElement { let mut modifiers = event.modifiers; if let Some(hovered_hunk) = hovered_hunk { - editor.toggle_hovered_hunk(&hovered_hunk, cx); + editor.toggle_diff_hunks_in_ranges(vec![hovered_hunk], cx); cx.notify(); return; } else if gutter_hitbox.is_hovered(cx) { @@ -1252,7 +1265,7 @@ impl EditorElement { let editor = self.editor.read(cx); let is_singleton = editor.is_singleton(cx); // Git - (is_singleton && scrollbar_settings.git_diff && !snapshot.diff_map.is_empty()) + (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_diff_hunks()) || // Buffer Search Results (is_singleton && scrollbar_settings.search_results && editor.has_background_highlights::()) @@ -1491,108 +1504,77 @@ impl EditorElement { // Folds contained in a hunk are ignored apart from shrinking visual size // If a fold contains any hunks then that fold line is marked as modified - fn layout_gutter_git_hunks( + fn layout_gutter_diff_hunks( &self, line_height: Pixels, gutter_hitbox: &Hitbox, display_rows: Range, - anchor_range: Range, snapshot: &EditorSnapshot, cx: &mut WindowContext, ) -> Vec<(DisplayDiffHunk, Option)> { - let buffer_snapshot = &snapshot.buffer_snapshot; let buffer_start = DisplayPoint::new(display_rows.start, 0).to_point(snapshot); let buffer_end = DisplayPoint::new(display_rows.end, 0).to_point(snapshot); + let mut display_hunks = Vec::<(DisplayDiffHunk, Option)>::new(); + let folded_buffers = self.editor.read(cx).folded_buffers(cx); + + for hunk in snapshot + .buffer_snapshot + .diff_hunks_in_range(buffer_start..buffer_end) + { + if folded_buffers.contains(&hunk.buffer_id) { + continue; + } + + let hunk_start_point = Point::new(hunk.row_range.start.0, 0); + let hunk_end_point = Point::new(hunk.row_range.end.0, 0); + + let hunk_display_start = snapshot.point_to_display_point(hunk_start_point, Bias::Left); + let hunk_display_end = snapshot.point_to_display_point(hunk_end_point, Bias::Right); + + let display_hunk = if hunk_display_start.column() != 0 || hunk_display_end.column() != 0 + { + DisplayDiffHunk::Folded { + display_row: hunk_display_start.row(), + } + } else { + DisplayDiffHunk::Unfolded { + status: hunk.status(), + diff_base_byte_range: hunk.diff_base_byte_range, + display_row_range: hunk_display_start.row()..hunk_display_end.row(), + multi_buffer_range: Anchor::range_in_buffer( + hunk.excerpt_id, + hunk.buffer_id, + hunk.buffer_range, + ), + } + }; + + display_hunks.push((display_hunk, None)); + } + let git_gutter_setting = ProjectSettings::get_global(cx) .git .git_gutter .unwrap_or_default(); - - self.editor.update(cx, |editor, cx| { - let expanded_hunks = &editor.diff_map.hunks; - let expanded_hunks_start_ix = expanded_hunks - .binary_search_by(|hunk| { - hunk.hunk_range - .end - .cmp(&anchor_range.start, &buffer_snapshot) - .then(Ordering::Less) - }) - .unwrap_err(); - let mut expanded_hunks = expanded_hunks[expanded_hunks_start_ix..].iter().peekable(); - - let mut display_hunks: Vec<(DisplayDiffHunk, Option)> = editor - .diff_map - .snapshot - .diff_hunks_in_range(buffer_start..buffer_end, &buffer_snapshot) - .filter_map(|hunk| { - let display_hunk = diff_hunk_to_display(&hunk, snapshot); - - if let DisplayDiffHunk::Unfolded { - multi_buffer_range, - status, - .. - } = &display_hunk - { - let mut is_expanded = false; - while let Some(expanded_hunk) = expanded_hunks.peek() { - match expanded_hunk - .hunk_range - .start - .cmp(&multi_buffer_range.start, &buffer_snapshot) - { - Ordering::Less => { - expanded_hunks.next(); - } - Ordering::Equal => { - is_expanded = true; - break; - } - Ordering::Greater => { - break; - } - } - } - match status { - DiffHunkStatus::Added => {} - DiffHunkStatus::Modified => {} - DiffHunkStatus::Removed => { - if is_expanded { - return None; - } - } - } - } - - Some(display_hunk) - }) - .dedup() - .map(|hunk| (hunk, None)) - .collect(); - - if let GitGutterSetting::TrackedFiles = git_gutter_setting { - for (hunk, hitbox) in &mut display_hunks { - if let DisplayDiffHunk::Unfolded { .. } = hunk { - let hunk_bounds = Self::diff_hunk_bounds( - snapshot, - line_height, - gutter_hitbox.bounds, - &hunk, - ); - *hitbox = Some(cx.insert_hitbox(hunk_bounds, true)); - }; + if let GitGutterSetting::TrackedFiles = git_gutter_setting { + for (hunk, hitbox) in &mut display_hunks { + if matches!(hunk, DisplayDiffHunk::Unfolded { .. }) { + let hunk_bounds = + Self::diff_hunk_bounds(snapshot, line_height, gutter_hitbox.bounds, hunk); + *hitbox = Some(cx.insert_hitbox(hunk_bounds, true)); } } + } - display_hunks - }) + display_hunks } #[allow(clippy::too_many_arguments)] fn layout_inline_blame( &self, display_row: DisplayRow, - display_snapshot: &DisplaySnapshot, + row_info: &RowInfo, line_layout: &LineWithInvisibles, crease_trailer: Option<&CreaseTrailerLayout>, em_width: Pixels, @@ -1615,9 +1597,6 @@ impl EditorElement { .as_ref() .map(|(w, _)| w.clone()); - let display_point = DisplayPoint::new(display_row, 0); - let buffer_row = MultiBufferRow(display_point.to_point(display_snapshot).row); - let editor = self.editor.read(cx); let blame = editor.blame.clone()?; let padding = { @@ -1641,7 +1620,7 @@ impl EditorElement { let blame_entry = blame .update(cx, |blame, cx| { - blame.blame_for_rows([Some(buffer_row)], cx).next() + blame.blame_for_rows(&[*row_info], cx).next() }) .flatten()?; @@ -1680,7 +1659,7 @@ impl EditorElement { #[allow(clippy::too_many_arguments)] fn layout_blame_entries( &self, - buffer_rows: impl Iterator>, + buffer_rows: &[RowInfo], em_width: Pixels, scroll_position: gpui::Point, line_height: Pixels, @@ -1776,7 +1755,7 @@ impl EditorElement { let start_x = content_origin.x + total_width - scroll_pixel_position.x; if start_x >= text_origin.x { let (offset_y, length) = Self::calculate_indent_guide_bounds( - indent_guide.multibuffer_row_range.clone(), + indent_guide.start_row..indent_guide.end_row, line_height, snapshot, ); @@ -1910,7 +1889,7 @@ impl EditorElement { .buffer_snapshot .buffer_line_for_row(multibuffer_row) .map(|(buffer_snapshot, _)| buffer_snapshot.remote_id()) - .map(|buffer_id| editor.buffer_folded(buffer_id, cx)) + .map(|buffer_id| editor.is_buffer_folded(buffer_id, cx)) .unwrap_or(false); if buffer_folded { return None; @@ -2017,7 +1996,7 @@ impl EditorElement { let end = rows.end.max(relative_to); let buffer_rows = snapshot - .buffer_rows(start) + .row_infos(start) .take(1 + end.minus(start) as usize) .collect::>(); @@ -2025,7 +2004,7 @@ impl EditorElement { let mut delta = 1; let mut i = head_idx + 1; while i < buffer_rows.len() as u32 { - if buffer_rows[i as usize].is_some() { + if buffer_rows[i as usize].buffer_row.is_some() { if rows.contains(&DisplayRow(i + start.0)) { relative_rows.insert(DisplayRow(i + start.0), delta); } @@ -2035,13 +2014,13 @@ impl EditorElement { } delta = 1; i = head_idx.min(buffer_rows.len() as u32 - 1); - while i > 0 && buffer_rows[i as usize].is_none() { + while i > 0 && buffer_rows[i as usize].buffer_row.is_none() { i -= 1; } while i > 0 { i -= 1; - if buffer_rows[i as usize].is_some() { + if buffer_rows[i as usize].buffer_row.is_some() { if rows.contains(&DisplayRow(i + start.0)) { relative_rows.insert(DisplayRow(i + start.0), delta); } @@ -2060,7 +2039,7 @@ impl EditorElement { line_height: Pixels, scroll_position: gpui::Point, rows: Range, - buffer_rows: impl Iterator>, + buffer_rows: &[RowInfo], newest_selection_head: Option, snapshot: &EditorSnapshot, cx: &mut WindowContext, @@ -2100,15 +2079,17 @@ impl EditorElement { let line_numbers = buffer_rows .into_iter() .enumerate() - .flat_map(|(ix, buffer_row)| { - let buffer_row = buffer_row?; - line_number.clear(); + .flat_map(|(ix, row_info)| { let display_row = DisplayRow(rows.start.0 + ix as u32); - let non_relative_number = buffer_row.0 + 1; + line_number.clear(); + let non_relative_number = row_info.buffer_row? + 1; let number = relative_rows .get(&display_row) .unwrap_or(&non_relative_number); write!(&mut line_number, "{number}").unwrap(); + if row_info.diff_status == Some(DiffHunkStatus::Removed) { + return None; + } let color = cx.theme().colors().editor_line_number; let shaped_line = self @@ -2152,7 +2133,7 @@ impl EditorElement { fn layout_crease_toggles( &self, rows: Range, - buffer_rows: impl IntoIterator>, + row_infos: &[RowInfo], active_rows: &BTreeMap, snapshot: &EditorSnapshot, cx: &mut WindowContext, @@ -2161,22 +2142,15 @@ impl EditorElement { && snapshot.mode == EditorMode::Full && self.editor.read(cx).is_singleton(cx); if include_fold_statuses { - buffer_rows + row_infos .into_iter() .enumerate() - .map(|(ix, row)| { - if let Some(multibuffer_row) = row { - let display_row = DisplayRow(rows.start.0 + ix as u32); - let active = active_rows.contains_key(&display_row); - snapshot.render_crease_toggle( - multibuffer_row, - active, - self.editor.clone(), - cx, - ) - } else { - None - } + .map(|(ix, info)| { + let row = info.multibuffer_row?; + let display_row = DisplayRow(rows.start.0 + ix as u32); + let active = active_rows.contains_key(&display_row); + + snapshot.render_crease_toggle(row, active, self.editor.clone(), cx) }) .collect() } else { @@ -2186,15 +2160,15 @@ impl EditorElement { fn layout_crease_trailers( &self, - buffer_rows: impl IntoIterator>, + buffer_rows: impl IntoIterator, snapshot: &EditorSnapshot, cx: &mut WindowContext, ) -> Vec> { buffer_rows .into_iter() - .map(|row| { - if let Some(multibuffer_row) = row { - snapshot.render_crease_trailer(multibuffer_row, cx) + .map(|row_info| { + if let Some(row) = row_info.multibuffer_row { + snapshot.render_crease_trailer(row, cx) } else { None } @@ -3687,6 +3661,76 @@ impl EditorElement { } } + #[allow(clippy::too_many_arguments)] + fn layout_diff_hunk_controls( + &self, + row_range: Range, + row_infos: &[RowInfo], + text_hitbox: &Hitbox, + position_map: &PositionMap, + newest_cursor_position: Option, + line_height: Pixels, + scroll_pixel_position: gpui::Point, + display_hunks: &[(DisplayDiffHunk, Option)], + editor: View, + cx: &mut WindowContext, + ) -> Vec { + let point_for_position = + position_map.point_for_position(text_hitbox.bounds, cx.mouse_position()); + + let mut controls = vec![]; + + let active_positions = [ + Some(point_for_position.previous_valid), + newest_cursor_position, + ]; + + for (hunk, _) in display_hunks { + if let DisplayDiffHunk::Unfolded { + display_row_range, + multi_buffer_range, + status, + .. + } = &hunk + { + if display_row_range.start < row_range.start + || display_row_range.start >= row_range.end + { + continue; + } + let row_ix = (display_row_range.start - row_range.start).0 as usize; + if row_infos[row_ix].diff_status.is_none() { + continue; + } + if row_infos[row_ix].diff_status == Some(DiffHunkStatus::Added) + && *status != DiffHunkStatus::Added + { + continue; + } + if active_positions + .iter() + .any(|p| p.map_or(false, |p| display_row_range.contains(&p.row()))) + { + let y = display_row_range.start.as_f32() * line_height + + text_hitbox.bounds.top() + - scroll_pixel_position.y; + let x = text_hitbox.bounds.right() - px(100.); + + let mut element = + diff_hunk_controls(multi_buffer_range.clone(), line_height, &editor, cx); + element.prepaint_as_root( + gpui::Point::new(x, y), + size(px(100.0), line_height).into(), + cx, + ); + controls.push(element); + } + } + } + + controls + } + #[allow(clippy::too_many_arguments)] fn layout_signature_help( &self, @@ -4047,31 +4091,38 @@ impl EditorElement { Corners::all(px(0.)), )) } - DisplayDiffHunk::Unfolded { status, .. } => { - hitbox.as_ref().map(|hunk_hitbox| match status { - DiffHunkStatus::Added => ( - hunk_hitbox.bounds, - cx.theme().status().created, - Corners::all(px(0.)), - ), - DiffHunkStatus::Modified => ( - hunk_hitbox.bounds, - cx.theme().status().modified, - Corners::all(px(0.)), - ), - DiffHunkStatus::Removed => ( - Bounds::new( - point( - hunk_hitbox.origin.x - hunk_hitbox.size.width, - hunk_hitbox.origin.y, - ), - size(hunk_hitbox.size.width * px(2.), hunk_hitbox.size.height), + DisplayDiffHunk::Unfolded { + status, + display_row_range, + .. + } => hitbox.as_ref().map(|hunk_hitbox| match status { + DiffHunkStatus::Added => ( + hunk_hitbox.bounds, + cx.theme().status().created, + Corners::all(px(0.)), + ), + DiffHunkStatus::Modified => ( + hunk_hitbox.bounds, + cx.theme().status().modified, + Corners::all(px(0.)), + ), + DiffHunkStatus::Removed if !display_row_range.is_empty() => ( + hunk_hitbox.bounds, + cx.theme().status().deleted, + Corners::all(px(0.)), + ), + DiffHunkStatus::Removed => ( + Bounds::new( + point( + hunk_hitbox.origin.x - hunk_hitbox.size.width, + hunk_hitbox.origin.y, ), - cx.theme().status().deleted, - Corners::all(1. * line_height), + size(hunk_hitbox.size.width * px(2.), hunk_hitbox.size.height), ), - }) - } + cx.theme().status().deleted, + Corners::all(1. * line_height), + ), + }), }; if let Some((hunk_bounds, background_color, corner_radii)) = hunk_to_paint { @@ -4110,8 +4161,19 @@ impl EditorElement { display_row_range, status, .. - } => match status { - DiffHunkStatus::Added | DiffHunkStatus::Modified => { + } => { + if *status == DiffHunkStatus::Removed && display_row_range.is_empty() { + let row = display_row_range.start; + + let offset = line_height / 2.; + let start_y = row.as_f32() * line_height - offset - scroll_top; + let end_y = start_y + line_height; + + let width = (0.35 * line_height).floor(); + let highlight_origin = gutter_bounds.origin + point(px(0.), start_y); + let highlight_size = size(width, end_y - start_y); + Bounds::new(highlight_origin, highlight_size) + } else { let start_row = display_row_range.start; let end_row = display_row_range.end; // If we're in a multibuffer, row range span might include an @@ -4139,19 +4201,7 @@ impl EditorElement { let highlight_size = size(width, end_y - start_y); Bounds::new(highlight_origin, highlight_size) } - DiffHunkStatus::Removed => { - let row = display_row_range.start; - - let offset = line_height / 2.; - let start_y = row.as_f32() * line_height - offset - scroll_top; - let end_y = start_y + line_height; - - let width = (0.35 * line_height).floor(); - let highlight_origin = gutter_bounds.origin + point(px(0.), start_y); - let highlight_size = size(width, end_y - start_y); - Bounds::new(highlight_origin, highlight_size) - } - }, + } } } @@ -4266,6 +4316,7 @@ impl EditorElement { self.paint_redactions(layout, cx); self.paint_cursors(layout, cx); self.paint_inline_blame(layout, cx); + self.paint_diff_hunk_controls(layout, cx); cx.with_element_namespace("crease_trailers", |cx| { for trailer in layout.crease_trailers.iter_mut().flatten() { trailer.element.paint(cx); @@ -4733,10 +4784,8 @@ impl EditorElement { let max_point = snapshot.display_snapshot.buffer_snapshot.max_point(); let mut marker_quads = Vec::new(); if scrollbar_settings.git_diff { - let marker_row_ranges = snapshot - .diff_map - .diff_hunks(&snapshot.buffer_snapshot) - .map(|hunk| { + let marker_row_ranges = + snapshot.buffer_snapshot.diff_hunks().map(|hunk| { let start_display_row = MultiBufferPoint::new(hunk.row_range.start.0, 0) .to_display_point(&snapshot.display_snapshot) @@ -4748,7 +4797,7 @@ impl EditorElement { if end_display_row != start_display_row { end_display_row.0 -= 1; } - let color = match hunk_status(&hunk) { + let color = match &hunk.status() { DiffHunkStatus::Added => theme.status().created, DiffHunkStatus::Modified => theme.status().modified, DiffHunkStatus::Removed => theme.status().deleted, @@ -4804,11 +4853,7 @@ impl EditorElement { if scrollbar_settings.diagnostics != ScrollbarDiagnostics::None { let diagnostics = snapshot .buffer_snapshot - .diagnostics_in_range(Point::zero()..max_point, false) - .map(|DiagnosticEntry { diagnostic, range }| DiagnosticEntry { - diagnostic, - range: range.to_point(&snapshot.buffer_snapshot), - }) + .diagnostics_in_range::<_, Point>(Point::zero()..max_point) // Don't show diagnostics the user doesn't care about .filter(|diagnostic| { match ( @@ -4948,6 +4993,12 @@ impl EditorElement { } } + fn paint_diff_hunk_controls(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) { + for mut diff_hunk_control in layout.diff_hunk_controls.drain(..) { + diff_hunk_control.paint(cx); + } + } + fn paint_blocks(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) { for mut block in layout.blocks.drain(..) { block.element.paint(cx); @@ -5033,12 +5084,7 @@ impl EditorElement { }); } - fn paint_mouse_listeners( - &mut self, - layout: &EditorLayout, - hovered_hunk: Option, - cx: &mut WindowContext, - ) { + fn paint_mouse_listeners(&mut self, layout: &EditorLayout, cx: &mut WindowContext) { self.paint_scroll_wheel_listener(layout, cx); cx.on_mouse_event({ @@ -5046,6 +5092,26 @@ impl EditorElement { let editor = self.editor.clone(); let text_hitbox = layout.text_hitbox.clone(); let gutter_hitbox = layout.gutter_hitbox.clone(); + let hovered_hunk = + layout + .display_hunks + .iter() + .find_map(|(hunk, hunk_hitbox)| match hunk { + DisplayDiffHunk::Folded { .. } => None, + DisplayDiffHunk::Unfolded { + multi_buffer_range, .. + } => { + if hunk_hitbox + .as_ref() + .map(|hitbox| hitbox.is_hovered(cx)) + .unwrap_or(false) + { + Some(multi_buffer_range.clone()) + } else { + None + } + } + }); let line_numbers = layout.line_numbers.clone(); move |event: &MouseDownEvent, phase, cx| { @@ -6232,12 +6298,15 @@ impl Element for EditorElement { ); let end_row = DisplayRow(end_row); - let buffer_rows = snapshot - .buffer_rows(start_row) + let row_infos = snapshot + .row_infos(start_row) .take((start_row..end_row).len()) - .collect::>(); - let is_row_soft_wrapped = - |row| buffer_rows.get(row).copied().flatten().is_none(); + .collect::>(); + let is_row_soft_wrapped = |row: usize| { + row_infos + .get(row) + .map_or(true, |info| info.buffer_row.is_none()) + }; let start_anchor = if start_row == Default::default() { Anchor::min() @@ -6254,9 +6323,21 @@ impl Element for EditorElement { ) }; - let highlighted_rows = self + let mut highlighted_rows = self .editor .update(cx, |editor, cx| editor.highlighted_display_rows(cx)); + + for (ix, row_info) in row_infos.iter().enumerate() { + let color = match row_info.diff_status { + Some(DiffHunkStatus::Added) => style.status.created_background, + Some(DiffHunkStatus::Removed) => style.status.deleted_background, + _ => continue, + }; + highlighted_rows + .entry(start_row + DisplayRow(ix as u32)) + .or_insert(color); + } + let highlighted_ranges = self.editor.read(cx).background_highlights_in_range( start_anchor..end_anchor, &snapshot.display_snapshot, @@ -6288,7 +6369,7 @@ impl Element for EditorElement { for selection in all_selections { for buffer_id in snapshot .buffer_snapshot - .buffer_ids_in_selected_rows(selection) + .buffer_ids_for_range(selection.range()) { if selected_buffer_ids.last() != Some(&buffer_id) { selected_buffer_ids.push(buffer_id); @@ -6323,7 +6404,7 @@ impl Element for EditorElement { line_height, scroll_position, start_row..end_row, - buffer_rows.iter().copied(), + &row_infos, newest_selection_head, &snapshot, cx, @@ -6332,21 +6413,20 @@ impl Element for EditorElement { let mut crease_toggles = cx.with_element_namespace("crease_toggles", |cx| { self.layout_crease_toggles( start_row..end_row, - buffer_rows.iter().copied(), + &row_infos, &active_rows, &snapshot, cx, ) }); let crease_trailers = cx.with_element_namespace("crease_trailers", |cx| { - self.layout_crease_trailers(buffer_rows.iter().copied(), &snapshot, cx) + self.layout_crease_trailers(row_infos.iter().copied(), &snapshot, cx) }); - let display_hunks = self.layout_gutter_git_hunks( + let display_hunks = self.layout_gutter_diff_hunks( line_height, &gutter_hitbox, start_row..end_row, - start_anchor..end_anchor, &snapshot, cx, ); @@ -6504,11 +6584,12 @@ impl Element for EditorElement { let display_row = newest_selection_head.row(); if (start_row..end_row).contains(&display_row) { let line_ix = display_row.minus(start_row) as usize; + let row_info = &row_infos[line_ix]; let line_layout = &line_layouts[line_ix]; let crease_trailer_layout = crease_trailers[line_ix].as_ref(); inline_blame = self.layout_inline_blame( display_row, - &snapshot.display_snapshot, + row_info, line_layout, crease_trailer_layout, em_width, @@ -6521,7 +6602,7 @@ impl Element for EditorElement { } let blamed_display_rows = self.layout_blame_entries( - buffer_rows.into_iter(), + &row_infos, em_width, scroll_position, line_height, @@ -6612,22 +6693,6 @@ impl Element for EditorElement { let gutter_settings = EditorSettings::get_global(cx).gutter; - let expanded_add_hunks_by_rows = self.editor.update(cx, |editor, _| { - editor - .diff_map - .hunks(false) - .filter(|hunk| hunk.status == DiffHunkStatus::Added) - .map(|expanded_hunk| { - let start_row = expanded_hunk - .hunk_range - .start - .to_display_point(&snapshot) - .row(); - (start_row, expanded_hunk.clone()) - }) - .collect::>() - }); - let rows_with_hunk_bounds = display_hunks .iter() .filter_map(|(hunk, hitbox)| Some((hunk, hitbox.as_ref()?.bounds))) @@ -6670,38 +6735,32 @@ impl Element for EditorElement { if show_code_actions { let newest_selection_point = newest_selection_head.to_point(&snapshot.display_snapshot); - let newest_selection_display_row = - newest_selection_point.to_display_point(&snapshot).row(); - if !expanded_add_hunks_by_rows - .contains_key(&newest_selection_display_row) + if !snapshot + .is_line_folded(MultiBufferRow(newest_selection_point.row)) { - if !snapshot - .is_line_folded(MultiBufferRow(newest_selection_point.row)) - { - let buffer = snapshot.buffer_snapshot.buffer_line_for_row( - MultiBufferRow(newest_selection_point.row), - ); - if let Some((buffer, range)) = buffer { - let buffer_id = buffer.remote_id(); - let row = range.start.row; - let has_test_indicator = self - .editor - .read(cx) - .tasks - .contains_key(&(buffer_id, row)); + let buffer = snapshot.buffer_snapshot.buffer_line_for_row( + MultiBufferRow(newest_selection_point.row), + ); + if let Some((buffer, range)) = buffer { + let buffer_id = buffer.remote_id(); + let row = range.start.row; + let has_test_indicator = self + .editor + .read(cx) + .tasks + .contains_key(&(buffer_id, row)); - if !has_test_indicator { - code_actions_indicator = self - .layout_code_actions_indicator( - line_height, - newest_selection_head, - scroll_pixel_position, - &gutter_dimensions, - &gutter_hitbox, - &rows_with_hunk_bounds, - cx, - ); - } + if !has_test_indicator { + code_actions_indicator = self + .layout_code_actions_indicator( + line_height, + newest_selection_head, + scroll_pixel_position, + &gutter_dimensions, + &gutter_hitbox, + &rows_with_hunk_bounds, + cx, + ); } } } @@ -6816,18 +6875,35 @@ impl Element for EditorElement { ) .unwrap(); + let mode = snapshot.mode; + + let position_map = Rc::new(PositionMap { + size: bounds.size, + scroll_pixel_position, + scroll_max, + line_layouts, + line_height, + em_width, + em_advance, + snapshot, + }); + + let hunk_controls = self.layout_diff_hunk_controls( + start_row..end_row, + &row_infos, + &text_hitbox, + &position_map, + newest_selection_head, + line_height, + scroll_pixel_position, + &display_hunks, + self.editor.clone(), + cx, + ); + EditorLayout { - mode: snapshot.mode, - position_map: Rc::new(PositionMap { - size: bounds.size, - scroll_pixel_position, - scroll_max, - line_layouts, - line_height, - em_width, - em_advance, - snapshot, - }), + mode, + position_map, visible_display_row_range: start_row..end_row, wrap_guides, indent_guides, @@ -6851,6 +6927,7 @@ impl Element for EditorElement { visible_cursors, selections, inline_completion_popover, + diff_hunk_controls: hunk_controls, mouse_context_menu, test_indicators, code_actions_indicator, @@ -6889,37 +6966,11 @@ impl Element for EditorElement { line_height: Some(self.style.text.line_height), ..Default::default() }; - let hovered_hunk = layout - .display_hunks - .iter() - .find_map(|(hunk, hunk_hitbox)| match hunk { - DisplayDiffHunk::Folded { .. } => None, - DisplayDiffHunk::Unfolded { - diff_base_byte_range, - multi_buffer_range, - status, - .. - } => { - if hunk_hitbox - .as_ref() - .map(|hitbox| hitbox.is_hovered(cx)) - .unwrap_or(false) - { - Some(HoveredHunk { - status: *status, - multi_buffer_range: multi_buffer_range.clone(), - diff_base_byte_range: diff_base_byte_range.clone(), - }) - } else { - None - } - } - }); let rem_size = self.rem_size(cx); cx.with_rem_size(rem_size, |cx| { cx.with_text_style(Some(text_style), |cx| { cx.with_content_mask(Some(ContentMask { bounds }), |cx| { - self.paint_mouse_listeners(layout, hovered_hunk, cx); + self.paint_mouse_listeners(layout, cx); self.paint_background(layout, cx); self.paint_indent_guides(layout, cx); @@ -7048,6 +7099,7 @@ pub struct EditorLayout { code_actions_indicator: Option, test_indicators: Vec, crease_toggles: Vec>, + diff_hunk_controls: Vec, crease_trailers: Vec>, inline_completion_popover: Option, mouse_context_menu: Option, @@ -7709,7 +7761,12 @@ mod tests { line_height, gpui::Point::default(), DisplayRow(0)..DisplayRow(6), - (0..6).map(MultiBufferRow).map(Some), + &(0..6) + .map(|row| RowInfo { + buffer_row: Some(row), + ..Default::default() + }) + .collect::>(), Some(DisplayPoint::new(DisplayRow(0), 0)), &snapshot, cx, @@ -8174,3 +8231,93 @@ mod tests { .collect() } } + +fn diff_hunk_controls( + hunk_range: Range, + line_height: Pixels, + editor: &View, + cx: &mut WindowContext, +) -> AnyElement { + h_flex() + .h(line_height) + .mr_1() + .gap_1() + .px_1() + .pb_1() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .rounded_b_lg() + .bg(cx.theme().colors().editor_background) + .gap_1() + .child( + IconButton::new("next-hunk", IconName::ArrowDown) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + // .disabled(!has_multiple_hunks) + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |cx| Tooltip::for_action_in("Next Hunk", &GoToHunk, &focus_handle, cx) + }) + .on_click({ + let editor = editor.clone(); + move |_event, cx| { + editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + let position = hunk_range.end.to_point(&snapshot.buffer_snapshot); + editor.go_to_hunk_after_position(&snapshot, position, cx); + editor.expand_selected_diff_hunks(cx); + }); + } + }), + ) + .child( + IconButton::new("prev-hunk", IconName::ArrowUp) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + // .disabled(!has_multiple_hunks) + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |cx| { + Tooltip::for_action_in("Previous Hunk", &GoToPrevHunk, &focus_handle, cx) + } + }) + .on_click({ + let editor = editor.clone(); + move |_event, cx| { + editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + let point = hunk_range.start.to_point(&snapshot.buffer_snapshot); + editor.go_to_hunk_before_position(&snapshot, point, cx); + editor.expand_selected_diff_hunks(cx); + }); + } + }), + ) + .child( + IconButton::new("discard", IconName::Undo) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |cx| { + Tooltip::for_action_in( + "Discard Hunk", + &RevertSelectedHunks, + &focus_handle, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + move |_event, cx| { + editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + let point = hunk_range.start.to_point(&snapshot.buffer_snapshot); + editor.revert_hunks_in_ranges([point..point].into_iter(), cx); + }); + } + }), + ) + .into_any_element() +} diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index 51fa812623..774408a445 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -1,5 +1,3 @@ -use std::{sync::Arc, time::Duration}; - use anyhow::Result; use collections::HashMap; use git::{ @@ -9,9 +7,10 @@ use git::{ use gpui::{AppContext, Model, ModelContext, Subscription, Task}; use http_client::HttpClient; use language::{markdown, Bias, Buffer, BufferSnapshot, Edit, LanguageRegistry, ParsedMarkdown}; -use multi_buffer::MultiBufferRow; +use multi_buffer::RowInfo; use project::{Project, ProjectItem}; use smallvec::SmallVec; +use std::{sync::Arc, time::Duration}; use sum_tree::SumTree; use url::Url; @@ -194,15 +193,15 @@ impl GitBlame { pub fn blame_for_rows<'a>( &'a mut self, - rows: impl 'a + IntoIterator>, + rows: &'a [RowInfo], cx: &AppContext, ) -> impl 'a + Iterator> { self.sync(cx); let mut cursor = self.entries.cursor::(&()); - rows.into_iter().map(move |row| { - let row = row?; - cursor.seek_forward(&row.0, Bias::Right, &()); + rows.into_iter().map(move |info| { + let row = info.buffer_row?; + cursor.seek_forward(&row, Bias::Right, &()); cursor.item()?.blame.clone() }) } @@ -563,15 +562,38 @@ mod tests { use unindent::Unindent as _; use util::RandomCharIter; - macro_rules! assert_blame_rows { - ($blame:expr, $rows:expr, $expected:expr, $cx:expr) => { - assert_eq!( - $blame - .blame_for_rows($rows.map(MultiBufferRow).map(Some), $cx) - .collect::>(), - $expected - ); - }; + // macro_rules! assert_blame_rows { + // ($blame:expr, $rows:expr, $expected:expr, $cx:expr) => { + // assert_eq!( + // $blame + // .blame_for_rows($rows.map(MultiBufferRow).map(Some), $cx) + // .collect::>(), + // $expected + // ); + // }; + // } + + #[track_caller] + fn assert_blame_rows( + blame: &mut GitBlame, + rows: Range, + expected: Vec>, + cx: &mut ModelContext, + ) { + assert_eq!( + blame + .blame_for_rows( + &rows + .map(|row| RowInfo { + buffer_row: Some(row), + ..Default::default() + }) + .collect::>(), + cx + ) + .collect::>(), + expected + ); } fn init_test(cx: &mut gpui::TestAppContext) { @@ -634,7 +656,15 @@ mod tests { blame.update(cx, |blame, cx| { assert_eq!( blame - .blame_for_rows((0..1).map(MultiBufferRow).map(Some), cx) + .blame_for_rows( + &(0..1) + .map(|row| RowInfo { + buffer_row: Some(row), + ..Default::default() + }) + .collect::>(), + cx + ) .collect::>(), vec![None] ); @@ -698,7 +728,15 @@ mod tests { // All lines assert_eq!( blame - .blame_for_rows((0..8).map(MultiBufferRow).map(Some), cx) + .blame_for_rows( + &(0..8) + .map(|buffer_row| RowInfo { + buffer_row: Some(buffer_row), + ..Default::default() + }) + .collect::>(), + cx + ) .collect::>(), vec![ Some(blame_entry("1b1b1b", 0..1)), @@ -714,7 +752,15 @@ mod tests { // Subset of lines assert_eq!( blame - .blame_for_rows((1..4).map(MultiBufferRow).map(Some), cx) + .blame_for_rows( + &(1..4) + .map(|buffer_row| RowInfo { + buffer_row: Some(buffer_row), + ..Default::default() + }) + .collect::>(), + cx + ) .collect::>(), vec![ Some(blame_entry("0d0d0d", 1..2)), @@ -725,7 +771,17 @@ mod tests { // Subset of lines, with some not displayed assert_eq!( blame - .blame_for_rows(vec![Some(MultiBufferRow(1)), None, None], cx) + .blame_for_rows( + &[ + RowInfo { + buffer_row: Some(1), + ..Default::default() + }, + Default::default(), + Default::default(), + ], + cx + ) .collect::>(), vec![Some(blame_entry("0d0d0d", 1..2)), None, None] ); @@ -777,16 +833,16 @@ mod tests { git_blame.update(cx, |blame, cx| { // Sanity check before edits: make sure that we get the same blame entry for all // lines. - assert_blame_rows!( + assert_blame_rows( blame, - (0..4), + 0..4, vec![ Some(blame_entry("1b1b1b", 0..4)), Some(blame_entry("1b1b1b", 0..4)), Some(blame_entry("1b1b1b", 0..4)), Some(blame_entry("1b1b1b", 0..4)), ], - cx + cx, ); }); @@ -795,11 +851,11 @@ mod tests { buffer.edit([(Point::new(0, 0)..Point::new(0, 0), "X")], None, cx); }); git_blame.update(cx, |blame, cx| { - assert_blame_rows!( + assert_blame_rows( blame, - (0..2), + 0..2, vec![None, Some(blame_entry("1b1b1b", 0..4))], - cx + cx, ); }); // Modify a single line, in the middle of the line @@ -807,21 +863,21 @@ mod tests { buffer.edit([(Point::new(1, 2)..Point::new(1, 2), "X")], None, cx); }); git_blame.update(cx, |blame, cx| { - assert_blame_rows!( + assert_blame_rows( blame, - (1..4), + 1..4, vec![ None, Some(blame_entry("1b1b1b", 0..4)), - Some(blame_entry("1b1b1b", 0..4)) + Some(blame_entry("1b1b1b", 0..4)), ], - cx + cx, ); }); // Before we insert a newline at the end, sanity check: git_blame.update(cx, |blame, cx| { - assert_blame_rows!(blame, (3..4), vec![Some(blame_entry("1b1b1b", 0..4))], cx); + assert_blame_rows(blame, 3..4, vec![Some(blame_entry("1b1b1b", 0..4))], cx); }); // Insert a newline at the end buffer.update(cx, |buffer, cx| { @@ -829,17 +885,17 @@ mod tests { }); // Only the new line is marked as edited: git_blame.update(cx, |blame, cx| { - assert_blame_rows!( + assert_blame_rows( blame, - (3..5), + 3..5, vec![Some(blame_entry("1b1b1b", 0..4)), None], - cx + cx, ); }); // Before we insert a newline at the start, sanity check: git_blame.update(cx, |blame, cx| { - assert_blame_rows!(blame, (2..3), vec![Some(blame_entry("1b1b1b", 0..4)),], cx); + assert_blame_rows(blame, 2..3, vec![Some(blame_entry("1b1b1b", 0..4))], cx); }); // Usage example @@ -849,11 +905,11 @@ mod tests { }); // Only the new line is marked as edited: git_blame.update(cx, |blame, cx| { - assert_blame_rows!( + assert_blame_rows( blame, - (2..4), - vec![None, Some(blame_entry("1b1b1b", 0..4)),], - cx + 2..4, + vec![None, Some(blame_entry("1b1b1b", 0..4))], + cx, ); }); } diff --git a/crates/editor/src/git/project_diff.rs b/crates/editor/src/git/project_diff.rs index 9379b6242c..b067f44bbf 100644 --- a/crates/editor/src/git/project_diff.rs +++ b/crates/editor/src/git/project_diff.rs @@ -146,7 +146,7 @@ impl ProjectDiffEditor { let editor = cx.new_view(|cx| { let mut diff_display_editor = Editor::for_multibuffer(excerpts.clone(), Some(project.clone()), true, cx); - diff_display_editor.set_expand_all_diff_hunks(); + diff_display_editor.set_expand_all_diff_hunks(cx); diff_display_editor }); @@ -310,9 +310,11 @@ impl ProjectDiffEditor { .update(&mut cx, |project_diff_editor, cx| { project_diff_editor.update_excerpts(id, new_changes, new_entry_order, cx); project_diff_editor.editor.update(cx, |editor, cx| { - for change_set in change_sets { - editor.diff_map.add_change_set(change_set, cx) - } + editor.buffer.update(cx, |buffer, cx| { + for change_set in change_sets { + buffer.add_change_set(change_set, cx) + } + }); }); }) .ok(); @@ -1105,6 +1107,8 @@ mod tests { path::{Path, PathBuf}, }; + use crate::test::editor_test_context::assert_state_with_diff; + use super::*; // TODO finish @@ -1183,19 +1187,13 @@ mod tests { let change_set = cx.new_model(|cx| { BufferChangeSet::new_with_base_text( old_text.clone(), - file_a_editor - .buffer() - .read(cx) - .as_singleton() - .unwrap() - .read(cx) - .text_snapshot(), + &file_a_editor.buffer().read(cx).as_singleton().unwrap(), cx, ) }); - file_a_editor - .diff_map - .add_change_set(change_set.clone(), cx); + file_a_editor.buffer.update(cx, |buffer, cx| { + buffer.add_change_set(change_set.clone(), cx) + }); project.update(cx, |project, cx| { project.buffer_store().update(cx, |buffer_store, cx| { buffer_store.set_change_set( @@ -1225,15 +1223,17 @@ mod tests { cx.executor() .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); cx.run_until_parked(); + let editor = project_diff_editor.update(cx, |view, _| view.editor.clone()); - project_diff_editor.update(cx, |project_diff_editor, cx| { - assert_eq!( - // TODO assert it better: extract added text (based on the background changes) and deleted text (based on the deleted blocks added) - project_diff_editor.editor.read(cx).text(cx), - format!("{change}{old_text}"), - "Should have a new change shown in the beginning, and the old text shown as deleted text afterwards" - ); - }); + assert_state_with_diff( + &editor, + cx, + indoc::indoc! { + " + - This is file_a + + an edit after git addThis is file_aˇ", + }, + ); } fn init_test(cx: &mut gpui::TestAppContext) { diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 7050972e59..e923770ec2 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -265,12 +265,9 @@ fn show_hover( let local_diagnostic = snapshot .buffer_snapshot - .diagnostics_in_range(anchor..anchor, false) + .diagnostics_in_range::<_, usize>(anchor..anchor) // Find the entry with the most specific range - .min_by_key(|entry| { - let range = entry.range.to_offset(&snapshot.buffer_snapshot); - range.end - range.start - }); + .min_by_key(|entry| entry.range.len()); let diagnostic_popover = if let Some(local_diagnostic) = local_diagnostic { let text = match local_diagnostic.diagnostic.source { @@ -279,6 +276,15 @@ fn show_hover( } None => local_diagnostic.diagnostic.message.clone(), }; + let local_diagnostic = DiagnosticEntry { + diagnostic: local_diagnostic.diagnostic, + range: snapshot + .buffer_snapshot + .anchor_before(local_diagnostic.range.start) + ..snapshot + .buffer_snapshot + .anchor_after(local_diagnostic.range.end), + }; let mut border_color: Option = None; let mut background_color: Option = None; @@ -770,7 +776,7 @@ impl InfoPopover { #[derive(Debug, Clone)] pub struct DiagnosticPopover { - local_diagnostic: DiagnosticEntry, + pub(crate) local_diagnostic: DiagnosticEntry, parsed_content: Option>, border_color: Option, background_color: Option, @@ -823,10 +829,6 @@ impl DiagnosticPopover { diagnostic_div.into_any_element() } - - pub fn group_id(&self) -> usize { - self.local_diagnostic.diagnostic.group_id - } } #[cfg(test)] diff --git a/crates/editor/src/hunk_diff.rs b/crates/editor/src/hunk_diff.rs deleted file mode 100644 index 85bd964699..0000000000 --- a/crates/editor/src/hunk_diff.rs +++ /dev/null @@ -1,1505 +0,0 @@ -use collections::{HashMap, HashSet}; -use git::diff::DiffHunkStatus; -use gpui::{ - Action, AppContext, Corner, CursorStyle, Hsla, Model, MouseButton, Subscription, Task, View, -}; -use language::{Buffer, BufferId, Point}; -use multi_buffer::{ - Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow, - MultiBufferSnapshot, ToOffset, ToPoint, -}; -use project::buffer_store::BufferChangeSet; -use std::{ops::Range, sync::Arc}; -use sum_tree::TreeMap; -use text::OffsetRangeExt; -use ui::{ - prelude::*, ActiveTheme, ContextMenu, IconButtonShape, InteractiveElement, IntoElement, - ParentElement, PopoverMenu, Styled, Tooltip, ViewContext, VisualContext, -}; -use util::RangeExt; -use workspace::Item; - -use crate::{ - editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, ApplyAllDiffHunks, - ApplyDiffHunk, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, - DisplayRow, DisplaySnapshot, Editor, EditorElement, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk, - RevertFile, RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff, -}; - -#[derive(Debug, Clone)] -pub(super) struct HoveredHunk { - pub multi_buffer_range: Range, - pub status: DiffHunkStatus, - pub diff_base_byte_range: Range, -} - -#[derive(Default)] -pub(super) struct DiffMap { - pub(crate) hunks: Vec, - pub(crate) diff_bases: HashMap, - pub(crate) snapshot: DiffMapSnapshot, - hunk_update_tasks: HashMap, Task<()>>, - expand_all: bool, -} - -#[derive(Debug, Clone)] -pub(super) struct ExpandedHunk { - pub blocks: Vec, - pub hunk_range: Range, - pub diff_base_byte_range: Range, - pub status: DiffHunkStatus, - pub folded: bool, -} - -#[derive(Clone, Debug, Default)] -pub(crate) struct DiffMapSnapshot(TreeMap); - -pub(crate) struct DiffBaseState { - pub(crate) change_set: Model, - pub(crate) last_version: Option, - _subscription: Subscription, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum DisplayDiffHunk { - Folded { - display_row: DisplayRow, - }, - - Unfolded { - diff_base_byte_range: Range, - display_row_range: Range, - multi_buffer_range: Range, - status: DiffHunkStatus, - }, -} - -impl DiffMap { - pub fn snapshot(&self) -> DiffMapSnapshot { - self.snapshot.clone() - } - - pub fn add_change_set( - &mut self, - change_set: Model, - cx: &mut ViewContext, - ) { - let buffer_id = change_set.read(cx).buffer_id; - self.snapshot - .0 - .insert(buffer_id, change_set.read(cx).diff_to_buffer.clone()); - self.diff_bases.insert( - buffer_id, - DiffBaseState { - last_version: None, - _subscription: cx.observe(&change_set, move |editor, change_set, cx| { - editor - .diff_map - .snapshot - .0 - .insert(buffer_id, change_set.read(cx).diff_to_buffer.clone()); - Editor::sync_expanded_diff_hunks(&mut editor.diff_map, buffer_id, cx); - }), - change_set, - }, - ); - Editor::sync_expanded_diff_hunks(self, buffer_id, cx); - } - - pub fn hunks(&self, include_folded: bool) -> impl Iterator { - self.hunks - .iter() - .filter(move |hunk| include_folded || !hunk.folded) - } -} - -impl DiffMapSnapshot { - pub fn is_empty(&self) -> bool { - self.0.values().all(|diff| diff.is_empty()) - } - - pub fn diff_hunks<'a>( - &'a self, - buffer_snapshot: &'a MultiBufferSnapshot, - ) -> impl Iterator + 'a { - self.diff_hunks_in_range(0..buffer_snapshot.len(), buffer_snapshot) - } - - pub fn diff_hunks_in_range<'a, T: ToOffset>( - &'a self, - range: Range, - buffer_snapshot: &'a MultiBufferSnapshot, - ) -> impl Iterator + 'a { - let range = range.start.to_offset(buffer_snapshot)..range.end.to_offset(buffer_snapshot); - buffer_snapshot - .excerpts_for_range(range.clone()) - .filter_map(move |excerpt| { - let buffer = excerpt.buffer(); - let buffer_id = buffer.remote_id(); - let diff = self.0.get(&buffer_id)?; - let buffer_range = excerpt.map_range_to_buffer(range.clone()); - let buffer_range = - buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end); - Some( - diff.hunks_intersecting_range(buffer_range, excerpt.buffer()) - .map(move |hunk| { - let start = - excerpt.map_point_from_buffer(Point::new(hunk.row_range.start, 0)); - let end = - excerpt.map_point_from_buffer(Point::new(hunk.row_range.end, 0)); - MultiBufferDiffHunk { - row_range: MultiBufferRow(start.row)..MultiBufferRow(end.row), - buffer_id, - buffer_range: hunk.buffer_range.clone(), - diff_base_byte_range: hunk.diff_base_byte_range.clone(), - } - }), - ) - }) - .flatten() - } - - pub fn diff_hunks_in_range_rev<'a, T: ToOffset>( - &'a self, - range: Range, - buffer_snapshot: &'a MultiBufferSnapshot, - ) -> impl Iterator + 'a { - let range = range.start.to_offset(buffer_snapshot)..range.end.to_offset(buffer_snapshot); - buffer_snapshot - .excerpts_for_range_rev(range.clone()) - .filter_map(move |excerpt| { - let buffer = excerpt.buffer(); - let buffer_id = buffer.remote_id(); - let diff = self.0.get(&buffer_id)?; - let buffer_range = excerpt.map_range_to_buffer(range.clone()); - let buffer_range = - buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end); - Some( - diff.hunks_intersecting_range_rev(buffer_range, excerpt.buffer()) - .map(move |hunk| { - let start_row = excerpt - .map_point_from_buffer(Point::new(hunk.row_range.start, 0)) - .row; - let end_row = excerpt - .map_point_from_buffer(Point::new(hunk.row_range.end, 0)) - .row; - MultiBufferDiffHunk { - row_range: MultiBufferRow(start_row)..MultiBufferRow(end_row), - buffer_id, - buffer_range: hunk.buffer_range.clone(), - diff_base_byte_range: hunk.diff_base_byte_range.clone(), - } - }), - ) - }) - .flatten() - } -} - -impl Editor { - pub fn set_expand_all_diff_hunks(&mut self) { - self.diff_map.expand_all = true; - } - - pub(super) fn toggle_hovered_hunk( - &mut self, - hovered_hunk: &HoveredHunk, - cx: &mut ViewContext, - ) { - let editor_snapshot = self.snapshot(cx); - if let Some(diff_hunk) = to_diff_hunk(hovered_hunk, &editor_snapshot.buffer_snapshot) { - self.toggle_hunks_expanded(vec![diff_hunk], cx); - self.change_selections(None, cx, |selections| selections.refresh()); - } - } - - pub fn toggle_hunk_diff(&mut self, _: &ToggleHunkDiff, cx: &mut ViewContext) { - let snapshot = self.snapshot(cx); - let selections = self.selections.all(cx); - self.toggle_hunks_expanded(hunks_for_selections(&snapshot, &selections), cx); - } - - pub fn expand_all_hunk_diffs(&mut self, _: &ExpandAllHunkDiffs, cx: &mut ViewContext) { - let snapshot = self.snapshot(cx); - let display_rows_with_expanded_hunks = self - .diff_map - .hunks(false) - .map(|hunk| &hunk.hunk_range) - .map(|anchor_range| { - ( - anchor_range - .start - .to_display_point(&snapshot.display_snapshot) - .row(), - anchor_range - .end - .to_display_point(&snapshot.display_snapshot) - .row(), - ) - }) - .collect::>(); - let hunks = self - .diff_map - .snapshot - .diff_hunks(&snapshot.display_snapshot.buffer_snapshot) - .filter(|hunk| { - let hunk_display_row_range = Point::new(hunk.row_range.start.0, 0) - .to_display_point(&snapshot.display_snapshot) - ..Point::new(hunk.row_range.end.0, 0) - .to_display_point(&snapshot.display_snapshot); - let row_range_end = - display_rows_with_expanded_hunks.get(&hunk_display_row_range.start.row()); - row_range_end.is_none() || row_range_end != Some(&hunk_display_row_range.end.row()) - }); - self.toggle_hunks_expanded(hunks.collect(), cx); - } - - fn toggle_hunks_expanded( - &mut self, - hunks_to_toggle: Vec, - cx: &mut ViewContext, - ) { - if self.diff_map.expand_all { - return; - } - - let previous_toggle_task = self.diff_map.hunk_update_tasks.remove(&None); - let new_toggle_task = cx.spawn(move |editor, mut cx| async move { - if let Some(task) = previous_toggle_task { - task.await; - } - - editor - .update(&mut cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - let mut hunks_to_toggle = hunks_to_toggle.into_iter().fuse().peekable(); - let mut highlights_to_remove = Vec::with_capacity(editor.diff_map.hunks.len()); - let mut blocks_to_remove = HashSet::default(); - let mut hunks_to_expand = Vec::new(); - editor.diff_map.hunks.retain(|expanded_hunk| { - if expanded_hunk.folded { - return true; - } - let expanded_hunk_row_range = expanded_hunk - .hunk_range - .start - .to_display_point(&snapshot) - .row() - ..expanded_hunk - .hunk_range - .end - .to_display_point(&snapshot) - .row(); - let mut retain = true; - while let Some(hunk_to_toggle) = hunks_to_toggle.peek() { - match diff_hunk_to_display(hunk_to_toggle, &snapshot) { - DisplayDiffHunk::Folded { .. } => { - hunks_to_toggle.next(); - continue; - } - DisplayDiffHunk::Unfolded { - diff_base_byte_range, - display_row_range, - multi_buffer_range, - status, - } => { - let hunk_to_toggle_row_range = display_row_range; - if hunk_to_toggle_row_range.start > expanded_hunk_row_range.end - { - break; - } else if expanded_hunk_row_range == hunk_to_toggle_row_range { - highlights_to_remove.push(expanded_hunk.hunk_range.clone()); - blocks_to_remove - .extend(expanded_hunk.blocks.iter().copied()); - hunks_to_toggle.next(); - retain = false; - break; - } else { - hunks_to_expand.push(HoveredHunk { - status, - multi_buffer_range, - diff_base_byte_range, - }); - hunks_to_toggle.next(); - continue; - } - } - } - } - - retain - }); - for hunk in hunks_to_toggle { - let remaining_hunk_point_range = Point::new(hunk.row_range.start.0, 0) - ..Point::new(hunk.row_range.end.0, 0); - let hunk_start = snapshot - .buffer_snapshot - .anchor_before(remaining_hunk_point_range.start); - let hunk_end = snapshot - .buffer_snapshot - .anchor_in_excerpt(hunk_start.excerpt_id, hunk.buffer_range.end) - .unwrap(); - hunks_to_expand.push(HoveredHunk { - status: hunk_status(&hunk), - multi_buffer_range: hunk_start..hunk_end, - diff_base_byte_range: hunk.diff_base_byte_range.clone(), - }); - } - - editor.remove_highlighted_rows::(highlights_to_remove, cx); - editor.remove_blocks(blocks_to_remove, None, cx); - for hunk in hunks_to_expand { - editor.expand_diff_hunk(None, &hunk, cx); - } - cx.notify(); - }) - .ok(); - }); - - self.diff_map - .hunk_update_tasks - .insert(None, cx.background_executor().spawn(new_toggle_task)); - } - - pub(super) fn expand_diff_hunk( - &mut self, - diff_base_buffer: Option>, - hunk: &HoveredHunk, - cx: &mut ViewContext, - ) -> Option<()> { - let buffer = self.buffer.clone(); - let multi_buffer_snapshot = buffer.read(cx).snapshot(cx); - let hunk_range = hunk.multi_buffer_range.clone(); - let buffer_id = hunk_range.start.buffer_id?; - let diff_base_buffer = diff_base_buffer.or_else(|| { - self.diff_map - .diff_bases - .get(&buffer_id)? - .change_set - .read(cx) - .base_text - .clone() - })?; - - let diff_base = diff_base_buffer.read(cx); - let diff_start_row = diff_base - .offset_to_point(hunk.diff_base_byte_range.start) - .row; - let diff_end_row = diff_base.offset_to_point(hunk.diff_base_byte_range.end).row; - let deleted_text_lines = diff_end_row - diff_start_row; - - let block_insert_index = self - .diff_map - .hunks - .binary_search_by(|probe| { - probe - .hunk_range - .start - .cmp(&hunk_range.start, &multi_buffer_snapshot) - }) - .err()?; - - let blocks; - match hunk.status { - DiffHunkStatus::Removed => { - blocks = self.insert_blocks( - [ - self.hunk_header_block(&hunk, cx), - Self::deleted_text_block(hunk, diff_base_buffer, deleted_text_lines, cx), - ], - None, - cx, - ); - } - DiffHunkStatus::Added => { - self.highlight_rows::( - hunk_range.clone(), - added_hunk_color(cx), - false, - cx, - ); - blocks = self.insert_blocks([self.hunk_header_block(&hunk, cx)], None, cx); - } - DiffHunkStatus::Modified => { - self.highlight_rows::( - hunk_range.clone(), - added_hunk_color(cx), - false, - cx, - ); - blocks = self.insert_blocks( - [ - self.hunk_header_block(&hunk, cx), - Self::deleted_text_block(hunk, diff_base_buffer, deleted_text_lines, cx), - ], - None, - cx, - ); - } - }; - self.diff_map.hunks.insert( - block_insert_index, - ExpandedHunk { - blocks, - hunk_range, - status: hunk.status, - folded: false, - diff_base_byte_range: hunk.diff_base_byte_range.clone(), - }, - ); - - Some(()) - } - - fn apply_diff_hunks_in_range( - &mut self, - range: Range, - cx: &mut ViewContext, - ) -> Option<()> { - let multi_buffer = self.buffer.read(cx); - let multi_buffer_snapshot = multi_buffer.snapshot(cx); - let (excerpt, range) = multi_buffer_snapshot - .range_to_buffer_ranges(range) - .into_iter() - .next()?; - - multi_buffer - .buffer(excerpt.buffer_id()) - .unwrap() - .update(cx, |branch_buffer, cx| { - branch_buffer.merge_into_base(vec![range], cx); - }); - - if let Some(project) = self.project.clone() { - self.save(true, project, cx).detach_and_log_err(cx); - } - - None - } - - pub(crate) fn apply_all_diff_hunks( - &mut self, - _: &ApplyAllDiffHunks, - cx: &mut ViewContext, - ) { - let buffers = self.buffer.read(cx).all_buffers(); - for branch_buffer in buffers { - branch_buffer.update(cx, |branch_buffer, cx| { - branch_buffer.merge_into_base(Vec::new(), cx); - }); - } - - if let Some(project) = self.project.clone() { - self.save(true, project, cx).detach_and_log_err(cx); - } - } - - pub(crate) fn apply_selected_diff_hunks( - &mut self, - _: &ApplyDiffHunk, - cx: &mut ViewContext, - ) { - let snapshot = self.snapshot(cx); - let hunks = hunks_for_selections(&snapshot, &self.selections.all(cx)); - let mut ranges_by_buffer = HashMap::default(); - self.transact(cx, |editor, cx| { - for hunk in hunks { - if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) { - ranges_by_buffer - .entry(buffer.clone()) - .or_insert_with(Vec::new) - .push(hunk.buffer_range.to_offset(buffer.read(cx))); - } - } - - for (buffer, ranges) in ranges_by_buffer { - buffer.update(cx, |buffer, cx| { - buffer.merge_into_base(ranges, cx); - }); - } - }); - - if let Some(project) = self.project.clone() { - self.save(true, project, cx).detach_and_log_err(cx); - } - } - - fn has_multiple_hunks(&self, cx: &AppContext) -> bool { - let snapshot = self.buffer.read(cx).snapshot(cx); - let mut hunks = self.diff_map.snapshot.diff_hunks(&snapshot); - hunks.nth(1).is_some() - } - - fn hunk_header_block( - &self, - hunk: &HoveredHunk, - cx: &mut ViewContext, - ) -> BlockProperties { - let is_branch_buffer = self - .buffer - .read(cx) - .point_to_buffer_offset(hunk.multi_buffer_range.start, cx) - .map_or(false, |(buffer, _, _)| { - buffer.read(cx).base_buffer().is_some() - }); - - let border_color = cx.theme().colors().border_variant; - let bg_color = cx.theme().colors().editor_background; - let gutter_color = match hunk.status { - DiffHunkStatus::Added => cx.theme().status().created, - DiffHunkStatus::Modified => cx.theme().status().modified, - DiffHunkStatus::Removed => cx.theme().status().deleted, - }; - - BlockProperties { - placement: BlockPlacement::Above(hunk.multi_buffer_range.start), - height: 1, - style: BlockStyle::Sticky, - priority: 0, - render: Arc::new({ - let editor = cx.view().clone(); - let hunk = hunk.clone(); - let has_multiple_hunks = self.has_multiple_hunks(cx); - - move |cx| { - let hunk_controls_menu_handle = - editor.read(cx).hunk_controls_menu_handle.clone(); - - h_flex() - .id(cx.block_id) - .block_mouse_down() - .h(cx.line_height()) - .w_full() - .border_t_1() - .border_color(border_color) - .bg(bg_color) - .child( - div() - .id("gutter-strip") - .w(EditorElement::diff_hunk_strip_width(cx.line_height())) - .h_full() - .bg(gutter_color) - .cursor(CursorStyle::PointingHand) - .on_click({ - let editor = editor.clone(); - let hunk = hunk.clone(); - move |_event, cx| { - editor.update(cx, |editor, cx| { - editor.toggle_hovered_hunk(&hunk, cx); - }); - } - }), - ) - .child( - h_flex() - .px_6() - .size_full() - .justify_end() - .child( - h_flex() - .gap_1() - .when(!is_branch_buffer, |row| { - row.child( - IconButton::new("next-hunk", IconName::ArrowDown) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .disabled(!has_multiple_hunks) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |cx| { - Tooltip::for_action_in( - "Next Hunk", - &GoToHunk, - &focus_handle, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - let hunk = hunk.clone(); - move |_event, cx| { - editor.update(cx, |editor, cx| { - editor.go_to_subsequent_hunk( - hunk.multi_buffer_range.end, - cx, - ); - }); - } - }), - ) - .child( - IconButton::new("prev-hunk", IconName::ArrowUp) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .disabled(!has_multiple_hunks) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |cx| { - Tooltip::for_action_in( - "Previous Hunk", - &GoToPrevHunk, - &focus_handle, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - let hunk = hunk.clone(); - move |_event, cx| { - editor.update(cx, |editor, cx| { - editor.go_to_preceding_hunk( - hunk.multi_buffer_range.start, - cx, - ); - }); - } - }), - ) - }) - .child( - IconButton::new("discard", IconName::Undo) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |cx| { - Tooltip::for_action_in( - "Discard Hunk", - &RevertSelectedHunks, - &focus_handle, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - let hunk = hunk.clone(); - move |_event, cx| { - editor.update(cx, |editor, cx| { - editor.revert_hunk(hunk.clone(), cx); - }); - } - }), - ) - .map(|this| { - if is_branch_buffer { - this.child( - IconButton::new("apply", IconName::Check) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .tooltip({ - let focus_handle = - editor.focus_handle(cx); - move |cx| { - Tooltip::for_action_in( - "Apply Hunk", - &ApplyDiffHunk, - &focus_handle, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - let hunk = hunk.clone(); - move |_event, cx| { - editor.update(cx, |editor, cx| { - editor - .apply_diff_hunks_in_range( - hunk.multi_buffer_range - .clone(), - cx, - ); - }); - } - }), - ) - } else { - this.child({ - let focus = editor.focus_handle(cx); - PopoverMenu::new("hunk-controls-dropdown") - .trigger( - IconButton::new( - "toggle_editor_selections_icon", - IconName::EllipsisVertical, - ) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .style(ButtonStyle::Subtle) - .toggle_state( - hunk_controls_menu_handle - .is_deployed(), - ) - .when( - !hunk_controls_menu_handle - .is_deployed(), - |this| { - this.tooltip(|cx| { - Tooltip::text( - "Hunk Controls", - cx, - ) - }) - }, - ), - ) - .anchor(Corner::TopRight) - .with_handle(hunk_controls_menu_handle) - .menu(move |cx| { - let focus = focus.clone(); - let menu = ContextMenu::build( - cx, - move |menu, _| { - menu.context(focus.clone()) - .action( - "Discard All Hunks", - RevertFile - .boxed_clone(), - ) - }, - ); - Some(menu) - }) - }) - } - }), - ) - .when(!is_branch_buffer, |div| { - div.child( - IconButton::new("collapse", IconName::Close) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |cx| { - Tooltip::for_action_in( - "Collapse Hunk", - &ToggleHunkDiff, - &focus_handle, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - let hunk = hunk.clone(); - move |_event, cx| { - editor.update(cx, |editor, cx| { - editor.toggle_hovered_hunk(&hunk, cx); - }); - } - }), - ) - }), - ) - .into_any_element() - } - }), - } - } - - fn deleted_text_block( - hunk: &HoveredHunk, - diff_base_buffer: Model, - deleted_text_height: u32, - cx: &mut ViewContext, - ) -> BlockProperties { - let gutter_color = match hunk.status { - DiffHunkStatus::Added => unreachable!(), - DiffHunkStatus::Modified => cx.theme().status().modified, - DiffHunkStatus::Removed => cx.theme().status().deleted, - }; - let deleted_hunk_color = deleted_hunk_color(cx); - let (editor_height, editor_with_deleted_text) = - editor_with_deleted_text(diff_base_buffer, deleted_hunk_color, hunk, cx); - let editor = cx.view().clone(); - let hunk = hunk.clone(); - let height = editor_height.max(deleted_text_height); - BlockProperties { - placement: BlockPlacement::Above(hunk.multi_buffer_range.start), - height, - style: BlockStyle::Flex, - priority: 0, - render: Arc::new(move |cx| { - let width = EditorElement::diff_hunk_strip_width(cx.line_height()); - let gutter_dimensions = editor.read(cx.context).gutter_dimensions; - - h_flex() - .id(cx.block_id) - .block_mouse_down() - .bg(deleted_hunk_color) - .h(height as f32 * cx.line_height()) - .w_full() - .child( - h_flex() - .id("gutter") - .max_w(gutter_dimensions.full_width()) - .min_w(gutter_dimensions.full_width()) - .size_full() - .child( - h_flex() - .id("gutter hunk") - .bg(gutter_color) - .pl(gutter_dimensions.margin - + gutter_dimensions - .git_blame_entries_width - .unwrap_or_default()) - .max_w(width) - .min_w(width) - .size_full() - .cursor(CursorStyle::PointingHand) - .on_mouse_down(MouseButton::Left, { - let editor = editor.clone(); - let hunk = hunk.clone(); - move |_event, cx| { - editor.update(cx, |editor, cx| { - editor.toggle_hovered_hunk(&hunk, cx); - }); - } - }), - ), - ) - .child(editor_with_deleted_text.clone()) - .into_any_element() - }), - } - } - - pub(super) fn clear_expanded_diff_hunks(&mut self, cx: &mut ViewContext) -> bool { - if self.diff_map.expand_all { - return false; - } - self.diff_map.hunk_update_tasks.clear(); - self.clear_row_highlights::(); - let to_remove = self - .diff_map - .hunks - .drain(..) - .flat_map(|expanded_hunk| expanded_hunk.blocks.into_iter()) - .collect::>(); - if to_remove.is_empty() { - false - } else { - self.remove_blocks(to_remove, None, cx); - true - } - } - - pub(super) fn sync_expanded_diff_hunks( - diff_map: &mut DiffMap, - buffer_id: BufferId, - cx: &mut ViewContext, - ) { - let diff_base_state = diff_map.diff_bases.get_mut(&buffer_id); - let mut diff_base_buffer = None; - let mut diff_base_buffer_unchanged = true; - if let Some(diff_base_state) = diff_base_state { - diff_base_state.change_set.update(cx, |change_set, _| { - if diff_base_state.last_version != Some(change_set.base_text_version) { - diff_base_state.last_version = Some(change_set.base_text_version); - diff_base_buffer_unchanged = false; - } - diff_base_buffer = change_set.base_text.clone(); - }) - } - - diff_map.hunk_update_tasks.remove(&Some(buffer_id)); - - let new_sync_task = cx.spawn(move |editor, mut cx| async move { - editor - .update(&mut cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - let mut recalculated_hunks = snapshot - .diff_map - .diff_hunks(&snapshot.buffer_snapshot) - .filter(|hunk| hunk.buffer_id == buffer_id) - .fuse() - .peekable(); - let mut highlights_to_remove = Vec::with_capacity(editor.diff_map.hunks.len()); - let mut blocks_to_remove = HashSet::default(); - let mut hunks_to_reexpand = Vec::with_capacity(editor.diff_map.hunks.len()); - editor.diff_map.hunks.retain_mut(|expanded_hunk| { - if expanded_hunk.hunk_range.start.buffer_id != Some(buffer_id) { - return true; - }; - - let mut retain = false; - if diff_base_buffer_unchanged { - let expanded_hunk_display_range = expanded_hunk - .hunk_range - .start - .to_display_point(&snapshot) - .row() - ..expanded_hunk - .hunk_range - .end - .to_display_point(&snapshot) - .row(); - while let Some(buffer_hunk) = recalculated_hunks.peek() { - match diff_hunk_to_display(buffer_hunk, &snapshot) { - DisplayDiffHunk::Folded { display_row } => { - recalculated_hunks.next(); - if !expanded_hunk.folded - && expanded_hunk_display_range - .to_inclusive() - .contains(&display_row) - { - retain = true; - expanded_hunk.folded = true; - highlights_to_remove - .push(expanded_hunk.hunk_range.clone()); - for block in expanded_hunk.blocks.drain(..) { - blocks_to_remove.insert(block); - } - break; - } else { - continue; - } - } - DisplayDiffHunk::Unfolded { - diff_base_byte_range, - display_row_range, - multi_buffer_range, - status, - } => { - let hunk_display_range = display_row_range; - - if expanded_hunk_display_range.start - > hunk_display_range.end - { - recalculated_hunks.next(); - if editor.diff_map.expand_all { - hunks_to_reexpand.push(HoveredHunk { - status, - multi_buffer_range, - diff_base_byte_range, - }); - } - continue; - } - - if expanded_hunk_display_range.end - < hunk_display_range.start - { - break; - } - - if !expanded_hunk.folded - && expanded_hunk_display_range == hunk_display_range - && expanded_hunk.status == hunk_status(buffer_hunk) - && expanded_hunk.diff_base_byte_range - == buffer_hunk.diff_base_byte_range - { - recalculated_hunks.next(); - retain = true; - } else { - hunks_to_reexpand.push(HoveredHunk { - status, - multi_buffer_range, - diff_base_byte_range, - }); - } - break; - } - } - } - } - if !retain { - blocks_to_remove.extend(expanded_hunk.blocks.drain(..)); - highlights_to_remove.push(expanded_hunk.hunk_range.clone()); - } - retain - }); - - if editor.diff_map.expand_all { - for hunk in recalculated_hunks { - match diff_hunk_to_display(&hunk, &snapshot) { - DisplayDiffHunk::Folded { .. } => {} - DisplayDiffHunk::Unfolded { - diff_base_byte_range, - multi_buffer_range, - status, - .. - } => { - hunks_to_reexpand.push(HoveredHunk { - status, - multi_buffer_range, - diff_base_byte_range, - }); - } - } - } - } else { - drop(recalculated_hunks); - } - - editor.remove_highlighted_rows::(highlights_to_remove, cx); - editor.remove_blocks(blocks_to_remove, None, cx); - - if let Some(diff_base_buffer) = &diff_base_buffer { - for hunk in hunks_to_reexpand { - editor.expand_diff_hunk(Some(diff_base_buffer.clone()), &hunk, cx); - } - } - }) - .ok(); - }); - - diff_map.hunk_update_tasks.insert( - Some(buffer_id), - cx.background_executor().spawn(new_sync_task), - ); - } - - fn go_to_subsequent_hunk(&mut self, position: Anchor, cx: &mut ViewContext) { - let snapshot = self.snapshot(cx); - let position = position.to_point(&snapshot.buffer_snapshot); - if let Some(hunk) = self.go_to_hunk_after_position(&snapshot, position, cx) { - let multi_buffer_start = snapshot - .buffer_snapshot - .anchor_before(Point::new(hunk.row_range.start.0, 0)); - let multi_buffer_end = snapshot - .buffer_snapshot - .anchor_after(Point::new(hunk.row_range.end.0, 0)); - self.expand_diff_hunk( - None, - &HoveredHunk { - multi_buffer_range: multi_buffer_start..multi_buffer_end, - status: hunk_status(&hunk), - diff_base_byte_range: hunk.diff_base_byte_range, - }, - cx, - ); - } - } - - fn go_to_preceding_hunk(&mut self, position: Anchor, cx: &mut ViewContext) { - let snapshot = self.snapshot(cx); - let position = position.to_point(&snapshot.buffer_snapshot); - let hunk = self.go_to_hunk_before_position(&snapshot, position, cx); - if let Some(hunk) = hunk { - let multi_buffer_start = snapshot - .buffer_snapshot - .anchor_before(Point::new(hunk.row_range.start.0, 0)); - let multi_buffer_end = snapshot - .buffer_snapshot - .anchor_after(Point::new(hunk.row_range.end.0, 0)); - self.expand_diff_hunk( - None, - &HoveredHunk { - multi_buffer_range: multi_buffer_start..multi_buffer_end, - status: hunk_status(&hunk), - diff_base_byte_range: hunk.diff_base_byte_range, - }, - cx, - ); - } - } -} - -pub(crate) fn to_diff_hunk( - hovered_hunk: &HoveredHunk, - multi_buffer_snapshot: &MultiBufferSnapshot, -) -> Option { - let buffer_id = hovered_hunk - .multi_buffer_range - .start - .buffer_id - .or(hovered_hunk.multi_buffer_range.end.buffer_id)?; - let buffer_range = hovered_hunk.multi_buffer_range.start.text_anchor - ..hovered_hunk.multi_buffer_range.end.text_anchor; - let point_range = hovered_hunk - .multi_buffer_range - .to_point(multi_buffer_snapshot); - Some(MultiBufferDiffHunk { - row_range: MultiBufferRow(point_range.start.row)..MultiBufferRow(point_range.end.row), - buffer_id, - buffer_range, - diff_base_byte_range: hovered_hunk.diff_base_byte_range.clone(), - }) -} - -fn added_hunk_color(cx: &AppContext) -> Hsla { - let mut created_color = cx.theme().status().git().created; - created_color.fade_out(0.7); - created_color -} - -fn deleted_hunk_color(cx: &AppContext) -> Hsla { - let mut deleted_color = cx.theme().status().deleted; - deleted_color.fade_out(0.7); - deleted_color -} - -fn editor_with_deleted_text( - diff_base_buffer: Model, - deleted_color: Hsla, - hunk: &HoveredHunk, - cx: &mut ViewContext, -) -> (u32, View) { - let parent_editor = cx.view().downgrade(); - let editor = cx.new_view(|cx| { - let multi_buffer = - cx.new_model(|_| MultiBuffer::without_headers(language::Capability::ReadOnly)); - multi_buffer.update(cx, |multi_buffer, cx| { - multi_buffer.push_excerpts( - diff_base_buffer, - Some(ExcerptRange { - context: hunk.diff_base_byte_range.clone(), - primary: None, - }), - cx, - ); - }); - - let mut editor = Editor::for_multibuffer(multi_buffer, None, true, cx); - editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx); - editor.set_show_wrap_guides(false, cx); - editor.set_show_gutter(false, cx); - editor.set_show_line_numbers(false, cx); - editor.set_show_scrollbars(false, cx); - editor.set_show_runnables(false, cx); - editor.set_show_git_diff_gutter(false, cx); - editor.set_show_code_actions(false, cx); - editor.scroll_manager.set_forbid_vertical_scroll(true); - editor.set_read_only(true); - editor.set_show_inline_completions(Some(false), cx); - - enum DeletedBlockRowHighlight {} - editor.highlight_rows::( - Anchor::min()..Anchor::max(), - deleted_color, - false, - cx, - ); - editor.set_current_line_highlight(Some(CurrentLineHighlight::None)); - editor - ._subscriptions - .extend([cx.on_blur(&editor.focus_handle, |editor, cx| { - editor.change_selections(None, cx, |s| { - s.try_cancel(); - }); - })]); - - editor - .register_action::({ - let hunk = hunk.clone(); - let parent_editor = parent_editor.clone(); - move |_, cx| { - parent_editor - .update(cx, |editor, cx| editor.revert_hunk(hunk.clone(), cx)) - .ok(); - } - }) - .detach(); - editor - .register_action::({ - let hunk = hunk.clone(); - move |_, cx| { - parent_editor - .update(cx, |editor, cx| { - editor.toggle_hovered_hunk(&hunk, cx); - }) - .ok(); - } - }) - .detach(); - editor - }); - - let editor_height = editor.update(cx, |editor, cx| editor.max_point(cx).row().0); - (editor_height, editor) -} - -impl DisplayDiffHunk { - pub fn start_display_row(&self) -> DisplayRow { - match self { - &DisplayDiffHunk::Folded { display_row } => display_row, - DisplayDiffHunk::Unfolded { - display_row_range, .. - } => display_row_range.start, - } - } - - pub fn contains_display_row(&self, display_row: DisplayRow) -> bool { - let range = match self { - &DisplayDiffHunk::Folded { display_row } => display_row..=display_row, - - DisplayDiffHunk::Unfolded { - display_row_range, .. - } => display_row_range.start..=display_row_range.end, - }; - - range.contains(&display_row) - } -} - -pub fn diff_hunk_to_display( - hunk: &MultiBufferDiffHunk, - snapshot: &DisplaySnapshot, -) -> DisplayDiffHunk { - let hunk_start_point = Point::new(hunk.row_range.start.0, 0); - let hunk_start_point_sub = Point::new(hunk.row_range.start.0.saturating_sub(1), 0); - let hunk_end_point_sub = Point::new( - hunk.row_range - .end - .0 - .saturating_sub(1) - .max(hunk.row_range.start.0), - 0, - ); - - let status = hunk_status(hunk); - let is_removal = status == DiffHunkStatus::Removed; - - let folds_start = Point::new(hunk.row_range.start.0.saturating_sub(2), 0); - let folds_end = Point::new(hunk.row_range.end.0 + 2, 0); - let folds_range = folds_start..folds_end; - - let containing_fold = snapshot.folds_in_range(folds_range).find(|fold| { - let fold_point_range = fold.range.to_point(&snapshot.buffer_snapshot); - let fold_point_range = fold_point_range.start..=fold_point_range.end; - - let folded_start = fold_point_range.contains(&hunk_start_point); - let folded_end = fold_point_range.contains(&hunk_end_point_sub); - let folded_start_sub = fold_point_range.contains(&hunk_start_point_sub); - - (folded_start && folded_end) || (is_removal && folded_start_sub) - }); - - if let Some(fold) = containing_fold { - let row = fold.range.start.to_display_point(snapshot).row(); - DisplayDiffHunk::Folded { display_row: row } - } else { - let start = hunk_start_point.to_display_point(snapshot).row(); - - let hunk_end_row = hunk.row_range.end.max(hunk.row_range.start); - let hunk_end_point = Point::new(hunk_end_row.0, 0); - - let multi_buffer_start = snapshot.buffer_snapshot.anchor_before(hunk_start_point); - let multi_buffer_end = snapshot - .buffer_snapshot - .anchor_in_excerpt(multi_buffer_start.excerpt_id, hunk.buffer_range.end) - .unwrap(); - let end = hunk_end_point.to_display_point(snapshot).row(); - - DisplayDiffHunk::Unfolded { - display_row_range: start..end, - multi_buffer_range: multi_buffer_start..multi_buffer_end, - status, - diff_base_byte_range: hunk.diff_base_byte_range.clone(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{editor_tests::init_test, hunk_status}; - use gpui::{Context, TestAppContext}; - use language::Capability::ReadWrite; - use multi_buffer::{ExcerptRange, MultiBuffer, MultiBufferRow}; - use project::{FakeFs, Project}; - use unindent::Unindent as _; - - #[gpui::test] - async fn test_diff_hunks_in_range(cx: &mut TestAppContext) { - use git::diff::DiffHunkStatus; - init_test(cx, |_| {}); - - let fs = FakeFs::new(cx.background_executor.clone()); - let project = Project::test(fs, [], cx).await; - - // buffer has two modified hunks with two rows each - let diff_base_1 = " - 1.zero - 1.one - 1.two - 1.three - 1.four - 1.five - 1.six - " - .unindent(); - - let text_1 = " - 1.zero - 1.ONE - 1.TWO - 1.three - 1.FOUR - 1.FIVE - 1.six - " - .unindent(); - - // buffer has a deletion hunk and an insertion hunk - let diff_base_2 = " - 2.zero - 2.one - 2.one-and-a-half - 2.two - 2.three - 2.four - 2.six - " - .unindent(); - - let text_2 = " - 2.zero - 2.one - 2.two - 2.three - 2.four - 2.five - 2.six - " - .unindent(); - - let buffer_1 = project.update(cx, |project, cx| { - project.create_local_buffer(text_1.as_str(), None, cx) - }); - let buffer_2 = project.update(cx, |project, cx| { - project.create_local_buffer(text_2.as_str(), None, cx) - }); - - let multibuffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(ReadWrite); - multibuffer.push_excerpts( - buffer_1.clone(), - [ - // excerpt ends in the middle of a modified hunk - ExcerptRange { - context: Point::new(0, 0)..Point::new(1, 5), - primary: Default::default(), - }, - // excerpt begins in the middle of a modified hunk - ExcerptRange { - context: Point::new(5, 0)..Point::new(6, 5), - primary: Default::default(), - }, - ], - cx, - ); - multibuffer.push_excerpts( - buffer_2.clone(), - [ - // excerpt ends at a deletion - ExcerptRange { - context: Point::new(0, 0)..Point::new(1, 5), - primary: Default::default(), - }, - // excerpt starts at a deletion - ExcerptRange { - context: Point::new(2, 0)..Point::new(2, 5), - primary: Default::default(), - }, - // excerpt fully contains a deletion hunk - ExcerptRange { - context: Point::new(1, 0)..Point::new(2, 5), - primary: Default::default(), - }, - // excerpt fully contains an insertion hunk - ExcerptRange { - context: Point::new(4, 0)..Point::new(6, 5), - primary: Default::default(), - }, - ], - cx, - ); - multibuffer - }); - - let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, false, cx)); - editor - .update(cx, |editor, cx| { - for (buffer, diff_base) in [ - (buffer_1.clone(), diff_base_1), - (buffer_2.clone(), diff_base_2), - ] { - let change_set = cx.new_model(|cx| { - BufferChangeSet::new_with_base_text( - diff_base.to_string(), - buffer.read(cx).text_snapshot(), - cx, - ) - }); - editor.diff_map.add_change_set(change_set, cx) - } - }) - .unwrap(); - cx.background_executor.run_until_parked(); - - let snapshot = editor.update(cx, |editor, cx| editor.snapshot(cx)).unwrap(); - - assert_eq!( - snapshot.buffer_snapshot.text(), - " - 1.zero - 1.ONE - 1.FIVE - 1.six - 2.zero - 2.one - 2.two - 2.one - 2.two - 2.four - 2.five - 2.six" - .unindent() - ); - - let expected = [ - ( - DiffHunkStatus::Modified, - MultiBufferRow(1)..MultiBufferRow(2), - ), - ( - DiffHunkStatus::Modified, - MultiBufferRow(2)..MultiBufferRow(3), - ), - //TODO: Define better when and where removed hunks show up at range extremities - ( - DiffHunkStatus::Removed, - MultiBufferRow(6)..MultiBufferRow(6), - ), - ( - DiffHunkStatus::Removed, - MultiBufferRow(8)..MultiBufferRow(8), - ), - ( - DiffHunkStatus::Added, - MultiBufferRow(10)..MultiBufferRow(11), - ), - ]; - - assert_eq!( - snapshot - .diff_map - .diff_hunks_in_range(Point::zero()..Point::new(12, 0), &snapshot.buffer_snapshot) - .map(|hunk| (hunk_status(&hunk), hunk.row_range)) - .collect::>(), - &expected, - ); - - assert_eq!( - snapshot - .diff_map - .diff_hunks_in_range_rev( - Point::zero()..Point::new(12, 0), - &snapshot.buffer_snapshot - ) - .map(|hunk| (hunk_status(&hunk), hunk.row_range)) - .collect::>(), - expected - .iter() - .rev() - .cloned() - .collect::>() - .as_slice(), - ); - } -} diff --git a/crates/editor/src/indent_guides.rs b/crates/editor/src/indent_guides.rs index 1284ba8156..21e698c399 100644 --- a/crates/editor/src/indent_guides.rs +++ b/crates/editor/src/indent_guides.rs @@ -2,17 +2,16 @@ use std::{ops::Range, time::Duration}; use collections::HashSet; use gpui::{AppContext, Task}; -use language::{language_settings::language_settings, BufferRow}; -use multi_buffer::{MultiBufferIndentGuide, MultiBufferRow}; -use text::{BufferId, LineIndent, Point}; +use language::language_settings::language_settings; +use multi_buffer::{IndentGuide, MultiBufferRow}; +use text::{LineIndent, Point}; use ui::ViewContext; use util::ResultExt; use crate::{DisplaySnapshot, Editor}; struct ActiveIndentedRange { - buffer_id: BufferId, - row_range: Range, + row_range: Range, indent: LineIndent, } @@ -36,7 +35,7 @@ impl Editor { visible_buffer_range: Range, snapshot: &DisplaySnapshot, cx: &mut ViewContext, - ) -> Option> { + ) -> Option> { let show_indent_guides = self.should_show_indent_guides().unwrap_or_else(|| { if let Some(buffer) = self.buffer().read(cx).as_singleton() { language_settings( @@ -66,7 +65,7 @@ impl Editor { pub fn find_active_indent_guide_indices( &mut self, - indent_guides: &[MultiBufferIndentGuide], + indent_guides: &[IndentGuide], snapshot: &DisplaySnapshot, cx: &mut ViewContext, ) -> Option> { @@ -134,9 +133,7 @@ impl Editor { .iter() .enumerate() .filter(|(_, indent_guide)| { - indent_guide.buffer_id == active_indent_range.buffer_id - && indent_guide.indent_level() - == active_indent_range.indent.len(indent_guide.tab_size) + indent_guide.indent_level() == active_indent_range.indent.len(indent_guide.tab_size) }); let mut matches = HashSet::default(); @@ -158,7 +155,7 @@ pub fn indent_guides_in_range( ignore_disabled_for_language: bool, snapshot: &DisplaySnapshot, cx: &AppContext, -) -> Vec { +) -> Vec { let start_anchor = snapshot .buffer_snapshot .anchor_before(Point::new(visible_buffer_range.start.0, 0)); @@ -169,14 +166,12 @@ pub fn indent_guides_in_range( snapshot .buffer_snapshot .indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx) - .into_iter() .filter(|indent_guide| { - if editor.buffer_folded(indent_guide.buffer_id, cx) { + if editor.is_buffer_folded(indent_guide.buffer_id, cx) { return false; } - let start = - MultiBufferRow(indent_guide.multibuffer_row_range.start.0.saturating_sub(1)); + let start = MultiBufferRow(indent_guide.start_row.0.saturating_sub(1)); // Filter out indent guides that are inside a fold // All indent guides that are starting "offscreen" have a start value of the first visible row minus one // Therefore checking if a line is folded at first visible row minus one causes the other indent guides that are not related to the fold to disappear as well @@ -193,24 +188,11 @@ async fn resolve_indented_range( snapshot: DisplaySnapshot, buffer_row: MultiBufferRow, ) -> Option { - let (buffer_row, buffer_snapshot, buffer_id) = - if let Some((_, buffer_id, snapshot)) = snapshot.buffer_snapshot.as_singleton() { - (buffer_row.0, snapshot, buffer_id) - } else { - let (snapshot, point) = snapshot.buffer_snapshot.buffer_line_for_row(buffer_row)?; - - let buffer_id = snapshot.remote_id(); - (point.start.row, snapshot, buffer_id) - }; - - buffer_snapshot + snapshot + .buffer_snapshot .enclosing_indent(buffer_row) .await - .map(|(row_range, indent)| ActiveIndentedRange { - row_range, - indent, - buffer_id, - }) + .map(|(row_range, indent)| ActiveIndentedRange { row_range, indent }) } fn should_recalculate_indented_range( @@ -222,23 +204,23 @@ fn should_recalculate_indented_range( if prev_row.0 == new_row.0 { return false; } - if let Some((_, _, snapshot)) = snapshot.buffer_snapshot.as_singleton() { - if !current_indent_range.row_range.contains(&new_row.0) { + if snapshot.buffer_snapshot.is_singleton() { + if !current_indent_range.row_range.contains(&new_row) { return true; } - let old_line_indent = snapshot.line_indent_for_row(prev_row.0); - let new_line_indent = snapshot.line_indent_for_row(new_row.0); + let old_line_indent = snapshot.buffer_snapshot.line_indent_for_row(prev_row); + let new_line_indent = snapshot.buffer_snapshot.line_indent_for_row(new_row); if old_line_indent.is_line_empty() || new_line_indent.is_line_empty() || old_line_indent != new_line_indent - || snapshot.max_point().row == new_row.0 + || snapshot.buffer_snapshot.max_point().row == new_row.0 { return true; } - let next_line_indent = snapshot.line_indent_for_row(new_row.0 + 1); + let next_line_indent = snapshot.buffer_snapshot.line_indent_for_row(new_row + 1); next_line_indent.is_line_empty() || next_line_indent != old_line_indent } else { true diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 9931685035..b3d400fd00 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -20,7 +20,6 @@ use language::{ SelectionGoal, }; use lsp::DiagnosticSeverity; -use multi_buffer::AnchorRangeExt; use project::{ lsp_store::FormatTrigger, project_settings::ProjectSettings, search::SearchQuery, Project, ProjectItem as _, ProjectPath, @@ -528,6 +527,7 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor) excerpt_id, text_anchor: language::proto::deserialize_anchor(anchor.anchor?)?, buffer_id: buffer.buffer_id_for_excerpt(excerpt_id), + diff_base_anchor: None, }) } @@ -1435,59 +1435,34 @@ impl SearchableItem for Editor { cx.background_executor().spawn(async move { let mut ranges = Vec::new(); - if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() { - let search_within_ranges = if search_within_ranges.is_empty() { - vec![None] - } else { - search_within_ranges - .into_iter() - .map(|range| Some(range.to_offset(&buffer))) - .collect::>() - }; - - for range in search_within_ranges { - let buffer = &buffer; - ranges.extend( - query - .search(excerpt_buffer, range.clone()) - .await - .into_iter() - .map(|matched_range| { - let offset = range.clone().map(|r| r.start).unwrap_or(0); - buffer.anchor_after(matched_range.start + offset) - ..buffer.anchor_before(matched_range.end + offset) - }), - ); - } + let search_within_ranges = if search_within_ranges.is_empty() { + vec![buffer.anchor_before(0)..buffer.anchor_after(buffer.len())] } else { - let search_within_ranges = if search_within_ranges.is_empty() { - vec![buffer.anchor_before(0)..buffer.anchor_after(buffer.len())] - } else { - search_within_ranges - }; - - for (excerpt_id, search_buffer, search_range) in - buffer.excerpts_in_ranges(search_within_ranges) - { - if !search_range.is_empty() { - ranges.extend( - query - .search(search_buffer, Some(search_range.clone())) - .await - .into_iter() - .map(|match_range| { - let start = search_buffer - .anchor_after(search_range.start + match_range.start); - let end = search_buffer - .anchor_before(search_range.start + match_range.end); - buffer.anchor_in_excerpt(excerpt_id, start).unwrap() - ..buffer.anchor_in_excerpt(excerpt_id, end).unwrap() - }), - ); - } - } + search_within_ranges }; + for (search_buffer, search_range, excerpt_id) in + buffer.ranges_to_buffer_ranges(search_within_ranges.into_iter()) + { + ranges.extend( + query + .search(search_buffer, Some(search_range.clone())) + .await + .into_iter() + .map(|match_range| { + let start = + search_buffer.anchor_after(search_range.start + match_range.start); + let end = + search_buffer.anchor_before(search_range.start + match_range.end); + Anchor::range_in_buffer( + excerpt_id, + search_buffer.remote_id(), + start..end, + ) + }), + ); + } + ranges }) } diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index 923dcc24b9..2021ed0721 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -61,7 +61,7 @@ impl ProposedChangesEditor { let mut this = Self { editor: cx.new_view(|cx| { let mut editor = Editor::for_multibuffer(multibuffer.clone(), project, true, cx); - editor.set_expand_all_diff_hunks(); + editor.set_expand_all_diff_hunks(cx); editor.set_completion_provider(None); editor.clear_code_action_providers(); editor.set_semantics_provider( @@ -104,16 +104,10 @@ impl ProposedChangesEditor { let buffer = buffer.read(cx); let base_buffer = buffer.base_buffer()?; let buffer = buffer.text_snapshot(); - let change_set = this.editor.update(cx, |editor, _| { - Some( - editor - .diff_map - .diff_bases - .get(&buffer.remote_id())? - .change_set - .clone(), - ) - })?; + let change_set = this + .multibuffer + .read(cx) + .change_set_for(buffer.remote_id())?; Some(change_set.update(cx, |change_set, cx| { change_set.set_base_text( base_buffer.read(cx).text(), @@ -193,7 +187,7 @@ impl ProposedChangesEditor { } else { branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx)); new_change_sets.push(cx.new_model(|cx| { - let mut change_set = BufferChangeSet::new(branch_buffer.read(cx)); + let mut change_set = BufferChangeSet::new(&branch_buffer, cx); let _ = change_set.set_base_text( location.buffer.read(cx).text(), branch_buffer.read(cx).text_snapshot(), @@ -223,9 +217,11 @@ impl ProposedChangesEditor { self.buffer_entries = buffer_entries; self.editor.update(cx, |editor, cx| { editor.change_selections(None, cx, |selections| selections.refresh()); - for change_set in new_change_sets { - editor.diff_map.add_change_set(change_set, cx) - } + editor.buffer.update(cx, |buffer, cx| { + for change_set in new_change_sets { + buffer.add_change_set(change_set, cx) + } + }) }); } diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index c712f1ccd2..72ae5233d6 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -323,8 +323,7 @@ impl SelectionsCollection { self.all(cx).last().unwrap().clone() } - #[cfg(any(test, feature = "test-support"))] - pub fn ranges + std::fmt::Debug>( + pub fn ranges>( &self, cx: &mut AppContext, ) -> Vec> { @@ -332,9 +331,9 @@ impl SelectionsCollection { .iter() .map(|s| { if s.reversed { - s.end.clone()..s.start.clone() + s.end..s.start } else { - s.start.clone()..s.end.clone() + s.start..s.end } }) .collect() @@ -921,7 +920,7 @@ pub(crate) fn resolve_selections<'a, D, I>( map: &'a DisplaySnapshot, ) -> impl 'a + Iterator> where - D: TextDimension + Clone + Ord + Sub, + D: TextDimension + Ord + Sub, I: 'a + IntoIterator>, { let (to_convert, selections) = resolve_selections_display(selections, map).tee(); diff --git a/crates/editor/src/signature_help.rs b/crates/editor/src/signature_help.rs index ae886ff30e..067db54844 100644 --- a/crates/editor/src/signature_help.rs +++ b/crates/editor/src/signature_help.rs @@ -5,6 +5,7 @@ use crate::actions::ShowSignatureHelp; use crate::{Editor, EditorSettings, ToggleAutoSignatureHelp}; use gpui::{AppContext, ViewContext}; use language::markdown::parse_markdown; +use language::BufferSnapshot; use multi_buffer::{Anchor, ToOffset}; use settings::Settings; use std::ops::Range; @@ -94,13 +95,14 @@ impl Editor { (a, b) if b <= buffer_snapshot.len() => a - 1..b, (a, b) => a - 1..b - 1, }; - let not_quote_like_brackets = |start: Range, end: Range| { - let text = buffer_snapshot.text(); - let (text_start, text_end) = (text.get(start), text.get(end)); - QUOTE_PAIRS - .into_iter() - .all(|(start, end)| text_start != Some(start) && text_end != Some(end)) - }; + let not_quote_like_brackets = + |buffer: &BufferSnapshot, start: Range, end: Range| { + let text_start = buffer.text_for_range(start).collect::(); + let text_end = buffer.text_for_range(end).collect::(); + QUOTE_PAIRS + .into_iter() + .all(|(start, end)| text_start != start && text_end != end) + }; let previous_position = old_cursor_position.to_offset(&buffer_snapshot); let previous_brackets_range = bracket_range(previous_position); diff --git a/crates/editor/src/tasks.rs b/crates/editor/src/tasks.rs index 4e2cccea02..f4aa52c375 100644 --- a/crates/editor/src/tasks.rs +++ b/crates/editor/src/tasks.rs @@ -15,7 +15,7 @@ fn task_context_with_editor( }; let (selection, buffer, editor_snapshot) = { let selection = editor.selections.newest_adjusted(cx); - let Some((buffer, _, _)) = editor + let Some((buffer, _)) = editor .buffer() .read(cx) .point_to_buffer_offset(selection.start, cx) diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index 23e37a1267..c38dfa0e95 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -67,6 +67,13 @@ pub(crate) fn rust_lang() -> Arc { ("<" @open ">" @close) ("\"" @open "\"" @close) (closure_parameters "|" @open "|" @close)"#})), + text_objects: Some(Cow::from(indoc! {r#" + (function_item + body: (_ + "{" + (_)* @function.inside + "}" )) @function.around + "#})), ..Default::default() }) .expect("Could not parse queries"); diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 1cbd238e7d..b66388f3cd 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -1,6 +1,6 @@ use crate::{ - display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DiffRowHighlight, DisplayPoint, - Editor, MultiBuffer, RowExt, + display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer, + RowExt, }; use collections::BTreeMap; use futures::Future; @@ -11,7 +11,7 @@ use gpui::{ }; use itertools::Itertools; use language::{Buffer, BufferSnapshot, LanguageRegistry}; -use multi_buffer::{ExcerptRange, ToPoint}; +use multi_buffer::{ExcerptRange, MultiBufferRow}; use parking_lot::RwLock; use project::{FakeFs, Project}; use std::{ @@ -333,85 +333,8 @@ impl EditorTestContext { /// /// Diff hunks are indicated by lines starting with `+` and `-`. #[track_caller] - pub fn assert_state_with_diff(&mut self, expected_diff: String) { - let has_diff_markers = expected_diff - .lines() - .any(|line| line.starts_with("+") || line.starts_with("-")); - let expected_diff_text = expected_diff - .split('\n') - .map(|line| { - let trimmed = line.trim(); - if trimmed.is_empty() { - String::new() - } else if has_diff_markers { - line.to_string() - } else { - format!(" {line}") - } - }) - .join("\n"); - - let actual_selections = self.editor_selections(); - let actual_marked_text = - generate_marked_text(&self.buffer_text(), &actual_selections, true); - - // Read the actual diff from the editor's row highlights and block - // decorations. - let actual_diff = self.editor.update(&mut self.cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - let insertions = editor - .highlighted_rows::() - .map(|(range, _)| { - let start = range.start.to_point(&snapshot.buffer_snapshot); - let end = range.end.to_point(&snapshot.buffer_snapshot); - start.row..end.row - }) - .collect::>(); - let deletions = editor - .diff_map - .hunks - .iter() - .filter_map(|hunk| { - if hunk.blocks.is_empty() { - return None; - } - let row = hunk - .hunk_range - .start - .to_point(&snapshot.buffer_snapshot) - .row; - let (_, buffer, _) = editor - .buffer() - .read(cx) - .excerpt_containing(hunk.hunk_range.start, cx) - .expect("no excerpt for expanded buffer's hunk start"); - let buffer_id = buffer.read(cx).remote_id(); - let change_set = &editor - .diff_map - .diff_bases - .get(&buffer_id) - .expect("should have a diff base for expanded hunk") - .change_set; - let deleted_text = change_set - .read(cx) - .base_text - .as_ref() - .expect("no base text for expanded hunk") - .read(cx) - .as_rope() - .slice(hunk.diff_base_byte_range.clone()) - .to_string(); - if let DiffHunkStatus::Modified | DiffHunkStatus::Removed = hunk.status { - Some((row, deleted_text)) - } else { - None - } - }) - .collect::>(); - format_diff(actual_marked_text, deletions, insertions) - }); - - pretty_assertions::assert_eq!(actual_diff, expected_diff_text, "unexpected diff state"); + pub fn assert_state_with_diff(&mut self, expected_diff_text: String) { + assert_state_with_diff(&self.editor, &mut self.cx, &expected_diff_text); } /// Make an assertion about the editor's text and the ranges and directions @@ -504,44 +427,49 @@ impl EditorTestContext { } } -fn format_diff( - text: String, - actual_deletions: Vec<(u32, String)>, - actual_insertions: Vec>, -) -> String { - let mut diff = String::new(); - for (row, line) in text.split('\n').enumerate() { - let row = row as u32; - if row > 0 { - diff.push('\n'); - } - if let Some(text) = actual_deletions - .iter() - .find_map(|(deletion_row, deleted_text)| { - if *deletion_row == row { - Some(deleted_text) - } else { - None +#[track_caller] +pub fn assert_state_with_diff( + editor: &View, + cx: &mut VisualTestContext, + expected_diff_text: &str, +) { + let (snapshot, selections) = editor.update(cx, |editor, cx| { + ( + editor.snapshot(cx).buffer_snapshot.clone(), + editor.selections.ranges::(cx), + ) + }); + + let actual_marked_text = generate_marked_text(&snapshot.text(), &selections, true); + + // Read the actual diff. + let line_infos = snapshot.row_infos(MultiBufferRow(0)).collect::>(); + let has_diff = line_infos.iter().any(|info| info.diff_status.is_some()); + let actual_diff = actual_marked_text + .split('\n') + .zip(line_infos) + .map(|(line, info)| { + let mut marker = match info.diff_status { + Some(DiffHunkStatus::Added) => "+ ", + Some(DiffHunkStatus::Removed) => "- ", + Some(DiffHunkStatus::Modified) => unreachable!(), + None => { + if has_diff { + " " + } else { + "" + } } - }) - { - for line in text.lines() { - diff.push('-'); - if !line.is_empty() { - diff.push(' '); - diff.push_str(line); - } - diff.push('\n'); + }; + if line.is_empty() { + marker = marker.trim(); } - } - let marker = if actual_insertions.iter().any(|range| range.contains(&row)) { - "+ " - } else { - " " - }; - diff.push_str(format!("{marker}{line}").trim_end()); - } - diff + format!("{marker}{line}") + }) + .collect::>() + .join("\n"); + + pretty_assertions::assert_eq!(actual_diff, expected_diff_text, "unexpected diff state"); } impl Deref for EditorTestContext { diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index c5c620b4f4..cbd00dc62e 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -9,7 +9,7 @@ use futures::future::join_all; pub use open_path_prompt::OpenPathDelegate; use collections::HashMap; -use editor::{scroll::Autoscroll, Bias, Editor}; +use editor::Editor; use file_finder_settings::{FileFinderSettings, FileFinderWidth}; use file_icons::FileIcons; use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; @@ -1162,13 +1162,7 @@ impl PickerDelegate for FileFinderDelegate { active_editor .downgrade() .update(&mut cx, |editor, cx| { - let snapshot = editor.snapshot(cx).display_snapshot; - let point = snapshot - .buffer_snapshot - .clip_point(Point::new(row, col), Bias::Left); - editor.change_selections(Some(Autoscroll::center()), cx, |s| { - s.select_ranges([point..point]) - }); + editor.go_to_singleton_buffer_point(Point::new(row, col), cx); }) .log_err(); } diff --git a/crates/git/src/diff.rs b/crates/git/src/diff.rs index d468603663..d0fc05d82d 100644 --- a/crates/git/src/diff.rs +++ b/crates/git/src/diff.rs @@ -74,7 +74,7 @@ impl BufferDiff { } } - pub async fn build(diff_base: &str, buffer: &text::BufferSnapshot) -> Self { + pub fn build(diff_base: &str, buffer: &text::BufferSnapshot) -> Self { let mut tree = SumTree::new(buffer); let buffer_text = buffer.as_rope().to_string(); @@ -119,32 +119,38 @@ impl BufferDiff { !before_start && !after_end }); - let anchor_iter = std::iter::from_fn(move || { + let anchor_iter = iter::from_fn(move || { cursor.next(buffer); cursor.item() }) .flat_map(move |hunk| { [ - (&hunk.buffer_range.start, hunk.diff_base_byte_range.start), - (&hunk.buffer_range.end, hunk.diff_base_byte_range.end), + ( + &hunk.buffer_range.start, + (hunk.buffer_range.start, hunk.diff_base_byte_range.start), + ), + ( + &hunk.buffer_range.end, + (hunk.buffer_range.end, hunk.diff_base_byte_range.end), + ), ] - .into_iter() }); let mut summaries = buffer.summaries_for_anchors_with_payload::(anchor_iter); iter::from_fn(move || { - let (start_point, start_base) = summaries.next()?; - let (mut end_point, end_base) = summaries.next()?; + let (start_point, (start_anchor, start_base)) = summaries.next()?; + let (mut end_point, (mut end_anchor, end_base)) = summaries.next()?; if end_point.column > 0 { end_point.row += 1; end_point.column = 0; + end_anchor = buffer.anchor_before(end_point); } Some(DiffHunk { row_range: start_point.row..end_point.row, diff_base_byte_range: start_base..end_base, - buffer_range: buffer.anchor_before(start_point)..buffer.anchor_after(end_point), + buffer_range: start_anchor..end_anchor, }) }) } @@ -162,7 +168,7 @@ impl BufferDiff { !before_start && !after_end }); - std::iter::from_fn(move || { + iter::from_fn(move || { cursor.prev(buffer); let hunk = cursor.item()?; @@ -186,8 +192,8 @@ impl BufferDiff { self.tree = SumTree::new(buffer); } - pub async fn update(&mut self, diff_base: &Rope, buffer: &text::BufferSnapshot) { - *self = Self::build(&diff_base.to_string(), buffer).await; + pub fn update(&mut self, diff_base: &Rope, buffer: &text::BufferSnapshot) { + *self = Self::build(&diff_base.to_string(), buffer); } #[cfg(test)] @@ -346,7 +352,7 @@ mod tests { let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text); let mut diff = BufferDiff::new(&buffer); - smol::block_on(diff.update(&diff_base_rope, &buffer)); + diff.update(&diff_base_rope, &buffer); assert_hunks( diff.hunks(&buffer), &buffer, @@ -355,7 +361,7 @@ mod tests { ); buffer.edit([(0..0, "point five\n")]); - smol::block_on(diff.update(&diff_base_rope, &buffer)); + diff.update(&diff_base_rope, &buffer); assert_hunks( diff.hunks(&buffer), &buffer, @@ -407,7 +413,7 @@ mod tests { let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text); let mut diff = BufferDiff::new(&buffer); - smol::block_on(diff.update(&diff_base_rope, &buffer)); + diff.update(&diff_base_rope, &buffer); assert_eq!(diff.hunks(&buffer).count(), 8); assert_hunks( diff --git a/crates/go_to_line/Cargo.toml b/crates/go_to_line/Cargo.toml index 9dac0cd4ec..2e7c518e94 100644 --- a/crates/go_to_line/Cargo.toml +++ b/crates/go_to_line/Cargo.toml @@ -16,6 +16,7 @@ doctest = false anyhow.workspace = true editor.workspace = true gpui.workspace = true +language.workspace = true menu.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index 65ef247b91..ed5ed4525d 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -20,7 +20,7 @@ pub(crate) struct SelectionStats { } pub struct CursorPosition { - position: Option, + position: Option<(Point, bool)>, selected_count: SelectionStats, context: Option, workspace: WeakView, @@ -97,8 +97,11 @@ impl CursorPosition { } } } - cursor_position.position = - last_selection.map(|s| s.head().to_point(&buffer)); + cursor_position.position = last_selection.and_then(|s| { + buffer + .point_to_buffer_point(s.head().to_point(&buffer)) + .map(|(_, point, is_main_buffer)| (point, is_main_buffer)) + }); cursor_position.context = Some(editor.focus_handle(cx)); } } @@ -163,9 +166,10 @@ impl CursorPosition { impl Render for CursorPosition { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - div().when_some(self.position, |el, position| { + div().when_some(self.position, |el, (position, is_main_buffer)| { let mut text = format!( - "{}{FILE_ROW_COLUMN_DELIMITER}{}", + "{}{}{FILE_ROW_COLUMN_DELIMITER}{}", + if is_main_buffer { "" } else { "(deleted) " }, position.row + 1, position.column + 1 ); @@ -183,8 +187,12 @@ impl Render for CursorPosition { .active_item(cx) .and_then(|item| item.act_as::(cx)) { - workspace - .toggle_modal(cx, |cx| crate::GoToLine::new(editor, cx)) + if let Some((_, buffer, _)) = editor.read(cx).active_excerpt(cx) + { + workspace.toggle_modal(cx, |cx| { + crate::GoToLine::new(editor, buffer, cx) + }) + } } }); } diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index df673ef823..acbcf5ee9d 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -1,13 +1,15 @@ pub mod cursor_position; use cursor_position::LineIndicatorFormat; -use editor::{scroll::Autoscroll, Editor}; +use editor::{scroll::Autoscroll, Anchor, Editor, MultiBuffer, ToPoint}; use gpui::{ div, prelude::*, AnyWindowHandle, AppContext, DismissEvent, EventEmitter, FocusHandle, - FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, + FocusableView, Model, Render, SharedString, Styled, Subscription, View, ViewContext, + VisualContext, }; +use language::Buffer; use settings::Settings; -use text::{Bias, Point}; +use text::Point; use theme::ActiveTheme; use ui::prelude::*; use util::paths::FILE_ROW_COLUMN_DELIMITER; @@ -21,6 +23,7 @@ pub fn init(cx: &mut AppContext) { pub struct GoToLine { line_editor: View, active_editor: View, + active_buffer: Model, current_text: SharedString, prev_scroll_position: Option>, _subscriptions: Vec, @@ -42,22 +45,43 @@ impl GoToLine { let handle = cx.view().downgrade(); editor .register_action(move |_: &editor::actions::ToggleGoToLine, cx| { - let Some(editor) = handle.upgrade() else { + let Some(editor_handle) = handle.upgrade() else { return; }; - let Some(workspace) = editor.read(cx).workspace() else { + let Some(workspace) = editor_handle.read(cx).workspace() else { + return; + }; + let editor = editor_handle.read(cx); + let Some((_, buffer, _)) = editor.active_excerpt(cx) else { return; }; workspace.update(cx, |workspace, cx| { - workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx)); + workspace.toggle_modal(cx, move |cx| GoToLine::new(editor_handle, buffer, cx)); }) }) .detach(); } - pub fn new(active_editor: View, cx: &mut ViewContext) -> Self { - let cursor = - active_editor.update(cx, |editor, cx| editor.selections.last::(cx).head()); + pub fn new( + active_editor: View, + active_buffer: Model, + cx: &mut ViewContext, + ) -> Self { + let (cursor, last_line, scroll_position) = active_editor.update(cx, |editor, cx| { + let cursor = editor.selections.last::(cx).head(); + let snapshot = active_buffer.read(cx).snapshot(); + + let last_line = editor + .buffer() + .read(cx) + .excerpts_for_buffer(&active_buffer, cx) + .into_iter() + .map(move |(_, range)| text::ToPoint::to_point(&range.context.end, &snapshot).row) + .max() + .unwrap_or(0); + + (cursor, last_line, editor.scroll_position(cx)) + }); let line = cursor.row + 1; let column = cursor.column + 1; @@ -69,15 +93,17 @@ impl GoToLine { }); let line_editor_change = cx.subscribe(&line_editor, Self::on_line_editor_event); - let editor = active_editor.read(cx); - let last_line = editor.buffer().read(cx).snapshot(cx).max_point().row; - let scroll_position = active_editor.update(cx, |editor, cx| editor.scroll_position(cx)); - - let current_text = format!("{} of {} (column {})", line, last_line + 1, column); + let current_text = format!( + "Current Line: {} of {} (column {})", + line, + last_line + 1, + column + ); Self { line_editor, active_editor, + active_buffer, current_text: current_text.into(), prev_scroll_position: Some(scroll_position), _subscriptions: vec![line_editor_change, cx.on_release(Self::release)], @@ -113,35 +139,40 @@ impl GoToLine { } fn highlight_current_line(&mut self, cx: &mut ViewContext) { - if let Some(point) = self.point_from_query(cx) { - self.active_editor.update(cx, |active_editor, cx| { - let snapshot = active_editor.snapshot(cx).display_snapshot; - let start = snapshot.buffer_snapshot.clip_point(point, Bias::Left); - let end = start + Point::new(1, 0); - let start = snapshot.buffer_snapshot.anchor_before(start); - let end = snapshot.buffer_snapshot.anchor_after(end); - active_editor.clear_row_highlights::(); - active_editor.highlight_rows::( - start..end, - cx.theme().colors().editor_highlighted_line_background, - true, - cx, - ); - active_editor.request_autoscroll(Autoscroll::center(), cx); - }); - cx.notify(); - } + self.active_editor.update(cx, |editor, cx| { + editor.clear_row_highlights::(); + let multibuffer = editor.buffer().read(cx); + let snapshot = multibuffer.snapshot(cx); + let Some(start) = self.anchor_from_query(&multibuffer, cx) else { + return; + }; + let start_point = start.to_point(&snapshot); + let end_point = start_point + Point::new(1, 0); + let end = snapshot.anchor_after(end_point); + editor.highlight_rows::( + start..end, + cx.theme().colors().editor_highlighted_line_background, + true, + cx, + ); + editor.request_autoscroll(Autoscroll::center(), cx); + }); + cx.notify(); } - fn point_from_query(&self, cx: &ViewContext) -> Option { - let (row, column) = self.line_column_from_query(cx); - Some(Point::new( - row?.saturating_sub(1), - column.unwrap_or(0).saturating_sub(1), - )) + fn anchor_from_query( + &self, + multibuffer: &MultiBuffer, + cx: &ViewContext, + ) -> Option { + let (Some(row), column) = self.line_column_from_query(cx) else { + return None; + }; + let point = Point::new(row.saturating_sub(1), column.unwrap_or(0).saturating_sub(1)); + multibuffer.buffer_point_to_anchor(&self.active_buffer, point, cx) } - fn line_column_from_query(&self, cx: &ViewContext) -> (Option, Option) { + fn line_column_from_query(&self, cx: &AppContext) -> (Option, Option) { let input = self.line_editor.read(cx).text(cx); let mut components = input .splitn(2, FILE_ROW_COLUMN_DELIMITER) @@ -157,18 +188,18 @@ impl GoToLine { } fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { - if let Some(point) = self.point_from_query(cx) { - self.active_editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx).display_snapshot; - let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); - editor.change_selections(Some(Autoscroll::center()), cx, |s| { - s.select_ranges([point..point]) - }); - editor.focus(cx); - cx.notify(); + self.active_editor.update(cx, |editor, cx| { + let multibuffer = editor.buffer().read(cx); + let Some(start) = self.anchor_from_query(&multibuffer, cx) else { + return; + }; + editor.change_selections(Some(Autoscroll::center()), cx, |s| { + s.select_anchor_ranges([start..start]) }); - self.prev_scroll_position.take(); - } + editor.focus(cx); + cx.notify() + }); + self.prev_scroll_position.take(); cx.emit(DismissEvent); } @@ -205,7 +236,6 @@ impl Render for GoToLine { .px_2() .py_1() .gap_1() - .child(Label::new("Current Line:").color(Color::Muted)) .child(Label::new(help_text).color(Color::Muted)), ) } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 7ac1263dec..3a70da0788 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -6,7 +6,7 @@ pub use crate::{ }; use crate::{ diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, - language_settings::{language_settings, IndentGuideSettings, LanguageSettings}, + language_settings::{language_settings, LanguageSettings}, markdown::parse_markdown, outline::OutlineItem, syntax_map::{ @@ -144,7 +144,7 @@ struct BufferBranchState { /// An immutable, cheaply cloneable representation of a fixed /// state of a buffer. pub struct BufferSnapshot { - text: text::BufferSnapshot, + pub text: text::BufferSnapshot, pub(crate) syntax: SyntaxSnapshot, file: Option>, diagnostics: SmallVec<[(LanguageServerId, DiagnosticSet); 2]>, @@ -587,22 +587,6 @@ pub struct Runnable { pub buffer: BufferId, } -#[derive(Clone, Debug, PartialEq)] -pub struct IndentGuide { - pub buffer_id: BufferId, - pub start_row: BufferRow, - pub end_row: BufferRow, - pub depth: u32, - pub tab_size: u32, - pub settings: IndentGuideSettings, -} - -impl IndentGuide { - pub fn indent_level(&self) -> u32 { - self.depth * self.tab_size - } -} - #[derive(Clone)] pub struct EditPreview { applied_edits_snapshot: text::BufferSnapshot, @@ -937,6 +921,36 @@ impl Buffer { } } + pub fn build_snapshot( + text: Rope, + language: Option>, + language_registry: Option>, + cx: &mut AppContext, + ) -> impl Future { + let entity_id = cx.reserve_model::().entity_id(); + let buffer_id = entity_id.as_non_zero_u64().into(); + async move { + let text = + TextBuffer::new_normalized(0, buffer_id, Default::default(), text).snapshot(); + let mut syntax = SyntaxMap::new(&text).snapshot(); + if let Some(language) = language.clone() { + let text = text.clone(); + let language = language.clone(); + let language_registry = language_registry.clone(); + syntax.reparse(&text, language_registry, language); + } + BufferSnapshot { + text, + syntax, + file: None, + diagnostics: Default::default(), + remote_selections: Default::default(), + language, + non_text_state_update_count: 0, + } + } + } + /// Retrieve a snapshot of the buffer's current state. This is computationally /// cheap, and allows reading from the buffer on a background thread. pub fn snapshot(&self) -> BufferSnapshot { @@ -2633,7 +2647,8 @@ impl Buffer { last_end = Some(range.end); let new_text_len = rng.gen_range(0..10); - let new_text: String = RandomCharIter::new(&mut *rng).take(new_text_len).collect(); + let mut new_text: String = RandomCharIter::new(&mut *rng).take(new_text_len).collect(); + new_text = new_text.to_uppercase(); edits.push((range, new_text)); } @@ -3730,10 +3745,8 @@ impl BufferSnapshot { pub fn runnable_ranges( &self, - range: Range, + offset_range: Range, ) -> impl Iterator + '_ { - let offset_range = range.start.to_offset(self)..range.end.to_offset(self); - let mut syntax_matches = self.syntax.matches(offset_range, self, |grammar| { grammar.runnable_config.as_ref().map(|config| &config.query) }); @@ -3833,245 +3846,6 @@ impl BufferSnapshot { }) } - pub fn indent_guides_in_range( - &self, - range: Range, - ignore_disabled_for_language: bool, - cx: &AppContext, - ) -> Vec { - let language_settings = - language_settings(self.language().map(|l| l.name()), self.file.as_ref(), cx); - let settings = language_settings.indent_guides; - if !ignore_disabled_for_language && !settings.enabled { - return Vec::new(); - } - let tab_size = language_settings.tab_size.get() as u32; - - let start_row = range.start.to_point(self).row; - let end_row = range.end.to_point(self).row; - let row_range = start_row..end_row + 1; - - let mut row_indents = self.line_indents_in_row_range(row_range.clone()); - - let mut result_vec = Vec::new(); - let mut indent_stack = SmallVec::<[IndentGuide; 8]>::new(); - - while let Some((first_row, mut line_indent)) = row_indents.next() { - let current_depth = indent_stack.len() as u32; - - // When encountering empty, continue until found useful line indent - // then add to the indent stack with the depth found - let mut found_indent = false; - let mut last_row = first_row; - if line_indent.is_line_empty() { - let mut trailing_row = end_row; - while !found_indent { - let (target_row, new_line_indent) = - if let Some(display_row) = row_indents.next() { - display_row - } else { - // This means we reached the end of the given range and found empty lines at the end. - // We need to traverse further until we find a non-empty line to know if we need to add - // an indent guide for the last visible indent. - trailing_row += 1; - - const TRAILING_ROW_SEARCH_LIMIT: u32 = 25; - if trailing_row > self.max_point().row - || trailing_row > end_row + TRAILING_ROW_SEARCH_LIMIT - { - break; - } - let new_line_indent = self.line_indent_for_row(trailing_row); - (trailing_row, new_line_indent) - }; - - if new_line_indent.is_line_empty() { - continue; - } - last_row = target_row.min(end_row); - line_indent = new_line_indent; - found_indent = true; - break; - } - } else { - found_indent = true - } - - let depth = if found_indent { - line_indent.len(tab_size) / tab_size - + ((line_indent.len(tab_size) % tab_size) > 0) as u32 - } else { - current_depth - }; - - match depth.cmp(¤t_depth) { - Ordering::Less => { - for _ in 0..(current_depth - depth) { - let mut indent = indent_stack.pop().unwrap(); - if last_row != first_row { - // In this case, we landed on an empty row, had to seek forward, - // and discovered that the indent we where on is ending. - // This means that the last display row must - // be on line that ends this indent range, so we - // should display the range up to the first non-empty line - indent.end_row = first_row.saturating_sub(1); - } - - result_vec.push(indent) - } - } - Ordering::Greater => { - for next_depth in current_depth..depth { - indent_stack.push(IndentGuide { - buffer_id: self.remote_id(), - start_row: first_row, - end_row: last_row, - depth: next_depth, - tab_size, - settings, - }); - } - } - _ => {} - } - - for indent in indent_stack.iter_mut() { - indent.end_row = last_row; - } - } - - result_vec.extend(indent_stack); - - result_vec - } - - pub async fn enclosing_indent( - &self, - mut buffer_row: BufferRow, - ) -> Option<(Range, LineIndent)> { - let max_row = self.max_point().row; - if buffer_row >= max_row { - return None; - } - - let mut target_indent = self.line_indent_for_row(buffer_row); - - // If the current row is at the start of an indented block, we want to return this - // block as the enclosing indent. - if !target_indent.is_line_empty() && buffer_row < max_row { - let next_line_indent = self.line_indent_for_row(buffer_row + 1); - if !next_line_indent.is_line_empty() - && target_indent.raw_len() < next_line_indent.raw_len() - { - target_indent = next_line_indent; - buffer_row += 1; - } - } - - const SEARCH_ROW_LIMIT: u32 = 25000; - const SEARCH_WHITESPACE_ROW_LIMIT: u32 = 2500; - const YIELD_INTERVAL: u32 = 100; - - let mut accessed_row_counter = 0; - - // If there is a blank line at the current row, search for the next non indented lines - if target_indent.is_line_empty() { - let start = buffer_row.saturating_sub(SEARCH_WHITESPACE_ROW_LIMIT); - let end = (max_row + 1).min(buffer_row + SEARCH_WHITESPACE_ROW_LIMIT); - - let mut non_empty_line_above = None; - for (row, indent) in self - .text - .reversed_line_indents_in_row_range(start..buffer_row) - { - accessed_row_counter += 1; - if accessed_row_counter == YIELD_INTERVAL { - accessed_row_counter = 0; - yield_now().await; - } - if !indent.is_line_empty() { - non_empty_line_above = Some((row, indent)); - break; - } - } - - let mut non_empty_line_below = None; - for (row, indent) in self.text.line_indents_in_row_range((buffer_row + 1)..end) { - accessed_row_counter += 1; - if accessed_row_counter == YIELD_INTERVAL { - accessed_row_counter = 0; - yield_now().await; - } - if !indent.is_line_empty() { - non_empty_line_below = Some((row, indent)); - break; - } - } - - let (row, indent) = match (non_empty_line_above, non_empty_line_below) { - (Some((above_row, above_indent)), Some((below_row, below_indent))) => { - if above_indent.raw_len() >= below_indent.raw_len() { - (above_row, above_indent) - } else { - (below_row, below_indent) - } - } - (Some(above), None) => above, - (None, Some(below)) => below, - _ => return None, - }; - - target_indent = indent; - buffer_row = row; - } - - let start = buffer_row.saturating_sub(SEARCH_ROW_LIMIT); - let end = (max_row + 1).min(buffer_row + SEARCH_ROW_LIMIT); - - let mut start_indent = None; - for (row, indent) in self - .text - .reversed_line_indents_in_row_range(start..buffer_row) - { - accessed_row_counter += 1; - if accessed_row_counter == YIELD_INTERVAL { - accessed_row_counter = 0; - yield_now().await; - } - if !indent.is_line_empty() && indent.raw_len() < target_indent.raw_len() { - start_indent = Some((row, indent)); - break; - } - } - let (start_row, start_indent_size) = start_indent?; - - let mut end_indent = (end, None); - for (row, indent) in self.text.line_indents_in_row_range((buffer_row + 1)..end) { - accessed_row_counter += 1; - if accessed_row_counter == YIELD_INTERVAL { - accessed_row_counter = 0; - yield_now().await; - } - if !indent.is_line_empty() && indent.raw_len() < target_indent.raw_len() { - end_indent = (row.saturating_sub(1), Some(indent)); - break; - } - } - let (end_row, end_indent_size) = end_indent; - - let indent = if let Some(end_indent_size) = end_indent_size { - if start_indent_size.raw_len() > end_indent_size.raw_len() { - start_indent_size - } else { - end_indent_size - } - } else { - start_indent_size - }; - - Some((start_row..end_row, indent)) - } - /// Returns selections for remote peers intersecting the given range. #[allow(clippy::type_complexity)] pub fn selections_in_range( @@ -4395,6 +4169,10 @@ impl<'a> BufferChunks<'a> { self.range.start } + pub fn range(&self) -> Range { + self.range.clone() + } + fn update_diagnostic_depths(&mut self, endpoint: DiagnosticEndpoint) { let depth = match endpoint.severity { DiagnosticSeverity::ERROR => &mut self.error_depth, diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index e0af4998a8..d6caa5d981 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -21,7 +21,7 @@ use std::{ }; use syntax_map::TreeSitterOptions; use text::network::Network; -use text::{BufferId, LineEnding, LineIndent}; +use text::{BufferId, LineEnding}; use text::{Point, ToPoint}; use unindent::Unindent as _; use util::{assert_set_eq, post_inc, test::marked_text_ranges, RandomCharIter}; @@ -2475,92 +2475,6 @@ fn test_serialization(cx: &mut gpui::AppContext) { assert_eq!(buffer2.read(cx).text(), "abcDF"); } -#[gpui::test] -async fn test_find_matching_indent(cx: &mut TestAppContext) { - cx.update(|cx| init_settings(cx, |_| {})); - - async fn enclosing_indent( - text: impl Into, - buffer_row: u32, - cx: &mut TestAppContext, - ) -> Option<(Range, LineIndent)> { - let buffer = cx.new_model(|cx| Buffer::local(text, cx)); - let snapshot = cx.read(|cx| buffer.read(cx).snapshot()); - snapshot.enclosing_indent(buffer_row).await - } - - assert_eq!( - enclosing_indent( - " - fn b() { - if c { - let d = 2; - } - }" - .unindent(), - 1, - cx, - ) - .await, - Some(( - 1..2, - LineIndent { - tabs: 0, - spaces: 4, - line_blank: false, - } - )) - ); - - assert_eq!( - enclosing_indent( - " - fn b() { - if c { - let d = 2; - } - }" - .unindent(), - 2, - cx, - ) - .await, - Some(( - 1..2, - LineIndent { - tabs: 0, - spaces: 4, - line_blank: false, - } - )) - ); - - assert_eq!( - enclosing_indent( - " - fn b() { - if c { - let d = 2; - - let e = 5; - } - }" - .unindent(), - 3, - cx, - ) - .await, - Some(( - 1..4, - LineIndent { - tabs: 0, - spaces: 4, - line_blank: false, - } - )) - ); -} - #[gpui::test] fn test_branch_and_merge(cx: &mut TestAppContext) { cx.update(|cx| init_settings(cx, |_| {})); diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 97c29b8615..34c85d43cf 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -131,15 +131,15 @@ impl SyntaxTreeView { let snapshot = editor_state .editor .update(cx, |editor, cx| editor.snapshot(cx)); - let (excerpt, buffer, range) = editor_state.editor.update(cx, |editor, cx| { + let (buffer, range, excerpt_id) = editor_state.editor.update(cx, |editor, cx| { let selection_range = editor.selections.last::(cx).range(); let multi_buffer = editor.buffer().read(cx); - let (excerpt, range) = snapshot + let (buffer, range, excerpt_id) = snapshot .buffer_snapshot .range_to_buffer_ranges(selection_range) .pop()?; - let buffer = multi_buffer.buffer(excerpt.buffer_id()).unwrap().clone(); - Some((excerpt, buffer, range)) + let buffer = multi_buffer.buffer(buffer.remote_id()).unwrap().clone(); + Some((buffer, range, excerpt_id)) })?; // If the cursor has moved into a different excerpt, retrieve a new syntax layer @@ -148,16 +148,16 @@ impl SyntaxTreeView { .active_buffer .get_or_insert_with(|| BufferState { buffer: buffer.clone(), - excerpt_id: excerpt.id(), + excerpt_id, active_layer: None, }); let mut prev_layer = None; if did_reparse { prev_layer = buffer_state.active_layer.take(); } - if buffer_state.buffer != buffer || buffer_state.excerpt_id != excerpt.id() { + if buffer_state.buffer != buffer || buffer_state.excerpt_id != excerpt_id { buffer_state.buffer = buffer.clone(); - buffer_state.excerpt_id = excerpt.id(); + buffer_state.excerpt_id = excerpt_id; buffer_state.active_layer = None; } diff --git a/crates/multi_buffer/Cargo.toml b/crates/multi_buffer/Cargo.toml index 8a102f7f00..b8b625378d 100644 --- a/crates/multi_buffer/Cargo.toml +++ b/crates/multi_buffer/Cargo.toml @@ -27,12 +27,16 @@ collections.workspace = true ctor.workspace = true env_logger.workspace = true futures.workspace = true +git.workspace = true gpui.workspace = true itertools.workspace = true language.workspace = true log.workspace = true parking_lot.workspace = true +project.workspace = true rand.workspace = true +rope.workspace = true +smol.workspace = true settings.workspace = true serde.workspace = true smallvec.workspace = true @@ -45,7 +49,10 @@ util.workspace = true [dev-dependencies] gpui = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] } +project = { workspace = true, features = ["test-support"] } rand.workspace = true settings = { workspace = true, features = ["test-support"] } text = { workspace = true, features = ["test-support"] } util = { workspace = true, features = ["test-support"] } +pretty_assertions.workspace = true +indoc.workspace = true diff --git a/crates/multi_buffer/src/anchor.rs b/crates/multi_buffer/src/anchor.rs index 03e21b8570..423f4af31f 100644 --- a/crates/multi_buffer/src/anchor.rs +++ b/crates/multi_buffer/src/anchor.rs @@ -12,14 +12,38 @@ pub struct Anchor { pub buffer_id: Option, pub excerpt_id: ExcerptId, pub text_anchor: text::Anchor, + pub diff_base_anchor: Option, } impl Anchor { + pub fn in_buffer( + excerpt_id: ExcerptId, + buffer_id: BufferId, + text_anchor: text::Anchor, + ) -> Self { + Self { + buffer_id: Some(buffer_id), + excerpt_id, + text_anchor, + diff_base_anchor: None, + } + } + + pub fn range_in_buffer( + excerpt_id: ExcerptId, + buffer_id: BufferId, + range: Range, + ) -> Range { + Self::in_buffer(excerpt_id, buffer_id, range.start) + ..Self::in_buffer(excerpt_id, buffer_id, range.end) + } + pub fn min() -> Self { Self { buffer_id: None, excerpt_id: ExcerptId::min(), text_anchor: text::Anchor::MIN, + diff_base_anchor: None, } } @@ -28,22 +52,47 @@ impl Anchor { buffer_id: None, excerpt_id: ExcerptId::max(), text_anchor: text::Anchor::MAX, + diff_base_anchor: None, } } pub fn cmp(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Ordering { let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id, snapshot); - if excerpt_id_cmp.is_eq() { - if self.excerpt_id == ExcerptId::min() || self.excerpt_id == ExcerptId::max() { - Ordering::Equal - } else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { - self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer) - } else { - Ordering::Equal - } - } else { - excerpt_id_cmp + if excerpt_id_cmp.is_ne() { + return excerpt_id_cmp; } + if self.excerpt_id == ExcerptId::min() || self.excerpt_id == ExcerptId::max() { + return Ordering::Equal; + } + if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { + let text_cmp = self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer); + if text_cmp.is_ne() { + return text_cmp; + } + if self.diff_base_anchor.is_some() || other.diff_base_anchor.is_some() { + if let Some(diff_base) = snapshot.diffs.get(&excerpt.buffer_id) { + let self_anchor = self + .diff_base_anchor + .filter(|a| diff_base.base_text.can_resolve(a)); + let other_anchor = other + .diff_base_anchor + .filter(|a| diff_base.base_text.can_resolve(a)); + return match (self_anchor, other_anchor) { + (Some(a), Some(b)) => a.cmp(&b, &diff_base.base_text), + (Some(_), None) => match other.text_anchor.bias { + Bias::Left => Ordering::Greater, + Bias::Right => Ordering::Less, + }, + (None, Some(_)) => match self.text_anchor.bias { + Bias::Left => Ordering::Less, + Bias::Right => Ordering::Greater, + }, + (None, None) => Ordering::Equal, + }; + } + } + } + Ordering::Equal } pub fn bias(&self) -> Bias { @@ -57,6 +106,14 @@ impl Anchor { buffer_id: self.buffer_id, excerpt_id: self.excerpt_id, text_anchor: self.text_anchor.bias_left(&excerpt.buffer), + diff_base_anchor: self.diff_base_anchor.map(|a| { + if let Some(base) = snapshot.diffs.get(&excerpt.buffer_id) { + if a.buffer_id == Some(base.base_text.remote_id()) { + return a.bias_left(&base.base_text); + } + } + a + }), }; } } @@ -70,6 +127,14 @@ impl Anchor { buffer_id: self.buffer_id, excerpt_id: self.excerpt_id, text_anchor: self.text_anchor.bias_right(&excerpt.buffer), + diff_base_anchor: self.diff_base_anchor.map(|a| { + if let Some(base) = snapshot.diffs.get(&excerpt.buffer_id) { + if a.buffer_id == Some(base.base_text.remote_id()) { + return a.bias_right(&base.base_text); + } + } + a + }), }; } } diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 3e6e651870..b489d48227 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -1,27 +1,34 @@ mod anchor; #[cfg(test)] mod multi_buffer_tests; +mod position; pub use anchor::{Anchor, AnchorRangeExt, Offset}; +pub use position::{TypedOffset, TypedPoint, TypedRow}; + use anyhow::{anyhow, Result}; use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet}; use futures::{channel::mpsc, SinkExt}; +use git::diff::DiffHunkStatus; use gpui::{AppContext, EntityId, EventEmitter, Model, ModelContext, Task}; use itertools::Itertools; use language::{ - language_settings::{language_settings, LanguageSettings}, + language_settings::{language_settings, IndentGuideSettings, LanguageSettings}, AutoindentMode, Buffer, BufferChunks, BufferRow, BufferSnapshot, Capability, CharClassifier, - CharKind, Chunk, CursorShape, DiagnosticEntry, DiskState, File, IndentGuide, IndentSize, - Language, LanguageScope, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem, Point, PointUtf16, - Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, - TransactionId, Unclipped, + CharKind, Chunk, CursorShape, DiagnosticEntry, DiskState, File, IndentSize, Language, + LanguageScope, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem, Point, PointUtf16, Selection, + TextDimension, TextObject, ToOffset as _, ToPoint as _, TransactionId, TreeSitterOptions, + Unclipped, }; +use project::buffer_store::BufferChangeSet; +use rope::DimensionPair; use smallvec::SmallVec; +use smol::future::yield_now; use std::{ any::type_name, borrow::Cow, - cell::{Ref, RefCell}, + cell::{Ref, RefCell, RefMut}, cmp, fmt, future::Future, io, @@ -32,14 +39,13 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use sum_tree::{Bias, Cursor, SumTree}; +use sum_tree::{Bias, Cursor, SumTree, TreeMap}; use text::{ locator::Locator, subscription::{Subscription, Topic}, - BufferId, Edit, TextSummary, + BufferId, Edit, LineIndent, TextSummary, }; use theme::SyntaxTheme; - use util::post_inc; #[cfg(any(test, feature = "test-support"))] @@ -50,12 +56,6 @@ const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize]; #[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct ExcerptId(usize); -impl From for EntityId { - fn from(id: ExcerptId) -> Self { - EntityId::from(id.0 as u64) - } -} - /// One or more [`Buffers`](Buffer) being edited in a single view. /// /// See @@ -65,6 +65,8 @@ pub struct MultiBuffer { snapshot: RefCell, /// Contains the state of the buffers being edited buffers: RefCell>, + diff_bases: HashMap, + all_diff_hunks_expanded: bool, subscriptions: Topic, /// If true, the multi-buffer only contains a single [`Buffer`] and a single [`Excerpt`] singleton: bool, @@ -119,11 +121,27 @@ pub struct MultiBufferDiffHunk { pub buffer_id: BufferId, /// The range of the underlying buffer that this hunk corresponds to. pub buffer_range: Range, + /// The excerpt that contains the diff hunk. + pub excerpt_id: ExcerptId, /// The range within the buffer's diff base that this hunk corresponds to. pub diff_base_byte_range: Range, } +impl MultiBufferDiffHunk { + pub fn status(&self) -> DiffHunkStatus { + if self.buffer_range.start == self.buffer_range.end { + DiffHunkStatus::Removed + } else if self.diff_base_byte_range.is_empty() { + DiffHunkStatus::Added + } else { + DiffHunkStatus::Modified + } + } +} + pub type MultiBufferPoint = Point; +type ExcerptOffset = TypedOffset; +type ExcerptPoint = TypedPoint; #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq, Hash, serde::Deserialize)] #[serde(transparent)] @@ -134,6 +152,14 @@ impl MultiBufferRow { pub const MAX: Self = Self(u32::MAX); } +impl std::ops::Add for MultiBufferRow { + type Output = Self; + + fn add(self, rhs: usize) -> Self::Output { + MultiBufferRow(self.0 + rhs as u32) + } +} + #[derive(Clone)] struct History { next_transaction_id: TransactionId, @@ -176,12 +202,19 @@ struct BufferState { _subscriptions: [gpui::Subscription; 2], } +struct ChangeSetState { + change_set: Model, + _subscription: gpui::Subscription, +} + /// The contents of a [`MultiBuffer`] at a single point in time. #[derive(Clone, Default)] pub struct MultiBufferSnapshot { singleton: bool, excerpts: SumTree, excerpt_ids: SumTree, + diffs: TreeMap, + pub diff_transforms: SumTree, trailing_excerpt_update_count: usize, non_text_state_update_count: usize, edit_count: usize, @@ -191,13 +224,34 @@ pub struct MultiBufferSnapshot { show_headers: bool, } +#[derive(Debug, Clone)] +pub enum DiffTransform { + BufferContent { + summary: TextSummary, + inserted_hunk_anchor: Option<(ExcerptId, text::Anchor)>, + }, + DeletedHunk { + summary: TextSummary, + buffer_id: BufferId, + hunk_anchor: (ExcerptId, text::Anchor), + base_text_byte_range: Range, + has_trailing_newline: bool, + }, +} + +#[derive(Clone)] +struct DiffSnapshot { + diff: git::diff::BufferDiff, + base_text: language::BufferSnapshot, +} + #[derive(Clone)] pub struct ExcerptInfo { pub id: ExcerptId, pub buffer: BufferSnapshot, pub buffer_id: BufferId, pub range: ExcerptRange, - pub text_summary: TextSummary, + pub end_row: MultiBufferRow, } impl std::fmt::Debug for ExcerptInfo { @@ -230,6 +284,13 @@ impl ExcerptBoundary { } } +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub struct RowInfo { + pub buffer_row: Option, + pub multibuffer_row: Option, + pub diff_status: Option, +} + /// A slice into a [`Buffer`] that is being edited in a [`MultiBuffer`]. #[derive(Clone)] struct Excerpt { @@ -257,8 +318,11 @@ struct Excerpt { #[derive(Clone)] pub struct MultiBufferExcerpt<'a> { excerpt: &'a Excerpt, - excerpt_offset: usize, - excerpt_position: Point, + diff_transforms: + sum_tree::Cursor<'a, DiffTransform, (OutputDimension, ExcerptDimension)>, + offset: usize, + excerpt_offset: ExcerptDimension, + buffer_offset: usize, } #[derive(Clone, Debug)] @@ -287,50 +351,92 @@ pub struct ExcerptSummary { text: TextSummary, } +#[derive(Debug, Clone)] +pub struct DiffTransformSummary { + input: TextSummary, + output: TextSummary, +} + #[derive(Clone)] pub struct MultiBufferRows<'a> { - buffer_row_range: Range, - excerpts: Cursor<'a, Excerpt, Point>, + point: Point, + is_empty: bool, + cursor: MultiBufferCursor<'a, Point>, } pub struct MultiBufferChunks<'a> { + excerpts: Cursor<'a, Excerpt, ExcerptOffset>, + diff_transforms: Cursor<'a, DiffTransform, (usize, ExcerptOffset)>, + diffs: &'a TreeMap, + diff_base_chunks: Option<(BufferId, BufferChunks<'a>)>, + buffer_chunk: Option>, range: Range, - excerpts: Cursor<'a, Excerpt, usize>, + excerpt_offset_range: Range, excerpt_chunks: Option>, language_aware: bool, } +pub struct ReversedMultiBufferChunks<'a> { + cursor: MultiBufferCursor<'a, usize>, + current_chunks: Option>, + start: usize, + offset: usize, +} + pub struct MultiBufferBytes<'a> { range: Range, - excerpts: Cursor<'a, Excerpt, usize>, - excerpt_bytes: Option>, + cursor: MultiBufferCursor<'a, usize>, + excerpt_bytes: Option>, + has_trailing_newline: bool, chunk: &'a [u8], } pub struct ReversedMultiBufferBytes<'a> { range: Range, - excerpts: Cursor<'a, Excerpt, usize>, - excerpt_bytes: Option>, + chunks: ReversedMultiBufferChunks<'a>, chunk: &'a [u8], } +#[derive(Clone)] +struct MultiBufferCursor<'a, D: TextDimension> { + excerpts: Cursor<'a, Excerpt, ExcerptDimension>, + diff_transforms: Cursor<'a, DiffTransform, (OutputDimension, ExcerptDimension)>, + diffs: &'a TreeMap, + cached_region: Option>, +} + +#[derive(Clone)] +struct MultiBufferRegion<'a, D: TextDimension> { + buffer: &'a BufferSnapshot, + is_main_buffer: bool, + is_inserted_hunk: bool, + excerpt: &'a Excerpt, + buffer_range: Range, + range: Range, + has_trailing_newline: bool, +} + struct ExcerptChunks<'a> { excerpt_id: ExcerptId, content_chunks: BufferChunks<'a>, footer_height: usize, } -struct ExcerptBytes<'a> { - content_bytes: text::Bytes<'a>, - padding_height: usize, - reversed: bool, -} - +#[derive(Debug)] struct BufferEdit { range: Range, new_text: Arc, is_insertion: bool, original_indent_column: u32, + excerpt_id: ExcerptId, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +enum DiffChangeKind { + BufferEdited, + ExcerptsChanged, + DiffUpdated { base_changed: bool }, + ExpandOrCollapseHunks { expand: bool }, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -359,16 +465,18 @@ impl ExpandExcerptDirection { } #[derive(Clone, Debug, PartialEq)] -pub struct MultiBufferIndentGuide { - pub multibuffer_row_range: Range, - pub buffer: IndentGuide, +pub struct IndentGuide { + pub buffer_id: BufferId, + pub start_row: MultiBufferRow, + pub end_row: MultiBufferRow, + pub depth: u32, + pub tab_size: u32, + pub settings: IndentGuideSettings, } -impl std::ops::Deref for MultiBufferIndentGuide { - type Target = IndentGuide; - - fn deref(&self) -> &Self::Target { - &self.buffer +impl IndentGuide { + pub fn indent_level(&self) -> u32 { + self.depth * self.tab_size } } @@ -380,6 +488,8 @@ impl MultiBuffer { ..MultiBufferSnapshot::default() }), buffers: RefCell::default(), + diff_bases: HashMap::default(), + all_diff_hunks_expanded: false, subscriptions: Topic::default(), singleton: false, capability, @@ -398,6 +508,8 @@ impl MultiBuffer { Self { snapshot: Default::default(), buffers: Default::default(), + diff_bases: HashMap::default(), + all_diff_hunks_expanded: false, subscriptions: Default::default(), singleton: false, capability, @@ -429,9 +541,22 @@ impl MultiBuffer { }, ); } + let mut diff_bases = HashMap::default(); + for (buffer_id, change_set_state) in self.diff_bases.iter() { + diff_bases.insert( + *buffer_id, + ChangeSetState { + _subscription: new_cx + .observe(&change_set_state.change_set, Self::buffer_diff_changed), + change_set: change_set_state.change_set.clone(), + }, + ); + } Self { snapshot: RefCell::new(self.snapshot.borrow().clone()), buffers: RefCell::new(buffers), + diff_bases, + all_diff_hunks_expanded: self.all_diff_hunks_expanded, subscriptions: Default::default(), singleton: self.singleton, capability: self.capability, @@ -566,16 +691,6 @@ impl MultiBuffer { return; } - if let Some(buffer) = this.as_singleton() { - buffer.update(cx, |buffer, cx| { - buffer.edit(edits, autoindent_mode, cx); - }); - cx.emit(Event::ExcerptsEdited { - ids: this.excerpt_ids(), - }); - return; - } - let original_indent_columns = match &mut autoindent_mode { Some(AutoindentMode::Block { original_indent_columns, @@ -588,7 +703,7 @@ impl MultiBuffer { drop(snapshot); for (buffer_id, mut edits) in buffer_edits { - edits.sort_unstable_by_key(|edit| edit.range.start); + edits.sort_by_key(|edit| edit.range.start); this.buffers.borrow()[&buffer_id] .buffer .update(cx, |buffer, cx| { @@ -599,20 +714,26 @@ impl MultiBuffer { let empty_str: Arc = Arc::default(); while let Some(BufferEdit { mut range, - new_text, + mut new_text, mut is_insertion, original_indent_column, + excerpt_id, }) = edits.next() { while let Some(BufferEdit { range: next_range, is_insertion: next_is_insertion, + new_text: next_new_text, + excerpt_id: next_excerpt_id, .. }) = edits.peek() { if range.end >= next_range.start { range.end = cmp::max(next_range.end, range.end); is_insertion |= *next_is_insertion; + if excerpt_id == *next_excerpt_id { + new_text = format!("{new_text}{next_new_text}").into(); + } edits.next(); } else { break; @@ -671,96 +792,113 @@ impl MultiBuffer { ) -> (HashMap>, Vec) { let mut buffer_edits: HashMap> = Default::default(); let mut edited_excerpt_ids = Vec::new(); - let mut cursor = snapshot.excerpts.cursor::(&()); + let mut cursor = snapshot.cursor::(); for (ix, (range, new_text)) in edits.into_iter().enumerate() { let original_indent_column = original_indent_columns.get(ix).copied().unwrap_or(0); - cursor.seek(&range.start, Bias::Right, &()); - if cursor.item().is_none() && range.start == *cursor.start() { - cursor.prev(&()); + + cursor.seek_forward(&range.start); + let mut start_region = cursor.region().expect("start offset out of bounds"); + if !start_region.is_main_buffer { + cursor.next(); + if let Some(region) = cursor.region() { + start_region = region; + } else { + continue; + } } - let start_excerpt = cursor.item().expect("start offset out of bounds"); - let start_overshoot = range.start - cursor.start(); - let buffer_start = start_excerpt - .range - .context - .start - .to_offset(&start_excerpt.buffer) - + start_overshoot; - edited_excerpt_ids.push(start_excerpt.id); - cursor.seek(&range.end, Bias::Right, &()); - if cursor.item().is_none() && range.end == *cursor.start() { - cursor.prev(&()); + if range.end < start_region.range.start { + continue; } - let end_excerpt = cursor.item().expect("end offset out of bounds"); - let end_overshoot = range.end - cursor.start(); - let buffer_end = end_excerpt - .range - .context - .start - .to_offset(&end_excerpt.buffer) - + end_overshoot; - if start_excerpt.id == end_excerpt.id { - buffer_edits - .entry(start_excerpt.buffer_id) - .or_default() - .push(BufferEdit { - range: buffer_start..buffer_end, - new_text, - is_insertion: true, - original_indent_column, - }); - } else { - edited_excerpt_ids.push(end_excerpt.id); - let start_excerpt_range = buffer_start - ..start_excerpt - .range - .context - .end - .to_offset(&start_excerpt.buffer); - let end_excerpt_range = end_excerpt - .range - .context - .start - .to_offset(&end_excerpt.buffer) - ..buffer_end; - buffer_edits - .entry(start_excerpt.buffer_id) - .or_default() - .push(BufferEdit { - range: start_excerpt_range, - new_text: new_text.clone(), - is_insertion: true, - original_indent_column, - }); - buffer_edits - .entry(end_excerpt.buffer_id) - .or_default() - .push(BufferEdit { - range: end_excerpt_range, - new_text: new_text.clone(), - is_insertion: false, - original_indent_column, - }); + if range.end > start_region.range.end { + cursor.seek_forward(&range.end); + } + let mut end_region = cursor.region().expect("end offset out of bounds"); + if !end_region.is_main_buffer { + cursor.prev(); + if let Some(region) = cursor.region() { + end_region = region; + } else { + continue; + } + } - cursor.seek(&range.start, Bias::Right, &()); - cursor.next(&()); - while let Some(excerpt) = cursor.item() { - if excerpt.id == end_excerpt.id { - break; - } + if range.start > end_region.range.end { + continue; + } + + let start_overshoot = range.start.saturating_sub(start_region.range.start); + let end_overshoot = range.end.saturating_sub(end_region.range.start); + let buffer_start = (start_region.buffer_range.start + start_overshoot) + .min(start_region.buffer_range.end); + let buffer_end = + (end_region.buffer_range.start + end_overshoot).min(end_region.buffer_range.end); + + if start_region.excerpt.id == end_region.excerpt.id { + if start_region.is_main_buffer { + edited_excerpt_ids.push(start_region.excerpt.id); buffer_edits - .entry(excerpt.buffer_id) + .entry(start_region.buffer.remote_id()) .or_default() .push(BufferEdit { - range: excerpt.range.context.to_offset(&excerpt.buffer), + range: buffer_start..buffer_end, + new_text, + is_insertion: true, + original_indent_column, + excerpt_id: start_region.excerpt.id, + }); + } + } else { + let start_excerpt_range = buffer_start..start_region.buffer_range.end; + let end_excerpt_range = end_region.buffer_range.start..buffer_end; + if start_region.is_main_buffer { + edited_excerpt_ids.push(start_region.excerpt.id); + buffer_edits + .entry(start_region.buffer.remote_id()) + .or_default() + .push(BufferEdit { + range: start_excerpt_range, + new_text: new_text.clone(), + is_insertion: true, + original_indent_column, + excerpt_id: start_region.excerpt.id, + }); + } + if end_region.is_main_buffer { + edited_excerpt_ids.push(end_region.excerpt.id); + buffer_edits + .entry(end_region.buffer.remote_id()) + .or_default() + .push(BufferEdit { + range: end_excerpt_range, new_text: new_text.clone(), is_insertion: false, original_indent_column, + excerpt_id: end_region.excerpt.id, }); - edited_excerpt_ids.push(excerpt.id); - cursor.next(&()); + } + + cursor.seek(&range.start); + cursor.next_excerpt(); + while let Some(region) = cursor.region() { + if region.excerpt.id == end_region.excerpt.id { + break; + } + if region.is_main_buffer { + edited_excerpt_ids.push(region.excerpt.id); + buffer_edits + .entry(region.buffer.remote_id()) + .or_default() + .push(BufferEdit { + range: region.buffer_range, + new_text: new_text.clone(), + is_insertion: false, + original_indent_column, + excerpt_id: region.excerpt.id, + }); + } + cursor.next_excerpt(); } } } @@ -797,16 +935,6 @@ impl MultiBuffer { return; } - if let Some(buffer) = this.as_singleton() { - buffer.update(cx, |buffer, cx| { - buffer.autoindent_ranges(edits.into_iter().map(|e| e.0), cx); - }); - cx.emit(Event::ExcerptsEdited { - ids: this.excerpt_ids(), - }); - return; - } - let (buffer_edits, edited_excerpt_ids) = this.convert_edits_to_buffer_edits(edits, &snapshot, &[]); drop(snapshot); @@ -849,20 +977,13 @@ impl MultiBuffer { cx: &mut ModelContext, ) -> Point { let multibuffer_point = position.to_point(&self.read(cx)); - if let Some(buffer) = self.as_singleton() { - buffer.update(cx, |buffer, cx| { - buffer.insert_empty_line(multibuffer_point, space_above, space_below, cx) - }) - } else { - let (buffer, buffer_point, _) = - self.point_to_buffer_point(multibuffer_point, cx).unwrap(); - self.start_transaction(cx); - let empty_line_start = buffer.update(cx, |buffer, cx| { - buffer.insert_empty_line(buffer_point, space_above, space_below, cx) - }); - self.end_transaction(cx); - multibuffer_point + (empty_line_start - buffer_point) - } + let (buffer, buffer_point, _) = self.point_to_buffer_point(multibuffer_point, cx).unwrap(); + self.start_transaction(cx); + let empty_line_start = buffer.update(cx, |buffer, cx| { + buffer.insert_empty_line(buffer_point, space_above, space_below, cx) + }); + self.end_transaction(cx); + multibuffer_point + (empty_line_start - buffer_point) } pub fn start_transaction(&mut self, cx: &mut ModelContext) -> Option { @@ -922,13 +1043,6 @@ impl MultiBuffer { where D: TextDimension + Ord + Sub, { - if let Some(buffer) = self.as_singleton() { - return buffer - .read(cx) - .edited_ranges_for_transaction_id(transaction_id) - .collect::>(); - } - let Some(transaction) = self.history.transaction(transaction_id) else { return Vec::new(); }; @@ -952,14 +1066,14 @@ impl MultiBuffer { let excerpt_buffer_start = excerpt.range.context.start.summary::(buffer); let excerpt_buffer_end = excerpt.range.context.end.summary::(buffer); - let excerpt_range = excerpt_buffer_start.clone()..excerpt_buffer_end; + let excerpt_range = excerpt_buffer_start..excerpt_buffer_end; if excerpt_range.contains(&range.start) && excerpt_range.contains(&range.end) { let excerpt_start = D::from_text_summary(&cursor.start().text); - let mut start = excerpt_start.clone(); - start.add_assign(&(range.start - excerpt_buffer_start.clone())); + let mut start = excerpt_start; + start.add_assign(&(range.start - excerpt_buffer_start)); let mut end = excerpt_start; end.add_assign(&(range.end - excerpt_buffer_start)); @@ -972,7 +1086,7 @@ impl MultiBuffer { } } - ranges.sort_by_key(|range| range.start.clone()); + ranges.sort_by_key(|range| range.start); ranges } @@ -1258,11 +1372,13 @@ impl MultiBuffer { buffer_id: Some(buffer_id), excerpt_id, text_anchor: buffer_snapshot.anchor_after(range.start), + diff_base_anchor: None, }; let end = Anchor { buffer_id: Some(buffer_id), excerpt_id, text_anchor: buffer_snapshot.anchor_after(range.end), + diff_base_anchor: None, }; start..end })) @@ -1339,11 +1455,13 @@ impl MultiBuffer { buffer_id: Some(buffer_id), excerpt_id, text_anchor: range.start, + diff_base_anchor: None, }; let end = Anchor { buffer_id: Some(buffer_id), excerpt_id, text_anchor: range.end, + diff_base_anchor: None, }; multi_buffer_ranges.push(start..end); } @@ -1425,7 +1543,7 @@ impl MultiBuffer { let mut new_excerpts = cursor.slice(&prev_locator, Bias::Right, &()); prev_locator = cursor.start().unwrap_or(Locator::min_ref()).clone(); - let edit_start = new_excerpts.summary().text.len; + let edit_start = ExcerptOffset::new(new_excerpts.summary().text.len); new_excerpts.update_last( |excerpt| { excerpt.has_trailing_newline = true; @@ -1471,7 +1589,7 @@ impl MultiBuffer { new_excerpt_ids.push(ExcerptIdMapping { id, locator }, &()); } - let edit_end = new_excerpts.summary().text.len; + let edit_end = ExcerptOffset::new(new_excerpts.summary().text.len); let suffix = cursor.suffix(&()); let changed_trailing_excerpt = suffix.is_empty(); @@ -1483,10 +1601,14 @@ impl MultiBuffer { snapshot.trailing_excerpt_update_count += 1; } - self.subscriptions.publish_mut([Edit { - old: edit_start..edit_start, - new: edit_start..edit_end, - }]); + self.sync_diff_transforms( + snapshot, + vec![Edit { + old: edit_start..edit_start, + new: edit_start..edit_end, + }], + DiffChangeKind::ExcerptsChanged, + ); cx.emit(Event::Edited { singleton_buffer_edited: false, edited_buffer: None, @@ -1504,17 +1626,22 @@ impl MultiBuffer { let ids = self.excerpt_ids(); self.buffers.borrow_mut().clear(); let mut snapshot = self.snapshot.borrow_mut(); - let prev_len = snapshot.len(); + let start = ExcerptOffset::new(0); + let prev_len = ExcerptOffset::new(snapshot.excerpts.summary().text.len); snapshot.excerpts = Default::default(); snapshot.trailing_excerpt_update_count += 1; snapshot.is_dirty = false; snapshot.has_deleted_file = false; snapshot.has_conflict = false; - self.subscriptions.publish_mut([Edit { - old: 0..prev_len, - new: 0..0, - }]); + self.sync_diff_transforms( + snapshot, + vec![Edit { + old: start..prev_len, + new: start..start, + }], + DiffChangeKind::ExcerptsChanged, + ); cx.emit(Event::Edited { singleton_buffer_edited: false, edited_buffer: None, @@ -1556,24 +1683,39 @@ impl MultiBuffer { ) -> Vec> { let snapshot = self.read(cx); let buffers = self.buffers.borrow(); - let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, Point)>(&()); - buffers + let mut excerpts = snapshot + .excerpts + .cursor::<(Option<&Locator>, ExcerptDimension)>(&()); + let mut diff_transforms = snapshot + .diff_transforms + .cursor::<(ExcerptDimension, OutputDimension)>(&()); + diff_transforms.next(&()); + let locators = buffers .get(&buffer_id) .into_iter() - .flat_map(|state| &state.excerpts) - .filter_map(move |locator| { - cursor.seek_forward(&Some(locator), Bias::Left, &()); - cursor.item().and_then(|excerpt| { - if excerpt.locator == *locator { - let excerpt_start = cursor.start().1; - let excerpt_end = excerpt_start + excerpt.text_summary.lines; - Some(excerpt_start..excerpt_end) - } else { - None - } - }) - }) - .collect() + .flat_map(|state| &state.excerpts); + let mut result = Vec::new(); + for locator in locators { + excerpts.seek_forward(&Some(locator), Bias::Left, &()); + if let Some(excerpt) = excerpts.item() { + if excerpt.locator == *locator { + let excerpt_start = excerpts.start().1.clone(); + let excerpt_end = + ExcerptDimension(excerpt_start.0 + excerpt.text_summary.lines); + + diff_transforms.seek_forward(&excerpt_start, Bias::Left, &()); + let overshoot = excerpt_start.0 - diff_transforms.start().0 .0; + let start = diff_transforms.start().1 .0 + overshoot; + + diff_transforms.seek_forward(&excerpt_end, Bias::Right, &()); + let overshoot = excerpt_end.0 - diff_transforms.start().0 .0; + let end = diff_transforms.start().1 .0 + overshoot; + + result.push(start..end) + } + } + } + result } pub fn excerpt_buffer_ids(&self) -> Vec { @@ -1600,12 +1742,12 @@ impl MultiBuffer { cx: &AppContext, ) -> Option<(ExcerptId, Model, Range)> { let snapshot = self.read(cx); - let position = position.to_offset(&snapshot); + let offset = position.to_offset(&snapshot); - let mut cursor = snapshot.excerpts.cursor::(&()); - cursor.seek(&position, Bias::Right, &()); + let mut cursor = snapshot.cursor::(); + cursor.seek(&offset); cursor - .item() + .excerpt() .or_else(|| snapshot.excerpts.last()) .map(|excerpt| { ( @@ -1626,22 +1768,17 @@ impl MultiBuffer { &self, point: T, cx: &AppContext, - ) -> Option<(Model, usize, ExcerptId)> { + ) -> Option<(Model, usize)> { let snapshot = self.read(cx); - let offset = point.to_offset(&snapshot); - let mut cursor = snapshot.excerpts.cursor::(&()); - cursor.seek(&offset, Bias::Right, &()); - if cursor.item().is_none() { - cursor.prev(&()); - } - - cursor.item().map(|excerpt| { - let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let buffer_point = excerpt_start + offset - *cursor.start(); - let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone(); - - (buffer, buffer_point, excerpt.id) - }) + let (buffer, offset) = snapshot.point_to_buffer_offset(point)?; + Some(( + self.buffers + .borrow() + .get(&buffer.remote_id())? + .buffer + .clone(), + offset, + )) } // If point is at the end of the buffer, the last excerpt is returned @@ -1652,18 +1789,49 @@ impl MultiBuffer { ) -> Option<(Model, Point, ExcerptId)> { let snapshot = self.read(cx); let point = point.to_point(&snapshot); - let mut cursor = snapshot.excerpts.cursor::(&()); - cursor.seek(&point, Bias::Right, &()); - if cursor.item().is_none() { - cursor.prev(&()); + let mut cursor = snapshot.cursor::(); + cursor.seek(&point); + + cursor.region().and_then(|region| { + if !region.is_main_buffer { + return None; + } + + let overshoot = point - region.range.start; + let buffer_point = region.buffer_range.start + overshoot; + let buffer = self.buffers.borrow()[®ion.buffer.remote_id()] + .buffer + .clone(); + Some((buffer, buffer_point, region.excerpt.id)) + }) + } + + pub fn buffer_point_to_anchor( + &self, + buffer: &Model, + point: Point, + cx: &AppContext, + ) -> Option { + let mut found = None; + let snapshot = buffer.read(cx).snapshot(); + for (excerpt_id, range) in self.excerpts_for_buffer(buffer, cx) { + let start = range.context.start.to_point(&snapshot); + let end = range.context.end.to_point(&snapshot); + if start <= point && point < end { + found = Some((snapshot.clip_point(point, Bias::Left), excerpt_id)); + break; + } + if point < start { + found = Some((start, excerpt_id)); + } + if point > end { + found = Some((end, excerpt_id)); + } } - cursor.item().map(|excerpt| { - let excerpt_start = excerpt.range.context.start.to_point(&excerpt.buffer); - let buffer_point = excerpt_start + point - *cursor.start(); - let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone(); - - (buffer, buffer_point, excerpt.id) + found.map(|(point, excerpt_id)| { + let text_anchor = snapshot.anchor_after(point); + Anchor::in_buffer(excerpt_id, snapshot.remote_id(), text_anchor) }) } @@ -1681,7 +1849,9 @@ impl MultiBuffer { let mut buffers = self.buffers.borrow_mut(); let mut snapshot = self.snapshot.borrow_mut(); let mut new_excerpts = SumTree::default(); - let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>(&()); + let mut cursor = snapshot + .excerpts + .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); let mut edits = Vec::new(); let mut excerpt_ids = ids.iter().copied().peekable(); @@ -1723,14 +1893,14 @@ impl MultiBuffer { // When removing the last excerpt, remove the trailing newline from // the previous excerpt. - if cursor.item().is_none() && old_start > 0 { - old_start -= 1; + if cursor.item().is_none() && old_start.value > 0 { + old_start.value -= 1; new_excerpts.update_last(|e| e.has_trailing_newline = false, &()); } // Push an edit for the removal of this run of excerpts. let old_end = cursor.start().1; - let new_start = new_excerpts.summary().text.len; + let new_start = ExcerptOffset::new(new_excerpts.summary().text.len); edits.push(Edit { old: old_start..old_end, new: new_start..new_start, @@ -1747,7 +1917,7 @@ impl MultiBuffer { snapshot.trailing_excerpt_update_count += 1; } - self.subscriptions.publish_mut(edits); + self.sync_diff_transforms(snapshot, edits, DiffChangeKind::ExcerptsChanged); cx.emit(Event::Edited { singleton_buffer_edited: false, edited_buffer: None, @@ -1832,11 +2002,80 @@ impl MultiBuffer { self.capability = buffer.read(cx).capability(); Event::CapabilityChanged } - // language::BufferEvent::Operation { .. } => return, }); } + fn buffer_diff_changed( + &mut self, + change_set: Model, + cx: &mut ModelContext, + ) { + let change_set = change_set.read(cx); + let buffer_id = change_set.buffer_id; + let diff = change_set.diff_to_buffer.clone(); + let base_text = change_set.base_text.clone(); + self.sync(cx); + let mut snapshot = self.snapshot.borrow_mut(); + let base_text_version_changed = + snapshot + .diffs + .get(&buffer_id) + .map_or(true, |diff_snapshot| { + change_set.base_text.as_ref().map_or(true, |base_text| { + base_text.remote_id() != diff_snapshot.base_text.remote_id() + }) + }); + + if let Some(base_text) = base_text { + snapshot.diffs.insert( + buffer_id, + DiffSnapshot { + diff: diff.clone(), + base_text, + }, + ); + } else { + snapshot.diffs.remove(&buffer_id); + } + + let mut excerpt_edits = Vec::new(); + for locator in self + .buffers + .borrow() + .get(&buffer_id) + .map(|state| &state.excerpts) + .into_iter() + .flatten() + { + let mut cursor = snapshot + .excerpts + .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); + cursor.seek_forward(&Some(locator), Bias::Left, &()); + if let Some(excerpt) = cursor.item() { + if excerpt.locator == *locator { + let excerpt_range = cursor.start().1..cursor.end(&()).1; + excerpt_edits.push(Edit { + old: excerpt_range.clone(), + new: excerpt_range.clone(), + }); + } + } + } + + self.sync_diff_transforms( + snapshot, + excerpt_edits, + DiffChangeKind::DiffUpdated { + base_changed: base_text_version_changed, + }, + ); + cx.emit(Event::Edited { + singleton_buffer_edited: false, + edited_buffer: None, + }); + } + pub fn all_buffers(&self) -> HashSet> { self.buffers .borrow() @@ -1854,7 +2093,7 @@ impl MultiBuffer { pub fn language_at(&self, point: T, cx: &AppContext) -> Option> { self.point_to_buffer_offset(point, cx) - .and_then(|(buffer, offset, _)| buffer.read(cx).language_at(offset)) + .and_then(|(buffer, offset)| buffer.read(cx).language_at(offset)) } pub fn settings_at<'a, T: ToOffset>( @@ -1864,7 +2103,7 @@ impl MultiBuffer { ) -> Cow<'a, LanguageSettings> { let mut language = None; let mut file = None; - if let Some((buffer, offset, _)) = self.point_to_buffer_offset(point, cx) { + if let Some((buffer, offset)) = self.point_to_buffer_offset(point, cx) { let buffer = buffer.read(cx); language = buffer.language_at(offset); file = buffer.file(); @@ -1920,6 +2159,142 @@ impl MultiBuffer { self.as_singleton().unwrap().read(cx).is_parsing() } + pub fn add_change_set( + &mut self, + change_set: Model, + cx: &mut ModelContext, + ) { + let buffer_id = change_set.read(cx).buffer_id; + self.buffer_diff_changed(change_set.clone(), cx); + self.diff_bases.insert( + buffer_id, + ChangeSetState { + _subscription: cx.observe(&change_set, Self::buffer_diff_changed), + change_set, + }, + ); + } + + pub fn change_set_for(&self, buffer_id: BufferId) -> Option> { + self.diff_bases + .get(&buffer_id) + .map(|state| state.change_set.clone()) + } + + pub fn expand_diff_hunks(&mut self, ranges: Vec>, cx: &mut ModelContext) { + self.expand_or_collapse_diff_hunks(ranges, true, cx); + } + + pub fn collapse_diff_hunks(&mut self, ranges: Vec>, cx: &mut ModelContext) { + self.expand_or_collapse_diff_hunks(ranges, false, cx); + } + + pub fn set_all_diff_hunks_expanded(&mut self, cx: &mut ModelContext) { + self.all_diff_hunks_expanded = true; + self.expand_or_collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], true, cx); + } + + pub fn all_diff_hunks_expanded(&mut self) -> bool { + self.all_diff_hunks_expanded + } + + pub fn has_multiple_hunks(&self, cx: &AppContext) -> bool { + self.read(cx) + .diff_hunks_in_range(Anchor::min()..Anchor::max()) + .nth(1) + .is_some() + } + + pub fn has_expanded_diff_hunks_in_ranges( + &self, + ranges: &[Range], + cx: &AppContext, + ) -> bool { + let snapshot = self.read(cx); + let mut cursor = snapshot.diff_transforms.cursor::(&()); + for range in ranges { + let range = range.to_point(&snapshot); + let start = snapshot.point_to_offset(Point::new(range.start.row, 0)); + let end = snapshot.point_to_offset(Point::new(range.end.row + 1, 0)); + let start = start.saturating_sub(1); + let end = snapshot.len().min(end + 1); + cursor.seek(&start, Bias::Right, &()); + while let Some(item) = cursor.item() { + if *cursor.start() >= end { + break; + } + if item.hunk_anchor().is_some() { + return true; + } + cursor.next(&()); + } + } + false + } + + fn expand_or_collapse_diff_hunks( + &mut self, + ranges: Vec>, + expand: bool, + cx: &mut ModelContext, + ) { + self.sync(cx); + let snapshot = self.snapshot.borrow_mut(); + let mut excerpt_edits = Vec::new(); + for range in ranges.iter() { + let range = range.to_point(&snapshot); + + let mut start = snapshot.anchor_before(Point::new(range.start.row, 0)); + let mut end = snapshot.anchor_before(Point::new( + range.end.row, + snapshot.line_len(MultiBufferRow(range.end.row)), + )); + let peek_end = if range.end.row < snapshot.max_row().0 { + Point::new(range.end.row + 1, 0) + } else { + range.end + }; + + for diff_hunk in snapshot.diff_hunks_in_range(range.start..peek_end) { + if diff_hunk.row_range.start.0 <= range.start.row + && diff_hunk.row_range.end.0 >= range.start.row + && diff_hunk.excerpt_id == start.excerpt_id + { + start = Anchor::in_buffer( + diff_hunk.excerpt_id, + diff_hunk.buffer_id, + diff_hunk.buffer_range.start, + ); + } + if diff_hunk.row_range.start.0 == peek_end.row + && diff_hunk.excerpt_id == end.excerpt_id + { + end = Anchor::in_buffer( + diff_hunk.excerpt_id, + diff_hunk.buffer_id, + diff_hunk.buffer_range.end, + ); + } + } + let start = snapshot.excerpt_offset_for_anchor(&start); + let end = snapshot.excerpt_offset_for_anchor(&end); + excerpt_edits.push(text::Edit { + old: start..end, + new: start..end, + }); + } + + self.sync_diff_transforms( + snapshot, + excerpt_edits, + DiffChangeKind::ExpandOrCollapseHunks { expand }, + ); + cx.emit(Event::Edited { + singleton_buffer_edited: false, + edited_buffer: None, + }); + } + pub fn resize_excerpt( &mut self, id: ExcerptId, @@ -1928,17 +2303,19 @@ impl MultiBuffer { ) { self.sync(cx); - let snapshot = self.snapshot(cx); + let mut snapshot = self.snapshot.borrow_mut(); let locator = snapshot.excerpt_locator_for_id(id); let mut new_excerpts = SumTree::default(); - let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>(&()); - let mut edits = Vec::>::new(); + let mut cursor = snapshot + .excerpts + .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); + let mut edits = Vec::>::new(); let prefix = cursor.slice(&Some(locator), Bias::Left, &()); new_excerpts.append(prefix, &()); let mut excerpt = cursor.item().unwrap().clone(); - let old_text_len = excerpt.text_summary.len; + let old_text_len = ExcerptOffset::new(excerpt.text_summary.len); excerpt.range.context.start = range.start; excerpt.range.context.end = range.end; @@ -1948,11 +2325,12 @@ impl MultiBuffer { .buffer .text_summary_for_range(excerpt.range.context.clone()); - let new_start_offset = new_excerpts.summary().text.len; + let new_start_offset = ExcerptOffset::new(new_excerpts.summary().text.len); let old_start_offset = cursor.start().1; + let new_text_len = ExcerptOffset::new(excerpt.text_summary.len); let edit = Edit { old: old_start_offset..old_start_offset + old_text_len, - new: new_start_offset..new_start_offset + excerpt.text_summary.len, + new: new_start_offset..new_start_offset + new_text_len, }; if let Some(last_edit) = edits.last_mut() { @@ -1973,9 +2351,9 @@ impl MultiBuffer { new_excerpts.append(cursor.suffix(&()), &()); drop(cursor); - self.snapshot.borrow_mut().excerpts = new_excerpts; + snapshot.excerpts = new_excerpts; - self.subscriptions.publish_mut(edits); + self.sync_diff_transforms(snapshot, edits, DiffChangeKind::ExcerptsChanged); cx.emit(Event::Edited { singleton_buffer_edited: false, edited_buffer: None, @@ -1995,20 +2373,22 @@ impl MultiBuffer { return; } self.sync(cx); + let mut snapshot = self.snapshot.borrow_mut(); let ids = ids.into_iter().collect::>(); - let snapshot = self.snapshot(cx); let locators = snapshot.excerpt_locators_for_ids(ids.iter().copied()); let mut new_excerpts = SumTree::default(); - let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>(&()); - let mut edits = Vec::>::new(); + let mut cursor = snapshot + .excerpts + .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); + let mut edits = Vec::>::new(); for locator in &locators { let prefix = cursor.slice(&Some(locator), Bias::Left, &()); new_excerpts.append(prefix, &()); let mut excerpt = cursor.item().unwrap().clone(); - let old_text_len = excerpt.text_summary.len; + let old_text_len = ExcerptOffset::new(excerpt.text_summary.len); let up_line_count = if direction.should_expand_up() { line_count @@ -2045,11 +2425,12 @@ impl MultiBuffer { .buffer .text_summary_for_range(excerpt.range.context.clone()); - let new_start_offset = new_excerpts.summary().text.len; + let new_start_offset = ExcerptOffset::new(new_excerpts.summary().text.len); let old_start_offset = cursor.start().1; + let new_text_len = ExcerptOffset::new(excerpt.text_summary.len); let edit = Edit { old: old_start_offset..old_start_offset + old_text_len, - new: new_start_offset..new_start_offset + excerpt.text_summary.len, + new: new_start_offset..new_start_offset + new_text_len, }; if let Some(last_edit) = edits.last_mut() { @@ -2071,9 +2452,9 @@ impl MultiBuffer { new_excerpts.append(cursor.suffix(&()), &()); drop(cursor); - self.snapshot.borrow_mut().excerpts = new_excerpts; + snapshot.excerpts = new_excerpts; - self.subscriptions.publish_mut(edits); + self.sync_diff_transforms(snapshot, edits, DiffChangeKind::ExcerptsChanged); cx.emit(Event::Edited { singleton_buffer_edited: false, edited_buffer: None, @@ -2132,7 +2513,9 @@ impl MultiBuffer { let mut edits = Vec::new(); let mut new_excerpts = SumTree::default(); - let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>(&()); + let mut cursor = snapshot + .excerpts + .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); for (locator, buffer, buffer_edited) in excerpts_to_edit { new_excerpts.append(cursor.slice(&Some(locator), Bias::Left, &()), &()); @@ -2148,14 +2531,18 @@ impl MultiBuffer { old_excerpt.buffer.version(), old_excerpt.range.context.clone(), ) - .map(|mut edit| { + .map(|edit| { let excerpt_old_start = cursor.start().1; - let excerpt_new_start = new_excerpts.summary().text.len; - edit.old.start += excerpt_old_start; - edit.old.end += excerpt_old_start; - edit.new.start += excerpt_new_start; - edit.new.end += excerpt_new_start; - edit + let excerpt_new_start = + ExcerptOffset::new(new_excerpts.summary().text.len); + let old_start = excerpt_old_start + ExcerptOffset::new(edit.old.start); + let old_end = excerpt_old_start + ExcerptOffset::new(edit.old.end); + let new_start = excerpt_new_start + ExcerptOffset::new(edit.new.start); + let new_end = excerpt_new_start + ExcerptOffset::new(edit.new.end); + Edit { + old: old_start..old_end, + new: new_start..new_end, + } }), ); @@ -2180,7 +2567,471 @@ impl MultiBuffer { drop(cursor); snapshot.excerpts = new_excerpts; - self.subscriptions.publish(edits); + self.sync_diff_transforms(snapshot, edits, DiffChangeKind::BufferEdited); + } + + fn sync_diff_transforms( + &self, + mut snapshot: RefMut, + excerpt_edits: Vec>, + change_kind: DiffChangeKind, + ) { + if excerpt_edits.is_empty() { + return; + } + + let mut excerpts = snapshot.excerpts.cursor::(&()); + let mut old_diff_transforms = snapshot + .diff_transforms + .cursor::<(ExcerptOffset, usize)>(&()); + let mut new_diff_transforms = SumTree::default(); + let mut old_expanded_hunks = HashSet::default(); + let mut output_edits = Vec::new(); + let mut output_delta = 0_isize; + 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() { + excerpts.seek_forward(&edit.new.start, Bias::Right, &()); + if excerpts.item().is_none() && *excerpts.start() == edit.new.start { + excerpts.prev(&()); + } + + // Keep any transforms that are before the edit. + if at_transform_boundary { + at_transform_boundary = false; + let transforms_before_edit = + old_diff_transforms.slice(&edit.old.start, Bias::Left, &()); + self.append_diff_transforms(&mut new_diff_transforms, transforms_before_edit); + if let Some(transform) = old_diff_transforms.item() { + if old_diff_transforms.end(&()).0 == edit.old.start + && old_diff_transforms.start().0 < edit.old.start + { + self.push_diff_transform(&mut new_diff_transforms, transform.clone()); + old_diff_transforms.next(&()); + } + } + } + + // Compute the start of the edit in output coordinates. + let edit_start_overshoot = (edit.old.start - old_diff_transforms.start().0).value; + let edit_old_start = old_diff_transforms.start().1 + edit_start_overshoot; + let edit_new_start = (edit_old_start as isize + output_delta) as usize; + + if change_kind == DiffChangeKind::BufferEdited { + self.interpolate_diff_transforms_for_edit( + &edit, + &excerpts, + &mut old_diff_transforms, + &mut new_diff_transforms, + &mut end_of_current_insert, + ); + } else { + self.recompute_diff_transforms_for_edit( + &edit, + &mut excerpts, + &mut old_diff_transforms, + &mut new_diff_transforms, + &mut end_of_current_insert, + &mut old_expanded_hunks, + &snapshot, + change_kind, + ); + } + + self.push_buffer_content_transform( + &snapshot, + &mut new_diff_transforms, + edit.new.end, + end_of_current_insert, + ); + + // Compute the end of the edit in output coordinates. + let edit_end_overshoot = (edit.old.end - old_diff_transforms.start().0).value; + let edit_old_end = old_diff_transforms.start().1 + edit_end_overshoot; + let edit_new_end = new_diff_transforms.summary().output.len; + let output_edit = Edit { + old: edit_old_start..edit_old_end, + new: edit_new_start..edit_new_end, + }; + + output_delta += (output_edit.new.end - output_edit.new.start) as isize + - (output_edit.old.end - output_edit.old.start) as isize; + output_edits.push(output_edit); + + // If this is the last edit that intersects the current diff transform, + // then preserve a suffix of the this diff transform. + if excerpt_edits.peek().map_or(true, |next_edit| { + next_edit.old.start >= old_diff_transforms.end(&()).0 + }) { + if old_diff_transforms.start().0 < edit.old.end { + let suffix = old_diff_transforms.end(&()).0 - edit.old.end; + let transform_end = new_diff_transforms.summary().excerpt_len() + suffix; + self.push_buffer_content_transform( + &snapshot, + &mut new_diff_transforms, + transform_end, + end_of_current_insert, + ); + old_diff_transforms.next(&()); + } + at_transform_boundary = true; + } + } + + // Keep any transforms that are after the last edit. + self.append_diff_transforms(&mut new_diff_transforms, old_diff_transforms.suffix(&())); + + // Ensure there's always at least one buffer content transform. + if new_diff_transforms.is_empty() { + new_diff_transforms.push( + DiffTransform::BufferContent { + summary: Default::default(), + inserted_hunk_anchor: None, + }, + &(), + ); + } + + self.subscriptions.publish(output_edits); + drop(old_diff_transforms); + drop(excerpts); + snapshot.diff_transforms = new_diff_transforms; + snapshot.edit_count += 1; + + #[cfg(any(test, feature = "test-support"))] + snapshot.check_invariants(); + } + + #[allow(clippy::too_many_arguments)] + fn recompute_diff_transforms_for_edit( + &self, + edit: &Edit>, + excerpts: &mut Cursor>, + old_diff_transforms: &mut Cursor, usize)>, + new_diff_transforms: &mut SumTree, + end_of_current_insert: &mut Option<(TypedOffset, ExcerptId, text::Anchor)>, + old_expanded_hunks: &mut HashSet<(ExcerptId, text::Anchor)>, + snapshot: &MultiBufferSnapshot, + change_kind: DiffChangeKind, + ) { + log::trace!( + "recomputing diff transform for edit {:?} => {:?}", + edit.old.start.value..edit.old.end.value, + edit.new.start.value..edit.new.end.value + ); + + // Record which hunks were previously expanded. + old_expanded_hunks.clear(); + while let Some(item) = old_diff_transforms.item() { + if old_diff_transforms.end(&()).0 > edit.old.end { + break; + } + if let Some(hunk_anchor) = item.hunk_anchor() { + log::trace!( + "previously expanded hunk at {}", + old_diff_transforms.start().0 + ); + old_expanded_hunks.insert(hunk_anchor); + } + old_diff_transforms.next(&()); + } + + // Visit each excerpt that intersects the edit. + while let Some(excerpt) = excerpts.item() { + if excerpt.text_summary.len == 0 { + if excerpts.end(&()) <= edit.new.end { + excerpts.next(&()); + continue; + } else { + break; + } + } + + // Recompute the expanded hunks in the portion of the excerpt that + // intersects the edit. + if let Some(diff_state) = snapshot.diffs.get(&excerpt.buffer_id) { + let diff = &diff_state.diff; + let base_text = &diff_state.base_text; + let buffer = &excerpt.buffer; + let excerpt_start = *excerpts.start(); + let excerpt_end = excerpt_start + ExcerptOffset::new(excerpt.text_summary.len); + let excerpt_buffer_start = excerpt.range.context.start.to_offset(buffer); + let excerpt_buffer_end = excerpt_buffer_start + excerpt.text_summary.len; + let edit_buffer_start = + excerpt_buffer_start + edit.new.start.value.saturating_sub(excerpt_start.value); + let edit_buffer_end = + excerpt_buffer_start + edit.new.end.value.saturating_sub(excerpt_start.value); + let edit_buffer_end = edit_buffer_end.min(excerpt_buffer_end); + let edit_anchor_range = + buffer.anchor_before(edit_buffer_start)..buffer.anchor_after(edit_buffer_end); + + for hunk in diff.hunks_intersecting_range(edit_anchor_range, buffer) { + let hunk_anchor = (excerpt.id, hunk.buffer_range.start); + if !hunk_anchor.1.is_valid(buffer) { + continue; + } + + let hunk_buffer_range = hunk.buffer_range.to_offset(buffer); + let hunk_excerpt_start = excerpt_start + + ExcerptOffset::new( + hunk_buffer_range.start.saturating_sub(excerpt_buffer_start), + ); + let hunk_excerpt_end = excerpt_start + + ExcerptOffset::new(hunk_buffer_range.end - excerpt_buffer_start); + + self.push_buffer_content_transform( + snapshot, + new_diff_transforms, + hunk_excerpt_start, + *end_of_current_insert, + ); + + // For every existing hunk, determine if it was previously expanded + // and if it should currently be expanded. + let was_previously_expanded = old_expanded_hunks.contains(&hunk_anchor); + let should_expand_hunk = match &change_kind { + DiffChangeKind::DiffUpdated { base_changed: true } => { + self.all_diff_hunks_expanded + } + DiffChangeKind::ExpandOrCollapseHunks { expand } => { + let intersects = hunk_buffer_range.is_empty() + || hunk_buffer_range.end > edit_buffer_start; + if *expand { + intersects + || was_previously_expanded + || self.all_diff_hunks_expanded + } else { + !intersects + && (was_previously_expanded || self.all_diff_hunks_expanded) + } + } + _ => was_previously_expanded || self.all_diff_hunks_expanded, + }; + + if should_expand_hunk { + log::trace!("expanding hunk at {}", hunk_excerpt_start.value); + + if !hunk.diff_base_byte_range.is_empty() + && hunk_buffer_range.start >= edit_buffer_start + && hunk_buffer_range.start <= excerpt_buffer_end + { + let mut text_cursor = + base_text.as_rope().cursor(hunk.diff_base_byte_range.start); + let mut base_text_summary = + text_cursor.summary::(hunk.diff_base_byte_range.end); + + let mut has_trailing_newline = false; + if base_text_summary.last_line_chars > 0 { + base_text_summary += TextSummary::newline(); + has_trailing_newline = true; + } + + 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_anchor, + has_trailing_newline, + }, + &(), + ); + } + + if !hunk_buffer_range.is_empty() { + *end_of_current_insert = Some(( + hunk_excerpt_end.min(excerpt_end), + hunk_anchor.0, + hunk_anchor.1, + )); + } + } + } + } + + if excerpts.end(&()) <= edit.new.end { + excerpts.next(&()); + } else { + break; + } + } + } + + fn interpolate_diff_transforms_for_edit( + &self, + edit: &Edit>, + excerpts: &Cursor>, + old_diff_transforms: &mut Cursor, usize)>, + new_diff_transforms: &mut SumTree, + end_of_current_insert: &mut Option<(TypedOffset, ExcerptId, text::Anchor)>, + ) { + log::trace!( + "interpolating diff transform for edit {:?} => {:?}", + edit.old.start.value..edit.old.end.value, + edit.new.start.value..edit.new.end.value + ); + + // Preserve deleted hunks immediately preceding edits. + if let Some(transform) = old_diff_transforms.item() { + if old_diff_transforms.start().0 == edit.old.start { + if let DiffTransform::DeletedHunk { hunk_anchor, .. } = transform { + if excerpts + .item() + .map_or(false, |excerpt| hunk_anchor.1.is_valid(&excerpt.buffer)) + { + self.push_diff_transform(new_diff_transforms, transform.clone()); + old_diff_transforms.next(&()); + } + } + } + } + + let edit_start_transform = old_diff_transforms.item(); + + // When an edit starts within an inserted hunks, extend the hunk + // to include the lines of the edit. + if let Some(( + DiffTransform::BufferContent { + inserted_hunk_anchor: Some(inserted_hunk_anchor), + .. + }, + excerpt, + )) = edit_start_transform.zip(excerpts.item()) + { + let buffer = &excerpt.buffer; + if inserted_hunk_anchor.1.is_valid(buffer) { + let excerpt_start = *excerpts.start(); + let excerpt_end = excerpt_start + ExcerptOffset::new(excerpt.text_summary.len); + let excerpt_buffer_start = excerpt.range.context.start.to_offset(buffer); + let edit_buffer_end = + excerpt_buffer_start + edit.new.end.value.saturating_sub(excerpt_start.value); + let edit_buffer_end_point = buffer.offset_to_point(edit_buffer_end); + let edited_buffer_line_end = + buffer.point_to_offset(edit_buffer_end_point + Point::new(1, 0)); + let edited_line_end = excerpt_start + + ExcerptOffset::new(edited_buffer_line_end - excerpt_buffer_start); + let hunk_end = edited_line_end.min(excerpt_end); + *end_of_current_insert = + Some((hunk_end, inserted_hunk_anchor.0, inserted_hunk_anchor.1)); + } + } + + old_diff_transforms.seek_forward(&edit.old.end, Bias::Right, &()); + } + + fn append_diff_transforms( + &self, + new_transforms: &mut SumTree, + subtree: SumTree, + ) { + if let Some(DiffTransform::BufferContent { + inserted_hunk_anchor, + summary, + }) = subtree.first() + { + if self.extend_last_buffer_content_transform( + new_transforms, + *inserted_hunk_anchor, + *summary, + ) { + let mut cursor = subtree.cursor::<()>(&()); + cursor.next(&()); + cursor.next(&()); + new_transforms.append(cursor.suffix(&()), &()); + return; + } + } + new_transforms.append(subtree, &()); + } + + fn push_diff_transform( + &self, + new_transforms: &mut SumTree, + transform: DiffTransform, + ) { + if let DiffTransform::BufferContent { + inserted_hunk_anchor, + summary, + } = transform + { + if self.extend_last_buffer_content_transform( + new_transforms, + inserted_hunk_anchor, + summary, + ) { + return; + } + } + new_transforms.push(transform, &()); + } + + fn push_buffer_content_transform( + &self, + old_snapshot: &MultiBufferSnapshot, + new_transforms: &mut SumTree, + end_offset: ExcerptOffset, + current_inserted_hunk: Option<(ExcerptOffset, ExcerptId, text::Anchor)>, + ) { + let inserted_region = + current_inserted_hunk.map(|(insertion_end_offset, excerpt_id, anchor)| { + ( + end_offset.min(insertion_end_offset), + Some((excerpt_id, anchor)), + ) + }); + let unchanged_region = [(end_offset, None)]; + + for (end_offset, inserted_hunk_anchor) in + inserted_region.into_iter().chain(unchanged_region) + { + let start_offset = new_transforms.summary().excerpt_len(); + if end_offset <= start_offset { + continue; + } + let summary_to_add = old_snapshot + .text_summary_for_excerpt_offset_range::(start_offset..end_offset); + + if !self.extend_last_buffer_content_transform( + new_transforms, + inserted_hunk_anchor, + summary_to_add, + ) { + new_transforms.push( + DiffTransform::BufferContent { + summary: summary_to_add, + inserted_hunk_anchor, + }, + &(), + ) + } + } + } + + fn extend_last_buffer_content_transform( + &self, + new_transforms: &mut SumTree, + new_inserted_hunk_anchor: Option<(ExcerptId, text::Anchor)>, + summary_to_add: TextSummary, + ) -> bool { + let mut did_extend = false; + new_transforms.update_last( + |last_transform| { + if let DiffTransform::BufferContent { + summary, + inserted_hunk_anchor, + } = last_transform + { + if *inserted_hunk_anchor == new_inserted_hunk_anchor { + *summary += summary_to_add; + did_extend = true; + } + } + }, + &(), + ); + did_extend } } @@ -2396,30 +3247,8 @@ impl MultiBuffer { self.check_invariants(cx); } - fn check_invariants(&self, cx: &mut ModelContext) { - let snapshot = self.read(cx); - let excerpts = snapshot.excerpts.items(&()); - let excerpt_ids = snapshot.excerpt_ids.items(&()); - - for (ix, excerpt) in excerpts.iter().enumerate() { - if ix == 0 { - if excerpt.locator <= Locator::min() { - panic!("invalid first excerpt locator {:?}", excerpt.locator); - } - } else if excerpt.locator <= excerpts[ix - 1].locator { - panic!("excerpts are out-of-order: {:?}", excerpts); - } - } - - for (ix, entry) in excerpt_ids.iter().enumerate() { - if ix == 0 { - if entry.id.cmp(&ExcerptId::min(), &snapshot).is_le() { - panic!("invalid first excerpt id {:?}", entry.id); - } - } else if entry.id <= excerpt_ids[ix - 1].id { - panic!("excerpt ids are out-of-order: {:?}", excerpt_ids); - } - } + fn check_invariants(&self, cx: &AppContext) { + self.read(cx).check_invariants(); } } @@ -2433,37 +3262,26 @@ impl MultiBufferSnapshot { } pub fn reversed_chars_at(&self, position: T) -> impl Iterator + '_ { - let mut offset = position.to_offset(self); - let mut cursor = self.excerpts.cursor::(&()); - cursor.seek(&offset, Bias::Left, &()); - let mut excerpt_chunks = cursor.item().map(|excerpt| { - let end_before_footer = cursor.start() + excerpt.text_summary.len; - let start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let end = start + (cmp::min(offset, end_before_footer) - cursor.start()); - excerpt.buffer.reversed_chunks_in_range(start..end) - }); - iter::from_fn(move || { - if offset == *cursor.start() { - cursor.prev(&()); - let excerpt = cursor.item()?; - excerpt_chunks = Some( - excerpt - .buffer - .reversed_chunks_in_range(excerpt.range.context.clone()), - ); - } + self.reversed_chunks_in_range(0..position.to_offset(self)) + .flat_map(|c| c.chars().rev()) + } - let excerpt = cursor.item().unwrap(); - if offset == cursor.end(&()) && excerpt.has_trailing_newline { - offset -= 1; - Some("\n") - } else { - let chunk = excerpt_chunks.as_mut().unwrap().next().unwrap(); - offset -= chunk.len(); - Some(chunk) - } - }) - .flat_map(|c| c.chars().rev()) + fn reversed_chunks_in_range(&self, range: Range) -> ReversedMultiBufferChunks { + let mut cursor = self.cursor::(); + cursor.seek(&range.end); + let current_chunks = cursor.region().as_ref().map(|region| { + let start_overshoot = range.start.saturating_sub(region.range.start); + let end_overshoot = range.end - region.range.start; + let end = (region.buffer_range.start + end_overshoot).min(region.buffer_range.end); + let start = region.buffer_range.start + start_overshoot; + region.buffer.reversed_chunks_in_range(start..end) + }); + ReversedMultiBufferChunks { + cursor, + current_chunks, + start: range.start, + offset: range.end, + } } pub fn chars_at(&self, position: T) -> impl Iterator + '_ { @@ -2495,6 +3313,369 @@ impl MultiBufferSnapshot { .eq(needle.bytes()) } + pub fn diff_hunks(&self) -> impl Iterator + '_ { + self.diff_hunks_in_range(Anchor::min()..Anchor::max()) + } + + pub fn diff_hunks_in_range( + &self, + range: Range, + ) -> impl Iterator + '_ { + let range = range.start.to_offset(self)..range.end.to_offset(self); + self.lift_buffer_metadata(range.clone(), move |buffer, buffer_range| { + let diff = self.diffs.get(&buffer.remote_id())?; + let buffer_start = buffer.anchor_before(buffer_range.start); + let buffer_end = buffer.anchor_after(buffer_range.end); + Some( + diff.diff + .hunks_intersecting_range(buffer_start..buffer_end, buffer) + .map(|hunk| { + ( + Point::new(hunk.row_range.start, 0)..Point::new(hunk.row_range.end, 0), + hunk, + ) + }), + ) + }) + .map(|(range, hunk, excerpt)| { + let end_row = if range.end.column == 0 { + range.end.row + } else { + range.end.row + 1 + }; + MultiBufferDiffHunk { + row_range: MultiBufferRow(range.start.row)..MultiBufferRow(end_row), + buffer_id: excerpt.buffer_id, + excerpt_id: excerpt.id, + buffer_range: hunk.buffer_range.clone(), + diff_base_byte_range: hunk.diff_base_byte_range.clone(), + } + }) + } + + pub fn excerpt_ids_for_range( + &self, + range: Range, + ) -> impl Iterator + '_ { + let range = range.start.to_offset(self)..range.end.to_offset(self); + let mut cursor = self.cursor::(); + cursor.seek(&range.start); + std::iter::from_fn(move || { + let region = cursor.region()?; + if region.range.start >= range.end { + return None; + } + cursor.next_excerpt(); + Some(region.excerpt.id) + }) + } + + pub fn buffer_ids_for_range( + &self, + range: Range, + ) -> impl Iterator + '_ { + let range = range.start.to_offset(self)..range.end.to_offset(self); + let mut cursor = self.cursor::(); + cursor.seek(&range.start); + std::iter::from_fn(move || { + let region = cursor.region()?; + if region.range.start >= range.end { + return None; + } + cursor.next_excerpt(); + Some(region.excerpt.buffer_id) + }) + } + + pub fn ranges_to_buffer_ranges( + &self, + ranges: impl Iterator>, + ) -> impl Iterator, ExcerptId)> { + ranges.flat_map(|range| self.range_to_buffer_ranges(range).into_iter()) + } + + pub fn range_to_buffer_ranges( + &self, + range: Range, + ) -> Vec<(&BufferSnapshot, Range, ExcerptId)> { + let start = range.start.to_offset(&self); + let end = range.end.to_offset(&self); + + let mut cursor = self.cursor::(); + cursor.seek(&start); + + let mut result: Vec<(&BufferSnapshot, Range, ExcerptId)> = Vec::new(); + while let Some(region) = cursor.region() { + if region.range.start > end { + break; + } + if region.is_main_buffer { + let start_overshoot = start.saturating_sub(region.range.start); + let end_overshoot = end.saturating_sub(region.range.start); + let start = region + .buffer_range + .end + .min(region.buffer_range.start + start_overshoot); + let end = region + .buffer_range + .end + .min(region.buffer_range.start + end_overshoot); + if let Some(prev) = result.last_mut().filter(|(_, prev_range, excerpt_id)| { + *excerpt_id == region.excerpt.id && prev_range.end == start + }) { + prev.1.end = end; + } else { + result.push((region.buffer, start..end, region.excerpt.id)); + } + } + cursor.next(); + } + result + } + + /// Retrieves buffer metadata for the given range, and converts it into multi-buffer + /// coordinates. + /// + /// The given callback will be called for every excerpt intersecting the given range. It will + /// be passed the excerpt's buffer and the buffer range that the input range intersects. + /// The callback should return an iterator of metadata items from that buffer, each paired + /// with a buffer range. + /// + /// The returned iterator yields each of these metadata items, paired with its range in + /// multi-buffer coordinates. + fn lift_buffer_metadata<'a, D, M, I>( + &'a self, + range: Range, + get_buffer_metadata: impl 'a + Fn(&'a BufferSnapshot, Range) -> Option, + ) -> impl Iterator, M, &'a Excerpt)> + 'a + where + I: Iterator, M)> + 'a, + D: TextDimension + Ord + Sub, + { + let max_position = D::from_text_summary(&self.text_summary()); + let mut current_excerpt_metadata: Option<(ExcerptId, I)> = None; + let mut cursor = self.cursor::>(); + + // Find the excerpt and buffer offset where the given range ends. + cursor.seek(&DimensionPair { + key: range.end, + value: None, + }); + let mut range_end = None; + while let Some(region) = cursor.region() { + if region.is_main_buffer { + let mut buffer_end = region.buffer_range.start.key; + if region.is_main_buffer { + let overshoot = range.end.saturating_sub(region.range.start.key); + buffer_end.add_assign(&overshoot); + } + range_end = Some((region.excerpt.id, buffer_end)); + break; + } + cursor.next(); + } + + cursor.seek(&DimensionPair { + key: range.start, + value: None, + }); + + iter::from_fn(move || loop { + let excerpt = cursor.excerpt()?; + + // If we have already retrieved metadata for this excerpt, continue to use it. + let metadata_iter = if let Some((_, metadata)) = current_excerpt_metadata + .as_mut() + .filter(|(excerpt_id, _)| *excerpt_id == excerpt.id) + { + Some(metadata) + } + // Otherwise, compute the intersection of the input range with the excerpt's range, + // and retrieve the metadata for the resulting range. + else { + let region = cursor.region()?; + let buffer_start = if region.is_main_buffer { + let start_overshoot = range.start.saturating_sub(region.range.start.key); + region.buffer_range.start.key + start_overshoot + } else { + cursor.main_buffer_position()?.key + }; + let mut buffer_end = excerpt.range.context.end.to_offset(&excerpt.buffer); + if let Some((end_excerpt_id, end_buffer_offset)) = range_end { + if excerpt.id == end_excerpt_id { + buffer_end = buffer_end.min(end_buffer_offset); + } + } + + if let Some(iterator) = + get_buffer_metadata(&excerpt.buffer, buffer_start..buffer_end) + { + Some(&mut current_excerpt_metadata.insert((excerpt.id, iterator)).1) + } else { + None + } + }; + + // Visit each metadata item. + if let Some((range, metadata)) = metadata_iter.and_then(Iterator::next) { + // Find the multibuffer regions that contain the start and end of + // the metadata item's range. + if range.start > D::default() { + while let Some(region) = cursor.region() { + if region.buffer.remote_id() == excerpt.buffer_id + && region.buffer_range.end.value.unwrap() < range.start + { + cursor.next(); + } else { + break; + } + } + } + let start_region = cursor.region()?; + while let Some(region) = cursor.region() { + if !region.is_main_buffer + || region.buffer.remote_id() == excerpt.buffer_id + && region.buffer_range.end.value.unwrap() <= range.end + { + cursor.next(); + } else { + break; + } + } + let end_region = cursor + .region() + .filter(|region| region.buffer.remote_id() == excerpt.buffer_id); + + // Convert the metadata item's range into multibuffer coordinates. + let mut start = start_region.range.start.value.unwrap(); + let region_buffer_start = start_region.buffer_range.start.value.unwrap(); + if start_region.is_main_buffer && range.start > region_buffer_start { + start.add_assign(&(range.start - region_buffer_start)); + } + let mut end = max_position; + if let Some(end_region) = end_region { + end = end_region.range.start.value.unwrap(); + debug_assert!(end_region.is_main_buffer); + let region_buffer_start = end_region.buffer_range.start.value.unwrap(); + if range.end > region_buffer_start { + end.add_assign(&(range.end - region_buffer_start)); + } + } + + return Some((start..end, metadata, excerpt)); + } + // When there are no more metadata items for this excerpt, move to the next excerpt. + else { + current_excerpt_metadata.take(); + cursor.next_excerpt(); + } + }) + } + + pub fn diff_hunk_before(&self, position: T) -> Option { + let offset = position.to_offset(self); + + // Go to the region containing the given offset. + let mut cursor = self.cursor::>(); + cursor.seek(&DimensionPair { + key: offset, + value: None, + }); + let mut region = cursor.region()?; + if region.range.start.key == offset || !region.is_main_buffer { + cursor.prev(); + region = cursor.region()?; + } + + // Find the corresponding buffer offset. + let overshoot = if region.is_main_buffer { + offset - region.range.start.key + } else { + 0 + }; + let mut max_buffer_offset = region + .buffer + .clip_offset(region.buffer_range.start.key + overshoot, Bias::Right); + + loop { + let excerpt = cursor.excerpt()?; + let excerpt_end = excerpt.range.context.end.to_offset(&excerpt.buffer); + let buffer_offset = excerpt_end.min(max_buffer_offset); + let buffer_end = excerpt.buffer.anchor_before(buffer_offset); + let buffer_end_row = buffer_end.to_point(&excerpt.buffer).row; + + if let Some(diff_state) = self.diffs.get(&excerpt.buffer_id) { + for hunk in diff_state.diff.hunks_intersecting_range_rev( + excerpt.range.context.start..buffer_end, + &excerpt.buffer, + ) { + let hunk_range = hunk.buffer_range.to_offset(&excerpt.buffer); + if hunk.row_range.end >= buffer_end_row { + continue; + } + + let hunk_start = Point::new(hunk.row_range.start, 0); + let hunk_end = Point::new(hunk.row_range.end, 0); + + cursor.seek_to_buffer_position_in_current_excerpt(&DimensionPair { + key: hunk_range.start, + value: None, + }); + + let mut region = cursor.region()?; + while !region.is_main_buffer || region.buffer_range.start.key >= hunk_range.end + { + cursor.prev(); + region = cursor.region()?; + } + + let overshoot = if region.is_main_buffer { + hunk_start.saturating_sub(region.buffer_range.start.value.unwrap()) + } else { + Point::zero() + }; + let start = region.range.start.value.unwrap() + overshoot; + + while let Some(region) = cursor.region() { + if !region.is_main_buffer + || region.buffer_range.end.value.unwrap() <= hunk_end + { + cursor.next(); + } else { + break; + } + } + + let end = if let Some(region) = cursor.region() { + let overshoot = if region.is_main_buffer { + hunk_end.saturating_sub(region.buffer_range.start.value.unwrap()) + } else { + Point::zero() + }; + region.range.start.value.unwrap() + overshoot + } else { + self.max_point() + }; + + return Some(MultiBufferDiffHunk { + row_range: MultiBufferRow(start.row)..MultiBufferRow(end.row), + buffer_id: excerpt.buffer_id, + excerpt_id: excerpt.id, + buffer_range: hunk.buffer_range.clone(), + diff_base_byte_range: hunk.diff_base_byte_range.clone(), + }); + } + } + + cursor.prev_excerpt(); + max_buffer_offset = usize::MAX; + } + } + + pub fn has_diff_hunks(&self) -> bool { + self.diffs.values().any(|diff| !diff.diff.is_empty()) + } + pub fn surrounding_word( &self, start: T, @@ -2533,6 +3714,10 @@ impl MultiBufferSnapshot { (start..end, word_kind) } + pub fn is_singleton(&self) -> bool { + self.singleton + } + pub fn as_singleton(&self) -> Option<(&ExcerptId, BufferId, &BufferSnapshot)> { if self.singleton { self.excerpts @@ -2545,7 +3730,7 @@ impl MultiBufferSnapshot { } pub fn len(&self) -> usize { - self.excerpts.summary().text.len + self.diff_transforms.summary().output.len } pub fn is_empty(&self) -> bool { @@ -2556,102 +3741,38 @@ impl MultiBufferSnapshot { self.excerpts.summary().widest_line_number + 1 } - pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer.clip_offset(offset, bias); - } - - let mut cursor = self.excerpts.cursor::(&()); - cursor.seek(&offset, Bias::Right, &()); - let overshoot = if let Some(excerpt) = cursor.item() { - let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let buffer_offset = excerpt - .buffer - .clip_offset(excerpt_start + (offset - cursor.start()), bias); - buffer_offset.saturating_sub(excerpt_start) - } else { - 0 - }; - cursor.start() + overshoot - } - - pub fn clip_point(&self, point: Point, bias: Bias) -> Point { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer.clip_point(point, bias); - } - - let mut cursor = self.excerpts.cursor::(&()); - cursor.seek(&point, Bias::Right, &()); - let overshoot = if let Some(excerpt) = cursor.item() { - let excerpt_start = excerpt.range.context.start.to_point(&excerpt.buffer); - let buffer_point = excerpt - .buffer - .clip_point(excerpt_start + (point - cursor.start()), bias); - buffer_point.saturating_sub(excerpt_start) - } else { - Point::zero() - }; - *cursor.start() + overshoot - } - - pub fn clip_offset_utf16(&self, offset: OffsetUtf16, bias: Bias) -> OffsetUtf16 { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer.clip_offset_utf16(offset, bias); - } - - let mut cursor = self.excerpts.cursor::(&()); - cursor.seek(&offset, Bias::Right, &()); - let overshoot = if let Some(excerpt) = cursor.item() { - let excerpt_start = excerpt.range.context.start.to_offset_utf16(&excerpt.buffer); - let buffer_offset = excerpt - .buffer - .clip_offset_utf16(excerpt_start + (offset - cursor.start()), bias); - OffsetUtf16(buffer_offset.0.saturating_sub(excerpt_start.0)) - } else { - OffsetUtf16(0) - }; - *cursor.start() + overshoot - } - - pub fn clip_point_utf16(&self, point: Unclipped, bias: Bias) -> PointUtf16 { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer.clip_point_utf16(point, bias); - } - - let mut cursor = self.excerpts.cursor::(&()); - cursor.seek(&point.0, Bias::Right, &()); - let overshoot = if let Some(excerpt) = cursor.item() { - let excerpt_start = excerpt - .buffer - .offset_to_point_utf16(excerpt.range.context.start.to_offset(&excerpt.buffer)); - let buffer_point = excerpt - .buffer - .clip_point_utf16(Unclipped(excerpt_start + (point.0 - cursor.start())), bias); - buffer_point.saturating_sub(excerpt_start) - } else { - PointUtf16::zero() - }; - *cursor.start() + overshoot - } - pub fn bytes_in_range(&self, range: Range) -> MultiBufferBytes { let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut excerpts = self.excerpts.cursor::(&()); - excerpts.seek(&range.start, Bias::Right, &()); + let mut excerpts = self.cursor::(); + excerpts.seek(&range.start); - let mut chunk = &[][..]; - let excerpt_bytes = if let Some(excerpt) = excerpts.item() { - let mut excerpt_bytes = excerpt - .bytes_in_range(range.start - excerpts.start()..range.end - excerpts.start()); - chunk = excerpt_bytes.next().unwrap_or(&[][..]); - Some(excerpt_bytes) + let mut chunk; + let mut has_trailing_newline; + let excerpt_bytes; + if let Some(region) = excerpts.region() { + let mut bytes = region.buffer.bytes_in_range( + region.buffer_range.start + range.start - region.range.start + ..(region.buffer_range.start + range.end - region.range.start) + .min(region.buffer_range.end), + ); + chunk = bytes.next().unwrap_or(&[][..]); + excerpt_bytes = Some(bytes); + has_trailing_newline = region.has_trailing_newline && range.end >= region.range.end; + if chunk.is_empty() && has_trailing_newline { + chunk = b"\n"; + has_trailing_newline = false; + } } else { - None + chunk = &[][..]; + excerpt_bytes = None; + has_trailing_newline = false; }; + MultiBufferBytes { range, - excerpts, + cursor: excerpts, excerpt_bytes, + has_trailing_newline, chunk, } } @@ -2661,200 +3782,145 @@ impl MultiBufferSnapshot { range: Range, ) -> ReversedMultiBufferBytes { let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut excerpts = self.excerpts.cursor::(&()); - excerpts.seek(&range.end, Bias::Left, &()); - - let mut chunk = &[][..]; - let excerpt_bytes = if let Some(excerpt) = excerpts.item() { - let mut excerpt_bytes = excerpt.reversed_bytes_in_range( - range.start.saturating_sub(*excerpts.start())..range.end - *excerpts.start(), - ); - chunk = excerpt_bytes.next().unwrap_or(&[][..]); - Some(excerpt_bytes) - } else { - None - }; - + let mut chunks = self.reversed_chunks_in_range(range.clone()); + let chunk = chunks.next().map_or(&[][..], |c| c.as_bytes()); ReversedMultiBufferBytes { range, - excerpts, - excerpt_bytes, + chunks, chunk, } } - pub fn buffer_rows(&self, start_row: MultiBufferRow) -> MultiBufferRows { + pub fn row_infos(&self, start_row: MultiBufferRow) -> MultiBufferRows { + let mut cursor = self.cursor::(); + cursor.seek(&Point::new(start_row.0, 0)); let mut result = MultiBufferRows { - buffer_row_range: 0..0, - excerpts: self.excerpts.cursor(&()), + point: Point::new(0, 0), + is_empty: self.excerpts.is_empty(), + cursor, }; result.seek(start_row); result } pub fn chunks(&self, range: Range, language_aware: bool) -> MultiBufferChunks { - let range = range.start.to_offset(self)..range.end.to_offset(self); let mut chunks = MultiBufferChunks { - range: range.clone(), + excerpt_offset_range: ExcerptOffset::new(0)..ExcerptOffset::new(0), + range: 0..0, excerpts: self.excerpts.cursor(&()), + diff_transforms: self.diff_transforms.cursor(&()), + diffs: &self.diffs, + diff_base_chunks: None, excerpt_chunks: None, + buffer_chunk: None, language_aware, }; + let range = range.start.to_offset(self)..range.end.to_offset(self); chunks.seek(range); chunks } - pub fn offset_to_point(&self, offset: usize) -> Point { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer.offset_to_point(offset); - } + pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize { + self.clip_dimension(offset, bias, text::BufferSnapshot::clip_offset) + } - let mut cursor = self.excerpts.cursor::<(usize, Point)>(&()); - cursor.seek(&offset, Bias::Right, &()); - if let Some(excerpt) = cursor.item() { - let (start_offset, start_point) = cursor.start(); - let overshoot = offset - start_offset; - let excerpt_start_offset = excerpt.range.context.start.to_offset(&excerpt.buffer); - let excerpt_start_point = excerpt.range.context.start.to_point(&excerpt.buffer); - let buffer_point = excerpt - .buffer - .offset_to_point(excerpt_start_offset + overshoot); - *start_point + (buffer_point - excerpt_start_point) - } else { - self.excerpts.summary().text.lines - } + pub fn clip_point(&self, point: Point, bias: Bias) -> Point { + self.clip_dimension(point, bias, text::BufferSnapshot::clip_point) + } + + pub fn clip_offset_utf16(&self, offset: OffsetUtf16, bias: Bias) -> OffsetUtf16 { + self.clip_dimension(offset, bias, text::BufferSnapshot::clip_offset_utf16) + } + + pub fn clip_point_utf16(&self, point: Unclipped, bias: Bias) -> PointUtf16 { + self.clip_dimension(point.0, bias, |buffer, point, bias| { + buffer.clip_point_utf16(Unclipped(point), bias) + }) + } + + pub fn offset_to_point(&self, offset: usize) -> Point { + self.convert_dimension(offset, text::BufferSnapshot::offset_to_point) } pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer.offset_to_point_utf16(offset); - } - - let mut cursor = self.excerpts.cursor::<(usize, PointUtf16)>(&()); - cursor.seek(&offset, Bias::Right, &()); - if let Some(excerpt) = cursor.item() { - let (start_offset, start_point) = cursor.start(); - let overshoot = offset - start_offset; - let excerpt_start_offset = excerpt.range.context.start.to_offset(&excerpt.buffer); - let excerpt_start_point = excerpt.range.context.start.to_point_utf16(&excerpt.buffer); - let buffer_point = excerpt - .buffer - .offset_to_point_utf16(excerpt_start_offset + overshoot); - *start_point + (buffer_point - excerpt_start_point) - } else { - self.excerpts.summary().text.lines_utf16() - } + self.convert_dimension(offset, text::BufferSnapshot::offset_to_point_utf16) } pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer.point_to_point_utf16(point); - } - - let mut cursor = self.excerpts.cursor::<(Point, PointUtf16)>(&()); - cursor.seek(&point, Bias::Right, &()); - if let Some(excerpt) = cursor.item() { - let (start_offset, start_point) = cursor.start(); - let overshoot = point - start_offset; - let excerpt_start_point = excerpt.range.context.start.to_point(&excerpt.buffer); - let excerpt_start_point_utf16 = - excerpt.range.context.start.to_point_utf16(&excerpt.buffer); - let buffer_point = excerpt - .buffer - .point_to_point_utf16(excerpt_start_point + overshoot); - *start_point + (buffer_point - excerpt_start_point_utf16) - } else { - self.excerpts.summary().text.lines_utf16() - } + self.convert_dimension(point, text::BufferSnapshot::point_to_point_utf16) } pub fn point_to_offset(&self, point: Point) -> usize { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer.point_to_offset(point); - } - - let mut cursor = self.excerpts.cursor::<(Point, usize)>(&()); - cursor.seek(&point, Bias::Right, &()); - if let Some(excerpt) = cursor.item() { - let (start_point, start_offset) = cursor.start(); - let overshoot = point - start_point; - let excerpt_start_offset = excerpt.range.context.start.to_offset(&excerpt.buffer); - let excerpt_start_point = excerpt.range.context.start.to_point(&excerpt.buffer); - let buffer_offset = excerpt - .buffer - .point_to_offset(excerpt_start_point + overshoot); - *start_offset + buffer_offset - excerpt_start_offset - } else { - self.excerpts.summary().text.len - } + self.convert_dimension(point, text::BufferSnapshot::point_to_offset) } - pub fn offset_utf16_to_offset(&self, offset_utf16: OffsetUtf16) -> usize { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer.offset_utf16_to_offset(offset_utf16); - } - - let mut cursor = self.excerpts.cursor::<(OffsetUtf16, usize)>(&()); - cursor.seek(&offset_utf16, Bias::Right, &()); - if let Some(excerpt) = cursor.item() { - let (start_offset_utf16, start_offset) = cursor.start(); - let overshoot = offset_utf16 - start_offset_utf16; - let excerpt_start_offset = excerpt.range.context.start.to_offset(&excerpt.buffer); - let excerpt_start_offset_utf16 = - excerpt.buffer.offset_to_offset_utf16(excerpt_start_offset); - let buffer_offset = excerpt - .buffer - .offset_utf16_to_offset(excerpt_start_offset_utf16 + overshoot); - *start_offset + (buffer_offset - excerpt_start_offset) - } else { - self.excerpts.summary().text.len - } + pub fn offset_utf16_to_offset(&self, offset: OffsetUtf16) -> usize { + self.convert_dimension(offset, text::BufferSnapshot::offset_utf16_to_offset) } pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer.offset_to_offset_utf16(offset); - } - - let mut cursor = self.excerpts.cursor::<(usize, OffsetUtf16)>(&()); - cursor.seek(&offset, Bias::Right, &()); - if let Some(excerpt) = cursor.item() { - let (start_offset, start_offset_utf16) = cursor.start(); - let overshoot = offset - start_offset; - let excerpt_start_offset_utf16 = - excerpt.range.context.start.to_offset_utf16(&excerpt.buffer); - let excerpt_start_offset = excerpt - .buffer - .offset_utf16_to_offset(excerpt_start_offset_utf16); - let buffer_offset_utf16 = excerpt - .buffer - .offset_to_offset_utf16(excerpt_start_offset + overshoot); - *start_offset_utf16 + (buffer_offset_utf16 - excerpt_start_offset_utf16) - } else { - self.excerpts.summary().text.len_utf16 - } + self.convert_dimension(offset, text::BufferSnapshot::offset_to_offset_utf16) } pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer.point_utf16_to_offset(point); - } + self.convert_dimension(point, text::BufferSnapshot::point_utf16_to_offset) + } - let mut cursor = self.excerpts.cursor::<(PointUtf16, usize)>(&()); - cursor.seek(&point, Bias::Right, &()); - if let Some(excerpt) = cursor.item() { - let (start_point, start_offset) = cursor.start(); - let overshoot = point - start_point; - let excerpt_start_offset = excerpt.range.context.start.to_offset(&excerpt.buffer); - let excerpt_start_point = excerpt - .buffer - .offset_to_point_utf16(excerpt.range.context.start.to_offset(&excerpt.buffer)); - let buffer_offset = excerpt - .buffer - .point_utf16_to_offset(excerpt_start_point + overshoot); - *start_offset + (buffer_offset - excerpt_start_offset) + fn clip_dimension( + &self, + position: D, + bias: Bias, + clip_buffer_position: fn(&text::BufferSnapshot, D, Bias) -> D, + ) -> D + where + D: TextDimension + Ord + Sub, + { + let mut cursor = self.cursor(); + cursor.seek(&position); + if let Some(region) = cursor.region() { + if position >= region.range.end { + return region.range.end; + } + let overshoot = position - region.range.start; + let mut buffer_position = region.buffer_range.start; + buffer_position.add_assign(&overshoot); + let clipped_buffer_position = + clip_buffer_position(®ion.buffer, buffer_position, bias); + let mut position = region.range.start; + position.add_assign(&(clipped_buffer_position - region.buffer_range.start)); + position } else { - self.excerpts.summary().text.len + D::from_text_summary(&self.text_summary()) + } + } + + fn convert_dimension( + &self, + key: D1, + convert_buffer_dimension: fn(&text::BufferSnapshot, D1) -> D2, + ) -> D2 + where + D1: TextDimension + Ord + Sub, + D2: TextDimension + Ord + Sub, + { + let mut cursor = self.cursor::>(); + cursor.seek(&DimensionPair { key, value: None }); + if let Some(region) = cursor.region() { + if key >= region.range.end.key { + return region.range.end.value.unwrap(); + } + let start_key = region.range.start.key; + let start_value = region.range.start.value.unwrap(); + let buffer_start_key = region.buffer_range.start.key; + let buffer_start_value = region.buffer_range.start.value.unwrap(); + let mut buffer_key = buffer_start_key; + buffer_key.add_assign(&(key - start_key)); + let buffer_value = convert_buffer_dimension(®ion.buffer, buffer_key); + let mut result = start_value; + result.add_assign(&(buffer_value - buffer_start_value)); + result + } else { + D2::from_text_summary(&self.text_summary()) } } @@ -2863,17 +3929,21 @@ impl MultiBufferSnapshot { point: T, ) -> Option<(&BufferSnapshot, usize)> { let offset = point.to_offset(self); - let mut cursor = self.excerpts.cursor::(&()); - cursor.seek(&offset, Bias::Right, &()); - if cursor.item().is_none() { - cursor.prev(&()); - } + let mut cursor = self.cursor::(); + cursor.seek(&offset); + let region = cursor.region()?; + let overshoot = offset - region.range.start; + let buffer_offset = region.buffer_range.start + overshoot; + Some((region.buffer, buffer_offset)) + } - cursor.item().map(|excerpt| { - let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let buffer_point = excerpt_start + offset - *cursor.start(); - (&excerpt.buffer, buffer_point) - }) + pub fn point_to_buffer_point(&self, point: Point) -> Option<(&BufferSnapshot, Point, bool)> { + let mut cursor = self.cursor::(); + cursor.seek(&point); + let region = cursor.region()?; + let overshoot = point - region.range.start; + let buffer_offset = region.buffer_range.start + overshoot; + Some((region.buffer, buffer_offset, region.is_main_buffer)) } pub fn suggested_indents( @@ -2884,35 +3954,35 @@ impl MultiBufferSnapshot { let mut result = BTreeMap::new(); let mut rows_for_excerpt = Vec::new(); - let mut cursor = self.excerpts.cursor::(&()); + let mut cursor = self.cursor::(); let mut rows = rows.into_iter().peekable(); let mut prev_row = u32::MAX; let mut prev_language_indent_size = IndentSize::default(); while let Some(row) = rows.next() { - cursor.seek(&Point::new(row, 0), Bias::Right, &()); - let excerpt = match cursor.item() { - Some(excerpt) => excerpt, - _ => continue, + cursor.seek(&Point::new(row, 0)); + let Some(region) = cursor.region() else { + continue; }; // Retrieve the language and indent size once for each disjoint region being indented. let single_indent_size = if row.saturating_sub(1) == prev_row { prev_language_indent_size } else { - excerpt + region .buffer .language_indent_size_at(Point::new(row, 0), cx) }; prev_language_indent_size = single_indent_size; prev_row = row; - let start_buffer_row = excerpt.range.context.start.to_point(&excerpt.buffer).row; - let start_multibuffer_row = cursor.start().row; + let start_buffer_row = region.buffer_range.start.row; + let start_multibuffer_row = region.range.start.row; + let end_multibuffer_row = region.range.end.row; rows_for_excerpt.push(row); while let Some(next_row) = rows.peek().copied() { - if cursor.end(&()).row > next_row { + if end_multibuffer_row > next_row { rows_for_excerpt.push(next_row); rows.next(); } else { @@ -2923,7 +3993,7 @@ impl MultiBufferSnapshot { let buffer_rows = rows_for_excerpt .drain(..) .map(|row| start_buffer_row + row - start_multibuffer_row); - let buffer_indents = excerpt + let buffer_indents = region .buffer .suggested_indents(buffer_rows, single_indent_size); let multibuffer_indents = buffer_indents.into_iter().map(|(row, indent)| { @@ -2951,6 +4021,14 @@ impl MultiBufferSnapshot { } } + pub fn line_indent_for_row(&self, row: MultiBufferRow) -> LineIndent { + if let Some((buffer, range)) = self.buffer_line_for_row(row) { + LineIndent::from_iter(buffer.text_for_range(range).flat_map(|s| s.chars())) + } else { + LineIndent::spaces(0) + } + } + pub fn indent_and_comment_for_line(&self, row: MultiBufferRow, cx: &AppContext) -> String { let mut indent = self.indent_size_for_line(row).chars().collect::(); @@ -2997,25 +4075,19 @@ impl MultiBufferSnapshot { &self, row: MultiBufferRow, ) -> Option<(&BufferSnapshot, Range)> { - let mut cursor = self.excerpts.cursor::(&()); + let mut cursor = self.cursor::(); let point = Point::new(row.0, 0); - cursor.seek(&point, Bias::Right, &()); - if cursor.item().is_none() && *cursor.start() == point { - cursor.prev(&()); + cursor.seek(&point); + let region = cursor.region()?; + let overshoot = point.min(region.range.end) - region.range.start; + let buffer_point = region.buffer_range.start + overshoot; + if buffer_point.row > region.buffer_range.end.row { + return None; } - if let Some(excerpt) = cursor.item() { - let overshoot = row.0 - cursor.start().row; - let excerpt_start = excerpt.range.context.start.to_point(&excerpt.buffer); - let excerpt_end = excerpt.range.context.end.to_point(&excerpt.buffer); - let buffer_row = excerpt_start.row + overshoot; - let line_start = Point::new(buffer_row, 0); - let line_end = Point::new(buffer_row, excerpt.buffer.line_len(buffer_row)); - return Some(( - &excerpt.buffer, - line_start.max(excerpt_start)..line_end.min(excerpt_end), - )); - } - None + let line_start = Point::new(buffer_point.row, 0).max(region.buffer_range.start); + let line_end = Point::new(buffer_point.row, region.buffer.line_len(buffer_point.row)) + .min(region.buffer_range.end); + Some((region.buffer, line_start..line_end)) } pub fn max_point(&self) -> Point { @@ -3027,7 +4099,7 @@ impl MultiBufferSnapshot { } pub fn text_summary(&self) -> TextSummary { - self.excerpts.summary().text.clone() + self.diff_transforms.summary().output } pub fn text_summary_for_range(&self, range: Range) -> D @@ -3035,20 +4107,119 @@ impl MultiBufferSnapshot { D: TextDimension, O: ToOffset, { + let range = range.start.to_offset(self)..range.end.to_offset(self); + let mut cursor = self.diff_transforms.cursor::<(usize, ExcerptOffset)>(&()); + cursor.seek(&range.start, Bias::Right, &()); + + let Some(first_transform) = cursor.item() else { + return D::from_text_summary(&TextSummary::default()); + }; + + let diff_transform_start = cursor.start().0; + let diff_transform_end = cursor.end(&()).0; + let diff_start = range.start; + let start_overshoot = diff_start - diff_transform_start; + let end_overshoot = std::cmp::min(range.end, diff_transform_end) - diff_transform_start; + + let mut result = match first_transform { + DiffTransform::BufferContent { .. } => { + let excerpt_start = cursor.start().1 + ExcerptOffset::new(start_overshoot); + let excerpt_end = cursor.start().1 + ExcerptOffset::new(end_overshoot); + self.text_summary_for_excerpt_offset_range(excerpt_start..excerpt_end) + } + DiffTransform::DeletedHunk { + buffer_id, + base_text_byte_range, + has_trailing_newline, + .. + } => { + let buffer_start = base_text_byte_range.start + start_overshoot; + let mut buffer_end = base_text_byte_range.start + end_overshoot; + let Some(buffer_diff) = self.diffs.get(buffer_id) else { + panic!("{:?} is in non-existent deleted hunk", range.start) + }; + + let include_trailing_newline = + *has_trailing_newline && range.end >= diff_transform_end; + if include_trailing_newline { + buffer_end -= 1; + } + + let mut summary = buffer_diff + .base_text + .text_summary_for_range::(buffer_start..buffer_end); + + if include_trailing_newline { + summary.add_assign(&D::from_text_summary(&TextSummary::newline())) + } + + summary + } + }; + if range.end < diff_transform_end { + return result; + } + + cursor.next(&()); + result.add_assign(&D::from_text_summary(&cursor.summary( + &range.end, + Bias::Right, + &(), + ))); + + let Some(last_transform) = cursor.item() else { + return result; + }; + + let overshoot = range.end - cursor.start().0; + let suffix = match last_transform { + DiffTransform::BufferContent { .. } => { + let end = cursor.start().1 + ExcerptOffset::new(overshoot); + self.text_summary_for_excerpt_offset_range::(cursor.start().1..end) + } + DiffTransform::DeletedHunk { + base_text_byte_range, + buffer_id, + has_trailing_newline, + .. + } => { + let buffer_end = base_text_byte_range.start + overshoot; + let Some(buffer_diff) = self.diffs.get(buffer_id) else { + panic!("{:?} is in non-extant deleted hunk", range.end) + }; + + let mut suffix = buffer_diff + .base_text + .text_summary_for_range::(base_text_byte_range.start..buffer_end); + if *has_trailing_newline && buffer_end == base_text_byte_range.end + 1 { + suffix.add_assign(&D::from_text_summary(&TextSummary::newline())) + } + suffix + } + }; + + result.add_assign(&suffix); + result + } + + fn text_summary_for_excerpt_offset_range(&self, mut range: Range) -> D + where + D: TextDimension, + { + // let mut range = range.start..range.end; let mut summary = D::zero(&()); - let mut range = range.start.to_offset(self)..range.end.to_offset(self); - let mut cursor = self.excerpts.cursor::(&()); + let mut cursor = self.excerpts.cursor::(&()); cursor.seek(&range.start, Bias::Right, &()); if let Some(excerpt) = cursor.item() { let mut end_before_newline = cursor.end(&()); if excerpt.has_trailing_newline { - end_before_newline -= 1; + end_before_newline -= ExcerptOffset::new(1); } let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let start_in_excerpt = excerpt_start + (range.start - cursor.start()); + let start_in_excerpt = excerpt_start + (range.start - *cursor.start()).value; let end_in_excerpt = - excerpt_start + (cmp::min(end_before_newline, range.end) - cursor.start()); + excerpt_start + (cmp::min(end_before_newline, range.end) - *cursor.start()).value; summary.add_assign( &excerpt .buffer @@ -3063,16 +4234,16 @@ impl MultiBufferSnapshot { } if range.end > *cursor.start() { - summary.add_assign(&D::from_text_summary(&cursor.summary::<_, TextSummary>( - &range.end, - Bias::Right, - &(), - ))); + summary.add_assign( + &cursor + .summary::<_, ExcerptDimension>(&range.end, Bias::Right, &()) + .0, + ); if let Some(excerpt) = cursor.item() { range.end = cmp::max(*cursor.start(), range.end); let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let end_in_excerpt = excerpt_start + (range.end - cursor.start()); + let end_in_excerpt = excerpt_start + (range.end - *cursor.start()).value; summary.add_assign( &excerpt .buffer @@ -3096,7 +4267,7 @@ impl MultiBufferSnapshot { cursor.next(&()); } - let mut position = D::from_text_summary(&cursor.start().text); + let mut excerpt_position = D::from_text_summary(&cursor.start().text); if let Some(excerpt) = cursor.item() { if excerpt.id == anchor.excerpt_id { let excerpt_buffer_start = @@ -3107,7 +4278,108 @@ impl MultiBufferSnapshot { anchor.text_anchor.summary::(&excerpt.buffer), ); if buffer_position > excerpt_buffer_start { - position.add_assign(&(buffer_position - excerpt_buffer_start)); + excerpt_position.add_assign(&(buffer_position - excerpt_buffer_start)); + } + } + } + + let mut diff_transforms_cursor = self + .diff_transforms + .cursor::<(ExcerptDimension, OutputDimension)>(&()); + diff_transforms_cursor.seek(&ExcerptDimension(excerpt_position), Bias::Left, &()); + + self.resolve_summary_for_anchor(anchor, excerpt_position, &mut diff_transforms_cursor) + } + + fn resolve_summary_for_anchor( + &self, + anchor: &Anchor, + excerpt_position: D, + diff_transforms: &mut Cursor, OutputDimension)>, + ) -> D + where + D: TextDimension + Ord + Sub, + { + loop { + let transform_end_position = diff_transforms.end(&()).0 .0; + let at_transform_end = + excerpt_position == transform_end_position && diff_transforms.item().is_some(); + if at_transform_end && anchor.text_anchor.bias == Bias::Right { + diff_transforms.next(&()); + continue; + } + + let mut position = diff_transforms.start().1 .0; + match diff_transforms.item() { + Some(DiffTransform::DeletedHunk { + buffer_id, + base_text_byte_range, + .. + }) => { + let mut in_deleted_hunk = false; + if let Some(diff_base_anchor) = &anchor.diff_base_anchor { + if let Some(diff) = self.diffs.get(buffer_id) { + if diff.base_text.can_resolve(&diff_base_anchor) { + let base_text_offset = diff_base_anchor.to_offset(&diff.base_text); + if base_text_offset >= base_text_byte_range.start + && base_text_offset <= base_text_byte_range.end + { + let position_in_hunk = + diff.base_text.text_summary_for_range::( + base_text_byte_range.start..base_text_offset, + ); + position.add_assign(&position_in_hunk); + in_deleted_hunk = true; + } else if at_transform_end { + diff_transforms.next(&()); + continue; + } + } + } + } + if !in_deleted_hunk { + position = diff_transforms.end(&()).1 .0; + } + } + _ => { + if at_transform_end && anchor.diff_base_anchor.is_some() { + diff_transforms.next(&()); + continue; + } + let overshoot = excerpt_position - diff_transforms.start().0 .0; + position.add_assign(&overshoot); + } + } + + return position; + } + } + + fn excerpt_offset_for_anchor(&self, anchor: &Anchor) -> ExcerptOffset { + let mut cursor = self + .excerpts + .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); + let locator = self.excerpt_locator_for_id(anchor.excerpt_id); + + cursor.seek(&Some(locator), Bias::Left, &()); + if cursor.item().is_none() { + cursor.next(&()); + } + + let mut position = cursor.start().1; + if let Some(excerpt) = cursor.item() { + if excerpt.id == anchor.excerpt_id { + let excerpt_buffer_start = excerpt + .buffer + .offset_for_anchor(&excerpt.range.context.start); + let excerpt_buffer_end = + excerpt.buffer.offset_for_anchor(&excerpt.range.context.end); + let buffer_position = cmp::min( + excerpt_buffer_end, + excerpt.buffer.offset_for_anchor(&anchor.text_anchor), + ); + if buffer_position > excerpt_buffer_start { + position.value += buffer_position - excerpt_buffer_start; } } } @@ -3119,21 +4391,20 @@ impl MultiBufferSnapshot { D: TextDimension + Ord + Sub, I: 'a + IntoIterator, { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer - .summaries_for_anchors(anchors.into_iter().map(|a| &a.text_anchor)) - .collect(); - } - let mut anchors = anchors.into_iter().peekable(); let mut cursor = self.excerpts.cursor::(&()); + let mut diff_transforms_cursor = self + .diff_transforms + .cursor::<(ExcerptDimension, OutputDimension)>(&()); + diff_transforms_cursor.next(&()); + let mut summaries = Vec::new(); while let Some(anchor) = anchors.peek() { let excerpt_id = anchor.excerpt_id; let excerpt_anchors = iter::from_fn(|| { let anchor = anchors.peek()?; if anchor.excerpt_id == excerpt_id { - Some(&anchors.next().unwrap().text_anchor) + Some(anchors.next().unwrap()) } else { None } @@ -3145,32 +4416,50 @@ impl MultiBufferSnapshot { cursor.next(&()); } - let position = D::from_text_summary(&cursor.start().text); - if let Some(excerpt) = cursor.item() { - if excerpt.id == excerpt_id { - let excerpt_buffer_start = - excerpt.range.context.start.summary::(&excerpt.buffer); - let excerpt_buffer_end = - excerpt.range.context.end.summary::(&excerpt.buffer); - summaries.extend( - excerpt - .buffer - .summaries_for_anchors::(excerpt_anchors) - .map(move |summary| { - let summary = cmp::min(excerpt_buffer_end.clone(), summary); - let mut position = position.clone(); - let excerpt_buffer_start = excerpt_buffer_start.clone(); - if summary > excerpt_buffer_start { - position.add_assign(&(summary - excerpt_buffer_start)); - } - position - }), - ); - continue; - } - } + let excerpt_start_position = D::from_text_summary(&cursor.start().text); + if let Some(excerpt) = cursor.item().filter(|excerpt| excerpt.id == excerpt_id) { + let excerpt_buffer_start = + excerpt.range.context.start.summary::(&excerpt.buffer); + let excerpt_buffer_end = excerpt.range.context.end.summary::(&excerpt.buffer); + for (buffer_summary, anchor) in excerpt + .buffer + .summaries_for_anchors_with_payload::( + excerpt_anchors.map(|a| (&a.text_anchor, a)), + ) + { + let summary = cmp::min(excerpt_buffer_end, buffer_summary); + let mut position = excerpt_start_position; + if summary > excerpt_buffer_start { + position.add_assign(&(summary - excerpt_buffer_start)); + } - summaries.extend(excerpt_anchors.map(|_| position.clone())); + if position > diff_transforms_cursor.start().0 .0 { + diff_transforms_cursor.seek_forward( + &ExcerptDimension(position), + Bias::Left, + &(), + ); + } + + summaries.push(self.resolve_summary_for_anchor( + anchor, + position, + &mut diff_transforms_cursor, + )); + } + } else { + diff_transforms_cursor.seek_forward( + &ExcerptDimension(excerpt_start_position), + Bias::Left, + &(), + ); + let position = self.resolve_summary_for_anchor( + &Anchor::max(), + excerpt_start_position, + &mut diff_transforms_cursor, + ); + summaries.extend(excerpt_anchors.map(|_| position)); + } } summaries @@ -3181,50 +4470,35 @@ impl MultiBufferSnapshot { points: impl 'a + IntoIterator, ) -> impl 'a + Iterator where - D: TextDimension, + D: TextDimension + Sub, { - let mut cursor = self.excerpts.cursor::(&()); - let mut memoized_source_start: Option = None; + let mut cursor = self.cursor::>(); + cursor.seek(&DimensionPair { + key: Point::default(), + value: None, + }); let mut points = points.into_iter(); - std::iter::from_fn(move || { + iter::from_fn(move || { let point = points.next()?; - // Clear the memoized source start if the point is in a different excerpt than previous. - if memoized_source_start.map_or(false, |_| point >= cursor.end(&()).lines) { - memoized_source_start = None; - } + cursor.seek_forward(&DimensionPair { + key: point, + value: None, + }); - // Now determine where the excerpt containing the point starts in its source buffer. - // We'll use this value to calculate overshoot next. - let source_start = if let Some(source_start) = memoized_source_start { - source_start + if let Some(region) = cursor.region() { + let overshoot = point - region.range.start.key; + let buffer_point = region.buffer_range.start.key + overshoot; + let mut position = region.range.start.value.unwrap(); + position.add_assign( + ®ion + .buffer + .text_summary_for_range(region.buffer_range.start.key..buffer_point), + ); + return Some(position); } else { - cursor.seek_forward(&point, Bias::Right, &()); - if let Some(excerpt) = cursor.item() { - let source_start = excerpt.range.context.start.to_point(&excerpt.buffer); - memoized_source_start = Some(source_start); - source_start - } else { - return Some(D::from_text_summary(cursor.start())); - } - }; - - // First, assume the output dimension is at least the start of the excerpt containing the point - let mut output = D::from_text_summary(cursor.start()); - - // If the point lands within its excerpt, calculate and add the overshoot in dimension D. - if let Some(excerpt) = cursor.item() { - let overshoot = point - cursor.start().lines; - if !overshoot.is_zero() { - let end_in_excerpt = source_start + overshoot; - output.add_assign( - &excerpt - .buffer - .text_summary_for_range::(source_start..end_in_excerpt), - ); - } + return Some(D::from_text_summary(&self.text_summary())); } - Some(output) }) } @@ -3298,6 +4572,7 @@ impl MultiBufferSnapshot { buffer_id: Some(excerpt.buffer_id), excerpt_id: excerpt.id, text_anchor, + diff_base_anchor: None, } } else if let Some(excerpt) = prev_excerpt { let mut text_anchor = excerpt @@ -3315,6 +4590,7 @@ impl MultiBufferSnapshot { buffer_id: Some(excerpt.buffer_id), excerpt_id: excerpt.id, text_anchor, + diff_base_anchor: None, } } else if anchor.text_anchor.bias == Bias::Left { Anchor::min() @@ -3340,22 +4616,67 @@ impl MultiBufferSnapshot { pub fn anchor_at(&self, position: T, mut bias: Bias) -> Anchor { let offset = position.to_offset(self); + + // Find the given position in the diff transforms. Determine the corresponding + // offset in the excerpts, and whether the position is within a deleted hunk. + let mut diff_transforms = self.diff_transforms.cursor::<(usize, ExcerptOffset)>(&()); + diff_transforms.seek(&offset, Bias::Right, &()); + + if offset == diff_transforms.start().0 && bias == Bias::Left { + if let Some(prev_item) = diff_transforms.prev_item() { + match prev_item { + DiffTransform::DeletedHunk { .. } => { + diff_transforms.prev(&()); + } + _ => {} + } + } + } + let offset_in_transform = offset - diff_transforms.start().0; + let mut excerpt_offset = diff_transforms.start().1; + let mut diff_base_anchor = None; + if let Some(DiffTransform::DeletedHunk { + buffer_id, + base_text_byte_range, + has_trailing_newline, + .. + }) = diff_transforms.item() + { + let diff_base = self.diffs.get(buffer_id).expect("missing diff base"); + if offset_in_transform > base_text_byte_range.len() { + debug_assert!(*has_trailing_newline); + bias = Bias::Right; + } else { + diff_base_anchor = Some( + diff_base + .base_text + .anchor_at(base_text_byte_range.start + offset_in_transform, bias), + ); + bias = Bias::Left; + } + } else { + excerpt_offset += ExcerptOffset::new(offset_in_transform); + }; + if let Some((excerpt_id, buffer_id, buffer)) = self.as_singleton() { return Anchor { buffer_id: Some(buffer_id), excerpt_id: *excerpt_id, - text_anchor: buffer.anchor_at(offset, bias), + text_anchor: buffer.anchor_at(excerpt_offset.value, bias), + diff_base_anchor, }; } - let mut cursor = self.excerpts.cursor::<(usize, Option)>(&()); - cursor.seek(&offset, Bias::Right, &()); - if cursor.item().is_none() && offset == cursor.start().0 && bias == Bias::Left { - cursor.prev(&()); + let mut excerpts = self + .excerpts + .cursor::<(ExcerptOffset, Option)>(&()); + excerpts.seek(&excerpt_offset, Bias::Right, &()); + if excerpts.item().is_none() && excerpt_offset == excerpts.start().0 && bias == Bias::Left { + excerpts.prev(&()); } - if let Some(excerpt) = cursor.item() { - let mut overshoot = offset.saturating_sub(cursor.start().0); - if excerpt.has_trailing_newline && offset == cursor.end(&()).0 { + if let Some(excerpt) = excerpts.item() { + let mut overshoot = excerpt_offset.saturating_sub(excerpts.start().0).value; + if excerpt.has_trailing_newline && excerpt_offset == excerpts.end(&()).0 { overshoot -= 1; bias = Bias::Right; } @@ -3367,8 +4688,9 @@ impl MultiBufferSnapshot { buffer_id: Some(excerpt.buffer_id), excerpt_id: excerpt.id, text_anchor, + diff_base_anchor, } - } else if offset == 0 && bias == Bias::Left { + } else if excerpt_offset.is_zero() && bias == Bias::Left { Anchor::min() } else { Anchor::max() @@ -3393,6 +4715,7 @@ impl MultiBufferSnapshot { buffer_id: Some(excerpt.buffer_id), excerpt_id, text_anchor, + diff_base_anchor: None, }); } } @@ -3413,24 +4736,6 @@ impl MultiBufferSnapshot { } } - pub fn buffer_ids_in_selected_rows( - &self, - selection: Selection, - ) -> impl Iterator + '_ { - let mut cursor = self.excerpts.cursor::(&()); - cursor.seek(&Point::new(selection.start.row, 0), Bias::Right, &()); - cursor.prev(&()); - - iter::from_fn(move || { - cursor.next(&()); - if cursor.start().row <= selection.end.row { - cursor.item().map(|item| item.buffer_id) - } else { - None - } - }) - } - pub fn excerpts( &self, ) -> impl Iterator)> { @@ -3439,86 +4744,40 @@ impl MultiBufferSnapshot { .map(|excerpt| (excerpt.id, &excerpt.buffer, excerpt.range.clone())) } - pub fn all_excerpts(&self) -> impl Iterator { - let mut cursor = self.excerpts.cursor::<(usize, Point)>(&()); - cursor.next(&()); - std::iter::from_fn(move || { - let excerpt = cursor.item()?; - let excerpt = MultiBufferExcerpt::new(excerpt, *cursor.start()); - cursor.next(&()); - Some(excerpt) - }) - } - - pub fn excerpts_for_range( - &self, - range: Range, - ) -> impl Iterator + '_ { - let range = range.start.to_offset(self)..range.end.to_offset(self); - - let mut cursor = self.excerpts.cursor::<(usize, Point)>(&()); - cursor.seek(&range.start, Bias::Right, &()); - cursor.prev(&()); - - iter::from_fn(move || { - cursor.next(&()); - if cursor.start().0 < range.end { - cursor - .item() - .map(|item| MultiBufferExcerpt::new(item, *cursor.start())) - } else { - None - } - }) - } - - pub fn excerpts_for_range_rev( - &self, - range: Range, - ) -> impl Iterator + '_ { - let range = range.start.to_offset(self)..range.end.to_offset(self); - - let mut cursor = self.excerpts.cursor::<(usize, Point)>(&()); - cursor.seek(&range.end, Bias::Left, &()); - if cursor.item().is_none() { - cursor.prev(&()); + fn cursor(&self) -> MultiBufferCursor { + let excerpts = self.excerpts.cursor(&()); + let diff_transforms = self.diff_transforms.cursor(&()); + MultiBufferCursor { + excerpts, + diff_transforms, + diffs: &self.diffs, + cached_region: None, } - - std::iter::from_fn(move || { - let excerpt = cursor.item()?; - let excerpt = MultiBufferExcerpt::new(excerpt, *cursor.start()); - cursor.prev(&()); - Some(excerpt) - }) } pub fn excerpt_before(&self, id: ExcerptId) -> Option> { let start_locator = self.excerpt_locator_for_id(id); - let mut cursor = self.excerpts.cursor::(&()); - cursor.seek(start_locator, Bias::Left, &()); - cursor.prev(&()); - let excerpt = cursor.item()?; - let excerpt_offset = cursor.start().text.len; - let excerpt_position = cursor.start().text.lines; - Some(MultiBufferExcerpt { - excerpt, - excerpt_offset, - excerpt_position, - }) - } + let mut excerpts = self + .excerpts + .cursor::<(Option<&Locator>, ExcerptDimension)>(&()); + excerpts.seek(&Some(start_locator), Bias::Left, &()); + excerpts.prev(&()); - pub fn excerpt_after(&self, id: ExcerptId) -> Option> { - let start_locator = self.excerpt_locator_for_id(id); - let mut cursor = self.excerpts.cursor::(&()); - cursor.seek(start_locator, Bias::Left, &()); - cursor.next(&()); - let excerpt = cursor.item()?; - let excerpt_offset = cursor.start().text.len; - let excerpt_position = cursor.start().text.lines; + let mut diff_transforms = self + .diff_transforms + .cursor::<(OutputDimension, ExcerptDimension)>(&()); + diff_transforms.seek(&excerpts.start().1, Bias::Left, &()); + if diff_transforms.end(&()).1 < excerpts.start().1 { + diff_transforms.next(&()); + } + + let excerpt = excerpts.item()?; Some(MultiBufferExcerpt { excerpt, - excerpt_offset, - excerpt_position, + offset: diff_transforms.start().0 .0, + buffer_offset: excerpt.range.context.start.to_offset(&excerpt.buffer), + excerpt_offset: excerpts.start().1.clone(), + diff_transforms, }) } @@ -3536,9 +4795,8 @@ impl MultiBufferSnapshot { start_offset = start.to_offset(self); Bound::Included(start_offset) } - Bound::Excluded(start) => { - start_offset = start.to_offset(self); - Bound::Excluded(start_offset) + Bound::Excluded(_) => { + panic!("not supported") } Bound::Unbounded => { start_offset = 0; @@ -3552,51 +4810,83 @@ impl MultiBufferSnapshot { }; let bounds = (start, end); - let mut cursor = self.excerpts.cursor::<(usize, Point)>(&()); - cursor.seek(&start_offset, Bias::Right, &()); - if cursor.item().is_none() { - cursor.prev(&()); - } - if !bounds.contains(&cursor.start().0) { - cursor.next(&()); + let mut cursor = self.cursor::>(); + cursor.seek(&DimensionPair { + key: start_offset, + value: None, + }); + + if cursor + .region() + .is_some_and(|region| bounds.contains(®ion.range.start.key)) + { + cursor.prev_excerpt(); + } else { + cursor.seek_to_start_of_current_excerpt(); } + let mut prev_region = cursor.region(); + + cursor.next_excerpt(); let mut visited_end = false; - std::iter::from_fn(move || { + iter::from_fn(move || { if self.singleton { - None - } else if bounds.contains(&cursor.start().0) { - let next = cursor.item().map(|excerpt| ExcerptInfo { - id: excerpt.id, - buffer: excerpt.buffer.clone(), - buffer_id: excerpt.buffer_id, - range: excerpt.range.clone(), - text_summary: excerpt.text_summary.clone(), - }); - - if next.is_none() { - if visited_end { - return None; - } else { - visited_end = true; - } - } - - let prev = cursor.prev_item().map(|prev_excerpt| ExcerptInfo { - id: prev_excerpt.id, - buffer: prev_excerpt.buffer.clone(), - buffer_id: prev_excerpt.buffer_id, - range: prev_excerpt.range.clone(), - text_summary: prev_excerpt.text_summary.clone(), - }); - let row = MultiBufferRow(cursor.start().1.row); - - cursor.next(&()); - - Some(ExcerptBoundary { row, prev, next }) - } else { - None + return None; } + + let next_region = cursor.region(); + cursor.next_excerpt(); + + let next_region_start = if let Some(region) = &next_region { + if !bounds.contains(®ion.range.start.key) { + return None; + } + region.range.start.value.unwrap() + } else { + if !bounds.contains(&self.len()) { + return None; + } + self.max_point() + }; + let next_region_end = if let Some(region) = cursor.region() { + region.range.start.value.unwrap() + } else { + self.max_point() + }; + + let prev = prev_region.as_ref().map(|region| ExcerptInfo { + id: region.excerpt.id, + buffer: region.excerpt.buffer.clone(), + buffer_id: region.excerpt.buffer_id, + range: region.excerpt.range.clone(), + end_row: MultiBufferRow(next_region_start.row), + }); + + let next = next_region.as_ref().map(|region| ExcerptInfo { + id: region.excerpt.id, + buffer: region.excerpt.buffer.clone(), + buffer_id: region.excerpt.buffer_id, + range: region.excerpt.range.clone(), + end_row: if region.excerpt.has_trailing_newline { + MultiBufferRow(next_region_end.row - 1) + } else { + MultiBufferRow(next_region_end.row) + }, + }); + + if next.is_none() { + if visited_end { + return None; + } else { + visited_end = true; + } + } + + let row = MultiBufferRow(next_region_start.row); + + prev_region = next_region; + + Some(ExcerptBoundary { row, prev, next }) }) } @@ -3616,20 +4906,18 @@ impl MultiBufferSnapshot { pub fn innermost_enclosing_bracket_ranges( &self, range: Range, - range_filter: Option<&dyn Fn(Range, Range) -> bool>, + range_filter: Option<&dyn Fn(&BufferSnapshot, Range, Range) -> bool>, ) -> Option<(Range, Range)> { let range = range.start.to_offset(self)..range.end.to_offset(self); - let excerpt = self.excerpt_containing(range.clone())?; + let mut excerpt = self.excerpt_containing(range.clone())?; + let buffer = excerpt.buffer(); + let excerpt_buffer_range = excerpt.buffer_range(); // Filter to ranges contained in the excerpt let range_filter = |open: Range, close: Range| -> bool { - excerpt.contains_buffer_range(open.start..close.end) - && range_filter.map_or(true, |filter| { - filter( - excerpt.map_range_from_buffer(open), - excerpt.map_range_from_buffer(close), - ) - }) + excerpt_buffer_range.contains(&open.start) + && excerpt_buffer_range.contains(&close.end) + && range_filter.map_or(true, |filter| filter(buffer, open, close)) }; let (open, close) = excerpt.buffer().innermost_enclosing_bracket_ranges( @@ -3650,7 +4938,7 @@ impl MultiBufferSnapshot { range: Range, ) -> Option, Range)> + '_> { let range = range.start.to_offset(self)..range.end.to_offset(self); - let excerpt = self.excerpt_containing(range.clone())?; + let mut excerpt = self.excerpt_containing(range.clone())?; Some( excerpt @@ -3669,6 +4957,31 @@ impl MultiBufferSnapshot { ) } + /// Returns enclosing bracket ranges containing the given range or returns None if the range is + /// not contained in a single excerpt + pub fn text_object_ranges( + &self, + range: Range, + options: TreeSitterOptions, + ) -> impl Iterator, TextObject)> + '_ { + let range = range.start.to_offset(self)..range.end.to_offset(self); + self.excerpt_containing(range.clone()) + .map(|mut excerpt| { + excerpt + .buffer() + .text_object_ranges(excerpt.map_range_to_buffer(range), options) + .filter_map(move |(range, text_object)| { + if excerpt.contains_buffer_range(range.clone()) { + Some((excerpt.map_range_from_buffer(range), text_object)) + } else { + None + } + }) + }) + .into_iter() + .flatten() + } + /// Returns bracket range pairs overlapping the given `range` or returns None if the `range` is /// not contained in a single excerpt pub fn bracket_ranges( @@ -3676,7 +4989,7 @@ impl MultiBufferSnapshot { range: Range, ) -> Option, Range)> + '_> { let range = range.start.to_offset(self)..range.end.to_offset(self); - let excerpt = self.excerpt_containing(range.clone())?; + let mut excerpt = self.excerpt_containing(range.clone())?; Some( excerpt @@ -3702,16 +5015,14 @@ impl MultiBufferSnapshot { redaction_enabled: impl Fn(Option<&Arc>) -> bool + 'a, ) -> impl Iterator> + 'a { let range = range.start.to_offset(self)..range.end.to_offset(self); - self.excerpts_for_range(range.clone()) - .filter(move |excerpt| redaction_enabled(excerpt.buffer().file())) - .flat_map(move |excerpt| { - excerpt - .buffer() - .redacted_ranges(excerpt.buffer_range().clone()) - .map(move |redacted_range| excerpt.map_range_from_buffer(redacted_range)) - .skip_while(move |redacted_range| redacted_range.end < range.start) - .take_while(move |redacted_range| redacted_range.start < range.end) - }) + self.lift_buffer_metadata(range, move |buffer, range| { + if redaction_enabled(buffer.file()) { + Some(buffer.redacted_ranges(range).map(|range| (range, ()))) + } else { + None + } + }) + .map(|(range, _, _)| range) } pub fn runnable_ranges( @@ -3719,90 +5030,335 @@ impl MultiBufferSnapshot { range: Range, ) -> impl Iterator + '_ { let range = range.start.to_offset(self)..range.end.to_offset(self); - self.excerpts_for_range(range.clone()) - .flat_map(move |excerpt| { - let excerpt_buffer_start = - excerpt.buffer_range().start.to_offset(&excerpt.buffer()); - - excerpt - .buffer() - .runnable_ranges(excerpt.buffer_range()) - .filter_map(move |mut runnable| { - // Re-base onto the excerpts coordinates in the multibuffer - // - // The node matching our runnables query might partially overlap with - // the provided range. If the run indicator is outside of excerpt bounds, do not actually show it. - if runnable.run_range.start < excerpt_buffer_start { - return None; - } - if language::ToPoint::to_point(&runnable.run_range.end, &excerpt.buffer()) - .row - > excerpt.max_buffer_row() - { - return None; - } - runnable.run_range = excerpt.map_range_from_buffer(runnable.run_range); - - Some(runnable) + self.lift_buffer_metadata(range, move |buffer, range| { + Some( + buffer + .runnable_ranges(range.clone()) + .filter(move |runnable| { + runnable.run_range.start >= range.start + && runnable.run_range.end < range.end }) - .skip_while(move |runnable| runnable.run_range.end < range.start) - .take_while(move |runnable| runnable.run_range.start < range.end) - }) + .map(|runnable| (runnable.run_range.clone(), runnable)), + ) + }) + .map(|(run_range, runnable, _)| language::RunnableRange { + run_range, + ..runnable + }) } - pub fn indent_guides_in_range( + pub fn line_indents( &self, - range: Range, - ignore_disabled_for_language: bool, - cx: &AppContext, - ) -> Vec { - // Fast path for singleton buffers, we can skip the conversion between offsets. - if let Some((_, _, snapshot)) = self.as_singleton() { - return snapshot - .indent_guides_in_range( - range.start.text_anchor..range.end.text_anchor, - ignore_disabled_for_language, - cx, - ) - .into_iter() - .map(|guide| MultiBufferIndentGuide { - multibuffer_row_range: MultiBufferRow(guide.start_row) - ..MultiBufferRow(guide.end_row), - buffer: guide, - }) - .collect(); + start_row: MultiBufferRow, + buffer_filter: impl Fn(&BufferSnapshot) -> bool, + ) -> impl Iterator { + let max_point = self.max_point(); + let mut cursor = self.cursor::(); + cursor.seek(&Point::new(start_row.0, 0)); + iter::from_fn(move || { + let mut region = cursor.region()?; + while !buffer_filter(®ion.excerpt.buffer) { + cursor.next(); + region = cursor.region()?; + } + let overshoot = start_row.0.saturating_sub(region.range.start.row); + let buffer_start_row = + (region.buffer_range.start.row + overshoot).min(region.buffer_range.end.row); + + let buffer_end_row = if region.is_main_buffer + && (region.has_trailing_newline || region.range.end == max_point) + { + region.buffer_range.end.row + } else { + region.buffer_range.end.row.saturating_sub(1) + }; + + let line_indents = region + .buffer + .line_indents_in_row_range(buffer_start_row..buffer_end_row); + cursor.next(); + return Some(line_indents.map(move |(buffer_row, indent)| { + let row = region.range.start.row + (buffer_row - region.buffer_range.start.row); + (MultiBufferRow(row), indent, ®ion.excerpt.buffer) + })); + }) + .flatten() + } + + pub fn reversed_line_indents( + &self, + end_row: MultiBufferRow, + buffer_filter: impl Fn(&BufferSnapshot) -> bool, + ) -> impl Iterator { + let max_point = self.max_point(); + let mut cursor = self.cursor::(); + cursor.seek(&Point::new(end_row.0, 0)); + iter::from_fn(move || { + let mut region = cursor.region()?; + while !buffer_filter(®ion.excerpt.buffer) { + cursor.prev(); + region = cursor.region()?; + } + + let buffer_start_row = region.buffer_range.start.row; + let buffer_end_row = if region.is_main_buffer + && (region.has_trailing_newline || region.range.end == max_point) + { + region.buffer_range.end.row + 1 + } else { + region.buffer_range.end.row + }; + + let overshoot = end_row.0 - region.range.start.row; + let buffer_end_row = + (region.buffer_range.start.row + overshoot + 1).min(buffer_end_row); + + let line_indents = region + .buffer + .reversed_line_indents_in_row_range(buffer_start_row..buffer_end_row); + cursor.prev(); + return Some(line_indents.map(move |(buffer_row, indent)| { + let row = region.range.start.row + (buffer_row - region.buffer_range.start.row); + (MultiBufferRow(row), indent, ®ion.excerpt.buffer) + })); + }) + .flatten() + } + + pub async fn enclosing_indent( + &self, + mut target_row: MultiBufferRow, + ) -> Option<(Range, LineIndent)> { + let max_row = MultiBufferRow(self.max_point().row); + if target_row >= max_row { + return None; } - let range = range.start.to_offset(self)..range.end.to_offset(self); + let mut target_indent = self.line_indent_for_row(target_row); - self.excerpts_for_range(range.clone()) - .flat_map(move |excerpt| { - let excerpt_buffer_start_row = - excerpt.buffer_range().start.to_point(&excerpt.buffer()).row; - let excerpt_offset_row = excerpt.start_point().row; + // If the current row is at the start of an indented block, we want to return this + // block as the enclosing indent. + if !target_indent.is_line_empty() && target_row < max_row { + let next_line_indent = self.line_indent_for_row(MultiBufferRow(target_row.0 + 1)); + if !next_line_indent.is_line_empty() + && target_indent.raw_len() < next_line_indent.raw_len() + { + target_indent = next_line_indent; + target_row.0 += 1; + } + } - excerpt - .buffer() - .indent_guides_in_range( - excerpt.buffer_range(), - ignore_disabled_for_language, - cx, - ) - .into_iter() - .map(move |indent_guide| { - let start_row = excerpt_offset_row - + (indent_guide.start_row - excerpt_buffer_start_row); - let end_row = - excerpt_offset_row + (indent_guide.end_row - excerpt_buffer_start_row); + const SEARCH_ROW_LIMIT: u32 = 25000; + const SEARCH_WHITESPACE_ROW_LIMIT: u32 = 2500; + const YIELD_INTERVAL: u32 = 100; - MultiBufferIndentGuide { - multibuffer_row_range: MultiBufferRow(start_row) - ..MultiBufferRow(end_row), - buffer: indent_guide, + let mut accessed_row_counter = 0; + + // If there is a blank line at the current row, search for the next non indented lines + if target_indent.is_line_empty() { + let start = MultiBufferRow(target_row.0.saturating_sub(SEARCH_WHITESPACE_ROW_LIMIT)); + let end = + MultiBufferRow((max_row.0 + 1).min(target_row.0 + SEARCH_WHITESPACE_ROW_LIMIT)); + + let mut non_empty_line_above = None; + for (row, indent, _) in self.reversed_line_indents(target_row, |_| true) { + if row < start { + break; + } + accessed_row_counter += 1; + if accessed_row_counter == YIELD_INTERVAL { + accessed_row_counter = 0; + yield_now().await; + } + if !indent.is_line_empty() { + non_empty_line_above = Some((row, indent)); + break; + } + } + + let mut non_empty_line_below = None; + for (row, indent, _) in self.line_indents(target_row, |_| true) { + if row > end { + break; + } + accessed_row_counter += 1; + if accessed_row_counter == YIELD_INTERVAL { + accessed_row_counter = 0; + yield_now().await; + } + if !indent.is_line_empty() { + non_empty_line_below = Some((row, indent)); + break; + } + } + + let (row, indent) = match (non_empty_line_above, non_empty_line_below) { + (Some((above_row, above_indent)), Some((below_row, below_indent))) => { + if above_indent.raw_len() >= below_indent.raw_len() { + (above_row, above_indent) + } else { + (below_row, below_indent) + } + } + (Some(above), None) => above, + (None, Some(below)) => below, + _ => return None, + }; + + target_indent = indent; + target_row = row; + } + + let start = MultiBufferRow(target_row.0.saturating_sub(SEARCH_ROW_LIMIT)); + let end = MultiBufferRow((max_row.0 + 1).min(target_row.0 + SEARCH_ROW_LIMIT)); + + let mut start_indent = None; + for (row, indent, _) in self.reversed_line_indents(target_row, |_| true) { + if row < start { + break; + } + accessed_row_counter += 1; + if accessed_row_counter == YIELD_INTERVAL { + accessed_row_counter = 0; + yield_now().await; + } + if !indent.is_line_empty() && indent.raw_len() < target_indent.raw_len() { + start_indent = Some((row, indent)); + break; + } + } + let (start_row, start_indent_size) = start_indent?; + + let mut end_indent = (end, None); + for (row, indent, _) in self.line_indents(target_row, |_| true) { + if row > end { + break; + } + accessed_row_counter += 1; + if accessed_row_counter == YIELD_INTERVAL { + accessed_row_counter = 0; + yield_now().await; + } + if !indent.is_line_empty() && indent.raw_len() < target_indent.raw_len() { + end_indent = (MultiBufferRow(row.0.saturating_sub(1)), Some(indent)); + break; + } + } + let (end_row, end_indent_size) = end_indent; + + let indent = if let Some(end_indent_size) = end_indent_size { + if start_indent_size.raw_len() > end_indent_size.raw_len() { + start_indent_size + } else { + end_indent_size + } + } else { + start_indent_size + }; + + Some((start_row..end_row, indent)) + } + + pub fn indent_guides_in_range( + &self, + range: Range, + ignore_disabled_for_language: bool, + cx: &AppContext, + ) -> impl Iterator { + let range = range.start.to_point(self)..range.end.to_point(self); + let start_row = MultiBufferRow(range.start.row); + let end_row = MultiBufferRow(range.end.row); + + let mut row_indents = self.line_indents(start_row, |buffer| { + let settings = + language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx); + settings.indent_guides.enabled || ignore_disabled_for_language + }); + + let mut result = Vec::new(); + let mut indent_stack = SmallVec::<[IndentGuide; 8]>::new(); + + while let Some((first_row, mut line_indent, buffer)) = row_indents.next() { + if first_row > end_row { + break; + } + let current_depth = indent_stack.len() as u32; + + let settings = + language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx); + let tab_size = settings.tab_size.get() as u32; + + // When encountering empty, continue until found useful line indent + // then add to the indent stack with the depth found + let mut found_indent = false; + let mut last_row = first_row; + if line_indent.is_line_empty() { + while !found_indent { + let Some((target_row, new_line_indent, _)) = row_indents.next() else { + break; + }; + const TRAILING_ROW_SEARCH_LIMIT: u32 = 25; + if target_row > MultiBufferRow(end_row.0 + TRAILING_ROW_SEARCH_LIMIT) { + break; + } + + if new_line_indent.is_line_empty() { + continue; + } + last_row = target_row.min(end_row); + line_indent = new_line_indent; + found_indent = true; + break; + } + } else { + found_indent = true + } + + let depth = if found_indent { + line_indent.len(tab_size) / tab_size + + ((line_indent.len(tab_size) % tab_size) > 0) as u32 + } else { + current_depth + }; + + match depth.cmp(¤t_depth) { + cmp::Ordering::Less => { + for _ in 0..(current_depth - depth) { + let mut indent = indent_stack.pop().unwrap(); + if last_row != first_row { + // In this case, we landed on an empty row, had to seek forward, + // and discovered that the indent we where on is ending. + // This means that the last display row must + // be on line that ends this indent range, so we + // should display the range up to the first non-empty line + indent.end_row = MultiBufferRow(first_row.0.saturating_sub(1)); } - }) - }) - .collect() + + result.push(indent) + } + } + cmp::Ordering::Greater => { + for next_depth in current_depth..depth { + indent_stack.push(IndentGuide { + buffer_id: buffer.remote_id(), + start_row: first_row, + end_row: last_row, + depth: next_depth, + tab_size, + settings: settings.indent_guides, + }); + } + } + _ => {} + } + + for indent in indent_stack.iter_mut() { + indent.end_row = last_row; + } + } + + result.extend(indent_stack); + result.into_iter() } pub fn trailing_excerpt_update_count(&self) -> usize { @@ -3873,41 +5429,39 @@ impl MultiBufferSnapshot { pub fn diagnostic_group( &self, + buffer_id: BufferId, group_id: usize, - ) -> impl Iterator> + '_ { - self.all_excerpts().flat_map(move |excerpt| { - excerpt.buffer().diagnostic_group(group_id).map( - move |DiagnosticEntry { diagnostic, range }| DiagnosticEntry { - diagnostic, - range: self.anchor_in_excerpt(excerpt.id(), range.start).unwrap() - ..self.anchor_in_excerpt(excerpt.id(), range.end).unwrap(), - }, + ) -> impl Iterator> + '_ { + self.lift_buffer_metadata(0..self.len(), move |buffer, _| { + if buffer.remote_id() != buffer_id { + return None; + }; + Some( + buffer + .diagnostic_group(group_id) + .map(move |DiagnosticEntry { diagnostic, range }| (range, diagnostic)), ) }) + .map(|(range, diagnostic, _)| DiagnosticEntry { diagnostic, range }) } - pub fn diagnostics_in_range<'a, T>( + pub fn diagnostics_in_range<'a, T, O>( &'a self, range: Range, - reversed: bool, - ) -> impl Iterator> + 'a + ) -> impl Iterator> + 'a where T: 'a + ToOffset, + O: 'a + text::FromAnchor + Copy + TextDimension + Ord + Sub + fmt::Debug, { - let mut ranges = self.range_to_buffer_ranges(range); - if reversed { - ranges.reverse(); - } - ranges.into_iter().flat_map(move |(excerpt, range)| { - let excerpt_id = excerpt.id(); - excerpt.buffer().diagnostics_in_range(range, reversed).map( - move |DiagnosticEntry { diagnostic, range }| DiagnosticEntry { - diagnostic, - range: self.anchor_in_excerpt(excerpt_id, range.start).unwrap() - ..self.anchor_in_excerpt(excerpt_id, range.end).unwrap(), - }, + let range = range.start.to_offset(self)..range.end.to_offset(self); + self.lift_buffer_metadata(range, move |buffer, buffer_range| { + Some( + buffer + .diagnostics_in_range(buffer_range.start..buffer_range.end, false) + .map(|entry| (entry.range, entry.diagnostic)), ) }) + .map(|(range, diagnostic, _)| DiagnosticEntry { diagnostic, range }) } pub fn syntax_ancestor( @@ -3915,7 +5469,7 @@ impl MultiBufferSnapshot { range: Range, ) -> Option<(tree_sitter::Node, Range)> { let range = range.start.to_offset(self)..range.end.to_offset(self); - let excerpt = self.excerpt_containing(range.clone())?; + let mut excerpt = self.excerpt_containing(range.clone())?; let node = excerpt .buffer() .syntax_ancestor(excerpt.map_range_to_buffer(range))?; @@ -4058,21 +5612,40 @@ impl MultiBufferSnapshot { Some(&self.excerpt(excerpt_id)?.buffer) } - pub fn range_for_excerpt<'a, T: sum_tree::Dimension<'a, ExcerptSummary>>( - &'a self, - excerpt_id: ExcerptId, - ) -> Option> { - let mut cursor = self.excerpts.cursor::<(Option<&Locator>, T)>(&()); + pub fn range_for_excerpt(&self, excerpt_id: ExcerptId) -> Option> { + let mut cursor = self + .excerpts + .cursor::<(Option<&Locator>, ExcerptDimension)>(&()); let locator = self.excerpt_locator_for_id(excerpt_id); if cursor.seek(&Some(locator), Bias::Left, &()) { let start = cursor.start().1.clone(); let end = cursor.end(&()).1; + let mut diff_transforms = self + .diff_transforms + .cursor::<(ExcerptDimension, OutputDimension)>(&()); + diff_transforms.seek(&start, Bias::Left, &()); + let overshoot = start.0 - diff_transforms.start().0 .0; + let start = diff_transforms.start().1 .0 + overshoot; + diff_transforms.seek(&end, Bias::Right, &()); + let overshoot = end.0 - diff_transforms.start().0 .0; + let end = diff_transforms.start().1 .0 + overshoot; Some(start..end) } else { None } } + pub fn buffer_range_for_excerpt(&self, excerpt_id: ExcerptId) -> Option> { + let mut cursor = self.excerpts.cursor::>(&()); + let locator = self.excerpt_locator_for_id(excerpt_id); + if cursor.seek(&Some(locator), Bias::Left, &()) { + if let Some(excerpt) = cursor.item() { + return Some(excerpt.range.context.clone()); + } + } + None + } + fn excerpt(&self, excerpt_id: ExcerptId) -> Option<&Excerpt> { let mut cursor = self.excerpts.cursor::>(&()); let locator = self.excerpt_locator_for_id(excerpt_id); @@ -4088,149 +5661,28 @@ impl MultiBufferSnapshot { /// Returns the excerpt containing range and its offset start within the multibuffer or none if `range` spans multiple excerpts pub fn excerpt_containing(&self, range: Range) -> Option { let range = range.start.to_offset(self)..range.end.to_offset(self); + let mut cursor = self.cursor::(); + cursor.seek(&range.start); - let mut cursor = self.excerpts.cursor::<(usize, Point)>(&()); - cursor.seek(&range.start, Bias::Right, &()); - let start_excerpt = cursor.item()?; - - if range.start == range.end { - return Some(MultiBufferExcerpt::new(start_excerpt, *cursor.start())); + let start_excerpt = cursor.excerpt()?; + if range.end != range.start { + cursor.seek_forward(&range.end); + if cursor.excerpt()?.id != start_excerpt.id { + return None; + } } - cursor.seek(&range.end, Bias::Right, &()); - let end_excerpt = cursor.item()?; - - if start_excerpt.id == end_excerpt.id { - Some(MultiBufferExcerpt::new(start_excerpt, *cursor.start())) - } else { - None - } - } - - // Takes an iterator over anchor ranges and returns a new iterator over anchor ranges that don't - // span across excerpt boundaries. - pub fn split_ranges<'a, I>(&'a self, ranges: I) -> impl Iterator> + 'a - where - I: IntoIterator> + 'a, - { - let mut ranges = ranges.into_iter().map(|range| range.to_offset(self)); - let mut cursor = self.excerpts.cursor::<(usize, Point)>(&()); - cursor.next(&()); - let mut current_range = ranges.next(); - iter::from_fn(move || { - let range = current_range.clone()?; - if range.start >= cursor.end(&()).0 { - cursor.seek_forward(&range.start, Bias::Right, &()); - if range.start == self.len() { - cursor.prev(&()); - } - } - - let excerpt = cursor.item()?; - let range_start_in_excerpt = cmp::max(range.start, cursor.start().0); - let range_end_in_excerpt = if excerpt.has_trailing_newline { - cmp::min(range.end, cursor.end(&()).0 - 1) - } else { - cmp::min(range.end, cursor.end(&()).0) - }; - let buffer_range = MultiBufferExcerpt::new(excerpt, *cursor.start()) - .map_range_to_buffer(range_start_in_excerpt..range_end_in_excerpt); - - let subrange_start_anchor = Anchor { - buffer_id: Some(excerpt.buffer_id), - excerpt_id: excerpt.id, - text_anchor: excerpt.buffer.anchor_before(buffer_range.start), - }; - let subrange_end_anchor = Anchor { - buffer_id: Some(excerpt.buffer_id), - excerpt_id: excerpt.id, - text_anchor: excerpt.buffer.anchor_after(buffer_range.end), - }; - - if range.end > cursor.end(&()).0 { - cursor.next(&()); - } else { - current_range = ranges.next(); - } - - Some(subrange_start_anchor..subrange_end_anchor) - }) - } - - pub fn range_to_buffer_ranges( - &self, - range: Range, - ) -> Vec<(MultiBufferExcerpt<'_>, Range)> { - let start = range.start.to_offset(self); - let end = range.end.to_offset(self); - - let mut result = Vec::new(); - let mut cursor = self.excerpts.cursor::<(usize, Point)>(&()); - cursor.seek(&start, Bias::Right, &()); - if cursor.item().is_none() { - cursor.prev(&()); - } - - while let Some(excerpt) = cursor.item() { - if cursor.start().0 > end { - break; - } - - let mut end_before_newline = cursor.end(&()).0; - if excerpt.has_trailing_newline { - end_before_newline -= 1; - } - let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let start = excerpt_start + (cmp::max(start, cursor.start().0) - cursor.start().0); - let end = excerpt_start + (cmp::min(end, end_before_newline) - cursor.start().0); - result.push(( - MultiBufferExcerpt::new(&excerpt, *cursor.start()), - start..end, - )); - cursor.next(&()); - } - - result - } - - /// Returns excerpts overlapping the given ranges. If range spans multiple excerpts returns one range for each excerpt - /// - /// The ranges are specified in the coordinate space of the multibuffer, not the individual excerpted buffers. - /// Each returned excerpt's range is in the coordinate space of its source buffer. - pub fn excerpts_in_ranges( - &self, - ranges: impl IntoIterator>, - ) -> impl Iterator)> { - let mut ranges = ranges.into_iter().map(|range| range.to_offset(self)); - let mut cursor = self.excerpts.cursor::<(usize, Point)>(&()); - cursor.next(&()); - let mut current_range = ranges.next(); - iter::from_fn(move || { - let range = current_range.clone()?; - if range.start >= cursor.end(&()).0 { - cursor.seek_forward(&range.start, Bias::Right, &()); - if range.start == self.len() { - cursor.prev(&()); - } - } - - let excerpt = cursor.item()?; - let range_start_in_excerpt = cmp::max(range.start, cursor.start().0); - let range_end_in_excerpt = if excerpt.has_trailing_newline { - cmp::min(range.end, cursor.end(&()).0 - 1) - } else { - cmp::min(range.end, cursor.end(&()).0) - }; - let buffer_range = MultiBufferExcerpt::new(excerpt, *cursor.start()) - .map_range_to_buffer(range_start_in_excerpt..range_end_in_excerpt); - - if range.end > cursor.end(&()).0 { - cursor.next(&()); - } else { - current_range = ranges.next(); - } - - Some((excerpt.id, &excerpt.buffer, buffer_range)) + cursor.seek_to_start_of_current_excerpt(); + let region = cursor.region()?; + let offset = region.range.start; + let buffer_offset = region.buffer_range.start; + let excerpt_offset = cursor.excerpts.start().clone(); + Some(MultiBufferExcerpt { + diff_transforms: cursor.diff_transforms, + excerpt: start_excerpt, + offset, + buffer_offset, + excerpt_offset, }) } @@ -4263,11 +5715,13 @@ impl MultiBufferSnapshot { buffer_id: Some(excerpt.buffer_id), excerpt_id: excerpt.id, text_anchor: selection.start, + diff_base_anchor: None, }; let mut end = Anchor { buffer_id: Some(excerpt.buffer_id), excerpt_id: excerpt.id, text_anchor: selection.end, + diff_base_anchor: None, }; if range.start.cmp(&start, self).is_gt() { start = range.start; @@ -4305,6 +5759,282 @@ impl MultiBufferSnapshot { let start = self.clip_offset(rng.gen_range(start_offset..=end), Bias::Right); start..end } + + #[cfg(any(test, feature = "test-support"))] + fn check_invariants(&self) { + let excerpts = self.excerpts.items(&()); + let excerpt_ids = self.excerpt_ids.items(&()); + + for (ix, excerpt) in excerpts.iter().enumerate() { + if ix == 0 { + if excerpt.locator <= Locator::min() { + panic!("invalid first excerpt locator {:?}", excerpt.locator); + } + } else if excerpt.locator <= excerpts[ix - 1].locator { + panic!("excerpts are out-of-order: {:?}", excerpts); + } + } + + for (ix, entry) in excerpt_ids.iter().enumerate() { + if ix == 0 { + if entry.id.cmp(&ExcerptId::min(), &self).is_le() { + panic!("invalid first excerpt id {:?}", entry.id); + } + } else if entry.id <= excerpt_ids[ix - 1].id { + panic!("excerpt ids are out-of-order: {:?}", excerpt_ids); + } + } + + if self.diff_transforms.summary().input != self.excerpts.summary().text { + panic!( + "incorrect input summary. expected {:?}, got {:?}. transforms: {:+?}", + self.excerpts.summary().text.len, + 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_anchor, + } = item + { + if let Some(DiffTransform::BufferContent { + inserted_hunk_anchor: prev_inserted_hunk_anchor, + .. + }) = prev_transform + { + if *inserted_hunk_anchor == *prev_inserted_hunk_anchor { + panic!( + "multiple adjacent buffer content transforms with is_inserted_hunk = {inserted_hunk_anchor:?}. transforms: {:+?}", + self.diff_transforms.items(&())); + } + } + if summary.len == 0 && !self.is_empty() { + panic!("empty buffer content transform"); + } + } + prev_transform = Some(item); + } + } +} + +impl<'a, D> MultiBufferCursor<'a, D> +where + D: TextDimension + Ord + Sub, +{ + fn seek(&mut self, position: &D) { + self.cached_region.take(); + self.diff_transforms + .seek(&OutputDimension(*position), Bias::Right, &()); + if self.diff_transforms.item().is_none() && *position == self.diff_transforms.start().0 .0 { + self.diff_transforms.prev(&()); + } + + let mut excerpt_position = self.diff_transforms.start().1 .0; + if let Some(DiffTransform::BufferContent { .. }) = self.diff_transforms.item() { + let overshoot = *position - self.diff_transforms.start().0 .0; + excerpt_position.add_assign(&overshoot); + } + + self.excerpts + .seek(&ExcerptDimension(excerpt_position), Bias::Right, &()); + if self.excerpts.item().is_none() && excerpt_position == self.excerpts.start().0 { + self.excerpts.prev(&()); + } + } + + fn seek_forward(&mut self, position: &D) { + self.cached_region.take(); + self.diff_transforms + .seek_forward(&OutputDimension(*position), Bias::Right, &()); + if self.diff_transforms.item().is_none() && *position == self.diff_transforms.start().0 .0 { + self.diff_transforms.prev(&()); + } + + let overshoot = *position - self.diff_transforms.start().0 .0; + let mut excerpt_position = self.diff_transforms.start().1 .0; + if let Some(DiffTransform::BufferContent { .. }) = self.diff_transforms.item() { + excerpt_position.add_assign(&overshoot); + } + + self.excerpts + .seek_forward(&ExcerptDimension(excerpt_position), Bias::Right, &()); + if self.excerpts.item().is_none() && excerpt_position == self.excerpts.start().0 { + self.excerpts.prev(&()); + } + } + + fn seek_to_buffer_position_in_current_excerpt(&mut self, position: &D) { + self.cached_region.take(); + if let Some(excerpt) = self.excerpts.item() { + let excerpt_start = excerpt.range.context.start.summary::(&excerpt.buffer); + let position_in_excerpt = *position - excerpt_start; + let mut excerpt_position = self.excerpts.start().0; + excerpt_position.add_assign(&position_in_excerpt); + self.diff_transforms + .seek(&ExcerptDimension(excerpt_position), Bias::Left, &()); + if self.diff_transforms.item().is_none() { + self.diff_transforms.next(&()); + } + } + } + + fn next_excerpt(&mut self) { + self.excerpts.next(&()); + self.seek_to_start_of_current_excerpt(); + } + + fn prev_excerpt(&mut self) { + self.excerpts.prev(&()); + self.seek_to_start_of_current_excerpt(); + } + + fn seek_to_start_of_current_excerpt(&mut self) { + self.cached_region.take(); + self.diff_transforms + .seek(self.excerpts.start(), Bias::Left, &()); + if self.diff_transforms.end(&()).1 == *self.excerpts.start() + && self.diff_transforms.start().1 < *self.excerpts.start() + && self.diff_transforms.next_item().is_some() + { + self.diff_transforms.next(&()); + } + } + + fn next(&mut self) { + self.cached_region.take(); + match self.diff_transforms.end(&()).1.cmp(&self.excerpts.end(&())) { + cmp::Ordering::Less => self.diff_transforms.next(&()), + cmp::Ordering::Greater => self.excerpts.next(&()), + cmp::Ordering::Equal => { + self.diff_transforms.next(&()); + if self.diff_transforms.end(&()).1 > self.excerpts.end(&()) + || self.diff_transforms.item().is_none() + { + self.excerpts.next(&()); + } + } + } + } + + fn prev(&mut self) { + self.cached_region.take(); + match self.diff_transforms.start().1.cmp(self.excerpts.start()) { + cmp::Ordering::Less => self.excerpts.prev(&()), + cmp::Ordering::Greater => self.diff_transforms.prev(&()), + cmp::Ordering::Equal => { + self.diff_transforms.prev(&()); + if self.diff_transforms.start().1 < *self.excerpts.start() + || self.diff_transforms.item().is_none() + { + self.excerpts.prev(&()); + } + } + } + } + + fn region(&mut self) -> Option> { + if self.cached_region.is_none() { + self.cached_region = self.build_region(); + } + self.cached_region.clone() + } + + fn main_buffer_position(&self) -> Option { + if let DiffTransform::BufferContent { .. } = self.diff_transforms.next_item()? { + let excerpt = self.excerpts.item()?; + let buffer = &excerpt.buffer; + let buffer_context_start = excerpt.range.context.start.summary::(buffer); + let mut buffer_start = buffer_context_start; + let overshoot = self.diff_transforms.end(&()).1 .0 - self.excerpts.start().0; + buffer_start.add_assign(&overshoot); + Some(buffer_start) + } else { + None + } + } + + fn build_region(&self) -> Option> { + let excerpt = self.excerpts.item()?; + match self.diff_transforms.item()? { + DiffTransform::DeletedHunk { + buffer_id, + base_text_byte_range, + has_trailing_newline, + .. + } => { + let diff = self.diffs.get(&buffer_id)?; + let buffer = &diff.base_text; + let mut rope_cursor = buffer.as_rope().cursor(0); + let buffer_start = rope_cursor.summary::(base_text_byte_range.start); + let buffer_range_len = rope_cursor.summary::(base_text_byte_range.end); + let mut buffer_end = buffer_start; + buffer_end.add_assign(&buffer_range_len); + let start = self.diff_transforms.start().0 .0; + let end = self.diff_transforms.end(&()).0 .0; + return Some(MultiBufferRegion { + buffer, + excerpt, + has_trailing_newline: *has_trailing_newline, + is_main_buffer: false, + is_inserted_hunk: false, + buffer_range: buffer_start..buffer_end, + range: start..end, + }); + } + DiffTransform::BufferContent { + inserted_hunk_anchor, + .. + } => { + let buffer = &excerpt.buffer; + let buffer_context_start = excerpt.range.context.start.summary::(buffer); + + let mut start = self.diff_transforms.start().0 .0; + let mut buffer_start = buffer_context_start; + if self.diff_transforms.start().1 < *self.excerpts.start() { + let overshoot = self.excerpts.start().0 - self.diff_transforms.start().1 .0; + start.add_assign(&overshoot); + } else { + let overshoot = self.diff_transforms.start().1 .0 - self.excerpts.start().0; + buffer_start.add_assign(&overshoot); + } + + let mut end; + let mut buffer_end; + let has_trailing_newline; + if self.diff_transforms.end(&()).1 .0 < self.excerpts.end(&()).0 { + let overshoot = self.diff_transforms.end(&()).1 .0 - self.excerpts.start().0; + end = self.diff_transforms.end(&()).0 .0; + buffer_end = buffer_context_start; + buffer_end.add_assign(&overshoot); + has_trailing_newline = false; + } else { + let overshoot = self.excerpts.end(&()).0 - self.diff_transforms.start().1 .0; + end = self.diff_transforms.start().0 .0; + end.add_assign(&overshoot); + buffer_end = excerpt.range.context.end.summary::(buffer); + has_trailing_newline = excerpt.has_trailing_newline; + }; + + Some(MultiBufferRegion { + buffer, + excerpt, + has_trailing_newline, + is_main_buffer: true, + is_inserted_hunk: inserted_hunk_anchor.is_some(), + buffer_range: buffer_start..buffer_end, + range: start..end, + }) + } + } + } + + fn excerpt(&self) -> Option<&'a Excerpt> { + self.excerpts.item() + } } impl History { @@ -4569,48 +6299,6 @@ impl Excerpt { }; } - fn bytes_in_range(&self, range: Range) -> ExcerptBytes { - let content_start = self.range.context.start.to_offset(&self.buffer); - let bytes_start = content_start + range.start; - let bytes_end = content_start + cmp::min(range.end, self.text_summary.len); - let footer_height = if self.has_trailing_newline - && range.start <= self.text_summary.len - && range.end > self.text_summary.len - { - 1 - } else { - 0 - }; - let content_bytes = self.buffer.bytes_in_range(bytes_start..bytes_end); - - ExcerptBytes { - content_bytes, - padding_height: footer_height, - reversed: false, - } - } - - fn reversed_bytes_in_range(&self, range: Range) -> ExcerptBytes { - let content_start = self.range.context.start.to_offset(&self.buffer); - let bytes_start = content_start + range.start; - let bytes_end = content_start + cmp::min(range.end, self.text_summary.len); - let footer_height = if self.has_trailing_newline - && range.start <= self.text_summary.len - && range.end > self.text_summary.len - { - 1 - } else { - 0 - }; - let content_bytes = self.buffer.reversed_bytes_in_range(bytes_start..bytes_end); - - ExcerptBytes { - content_bytes, - padding_height: footer_height, - reversed: true, - } - } - fn clip_anchor(&self, text_anchor: text::Anchor) -> text::Anchor { if text_anchor .cmp(&self.range.context.start, &self.buffer) @@ -4648,11 +6336,6 @@ impl Excerpt { self.range.context.start.to_offset(&self.buffer) } - /// The [`Excerpt`]'s start point in its [`Buffer`] - fn buffer_start_point(&self) -> Point { - self.range.context.start.to_point(&self.buffer) - } - /// The [`Excerpt`]'s end offset in its [`Buffer`] fn buffer_end_offset(&self) -> usize { self.buffer_start_offset() + self.text_summary.len @@ -4660,14 +6343,6 @@ impl Excerpt { } impl<'a> MultiBufferExcerpt<'a> { - fn new(excerpt: &'a Excerpt, (excerpt_offset, excerpt_position): (usize, Point)) -> Self { - MultiBufferExcerpt { - excerpt, - excerpt_offset, - excerpt_position, - } - } - pub fn id(&self) -> ExcerptId { self.excerpt.id } @@ -4681,6 +6356,7 @@ impl<'a> MultiBufferExcerpt<'a> { buffer_id: Some(self.excerpt.buffer_id), excerpt_id: self.excerpt.id, text_anchor: self.excerpt.range.context.start, + diff_base_anchor: None, } } @@ -4689,6 +6365,7 @@ impl<'a> MultiBufferExcerpt<'a> { buffer_id: Some(self.excerpt.buffer_id), excerpt_id: self.excerpt.id, text_anchor: self.excerpt.range.context.end, + diff_base_anchor: None, } } @@ -4696,59 +6373,75 @@ impl<'a> MultiBufferExcerpt<'a> { &self.excerpt.buffer } - pub fn buffer_range(&self) -> Range { - self.excerpt.range.context.clone() + pub fn buffer_range(&self) -> Range { + self.buffer_offset + ..self + .excerpt + .range + .context + .end + .to_offset(&self.excerpt.buffer.text) } pub fn start_offset(&self) -> usize { - self.excerpt_offset - } - - pub fn start_point(&self) -> Point { - self.excerpt_position + self.offset } /// Maps an offset within the [`MultiBuffer`] to an offset within the [`Buffer`] - pub fn map_offset_to_buffer(&self, offset: usize) -> usize { - self.excerpt.buffer_start_offset() - + offset - .saturating_sub(self.excerpt_offset) - .min(self.excerpt.text_summary.len) - } - - /// Maps a point within the [`MultiBuffer`] to a point within the [`Buffer`] - pub fn map_point_to_buffer(&self, point: Point) -> Point { - self.excerpt.buffer_start_point() - + point - .saturating_sub(self.excerpt_position) - .min(self.excerpt.text_summary.lines) + pub fn map_offset_to_buffer(&mut self, offset: usize) -> usize { + self.map_range_to_buffer(offset..offset).start } /// Maps a range within the [`MultiBuffer`] to a range within the [`Buffer`] - pub fn map_range_to_buffer(&self, range: Range) -> Range { - self.map_offset_to_buffer(range.start)..self.map_offset_to_buffer(range.end) + pub fn map_range_to_buffer(&mut self, range: Range) -> Range { + self.diff_transforms + .seek(&OutputDimension(range.start), Bias::Right, &()); + let start = self.map_offset_to_buffer_internal(range.start); + let end = if range.end > range.start { + self.diff_transforms + .seek_forward(&OutputDimension(range.end), Bias::Right, &()); + self.map_offset_to_buffer_internal(range.end) + } else { + start + }; + start..end + } + + fn map_offset_to_buffer_internal(&self, offset: usize) -> usize { + let mut excerpt_offset = self.diff_transforms.start().1.clone(); + if let Some(DiffTransform::BufferContent { .. }) = self.diff_transforms.item() { + excerpt_offset.0 += offset - self.diff_transforms.start().0 .0; + }; + let offset_in_excerpt = excerpt_offset.0.saturating_sub(self.excerpt_offset.0); + self.buffer_offset + offset_in_excerpt } /// Map an offset within the [`Buffer`] to an offset within the [`MultiBuffer`] - pub fn map_offset_from_buffer(&self, buffer_offset: usize) -> usize { - let buffer_offset_in_excerpt = buffer_offset - .saturating_sub(self.excerpt.buffer_start_offset()) - .min(self.excerpt.text_summary.len); - self.excerpt_offset + buffer_offset_in_excerpt - } - - /// Map a point within the [`Buffer`] to a point within the [`MultiBuffer`] - pub fn map_point_from_buffer(&self, buffer_position: Point) -> Point { - let position_in_excerpt = buffer_position.saturating_sub(self.excerpt.buffer_start_point()); - let position_in_excerpt = - position_in_excerpt.min(self.excerpt.text_summary.lines + Point::new(1, 0)); - self.excerpt_position + position_in_excerpt + pub fn map_offset_from_buffer(&mut self, buffer_offset: usize) -> usize { + self.map_range_from_buffer(buffer_offset..buffer_offset) + .start } /// Map a range within the [`Buffer`] to a range within the [`MultiBuffer`] - pub fn map_range_from_buffer(&self, buffer_range: Range) -> Range { - self.map_offset_from_buffer(buffer_range.start) - ..self.map_offset_from_buffer(buffer_range.end) + pub fn map_range_from_buffer(&mut self, buffer_range: Range) -> Range { + let overshoot = buffer_range.start - self.buffer_offset; + let excerpt_offset = ExcerptDimension(self.excerpt_offset.0 + overshoot); + self.diff_transforms.seek(&excerpt_offset, Bias::Right, &()); + let overshoot = excerpt_offset.0 - self.diff_transforms.start().1 .0; + let start = self.diff_transforms.start().0 .0 + overshoot; + + let end = if buffer_range.end > buffer_range.start { + let overshoot = buffer_range.end - self.buffer_offset; + let excerpt_offset = ExcerptDimension(self.excerpt_offset.0 + overshoot); + self.diff_transforms + .seek_forward(&excerpt_offset, Bias::Right, &()); + let overshoot = excerpt_offset.0 - self.diff_transforms.start().1 .0; + self.diff_transforms.start().0 .0 + overshoot + } else { + start + }; + + start..end } /// Returns true if the entirety of the given range is in the buffer's excerpt @@ -4809,7 +6502,7 @@ impl sum_tree::Item for Excerpt { type Summary = ExcerptSummary; fn summary(&self, _cx: &()) -> Self::Summary { - let mut text = self.text_summary.clone(); + let mut text = self.text_summary; if self.has_trailing_newline { text += TextSummary::from("\n"); } @@ -4838,6 +6531,57 @@ impl sum_tree::KeyedItem for ExcerptIdMapping { } } +impl DiffTransform { + fn hunk_anchor(&self) -> Option<(ExcerptId, text::Anchor)> { + match self { + DiffTransform::DeletedHunk { hunk_anchor, .. } => Some(*hunk_anchor), + DiffTransform::BufferContent { + inserted_hunk_anchor, + .. + } => *inserted_hunk_anchor, + } + } +} + +impl sum_tree::Item for DiffTransform { + type Summary = DiffTransformSummary; + + fn summary(&self, _: &::Context) -> Self::Summary { + match self { + DiffTransform::BufferContent { summary, .. } => DiffTransformSummary { + input: *summary, + output: *summary, + }, + DiffTransform::DeletedHunk { summary, .. } => DiffTransformSummary { + input: TextSummary::default(), + output: *summary, + }, + } + } +} + +impl DiffTransformSummary { + fn excerpt_len(&self) -> ExcerptOffset { + ExcerptOffset::new(self.input.len) + } +} + +impl sum_tree::Summary for DiffTransformSummary { + type Context = (); + + fn zero(_: &Self::Context) -> Self { + DiffTransformSummary { + input: TextSummary::default(), + output: TextSummary::default(), + } + } + + fn add_summary(&mut self, summary: &Self, _: &Self::Context) { + self.input += &summary.input; + self.output += &summary.output; + } +} + impl sum_tree::Summary for ExcerptId { type Context = (); @@ -4865,35 +6609,19 @@ impl sum_tree::Summary for ExcerptSummary { } } -impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for TextSummary { +impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for ExcerptOffset { fn zero(_cx: &()) -> Self { Default::default() } fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { - *self += &summary.text; + self.value += summary.text.len; } } -impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for usize { - fn zero(_cx: &()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { - *self += summary.text.len; - } -} - -impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for usize { +impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for ExcerptOffset { fn cmp(&self, cursor_location: &ExcerptSummary, _: &()) -> cmp::Ordering { - Ord::cmp(self, &cursor_location.text.len) - } -} - -impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, TextSummary> for Point { - fn cmp(&self, cursor_location: &TextSummary, _: &()) -> cmp::Ordering { - Ord::cmp(self, &cursor_location.lines) + Ord::cmp(&self.value, &cursor_location.text.len) } } @@ -4909,33 +6637,25 @@ impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for Locator { } } -impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for OffsetUtf16 { +impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for ExcerptPoint { fn zero(_cx: &()) -> Self { Default::default() } fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { - *self += summary.text.len_utf16; + self.value += summary.text.lines; } } -impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Point { - fn zero(_cx: &()) -> Self { - Default::default() +impl<'a, D: TextDimension + Default> sum_tree::Dimension<'a, ExcerptSummary> + for ExcerptDimension +{ + fn zero(_: &()) -> Self { + ExcerptDimension(D::default()) } fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { - *self += summary.text.lines; - } -} - -impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for PointUtf16 { - fn zero(_cx: &()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { - *self += summary.text.lines_utf16() + self.0.add_assign(&D::from_text_summary(&summary.text)) } } @@ -4959,47 +6679,169 @@ impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option { } } +#[derive(Clone, PartialOrd, Ord, Eq, PartialEq, Debug)] +struct ExcerptDimension(T); + +#[derive(Clone, PartialOrd, Ord, Eq, PartialEq, Debug)] +struct OutputDimension(T); + +impl<'a> sum_tree::Dimension<'a, DiffTransformSummary> for ExcerptOffset { + fn zero(_: &()) -> Self { + ExcerptOffset::new(0) + } + + fn add_summary(&mut self, summary: &'a DiffTransformSummary, _: &()) { + self.value += summary.input.len; + } +} + +impl<'a> sum_tree::Dimension<'a, DiffTransformSummary> for ExcerptPoint { + fn zero(_: &()) -> Self { + ExcerptPoint::new(0, 0) + } + + fn add_summary(&mut self, summary: &'a DiffTransformSummary, _: &()) { + self.value += summary.input.lines; + } +} + +impl<'a, D: TextDimension + Ord> + sum_tree::SeekTarget<'a, DiffTransformSummary, DiffTransformSummary> for ExcerptDimension +{ + fn cmp(&self, cursor_location: &DiffTransformSummary, _: &()) -> cmp::Ordering { + Ord::cmp(&self.0, &D::from_text_summary(&cursor_location.input)) + } +} + +impl<'a, D: TextDimension + Ord> + sum_tree::SeekTarget<'a, DiffTransformSummary, (OutputDimension, ExcerptDimension)> + for ExcerptDimension +{ + fn cmp( + &self, + cursor_location: &(OutputDimension, ExcerptDimension), + _: &(), + ) -> cmp::Ordering { + Ord::cmp(&self.0, &cursor_location.1 .0) + } +} + +impl<'a, D: TextDimension> sum_tree::Dimension<'a, DiffTransformSummary> for ExcerptDimension { + fn zero(_: &()) -> Self { + ExcerptDimension(D::default()) + } + + fn add_summary(&mut self, summary: &'a DiffTransformSummary, _: &()) { + self.0.add_assign(&D::from_text_summary(&summary.input)) + } +} + +impl<'a, D: TextDimension> sum_tree::Dimension<'a, DiffTransformSummary> for OutputDimension { + fn zero(_: &()) -> Self { + OutputDimension(D::default()) + } + + fn add_summary(&mut self, summary: &'a DiffTransformSummary, _: &()) { + self.0.add_assign(&D::from_text_summary(&summary.output)) + } +} + +impl<'a> sum_tree::Dimension<'a, DiffTransformSummary> for TextSummary { + fn zero(_: &()) -> Self { + TextSummary::default() + } + + fn add_summary(&mut self, summary: &'a DiffTransformSummary, _: &()) { + *self += summary.output + } +} + +impl<'a> sum_tree::Dimension<'a, DiffTransformSummary> for usize { + fn zero(_: &()) -> Self { + 0 + } + + fn add_summary(&mut self, summary: &'a DiffTransformSummary, _: &()) { + *self += summary.output.len + } +} + +impl<'a> sum_tree::Dimension<'a, DiffTransformSummary> for Point { + fn zero(_: &()) -> Self { + Point::new(0, 0) + } + + fn add_summary(&mut self, summary: &'a DiffTransformSummary, _: &()) { + *self += summary.output.lines + } +} + impl<'a> MultiBufferRows<'a> { - pub fn seek(&mut self, row: MultiBufferRow) { - self.buffer_row_range = 0..0; - - self.excerpts - .seek_forward(&Point::new(row.0, 0), Bias::Right, &()); - if self.excerpts.item().is_none() { - self.excerpts.prev(&()); - - if self.excerpts.item().is_none() && row.0 == 0 { - self.buffer_row_range = 0..1; - return; - } - } - - if let Some(excerpt) = self.excerpts.item() { - let overshoot = row.0 - self.excerpts.start().row; - let excerpt_start = excerpt.range.context.start.to_point(&excerpt.buffer).row; - self.buffer_row_range.start = excerpt_start + overshoot; - self.buffer_row_range.end = excerpt_start + excerpt.text_summary.lines.row + 1; - } + pub fn seek(&mut self, MultiBufferRow(row): MultiBufferRow) { + self.point = Point::new(row, 0); + self.cursor.seek(&self.point); } } impl<'a> Iterator for MultiBufferRows<'a> { - type Item = Option; + type Item = RowInfo; fn next(&mut self) -> Option { - loop { - if !self.buffer_row_range.is_empty() { - let row = Some(self.buffer_row_range.start); - self.buffer_row_range.start += 1; - return Some(row); - } - self.excerpts.item()?; - self.excerpts.next(&()); - let excerpt = self.excerpts.item()?; - self.buffer_row_range.start = excerpt.range.context.start.to_point(&excerpt.buffer).row; - self.buffer_row_range.end = - self.buffer_row_range.start + excerpt.text_summary.lines.row + 1; + if self.is_empty && self.point.row == 0 { + self.point += Point::new(1, 0); + return Some(RowInfo { + buffer_row: Some(0), + multibuffer_row: Some(MultiBufferRow(0)), + diff_status: None, + }); } + + let mut region = self.cursor.region()?; + while self.point >= region.range.end { + self.cursor.next(); + if let Some(next_region) = self.cursor.region() { + region = next_region; + } else { + if self.point == self.cursor.diff_transforms.end(&()).0 .0 { + let multibuffer_row = MultiBufferRow(self.point.row); + self.point += Point::new(1, 0); + let last_excerpt = self + .cursor + .excerpts + .item() + .or(self.cursor.excerpts.prev_item())?; + let last_row = last_excerpt + .range + .context + .end + .to_point(&last_excerpt.buffer) + .row; + return Some(RowInfo { + buffer_row: Some(last_row), + multibuffer_row: Some(multibuffer_row), + diff_status: None, + }); + } else { + return None; + } + }; + } + + let overshoot = self.point - region.range.start; + let buffer_point = region.buffer_range.start + overshoot; + let result = Some(RowInfo { + buffer_row: Some(buffer_point.row), + multibuffer_row: Some(MultiBufferRow(self.point.row)), + diff_status: if region.is_inserted_hunk && self.point < region.range.end { + Some(DiffHunkStatus::Added) + } else if !region.is_main_buffer { + Some(DiffHunkStatus::Removed) + } else { + None + }, + }); + self.point += Point::new(1, 0); + result } } @@ -5008,11 +6850,31 @@ impl<'a> MultiBufferChunks<'a> { self.range.start } - pub fn seek(&mut self, new_range: Range) { - self.range = new_range.clone(); + pub fn seek(&mut self, range: Range) { + self.diff_transforms.seek(&range.end, Bias::Right, &()); + let mut excerpt_end = self.diff_transforms.start().1; + if let Some(DiffTransform::BufferContent { .. }) = self.diff_transforms.item() { + let overshoot = range.end - self.diff_transforms.start().0; + excerpt_end.value += overshoot; + } + + self.diff_transforms.seek(&range.start, Bias::Right, &()); + let mut excerpt_start = self.diff_transforms.start().1; + if let Some(DiffTransform::BufferContent { .. }) = self.diff_transforms.item() { + let overshoot = range.start - self.diff_transforms.start().0; + excerpt_start.value += overshoot; + } + + self.seek_to_excerpt_offset_range(excerpt_start..excerpt_end); + self.buffer_chunk.take(); + self.range = range; + } + + fn seek_to_excerpt_offset_range(&mut self, new_range: Range) { + self.excerpt_offset_range = new_range.clone(); self.excerpts.seek(&new_range.start, Bias::Right, &()); if let Some(excerpt) = self.excerpts.item() { - let excerpt_start = self.excerpts.start(); + let excerpt_start = *self.excerpts.start(); if let Some(excerpt_chunks) = self .excerpt_chunks .as_mut() @@ -5020,11 +6882,13 @@ impl<'a> MultiBufferChunks<'a> { { excerpt.seek_chunks( excerpt_chunks, - self.range.start - excerpt_start..self.range.end - excerpt_start, + (self.excerpt_offset_range.start - excerpt_start).value + ..(self.excerpt_offset_range.end - excerpt_start).value, ); } else { self.excerpt_chunks = Some(excerpt.chunks_in_range( - self.range.start - excerpt_start..self.range.end - excerpt_start, + (self.excerpt_offset_range.start - excerpt_start).value + ..(self.excerpt_offset_range.end - excerpt_start).value, self.language_aware, )); } @@ -5032,25 +6896,134 @@ impl<'a> MultiBufferChunks<'a> { self.excerpt_chunks = None; } } + + fn next_excerpt_chunk(&mut self) -> Option> { + loop { + if self.excerpt_offset_range.is_empty() { + return None; + } else if let Some(chunk) = self.excerpt_chunks.as_mut()?.next() { + self.excerpt_offset_range.start.value += chunk.text.len(); + return Some(chunk); + } else { + self.excerpts.next(&()); + let excerpt = self.excerpts.item()?; + self.excerpt_chunks = Some(excerpt.chunks_in_range( + 0..(self.excerpt_offset_range.end - *self.excerpts.start()).value, + self.language_aware, + )); + } + } + } +} + +impl<'a> Iterator for ReversedMultiBufferChunks<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option { + let mut region = self.cursor.region()?; + if self.offset == region.range.start { + self.cursor.prev(); + region = self.cursor.region()?; + let start_overshoot = self.start.saturating_sub(region.range.start); + self.current_chunks = Some(region.buffer.reversed_chunks_in_range( + region.buffer_range.start + start_overshoot..region.buffer_range.end, + )); + } + + if self.offset == region.range.end && region.has_trailing_newline { + self.offset -= 1; + Some("\n") + } else { + let chunk = self.current_chunks.as_mut().unwrap().next().unwrap(); + self.offset -= chunk.len(); + Some(chunk) + } + } } impl<'a> Iterator for MultiBufferChunks<'a> { type Item = Chunk<'a>; - fn next(&mut self) -> Option { - if self.range.is_empty() { - None - } else if let Some(chunk) = self.excerpt_chunks.as_mut()?.next() { - self.range.start += chunk.text.len(); - Some(chunk) - } else { - self.excerpts.next(&()); - let excerpt = self.excerpts.item()?; - self.excerpt_chunks = Some(excerpt.chunks_in_range( - 0..self.range.end - self.excerpts.start(), - self.language_aware, - )); - self.next() + fn next(&mut self) -> Option> { + if self.range.start >= self.range.end { + return None; + } + if self.range.start == self.diff_transforms.end(&()).0 { + self.diff_transforms.next(&()); + } + + let diff_transform_start = self.diff_transforms.start().0; + let diff_transform_end = self.diff_transforms.end(&()).0; + debug_assert!(self.range.start < diff_transform_end); + + let diff_transform = self.diff_transforms.item()?; + match diff_transform { + DiffTransform::BufferContent { .. } => { + let chunk = if let Some(chunk) = &mut self.buffer_chunk { + chunk + } else { + let chunk = self.next_excerpt_chunk().unwrap(); + self.buffer_chunk.insert(chunk) + }; + + let chunk_end = self.range.start + chunk.text.len(); + let diff_transform_end = diff_transform_end.min(self.range.end); + + if diff_transform_end < chunk_end { + let (before, after) = + chunk.text.split_at(diff_transform_end - self.range.start); + self.range.start = diff_transform_end; + chunk.text = after; + Some(Chunk { + text: before, + ..chunk.clone() + }) + } else { + self.range.start = chunk_end; + self.buffer_chunk.take() + } + } + DiffTransform::DeletedHunk { + buffer_id, + base_text_byte_range, + has_trailing_newline, + .. + } => { + let base_text_start = + base_text_byte_range.start + self.range.start - diff_transform_start; + let base_text_end = + base_text_byte_range.start + self.range.end - diff_transform_start; + let base_text_end = base_text_end.min(base_text_byte_range.end); + + let mut chunks = if let Some((_, mut chunks)) = self + .diff_base_chunks + .take() + .filter(|(id, _)| id == buffer_id) + { + if chunks.range().start != base_text_start || chunks.range().end < base_text_end + { + chunks.seek(base_text_start..base_text_end); + } + chunks + } else { + let base_buffer = &self.diffs.get(&buffer_id)?.base_text; + base_buffer.chunks(base_text_start..base_text_end, self.language_aware) + }; + + let chunk = if let Some(chunk) = chunks.next() { + self.range.start += chunk.text.len(); + self.diff_base_chunks = Some((*buffer_id, chunks)); + chunk + } else { + debug_assert!(has_trailing_newline); + self.range.start += "\n".len(); + Chunk { + text: "\n", + ..Default::default() + } + }; + Some(chunk) + } } } } @@ -5063,13 +7036,25 @@ impl<'a> MultiBufferBytes<'a> { if !self.range.is_empty() && self.chunk.is_empty() { if let Some(chunk) = self.excerpt_bytes.as_mut().and_then(|bytes| bytes.next()) { self.chunk = chunk; + } else if self.has_trailing_newline { + self.has_trailing_newline = false; + self.chunk = b"\n"; } else { - self.excerpts.next(&()); - if let Some(excerpt) = self.excerpts.item() { - let mut excerpt_bytes = - excerpt.bytes_in_range(0..self.range.end - self.excerpts.start()); - self.chunk = excerpt_bytes.next().unwrap(); + self.cursor.next(); + if let Some(region) = self.cursor.region() { + let mut excerpt_bytes = region.buffer.bytes_in_range( + region.buffer_range.start + ..(region.buffer_range.start + self.range.end - region.range.start) + .min(region.buffer_range.end), + ); + self.chunk = excerpt_bytes.next().unwrap_or(&[]); self.excerpt_bytes = Some(excerpt_bytes); + self.has_trailing_newline = + region.has_trailing_newline && self.range.end >= region.range.end; + if self.chunk.is_empty() && self.has_trailing_newline { + self.has_trailing_newline = false; + self.chunk = b"\n"; + } } } } @@ -5101,62 +7086,21 @@ impl<'a> io::Read for MultiBufferBytes<'a> { } } -impl<'a> ReversedMultiBufferBytes<'a> { - fn consume(&mut self, len: usize) { - self.range.end -= len; - self.chunk = &self.chunk[..self.chunk.len() - len]; - - if !self.range.is_empty() && self.chunk.is_empty() { - if let Some(chunk) = self.excerpt_bytes.as_mut().and_then(|bytes| bytes.next()) { - self.chunk = chunk; - } else { - self.excerpts.prev(&()); - if let Some(excerpt) = self.excerpts.item() { - let mut excerpt_bytes = excerpt.reversed_bytes_in_range( - self.range.start.saturating_sub(*self.excerpts.start())..usize::MAX, - ); - self.chunk = excerpt_bytes.next().unwrap(); - self.excerpt_bytes = Some(excerpt_bytes); - } - } - } - } -} - impl<'a> io::Read for ReversedMultiBufferBytes<'a> { fn read(&mut self, buf: &mut [u8]) -> io::Result { let len = cmp::min(buf.len(), self.chunk.len()); buf[..len].copy_from_slice(&self.chunk[..len]); buf[..len].reverse(); if len > 0 { - self.consume(len); - } - Ok(len) - } -} -impl<'a> Iterator for ExcerptBytes<'a> { - type Item = &'a [u8]; - - fn next(&mut self) -> Option { - if self.reversed && self.padding_height > 0 { - let result = &NEWLINES[..self.padding_height]; - self.padding_height = 0; - return Some(result); - } - - if let Some(chunk) = self.content_bytes.next() { - if !chunk.is_empty() { - return Some(chunk); + self.range.end -= len; + self.chunk = &self.chunk[..self.chunk.len() - len]; + if !self.range.is_empty() && self.chunk.is_empty() { + if let Some(chunk) = self.chunks.next() { + self.chunk = chunk.as_bytes(); + } } } - - if self.padding_height > 0 { - let result = &NEWLINES[..self.padding_height]; - self.padding_height = 0; - return Some(result); - } - - None + Ok(len) } } @@ -5248,6 +7192,12 @@ impl ToPointUtf16 for PointUtf16 { } } +impl From for EntityId { + fn from(id: ExcerptId) -> Self { + EntityId::from(id.0 as u64) + } +} + pub fn build_excerpt_ranges( buffer: &BufferSnapshot, ranges: &[Range], diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index 059b279b78..60976a65b4 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -1,5 +1,7 @@ use super::*; +use git::diff::DiffHunkStatus; use gpui::{AppContext, Context, TestAppContext}; +use indoc::indoc; use language::{Buffer, Rope}; use parking_lot::RwLock; use rand::prelude::*; @@ -14,6 +16,22 @@ fn init_logger() { } } +#[gpui::test] +fn test_empty_singleton(cx: &mut AppContext) { + let buffer = cx.new_model(|cx| Buffer::local("", cx)); + let multibuffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); + let snapshot = multibuffer.read(cx).snapshot(cx); + assert_eq!(snapshot.text(), ""); + assert_eq!( + snapshot.row_infos(MultiBufferRow(0)).collect::>(), + [RowInfo { + buffer_row: Some(0), + multibuffer_row: Some(MultiBufferRow(0)), + diff_status: None + }] + ); +} + #[gpui::test] fn test_singleton(cx: &mut AppContext) { let buffer = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); @@ -23,22 +41,30 @@ fn test_singleton(cx: &mut AppContext) { assert_eq!(snapshot.text(), buffer.read(cx).text()); assert_eq!( - snapshot.buffer_rows(MultiBufferRow(0)).collect::>(), + snapshot + .row_infos(MultiBufferRow(0)) + .map(|info| info.buffer_row) + .collect::>(), (0..buffer.read(cx).row_count()) .map(Some) .collect::>() ); + assert_consistent_line_numbers(&snapshot); buffer.update(cx, |buffer, cx| buffer.edit([(1..3, "XXX\n")], None, cx)); let snapshot = multibuffer.read(cx).snapshot(cx); assert_eq!(snapshot.text(), buffer.read(cx).text()); assert_eq!( - snapshot.buffer_rows(MultiBufferRow(0)).collect::>(), + snapshot + .row_infos(MultiBufferRow(0)) + .map(|info| info.buffer_row) + .collect::>(), (0..buffer.read(cx).row_count()) .map(Some) .collect::>() ); + assert_consistent_line_numbers(&snapshot); } #[gpui::test] @@ -154,28 +180,41 @@ fn test_excerpt_boundaries_and_clipping(cx: &mut AppContext) { let snapshot = multibuffer.read(cx).snapshot(cx); assert_eq!( snapshot.text(), - concat!( - "bbbb\n", // Preserve newlines - "ccccc\n", // - "ddd\n", // - "eeee\n", // - "jj" // - ) + indoc!( + " + bbbb + ccccc + ddd + eeee + jj" + ), ); assert_eq!( - snapshot.buffer_rows(MultiBufferRow(0)).collect::>(), + snapshot + .row_infos(MultiBufferRow(0)) + .map(|info| info.buffer_row) + .collect::>(), [Some(1), Some(2), Some(3), Some(4), Some(3)] ); assert_eq!( - snapshot.buffer_rows(MultiBufferRow(2)).collect::>(), + snapshot + .row_infos(MultiBufferRow(2)) + .map(|info| info.buffer_row) + .collect::>(), [Some(3), Some(4), Some(3)] ); assert_eq!( - snapshot.buffer_rows(MultiBufferRow(4)).collect::>(), + snapshot + .row_infos(MultiBufferRow(4)) + .map(|info| info.buffer_row) + .collect::>(), [Some(3)] ); assert_eq!( - snapshot.buffer_rows(MultiBufferRow(5)).collect::>(), + snapshot + .row_infos(MultiBufferRow(5)) + .map(|info| info.buffer_row) + .collect::>(), [] ); @@ -314,6 +353,312 @@ fn test_excerpt_boundaries_and_clipping(cx: &mut AppContext) { } } +#[gpui::test] +fn test_diff_boundary_anchors(cx: &mut AppContext) { + let base_text = "one\ntwo\nthree\n"; + let text = "one\nthree\n"; + let buffer = cx.new_model(|cx| Buffer::local(text, cx)); + let snapshot = buffer.read(cx).snapshot(); + let change_set = cx.new_model(|cx| { + let mut change_set = BufferChangeSet::new(&buffer, cx); + change_set.recalculate_diff_sync(base_text.into(), snapshot.text, true, cx); + change_set + }); + let multibuffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.add_change_set(change_set, cx) + }); + + let (before, after) = multibuffer.update(cx, |multibuffer, cx| { + let before = multibuffer.snapshot(cx).anchor_before(Point::new(1, 0)); + let after = multibuffer.snapshot(cx).anchor_after(Point::new(1, 0)); + multibuffer.set_all_diff_hunks_expanded(cx); + (before, after) + }); + cx.background_executor().run_until_parked(); + + let snapshot = multibuffer.read(cx).snapshot(cx); + let actual_text = snapshot.text(); + let actual_row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::>(); + let actual_diff = format_diff(&actual_text, &actual_row_infos, &Default::default()); + pretty_assertions::assert_eq!( + actual_diff, + indoc! { + " one + - two + three + " + }, + ); + + multibuffer.update(cx, |multibuffer, cx| { + let snapshot = multibuffer.snapshot(cx); + assert_eq!(before.to_point(&snapshot), Point::new(1, 0)); + assert_eq!(after.to_point(&snapshot), Point::new(2, 0)); + assert_eq!( + vec![Point::new(1, 0), Point::new(2, 0),], + snapshot.summaries_for_anchors::(&[before, after]), + ) + }) +} + +#[gpui::test] +fn test_diff_hunks_in_range(cx: &mut TestAppContext) { + let base_text = "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\n"; + let text = "one\nfour\nseven\n"; + let buffer = cx.new_model(|cx| Buffer::local(text, cx)); + let change_set = cx.new_model(|cx| { + let mut change_set = BufferChangeSet::new(&buffer, cx); + let snapshot = buffer.read(cx).snapshot(); + change_set.recalculate_diff_sync(base_text.into(), snapshot.text, true, cx); + change_set + }); + let multibuffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { + (multibuffer.snapshot(cx), multibuffer.subscribe()) + }); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.add_change_set(change_set, cx); + multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx); + }); + + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc! { + " one + - two + - three + four + - five + - six + seven + - eight + " + }, + ); + + assert_eq!( + snapshot + .diff_hunks_in_range(Point::new(1, 0)..Point::MAX) + .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0) + .collect::>(), + vec![1..3, 4..6, 7..8] + ); + + assert_eq!( + snapshot + .diff_hunk_before(Point::new(1, 1)) + .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0), + None, + ); + assert_eq!( + snapshot + .diff_hunk_before(Point::new(7, 0)) + .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0), + Some(4..6) + ); + assert_eq!( + snapshot + .diff_hunk_before(Point::new(4, 0)) + .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0), + Some(1..3) + ); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], cx); + }); + + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc! { + " + one + four + seven + " + }, + ); + + assert_eq!( + snapshot + .diff_hunk_before(Point::new(2, 0)) + .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0), + Some(1..1), + ); + assert_eq!( + snapshot + .diff_hunk_before(Point::new(4, 0)) + .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0), + Some(2..2) + ); +} + +#[gpui::test] +fn test_editing_text_in_diff_hunks(cx: &mut TestAppContext) { + let base_text = "one\ntwo\nfour\nfive\nsix\nseven\n"; + let text = "one\ntwo\nTHREE\nfour\nfive\nseven\n"; + let buffer = cx.new_model(|cx| Buffer::local(text, cx)); + let change_set = cx.new_model(|cx| { + let mut change_set = BufferChangeSet::new(&buffer, cx); + let snapshot = buffer.read(cx).snapshot(); + change_set.recalculate_diff_sync(base_text.into(), snapshot.text, true, cx); + change_set + }); + let multibuffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); + + let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { + multibuffer.add_change_set(change_set.clone(), cx); + (multibuffer.snapshot(cx), multibuffer.subscribe()) + }); + + cx.executor().run_until_parked(); + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.set_all_diff_hunks_expanded(cx); + }); + + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc! { + " + one + two + + THREE + four + five + - six + seven + " + }, + ); + + // Insert a newline within an insertion hunk + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.edit([(Point::new(2, 0)..Point::new(2, 0), "__\n__")], None, cx); + }); + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc! { + " + one + two + + __ + + __THREE + four + five + - six + seven + " + }, + ); + + // Delete the newline before a deleted hunk. + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.edit([(Point::new(5, 4)..Point::new(6, 0), "")], None, cx); + }); + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc! { + " + one + two + + __ + + __THREE + four + fiveseven + " + }, + ); + + multibuffer.update(cx, |multibuffer, cx| multibuffer.undo(cx)); + change_set.update(cx, |change_set, cx| { + change_set.recalculate_diff_sync( + base_text.into(), + buffer.read(cx).text_snapshot(), + true, + cx, + ); + }); + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc! { + " + one + two + + __ + + __THREE + four + five + - six + seven + " + }, + ); + + // Cannot (yet) insert at the beginning of a deleted hunk. + // (because it would put the newline in the wrong place) + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.edit([(Point::new(6, 0)..Point::new(6, 0), "\n")], None, cx); + }); + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc! { + " + one + two + + __ + + __THREE + four + five + - six + seven + " + }, + ); + + // Replace a range that ends in a deleted hunk. + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.edit([(Point::new(5, 2)..Point::new(6, 2), "fty-")], None, cx); + }); + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc! { + " + one + two + + __ + + __THREE + four + fifty-seven + " + }, + ); +} + #[gpui::test] fn test_excerpt_events(cx: &mut AppContext) { let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(10, 3, 'a'), cx)); @@ -633,11 +978,17 @@ fn test_empty_multibuffer(cx: &mut AppContext) { let snapshot = multibuffer.read(cx).snapshot(cx); assert_eq!(snapshot.text(), ""); assert_eq!( - snapshot.buffer_rows(MultiBufferRow(0)).collect::>(), + snapshot + .row_infos(MultiBufferRow(0)) + .map(|info| info.buffer_row) + .collect::>(), &[Some(0)] ); assert_eq!( - snapshot.buffer_rows(MultiBufferRow(1)).collect::>(), + snapshot + .row_infos(MultiBufferRow(1)) + .map(|info| info.buffer_row) + .collect::>(), &[] ); } @@ -851,6 +1202,874 @@ fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut AppContext) { ); } +#[gpui::test] +fn test_basic_diff_hunks(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_model(|cx| Buffer::local(text, cx)); + let change_set = + cx.new_model(|cx| BufferChangeSet::new_with_base_text(base_text.to_string(), &buffer, cx)); + cx.run_until_parked(); + + let multibuffer = cx.new_model(|cx| { + let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx); + multibuffer.add_change_set(change_set.clone(), cx); + multibuffer + }); + + let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { + (multibuffer.snapshot(cx), multibuffer.subscribe()) + }); + assert_eq!( + snapshot.text(), + indoc!( + " + ZERO + one + TWO + three + six + " + ), + ); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx); + }); + + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc!( + " + + ZERO + one + - two + + TWO + three + - four + - five + six + " + ), + ); + + assert_eq!( + snapshot + .row_infos(MultiBufferRow(0)) + .map(|info| (info.buffer_row, info.diff_status)) + .collect::>(), + vec![ + (Some(0), Some(DiffHunkStatus::Added)), + (Some(1), None), + (Some(1), Some(DiffHunkStatus::Removed)), + (Some(2), Some(DiffHunkStatus::Added)), + (Some(3), None), + (Some(3), Some(DiffHunkStatus::Removed)), + (Some(4), Some(DiffHunkStatus::Removed)), + (Some(4), None), + (Some(5), None) + ] + ); + + assert_chunks_in_ranges(&snapshot); + assert_consistent_line_numbers(&snapshot); + assert_position_translation(&snapshot); + assert_line_indents(&snapshot); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], cx) + }); + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc!( + " + ZERO + one + TWO + three + six + " + ), + ); + + assert_chunks_in_ranges(&snapshot); + assert_consistent_line_numbers(&snapshot); + assert_position_translation(&snapshot); + assert_line_indents(&snapshot); + + // Expand the first diff hunk + multibuffer.update(cx, |multibuffer, cx| { + let position = multibuffer.read(cx).anchor_before(Point::new(2, 2)); + multibuffer.expand_diff_hunks(vec![position..position], cx) + }); + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc!( + " + ZERO + one + - two + + TWO + three + six + " + ), + ); + + // Expand the second diff hunk + multibuffer.update(cx, |multibuffer, cx| { + let start = multibuffer.read(cx).anchor_before(Point::new(4, 0)); + let end = multibuffer.read(cx).anchor_before(Point::new(5, 0)); + multibuffer.expand_diff_hunks(vec![start..end], cx) + }); + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc!( + " + ZERO + one + - two + + TWO + three + - four + - five + six + " + ), + ); + + assert_chunks_in_ranges(&snapshot); + assert_consistent_line_numbers(&snapshot); + assert_position_translation(&snapshot); + assert_line_indents(&snapshot); + + // Edit the buffer before the first hunk + buffer.update(cx, |buffer, cx| { + buffer.edit_via_marked_text( + indoc!( + " + ZERO + one« hundred + thousand» + TWO + three + six + " + ), + None, + cx, + ); + }); + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc!( + " + ZERO + one hundred + thousand + - two + + TWO + three + - four + - five + six + " + ), + ); + + assert_chunks_in_ranges(&snapshot); + assert_consistent_line_numbers(&snapshot); + assert_position_translation(&snapshot); + assert_line_indents(&snapshot); + + // Recalculate the diff, changing the first diff hunk. + let _ = change_set.update(cx, |change_set, cx| { + change_set.recalculate_diff(buffer.read(cx).text_snapshot(), cx) + }); + cx.run_until_parked(); + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc!( + " + ZERO + one hundred + thousand + TWO + three + - four + - five + six + " + ), + ); + + assert_eq!( + snapshot + .diff_hunks_in_range(0..snapshot.len()) + .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0) + .collect::>(), + &[0..4, 5..7] + ); +} + +#[gpui::test] +fn test_repeatedly_expand_a_diff_hunk(cx: &mut TestAppContext) { + let text = indoc!( + " + one + TWO + THREE + four + FIVE + six + " + ); + let base_text = indoc!( + " + one + four + six + " + ); + + let buffer = cx.new_model(|cx| Buffer::local(text, cx)); + let change_set = + cx.new_model(|cx| BufferChangeSet::new_with_base_text(base_text.to_string(), &buffer, cx)); + cx.run_until_parked(); + + let multibuffer = cx.new_model(|cx| { + let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx); + multibuffer.add_change_set(change_set.clone(), cx); + multibuffer + }); + + let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { + (multibuffer.snapshot(cx), multibuffer.subscribe()) + }); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx); + }); + + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc!( + " + one + + TWO + + THREE + four + + FIVE + six + " + ), + ); + + // Regression test: expanding diff hunks that are already expanded should not change anything. + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.expand_diff_hunks( + vec![ + snapshot.anchor_before(Point::new(2, 0))..snapshot.anchor_before(Point::new(2, 0)), + ], + cx, + ); + }); + + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc!( + " + one + + TWO + + THREE + four + + FIVE + six + " + ), + ); +} + +#[gpui::test] +fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) { + let base_text_1 = indoc!( + " + one + two + three + four + five + six + " + ); + let text_1 = indoc!( + " + ZERO + one + TWO + three + six + " + ); + let base_text_2 = indoc!( + " + seven + eight + nine + ten + eleven + twelve + " + ); + let text_2 = indoc!( + " + eight + nine + eleven + THIRTEEN + FOURTEEN + " + ); + + let buffer_1 = cx.new_model(|cx| Buffer::local(text_1, cx)); + let buffer_2 = cx.new_model(|cx| Buffer::local(text_2, cx)); + let change_set_1 = cx.new_model(|cx| { + BufferChangeSet::new_with_base_text(base_text_1.to_string(), &buffer_1, cx) + }); + let change_set_2 = cx.new_model(|cx| { + BufferChangeSet::new_with_base_text(base_text_2.to_string(), &buffer_2, cx) + }); + cx.run_until_parked(); + + let multibuffer = cx.new_model(|cx| { + let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); + multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: text::Anchor::MIN..text::Anchor::MAX, + primary: None, + }], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: text::Anchor::MIN..text::Anchor::MAX, + primary: None, + }], + cx, + ); + multibuffer.add_change_set(change_set_1.clone(), cx); + multibuffer.add_change_set(change_set_2.clone(), cx); + multibuffer + }); + + let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { + (multibuffer.snapshot(cx), multibuffer.subscribe()) + }); + assert_eq!( + snapshot.text(), + indoc!( + " + ZERO + one + TWO + three + six + + eight + nine + eleven + THIRTEEN + FOURTEEN + " + ), + ); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx); + }); + + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc!( + " + + ZERO + one + - two + + TWO + three + - four + - five + six + + - seven + eight + nine + - ten + eleven + - twelve + + THIRTEEN + + FOURTEEN + " + ), + ); + + let id_1 = buffer_1.read_with(cx, |buffer, _| buffer.remote_id()); + let id_2 = buffer_2.read_with(cx, |buffer, _| buffer.remote_id()); + let base_id_1 = change_set_1.read_with(cx, |change_set, _| { + change_set.base_text.as_ref().unwrap().remote_id() + }); + let base_id_2 = change_set_2.read_with(cx, |change_set, _| { + change_set.base_text.as_ref().unwrap().remote_id() + }); + + let buffer_lines = (0..=snapshot.max_row().0) + .map(|row| { + let (buffer, range) = snapshot.buffer_line_for_row(MultiBufferRow(row))?; + Some(( + buffer.remote_id(), + buffer.text_for_range(range).collect::(), + )) + }) + .collect::>(); + pretty_assertions::assert_eq!( + buffer_lines, + [ + Some((id_1, "ZERO".into())), + Some((id_1, "one".into())), + Some((base_id_1, "two".into())), + Some((id_1, "TWO".into())), + Some((id_1, " three".into())), + Some((base_id_1, "four".into())), + Some((base_id_1, "five".into())), + Some((id_1, "six".into())), + Some((id_1, "".into())), + Some((base_id_2, "seven".into())), + Some((id_2, " eight".into())), + Some((id_2, "nine".into())), + Some((base_id_2, "ten".into())), + Some((id_2, "eleven".into())), + Some((base_id_2, "twelve".into())), + Some((id_2, "THIRTEEN".into())), + Some((id_2, "FOURTEEN".into())), + Some((id_2, "".into())), + ] + ); + + assert_position_translation(&snapshot); + assert_line_indents(&snapshot); + + assert_eq!( + snapshot + .diff_hunks_in_range(0..snapshot.len()) + .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0) + .collect::>(), + &[0..1, 2..4, 5..7, 9..10, 12..13, 14..17] + ); + + buffer_2.update(cx, |buffer, cx| { + buffer.edit_via_marked_text( + indoc!( + " + eight + «»eleven + THIRTEEN + FOURTEEN + " + ), + None, + cx, + ); + }); + + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc!( + " + + ZERO + one + - two + + TWO + three + - four + - five + six + + - seven + eight + eleven + - twelve + + THIRTEEN + + FOURTEEN + " + ), + ); + + assert_line_indents(&snapshot); +} + +/// A naive implementation of a multi-buffer that does not maintain +/// any derived state, used for comparison in a randomized test. +#[derive(Default)] +struct ReferenceMultibuffer { + excerpts: Vec, + change_sets: HashMap>, +} + +struct ReferenceExcerpt { + id: ExcerptId, + buffer: Model, + range: Range, + expanded_diff_hunks: Vec, +} + +#[derive(Debug)] +struct ReferenceRegion { + range: Range, + buffer_start: Option, + status: Option, +} + +impl ReferenceMultibuffer { + fn expand_excerpts(&mut self, excerpts: &HashSet, line_count: u32, cx: &AppContext) { + if line_count == 0 { + return; + } + + for id in excerpts { + let excerpt = self.excerpts.iter_mut().find(|e| e.id == *id).unwrap(); + let snapshot = excerpt.buffer.read(cx).snapshot(); + let mut point_range = excerpt.range.to_point(&snapshot); + point_range.start = Point::new(point_range.start.row.saturating_sub(line_count), 0); + point_range.end = + snapshot.clip_point(Point::new(point_range.end.row + line_count, 0), Bias::Left); + point_range.end.column = snapshot.line_len(point_range.end.row); + excerpt.range = + snapshot.anchor_before(point_range.start)..snapshot.anchor_after(point_range.end); + } + } + + fn remove_excerpt(&mut self, id: ExcerptId, cx: &AppContext) { + let ix = self + .excerpts + .iter() + .position(|excerpt| excerpt.id == id) + .unwrap(); + let excerpt = self.excerpts.remove(ix); + let buffer = excerpt.buffer.read(cx); + log::info!( + "Removing excerpt {}: {:?}", + ix, + buffer + .text_for_range(excerpt.range.to_offset(buffer)) + .collect::(), + ); + } + + fn insert_excerpt_after( + &mut self, + prev_id: ExcerptId, + new_excerpt_id: ExcerptId, + (buffer_handle, anchor_range): (Model, Range), + ) { + let excerpt_ix = if prev_id == ExcerptId::max() { + self.excerpts.len() + } else { + self.excerpts + .iter() + .position(|excerpt| excerpt.id == prev_id) + .unwrap() + + 1 + }; + self.excerpts.insert( + excerpt_ix, + ReferenceExcerpt { + id: new_excerpt_id, + buffer: buffer_handle, + range: anchor_range, + expanded_diff_hunks: Vec::new(), + }, + ); + } + + fn expand_diff_hunks( + &mut self, + excerpt_id: ExcerptId, + range: Range, + cx: &AppContext, + ) { + let excerpt = self + .excerpts + .iter_mut() + .find(|e| e.id == excerpt_id) + .unwrap(); + let buffer = excerpt.buffer.read(cx).snapshot(); + let buffer_id = buffer.remote_id(); + let Some(change_set) = self.change_sets.get(&buffer_id) else { + return; + }; + let diff = change_set.read(cx).diff_to_buffer.clone(); + let excerpt_range = excerpt.range.to_offset(&buffer); + if excerpt_range.is_empty() { + return; + } + for hunk in diff.hunks_intersecting_range(range, &buffer) { + let hunk_range = hunk.buffer_range.to_offset(&buffer); + let hunk_precedes_excerpt = hunk + .buffer_range + .end + .cmp(&excerpt.range.start, &buffer) + .is_le(); + let hunk_follows_excerpt = hunk + .buffer_range + .start + .cmp(&excerpt.range.end, &buffer) + .is_ge(); + if hunk_precedes_excerpt || hunk_follows_excerpt { + continue; + } + + if let Err(ix) = excerpt + .expanded_diff_hunks + .binary_search_by(|anchor| anchor.cmp(&hunk.buffer_range.start, &buffer)) + { + log::info!( + "expanding diff hunk {:?}. excerpt: {:?}", + hunk_range, + excerpt_range + ); + excerpt + .expanded_diff_hunks + .insert(ix, hunk.buffer_range.start); + } + } + } + + fn expected_content(&self, cx: &AppContext) -> (String, Vec, HashSet) { + let mut text = String::new(); + let mut regions = Vec::::new(); + let mut excerpt_boundary_rows = HashSet::default(); + for excerpt in &self.excerpts { + excerpt_boundary_rows.insert(MultiBufferRow(text.matches('\n').count() as u32)); + let buffer = excerpt.buffer.read(cx); + let buffer_range = excerpt.range.to_offset(buffer); + let change_set = self.change_sets.get(&buffer.remote_id()).unwrap().read(cx); + let diff = change_set.diff_to_buffer.clone(); + let base_buffer = change_set.base_text.as_ref().unwrap(); + + let mut offset = buffer_range.start; + let mut hunks = diff + .hunks_intersecting_range(excerpt.range.clone(), buffer) + .peekable(); + + while let Some(hunk) = hunks.next() { + if !hunk.buffer_range.start.is_valid(&buffer) { + continue; + } + + // Ignore hunks that are outside the excerpt range. + let mut hunk_range = hunk.buffer_range.to_offset(buffer); + hunk_range.end = hunk_range.end.min(buffer_range.end); + if hunk_range.start > buffer_range.end + || hunk_range.end < buffer_range.start + || buffer_range.is_empty() + { + 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) + }) { + continue; + } + + if hunk_range.start >= offset { + // Add the buffer text before the hunk + let len = text.len(); + text.extend(buffer.text_for_range(offset..hunk_range.start)); + regions.push(ReferenceRegion { + range: len..text.len(), + buffer_start: Some(buffer.offset_to_point(offset)), + status: None, + }); + + // Add the deleted text for the hunk. + if !hunk.diff_base_byte_range.is_empty() { + let mut base_text = base_buffer + .text_for_range(hunk.diff_base_byte_range.clone()) + .collect::(); + if !base_text.ends_with('\n') { + base_text.push('\n'); + } + let len = text.len(); + text.push_str(&base_text); + regions.push(ReferenceRegion { + range: len..text.len(), + buffer_start: Some( + base_buffer.offset_to_point(hunk.diff_base_byte_range.start), + ), + status: Some(DiffHunkStatus::Removed), + }); + } + + offset = hunk_range.start; + } + + // 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 { + range: len..text.len(), + buffer_start: Some(buffer.offset_to_point(offset)), + status: Some(DiffHunkStatus::Added), + }); + offset = hunk_range.end; + } + } + + // Add the buffer text for the rest of the excerpt. + let len = text.len(); + text.extend(buffer.text_for_range(offset..buffer_range.end)); + text.push('\n'); + regions.push(ReferenceRegion { + range: len..text.len(), + buffer_start: Some(buffer.offset_to_point(offset)), + status: None, + }); + } + + // Remove final trailing newline. + if self.excerpts.is_empty() { + regions.push(ReferenceRegion { + range: 0..1, + buffer_start: Some(Point::new(0, 0)), + status: None, + }); + } else { + text.pop(); + } + + // Retrieve the row info using the region that contains + // the start of each multi-buffer line. + let mut ix = 0; + let row_infos = text + .split('\n') + .map(|line| { + let row_info = regions + .iter() + .find(|region| region.range.contains(&ix)) + .map_or(RowInfo::default(), |region| { + let buffer_row = region.buffer_start.map(|start_point| { + start_point.row + + text[region.range.start..ix].matches('\n').count() as u32 + }); + RowInfo { + diff_status: region.status, + buffer_row, + multibuffer_row: Some(MultiBufferRow( + text[..ix].matches('\n').count() as u32 + )), + } + }); + ix += line.len() + 1; + row_info + }) + .collect(); + + (text, row_infos, excerpt_boundary_rows) + } + + fn diffs_updated(&mut self, cx: &AppContext) { + for excerpt in &mut self.excerpts { + let buffer = excerpt.buffer.read(cx).snapshot(); + let excerpt_range = excerpt.range.to_offset(&buffer); + let buffer_id = buffer.remote_id(); + let diff = &self + .change_sets + .get(&buffer_id) + .unwrap() + .read(cx) + .diff_to_buffer; + let mut hunks = diff.hunks_in_row_range(0..u32::MAX, &buffer).peekable(); + excerpt.expanded_diff_hunks.retain(|hunk_anchor| { + if !hunk_anchor.is_valid(&buffer) { + return false; + } + while let Some(hunk) = hunks.peek() { + match hunk.buffer_range.start.cmp(&hunk_anchor, &buffer) { + cmp::Ordering::Less => { + hunks.next(); + } + cmp::Ordering::Equal => { + let hunk_range = hunk.buffer_range.to_offset(&buffer); + return hunk_range.end >= excerpt_range.start + && hunk_range.start <= excerpt_range.end; + } + cmp::Ordering::Greater => break, + } + } + false + }); + } + } + + fn add_change_set(&mut self, change_set: Model, cx: &mut AppContext) { + let buffer_id = change_set.read(cx).buffer_id; + self.change_sets.insert(buffer_id, change_set); + } +} + #[gpui::test(iterations = 100)] fn test_random_multibuffer(cx: &mut AppContext, mut rng: StdRng) { let operations = env::var("OPERATIONS") @@ -859,18 +2078,23 @@ fn test_random_multibuffer(cx: &mut AppContext, mut rng: StdRng) { let mut buffers: Vec> = Vec::new(); let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - let mut excerpt_ids = Vec::::new(); - let mut expected_excerpts = Vec::<(Model, Range)>::new(); + let mut reference = ReferenceMultibuffer::default(); let mut anchors = Vec::new(); let mut old_versions = Vec::new(); + let mut needs_diff_calculation = false; for _ in 0..operations { match rng.gen_range(0..100) { 0..=14 if !buffers.is_empty() => { let buffer = buffers.choose(&mut rng).unwrap(); - buffer.update(cx, |buf, cx| buf.randomly_edit(&mut rng, 5, cx)); + buffer.update(cx, |buf, cx| { + let edit_count = rng.gen_range(1..5); + buf.randomly_edit(&mut rng, edit_count, cx); + needs_diff_calculation = true; + }); + reference.diffs_updated(cx); } - 15..=19 if !expected_excerpts.is_empty() => { + 15..=19 if !reference.excerpts.is_empty() => { multibuffer.update(cx, |multibuffer, cx| { let ids = multibuffer.excerpt_ids(); let mut excerpts = HashSet::default(); @@ -882,7 +2106,7 @@ fn test_random_multibuffer(cx: &mut AppContext, mut rng: StdRng) { let excerpt_ixs = excerpts .iter() - .map(|id| excerpt_ids.iter().position(|i| i == id).unwrap()) + .map(|id| reference.excerpts.iter().position(|e| e.id == *id).unwrap()) .collect::>(); log::info!("Expanding excerpts {excerpt_ixs:?} by {line_count} lines"); multibuffer.expand_excerpts( @@ -892,43 +2116,18 @@ fn test_random_multibuffer(cx: &mut AppContext, mut rng: StdRng) { cx, ); - if line_count > 0 { - for id in excerpts { - let excerpt_ix = excerpt_ids.iter().position(|&i| i == id).unwrap(); - let (buffer, range) = &mut expected_excerpts[excerpt_ix]; - let snapshot = buffer.read(cx).snapshot(); - let mut point_range = range.to_point(&snapshot); - point_range.start = - Point::new(point_range.start.row.saturating_sub(line_count), 0); - point_range.end = snapshot.clip_point( - Point::new(point_range.end.row + line_count, 0), - Bias::Left, - ); - point_range.end.column = snapshot.line_len(point_range.end.row); - *range = snapshot.anchor_before(point_range.start) - ..snapshot.anchor_after(point_range.end); - } - } + reference.expand_excerpts(&excerpts, line_count, cx); }); } - 20..=29 if !expected_excerpts.is_empty() => { + 20..=29 if !reference.excerpts.is_empty() => { let mut ids_to_remove = vec![]; for _ in 0..rng.gen_range(1..=3) { - if expected_excerpts.is_empty() { + let Some(excerpt) = reference.excerpts.choose(&mut rng) else { break; - } - - let ix = rng.gen_range(0..expected_excerpts.len()); - ids_to_remove.push(excerpt_ids.remove(ix)); - let (buffer, range) = expected_excerpts.remove(ix); - let buffer = buffer.read(cx); - log::info!( - "Removing excerpt {}: {:?}", - ix, - buffer - .text_for_range(range.to_offset(buffer)) - .collect::(), - ); + }; + let id = excerpt.id; + reference.remove_excerpt(id, cx); + ids_to_remove.push(id); } let snapshot = multibuffer.read(cx).read(cx); ids_to_remove.sort_unstable_by(|a, b| a.cmp(b, &snapshot)); @@ -937,7 +2136,7 @@ fn test_random_multibuffer(cx: &mut AppContext, mut rng: StdRng) { multibuffer.remove_excerpts(ids_to_remove, cx) }); } - 30..=39 if !expected_excerpts.is_empty() => { + 30..=39 if !reference.excerpts.is_empty() => { let multibuffer = multibuffer.read(cx).read(cx); let offset = multibuffer.clip_offset(rng.gen_range(0..=multibuffer.len()), Bias::Left); @@ -970,33 +2169,86 @@ fn test_random_multibuffer(cx: &mut AppContext, mut rng: StdRng) { assert!(excerpt.contains(anchor)); } } + 45..=55 if !reference.excerpts.is_empty() => { + multibuffer.update(cx, |multibuffer, cx| { + let snapshot = multibuffer.snapshot(cx); + let excerpt_ix = rng.gen_range(0..reference.excerpts.len()); + let excerpt = &reference.excerpts[excerpt_ix]; + let start = excerpt.range.start; + let end = excerpt.range.end; + let range = snapshot.anchor_in_excerpt(excerpt.id, start).unwrap() + ..snapshot.anchor_in_excerpt(excerpt.id, end).unwrap(); + + log::info!("expanding diff hunks for excerpt {:?}", excerpt_ix); + reference.expand_diff_hunks(excerpt.id, start..end, cx); + multibuffer.expand_diff_hunks(vec![range], cx); + }); + } + 56..=85 if needs_diff_calculation => { + multibuffer.update(cx, |multibuffer, cx| { + for buffer in multibuffer.all_buffers() { + let snapshot = buffer.read(cx).snapshot(); + let _ = multibuffer + .change_set_for(snapshot.remote_id()) + .unwrap() + .update(cx, |change_set, cx| { + log::info!( + "recalculating diff for buffer {:?}", + snapshot.remote_id(), + ); + change_set.recalculate_diff_sync( + change_set.base_text.clone().unwrap().text(), + snapshot.text, + false, + cx, + ) + }); + } + reference.diffs_updated(cx); + needs_diff_calculation = false; + }); + } _ => { let buffer_handle = if buffers.is_empty() || rng.gen_bool(0.4) { let base_text = util::RandomCharIter::new(&mut rng) - .take(25) + .take(256) .collect::(); - buffers.push(cx.new_model(|cx| Buffer::local(base_text, cx))); + let buffer = cx.new_model(|cx| Buffer::local(base_text.clone(), cx)); + let snapshot = buffer.read(cx).snapshot(); + let change_set = cx.new_model(|cx| { + let mut change_set = BufferChangeSet::new(&buffer, cx); + change_set.recalculate_diff_sync(base_text, snapshot.text, true, cx); + change_set + }); + + reference.add_change_set(change_set.clone(), cx); + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.add_change_set(change_set, cx) + }); + buffers.push(buffer); buffers.last().unwrap() } else { buffers.choose(&mut rng).unwrap() }; let buffer = buffer_handle.read(cx); - let end_ix = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Right); - let start_ix = buffer.clip_offset(rng.gen_range(0..=end_ix), Bias::Left); + let end_row = rng.gen_range(0..=buffer.max_point().row); + let start_row = rng.gen_range(0..=end_row); + let end_ix = buffer.point_to_offset(Point::new(end_row, 0)); + let start_ix = buffer.point_to_offset(Point::new(start_row, 0)); let anchor_range = buffer.anchor_before(start_ix)..buffer.anchor_after(end_ix); - let prev_excerpt_ix = rng.gen_range(0..=expected_excerpts.len()); - let prev_excerpt_id = excerpt_ids + let prev_excerpt_ix = rng.gen_range(0..=reference.excerpts.len()); + let prev_excerpt_id = reference + .excerpts .get(prev_excerpt_ix) - .cloned() - .unwrap_or_else(ExcerptId::max); - let excerpt_ix = (prev_excerpt_ix + 1).min(expected_excerpts.len()); + .map_or(ExcerptId::max(), |e| e.id); + let excerpt_ix = (prev_excerpt_ix + 1).min(reference.excerpts.len()); log::info!( "Inserting excerpt at {} of {} for buffer {}: {:?}[{:?}] = {:?}", excerpt_ix, - expected_excerpts.len(), + reference.excerpts.len(), buffer_handle.read(cx).remote_id(), buffer.text(), start_ix..end_ix, @@ -1018,8 +2270,11 @@ fn test_random_multibuffer(cx: &mut AppContext, mut rng: StdRng) { .unwrap() }); - excerpt_ids.insert(excerpt_ix, excerpt_id); - expected_excerpts.insert(excerpt_ix, (buffer_handle.clone(), anchor_range)); + reference.insert_excerpt_after( + prev_excerpt_id, + excerpt_id, + (buffer_handle.clone(), anchor_range), + ); } } @@ -1030,49 +2285,38 @@ fn test_random_multibuffer(cx: &mut AppContext, mut rng: StdRng) { } let snapshot = multibuffer.read(cx).snapshot(cx); + let actual_text = snapshot.text(); + let actual_boundary_rows = snapshot + .excerpt_boundaries_in_range(0..) + .filter_map(|b| if b.next.is_some() { Some(b.row) } else { None }) + .collect::>(); + let actual_row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::>(); + let actual_diff = format_diff(&actual_text, &actual_row_infos, &actual_boundary_rows); - let mut excerpt_starts = Vec::new(); - let mut expected_text = String::new(); - let mut expected_buffer_rows = Vec::new(); - for (buffer, range) in &expected_excerpts { - let buffer = buffer.read(cx); - let buffer_range = range.to_offset(buffer); + let (expected_text, expected_row_infos, expected_boundary_rows) = + reference.expected_content(cx); + let expected_diff = + format_diff(&expected_text, &expected_row_infos, &expected_boundary_rows); - excerpt_starts.push(TextSummary::from(expected_text.as_str())); - expected_text.extend(buffer.text_for_range(buffer_range.clone())); - expected_text.push('\n'); - - let buffer_row_range = buffer.offset_to_point(buffer_range.start).row - ..=buffer.offset_to_point(buffer_range.end).row; - for row in buffer_row_range { - expected_buffer_rows.push(Some(row)); - } - } - // Remove final trailing newline. - if !expected_excerpts.is_empty() { - expected_text.pop(); - } - - // Always report one buffer row - if expected_buffer_rows.is_empty() { - expected_buffer_rows.push(Some(0)); - } - - assert_eq!(snapshot.text(), expected_text); - log::info!("MultiBuffer text: {:?}", expected_text); + log::info!("Multibuffer content:\n{}", actual_diff); assert_eq!( - snapshot.buffer_rows(MultiBufferRow(0)).collect::>(), - expected_buffer_rows, + 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_row_infos, expected_row_infos); + pretty_assertions::assert_eq!(actual_text, expected_text); for _ in 0..5 { - let start_row = rng.gen_range(0..=expected_buffer_rows.len()); + let start_row = rng.gen_range(0..=expected_row_infos.len()); assert_eq!( snapshot - .buffer_rows(MultiBufferRow(start_row as u32)) + .row_infos(MultiBufferRow(start_row as u32)) .collect::>(), - &expected_buffer_rows[start_row..], + &expected_row_infos[start_row..], "buffer_rows({})", start_row ); @@ -1080,135 +2324,22 @@ fn test_random_multibuffer(cx: &mut AppContext, mut rng: StdRng) { assert_eq!( snapshot.widest_line_number(), - expected_buffer_rows.into_iter().flatten().max().unwrap() + 1 + expected_row_infos + .into_iter() + .filter_map( + |info| if info.diff_status == Some(DiffHunkStatus::Removed) { + None + } else { + info.buffer_row + } + ) + .max() + .unwrap() + + 1 ); - let mut excerpt_starts = excerpt_starts.into_iter(); - for (buffer, range) in &expected_excerpts { - let buffer = buffer.read(cx); - let buffer_id = buffer.remote_id(); - let buffer_range = range.to_offset(buffer); - let buffer_start_point = buffer.offset_to_point(buffer_range.start); - let buffer_start_point_utf16 = - buffer.text_summary_for_range::(0..buffer_range.start); - - let excerpt_start = excerpt_starts.next().unwrap(); - let mut offset = excerpt_start.len; - let mut buffer_offset = buffer_range.start; - let mut point = excerpt_start.lines; - let mut buffer_point = buffer_start_point; - let mut point_utf16 = excerpt_start.lines_utf16(); - let mut buffer_point_utf16 = buffer_start_point_utf16; - for ch in buffer - .snapshot() - .chunks(buffer_range.clone(), false) - .flat_map(|c| c.text.chars()) - { - for _ in 0..ch.len_utf8() { - let left_offset = snapshot.clip_offset(offset, Bias::Left); - let right_offset = snapshot.clip_offset(offset, Bias::Right); - let buffer_left_offset = buffer.clip_offset(buffer_offset, Bias::Left); - let buffer_right_offset = buffer.clip_offset(buffer_offset, Bias::Right); - assert_eq!( - left_offset, - excerpt_start.len + (buffer_left_offset - buffer_range.start), - "clip_offset({:?}, Left). buffer: {:?}, buffer offset: {:?}", - offset, - buffer_id, - buffer_offset, - ); - assert_eq!( - right_offset, - excerpt_start.len + (buffer_right_offset - buffer_range.start), - "clip_offset({:?}, Right). buffer: {:?}, buffer offset: {:?}", - offset, - buffer_id, - buffer_offset, - ); - - let left_point = snapshot.clip_point(point, Bias::Left); - let right_point = snapshot.clip_point(point, Bias::Right); - let buffer_left_point = buffer.clip_point(buffer_point, Bias::Left); - let buffer_right_point = buffer.clip_point(buffer_point, Bias::Right); - assert_eq!( - left_point, - excerpt_start.lines + (buffer_left_point - buffer_start_point), - "clip_point({:?}, Left). buffer: {:?}, buffer point: {:?}", - point, - buffer_id, - buffer_point, - ); - assert_eq!( - right_point, - excerpt_start.lines + (buffer_right_point - buffer_start_point), - "clip_point({:?}, Right). buffer: {:?}, buffer point: {:?}", - point, - buffer_id, - buffer_point, - ); - - assert_eq!( - snapshot.point_to_offset(left_point), - left_offset, - "point_to_offset({:?})", - left_point, - ); - assert_eq!( - snapshot.offset_to_point(left_offset), - left_point, - "offset_to_point({:?})", - left_offset, - ); - - offset += 1; - buffer_offset += 1; - if ch == '\n' { - point += Point::new(1, 0); - buffer_point += Point::new(1, 0); - } else { - point += Point::new(0, 1); - buffer_point += Point::new(0, 1); - } - } - - for _ in 0..ch.len_utf16() { - let left_point_utf16 = - snapshot.clip_point_utf16(Unclipped(point_utf16), Bias::Left); - let right_point_utf16 = - snapshot.clip_point_utf16(Unclipped(point_utf16), Bias::Right); - let buffer_left_point_utf16 = - buffer.clip_point_utf16(Unclipped(buffer_point_utf16), Bias::Left); - let buffer_right_point_utf16 = - buffer.clip_point_utf16(Unclipped(buffer_point_utf16), Bias::Right); - assert_eq!( - left_point_utf16, - excerpt_start.lines_utf16() - + (buffer_left_point_utf16 - buffer_start_point_utf16), - "clip_point_utf16({:?}, Left). buffer: {:?}, buffer point_utf16: {:?}", - point_utf16, - buffer_id, - buffer_point_utf16, - ); - assert_eq!( - right_point_utf16, - excerpt_start.lines_utf16() - + (buffer_right_point_utf16 - buffer_start_point_utf16), - "clip_point_utf16({:?}, Right). buffer: {:?}, buffer point_utf16: {:?}", - point_utf16, - buffer_id, - buffer_point_utf16, - ); - - if ch == '\n' { - point_utf16 += PointUtf16::new(1, 0); - buffer_point_utf16 += PointUtf16::new(1, 0); - } else { - point_utf16 += PointUtf16::new(0, 1); - buffer_point_utf16 += PointUtf16::new(0, 1); - } - } - } - } + assert_consistent_line_numbers(&snapshot); + assert_position_translation(&snapshot); for (row, line) in expected_text.split('\n').enumerate() { assert_eq!( @@ -1234,23 +2365,6 @@ fn test_random_multibuffer(cx: &mut AppContext, mut rng: StdRng) { start_ix..end_ix ); - let snapshot = multibuffer.read(cx).snapshot(cx); - let excerpted_buffer_ranges = snapshot.range_to_buffer_ranges(start_ix..end_ix); - let excerpted_buffers_text = excerpted_buffer_ranges - .iter() - .map(|(excerpt, buffer_range)| { - excerpt - .buffer() - .text_for_range(buffer_range.clone()) - .collect::() - }) - .collect::>() - .join("\n"); - assert_eq!(excerpted_buffers_text, text_for_range); - if !expected_excerpts.is_empty() { - assert!(!excerpted_buffer_ranges.is_empty()); - } - let expected_summary = TextSummary::from(&expected_text[start_ix..end_ix]); assert_eq!( snapshot.text_summary_for_range::(start_ix..end_ix), @@ -1267,7 +2381,9 @@ fn test_random_multibuffer(cx: &mut AppContext, mut rng: StdRng) { assert!(resolved_offset <= snapshot.len()); assert_eq!( snapshot.summary_for_anchor::(anchor), - resolved_offset + resolved_offset, + "anchor: {:?}", + anchor ); } @@ -1463,527 +2579,361 @@ fn test_history(cx: &mut AppContext) { } #[gpui::test] -fn test_excerpts_in_ranges_no_ranges(cx: &mut AppContext) { - let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); - let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: 0..buffer_1.read(cx).len(), - primary: None, - }], +async fn test_enclosing_indent(cx: &mut TestAppContext) { + async fn enclosing_indent( + text: &str, + buffer_row: u32, + cx: &mut TestAppContext, + ) -> Option<(Range, LineIndent)> { + let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx)); + let snapshot = cx.read(|cx| buffer.read(cx).snapshot(cx)); + let (range, indent) = snapshot + .enclosing_indent(MultiBufferRow(buffer_row)) + .await?; + Some((range.start.0..range.end.0, indent)) + } + + assert_eq!( + enclosing_indent( + indoc!( + " + fn b() { + if c { + let d = 2; + } + } + " + ), + 1, cx, - ); - multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: 0..buffer_2.read(cx).len(), - primary: None, - }], + ) + .await, + Some(( + 1..2, + LineIndent { + tabs: 0, + spaces: 4, + line_blank: false, + } + )) + ); + + assert_eq!( + enclosing_indent( + indoc!( + " + fn b() { + if c { + let d = 2; + } + } + " + ), + 2, cx, - ); - }); + ) + .await, + Some(( + 1..2, + LineIndent { + tabs: 0, + spaces: 4, + line_blank: false, + } + )) + ); - let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); + assert_eq!( + enclosing_indent( + indoc!( + " + fn b() { + if c { + let d = 2; - let mut excerpts = snapshot.excerpts_in_ranges(iter::from_fn(|| None)); - - assert!(excerpts.next().is_none()); + let e = 5; + } + } + " + ), + 3, + cx, + ) + .await, + Some(( + 1..4, + LineIndent { + tabs: 0, + spaces: 4, + line_blank: false, + } + )) + ); } -fn validate_excerpts( - actual: &[(ExcerptId, BufferId, Range)], - expected: &Vec<(ExcerptId, BufferId, Range)>, +fn format_diff( + text: &str, + row_infos: &Vec, + boundary_rows: &HashSet, +) -> String { + let has_diff = row_infos.iter().any(|info| info.diff_status.is_some()); + text.split('\n') + .enumerate() + .zip(row_infos) + .map(|((ix, line), info)| { + let marker = match info.diff_status { + Some(DiffHunkStatus::Added) => "+ ", + Some(DiffHunkStatus::Removed) => "- ", + Some(DiffHunkStatus::Modified) => unreachable!(), + None => { + if has_diff && !line.is_empty() { + " " + } else { + "" + } + } + }; + let boundary_row = if boundary_rows.contains(&MultiBufferRow(ix as u32)) { + if has_diff { + " ----------\n" + } else { + "---------\n" + } + } else { + "" + }; + format!("{boundary_row}{marker}{line}") + }) + .collect::>() + .join("\n") +} + +#[track_caller] +fn assert_new_snapshot( + multibuffer: &Model, + snapshot: &mut MultiBufferSnapshot, + subscription: &mut Subscription, + cx: &mut TestAppContext, + expected_diff: &str, ) { - assert_eq!(actual.len(), expected.len()); - - actual - .iter() - .zip(expected) - .map(|(actual, expected)| { - assert_eq!(actual.0, expected.0); - assert_eq!(actual.1, expected.1); - assert_eq!(actual.2.start, expected.2.start); - assert_eq!(actual.2.end, expected.2.end); - }) - .collect_vec(); -} - -fn map_range_from_excerpt( - snapshot: &MultiBufferSnapshot, - excerpt_id: ExcerptId, - excerpt_buffer: &BufferSnapshot, - range: Range, -) -> Range { - snapshot - .anchor_in_excerpt(excerpt_id, excerpt_buffer.anchor_before(range.start)) - .unwrap() - ..snapshot - .anchor_in_excerpt(excerpt_id, excerpt_buffer.anchor_after(range.end)) - .unwrap() -} - -fn make_expected_excerpt_info( - snapshot: &MultiBufferSnapshot, - cx: &mut AppContext, - excerpt_id: ExcerptId, - buffer: &Model, - range: Range, -) -> (ExcerptId, BufferId, Range) { - ( - excerpt_id, - buffer.read(cx).remote_id(), - map_range_from_excerpt(snapshot, excerpt_id, &buffer.read(cx).snapshot(), range), - ) -} - -#[gpui::test] -fn test_excerpts_in_ranges_range_inside_the_excerpt(cx: &mut AppContext) { - let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); - let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); - let buffer_len = buffer_1.read(cx).len(); - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - let mut expected_excerpt_id = ExcerptId(0); - - multibuffer.update(cx, |multibuffer, cx| { - expected_excerpt_id = multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: 0..buffer_1.read(cx).len(), - primary: None, - }], - cx, - )[0]; - multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: 0..buffer_2.read(cx).len(), - primary: None, - }], - cx, - ); - }); - - let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - - let range = snapshot - .anchor_in_excerpt(expected_excerpt_id, buffer_1.read(cx).anchor_before(1)) - .unwrap() - ..snapshot - .anchor_in_excerpt( - expected_excerpt_id, - buffer_1.read(cx).anchor_after(buffer_len / 2), - ) - .unwrap(); - - let expected_excerpts = vec![make_expected_excerpt_info( - &snapshot, - cx, - expected_excerpt_id, - &buffer_1, - 1..(buffer_len / 2), - )]; - - let excerpts = snapshot - .excerpts_in_ranges(vec![range.clone()].into_iter()) - .map(|(excerpt_id, buffer, actual_range)| { - ( - excerpt_id, - buffer.remote_id(), - map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range), - ) - }) - .collect_vec(); - - validate_excerpts(&excerpts, &expected_excerpts); -} - -#[gpui::test] -fn test_excerpts_in_ranges_range_crosses_excerpts_boundary(cx: &mut AppContext) { - let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); - let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); - let buffer_len = buffer_1.read(cx).len(); - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - let mut excerpt_1_id = ExcerptId(0); - let mut excerpt_2_id = ExcerptId(0); - - multibuffer.update(cx, |multibuffer, cx| { - excerpt_1_id = multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: 0..buffer_1.read(cx).len(), - primary: None, - }], - cx, - )[0]; - excerpt_2_id = multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: 0..buffer_2.read(cx).len(), - primary: None, - }], - cx, - )[0]; - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - - let expected_range = snapshot - .anchor_in_excerpt( - excerpt_1_id, - buffer_1.read(cx).anchor_before(buffer_len / 2), - ) - .unwrap() - ..snapshot - .anchor_in_excerpt(excerpt_2_id, buffer_2.read(cx).anchor_after(buffer_len / 2)) - .unwrap(); - - let expected_excerpts = vec![ - make_expected_excerpt_info( - &snapshot, - cx, - excerpt_1_id, - &buffer_1, - (buffer_len / 2)..buffer_len, - ), - make_expected_excerpt_info(&snapshot, cx, excerpt_2_id, &buffer_2, 0..buffer_len / 2), - ]; - - let excerpts = snapshot - .excerpts_in_ranges(vec![expected_range.clone()].into_iter()) - .map(|(excerpt_id, buffer, actual_range)| { - ( - excerpt_id, - buffer.remote_id(), - map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range), - ) - }) - .collect_vec(); - - validate_excerpts(&excerpts, &expected_excerpts); -} - -#[gpui::test] -fn test_excerpts_in_ranges_range_encloses_excerpt(cx: &mut AppContext) { - let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); - let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); - let buffer_3 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'r'), cx)); - let buffer_len = buffer_1.read(cx).len(); - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - let mut excerpt_1_id = ExcerptId(0); - let mut excerpt_2_id = ExcerptId(0); - let mut excerpt_3_id = ExcerptId(0); - - multibuffer.update(cx, |multibuffer, cx| { - excerpt_1_id = multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: 0..buffer_1.read(cx).len(), - primary: None, - }], - cx, - )[0]; - excerpt_2_id = multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: 0..buffer_2.read(cx).len(), - primary: None, - }], - cx, - )[0]; - excerpt_3_id = multibuffer.push_excerpts( - buffer_3.clone(), - [ExcerptRange { - context: 0..buffer_3.read(cx).len(), - primary: None, - }], - cx, - )[0]; - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - - let expected_range = snapshot - .anchor_in_excerpt( - excerpt_1_id, - buffer_1.read(cx).anchor_before(buffer_len / 2), - ) - .unwrap() - ..snapshot - .anchor_in_excerpt(excerpt_3_id, buffer_3.read(cx).anchor_after(buffer_len / 2)) - .unwrap(); - - let expected_excerpts = vec![ - make_expected_excerpt_info( - &snapshot, - cx, - excerpt_1_id, - &buffer_1, - (buffer_len / 2)..buffer_len, - ), - make_expected_excerpt_info(&snapshot, cx, excerpt_2_id, &buffer_2, 0..buffer_len), - make_expected_excerpt_info(&snapshot, cx, excerpt_3_id, &buffer_3, 0..buffer_len / 2), - ]; - - let excerpts = snapshot - .excerpts_in_ranges(vec![expected_range.clone()].into_iter()) - .map(|(excerpt_id, buffer, actual_range)| { - ( - excerpt_id, - buffer.remote_id(), - map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range), - ) - }) - .collect_vec(); - - validate_excerpts(&excerpts, &expected_excerpts); -} - -#[gpui::test] -fn test_excerpts_in_ranges_multiple_ranges(cx: &mut AppContext) { - let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); - let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); - let buffer_len = buffer_1.read(cx).len(); - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - let mut excerpt_1_id = ExcerptId(0); - let mut excerpt_2_id = ExcerptId(0); - - multibuffer.update(cx, |multibuffer, cx| { - excerpt_1_id = multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: 0..buffer_1.read(cx).len(), - primary: None, - }], - cx, - )[0]; - excerpt_2_id = multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: 0..buffer_2.read(cx).len(), - primary: None, - }], - cx, - )[0]; - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - - let ranges = vec![ - 1..(buffer_len / 4), - (buffer_len / 3)..(buffer_len / 2), - (buffer_len / 4 * 3)..(buffer_len), - ]; - - let expected_excerpts = ranges - .iter() - .map(|range| { - make_expected_excerpt_info(&snapshot, cx, excerpt_1_id, &buffer_1, range.clone()) - }) - .collect_vec(); - - let ranges = ranges.into_iter().map(|range| { - map_range_from_excerpt( - &snapshot, - excerpt_1_id, - &buffer_1.read(cx).snapshot(), - range, - ) - }); - - let excerpts = snapshot - .excerpts_in_ranges(ranges) - .map(|(excerpt_id, buffer, actual_range)| { - ( - excerpt_id, - buffer.remote_id(), - map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range), - ) - }) - .collect_vec(); - - validate_excerpts(&excerpts, &expected_excerpts); -} - -#[gpui::test] -fn test_excerpts_in_ranges_range_ends_at_excerpt_end(cx: &mut AppContext) { - let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); - let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); - let buffer_len = buffer_1.read(cx).len(); - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - let mut excerpt_1_id = ExcerptId(0); - let mut excerpt_2_id = ExcerptId(0); - - multibuffer.update(cx, |multibuffer, cx| { - excerpt_1_id = multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: 0..buffer_1.read(cx).len(), - primary: None, - }], - cx, - )[0]; - excerpt_2_id = multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: 0..buffer_2.read(cx).len(), - primary: None, - }], - cx, - )[0]; - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - - let ranges = [0..buffer_len, (buffer_len / 3)..(buffer_len / 2)]; - - let expected_excerpts = vec![ - make_expected_excerpt_info(&snapshot, cx, excerpt_1_id, &buffer_1, ranges[0].clone()), - make_expected_excerpt_info(&snapshot, cx, excerpt_2_id, &buffer_2, ranges[1].clone()), - ]; - - let ranges = [ - map_range_from_excerpt( - &snapshot, - excerpt_1_id, - &buffer_1.read(cx).snapshot(), - ranges[0].clone(), - ), - map_range_from_excerpt( - &snapshot, - excerpt_2_id, - &buffer_2.read(cx).snapshot(), - ranges[1].clone(), - ), - ]; - - let excerpts = snapshot - .excerpts_in_ranges(ranges.into_iter()) - .map(|(excerpt_id, buffer, actual_range)| { - ( - excerpt_id, - buffer.remote_id(), - map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range), - ) - }) - .collect_vec(); - - validate_excerpts(&excerpts, &expected_excerpts); -} - -#[gpui::test] -fn test_split_ranges(cx: &mut AppContext) { - let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); - let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: 0..buffer_1.read(cx).len(), - primary: None, - }], - cx, - ); - multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: 0..buffer_2.read(cx).len(), - primary: None, - }], - cx, - ); - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - - let buffer_1_len = buffer_1.read(cx).len(); - let buffer_2_len = buffer_2.read(cx).len(); - let buffer_1_midpoint = buffer_1_len / 2; - let buffer_2_start = buffer_1_len + '\n'.len_utf8(); - let buffer_2_midpoint = buffer_2_start + buffer_2_len / 2; - let total_len = buffer_2_start + buffer_2_len; - - let input_ranges = [ - 0..buffer_1_midpoint, - buffer_1_midpoint..buffer_2_midpoint, - buffer_2_midpoint..total_len, - ] - .map(|range| snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end)); - - let actual_ranges = snapshot - .split_ranges(input_ranges.into_iter()) - .map(|range| range.to_offset(&snapshot)) + let new_snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); + let actual_text = new_snapshot.text(); + let line_infos = new_snapshot + .row_infos(MultiBufferRow(0)) .collect::>(); - - let expected_ranges = vec![ - 0..buffer_1_midpoint, - buffer_1_midpoint..buffer_1_len, - buffer_2_start..buffer_2_midpoint, - buffer_2_midpoint..total_len, - ]; - - assert_eq!(actual_ranges, expected_ranges); + let actual_diff = format_diff(&actual_text, &line_infos, &Default::default()); + pretty_assertions::assert_eq!(actual_diff, expected_diff); + check_edits( + snapshot, + &new_snapshot, + &subscription.consume().into_inner(), + ); + *snapshot = new_snapshot; } -#[gpui::test] -fn test_split_ranges_single_range_spanning_three_excerpts(cx: &mut AppContext) { - let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); - let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); - let buffer_3 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'm'), cx)); - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: 0..buffer_1.read(cx).len(), - primary: None, - }], - cx, +#[track_caller] +fn check_edits( + old_snapshot: &MultiBufferSnapshot, + new_snapshot: &MultiBufferSnapshot, + edits: &[Edit], +) { + let mut text = old_snapshot.text(); + let new_text = new_snapshot.text(); + for edit in edits.iter().rev() { + if !text.is_char_boundary(edit.old.start) + || !text.is_char_boundary(edit.old.end) + || !new_text.is_char_boundary(edit.new.start) + || !new_text.is_char_boundary(edit.new.end) + { + panic!( + "invalid edits: {:?}\nold text: {:?}\nnew text: {:?}", + edits, text, new_text + ); + } + + text.replace_range( + edit.old.start..edit.old.end, + &new_text[edit.new.start..edit.new.end], ); - multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: 0..buffer_2.read(cx).len(), - primary: None, - }], - cx, + } + + pretty_assertions::assert_eq!(text, new_text, "invalid edits: {:?}", edits); +} + +#[track_caller] +fn assert_chunks_in_ranges(snapshot: &MultiBufferSnapshot) { + let full_text = snapshot.text(); + for ix in 0..full_text.len() { + let mut chunks = snapshot.chunks(0..snapshot.len(), false); + chunks.seek(ix..snapshot.len()); + let tail = chunks.map(|chunk| chunk.text).collect::(); + assert_eq!(tail, &full_text[ix..], "seek to range: {:?}", ix..); + } +} + +#[track_caller] +fn assert_consistent_line_numbers(snapshot: &MultiBufferSnapshot) { + let all_line_numbers = snapshot.row_infos(MultiBufferRow(0)).collect::>(); + for start_row in 1..all_line_numbers.len() { + let line_numbers = snapshot + .row_infos(MultiBufferRow(start_row as u32)) + .collect::>(); + assert_eq!( + line_numbers, + all_line_numbers[start_row..], + "start_row: {start_row}" ); - multibuffer.push_excerpts( - buffer_3.clone(), - [ExcerptRange { - context: 0..buffer_3.read(cx).len(), - primary: None, - }], - cx, + } +} + +#[track_caller] +fn assert_position_translation(snapshot: &MultiBufferSnapshot) { + let text = Rope::from(snapshot.text()); + + let mut left_anchors = Vec::new(); + let mut right_anchors = Vec::new(); + let mut offsets = Vec::new(); + let mut points = Vec::new(); + for offset in 0..=text.len() + 1 { + let clipped_left = snapshot.clip_offset(offset, Bias::Left); + let clipped_right = snapshot.clip_offset(offset, Bias::Right); + assert_eq!( + clipped_left, + text.clip_offset(offset, Bias::Left), + "clip_offset({offset:?}, Left)" ); - }); + assert_eq!( + clipped_right, + text.clip_offset(offset, Bias::Right), + "clip_offset({offset:?}, Right)" + ); + assert_eq!( + snapshot.offset_to_point(clipped_left), + text.offset_to_point(clipped_left), + "offset_to_point({clipped_left})" + ); + assert_eq!( + snapshot.offset_to_point(clipped_right), + text.offset_to_point(clipped_right), + "offset_to_point({clipped_right})" + ); + let anchor_after = snapshot.anchor_after(clipped_left); + assert_eq!( + anchor_after.to_offset(snapshot), + clipped_left, + "anchor_after({clipped_left}).to_offset {anchor_after:?}" + ); + let anchor_before = snapshot.anchor_before(clipped_left); + assert_eq!( + anchor_before.to_offset(snapshot), + clipped_left, + "anchor_before({clipped_left}).to_offset" + ); + left_anchors.push(anchor_before); + right_anchors.push(anchor_after); + offsets.push(clipped_left); + points.push(text.offset_to_point(clipped_left)); + } - let snapshot = multibuffer.read(cx).snapshot(cx); + for row in 0..text.max_point().row { + for column in 0..text.line_len(row) + 1 { + let point = Point { row, column }; + let clipped_left = snapshot.clip_point(point, Bias::Left); + let clipped_right = snapshot.clip_point(point, Bias::Right); + assert_eq!( + clipped_left, + text.clip_point(point, Bias::Left), + "clip_point({point:?}, Left)" + ); + assert_eq!( + clipped_right, + text.clip_point(point, Bias::Right), + "clip_point({point:?}, Right)" + ); + assert_eq!( + snapshot.point_to_offset(clipped_left), + text.point_to_offset(clipped_left), + "point_to_offset({clipped_left:?})" + ); + assert_eq!( + snapshot.point_to_offset(clipped_right), + text.point_to_offset(clipped_right), + "point_to_offset({clipped_right:?})" + ); + } + } - let buffer_1_len = buffer_1.read(cx).len(); - let buffer_2_len = buffer_2.read(cx).len(); - let buffer_3_len = buffer_3.read(cx).len(); - let buffer_2_start = buffer_1_len + '\n'.len_utf8(); - let buffer_3_start = buffer_2_start + buffer_2_len + '\n'.len_utf8(); - let buffer_1_midpoint = buffer_1_len / 2; - let buffer_3_midpoint = buffer_3_start + buffer_3_len / 2; + assert_eq!( + snapshot.summaries_for_anchors::(&left_anchors), + offsets, + "left_anchors <-> offsets" + ); + assert_eq!( + snapshot.summaries_for_anchors::(&left_anchors), + points, + "left_anchors <-> points" + ); + assert_eq!( + snapshot.summaries_for_anchors::(&right_anchors), + offsets, + "right_anchors <-> offsets" + ); + assert_eq!( + snapshot.summaries_for_anchors::(&right_anchors), + points, + "right_anchors <-> points" + ); - let input_range = - snapshot.anchor_before(buffer_1_midpoint)..snapshot.anchor_after(buffer_3_midpoint); + for (anchors, bias) in [(&left_anchors, Bias::Left), (&right_anchors, Bias::Right)] { + for (ix, (offset, anchor)) in offsets.iter().zip(anchors).enumerate() { + if ix > 0 { + if *offset == 252 { + if offset > &offsets[ix - 1] { + let prev_anchor = left_anchors[ix - 1]; + assert!( + anchor.cmp(&prev_anchor, snapshot).is_gt(), + "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_gt()", + offsets[ix], + offsets[ix - 1], + ); + assert!( + prev_anchor.cmp(&anchor, snapshot).is_lt(), + "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_lt()", + offsets[ix - 1], + offsets[ix], + ); + } + } + } + } + } +} - let actual_ranges = snapshot - .split_ranges(std::iter::once(input_range)) - .map(|range| range.to_offset(&snapshot)) +fn assert_line_indents(snapshot: &MultiBufferSnapshot) { + let max_row = snapshot.max_point().row; + let buffer_id = snapshot.excerpts().next().unwrap().1.remote_id(); + let text = text::Buffer::new(0, buffer_id, snapshot.text()); + let mut line_indents = text + .line_indents_in_row_range(0..max_row + 1) .collect::>(); + for start_row in 0..snapshot.max_point().row { + pretty_assertions::assert_eq!( + snapshot + .line_indents(MultiBufferRow(start_row), |_| true) + .map(|(row, indent, _)| (row.0, indent)) + .collect::>(), + &line_indents[(start_row as usize)..], + "line_indents({start_row})" + ); + } - let expected_ranges = vec![ - buffer_1_midpoint..buffer_1_len, - buffer_2_start..buffer_2_start + buffer_2_len, - buffer_3_start..buffer_3_midpoint, - ]; - - assert_eq!(actual_ranges, expected_ranges); + line_indents.reverse(); + pretty_assertions::assert_eq!( + snapshot + .reversed_line_indents(MultiBufferRow(max_row), |_| true) + .map(|(row, indent, _)| (row.0, indent)) + .collect::>(), + &line_indents[..], + "reversed_line_indents({max_row})" + ); } diff --git a/crates/multi_buffer/src/position.rs b/crates/multi_buffer/src/position.rs new file mode 100644 index 0000000000..1ed2fe56e4 --- /dev/null +++ b/crates/multi_buffer/src/position.rs @@ -0,0 +1,264 @@ +use std::{ + fmt::{Debug, Display}, + marker::PhantomData, + ops::{Add, AddAssign, Sub, SubAssign}, +}; +use text::Point; + +#[repr(transparent)] +pub struct TypedOffset { + pub value: usize, + _marker: PhantomData, +} + +#[repr(transparent)] +pub struct TypedPoint { + pub value: Point, + _marker: PhantomData, +} + +#[repr(transparent)] +pub struct TypedRow { + pub value: u32, + _marker: PhantomData, +} + +impl TypedOffset { + pub fn new(offset: usize) -> Self { + Self { + value: offset, + _marker: PhantomData, + } + } + + pub fn saturating_sub(self, n: TypedOffset) -> Self { + Self { + value: self.value.saturating_sub(n.value), + _marker: PhantomData, + } + } + + pub fn zero() -> Self { + Self::new(0) + } + + pub fn is_zero(&self) -> bool { + self.value == 0 + } +} + +impl TypedPoint { + pub fn new(row: u32, column: u32) -> Self { + Self { + value: Point::new(row, column), + _marker: PhantomData, + } + } + + pub fn wrap(point: Point) -> Self { + Self { + value: point, + _marker: PhantomData, + } + } + + pub fn row(&self) -> u32 { + self.value.row + } + + pub fn column(&self) -> u32 { + self.value.column + } + + pub fn zero() -> Self { + Self::wrap(Point::zero()) + } + + pub fn is_zero(&self) -> bool { + self.value.is_zero() + } +} + +impl TypedRow { + pub fn new(row: u32) -> Self { + Self { + value: row, + _marker: PhantomData, + } + } +} + +impl Copy for TypedOffset {} +impl Copy for TypedPoint {} +impl Copy for TypedRow {} + +impl Clone for TypedOffset { + fn clone(&self) -> Self { + *self + } +} +impl Clone for TypedPoint { + fn clone(&self) -> Self { + *self + } +} +impl Clone for TypedRow { + fn clone(&self) -> Self { + *self + } +} + +impl Default for TypedOffset { + fn default() -> Self { + Self::new(0) + } +} +impl Default for TypedPoint { + fn default() -> Self { + Self::wrap(Point::default()) + } +} +impl Default for TypedRow { + fn default() -> Self { + Self::new(0) + } +} + +impl PartialOrd for TypedOffset { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.value.cmp(&other.value)) + } +} +impl PartialOrd for TypedPoint { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.value.cmp(&other.value)) + } +} +impl PartialOrd for TypedRow { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.value.cmp(&other.value)) + } +} + +impl Ord for TypedOffset { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.value.cmp(&other.value) + } +} +impl Ord for TypedPoint { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.value.cmp(&other.value) + } +} +impl Ord for TypedRow { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.value.cmp(&other.value) + } +} + +impl PartialEq for TypedOffset { + fn eq(&self, other: &Self) -> bool { + self.value == other.value + } +} +impl PartialEq for TypedPoint { + fn eq(&self, other: &Self) -> bool { + self.value == other.value + } +} +impl PartialEq for TypedRow { + fn eq(&self, other: &Self) -> bool { + self.value == other.value + } +} + +impl Eq for TypedOffset {} +impl Eq for TypedPoint {} +impl Eq for TypedRow {} + +impl Debug for TypedOffset { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}Offset({})", type_name::(), self.value) + } +} +impl Debug for TypedPoint { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}Point({}, {})", + type_name::(), + self.value.row, + self.value.column + ) + } +} +impl Debug for TypedRow { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}Row({})", type_name::(), self.value) + } +} + +impl Display for TypedOffset { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.value, f) + } +} +impl Display for TypedRow { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.value, f) + } +} + +fn type_name() -> &'static str { + std::any::type_name::().split("::").last().unwrap() +} + +impl Add> for TypedOffset { + type Output = Self; + + fn add(self, other: Self) -> Self { + TypedOffset::new(self.value + other.value) + } +} +impl Add> for TypedPoint { + type Output = Self; + + fn add(self, other: Self) -> Self { + TypedPoint::wrap(self.value + other.value) + } +} + +impl Sub> for TypedOffset { + type Output = Self; + fn sub(self, other: Self) -> Self { + TypedOffset::new(self.value - other.value) + } +} +impl Sub> for TypedPoint { + type Output = Self; + fn sub(self, other: Self) -> Self { + TypedPoint::wrap(self.value - other.value) + } +} + +impl AddAssign> for TypedOffset { + fn add_assign(&mut self, other: Self) { + self.value += other.value; + } +} +impl AddAssign> for TypedPoint { + fn add_assign(&mut self, other: Self) { + self.value += other.value; + } +} + +impl SubAssign for TypedOffset { + fn sub_assign(&mut self, other: Self) { + self.value -= other.value; + } +} +impl SubAssign for TypedRow { + fn sub_assign(&mut self, other: Self) { + self.value -= other.value; + } +} diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 9178704b82..9c46005925 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -1042,7 +1042,7 @@ impl OutlinePanel { .show_excerpt_controls(); let expand_excerpt_control_height = 1.0; if let Some(buffer_id) = scroll_to_buffer { - let current_folded = active_editor.read(cx).buffer_folded(buffer_id, cx); + let current_folded = active_editor.read(cx).is_buffer_folded(buffer_id, cx); if current_folded { if show_excerpt_controls { let previous_buffer_id = self @@ -1059,7 +1059,9 @@ impl OutlinePanel { .skip_while(|id| *id != buffer_id) .nth(1); if let Some(previous_buffer_id) = previous_buffer_id { - if !active_editor.read(cx).buffer_folded(previous_buffer_id, cx) + if !active_editor + .read(cx) + .is_buffer_folded(previous_buffer_id, cx) { offset.y += expand_excerpt_control_height; } @@ -1418,7 +1420,7 @@ impl OutlinePanel { }; active_editor.update(cx, |editor, cx| { - buffers_to_unfold.retain(|buffer_id| editor.buffer_folded(*buffer_id, cx)); + buffers_to_unfold.retain(|buffer_id| editor.is_buffer_folded(*buffer_id, cx)); }); self.select_entry(selected_entry, true, cx); if buffers_to_unfold.is_empty() { @@ -1504,7 +1506,7 @@ impl OutlinePanel { if collapsed { active_editor.update(cx, |editor, cx| { - buffers_to_fold.retain(|buffer_id| !editor.buffer_folded(*buffer_id, cx)); + buffers_to_fold.retain(|buffer_id| !editor.is_buffer_folded(*buffer_id, cx)); }); self.select_entry(selected_entry, true, cx); if buffers_to_fold.is_empty() { @@ -1569,7 +1571,7 @@ impl OutlinePanel { self.collapsed_entries .retain(|entry| !expanded_entries.contains(entry)); active_editor.update(cx, |editor, cx| { - buffers_to_unfold.retain(|buffer_id| editor.buffer_folded(*buffer_id, cx)); + buffers_to_unfold.retain(|buffer_id| editor.is_buffer_folded(*buffer_id, cx)); }); if buffers_to_unfold.is_empty() { self.update_cached_entries(None, cx); @@ -1617,7 +1619,7 @@ impl OutlinePanel { self.collapsed_entries.extend(new_entries); active_editor.update(cx, |editor, cx| { - buffers_to_fold.retain(|buffer_id| !editor.buffer_folded(*buffer_id, cx)); + buffers_to_fold.retain(|buffer_id| !editor.is_buffer_folded(*buffer_id, cx)); }); if buffers_to_fold.is_empty() { self.update_cached_entries(None, cx); @@ -1707,7 +1709,7 @@ impl OutlinePanel { active_editor.update(cx, |editor, cx| { buffers_to_toggle.retain(|buffer_id| { - let folded = editor.buffer_folded(*buffer_id, cx); + let folded = editor.is_buffer_folded(*buffer_id, cx); if fold { !folded } else { @@ -2471,7 +2473,7 @@ impl OutlinePanel { let worktree = file.map(|file| file.worktree.read(cx).snapshot()); let is_new = new_entries.contains(&excerpt_id) || !outline_panel.excerpts.contains_key(&buffer_id); - let is_folded = active_editor.read(cx).buffer_folded(buffer_id, cx); + let is_folded = active_editor.read(cx).is_buffer_folded(buffer_id, cx); buffer_excerpts .entry(buffer_id) .or_insert_with(|| (is_new, is_folded, Vec::new(), entry_id, worktree)) @@ -2875,7 +2877,7 @@ impl OutlinePanel { .excerpt_containing(selection, cx)?; let buffer_id = buffer.read(cx).remote_id(); - if editor.read(cx).buffer_folded(buffer_id, cx) { + if editor.read(cx).is_buffer_folded(buffer_id, cx) { return self .fs_entries .iter() @@ -3593,7 +3595,7 @@ impl OutlinePanel { None }; if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider { - if !active_editor.read(cx).buffer_folded(buffer_id, cx) { + if !active_editor.read(cx).is_buffer_folded(buffer_id, cx) { outline_panel.add_excerpt_entries( &mut generation_state, buffer_id, @@ -4004,12 +4006,12 @@ impl OutlinePanel { .filter(|(match_range, _)| { let editor = active_editor.read(cx); if let Some(buffer_id) = match_range.start.buffer_id { - if editor.buffer_folded(buffer_id, cx) { + if editor.is_buffer_folded(buffer_id, cx) { return false; } } if let Some(buffer_id) = match_range.start.buffer_id { - if editor.buffer_folded(buffer_id, cx) { + if editor.is_buffer_folded(buffer_id, cx) { return false; } } @@ -4883,7 +4885,7 @@ fn subscribe_for_editor_events( } }) .map(|buffer_id| { - if editor.read(cx).buffer_folded(*buffer_id, cx) { + if editor.read(cx).is_buffer_folded(*buffer_id, cx) { latest_folded_buffer_id = Some(*buffer_id); false } else { diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 03ca761fc0..1dcb84afd8 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -21,7 +21,7 @@ use language::{ deserialize_line_ending, deserialize_version, serialize_line_ending, serialize_version, split_operations, }, - Buffer, BufferEvent, Capability, DiskState, File as _, Language, Operation, + Buffer, BufferEvent, Capability, DiskState, File as _, Language, LanguageRegistry, Operation, }; use rpc::{proto, AnyProtoClient, ErrorExt as _, TypedEnvelope}; use serde::Deserialize; @@ -60,14 +60,14 @@ struct SharedBuffer { lsp_handle: Option, } -#[derive(Debug)] pub struct BufferChangeSet { pub buffer_id: BufferId, - pub base_text: Option>, + pub base_text: Option, + pub language: Option>, pub diff_to_buffer: git::diff::BufferDiff, pub recalculate_diff_task: Option>>, pub diff_updated_futures: Vec>, - pub base_text_version: usize, + pub language_registry: Option>, } enum BufferStoreState { @@ -1080,9 +1080,9 @@ impl BufferStore { Ok(text) => text, }; - let change_set = buffer.update(&mut cx, |buffer, cx| { - cx.new_model(|_| BufferChangeSet::new(buffer)) - })?; + let change_set = cx + .new_model(|cx| BufferChangeSet::new(&buffer, cx)) + .unwrap(); if let Some(text) = text { change_set @@ -1976,11 +1976,8 @@ impl BufferStore { shared.unstaged_changes = Some(change_set.clone()); } })?; - let staged_text = change_set.read_with(&cx, |change_set, cx| { - change_set - .base_text - .as_ref() - .map(|buffer| buffer.read(cx).text()) + let staged_text = change_set.read_with(&cx, |change_set, _| { + change_set.base_text.as_ref().map(|buffer| buffer.text()) })?; Ok(proto::GetStagedTextResponse { staged_text }) } @@ -2225,25 +2222,51 @@ impl BufferStore { } impl BufferChangeSet { - pub fn new(buffer: &text::BufferSnapshot) -> Self { + pub fn new(buffer: &Model, cx: &mut ModelContext) -> Self { + cx.subscribe(buffer, |this, buffer, event, cx| match event { + BufferEvent::LanguageChanged => { + this.language = buffer.read(cx).language().cloned(); + if let Some(base_text) = &this.base_text { + let snapshot = language::Buffer::build_snapshot( + base_text.as_rope().clone(), + this.language.clone(), + this.language_registry.clone(), + cx, + ); + this.recalculate_diff_task = Some(cx.spawn(|this, mut cx| async move { + let base_text = cx.background_executor().spawn(snapshot).await; + this.update(&mut cx, |this, cx| { + this.base_text = Some(base_text); + cx.notify(); + }) + })); + } + } + _ => {} + }) + .detach(); + + let buffer = buffer.read(cx); + Self { buffer_id: buffer.remote_id(), base_text: None, diff_to_buffer: git::diff::BufferDiff::new(buffer), recalculate_diff_task: None, diff_updated_futures: Vec::new(), - base_text_version: 0, + language: buffer.language().cloned(), + language_registry: buffer.language_registry(), } } #[cfg(any(test, feature = "test-support"))] pub fn new_with_base_text( base_text: String, - buffer: text::BufferSnapshot, + buffer: &Model, cx: &mut ModelContext, ) -> Self { - let mut this = Self::new(&buffer); - let _ = this.set_base_text(base_text, buffer, cx); + let mut this = Self::new(&buffer, cx); + let _ = this.set_base_text(base_text, buffer.read(cx).text_snapshot(), cx); this } @@ -2266,8 +2289,8 @@ impl BufferChangeSet { } #[cfg(any(test, feature = "test-support"))] - pub fn base_text_string(&self, cx: &AppContext) -> Option { - self.base_text.as_ref().map(|buffer| buffer.read(cx).text()) + pub fn base_text_string(&self) -> Option { + self.base_text.as_ref().map(|buffer| buffer.text()) } pub fn set_base_text( @@ -2289,7 +2312,6 @@ impl BufferChangeSet { self.base_text = None; self.diff_to_buffer = BufferDiff::new(&buffer_snapshot); self.recalculate_diff_task.take(); - self.base_text_version += 1; cx.notify(); } } @@ -2300,7 +2322,7 @@ impl BufferChangeSet { cx: &mut ModelContext, ) -> oneshot::Receiver<()> { if let Some(base_text) = self.base_text.clone() { - self.recalculate_diff_internal(base_text.read(cx).text(), buffer_snapshot, false, cx) + self.recalculate_diff_internal(base_text.text(), buffer_snapshot, false, cx) } else { oneshot::channel().1 } @@ -2316,19 +2338,30 @@ impl BufferChangeSet { let (tx, rx) = oneshot::channel(); self.diff_updated_futures.push(tx); self.recalculate_diff_task = Some(cx.spawn(|this, mut cx| async move { - let (base_text, diff) = cx + let new_base_text = if base_text_changed { + let base_text_rope: Rope = base_text.as_str().into(); + let snapshot = this.update(&mut cx, |this, cx| { + language::Buffer::build_snapshot( + base_text_rope, + this.language.clone(), + this.language_registry.clone(), + cx, + ) + })?; + Some(cx.background_executor().spawn(snapshot).await) + } else { + None + }; + let diff = cx .background_executor() - .spawn(async move { - let diff = BufferDiff::build(&base_text, &buffer_snapshot).await; - (base_text, diff) + .spawn({ + let buffer_snapshot = buffer_snapshot.clone(); + async move { BufferDiff::build(&base_text, &buffer_snapshot) } }) .await; this.update(&mut cx, |this, cx| { - if base_text_changed { - this.base_text_version += 1; - this.base_text = Some(cx.new_model(|cx| { - Buffer::local_normalized(Rope::from(base_text), LineEnding::default(), cx) - })); + if let Some(new_base_text) = new_base_text { + this.base_text = Some(new_base_text) } this.diff_to_buffer = diff; this.recalculate_diff_task.take(); @@ -2341,6 +2374,33 @@ impl BufferChangeSet { })); rx } + + #[cfg(any(test, feature = "test-support"))] + pub fn recalculate_diff_sync( + &mut self, + mut base_text: String, + buffer_snapshot: text::BufferSnapshot, + base_text_changed: bool, + cx: &mut ModelContext, + ) { + LineEnding::normalize(&mut base_text); + let diff = BufferDiff::build(&base_text, &buffer_snapshot); + if base_text_changed { + self.base_text = Some( + cx.background_executor() + .clone() + .block(Buffer::build_snapshot( + base_text.into(), + self.language.clone(), + self.language_registry.clone(), + cx, + )), + ); + } + self.diff_to_buffer = diff; + self.recalculate_diff_task.take(); + cx.notify(); + } } impl OpenBuffer { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 55598336a6..628379e162 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -1851,14 +1851,11 @@ impl LocalLspStore { let edits_since_save = std::cell::LazyCell::new(|| { let saved_version = buffer.read(cx).saved_version(); - Patch::new( - snapshot - .edits_since::>(saved_version) - .collect(), - ) + Patch::new(snapshot.edits_since::(saved_version).collect()) }); let mut sanitized_diagnostics = Vec::new(); + for entry in diagnostics { let start; let end; @@ -1866,8 +1863,8 @@ impl LocalLspStore { // Some diagnostics are based on files on disk instead of buffers' // current contents. Adjust these diagnostics' ranges to reflect // any unsaved edits. - start = (*edits_since_save).old_to_new(entry.range.start); - end = (*edits_since_save).old_to_new(entry.range.end); + start = Unclipped((*edits_since_save).old_to_new(entry.range.start.0)); + end = Unclipped((*edits_since_save).old_to_new(entry.range.end.0)); } else { start = entry.range.start; end = entry.range.end; diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index fe0bf8e0fa..c3af67ccf2 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -5651,7 +5651,7 @@ async fn test_unstaged_changes_for_buffer(cx: &mut gpui::TestAppContext) { assert_hunks( unstaged_changes.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot), &snapshot, - &unstaged_changes.base_text.as_ref().unwrap().read(cx).text(), + &unstaged_changes.base_text.as_ref().unwrap().text(), &[ (0..1, "", "// print goodbye\n"), ( @@ -5681,7 +5681,7 @@ async fn test_unstaged_changes_for_buffer(cx: &mut gpui::TestAppContext) { assert_hunks( unstaged_changes.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot), &snapshot, - &unstaged_changes.base_text.as_ref().unwrap().read(cx).text(), + &unstaged_changes.base_text.as_ref().unwrap().text(), &[(2..3, "", " println!(\"goodbye world\");\n")], ); }); diff --git a/crates/project/src/task_store.rs b/crates/project/src/task_store.rs index 0728526ad4..b8074b7d10 100644 --- a/crates/project/src/task_store.rs +++ b/crates/project/src/task_store.rs @@ -12,7 +12,7 @@ use language::{ use rpc::{proto, AnyProtoClient, TypedEnvelope}; use settings::{watch_config_file, SettingsLocation}; use task::{TaskContext, TaskVariables, VariableName}; -use text::BufferId; +use text::{BufferId, OffsetRangeExt}; use util::ResultExt; use crate::{ @@ -125,12 +125,10 @@ impl TaskStore { .filter_map(|(k, v)| Some((k.parse().log_err()?, v))), ); - for range in location - .buffer - .read(cx) - .snapshot() - .runnable_ranges(location.range.clone()) - { + let snapshot = location.buffer.read(cx).snapshot(); + let range = location.range.to_offset(&snapshot); + + for range in snapshot.runnable_ranges(range) { for (capture_name, value) in range.extra_captures { variables.insert(VariableName::Custom(capture_name.into()), value); } diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 723a5f5fc3..94610f6eb6 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -86,9 +86,9 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test .await .unwrap(); - change_set.update(cx, |change_set, cx| { + change_set.update(cx, |change_set, _| { assert_eq!( - change_set.base_text_string(cx).unwrap(), + change_set.base_text_string().unwrap(), "fn one() -> usize { 0 }" ); }); @@ -150,9 +150,9 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test &[(Path::new("src/lib2.rs"), "fn one() -> usize { 100 }".into())], ); cx.executor().run_until_parked(); - change_set.update(cx, |change_set, cx| { + change_set.update(cx, |change_set, _| { assert_eq!( - change_set.base_text_string(cx).unwrap(), + change_set.base_text_string().unwrap(), "fn one() -> usize { 100 }" ); }); diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 89cb1e7b63..95b313082a 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -4,16 +4,17 @@ mod point; mod point_utf16; mod unclipped; -use chunk::{Chunk, ChunkSlice}; +use chunk::Chunk; use rayon::iter::{IntoParallelIterator, ParallelIterator as _}; use smallvec::SmallVec; use std::{ cmp, fmt, io, mem, - ops::{AddAssign, Range}, + ops::{self, AddAssign, Range}, str, }; use sum_tree::{Bias, Dimension, SumTree}; +pub use chunk::ChunkSlice; pub use offset_utf16::OffsetUtf16; pub use point::Point; pub use point_utf16::PointUtf16; @@ -221,7 +222,7 @@ impl Rope { } pub fn summary(&self) -> TextSummary { - self.chunks.summary().text.clone() + self.chunks.summary().text } pub fn len(&self) -> usize { @@ -962,7 +963,7 @@ impl sum_tree::Summary for ChunkSummary { } /// Summary of a string of text. -#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] pub struct TextSummary { /// Length in UTF-8 pub len: usize, @@ -989,6 +990,27 @@ impl TextSummary { column: self.last_line_len_utf16, } } + + pub fn newline() -> Self { + Self { + len: 1, + len_utf16: OffsetUtf16(1), + first_line_chars: 0, + last_line_chars: 0, + last_line_len_utf16: 0, + lines: Point::new(1, 0), + longest_row: 0, + longest_row_chars: 0, + } + } + + pub fn add_newline(&mut self) { + self.len += 1; + self.len_utf16 += OffsetUtf16(self.len_utf16.0 + 1); + self.last_line_chars = 0; + self.last_line_len_utf16 = 0; + self.lines += Point::new(1, 0); + } } impl<'a> From<&'a str> for TextSummary { @@ -1048,7 +1070,7 @@ impl sum_tree::Summary for TextSummary { } } -impl std::ops::Add for TextSummary { +impl ops::Add for TextSummary { type Output = Self; fn add(mut self, rhs: Self) -> Self::Output { @@ -1057,7 +1079,7 @@ impl std::ops::Add for TextSummary { } } -impl<'a> std::ops::AddAssign<&'a Self> for TextSummary { +impl<'a> ops::AddAssign<&'a Self> for TextSummary { fn add_assign(&mut self, other: &'a Self) { let joined_chars = self.last_line_chars + other.first_line_chars; if joined_chars > self.longest_row_chars { @@ -1087,13 +1109,15 @@ impl<'a> std::ops::AddAssign<&'a Self> for TextSummary { } } -impl std::ops::AddAssign for TextSummary { +impl ops::AddAssign for TextSummary { fn add_assign(&mut self, other: Self) { *self += &other; } } -pub trait TextDimension: 'static + for<'a> Dimension<'a, ChunkSummary> { +pub trait TextDimension: + 'static + Clone + Copy + Default + for<'a> Dimension<'a, ChunkSummary> + std::fmt::Debug +{ fn from_text_summary(summary: &TextSummary) -> Self; fn from_chunk(chunk: ChunkSlice) -> Self; fn add_assign(&mut self, other: &Self); @@ -1129,7 +1153,7 @@ impl<'a> sum_tree::Dimension<'a, ChunkSummary> for TextSummary { impl TextDimension for TextSummary { fn from_text_summary(summary: &TextSummary) -> Self { - summary.clone() + *summary } fn from_chunk(chunk: ChunkSlice) -> Self { @@ -1240,6 +1264,118 @@ impl TextDimension for PointUtf16 { } } +/// A pair of text dimensions in which only the first dimension is used for comparison, +/// but both dimensions are updated during addition and subtraction. +#[derive(Clone, Copy, Debug)] +pub struct DimensionPair { + pub key: K, + pub value: Option, +} + +impl Default for DimensionPair { + fn default() -> Self { + Self { + key: Default::default(), + value: Some(Default::default()), + } + } +} + +impl cmp::Ord for DimensionPair +where + K: cmp::Ord, +{ + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.key.cmp(&other.key) + } +} + +impl cmp::PartialOrd for DimensionPair +where + K: cmp::PartialOrd, +{ + fn partial_cmp(&self, other: &Self) -> Option { + self.key.partial_cmp(&other.key) + } +} + +impl cmp::PartialEq for DimensionPair +where + K: cmp::PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.key.eq(&other.key) + } +} + +impl ops::Sub for DimensionPair +where + K: ops::Sub, + V: ops::Sub, +{ + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + Self { + key: self.key - rhs.key, + value: self.value.zip(rhs.value).map(|(a, b)| a - b), + } + } +} + +impl cmp::Eq for DimensionPair where K: cmp::Eq {} + +impl<'a, K, V> sum_tree::Dimension<'a, ChunkSummary> for DimensionPair +where + K: sum_tree::Dimension<'a, ChunkSummary>, + V: sum_tree::Dimension<'a, ChunkSummary>, +{ + fn zero(_cx: &()) -> Self { + Self { + key: K::zero(_cx), + value: Some(V::zero(_cx)), + } + } + + fn add_summary(&mut self, summary: &'a ChunkSummary, _cx: &()) { + self.key.add_summary(summary, _cx); + if let Some(value) = &mut self.value { + value.add_summary(summary, _cx); + } + } +} + +impl TextDimension for DimensionPair +where + K: TextDimension, + V: TextDimension, +{ + fn add_assign(&mut self, other: &Self) { + self.key.add_assign(&other.key); + if let Some(value) = &mut self.value { + if let Some(other_value) = other.value.as_ref() { + value.add_assign(other_value); + } else { + self.value.take(); + } + } + } + + fn from_chunk(chunk: ChunkSlice) -> Self { + Self { + key: K::from_chunk(chunk), + value: Some(V::from_chunk(chunk)), + } + } + + fn from_text_summary(summary: &TextSummary) -> Self { + Self { + key: K::from_text_summary(summary), + value: Some(V::from_text_summary(summary)), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/rope/src/unclipped.rs b/crates/rope/src/unclipped.rs index 679901875c..051293bfc7 100644 --- a/crates/rope/src/unclipped.rs +++ b/crates/rope/src/unclipped.rs @@ -1,4 +1,4 @@ -use crate::{chunk::ChunkSlice, ChunkSummary, TextDimension, TextSummary}; +use crate::ChunkSummary; use std::ops::{Add, AddAssign, Sub, SubAssign}; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -22,20 +22,6 @@ impl<'a, T: sum_tree::Dimension<'a, ChunkSummary>> sum_tree::Dimension<'a, Chunk } } -impl TextDimension for Unclipped { - fn from_text_summary(summary: &TextSummary) -> Self { - Unclipped(T::from_text_summary(summary)) - } - - fn from_chunk(chunk: ChunkSlice) -> Self { - Unclipped(T::from_chunk(chunk)) - } - - fn add_assign(&mut self, other: &Self) { - TextDimension::add_assign(&mut self.0, &other.0); - } -} - impl> Add> for Unclipped { type Output = Unclipped; diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index fa37c67599..15cb0bb813 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -115,14 +115,29 @@ impl<'a, T: Summary, D1: Dimension<'a, T>, D2: Dimension<'a, T>> Dimension<'a, T } } -impl<'a, S: Summary, D1: SeekTarget<'a, S, D1> + Dimension<'a, S>, D2: Dimension<'a, S>> - SeekTarget<'a, S, (D1, D2)> for D1 +impl<'a, S, D1, D2> SeekTarget<'a, S, (D1, D2)> for D1 +where + S: Summary, + D1: SeekTarget<'a, S, D1> + Dimension<'a, S>, + D2: Dimension<'a, S>, { fn cmp(&self, cursor_location: &(D1, D2), cx: &S::Context) -> Ordering { self.cmp(&cursor_location.0, cx) } } +impl<'a, S, D1, D2, D3> SeekTarget<'a, S, ((D1, D2), D3)> for D1 +where + S: Summary, + D1: SeekTarget<'a, S, D1> + Dimension<'a, S>, + D2: Dimension<'a, S>, + D3: Dimension<'a, S>, +{ + fn cmp(&self, cursor_location: &((D1, D2), D3), cx: &S::Context) -> Ordering { + self.cmp(&cursor_location.0 .0, cx) + } +} + struct End(PhantomData); impl End { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 8bd0fc97f7..982d544375 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -5,11 +5,7 @@ pub mod terminal_scrollbar; pub mod terminal_tab_tooltip; use collections::HashSet; -use editor::{ - actions::SelectAll, - scroll::{Autoscroll, ScrollbarAutoHide}, - Editor, EditorSettings, -}; +use editor::{actions::SelectAll, scroll::ScrollbarAutoHide, Editor, EditorSettings}; use futures::{stream::FuturesUnordered, StreamExt}; use gpui::{ anchored, deferred, div, impl_actions, AnyElement, AppContext, DismissEvent, EventEmitter, @@ -17,7 +13,6 @@ use gpui::{ MouseDownEvent, Pixels, Render, ScrollWheelEvent, Stateful, Styled, Subscription, Task, View, VisualContext, WeakModel, WeakView, }; -use language::Bias; use persistence::TERMINAL_DB; use project::{search::SearchQuery, terminals::TerminalKind, Fs, Metadata, Project}; use schemars::JsonSchema; @@ -885,19 +880,13 @@ fn subscribe_for_terminal_events( active_editor .downgrade() .update(&mut cx, |editor, cx| { - let snapshot = editor.snapshot(cx).display_snapshot; - let point = snapshot.buffer_snapshot.clip_point( + editor.go_to_singleton_buffer_point( language::Point::new( row.saturating_sub(1), col.saturating_sub(1), ), - Bias::Left, - ); - editor.change_selections( - Some(Autoscroll::center()), cx, - |s| s.select_ranges([point..point]), - ); + ) }) .log_err(); } diff --git a/crates/text/src/patch.rs b/crates/text/src/patch.rs index bb71a80c51..ff1d0c2e9a 100644 --- a/crates/text/src/patch.rs +++ b/crates/text/src/patch.rs @@ -42,6 +42,7 @@ where self.0 } + #[must_use] pub fn compose(&self, new_edits_iter: impl IntoIterator>) -> Self { let mut old_edits_iter = self.0.iter().cloned().peekable(); let mut new_edits_iter = new_edits_iter.into_iter().peekable(); diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index d869791864..55d2b2b623 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1507,9 +1507,9 @@ impl Buffer { let mut rope_cursor = self.visible_text.cursor(0); disjoint_ranges.map(move |range| { position.add_assign(&rope_cursor.summary(range.start)); - let start = position.clone(); + let start = position; position.add_assign(&rope_cursor.summary(range.end)); - let end = position.clone(); + let end = position; start..end }) } @@ -2029,11 +2029,11 @@ impl BufferSnapshot { row_range: Range, ) -> impl Iterator + '_ { let start = Point::new(row_range.start, 0).to_offset(self); - let end = Point::new(row_range.end - 1, self.line_len(row_range.end - 1)).to_offset(self); + let end = Point::new(row_range.end, self.line_len(row_range.end)).to_offset(self); let mut chunks = self.as_rope().chunks_in_range(start..end); let mut row = row_range.start; - let mut done = start == end; + let mut done = false; std::iter::from_fn(move || { if done { None @@ -2071,7 +2071,7 @@ impl BufferSnapshot { } let mut row = end_point.row; - let mut done = start == end; + let mut done = false; std::iter::from_fn(move || { if done { None @@ -2168,7 +2168,7 @@ impl BufferSnapshot { } position.add_assign(&text_cursor.summary(fragment_offset)); - (position.clone(), payload) + (position, payload) }) } @@ -2176,10 +2176,14 @@ impl BufferSnapshot { where D: TextDimension, { + self.text_summary_for_range(0..self.offset_for_anchor(anchor)) + } + + pub fn offset_for_anchor(&self, anchor: &Anchor) -> usize { if *anchor == Anchor::MIN { - D::zero(&()) + 0 } else if *anchor == Anchor::MAX { - D::from_text_summary(&self.visible_text.summary()) + self.visible_text.len() } else { let anchor_key = InsertionFragmentKey { timestamp: anchor.timestamp, @@ -2217,7 +2221,7 @@ impl BufferSnapshot { if fragment.visible { fragment_offset += anchor.offset - insertion.split_offset; } - self.text_summary_for_range(0..fragment_offset) + fragment_offset } } @@ -2580,16 +2584,16 @@ impl<'a, D: TextDimension + Ord, F: FnMut(&FragmentSummary) -> bool> Iterator fo } let fragment_summary = self.visible_cursor.summary(visible_end); - let mut new_end = self.new_end.clone(); + let mut new_end = self.new_end; new_end.add_assign(&fragment_summary); if let Some((edit, range)) = pending_edit.as_mut() { - edit.new.end = new_end.clone(); + edit.new.end = new_end; range.end = end_anchor; } else { pending_edit = Some(( Edit { - old: self.old_end.clone()..self.old_end.clone(), - new: self.new_end.clone()..new_end.clone(), + old: self.old_end..self.old_end, + new: self.new_end..new_end, }, start_anchor..end_anchor, )); @@ -2609,16 +2613,16 @@ impl<'a, D: TextDimension + Ord, F: FnMut(&FragmentSummary) -> bool> Iterator fo self.deleted_cursor.seek_forward(cursor.start().deleted); } let fragment_summary = self.deleted_cursor.summary(deleted_end); - let mut old_end = self.old_end.clone(); + let mut old_end = self.old_end; old_end.add_assign(&fragment_summary); if let Some((edit, range)) = pending_edit.as_mut() { - edit.old.end = old_end.clone(); + edit.old.end = old_end; range.end = end_anchor; } else { pending_edit = Some(( Edit { - old: self.old_end.clone()..old_end.clone(), - new: self.new_end.clone()..self.new_end.clone(), + old: self.old_end..old_end, + new: self.new_end..self.new_end, }, start_anchor..end_anchor, )); diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index abb4ec8bd8..9836c9505d 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -138,22 +138,27 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext) { Vim::action(editor, cx, |vim, action: &GoToLine, cx| { vim.switch_mode(Mode::Normal, false, cx); let result = vim.update_editor(cx, |vim, editor, cx| { - action.range.head().buffer_row(vim, editor, cx) + let snapshot = editor.snapshot(cx); + let buffer_row = action.range.head().buffer_row(vim, editor, cx)?; + let current = editor.selections.newest::(cx); + let target = snapshot + .buffer_snapshot + .clip_point(Point::new(buffer_row.0, current.head().column), Bias::Left); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([target..target]); + }); + + anyhow::Ok(()) }); - let buffer_row = match result { - None => return, - Some(e @ Err(_)) => { - let Some(workspace) = vim.workspace(cx) else { - return; - }; - workspace.update(cx, |workspace, cx| { - e.notify_err(workspace, cx); - }); + if let Some(e @ Err(_)) = result { + let Some(workspace) = vim.workspace(cx) else { return; - } - Some(Ok(result)) => result, - }; - vim.move_cursor(Motion::StartOfDocument, Some(buffer_row.0 as usize + 1), cx); + }; + workspace.update(cx, |workspace, cx| { + e.notify_err(workspace, cx); + }); + return; + } }); Vim::action(editor, cx, |vim, action: &YankCommand, cx| { @@ -462,7 +467,22 @@ impl Position { ) -> Result { let snapshot = editor.snapshot(cx); let target = match self { - Position::Line { row, offset } => row.saturating_add_signed(offset.saturating_sub(1)), + Position::Line { row, offset } => { + if let Some(anchor) = editor.active_excerpt(cx).and_then(|(_, buffer, _)| { + editor.buffer().read(cx).buffer_point_to_anchor( + &buffer, + Point::new(row.saturating_sub(1), 0), + cx, + ) + }) { + anchor + .to_point(&snapshot.buffer_snapshot) + .row + .saturating_add_signed(*offset) + } else { + row.saturating_add_signed(offset.saturating_sub(1)) + } + } Position::Mark { name, offset } => { let Some(mark) = vim.marks.get(&name.to_string()).and_then(|vec| vec.last()) else { return Err(anyhow!("mark {} not set", name)); @@ -697,7 +717,8 @@ fn generate_commands(_: &AppContext) -> Vec { VimCommand::new(("foldc", "lose"), editor::actions::Fold) .bang(editor::actions::FoldRecursive) .range(act_on_range), - VimCommand::new(("dif", "fupdate"), editor::actions::ToggleHunkDiff).range(act_on_range), + VimCommand::new(("dif", "fupdate"), editor::actions::ToggleSelectedDiffHunks) + .range(act_on_range), VimCommand::new(("rev", "ert"), editor::actions::RevertSelectedHunks).range(act_on_range), VimCommand::new(("d", "elete"), VisualDeleteLine).range(select_range), VimCommand::new(("y", "ank"), gpui::NoAction).range(|_, range| { diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 6539aab09f..6a40d3fe82 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -4,7 +4,7 @@ use editor::{ self, find_boundary, find_preceding_boundary_display_point, FindRange, TextLayoutDetails, }, scroll::Autoscroll, - Anchor, Bias, DisplayPoint, Editor, RowExt, ToOffset, + Anchor, Bias, DisplayPoint, Editor, RowExt, ToOffset, ToPoint, }; use gpui::{actions, impl_actions, px, ViewContext}; use language::{CharKind, Point, Selection, SelectionGoal}; @@ -847,7 +847,10 @@ impl Motion { SelectionGoal::None, ), CurrentLine => (next_line_end(map, point, times), SelectionGoal::None), - StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None), + StartOfDocument => ( + start_of_document(map, point, maybe_times), + SelectionGoal::None, + ), EndOfDocument => ( end_of_document(map, point, maybe_times), SelectionGoal::None, @@ -1956,25 +1959,96 @@ fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Opti Some(map.buffer_snapshot.len()) } -fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint { - let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map); - *new_point.column_mut() = point.column(); - map.clip_point(new_point, Bias::Left) +fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint { + let point = map.display_point_to_point(display_point, Bias::Left); + let Some(mut excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else { + return display_point; + }; + let offset = excerpt.buffer().point_to_offset( + excerpt + .buffer() + .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left), + ); + let buffer_range = excerpt.buffer_range(); + if offset >= buffer_range.start && offset <= buffer_range.end { + let point = map + .buffer_snapshot + .offset_to_point(excerpt.map_offset_from_buffer(offset)); + return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left); + } + let mut last_position = None; + for (excerpt, buffer, range) in map.buffer_snapshot.excerpts() { + let excerpt_range = language::ToOffset::to_offset(&range.context.start, &buffer) + ..language::ToOffset::to_offset(&range.context.end, &buffer); + if offset >= excerpt_range.start && offset <= excerpt_range.end { + let text_anchor = buffer.anchor_after(offset); + let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), text_anchor); + return anchor.to_display_point(map); + } else if offset <= excerpt_range.start { + let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), range.context.start); + return anchor.to_display_point(map); + } else { + last_position = Some(Anchor::in_buffer( + excerpt, + buffer.remote_id(), + range.context.end, + )); + } + } + + let mut last_point = last_position.unwrap().to_point(&map.buffer_snapshot); + last_point.column = point.column; + + map.clip_point( + map.point_to_display_point( + map.buffer_snapshot.clip_point(point, Bias::Left), + Bias::Left, + ), + Bias::Left, + ) +} + +fn start_of_document( + map: &DisplaySnapshot, + display_point: DisplayPoint, + maybe_times: Option, +) -> DisplayPoint { + if let Some(times) = maybe_times { + return go_to_line(map, display_point, times); + } + + let point = map.display_point_to_point(display_point, Bias::Left); + let mut first_point = Point::zero(); + first_point.column = point.column; + + map.clip_point( + map.point_to_display_point( + map.buffer_snapshot.clip_point(first_point, Bias::Left), + Bias::Left, + ), + Bias::Left, + ) } fn end_of_document( map: &DisplaySnapshot, - point: DisplayPoint, - line: Option, + display_point: DisplayPoint, + maybe_times: Option, ) -> DisplayPoint { - let new_row = if let Some(line) = line { - (line - 1) as u32 - } else { - map.buffer_snapshot.max_row().0 + if let Some(times) = maybe_times { + return go_to_line(map, display_point, times); }; + let point = map.display_point_to_point(display_point, Bias::Left); + let mut last_point = map.buffer_snapshot.max_point(); + last_point.column = point.column; - let new_point = Point::new(new_row, point.column()); - map.clip_point(new_point.to_display_point(map), Bias::Left) + map.clip_point( + map.point_to_display_point( + map.buffer_snapshot.clip_point(last_point, Bias::Left), + Bias::Left, + ), + Bias::Left, + ) } fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option { @@ -2545,7 +2619,7 @@ fn section_motion( direction: Direction, is_start: bool, ) -> DisplayPoint { - if let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() { + if map.buffer_snapshot.as_singleton().is_some() { for _ in 0..times { let offset = map .display_point_to_point(display_point, Bias::Left) @@ -2553,13 +2627,14 @@ fn section_motion( let range = if direction == Direction::Prev { 0..offset } else { - offset..buffer.len() + offset..map.buffer_snapshot.len() }; // we set a max start depth here because we want a section to only be "top level" // similar to vim's default of '{' in the first column. // (and without it, ]] at the start of editor.rs is -very- slow) - let mut possibilities = buffer + let mut possibilities = map + .buffer_snapshot .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3)) .filter(|(_, object)| { matches!( @@ -2591,7 +2666,7 @@ fn section_motion( let offset = if direction == Direction::Prev { possibilities.max().unwrap_or(0) } else { - possibilities.min().unwrap_or(buffer.len()) + possibilities.min().unwrap_or(map.buffer_snapshot.len()) }; let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left); diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index c08bd70fb2..733f7d9cde 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -494,7 +494,7 @@ pub fn surrounding_html_tag( let snapshot = &map.buffer_snapshot; let offset = head.to_offset(map, Bias::Left); - let excerpt = snapshot.excerpt_containing(offset..offset)?; + let mut excerpt = snapshot.excerpt_containing(offset..offset)?; let buffer = excerpt.buffer(); let offset = excerpt.map_offset_to_buffer(offset); @@ -664,7 +664,7 @@ fn text_object( let snapshot = &map.buffer_snapshot; let offset = relative_to.to_offset(map, Bias::Left); - let excerpt = snapshot.excerpt_containing(offset..offset)?; + let mut excerpt = snapshot.excerpt_containing(offset..offset)?; let buffer = excerpt.buffer(); let offset = excerpt.map_offset_to_buffer(offset); @@ -710,7 +710,7 @@ fn argument( let offset = relative_to.to_offset(map, Bias::Left); // The `argument` vim text object uses the syntax tree, so we operate at the buffer level and map back to the display level - let excerpt = snapshot.excerpt_containing(offset..offset)?; + let mut excerpt = snapshot.excerpt_containing(offset..offset)?; let buffer = excerpt.buffer(); fn comma_delimited_range_at( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6b3486fc8c..c414d29aa4 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -102,6 +102,8 @@ use crate::persistence::{ SerializedAxis, }; +pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200); + static ZED_WINDOW_SIZE: LazyLock>> = LazyLock::new(|| { env::var("ZED_WINDOW_SIZE") .ok() @@ -4344,7 +4346,6 @@ impl Workspace { cx: &mut AsyncWindowContext, ) -> Result<()> { const CHUNK_SIZE: usize = 200; - const THROTTLE_TIME: Duration = Duration::from_millis(200); let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE); @@ -4369,7 +4370,9 @@ impl Workspace { } } - cx.background_executor().timer(THROTTLE_TIME).await; + cx.background_executor() + .timer(SERIALIZATION_THROTTLE_TIME) + .await; } Ok(()) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index fcab94f0d2..ed21463f57 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1469,7 +1469,7 @@ mod tests { use workspace::{ item::{Item, ItemHandle}, open_new, open_paths, pane, NewFile, OpenVisible, SaveIntent, SplitDirection, - WorkspaceHandle, + WorkspaceHandle, SERIALIZATION_THROTTLE_TIME, }; #[gpui::test] @@ -2866,7 +2866,9 @@ mod tests { }) .unwrap(); - cx.run_until_parked(); + cx.background_executor + .advance_clock(SERIALIZATION_THROTTLE_TIME); + cx.update(|_| {}); editor_1.assert_released(); editor_2.assert_released(); buffer.assert_released(); diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index eaa912d990..f0589f8d50 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -6,7 +6,6 @@ use cli::{ipc::IpcSender, CliRequest, CliResponse}; use client::parse_zed_link; use collections::HashMap; use db::kvp::KEY_VALUE_STORE; -use editor::scroll::Autoscroll; use editor::Editor; use fs::Fs; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; @@ -14,7 +13,7 @@ use futures::channel::{mpsc, oneshot}; use futures::future::join_all; use futures::{FutureExt, SinkExt, StreamExt}; use gpui::{AppContext, AsyncAppContext, Global, WindowHandle}; -use language::{Bias, Point}; +use language::Point; use recent_projects::{open_ssh_project, SshSettings}; use remote::SshConnectionOptions; use settings::Settings; @@ -236,11 +235,7 @@ pub async fn open_paths_with_positions( workspace .update(cx, |_, cx| { active_editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx).display_snapshot; - let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); - editor.change_selections(Some(Autoscroll::center()), cx, |s| { - s.select_ranges([point..point]) - }); + editor.go_to_singleton_buffer_point(point, cx); }); }) .log_err();