From caa0d35b8beba7944e1ff3e7e6c7b65072ad47f8 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 1 May 2024 22:47:36 +0300 Subject: [PATCH] Allow to toggle git hunk diffs (#11080) Part of https://github.com/zed-industries/zed/issues/4523 Added two new actions with the default keybindings ``` "cmd-'": "editor::ToggleHunkDiff", "cmd-\"": "editor::ExpandAllHunkDiffs", ``` that allow to browse git hunk diffs in Zed: https://github.com/zed-industries/zed/assets/2690773/9a8a7d10-ed06-4960-b4ee-fe28fc5c4768 The hunks are dynamic and alter on user folds and modifications, or toggle hidden, if the modifications were not adjacent to the expanded hunk. Release Notes: - Added `editor::ToggleHunkDiff` (`cmd-'`) and `editor::ExpandAllHunkDiffs` (`cmd-"`) actions to browse git hunk diffs in Zed --- Cargo.lock | 2 + assets/keymaps/default-linux.json | 2 + assets/keymaps/default-macos.json | 2 + assets/settings/default.json | 15 +- crates/collab/src/tests/editor_tests.rs | 108 +- crates/editor/src/actions.rs | 2 + crates/editor/src/display_map/block_map.rs | 47 +- crates/editor/src/editor.rs | 174 +- crates/editor/src/editor_tests.rs | 1760 +++++++++++++++++++- crates/editor/src/element.rs | 421 +++-- crates/editor/src/git.rs | 10 +- crates/editor/src/hunk_diff.rs | 623 +++++++ crates/editor/src/scroll.rs | 22 + crates/editor/src/scroll/autoscroll.rs | 15 +- crates/editor/src/test.rs | 90 + crates/git/src/diff.rs | 5 +- crates/go_to_line/Cargo.toml | 1 + crates/go_to_line/src/go_to_line.rs | 6 +- crates/language/src/buffer.rs | 13 +- crates/language/src/language_settings.rs | 2 + crates/multi_buffer/src/multi_buffer.rs | 31 + crates/outline/Cargo.toml | 1 + crates/outline/src/outline.rs | 10 +- crates/project/src/project_settings.rs | 2 +- 24 files changed, 3115 insertions(+), 249 deletions(-) create mode 100644 crates/editor/src/hunk_diff.rs diff --git a/Cargo.lock b/Cargo.lock index e3347a2d9e..a5fbb22226 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4467,6 +4467,7 @@ name = "go_to_line" version = "0.1.0" dependencies = [ "anyhow", + "collections", "editor", "gpui", "indoc", @@ -6787,6 +6788,7 @@ dependencies = [ name = "outline" version = "0.1.0" dependencies = [ + "collections", "editor", "fuzzy", "gpui", diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 34c71f5228..26a32dd44a 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -138,6 +138,8 @@ "ctrl-alt-space": "editor::ShowCharacterPalette", "ctrl-;": "editor::ToggleLineNumbers", "ctrl-k ctrl-r": "editor::RevertSelectedHunks", + "ctrl-'": "editor::ToggleHunkDiff", + "ctrl-\"": "editor::ExpandAllHunkDiffs", "ctrl-alt-g b": "editor::ToggleGitBlame" } }, diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 3e2fae47da..d60c6326bb 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -159,6 +159,8 @@ "ctrl-cmd-space": "editor::ShowCharacterPalette", "cmd-;": "editor::ToggleLineNumbers", "cmd-alt-z": "editor::RevertSelectedHunks", + "cmd-'": "editor::ToggleHunkDiff", + "cmd-\"": "editor::ExpandAllHunkDiffs", "cmd-alt-g b": "editor::ToggleGitBlame" } }, diff --git a/assets/settings/default.json b/assets/settings/default.json index dcdd909da9..c8560d7f15 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -299,7 +299,9 @@ // The list of language servers to use (or disable) for all languages. // // This is typically customized on a per-language basis. - "language_servers": ["..."], + "language_servers": [ + "..." + ], // When to automatically save edited buffers. This setting can // take four values. // @@ -428,7 +430,9 @@ "copilot": { // The set of glob patterns for which copilot should be disabled // in any matching file. - "disabled_globs": [".env"] + "disabled_globs": [ + ".env" + ] }, // Settings specific to journaling "journal": { @@ -539,7 +543,12 @@ // Default directories to search for virtual environments, relative // to the current working directory. We recommend overriding this // in your project's settings, rather than globally. - "directories": [".env", "env", ".venv", "venv"], + "directories": [ + ".env", + "env", + ".venv", + "venv" + ], // Can also be 'csh', 'fish', and `nushell` "activate_script": "default" } diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 528fb066bd..e07ff0cdf8 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -9,10 +9,15 @@ use editor::{ ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Redo, Rename, RevertSelectedHunks, ToggleCodeActions, Undo, }, - test::editor_test_context::{AssertionContextManager, EditorTestContext}, + test::{ + editor_hunks, + editor_test_context::{AssertionContextManager, EditorTestContext}, + expanded_hunks, expanded_hunks_background_highlights, + }, Editor, }; use futures::StreamExt; +use git::diff::DiffHunkStatus; use gpui::{BorrowAppContext, TestAppContext, VisualContext, VisualTestContext}; use indoc::indoc; use language::{ @@ -1875,7 +1880,7 @@ async fn test_inlay_hint_refresh_is_forwarded( } #[gpui::test] -async fn test_multiple_types_reverts(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { +async fn test_multiple_hunk_types_revert(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let mut server = TestServer::start(cx_a.executor()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -1997,8 +2002,8 @@ struct Row10;"#}; cx_a.executor().run_until_parked(); cx_b.executor().run_until_parked(); - // client, selects a range in the updated buffer, and reverts it - // both host and the client observe the reverted state (with one hunk left, not covered by client's selection) + // the client selects a range in the updated buffer, expands it to see the diff for each hunk in the selection + // the host does not see the diffs toggled editor_cx_b.set_selections_state(indoc! {r#"«ˇstruct Row; struct Row0.1; struct Row0.2; @@ -2010,11 +2015,106 @@ struct Row10;"#}; struct R»ow9; struct Row1220;"#}); + editor_cx_b + .update_editor(|editor, cx| editor.toggle_hunk_diff(&editor::actions::ToggleHunkDiff, cx)); + cx_a.executor().run_until_parked(); + cx_b.executor().run_until_parked(); + editor_cx_a.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + Vec::new(), + ); + assert_eq!( + all_hunks, + vec![ + ("".to_string(), DiffHunkStatus::Added, 1..3), + ("struct Row2;\n".to_string(), DiffHunkStatus::Removed, 4..4), + ("struct Row5;\n".to_string(), DiffHunkStatus::Modified, 6..7), + ("struct Row8;\n".to_string(), DiffHunkStatus::Removed, 9..9), + ( + "struct Row10;".to_string(), + DiffHunkStatus::Modified, + 10..10, + ), + ] + ); + assert_eq!(all_expanded_hunks, Vec::new()); + }); + editor_cx_b.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + vec![1..3, 8..9], + ); + assert_eq!( + all_hunks, + vec![ + ("".to_string(), DiffHunkStatus::Added, 1..3), + ("struct Row2;\n".to_string(), DiffHunkStatus::Removed, 5..5), + ("struct Row5;\n".to_string(), DiffHunkStatus::Modified, 8..9), + ( + "struct Row8;\n".to_string(), + DiffHunkStatus::Removed, + 12..12 + ), + ( + "struct Row10;".to_string(), + DiffHunkStatus::Modified, + 13..13, + ), + ] + ); + assert_eq!(all_expanded_hunks, &all_hunks[..all_hunks.len() - 1]); + }); + + // the client reverts the hunks, removing the expanded diffs too + // both host and the client observe the reverted state (with one hunk left, not covered by client's selection) editor_cx_b.update_editor(|editor, cx| { editor.revert_selected_hunks(&RevertSelectedHunks, cx); }); cx_a.executor().run_until_parked(); cx_b.executor().run_until_parked(); + editor_cx_a.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + Vec::new(), + ); + assert_eq!( + all_hunks, + vec![( + "struct Row10;".to_string(), + DiffHunkStatus::Modified, + 10..10, + )] + ); + assert_eq!(all_expanded_hunks, Vec::new()); + }); + editor_cx_b.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + Vec::new(), + ); + assert_eq!( + all_hunks, + vec![( + "struct Row10;".to_string(), + DiffHunkStatus::Modified, + 10..10, + )] + ); + assert_eq!(all_expanded_hunks, Vec::new()); + }); editor_cx_a.assert_editor_state(indoc! {r#"struct Row; struct Row1; struct Row2; diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 1494c23c00..b050894bc7 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -253,6 +253,8 @@ gpui::actions!( TabPrev, ToggleGitBlame, ToggleGitBlameInline, + ToggleHunkDiff, + ExpandAllHunkDiffs, ToggleInlayHints, ToggleLineNumbers, ToggleSoftWrap, diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 026d5365cf..6cadf67845 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -364,28 +364,33 @@ impl BlockMap { (position.row(), TransformBlock::Custom(block.clone())) }), ); - blocks_in_edit.extend( - buffer - .excerpt_boundaries_in_range((start_bound, end_bound)) - .map(|excerpt_boundary| { - ( - wrap_snapshot - .make_wrap_point(Point::new(excerpt_boundary.row, 0), Bias::Left) - .row(), - TransformBlock::ExcerptHeader { - id: excerpt_boundary.id, - buffer: excerpt_boundary.buffer, - range: excerpt_boundary.range, - height: if excerpt_boundary.starts_new_buffer { - self.buffer_header_height - } else { - self.excerpt_header_height + if buffer.show_headers() { + blocks_in_edit.extend( + buffer + .excerpt_boundaries_in_range((start_bound, end_bound)) + .map(|excerpt_boundary| { + ( + wrap_snapshot + .make_wrap_point( + Point::new(excerpt_boundary.row, 0), + Bias::Left, + ) + .row(), + TransformBlock::ExcerptHeader { + id: excerpt_boundary.id, + buffer: excerpt_boundary.buffer, + range: excerpt_boundary.range, + height: if excerpt_boundary.starts_new_buffer { + self.buffer_header_height + } else { + self.excerpt_header_height + }, + starts_new_buffer: excerpt_boundary.starts_new_buffer, }, - starts_new_buffer: excerpt_boundary.starts_new_buffer, - }, - ) - }), - ); + ) + }), + ); + } // Place excerpt headers above custom blocks on the same row. blocks_in_edit.sort_unstable_by(|(row_a, block_a), (row_b, block_b)| { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e9e9088806..d10ee972c4 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -18,6 +18,7 @@ mod blink_manager; pub mod display_map; mod editor_settings; mod element; +mod hunk_diff; mod inlay_hint_cache; mod debounced_delay; @@ -71,6 +72,8 @@ use gpui::{ }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; +use hunk_diff::ExpandedHunks; +pub(crate) use hunk_diff::HunkToExpand; use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; pub use inline_completion_provider::*; pub use items::MAX_TAB_TITLE_LEN; @@ -230,6 +233,7 @@ impl InlayId { } } +enum DiffRowHighlight {} enum DocumentHighlightRead {} enum DocumentHighlightWrite {} enum InputComposition {} @@ -325,6 +329,7 @@ pub enum EditorMode { #[derive(Clone, Debug)] pub enum SoftWrap { None, + PreferLine, EditorWidth, Column(u32), } @@ -458,6 +463,7 @@ pub struct Editor { active_inline_completion: Option, show_inline_completions: bool, inlay_hint_cache: InlayHintCache, + expanded_hunks: ExpandedHunks, next_inlay_id: usize, _subscriptions: Vec, pixel_position_of_newest_cursor: Option>, @@ -1410,7 +1416,7 @@ impl Editor { let blink_manager = cx.new_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); let soft_wrap_mode_override = - (mode == EditorMode::SingleLine).then(|| language_settings::SoftWrap::None); + (mode == EditorMode::SingleLine).then(|| language_settings::SoftWrap::PreferLine); let mut project_subscriptions = Vec::new(); if mode == EditorMode::Full { @@ -1499,6 +1505,7 @@ impl Editor { inline_completion_provider: None, active_inline_completion: None, inlay_hint_cache: InlayHintCache::new(inlay_hint_settings), + expanded_hunks: ExpandedHunks::default(), gutter_hovered: false, pixel_position_of_newest_cursor: None, last_bounds: None, @@ -2379,6 +2386,7 @@ impl Editor { } pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + self.clear_expanded_diff_hunks(cx); if self.dismiss_menus_and_popups(cx) { return; } @@ -5000,48 +5008,8 @@ impl Editor { let mut revert_changes = HashMap::default(); self.buffer.update(cx, |multi_buffer, cx| { let multi_buffer_snapshot = multi_buffer.snapshot(cx); - let selected_multi_buffer_rows = selections.iter().map(|selection| { - let head = selection.head(); - let tail = selection.tail(); - let start = tail.to_point(&multi_buffer_snapshot).row; - let end = head.to_point(&multi_buffer_snapshot).row; - if start > end { - end..start - } else { - start..end - } - }); - - let mut processed_buffer_rows = - HashMap::>>::default(); - for selected_multi_buffer_rows in selected_multi_buffer_rows { - let query_rows = - selected_multi_buffer_rows.start..selected_multi_buffer_rows.end + 1; - for hunk in multi_buffer_snapshot.git_diff_hunks_in_range(query_rows.clone()) { - // 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.associated_range.overlaps(&query_rows) - || hunk.associated_range.start == query_rows.end - || hunk.associated_range.end == query_rows.start - } else { - // `selected_multi_buffer_rows` are inclusive (e.g. [2..2] means 2nd row is selected) - // `hunk.associated_range` is exclusive (e.g. [2..3] means 2nd row is selected) - hunk.associated_range.overlaps(&selected_multi_buffer_rows) - || selected_multi_buffer_rows.end == hunk.associated_range.start - }; - if related_to_selection { - if !processed_buffer_rows - .entry(hunk.buffer_id) - .or_default() - .insert(hunk.buffer_range.start..hunk.buffer_range.end) - { - continue; - } - Self::prepare_revert_change(&mut revert_changes, &multi_buffer, &hunk, cx); - } - } + for hunk in hunks_for_selections(&multi_buffer_snapshot, selections) { + Self::prepare_revert_change(&mut revert_changes, &multi_buffer, &hunk, cx); } }); revert_changes @@ -7674,7 +7642,7 @@ impl Editor { ) -> bool { let display_point = initial_point.to_display_point(snapshot); let mut hunks = hunks - .map(|hunk| diff_hunk_to_display(hunk, &snapshot)) + .map(|hunk| diff_hunk_to_display(&hunk, &snapshot)) .filter(|hunk| { if is_wrapped { true @@ -8765,7 +8733,17 @@ impl Editor { auto_scroll: bool, cx: &mut ViewContext, ) { - let mut ranges = ranges.into_iter().peekable(); + let mut fold_ranges = Vec::new(); + let mut buffers_affected = HashMap::default(); + let multi_buffer = self.buffer().read(cx); + for range in ranges { + if let Some((_, buffer, _)) = multi_buffer.excerpt_containing(range.start.clone(), cx) { + buffers_affected.insert(buffer.read(cx).remote_id(), buffer); + }; + fold_ranges.push(range); + } + + let mut ranges = fold_ranges.into_iter().peekable(); if ranges.peek().is_some() { self.display_map.update(cx, |map, cx| map.fold(ranges, cx)); @@ -8773,6 +8751,10 @@ impl Editor { self.request_autoscroll(Autoscroll::fit(), cx); } + for buffer in buffers_affected.into_values() { + self.sync_expanded_diff_hunks(buffer, cx); + } + cx.notify(); if let Some(active_diagnostics) = self.active_diagnostics.take() { @@ -8796,7 +8778,17 @@ impl Editor { auto_scroll: bool, cx: &mut ViewContext, ) { - let mut ranges = ranges.into_iter().peekable(); + let mut unfold_ranges = Vec::new(); + let mut buffers_affected = HashMap::default(); + let multi_buffer = self.buffer().read(cx); + for range in ranges { + if let Some((_, buffer, _)) = multi_buffer.excerpt_containing(range.start.clone(), cx) { + buffers_affected.insert(buffer.read(cx).remote_id(), buffer); + }; + unfold_ranges.push(range); + } + + let mut ranges = unfold_ranges.into_iter().peekable(); if ranges.peek().is_some() { self.display_map .update(cx, |map, cx| map.unfold(ranges, inclusive, cx)); @@ -8804,6 +8796,10 @@ impl Editor { self.request_autoscroll(Autoscroll::fit(), cx); } + for buffer in buffers_affected.into_values() { + self.sync_expanded_diff_hunks(buffer, cx); + } + cx.notify(); } } @@ -8925,6 +8921,7 @@ impl Editor { .unwrap_or_else(|| settings.soft_wrap); match mode { language_settings::SoftWrap::None => SoftWrap::None, + language_settings::SoftWrap::PreferLine => SoftWrap::PreferLine, language_settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, language_settings::SoftWrap::PreferredLineLength => { SoftWrap::Column(settings.preferred_line_length) @@ -8969,8 +8966,10 @@ impl Editor { self.soft_wrap_mode_override.take(); } else { let soft_wrap = match self.soft_wrap_mode(cx) { - SoftWrap::None => language_settings::SoftWrap::EditorWidth, - SoftWrap::EditorWidth | SoftWrap::Column(_) => language_settings::SoftWrap::None, + SoftWrap::None | SoftWrap::PreferLine => language_settings::SoftWrap::EditorWidth, + SoftWrap::EditorWidth | SoftWrap::Column(_) => { + language_settings::SoftWrap::PreferLine + } }; self.soft_wrap_mode_override = Some(soft_wrap); } @@ -9266,13 +9265,19 @@ impl Editor { ) } - // Merges all anchor ranges for all context types ever set, picking the last highlight added in case of a row conflict. - // Rerturns a map of display rows that are highlighted and their corresponding highlight color. - pub fn highlighted_display_rows(&mut self, cx: &mut WindowContext) -> BTreeMap { + /// Merges all anchor ranges for all context types ever set, picking the last highlight added in case of a row conflict. + /// Rerturns a map of display rows that are highlighted and their corresponding highlight color. + /// Allows to ignore certain kinds of highlights. + pub fn highlighted_display_rows( + &mut self, + exclude_highlights: HashSet, + cx: &mut WindowContext, + ) -> BTreeMap { let snapshot = self.snapshot(cx); let mut used_highlight_orders = HashMap::default(); self.highlighted_rows .iter() + .filter(|(type_id, _)| !exclude_highlights.contains(type_id)) .flat_map(|(_, highlighted_rows)| highlighted_rows.iter()) .fold( BTreeMap::::new(), @@ -9663,6 +9668,10 @@ impl Editor { cx.emit(EditorEvent::DiffBaseChanged); cx.notify(); } + multi_buffer::Event::DiffUpdated { buffer } => { + self.sync_expanded_diff_hunks(buffer.clone(), cx); + cx.notify(); + } multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed), multi_buffer::Event::DiagnosticsUpdated => { self.refresh_active_diagnostics(cx); @@ -10102,6 +10111,57 @@ impl Editor { } } +fn hunks_for_selections( + multi_buffer_snapshot: &MultiBufferSnapshot, + selections: &[Selection], +) -> Vec> { + let mut hunks = Vec::with_capacity(selections.len()); + let mut processed_buffer_rows: HashMap>> = + HashMap::default(); + let display_rows_for_selections = selections.iter().map(|selection| { + let head = selection.head(); + let tail = selection.tail(); + let start = tail.to_point(&multi_buffer_snapshot).row; + let end = head.to_point(&multi_buffer_snapshot).row; + if start > end { + end..start + } else { + start..end + } + }); + + for selected_multi_buffer_rows in display_rows_for_selections { + let query_rows = selected_multi_buffer_rows.start..selected_multi_buffer_rows.end + 1; + for hunk in multi_buffer_snapshot.git_diff_hunks_in_range(query_rows.clone()) { + // 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.associated_range.overlaps(&query_rows) + || hunk.associated_range.start == query_rows.end + || hunk.associated_range.end == query_rows.start + } else { + // `selected_multi_buffer_rows` are inclusive (e.g. [2..2] means 2nd row is selected) + // `hunk.associated_range` is exclusive (e.g. [2..3] means 2nd row is selected) + hunk.associated_range.overlaps(&selected_multi_buffer_rows) + || selected_multi_buffer_rows.end == hunk.associated_range.start + }; + 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>( @@ -10300,8 +10360,8 @@ impl EditorSnapshot { Some(GitGutterSetting::TrackedFiles) ); let gutter_settings = EditorSettings::get_global(cx).gutter; - - let line_gutter_width = if gutter_settings.line_numbers { + let gutter_lines_enabled = gutter_settings.line_numbers; + let line_gutter_width = if gutter_lines_enabled { // Avoid flicker-like gutter resizes when the line number gains another digit and only resize the gutter on files with N*10^5 lines. let min_width_for_number_on_gutter = em_width * 4.0; max_line_number_width.max(min_width_for_number_on_gutter) @@ -10316,19 +10376,19 @@ impl EditorSnapshot { let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO); left_padding += if gutter_settings.code_actions { em_width * 3.0 - } else if show_git_gutter && gutter_settings.line_numbers { + } else if show_git_gutter && gutter_lines_enabled { em_width * 2.0 - } else if show_git_gutter || gutter_settings.line_numbers { + } else if show_git_gutter || gutter_lines_enabled { em_width } else { px(0.) }; - let right_padding = if gutter_settings.folds && gutter_settings.line_numbers { + let right_padding = if gutter_settings.folds && gutter_lines_enabled { em_width * 4.0 } else if gutter_settings.folds { em_width * 3.0 - } else if gutter_settings.line_numbers { + } else if gutter_lines_enabled { em_width } else { px(0.) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 05d7aa243e..d10870ee67 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -2,8 +2,9 @@ use super::*; use crate::{ scroll::scroll_amount::ScrollAmount, test::{ - assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext, - editor_test_context::EditorTestContext, select_ranges, + assert_text_with_selections, build_editor, editor_hunks, + editor_lsp_test_context::EditorLspTestContext, editor_test_context::EditorTestContext, + expanded_hunks, expanded_hunks_background_highlights, select_ranges, }, JoinLines, }; @@ -9327,6 +9328,1761 @@ async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) { .unwrap(); } +#[gpui::test] +async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let diff_base = r#" + use some::mod; + + const A: u32 = 42; + + fn main() { + println!("hello"); + + println!("world"); + } + "# + .unindent(); + + cx.set_state( + &r#" + use some::modified; + + ˇ + fn main() { + println!("hello there"); + + println!("around the"); + println!("world"); + } + "# + .unindent(), + ); + + cx.set_diff_base(Some(&diff_base)); + executor.run_until_parked(); + let unexpanded_hunks = vec![ + ( + "use some::mod;\n".to_string(), + DiffHunkStatus::Modified, + 0..1, + ), + ( + "const A: u32 = 42;\n".to_string(), + DiffHunkStatus::Removed, + 2..2, + ), + ( + " println!(\"hello\");\n".to_string(), + DiffHunkStatus::Modified, + 4..5, + ), + ("".to_string(), DiffHunkStatus::Added, 6..7), + ]; + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + assert_eq!(all_hunks, unexpanded_hunks); + }); + + cx.update_editor(|editor, cx| { + for _ in 0..4 { + editor.go_to_hunk(&GoToHunk, cx); + editor.toggle_hunk_diff(&ToggleHunkDiff, cx); + } + }); + executor.run_until_parked(); + cx.assert_editor_state( + &r#" + use some::modified; + + ˇ + fn main() { + println!("hello there"); + + println!("around the"); + println!("world"); + } + "# + .unindent(), + ); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + vec![1..2, 7..8, 9..10], + "After expanding, all git additions should be highlighted for Modified (split into added and removed) and Added hunks" + ); + assert_eq!( + all_hunks, + vec![ + ("use some::mod;\n".to_string(), DiffHunkStatus::Modified, 1..2), + ("const A: u32 = 42;\n".to_string(), DiffHunkStatus::Removed, 4..4), + (" println!(\"hello\");\n".to_string(), DiffHunkStatus::Modified, 7..8), + ("".to_string(), DiffHunkStatus::Added, 9..10), + ], + "After expanding, all hunks' display rows should have shifted by the amount of deleted lines added \ + (from modified and removed hunks)" + ); + assert_eq!( + all_hunks, all_expanded_hunks, + "Editor hunks should not change and all be expanded" + ); + }); + + cx.update_editor(|editor, cx| { + editor.cancel(&Cancel, cx); + + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + Vec::new(), + "After cancelling in editor, no git highlights should be left" + ); + assert_eq!( + all_expanded_hunks, + Vec::new(), + "After cancelling in editor, no hunks should be expanded" + ); + assert_eq!( + all_hunks, unexpanded_hunks, + "After cancelling in editor, regular hunks' coordinates should get back to normal" + ); + }); +} + +#[gpui::test] +async fn test_toggled_diff_base_change( + 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"); + } + "# + .unindent(); + + cx.set_state( + &r#" + use some::mod2; + + const A: u32 = 42; + const C: u32 = 42; + + fn main(ˇ) { + //println!("hello"); + + println!("world"); + // + // + } + "# + .unindent(), + ); + + cx.set_diff_base(Some(&diff_base)); + executor.run_until_parked(); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + assert_eq!( + all_hunks, + vec![ + ( + "use some::mod1;\n".to_string(), + DiffHunkStatus::Removed, + 0..0 + ), + ( + "const B: u32 = 42;\n".to_string(), + DiffHunkStatus::Removed, + 3..3 + ), + ( + "fn main(ˇ) {\n println!(\"hello\");\n".to_string(), + DiffHunkStatus::Modified, + 5..7 + ), + ("".to_string(), DiffHunkStatus::Added, 9..11), + ] + ); + }); + + cx.update_editor(|editor, cx| { + editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx); + }); + executor.run_until_parked(); + cx.assert_editor_state( + &r#" + use some::mod2; + + const A: u32 = 42; + const C: u32 = 42; + + fn main(ˇ) { + //println!("hello"); + + println!("world"); + // + // + } + "# + .unindent(), + ); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + vec![9..11, 13..15], + "After expanding, all git additions should be highlighted for Modified (split into added and removed) and Added hunks" + ); + assert_eq!( + all_hunks, + vec![ + ("use some::mod1;\n".to_string(), DiffHunkStatus::Removed, 1..1), + ("const B: u32 = 42;\n".to_string(), DiffHunkStatus::Removed, 5..5), + ("fn main(ˇ) {\n println!(\"hello\");\n".to_string(), DiffHunkStatus::Modified, 9..11), + ("".to_string(), DiffHunkStatus::Added, 13..15), + ], + "After expanding, all hunks' display rows should have shifted by the amount of deleted lines added \ + (from modified and removed hunks)" + ); + assert_eq!( + all_hunks, all_expanded_hunks, + "Editor hunks should not change and all be expanded" + ); + }); + + cx.set_diff_base(Some("new diff base!")); + executor.run_until_parked(); + + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + Vec::new(), + "After diff base is changed, old git highlights should be removed" + ); + assert_eq!( + all_expanded_hunks, + Vec::new(), + "After diff base is changed, old git hunk expansions should be removed" + ); + assert_eq!( + all_hunks, + vec![( + "new diff base!".to_string(), + DiffHunkStatus::Modified, + 0..snapshot.display_snapshot.max_point().row() + )], + "After diff base is changed, hunks should update" + ); + }); +} + +#[gpui::test] +async fn test_fold_unfold_diff(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(Some(&diff_base)); + executor.run_until_parked(); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + assert_eq!( + all_hunks, + vec![ + ( + "use some::mod1;\n".to_string(), + DiffHunkStatus::Removed, + 0..0 + ), + ( + "const B: u32 = 42;\n".to_string(), + DiffHunkStatus::Removed, + 3..3 + ), + ( + "fn main(ˇ) {\n println!(\"hello\");\n".to_string(), + DiffHunkStatus::Modified, + 5..7 + ), + ("".to_string(), DiffHunkStatus::Added, 9..11), + ("".to_string(), DiffHunkStatus::Added, 15..16), + ( + "fn another2() {\n".to_string(), + DiffHunkStatus::Removed, + 18..18 + ), + ] + ); + }); + + cx.update_editor(|editor, cx| { + editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx); + }); + executor.run_until_parked(); + cx.assert_editor_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.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + vec![9..11, 13..15, 19..20] + ); + assert_eq!( + all_hunks, + vec![ + ( + "use some::mod1;\n".to_string(), + DiffHunkStatus::Removed, + 1..1 + ), + ( + "const B: u32 = 42;\n".to_string(), + DiffHunkStatus::Removed, + 5..5 + ), + ( + "fn main(ˇ) {\n println!(\"hello\");\n".to_string(), + DiffHunkStatus::Modified, + 9..11 + ), + ("".to_string(), DiffHunkStatus::Added, 13..15), + ("".to_string(), DiffHunkStatus::Added, 19..20), + ( + "fn another2() {\n".to_string(), + DiffHunkStatus::Removed, + 23..23 + ), + ], + ); + assert_eq!(all_hunks, all_expanded_hunks); + }); + + cx.update_editor(|editor, cx| editor.fold_selected_ranges(&FoldSelectedRanges, cx)); + cx.executor().run_until_parked(); + cx.assert_editor_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.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + vec![5..6], + "Only one hunk is left not folded, its highlight should be visible" + ); + assert_eq!( + all_hunks, + vec![ + ( + "use some::mod1;\n".to_string(), + DiffHunkStatus::Removed, + 0..0 + ), + ( + "const B: u32 = 42;\n".to_string(), + DiffHunkStatus::Removed, + 0..0 + ), + ( + "fn main(ˇ) {\n println!(\"hello\");\n".to_string(), + DiffHunkStatus::Modified, + 0..0 + ), + ("".to_string(), DiffHunkStatus::Added, 0..1), + ("".to_string(), DiffHunkStatus::Added, 5..6), + ( + "fn another2() {\n".to_string(), + DiffHunkStatus::Removed, + 9..9 + ), + ], + "Hunk list should still return shifted folded hunks" + ); + assert_eq!( + all_expanded_hunks, + vec![ + ("".to_string(), DiffHunkStatus::Added, 5..6), + ( + "fn another2() {\n".to_string(), + DiffHunkStatus::Removed, + 9..9 + ), + ], + "Only non-folded hunks should be left expanded" + ); + }); + + cx.update_editor(|editor, cx| { + editor.select_all(&SelectAll, cx); + editor.unfold_lines(&UnfoldLines, cx); + }); + cx.executor().run_until_parked(); + cx.assert_editor_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.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + vec![9..11, 13..15, 19..20], + "After unfolding, all hunk diffs should be visible again" + ); + assert_eq!( + all_hunks, + vec![ + ( + "use some::mod1;\n".to_string(), + DiffHunkStatus::Removed, + 1..1 + ), + ( + "const B: u32 = 42;\n".to_string(), + DiffHunkStatus::Removed, + 5..5 + ), + ( + "fn main(ˇ) {\n println!(\"hello\");\n".to_string(), + DiffHunkStatus::Modified, + 9..11 + ), + ("".to_string(), DiffHunkStatus::Added, 13..15), + ("".to_string(), DiffHunkStatus::Added, 19..20), + ( + "fn another2() {\n".to_string(), + DiffHunkStatus::Removed, + 23..23 + ), + ], + ); + assert_eq!(all_hunks, all_expanded_hunks); + }); +} + +#[gpui::test] +async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let cols = 4; + let rows = 10; + let sample_text_1 = sample_text(rows, cols, 'a'); + assert_eq!( + sample_text_1, + "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj" + ); + let modified_sample_text_1 = "aaaa\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"; + let sample_text_2 = sample_text(rows, cols, 'l'); + assert_eq!( + sample_text_2, + "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu" + ); + let modified_sample_text_2 = "llll\nmmmm\n1n1n1n1n1\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"; + let sample_text_3 = sample_text(rows, cols, 'v'); + assert_eq!( + sample_text_3, + "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}" + ); + let modified_sample_text_3 = + "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n@@@@\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"; + let buffer_1 = cx.new_model(|cx| { + let mut buffer = Buffer::local(modified_sample_text_1.to_string(), cx); + buffer.set_diff_base(Some(sample_text_1.clone()), cx); + buffer + }); + let buffer_2 = cx.new_model(|cx| { + let mut buffer = Buffer::local(modified_sample_text_2.to_string(), cx); + buffer.set_diff_base(Some(sample_text_2.clone()), cx); + buffer + }); + let buffer_3 = cx.new_model(|cx| { + let mut buffer = Buffer::local(modified_sample_text_3.to_string(), cx); + buffer.set_diff_base(Some(sample_text_3.clone()), cx); + buffer + }); + + let multi_buffer = cx.new_model(|cx| { + let mut multibuffer = MultiBuffer::new(0, ReadWrite); + multibuffer.push_excerpts( + buffer_1.clone(), + [ + ExcerptRange { + context: Point::new(0, 0)..Point::new(3, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(5, 0)..Point::new(7, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(9, 0)..Point::new(10, 4), + primary: None, + }, + ], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ + ExcerptRange { + context: Point::new(0, 0)..Point::new(3, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(5, 0)..Point::new(7, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(9, 0)..Point::new(10, 4), + primary: None, + }, + ], + cx, + ); + multibuffer.push_excerpts( + buffer_3.clone(), + [ + ExcerptRange { + context: Point::new(0, 0)..Point::new(3, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(5, 0)..Point::new(7, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(9, 0)..Point::new(10, 4), + primary: None, + }, + ], + cx, + ); + multibuffer + }); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/a", + json!({ + "main.rs": modified_sample_text_1, + "other.rs": modified_sample_text_2, + "lib.rs": modified_sample_text_3, + }), + ) + .await; + + let project = Project::test(fs, ["/a".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + let multi_buffer_editor = + cx.new_view(|cx| Editor::new(EditorMode::Full, multi_buffer, Some(project.clone()), cx)); + cx.executor().run_until_parked(); + + let expected_all_hunks = vec![ + ("bbbb\n".to_string(), DiffHunkStatus::Removed, 3..3), + ("nnnn\n".to_string(), DiffHunkStatus::Modified, 16..17), + ("".to_string(), DiffHunkStatus::Added, 31..32), + ]; + let expected_all_hunks_shifted = vec![ + ("bbbb\n".to_string(), DiffHunkStatus::Removed, 4..4), + ("nnnn\n".to_string(), DiffHunkStatus::Modified, 18..19), + ("".to_string(), DiffHunkStatus::Added, 33..34), + ]; + + multi_buffer_editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + Vec::new(), + ); + assert_eq!(all_hunks, expected_all_hunks); + assert_eq!(all_expanded_hunks, Vec::new()); + }); + + multi_buffer_editor.update(cx, |editor, cx| { + editor.select_all(&SelectAll, cx); + editor.toggle_hunk_diff(&ToggleHunkDiff, cx); + }); + cx.executor().run_until_parked(); + multi_buffer_editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + vec![18..19, 33..34], + ); + assert_eq!(all_hunks, expected_all_hunks_shifted); + assert_eq!(all_hunks, all_expanded_hunks); + }); + + multi_buffer_editor.update(cx, |editor, cx| { + editor.toggle_hunk_diff(&ToggleHunkDiff, cx); + }); + cx.executor().run_until_parked(); + multi_buffer_editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + Vec::new(), + ); + assert_eq!(all_hunks, expected_all_hunks); + assert_eq!(all_expanded_hunks, Vec::new()); + }); + + multi_buffer_editor.update(cx, |editor, cx| { + editor.toggle_hunk_diff(&ToggleHunkDiff, cx); + }); + cx.executor().run_until_parked(); + multi_buffer_editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + vec![18..19, 33..34], + ); + assert_eq!(all_hunks, expected_all_hunks_shifted); + assert_eq!(all_hunks, all_expanded_hunks); + }); + + multi_buffer_editor.update(cx, |editor, cx| { + editor.toggle_hunk_diff(&ToggleHunkDiff, cx); + }); + cx.executor().run_until_parked(); + multi_buffer_editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + Vec::new(), + ); + assert_eq!(all_hunks, expected_all_hunks); + assert_eq!(all_expanded_hunks, Vec::new()); + }); +} + +#[gpui::test] +async fn test_edits_around_toggled_additions( + 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; + + fn main() { + println!("hello"); + + println!("world"); + } + "# + .unindent(); + executor.run_until_parked(); + cx.set_state( + &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"); + } + "# + .unindent(), + ); + + cx.set_diff_base(Some(&diff_base)); + executor.run_until_parked(); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + assert_eq!( + all_hunks, + vec![("".to_string(), DiffHunkStatus::Added, 4..7)] + ); + }); + cx.update_editor(|editor, cx| { + editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx); + }); + executor.run_until_parked(); + cx.assert_editor_state( + &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"); + } + "# + .unindent(), + ); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + all_hunks, + vec![("".to_string(), DiffHunkStatus::Added, 4..7)] + ); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + vec![4..7] + ); + assert_eq!(all_hunks, all_expanded_hunks); + }); + + cx.update_editor(|editor, cx| editor.handle_input("const D: u32 = 42;\n", cx)); + executor.run_until_parked(); + cx.assert_editor_state( + &r#" + use some::mod1; + use some::mod2; + + const A: u32 = 42; + const B: u32 = 42; + const C: u32 = 42; + const D: u32 = 42; + ˇ + + fn main() { + println!("hello"); + + println!("world"); + } + "# + .unindent(), + ); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + all_hunks, + vec![("".to_string(), DiffHunkStatus::Added, 4..8)] + ); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + vec![4..8], + "Edited hunk should have one more line added" + ); + assert_eq!( + all_hunks, all_expanded_hunks, + "Expanded hunk should also grow with the addition" + ); + }); + + cx.update_editor(|editor, cx| editor.handle_input("const E: u32 = 42;\n", cx)); + executor.run_until_parked(); + cx.assert_editor_state( + &r#" + use some::mod1; + use some::mod2; + + const A: u32 = 42; + const B: u32 = 42; + const C: u32 = 42; + const D: u32 = 42; + const E: u32 = 42; + ˇ + + fn main() { + println!("hello"); + + println!("world"); + } + "# + .unindent(), + ); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + all_hunks, + vec![("".to_string(), DiffHunkStatus::Added, 4..9)] + ); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + vec![4..9], + "Edited hunk should have one more line added" + ); + assert_eq!(all_hunks, all_expanded_hunks); + }); + + cx.update_editor(|editor, cx| { + editor.move_up(&MoveUp, cx); + editor.delete_line(&DeleteLine, cx); + }); + executor.run_until_parked(); + cx.assert_editor_state( + &r#" + use some::mod1; + use some::mod2; + + const A: u32 = 42; + const B: u32 = 42; + const C: u32 = 42; + const D: u32 = 42; + ˇ + + fn main() { + println!("hello"); + + println!("world"); + } + "# + .unindent(), + ); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + all_hunks, + vec![("".to_string(), DiffHunkStatus::Added, 4..8)] + ); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + vec![4..8], + "Deleting a line should shrint the hunk" + ); + assert_eq!( + all_hunks, all_expanded_hunks, + "Expanded hunk should also shrink with the addition" + ); + }); + + cx.update_editor(|editor, cx| { + editor.move_up(&MoveUp, cx); + editor.delete_line(&DeleteLine, cx); + editor.move_up(&MoveUp, cx); + editor.delete_line(&DeleteLine, cx); + editor.move_up(&MoveUp, cx); + editor.delete_line(&DeleteLine, cx); + }); + executor.run_until_parked(); + cx.assert_editor_state( + &r#" + use some::mod1; + use some::mod2; + + const A: u32 = 42; + ˇ + + fn main() { + println!("hello"); + + println!("world"); + } + "# + .unindent(), + ); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + all_hunks, + vec![("".to_string(), DiffHunkStatus::Added, 5..6)] + ); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + vec![5..6] + ); + assert_eq!(all_hunks, all_expanded_hunks); + }); + + cx.update_editor(|editor, cx| { + editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, cx); + editor.delete_line(&DeleteLine, cx); + }); + executor.run_until_parked(); + cx.assert_editor_state( + &r#" + ˇ + + fn main() { + println!("hello"); + + println!("world"); + } + "# + .unindent(), + ); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + all_hunks, + vec![ + ( + "use some::mod1;\nuse some::mod2;\n".to_string(), + DiffHunkStatus::Removed, + 0..0 + ), + ( + "const A: u32 = 42;\n".to_string(), + DiffHunkStatus::Removed, + 2..2 + ) + ] + ); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + Vec::new(), + "Should close all stale expanded addition hunks" + ); + assert_eq!( + all_expanded_hunks, + vec![( + "const A: u32 = 42;\n".to_string(), + DiffHunkStatus::Removed, + 2..2 + )], + "Should open hunks that were adjacent to the stale addition one" + ); + }); +} + +#[gpui::test] +async fn test_edits_around_toggled_deletions( + 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"); + } + "# + .unindent(); + executor.run_until_parked(); + cx.set_state( + &r#" + use some::mod1; + use some::mod2; + + ˇconst B: u32 = 42; + const C: u32 = 42; + + + fn main() { + println!("hello"); + + println!("world"); + } + "# + .unindent(), + ); + + cx.set_diff_base(Some(&diff_base)); + executor.run_until_parked(); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + assert_eq!( + all_hunks, + vec![( + "const A: u32 = 42;\n".to_string(), + DiffHunkStatus::Removed, + 3..3 + )] + ); + }); + cx.update_editor(|editor, cx| { + editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx); + }); + executor.run_until_parked(); + cx.assert_editor_state( + &r#" + use some::mod1; + use some::mod2; + + ˇconst B: u32 = 42; + const C: u32 = 42; + + + fn main() { + println!("hello"); + + println!("world"); + } + "# + .unindent(), + ); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + Vec::new() + ); + assert_eq!( + all_hunks, + vec![( + "const A: u32 = 42;\n".to_string(), + DiffHunkStatus::Removed, + 4..4 + )] + ); + assert_eq!(all_hunks, all_expanded_hunks); + }); + + cx.update_editor(|editor, cx| { + editor.delete_line(&DeleteLine, cx); + }); + executor.run_until_parked(); + cx.assert_editor_state( + &r#" + use some::mod1; + use some::mod2; + + ˇconst C: u32 = 42; + + + fn main() { + println!("hello"); + + println!("world"); + } + "# + .unindent(), + ); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + Vec::new(), + "Deleted hunks do not highlight current editor's background" + ); + assert_eq!( + all_hunks, + vec![( + "const A: u32 = 42;\nconst B: u32 = 42;\n".to_string(), + DiffHunkStatus::Removed, + 5..5 + )] + ); + assert_eq!(all_hunks, all_expanded_hunks); + }); + + cx.update_editor(|editor, cx| { + editor.delete_line(&DeleteLine, cx); + }); + executor.run_until_parked(); + cx.assert_editor_state( + &r#" + use some::mod1; + use some::mod2; + + ˇ + + fn main() { + println!("hello"); + + println!("world"); + } + "# + .unindent(), + ); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + Vec::new() + ); + assert_eq!( + all_hunks, + vec![( + "const A: u32 = 42;\nconst B: u32 = 42;\nconst C: u32 = 42;\n".to_string(), + DiffHunkStatus::Removed, + 6..6 + )] + ); + assert_eq!(all_hunks, all_expanded_hunks); + }); + + cx.update_editor(|editor, cx| { + editor.handle_input("replacement", cx); + }); + executor.run_until_parked(); + cx.assert_editor_state( + &r#" + use some::mod1; + use some::mod2; + + replacementˇ + + fn main() { + println!("hello"); + + println!("world"); + } + "# + .unindent(), + ); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + all_hunks, + vec![( + "const A: u32 = 42;\nconst B: u32 = 42;\nconst C: u32 = 42;\n\n".to_string(), + DiffHunkStatus::Modified, + 7..8 + )] + ); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + vec![7..8], + "Modified expanded hunks should display additions and highlight their background" + ); + assert_eq!(all_hunks, all_expanded_hunks); + }); +} + +#[gpui::test] +async fn test_edits_around_toggled_modifications( + 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; + const D: u32 = 42; + + + fn main() { + println!("hello"); + + println!("world"); + }"# + .unindent(); + executor.run_until_parked(); + cx.set_state( + &r#" + use some::mod1; + use some::mod2; + + const A: u32 = 42; + const B: u32 = 42; + const C: u32 = 43ˇ + const D: u32 = 42; + + + fn main() { + println!("hello"); + + println!("world"); + }"# + .unindent(), + ); + + cx.set_diff_base(Some(&diff_base)); + executor.run_until_parked(); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + assert_eq!( + all_hunks, + vec![( + "const C: u32 = 42;\n".to_string(), + DiffHunkStatus::Modified, + 5..6 + )] + ); + }); + cx.update_editor(|editor, cx| { + editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx); + }); + executor.run_until_parked(); + cx.assert_editor_state( + &r#" + use some::mod1; + use some::mod2; + + const A: u32 = 42; + const B: u32 = 42; + const C: u32 = 43ˇ + const D: u32 = 42; + + + fn main() { + println!("hello"); + + println!("world"); + }"# + .unindent(), + ); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + vec![6..7], + ); + assert_eq!( + all_hunks, + vec![( + "const C: u32 = 42;\n".to_string(), + DiffHunkStatus::Modified, + 6..7 + )] + ); + assert_eq!(all_hunks, all_expanded_hunks); + }); + + cx.update_editor(|editor, cx| { + editor.handle_input("\nnew_line\n", cx); + }); + executor.run_until_parked(); + cx.assert_editor_state( + &r#" + use some::mod1; + use some::mod2; + + const A: u32 = 42; + const B: u32 = 42; + const C: u32 = 43 + new_line + ˇ + const D: u32 = 42; + + + fn main() { + println!("hello"); + + println!("world"); + }"# + .unindent(), + ); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + vec![6..9], + "Modified hunk should grow highlighted lines on more text additions" + ); + assert_eq!( + all_hunks, + vec![( + "const C: u32 = 42;\n".to_string(), + DiffHunkStatus::Modified, + 6..9 + )] + ); + assert_eq!(all_hunks, all_expanded_hunks); + }); + + cx.update_editor(|editor, cx| { + editor.move_up(&MoveUp, cx); + editor.move_up(&MoveUp, cx); + editor.move_up(&MoveUp, cx); + editor.delete_line(&DeleteLine, cx); + }); + executor.run_until_parked(); + cx.assert_editor_state( + &r#" + use some::mod1; + use some::mod2; + + const A: u32 = 42; + ˇconst C: u32 = 43 + new_line + + const D: u32 = 42; + + + fn main() { + println!("hello"); + + println!("world"); + }"# + .unindent(), + ); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + vec![6..9], + "Modified hunk should grow deleted lines on text deletions above" + ); + assert_eq!( + all_hunks, + vec![( + "const B: u32 = 42;\nconst C: u32 = 42;\n".to_string(), + DiffHunkStatus::Modified, + 6..9 + )] + ); + assert_eq!(all_hunks, all_expanded_hunks); + }); + + cx.update_editor(|editor, cx| { + editor.move_up(&MoveUp, cx); + editor.handle_input("v", cx); + }); + executor.run_until_parked(); + cx.assert_editor_state( + &r#" + use some::mod1; + use some::mod2; + + vˇconst A: u32 = 42; + const C: u32 = 43 + new_line + + const D: u32 = 42; + + + fn main() { + println!("hello"); + + println!("world"); + }"# + .unindent(), + ); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + vec![6..10], + "Modified hunk should grow deleted lines on text modifications above" + ); + assert_eq!( + all_hunks, + vec![( + "const A: u32 = 42;\nconst B: u32 = 42;\nconst C: u32 = 42;\n".to_string(), + DiffHunkStatus::Modified, + 6..10 + )] + ); + assert_eq!(all_hunks, all_expanded_hunks); + }); + + cx.update_editor(|editor, cx| { + editor.move_down(&MoveDown, cx); + editor.move_down(&MoveDown, cx); + editor.delete_line(&DeleteLine, cx) + }); + executor.run_until_parked(); + cx.assert_editor_state( + &r#" + use some::mod1; + use some::mod2; + + vconst A: u32 = 42; + const C: u32 = 43 + ˇ + const D: u32 = 42; + + + fn main() { + println!("hello"); + + println!("world"); + }"# + .unindent(), + ); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + vec![6..9], + "Modified hunk should grow shrink lines on modification lines removal" + ); + assert_eq!( + all_hunks, + vec![( + "const A: u32 = 42;\nconst B: u32 = 42;\nconst C: u32 = 42;\n".to_string(), + DiffHunkStatus::Modified, + 6..9 + )] + ); + assert_eq!(all_hunks, all_expanded_hunks); + }); + + cx.update_editor(|editor, cx| { + editor.move_up(&MoveUp, cx); + editor.move_up(&MoveUp, cx); + editor.select_down_by_lines(&SelectDownByLines { lines: 4 }, cx); + editor.delete_line(&DeleteLine, cx) + }); + executor.run_until_parked(); + cx.assert_editor_state( + &r#" + use some::mod1; + use some::mod2; + + ˇ + + fn main() { + println!("hello"); + + println!("world"); + }"# + .unindent(), + ); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + Vec::new(), + "Modified hunk should turn into a removed one on all modified lines removal" + ); + assert_eq!( + all_hunks, + vec![( + "const A: u32 = 42;\nconst B: u32 = 42;\nconst C: u32 = 42;\nconst D: u32 = 42;\n" + .to_string(), + DiffHunkStatus::Removed, + 7..7 + )] + ); + assert_eq!(all_hunks, all_expanded_hunks); + }); +} + +#[gpui::test] +async fn test_multiple_expanded_hunks_merge( + 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; + const D: u32 = 42; + + + fn main() { + println!("hello"); + + println!("world"); + }"# + .unindent(); + executor.run_until_parked(); + cx.set_state( + &r#" + use some::mod1; + use some::mod2; + + const A: u32 = 42; + const B: u32 = 42; + const C: u32 = 43ˇ + const D: u32 = 42; + + + fn main() { + println!("hello"); + + println!("world"); + }"# + .unindent(), + ); + + cx.set_diff_base(Some(&diff_base)); + executor.run_until_parked(); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + assert_eq!( + all_hunks, + vec![( + "const C: u32 = 42;\n".to_string(), + DiffHunkStatus::Modified, + 5..6 + )] + ); + }); + cx.update_editor(|editor, cx| { + editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx); + }); + executor.run_until_parked(); + cx.assert_editor_state( + &r#" + use some::mod1; + use some::mod2; + + const A: u32 = 42; + const B: u32 = 42; + const C: u32 = 43ˇ + const D: u32 = 42; + + + fn main() { + println!("hello"); + + println!("world"); + }"# + .unindent(), + ); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let all_hunks = editor_hunks(editor, &snapshot, cx); + let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx); + assert_eq!( + expanded_hunks_background_highlights(editor, &snapshot), + vec![6..7], + ); + assert_eq!( + all_hunks, + vec![( + "const C: u32 = 42;\n".to_string(), + DiffHunkStatus::Modified, + 6..7 + )] + ); + assert_eq!(all_hunks, all_expanded_hunks); + }); + + cx.update_editor(|editor, cx| { + editor.handle_input("\nnew_line\n", cx); + }); + executor.run_until_parked(); + cx.assert_editor_state( + &r#" + use some::mod1; + use some::mod2; + + const A: u32 = 42; + const B: u32 = 42; + const C: u32 = 43 + new_line + ˇ + const D: u32 = 42; + + + fn main() { + println!("hello"); + + println!("world"); + }"# + .unindent(), + ); +} + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(row as u32, column as u32); point..point diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 8a79239231..15f2be0e8b 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -14,12 +14,12 @@ use crate::{ scroll::scroll_amount::ScrollAmount, CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, GutterDimensions, HalfPageDown, - HalfPageUp, HoveredCursor, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, - SelectPhase, Selection, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN, + HalfPageUp, HoveredCursor, HunkToExpand, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, + Point, SelectPhase, Selection, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN, }; use anyhow::Result; use client::ParticipantIndex; -use collections::{BTreeMap, HashMap}; +use collections::{BTreeMap, HashMap, HashSet}; use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid}; use gpui::{ anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg, @@ -312,6 +312,8 @@ impl EditorElement { register_action(view, cx, Editor::open_permalink_to_line); 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, action, cx| { if let Some(task) = editor.format(action, cx) { task.detach_and_log_err(cx); @@ -411,6 +413,7 @@ impl EditorElement { fn mouse_left_down( editor: &mut Editor, event: &MouseDownEvent, + hovered_hunk: Option<&HunkToExpand>, position_map: &PositionMap, text_hitbox: &Hitbox, gutter_hitbox: &Hitbox, @@ -425,6 +428,8 @@ impl EditorElement { if gutter_hitbox.is_hovered(cx) { click_count = 3; // Simulate triple-click when clicking the gutter to select lines + } else if let Some(hovered_hunk) = hovered_hunk { + editor.expand_diff_hunk(None, hovered_hunk, cx); } else if !text_hitbox.is_hovered(cx) { return; } @@ -1162,13 +1167,16 @@ impl EditorElement { indicators } - //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 + // 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_git_gutters( &self, + line_height: Pixels, + gutter_hitbox: &Hitbox, display_rows: Range, snapshot: &EditorSnapshot, - ) -> Vec { + cx: &mut WindowContext, + ) -> Vec<(DisplayDiffHunk, Option)> { let buffer_snapshot = &snapshot.buffer_snapshot; let buffer_start_row = DisplayPoint::new(display_rows.start, 0) @@ -1178,10 +1186,55 @@ impl EditorElement { .to_point(snapshot) .row; + let expanded_hunk_display_rows = self.editor.update(cx, |editor, _| { + editor + .expanded_hunks + .hunks(false) + .map(|expanded_hunk| { + let start_row = expanded_hunk + .hunk_range + .start + .to_display_point(snapshot) + .row(); + let end_row = expanded_hunk + .hunk_range + .end + .to_display_point(snapshot) + .row(); + (start_row, end_row) + }) + .collect::>() + }); + buffer_snapshot .git_diff_hunks_in_range(buffer_start_row..buffer_end_row) - .map(|hunk| diff_hunk_to_display(hunk, snapshot)) + .map(|hunk| diff_hunk_to_display(&hunk, snapshot)) .dedup() + .map(|hunk| { + let hitbox = if let DisplayDiffHunk::Unfolded { + display_row_range, .. + } = &hunk + { + let was_expanded = expanded_hunk_display_rows + .get(&display_row_range.start) + .map(|expanded_end_row| expanded_end_row == &display_row_range.end) + .unwrap_or(false); + if was_expanded { + None + } else { + let hunk_bounds = Self::diff_hunk_bounds( + &snapshot, + line_height, + gutter_hitbox.bounds, + &hunk, + ); + Some(cx.insert_hitbox(hunk_bounds, true)) + } + } else { + None + }; + (hunk, hitbox) + }) .collect() } @@ -2187,39 +2240,30 @@ impl EditorElement { cx.paint_quad(fill(Bounds { origin, size }, color)); }; - let mut last_row = None; - let mut highlight_row_start = 0u32; - let mut highlight_row_end = 0u32; - for (&row, &color) in &layout.highlighted_rows { - let paint = last_row.map_or(false, |(last_row, last_color)| { - last_color != color || last_row + 1 < row - }); - - if paint { - let paint_range_is_unfinished = highlight_row_end == 0; - if paint_range_is_unfinished { - highlight_row_end = row; - last_row = None; + let mut current_paint: Option<(Hsla, Range)> = None; + for (&new_row, &new_color) in &layout.highlighted_rows { + match &mut current_paint { + Some((current_color, current_range)) => { + let current_color = *current_color; + let new_range_started = + current_color != new_color || current_range.end + 1 != new_row; + if new_range_started { + paint_highlight( + current_range.start, + current_range.end, + current_color, + ); + current_paint = Some((new_color, new_row..new_row)); + continue; + } else { + current_range.end += 1; + } } - paint_highlight(highlight_row_start, highlight_row_end, color); - highlight_row_start = 0; - highlight_row_end = 0; - if !paint_range_is_unfinished { - highlight_row_start = row; - last_row = Some((row, color)); - } - } else { - if last_row.is_none() { - highlight_row_start = row; - } else { - highlight_row_end = row; - } - last_row = Some((row, color)); - } + None => current_paint = Some((new_color, new_row..new_row)), + }; } - if let Some((row, hsla)) = last_row { - highlight_row_end = row; - paint_highlight(highlight_row_start, highlight_row_end, hsla); + if let Some((color, range)) = current_paint { + paint_highlight(range.start, range.end, color); } let scroll_left = @@ -2265,14 +2309,18 @@ impl EditorElement { let scroll_top = scroll_position.y * line_height; cx.set_cursor_style(CursorStyle::Arrow, &layout.gutter_hitbox); + for (_, hunk_hitbox) in &layout.display_hunks { + if let Some(hunk_hitbox) = hunk_hitbox { + cx.set_cursor_style(CursorStyle::PointingHand, hunk_hitbox); + } + } let show_git_gutter = matches!( ProjectSettings::get_global(cx).git.git_gutter, Some(GitGutterSetting::TrackedFiles) ); - if show_git_gutter { - Self::paint_diff_hunks(layout, cx); + Self::paint_diff_hunks(layout.gutter_hitbox.bounds, layout, cx) } if layout.blamed_display_rows.is_some() { @@ -2303,113 +2351,135 @@ impl EditorElement { if let Some(indicator) = layout.code_actions_indicator.as_mut() { indicator.paint(cx); } - }) + }); } - fn paint_diff_hunks(layout: &EditorLayout, cx: &mut WindowContext) { + fn paint_diff_hunks( + gutter_bounds: Bounds, + layout: &EditorLayout, + cx: &mut WindowContext, + ) { if layout.display_hunks.is_empty() { return; } let line_height = layout.position_map.line_height; + cx.paint_layer(layout.gutter_hitbox.bounds, |cx| { + for (hunk, hitbox) in &layout.display_hunks { + let hunk_to_paint = match hunk { + DisplayDiffHunk::Folded { .. } => { + let hunk_bounds = Self::diff_hunk_bounds( + &layout.position_map.snapshot, + line_height, + gutter_bounds, + &hunk, + ); + Some(( + hunk_bounds, + cx.theme().status().modified, + Corners::all(1. * line_height), + )) + } + DisplayDiffHunk::Unfolded { status, .. } => { + hitbox.as_ref().map(|hunk_hitbox| match status { + DiffHunkStatus::Added => ( + hunk_hitbox.bounds, + cx.theme().status().created, + Corners::all(0.05 * line_height), + ), + DiffHunkStatus::Modified => ( + hunk_hitbox.bounds, + cx.theme().status().modified, + Corners::all(0.05 * line_height), + ), + DiffHunkStatus::Removed => ( + hunk_hitbox.bounds, + cx.theme().status().deleted, + Corners::all(1. * line_height), + ), + }) + } + }; - let scroll_position = layout.position_map.snapshot.scroll_position(); + if let Some((hunk_bounds, background_color, corner_radii)) = hunk_to_paint { + cx.paint_quad(quad( + hunk_bounds, + corner_radii, + background_color, + Edges::default(), + transparent_black(), + )); + } + } + }); + } + + fn diff_hunk_bounds( + snapshot: &EditorSnapshot, + line_height: Pixels, + bounds: Bounds, + hunk: &DisplayDiffHunk, + ) -> Bounds { + let scroll_position = snapshot.scroll_position(); let scroll_top = scroll_position.y * line_height; - cx.paint_layer(layout.gutter_hitbox.bounds, |cx| { - for hunk in &layout.display_hunks { - let (display_row_range, status) = match hunk { - //TODO: This rendering is entirely a horrible hack - &DisplayDiffHunk::Folded { display_row: row } => { - let start_y = row as f32 * line_height - scroll_top; - let end_y = start_y + line_height; - - let width = 0.275 * line_height; - let highlight_origin = layout.gutter_hitbox.origin + point(-width, start_y); - let highlight_size = size(width * 2., end_y - start_y); - let highlight_bounds = Bounds::new(highlight_origin, highlight_size); - cx.paint_quad(quad( - highlight_bounds, - Corners::all(1. * line_height), - cx.theme().status().modified, - Edges::default(), - transparent_black(), - )); - - continue; - } - - DisplayDiffHunk::Unfolded { - display_row_range, - status, - } => (display_row_range, status), - }; - - let color = match status { - DiffHunkStatus::Added => cx.theme().status().created, - DiffHunkStatus::Modified => cx.theme().status().modified, - - //TODO: This rendering is entirely a horrible hack - 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.275 * line_height; - let highlight_origin = layout.gutter_hitbox.origin + point(-width, start_y); - let highlight_size = size(width * 2., end_y - start_y); - let highlight_bounds = Bounds::new(highlight_origin, highlight_size); - cx.paint_quad(quad( - highlight_bounds, - Corners::all(1. * line_height), - cx.theme().status().deleted, - Edges::default(), - transparent_black(), - )); - - continue; - } - }; - - 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 - // excerpt header, so if we were to draw the marker straight away, - // the hunk might include the rows of that header. - // Making the range inclusive doesn't quite cut it, as we rely on the exclusivity for the soft wrap. - // Instead, we simply check whether the range we're dealing with includes - // any excerpt headers and if so, we stop painting the diff hunk on the first row of that header. - let end_row_in_current_excerpt = layout - .position_map - .snapshot - .blocks_in_range(start_row..end_row) - .find_map(|(start_row, block)| { - if matches!(block, TransformBlock::ExcerptHeader { .. }) { - Some(start_row) - } else { - None - } - }) - .unwrap_or(end_row); - - let start_y = start_row as f32 * line_height - scroll_top; - let end_y = end_row_in_current_excerpt as f32 * line_height - scroll_top; + match hunk { + DisplayDiffHunk::Folded { display_row, .. } => { + let start_y = *display_row as f32 * line_height - scroll_top; + let end_y = start_y + line_height; let width = 0.275 * line_height; - let highlight_origin = layout.gutter_hitbox.origin + point(-width, start_y); + let highlight_origin = bounds.origin + point(-width, start_y); let highlight_size = size(width * 2., end_y - start_y); - let highlight_bounds = Bounds::new(highlight_origin, highlight_size); - cx.paint_quad(quad( - highlight_bounds, - Corners::all(0.05 * line_height), - color, - Edges::default(), - transparent_black(), - )); + Bounds::new(highlight_origin, highlight_size) } - }) + DisplayDiffHunk::Unfolded { + display_row_range, + status, + .. + } => match status { + DiffHunkStatus::Added | DiffHunkStatus::Modified => { + 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 + // excerpt header, so if we were to draw the marker straight away, + // the hunk might include the rows of that header. + // Making the range inclusive doesn't quite cut it, as we rely on the exclusivity for the soft wrap. + // Instead, we simply check whether the range we're dealing with includes + // any excerpt headers and if so, we stop painting the diff hunk on the first row of that header. + let end_row_in_current_excerpt = snapshot + .blocks_in_range(start_row..end_row) + .find_map(|(start_row, block)| { + if matches!(block, TransformBlock::ExcerptHeader { .. }) { + Some(start_row) + } else { + None + } + }) + .unwrap_or(end_row); + + let start_y = start_row as f32 * line_height - scroll_top; + let end_y = end_row_in_current_excerpt as f32 * line_height - scroll_top; + + let width = 0.275 * line_height; + let highlight_origin = bounds.origin + point(-width, start_y); + let highlight_size = size(width * 2., 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; + let highlight_origin = bounds.origin + point(-width, start_y); + let highlight_size = size(width * 2., end_y - start_y); + Bounds::new(highlight_origin, highlight_size) + } + }, + } } fn paint_blamed_display_rows(&self, layout: &mut EditorLayout, cx: &mut WindowContext) { @@ -3009,14 +3079,22 @@ impl EditorElement { } }; - let scroll_position = position_map.snapshot.scroll_position(); - let x = (scroll_position.x * max_glyph_width + let current_scroll_position = position_map.snapshot.scroll_position(); + let x = (current_scroll_position.x * max_glyph_width - (delta.x * scroll_sensitivity)) / max_glyph_width; - let y = (scroll_position.y * line_height - (delta.y * scroll_sensitivity)) + let y = (current_scroll_position.y * line_height + - (delta.y * scroll_sensitivity)) / line_height; - let scroll_position = + let mut scroll_position = point(x, y).clamp(&point(0., 0.), &position_map.scroll_max); + let forbid_vertical_scroll = editor.scroll_manager.forbid_vertical_scroll(); + if forbid_vertical_scroll { + scroll_position.y = current_scroll_position.y; + if scroll_position == current_scroll_position { + return; + } + } editor.scroll(scroll_position, axis, cx); cx.stop_propagation(); }); @@ -3025,7 +3103,12 @@ impl EditorElement { }); } - fn paint_mouse_listeners(&mut self, layout: &EditorLayout, cx: &mut WindowContext) { + fn paint_mouse_listeners( + &mut self, + layout: &EditorLayout, + hovered_hunk: Option, + cx: &mut WindowContext, + ) { self.paint_scroll_wheel_listener(layout, cx); cx.on_mouse_event({ @@ -3041,6 +3124,7 @@ impl EditorElement { Self::mouse_left_down( editor, event, + hovered_hunk.as_ref(), &position_map, &text_hitbox, &gutter_hitbox, @@ -3566,12 +3650,15 @@ impl Element for EditorElement { let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width; let wrap_width = match editor.soft_wrap_mode(cx) { - SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance, - SoftWrap::EditorWidth => editor_width, - SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance), + SoftWrap::None => None, + SoftWrap::PreferLine => Some((MAX_LINE_LEN / 2) as f32 * em_advance), + SoftWrap::EditorWidth => Some(editor_width), + SoftWrap::Column(column) => { + Some(editor_width.min(column as f32 * em_advance)) + } }; - if editor.set_wrap_width(Some(wrap_width), cx) { + if editor.set_wrap_width(wrap_width, cx) { editor.snapshot(cx) } else { snapshot @@ -3645,9 +3732,9 @@ impl Element for EditorElement { ) }; - let highlighted_rows = self - .editor - .update(cx, |editor, cx| editor.highlighted_display_rows(cx)); + let highlighted_rows = self.editor.update(cx, |editor, cx| { + editor.highlighted_display_rows(HashSet::default(), cx) + }); let highlighted_ranges = self.editor.read(cx).background_highlights_in_range( start_anchor..end_anchor, &snapshot.display_snapshot, @@ -3678,7 +3765,13 @@ impl Element for EditorElement { cx, ); - let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot); + let display_hunks = self.layout_git_gutters( + line_height, + &gutter_hitbox, + start_row..end_row, + &snapshot, + cx, + ); let mut max_visible_line_width = Pixels::ZERO; let line_layouts = @@ -3988,14 +4081,41 @@ impl Element for EditorElement { line_height: Some(self.style.text.line_height), ..Default::default() }; + let mouse_position = cx.mouse_position(); + 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.contains(&mouse_position)) + .unwrap_or(false) + { + Some(HunkToExpand { + status: *status, + multi_buffer_range: multi_buffer_range.clone(), + diff_base_byte_range: diff_base_byte_range.clone(), + }) + } else { + None + } + } + }); cx.with_text_style(Some(text_style), |cx| { cx.with_content_mask(Some(ContentMask { bounds }), |cx| { - self.paint_mouse_listeners(layout, cx); - + self.paint_mouse_listeners(layout, hovered_hunk, cx); self.paint_background(layout, cx); if layout.gutter_hitbox.size.width > Pixels::ZERO { - self.paint_gutter(layout, cx); + self.paint_gutter(layout, cx) } + self.paint_text(layout, cx); if !layout.blocks.is_empty() { @@ -4035,7 +4155,7 @@ pub struct EditorLayout { active_rows: BTreeMap, highlighted_rows: BTreeMap, line_numbers: Vec>, - display_hunks: Vec, + display_hunks: Vec<(DisplayDiffHunk, Option)>, blamed_display_rows: Option>, inline_blame: Option, folds: Vec, @@ -4565,6 +4685,7 @@ mod tests { use language::language_settings; use log::info; use std::num::NonZeroU32; + use ui::Context; use util::test::sample_text; #[gpui::test] diff --git a/crates/editor/src/git.rs b/crates/editor/src/git.rs index 817a70190f..7b43b4c230 100644 --- a/crates/editor/src/git.rs +++ b/crates/editor/src/git.rs @@ -4,6 +4,7 @@ use std::ops::Range; use git::diff::{DiffHunk, DiffHunkStatus}; use language::Point; +use multi_buffer::Anchor; use crate::{ display_map::{DisplaySnapshot, ToDisplayPoint}, @@ -17,7 +18,9 @@ pub enum DisplayDiffHunk { }, Unfolded { + diff_base_byte_range: Range, display_row_range: Range, + multi_buffer_range: Range, status: DiffHunkStatus, }, } @@ -45,7 +48,7 @@ impl DisplayDiffHunk { } } -pub fn diff_hunk_to_display(hunk: DiffHunk, snapshot: &DisplaySnapshot) -> DisplayDiffHunk { +pub fn diff_hunk_to_display(hunk: &DiffHunk, snapshot: &DisplaySnapshot) -> DisplayDiffHunk { let hunk_start_point = Point::new(hunk.associated_range.start, 0); let hunk_start_point_sub = Point::new(hunk.associated_range.start.saturating_sub(1), 0); let hunk_end_point_sub = Point::new( @@ -81,11 +84,16 @@ pub fn diff_hunk_to_display(hunk: DiffHunk, snapshot: &DisplaySnapshot) -> let hunk_end_row = hunk.associated_range.end.max(hunk.associated_range.start); let hunk_end_point = Point::new(hunk_end_row, 0); + + let multi_buffer_start = snapshot.buffer_snapshot.anchor_after(hunk_start_point); + let multi_buffer_end = snapshot.buffer_snapshot.anchor_before(hunk_end_point); 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: hunk.status(), + diff_base_byte_range: hunk.diff_base_byte_range.clone(), } } } diff --git a/crates/editor/src/hunk_diff.rs b/crates/editor/src/hunk_diff.rs new file mode 100644 index 0000000000..7f5895eae0 --- /dev/null +++ b/crates/editor/src/hunk_diff.rs @@ -0,0 +1,623 @@ +use std::ops::Range; + +use collections::{hash_map, HashMap, HashSet}; +use git::diff::{DiffHunk, DiffHunkStatus}; +use gpui::{AppContext, Hsla, Model, Task, View}; +use language::Buffer; +use multi_buffer::{Anchor, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToPoint}; +use text::{BufferId, Point}; +use ui::{ + div, ActiveTheme, Context as _, IntoElement, ParentElement, Styled, ViewContext, VisualContext, +}; +use util::{debug_panic, RangeExt}; + +use crate::{ + git::{diff_hunk_to_display, DisplayDiffHunk}, + hunks_for_selections, BlockDisposition, BlockId, BlockProperties, BlockStyle, DiffRowHighlight, + Editor, ExpandAllHunkDiffs, RangeToAnchorExt, ToDisplayPoint, ToggleHunkDiff, +}; + +#[derive(Debug, Clone)] +pub(super) struct HunkToExpand { + pub multi_buffer_range: Range, + pub status: DiffHunkStatus, + pub diff_base_byte_range: Range, +} + +#[derive(Debug, Default)] +pub(super) struct ExpandedHunks { + hunks: Vec, + diff_base: HashMap, + hunk_update_tasks: HashMap, Task<()>>, +} + +#[derive(Debug)] +struct DiffBaseBuffer { + buffer: Model, + diff_base_version: usize, +} + +impl ExpandedHunks { + pub fn hunks(&self, include_folded: bool) -> impl Iterator { + self.hunks + .iter() + .filter(move |hunk| include_folded || !hunk.folded) + } +} + +#[derive(Debug, Clone)] +pub(super) struct ExpandedHunk { + pub block: Option, + pub hunk_range: Range, + pub diff_base_byte_range: Range, + pub status: DiffHunkStatus, + pub folded: bool, +} + +impl Editor { + pub fn toggle_hunk_diff(&mut self, _: &ToggleHunkDiff, cx: &mut ViewContext) { + let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx); + let selections = self.selections.disjoint_anchors(); + self.toggle_hunks_expanded( + hunks_for_selections(&multi_buffer_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 + .expanded_hunks + .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 = snapshot + .display_snapshot + .buffer_snapshot + .git_diff_hunks_in_range(0..u32::MAX) + .filter(|hunk| { + let hunk_display_row_range = Point::new(hunk.associated_range.start, 0) + .to_display_point(&snapshot.display_snapshot) + ..Point::new(hunk.associated_range.end, 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, + ) { + let previous_toggle_task = self.expanded_hunks.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.expanded_hunks.hunks.len()); + let mut blocks_to_remove = HashSet::default(); + let mut hunks_to_expand = Vec::new(); + editor.expanded_hunks.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.block); + hunks_to_toggle.next(); + retain = false; + break; + } else { + hunks_to_expand.push(HunkToExpand { + status, + multi_buffer_range, + diff_base_byte_range, + }); + hunks_to_toggle.next(); + continue; + } + } + } + } + + retain + }); + for remaining_hunk in hunks_to_toggle { + let remaining_hunk_point_range = + Point::new(remaining_hunk.associated_range.start, 0) + ..Point::new(remaining_hunk.associated_range.end, 0); + hunks_to_expand.push(HunkToExpand { + status: remaining_hunk.status(), + multi_buffer_range: remaining_hunk_point_range + .to_anchors(&snapshot.buffer_snapshot), + diff_base_byte_range: remaining_hunk.diff_base_byte_range.clone(), + }); + } + + for removed_rows in highlights_to_remove { + editor.highlight_rows::(removed_rows, None, 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.expanded_hunks + .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: &HunkToExpand, + cx: &mut ViewContext<'_, Editor>, + ) -> Option<()> { + let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx); + let multi_buffer_row_range = hunk + .multi_buffer_range + .start + .to_point(&multi_buffer_snapshot) + ..hunk.multi_buffer_range.end.to_point(&multi_buffer_snapshot); + let hunk_start = hunk.multi_buffer_range.start; + let hunk_end = hunk.multi_buffer_range.end; + + let buffer = self.buffer().clone(); + let (diff_base_buffer, deleted_text_range, deleted_text_lines) = + buffer.update(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + let hunk = buffer_diff_hunk(&snapshot, multi_buffer_row_range.clone())?; + let mut buffer_ranges = buffer.range_to_buffer_ranges(multi_buffer_row_range, cx); + if buffer_ranges.len() == 1 { + let (buffer, _, _) = buffer_ranges.pop()?; + let diff_base_buffer = diff_base_buffer + .or_else(|| self.current_diff_base_buffer(&buffer, cx)) + .or_else(|| create_diff_base_buffer(&buffer, cx)); + let buffer = buffer.read(cx); + let deleted_text_lines = buffer.diff_base().and_then(|diff_base| { + Some( + diff_base + .get(hunk.diff_base_byte_range.clone())? + .lines() + .count(), + ) + }); + Some(( + diff_base_buffer?, + hunk.diff_base_byte_range, + deleted_text_lines, + )) + } else { + None + } + })?; + + let block_insert_index = match self.expanded_hunks.hunks.binary_search_by(|probe| { + probe + .hunk_range + .start + .cmp(&hunk_start, &multi_buffer_snapshot) + }) { + Ok(_already_present) => return None, + Err(ix) => ix, + }; + + let block = match hunk.status { + DiffHunkStatus::Removed => self.add_deleted_lines( + deleted_text_lines, + hunk_start, + diff_base_buffer, + deleted_text_range, + cx, + ), + DiffHunkStatus::Added => { + self.highlight_rows::( + hunk_start..hunk_end, + Some(added_hunk_color(cx)), + cx, + ); + None + } + DiffHunkStatus::Modified => { + self.highlight_rows::( + hunk_start..hunk_end, + Some(added_hunk_color(cx)), + cx, + ); + self.add_deleted_lines( + deleted_text_lines, + hunk_start, + diff_base_buffer, + deleted_text_range, + cx, + ) + } + }; + self.expanded_hunks.hunks.insert( + block_insert_index, + ExpandedHunk { + block, + hunk_range: hunk_start..hunk_end, + status: hunk.status, + folded: false, + diff_base_byte_range: hunk.diff_base_byte_range.clone(), + }, + ); + + Some(()) + } + + fn add_deleted_lines( + &mut self, + deleted_text_lines: Option, + hunk_start: Anchor, + diff_base_buffer: Model, + deleted_text_range: Range, + cx: &mut ViewContext<'_, Self>, + ) -> Option { + if let Some(deleted_text_lines) = deleted_text_lines { + self.insert_deleted_text_block( + hunk_start, + diff_base_buffer, + deleted_text_range, + deleted_text_lines as u8, + cx, + ) + } else { + debug_panic!("Found no deleted text for removed hunk on position {hunk_start:?}"); + None + } + } + + fn insert_deleted_text_block( + &mut self, + position: Anchor, + diff_base_buffer: Model, + deleted_text_range: Range, + deleted_text_height: u8, + cx: &mut ViewContext<'_, Self>, + ) -> Option { + let deleted_hunk_color = deleted_hunk_color(cx); + let (editor_height, editor_with_deleted_text) = + editor_with_deleted_text(diff_base_buffer, deleted_text_range, deleted_hunk_color, cx); + let parent_gutter_width = self.gutter_width; + let mut new_block_ids = self.insert_blocks( + Some(BlockProperties { + position, + height: editor_height.max(deleted_text_height), + style: BlockStyle::Flex, + render: Box::new(move |_| { + div() + .bg(deleted_hunk_color) + .size_full() + .pl(parent_gutter_width) + .child(editor_with_deleted_text.clone()) + .into_any_element() + }), + disposition: BlockDisposition::Above, + }), + None, + cx, + ); + if new_block_ids.len() == 1 { + new_block_ids.pop() + } else { + debug_panic!( + "Inserted one editor block but did not receive exactly one block id: {new_block_ids:?}" + ); + None + } + } + + pub(super) fn clear_expanded_diff_hunks(&mut self, cx: &mut ViewContext<'_, Editor>) { + self.expanded_hunks.hunk_update_tasks.clear(); + let to_remove = self + .expanded_hunks + .hunks + .drain(..) + .filter_map(|expanded_hunk| expanded_hunk.block) + .collect(); + self.clear_row_highlights::(); + self.remove_blocks(to_remove, None, cx); + } + + pub(super) fn sync_expanded_diff_hunks( + &mut self, + buffer: Model, + cx: &mut ViewContext<'_, Self>, + ) { + let buffer_id = buffer.read(cx).remote_id(); + let buffer_diff_base_version = buffer.read(cx).diff_base_version(); + self.expanded_hunks + .hunk_update_tasks + .remove(&Some(buffer_id)); + let diff_base_buffer = self.current_diff_base_buffer(&buffer, cx); + let new_sync_task = cx.spawn(move |editor, mut cx| async move { + let diff_base_buffer_unchanged = diff_base_buffer.is_some(); + let Ok(diff_base_buffer) = + cx.update(|cx| diff_base_buffer.or_else(|| create_diff_base_buffer(&buffer, cx))) + else { + return; + }; + editor + .update(&mut cx, |editor, cx| { + if let Some(diff_base_buffer) = &diff_base_buffer { + editor.expanded_hunks.diff_base.insert( + buffer_id, + DiffBaseBuffer { + buffer: diff_base_buffer.clone(), + diff_base_version: buffer_diff_base_version, + }, + ); + } + + let snapshot = editor.snapshot(cx); + let buffer_snapshot = buffer.read(cx).snapshot(); + let mut recalculated_hunks = buffer_snapshot + .git_diff_hunks_in_row_range(0..u32::MAX) + .fuse() + .peekable(); + let mut highlights_to_remove = + Vec::with_capacity(editor.expanded_hunks.hunks.len()); + let mut blocks_to_remove = HashSet::default(); + let mut hunks_to_reexpand = + Vec::with_capacity(editor.expanded_hunks.hunks.len()); + editor.expanded_hunks.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()); + if let Some(block) = expanded_hunk.block.take() { + 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(); + continue; + } else if expanded_hunk_display_range.end + < hunk_display_range.start + { + break; + } else { + if !expanded_hunk.folded + && expanded_hunk_display_range == hunk_display_range + && expanded_hunk.status == buffer_hunk.status() + && expanded_hunk.diff_base_byte_range + == buffer_hunk.diff_base_byte_range + { + recalculated_hunks.next(); + retain = true; + } else { + hunks_to_reexpand.push(HunkToExpand { + status, + multi_buffer_range, + diff_base_byte_range, + }); + } + break; + } + } + } + } + } + if !retain { + blocks_to_remove.extend(expanded_hunk.block); + highlights_to_remove.push(expanded_hunk.hunk_range.clone()); + } + retain + }); + + for removed_rows in highlights_to_remove { + editor.highlight_rows::(removed_rows, None, 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(); + }); + + self.expanded_hunks.hunk_update_tasks.insert( + Some(buffer_id), + cx.background_executor().spawn(new_sync_task), + ); + } + + fn current_diff_base_buffer( + &mut self, + buffer: &Model, + cx: &mut AppContext, + ) -> Option> { + buffer.update(cx, |buffer, _| { + match self.expanded_hunks.diff_base.entry(buffer.remote_id()) { + hash_map::Entry::Occupied(o) => { + if o.get().diff_base_version != buffer.diff_base_version() { + o.remove(); + None + } else { + Some(o.get().buffer.clone()) + } + } + hash_map::Entry::Vacant(_) => None, + } + }) + } +} + +fn create_diff_base_buffer(buffer: &Model, cx: &mut AppContext) -> Option> { + buffer + .update(cx, |buffer, _| { + let language = buffer.language().cloned(); + let diff_base = buffer.diff_base().map(|s| s.to_owned()); + Some((diff_base?, language)) + }) + .map(|(diff_base, language)| { + cx.new_model(|cx| { + let buffer = Buffer::local(diff_base, cx); + match language { + Some(language) => buffer.with_language(language, cx), + None => buffer, + } + }) + }) +} + +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().git().deleted; + deleted_color.fade_out(0.7); + deleted_color +} + +fn editor_with_deleted_text( + diff_base_buffer: Model, + deleted_text_range: Range, + deleted_color: Hsla, + cx: &mut ViewContext<'_, Editor>, +) -> (u8, View) { + let editor = cx.new_view(|cx| { + let multi_buffer = + cx.new_model(|_| MultiBuffer::without_headers(0, language::Capability::ReadOnly)); + multi_buffer.update(cx, |multi_buffer, cx| { + multi_buffer.push_excerpts( + diff_base_buffer, + Some(ExcerptRange { + context: deleted_text_range, + primary: None, + }), + cx, + ); + }); + + let mut editor = Editor::for_multibuffer(multi_buffer, None, cx); + editor.soft_wrap_mode_override = Some(language::language_settings::SoftWrap::None); + editor.show_wrap_guides = Some(false); + editor.show_gutter = false; + editor.scroll_manager.set_forbid_vertical_scroll(true); + editor.set_read_only(true); + + let editor_snapshot = editor.snapshot(cx); + let start = editor_snapshot.buffer_snapshot.anchor_before(0); + let end = editor_snapshot + .buffer_snapshot + .anchor_after(editor.buffer.read(cx).len(cx)); + + editor.highlight_rows::(start..end, Some(deleted_color), cx); + editor + }); + + let editor_height = editor.update(cx, |editor, cx| editor.max_point(cx).row() as u8); + (editor_height, editor) +} + +fn buffer_diff_hunk( + buffer_snapshot: &MultiBufferSnapshot, + row_range: Range, +) -> Option> { + let mut hunks = buffer_snapshot.git_diff_hunks_in_range(row_range.start.row..row_range.end.row); + let hunk = hunks.next()?; + let second_hunk = hunks.next(); + if second_hunk.is_none() { + return Some(hunk); + } + None +} diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 14f6edc1d4..97d5a6538a 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -137,6 +137,7 @@ pub struct ScrollManager { hide_scrollbar_task: Option>, dragging_scrollbar: bool, visible_line_count: Option, + forbid_vertical_scroll: bool, } impl ScrollManager { @@ -151,6 +152,7 @@ impl ScrollManager { dragging_scrollbar: false, last_autoscroll: None, visible_line_count: None, + forbid_vertical_scroll: false, } } @@ -185,6 +187,9 @@ impl ScrollManager { workspace_id: Option, cx: &mut ViewContext, ) { + if self.forbid_vertical_scroll { + return; + } let (new_anchor, top_row) = if scroll_position.y <= 0. { ( ScrollAnchor { @@ -224,6 +229,9 @@ impl ScrollManager { workspace_id: Option, cx: &mut ViewContext, ) { + if self.forbid_vertical_scroll { + return; + } self.anchor = anchor; cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll }); self.show_scrollbar(cx); @@ -298,6 +306,14 @@ impl ScrollManager { false } } + + pub fn set_forbid_vertical_scroll(&mut self, forbid: bool) { + self.forbid_vertical_scroll = forbid; + } + + pub fn forbid_vertical_scroll(&self) -> bool { + self.forbid_vertical_scroll + } } impl Editor { @@ -334,6 +350,9 @@ impl Editor { scroll_delta: gpui::Point, cx: &mut ViewContext, ) { + if self.scroll_manager.forbid_vertical_scroll { + return; + } let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let position = self.scroll_manager.anchor.scroll_position(&display_map) + scroll_delta; self.set_scroll_position_taking_display_map(position, true, false, display_map, cx); @@ -344,6 +363,9 @@ impl Editor { scroll_position: gpui::Point, cx: &mut ViewContext, ) { + if self.scroll_manager.forbid_vertical_scroll { + return; + } self.set_scroll_position_internal(scroll_position, true, false, cx); } diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index ccf0126b1e..d197d01046 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -1,9 +1,12 @@ -use std::{cmp, f32}; +use std::{any::TypeId, cmp, f32}; +use collections::HashSet; use gpui::{px, Bounds, Pixels, ViewContext}; use language::Point; -use crate::{display_map::ToDisplayPoint, Editor, EditorMode, LineWithInvisibles}; +use crate::{ + display_map::ToDisplayPoint, DiffRowHighlight, Editor, EditorMode, LineWithInvisibles, +}; #[derive(PartialEq, Eq, Clone, Copy)] pub enum Autoscroll { @@ -103,7 +106,13 @@ impl Editor { let mut target_top; let mut target_bottom; - if let Some(first_highlighted_row) = &self.highlighted_display_rows(cx).first_entry() { + if let Some(first_highlighted_row) = &self + .highlighted_display_rows( + HashSet::from_iter(Some(TypeId::of::())), + cx, + ) + .first_entry() + { target_top = *first_highlighted_row.key() as f32; target_bottom = target_top + 1.; } else { diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index f8d096db54..1bb8408889 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -75,3 +75,93 @@ pub(crate) fn build_editor_with_project( ) -> Editor { Editor::new(EditorMode::Full, buffer, Some(project), cx) } + +#[cfg(any(test, feature = "test-support"))] +pub fn editor_hunks( + editor: &Editor, + snapshot: &DisplaySnapshot, + cx: &mut ViewContext<'_, Editor>, +) -> Vec<(String, git::diff::DiffHunkStatus, core::ops::Range)> { + use text::Point; + + snapshot + .buffer_snapshot + .git_diff_hunks_in_range(0..u32::MAX) + .map(|hunk| { + let display_range = Point::new(hunk.associated_range.start, 0) + .to_display_point(snapshot) + .row() + ..Point::new(hunk.associated_range.end, 0) + .to_display_point(snapshot) + .row(); + let (_, buffer, _) = editor + .buffer() + .read(cx) + .excerpt_containing(Point::new(hunk.associated_range.start, 0), cx) + .expect("no excerpt for expanded buffer's hunk start"); + let diff_base = &buffer + .read(cx) + .diff_base() + .expect("should have a diff base for expanded hunk") + [hunk.diff_base_byte_range.clone()]; + (diff_base.to_owned(), hunk.status(), display_range) + }) + .collect() +} + +#[cfg(any(test, feature = "test-support"))] +pub fn expanded_hunks( + editor: &Editor, + snapshot: &DisplaySnapshot, + cx: &mut ViewContext<'_, Editor>, +) -> Vec<(String, git::diff::DiffHunkStatus, core::ops::Range)> { + editor + .expanded_hunks + .hunks(false) + .map(|expanded_hunk| { + let hunk_display_range = expanded_hunk + .hunk_range + .start + .to_display_point(snapshot) + .row() + ..expanded_hunk + .hunk_range + .end + .to_display_point(snapshot) + .row(); + let (_, buffer, _) = editor + .buffer() + .read(cx) + .excerpt_containing(expanded_hunk.hunk_range.start, cx) + .expect("no excerpt for expanded buffer's hunk start"); + let diff_base = &buffer + .read(cx) + .diff_base() + .expect("should have a diff base for expanded hunk") + [expanded_hunk.diff_base_byte_range.clone()]; + ( + diff_base.to_owned(), + expanded_hunk.status, + hunk_display_range, + ) + }) + .collect() +} + +#[cfg(any(test, feature = "test-support"))] +pub fn expanded_hunks_background_highlights( + editor: &Editor, + snapshot: &DisplaySnapshot, +) -> Vec> { + use itertools::Itertools; + + editor + .highlighted_rows::() + .into_iter() + .flatten() + .map(|(range, _)| { + range.start.to_display_point(snapshot).row()..range.end.to_display_point(snapshot).row() + }) + .unique() + .collect() +} diff --git a/crates/git/src/diff.rs b/crates/git/src/diff.rs index a8b14c2a49..3cab8cb664 100644 --- a/crates/git/src/diff.rs +++ b/crates/git/src/diff.rs @@ -5,7 +5,7 @@ use text::{Anchor, BufferId, BufferSnapshot, OffsetRangeExt, Point}; pub use git2 as libgit; use libgit::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch}; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum DiffHunkStatus { Added, Modified, @@ -173,7 +173,8 @@ impl BufferDiff { }) } - pub fn clear(&mut self, buffer: &text::BufferSnapshot) { + #[cfg(test)] + fn clear(&mut self, buffer: &text::BufferSnapshot) { self.last_buffer_version = Some(buffer.version().clone()); self.tree = SumTree::new(); } diff --git a/crates/go_to_line/Cargo.toml b/crates/go_to_line/Cargo.toml index 0e19b41b75..9cffedaa01 100644 --- a/crates/go_to_line/Cargo.toml +++ b/crates/go_to_line/Cargo.toml @@ -14,6 +14,7 @@ doctest = false [dependencies] anyhow.workspace = true +collections.workspace = true editor.workspace = true gpui.workspace = true menu.workspace = true diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 1ea492a8eb..f47c144580 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -221,6 +221,7 @@ impl Render for GoToLine { mod tests { use std::sync::Arc; + use collections::HashSet; use gpui::{TestAppContext, VisualTestContext}; use indoc::indoc; use project::{FakeFs, Project}; @@ -348,7 +349,10 @@ mod tests { fn highlighted_display_rows(editor: &View, cx: &mut VisualTestContext) -> Vec { editor.update(cx, |editor, cx| { - editor.highlighted_display_rows(cx).into_keys().collect() + editor + .highlighted_display_rows(HashSet::default(), cx) + .into_keys() + .collect() }) } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 1c5a6b0644..836e99bad3 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -109,6 +109,7 @@ pub struct Buffer { deferred_ops: OperationQueue, capability: Capability, has_conflict: bool, + diff_base_version: usize, } /// An immutable, cheaply cloneable representation of a fixed @@ -304,6 +305,8 @@ pub enum Event { Reloaded, /// The buffer's diff_base changed. DiffBaseChanged, + /// Buffer's excerpts for a certain diff base were recalculated. + DiffUpdated, /// The buffer's language was changed. LanguageChanged, /// The buffer's syntax trees were updated. @@ -643,6 +646,7 @@ impl Buffer { was_dirty_before_starting_transaction: None, text: buffer, diff_base, + diff_base_version: 0, git_diff: git::diff::BufferDiff::new(), file, capability, @@ -872,6 +876,7 @@ impl Buffer { /// against the buffer text. pub fn set_diff_base(&mut self, diff_base: Option, cx: &mut ModelContext) { self.diff_base = diff_base; + self.diff_base_version += 1; if let Some(recalc_task) = self.git_diff_recalc(cx) { cx.spawn(|buffer, mut cx| async move { recalc_task.await; @@ -885,6 +890,11 @@ impl Buffer { } } + /// Returns a number, unique per diff base set to the buffer. + pub fn diff_base_version(&self) -> usize { + self.diff_base_version + } + /// Recomputes the Git diff status. pub fn git_diff_recalc(&mut self, cx: &mut ModelContext) -> Option> { let diff_base = self.diff_base.clone()?; // TODO: Make this an Arc @@ -898,9 +908,10 @@ impl Buffer { Some(cx.spawn(|this, mut cx| async move { let buffer_diff = diff.await; - this.update(&mut cx, |this, _| { + this.update(&mut cx, |this, cx| { this.git_diff = buffer_diff; this.git_diff_update_count += 1; + cx.emit(Event::DiffUpdated); }) .ok(); })) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index e628b3d1c0..940e7ad18e 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -335,6 +335,8 @@ pub struct FeaturesContent { pub enum SoftWrap { /// Do not soft wrap. None, + /// Prefer a single line generally, unless an overly long line is encountered. + PreferLine, /// Soft wrap lines that overflow the editor EditorWidth, /// Soft wrap lines at the preferred line length diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index ee2d00aa04..38bb3deed0 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -87,6 +87,9 @@ pub enum Event { }, Reloaded, DiffBaseChanged, + DiffUpdated { + buffer: Model, + }, LanguageChanged, CapabilityChanged, Reparsed, @@ -156,6 +159,7 @@ pub struct MultiBufferSnapshot { edit_count: usize, is_dirty: bool, has_conflict: bool, + show_headers: bool, } /// A boundary between [`Excerpt`]s in a [`MultiBuffer`] @@ -269,6 +273,28 @@ struct ExcerptBytes<'a> { impl MultiBuffer { pub fn new(replica_id: ReplicaId, capability: Capability) -> Self { + Self { + snapshot: RefCell::new(MultiBufferSnapshot { + show_headers: true, + ..MultiBufferSnapshot::default() + }), + buffers: RefCell::default(), + subscriptions: Topic::default(), + singleton: false, + capability, + replica_id, + title: None, + history: History { + next_transaction_id: clock::Lamport::default(), + undo_stack: Vec::new(), + redo_stack: Vec::new(), + transaction_depth: 0, + group_interval: Duration::from_millis(300), + }, + } + } + + pub fn without_headers(replica_id: ReplicaId, capability: Capability) -> Self { Self { snapshot: Default::default(), buffers: Default::default(), @@ -1466,6 +1492,7 @@ impl MultiBuffer { language::Event::FileHandleChanged => Event::FileHandleChanged, language::Event::Reloaded => Event::Reloaded, language::Event::DiffBaseChanged => Event::DiffBaseChanged, + language::Event::DiffUpdated => Event::DiffUpdated { buffer }, language::Event::LanguageChanged => Event::LanguageChanged, language::Event::Reparsed => Event::Reparsed, language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated, @@ -3588,6 +3615,10 @@ impl MultiBufferSnapshot { }) }) } + + pub fn show_headers(&self) -> bool { + self.show_headers + } } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/outline/Cargo.toml b/crates/outline/Cargo.toml index 6f385f5d8d..230fb0d358 100644 --- a/crates/outline/Cargo.toml +++ b/crates/outline/Cargo.toml @@ -13,6 +13,7 @@ path = "src/outline.rs" doctest = false [dependencies] +collections.workspace = true editor.workspace = true fuzzy.workspace = true gpui.workspace = true diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index dab806ad54..e57f8c56b6 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -98,6 +98,8 @@ struct OutlineViewDelegate { last_query: String, } +enum OutlineRowHighlights {} + impl OutlineViewDelegate { fn new( outline_view: WeakView, @@ -150,8 +152,6 @@ impl OutlineViewDelegate { } } -enum OutlineRowHighlights {} - impl PickerDelegate for OutlineViewDelegate { type ListItem = ListItem; @@ -316,6 +316,7 @@ impl PickerDelegate for OutlineViewDelegate { #[cfg(test)] mod tests { + use collections::HashSet; use gpui::{TestAppContext, VisualTestContext}; use indoc::indoc; use language::{Language, LanguageConfig, LanguageMatcher}; @@ -482,7 +483,10 @@ mod tests { fn highlighted_display_rows(editor: &View, cx: &mut VisualTestContext) -> Vec { editor.update(cx, |editor, cx| { - editor.highlighted_display_rows(cx).into_keys().collect() + editor + .highlighted_display_rows(HashSet::default(), cx) + .into_keys() + .collect() }) } diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index b493315455..ed32b11dd4 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -32,7 +32,7 @@ pub struct GitSettings { /// Whether or not to show git blame data inline in /// the currently focused line. /// - /// Default: off + /// Default: on pub inline_blame: Option, }