Detailed changes
@@ -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",
@@ -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"
}
},
@@ -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"
}
},
@@ -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"
}
@@ -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;
@@ -253,6 +253,8 @@ gpui::actions!(
TabPrev,
ToggleGitBlame,
ToggleGitBlameInline,
+ ToggleHunkDiff,
+ ExpandAllHunkDiffs,
ToggleInlayHints,
ToggleLineNumbers,
ToggleSoftWrap,
@@ -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)| {
@@ -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<Inlay>,
show_inline_completions: bool,
inlay_hint_cache: InlayHintCache,
+ expanded_hunks: ExpandedHunks,
next_inlay_id: usize,
_subscriptions: Vec<Subscription>,
pixel_position_of_newest_cursor: Option<gpui::Point<Pixels>>,
@@ -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>) {
+ 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::<BufferId, HashSet<Range<text::Anchor>>>::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<Self>,
) {
- 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<Self>,
) {
- 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<u32, Hsla> {
+ /// 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<TypeId>,
+ cx: &mut WindowContext,
+ ) -> BTreeMap<u32, Hsla> {
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::<u32, Hsla>::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<Anchor>],
+) -> Vec<DiffHunk<u32>> {
+ let mut hunks = Vec::with_capacity(selections.len());
+ let mut processed_buffer_rows: HashMap<BufferId, HashSet<Range<text::Anchor>>> =
+ 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<PeerId, Collaborator>;
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.)
@@ -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<DisplayPoint> {
let point = DisplayPoint::new(row as u32, column as u32);
point..point
@@ -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<u32>,
snapshot: &EditorSnapshot,
- ) -> Vec<DisplayDiffHunk> {
+ cx: &mut WindowContext,
+ ) -> Vec<(DisplayDiffHunk, Option<Hitbox>)> {
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::<HashMap<_, _>>()
+ });
+
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;
- }
- 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;
+ let mut current_paint: Option<(Hsla, Range<u32>)> = 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;
+ }
}
- 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<Pixels>,
+ layout: &EditorLayout,
+ cx: &mut WindowContext,
+ ) {
if layout.display_hunks.is_empty() {
return;
}
let line_height = layout.position_map.line_height;
-
- let scroll_position = layout.position_map.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),
+ 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,
- Edges::default(),
- transparent_black(),
- ));
-
- continue;
+ 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),
+ ),
+ })
}
-
- 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,
+ 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(),
+ ));
+ }
+ }
+ });
+ }
- //TODO: This rendering is entirely a horrible hack
- DiffHunkStatus::Removed => {
- let row = display_row_range.start;
+ fn diff_hunk_bounds(
+ snapshot: &EditorSnapshot,
+ line_height: Pixels,
+ bounds: Bounds<Pixels>,
+ hunk: &DisplayDiffHunk,
+ ) -> Bounds<Pixels> {
+ let scroll_position = snapshot.scroll_position();
+ let scroll_top = scroll_position.y * line_height;
- let offset = line_height / 2.;
- let start_y = row as f32 * line_height - offset - scroll_top;
- let end_y = start_y + line_height;
+ 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_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(),
- ));
+ 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)
+ }
+ 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);
- continue;
- }
- };
+ 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 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 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 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 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(0.05 * line_height),
- color,
- Edges::default(),
- transparent_black(),
- ));
- }
- })
+ 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<HunkToExpand>,
+ 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<u32, bool>,
highlighted_rows: BTreeMap<u32, Hsla>,
line_numbers: Vec<Option<ShapedLine>>,
- display_hunks: Vec<DisplayDiffHunk>,
+ display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
blamed_display_rows: Option<Vec<AnyElement>>,
inline_blame: Option<AnyElement>,
folds: Vec<FoldLayout>,
@@ -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]
@@ -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<usize>,
display_row_range: Range<u32>,
+ multi_buffer_range: Range<Anchor>,
status: DiffHunkStatus,
},
}
@@ -45,7 +48,7 @@ impl DisplayDiffHunk {
}
}
-pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) -> DisplayDiffHunk {
+pub fn diff_hunk_to_display(hunk: &DiffHunk<u32>, 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<u32>, 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(),
}
}
}
@@ -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<Anchor>,
+ pub status: DiffHunkStatus,
+ pub diff_base_byte_range: Range<usize>,
+}
+
+#[derive(Debug, Default)]
+pub(super) struct ExpandedHunks {
+ hunks: Vec<ExpandedHunk>,
+ diff_base: HashMap<BufferId, DiffBaseBuffer>,
+ hunk_update_tasks: HashMap<Option<BufferId>, Task<()>>,
+}
+
+#[derive(Debug)]
+struct DiffBaseBuffer {
+ buffer: Model<Buffer>,
+ diff_base_version: usize,
+}
+
+impl ExpandedHunks {
+ pub fn hunks(&self, include_folded: bool) -> impl Iterator<Item = &ExpandedHunk> {
+ self.hunks
+ .iter()
+ .filter(move |hunk| include_folded || !hunk.folded)
+ }
+}
+
+#[derive(Debug, Clone)]
+pub(super) struct ExpandedHunk {
+ pub block: Option<BlockId>,
+ pub hunk_range: Range<Anchor>,
+ pub diff_base_byte_range: Range<usize>,
+ pub status: DiffHunkStatus,
+ pub folded: bool,
+}
+
+impl Editor {
+ pub fn toggle_hunk_diff(&mut self, _: &ToggleHunkDiff, cx: &mut ViewContext<Self>) {
+ 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<Self>) {
+ 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::<HashMap<_, _>>();
+ 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<DiffHunk<u32>>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ 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::<DiffRowHighlight>(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<Model<Buffer>>,
+ 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::<DiffRowHighlight>(
+ hunk_start..hunk_end,
+ Some(added_hunk_color(cx)),
+ cx,
+ );
+ None
+ }
+ DiffHunkStatus::Modified => {
+ self.highlight_rows::<DiffRowHighlight>(
+ 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<usize>,
+ hunk_start: Anchor,
+ diff_base_buffer: Model<Buffer>,
+ deleted_text_range: Range<usize>,
+ cx: &mut ViewContext<'_, Self>,
+ ) -> Option<BlockId> {
+ 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<Buffer>,
+ deleted_text_range: Range<usize>,
+ deleted_text_height: u8,
+ cx: &mut ViewContext<'_, Self>,
+ ) -> Option<BlockId> {
+ 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::<DiffRowHighlight>();
+ self.remove_blocks(to_remove, None, cx);
+ }
+
+ pub(super) fn sync_expanded_diff_hunks(
+ &mut self,
+ buffer: Model<Buffer>,
+ 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::<DiffRowHighlight>(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<Buffer>,
+ cx: &mut AppContext,
+ ) -> Option<Model<Buffer>> {
+ 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<Buffer>, cx: &mut AppContext) -> Option<Model<Buffer>> {
+ 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<Buffer>,
+ deleted_text_range: Range<usize>,
+ deleted_color: Hsla,
+ cx: &mut ViewContext<'_, Editor>,
+) -> (u8, View<Editor>) {
+ 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::<DiffRowHighlight>(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<Point>,
+) -> Option<DiffHunk<u32>> {
+ 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
+}
@@ -137,6 +137,7 @@ pub struct ScrollManager {
hide_scrollbar_task: Option<Task<()>>,
dragging_scrollbar: bool,
visible_line_count: Option<f32>,
+ 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<WorkspaceId>,
cx: &mut ViewContext<Editor>,
) {
+ 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<WorkspaceId>,
cx: &mut ViewContext<Editor>,
) {
+ 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<f32>,
cx: &mut ViewContext<Self>,
) {
+ 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<f32>,
cx: &mut ViewContext<Self>,
) {
+ if self.scroll_manager.forbid_vertical_scroll {
+ return;
+ }
self.set_scroll_position_internal(scroll_position, true, false, cx);
}
@@ -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::<DiffRowHighlight>())),
+ cx,
+ )
+ .first_entry()
+ {
target_top = *first_highlighted_row.key() as f32;
target_bottom = target_top + 1.;
} else {
@@ -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<u32>)> {
+ 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<u32>)> {
+ 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<core::ops::Range<u32>> {
+ use itertools::Itertools;
+
+ editor
+ .highlighted_rows::<crate::DiffRowHighlight>()
+ .into_iter()
+ .flatten()
+ .map(|(range, _)| {
+ range.start.to_display_point(snapshot).row()..range.end.to_display_point(snapshot).row()
+ })
+ .unique()
+ .collect()
+}
@@ -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();
}
@@ -14,6 +14,7 @@ doctest = false
[dependencies]
anyhow.workspace = true
+collections.workspace = true
editor.workspace = true
gpui.workspace = true
menu.workspace = true
@@ -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<Editor>, cx: &mut VisualTestContext) -> Vec<u32> {
editor.update(cx, |editor, cx| {
- editor.highlighted_display_rows(cx).into_keys().collect()
+ editor
+ .highlighted_display_rows(HashSet::default(), cx)
+ .into_keys()
+ .collect()
})
}
@@ -109,6 +109,7 @@ pub struct Buffer {
deferred_ops: OperationQueue<Operation>,
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<String>, cx: &mut ModelContext<Self>) {
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<Self>) -> Option<Task<()>> {
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();
}))
@@ -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
@@ -87,6 +87,9 @@ pub enum Event {
},
Reloaded,
DiffBaseChanged,
+ DiffUpdated {
+ buffer: Model<Buffer>,
+ },
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"))]
@@ -13,6 +13,7 @@ path = "src/outline.rs"
doctest = false
[dependencies]
+collections.workspace = true
editor.workspace = true
fuzzy.workspace = true
gpui.workspace = true
@@ -98,6 +98,8 @@ struct OutlineViewDelegate {
last_query: String,
}
+enum OutlineRowHighlights {}
+
impl OutlineViewDelegate {
fn new(
outline_view: WeakView<OutlineView>,
@@ -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<Editor>, cx: &mut VisualTestContext) -> Vec<u32> {
editor.update(cx, |editor, cx| {
- editor.highlighted_display_rows(cx).into_keys().collect()
+ editor
+ .highlighted_display_rows(HashSet::default(), cx)
+ .into_keys()
+ .collect()
})
}
@@ -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<InlineBlameSettings>,
}