Allow to toggle git hunk diffs (#11080)

Part of https://github.com/zed-industries/zed/issues/4523

Added two new actions with the default keybindings

```
"cmd-'": "editor::ToggleHunkDiff",
"cmd-\"": "editor::ExpandAllHunkDiffs",
```

that allow to browse git hunk diffs in Zed:


https://github.com/zed-industries/zed/assets/2690773/9a8a7d10-ed06-4960-b4ee-fe28fc5c4768


The hunks are dynamic and alter on user folds and modifications, or
toggle hidden, if the modifications were not adjacent to the expanded
hunk.


Release Notes:

- Added `editor::ToggleHunkDiff` (`cmd-'`) and
`editor::ExpandAllHunkDiffs` (`cmd-"`) actions to browse git hunk diffs
in Zed
This commit is contained in:
Kirill Bulatov 2024-05-01 22:47:36 +03:00 committed by GitHub
parent 5831d80f51
commit caa0d35b8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 3115 additions and 249 deletions

2
Cargo.lock generated
View file

@ -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",

View file

@ -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"
}
},

View file

@ -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"
}
},

View file

@ -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"
}

View file

@ -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;

View file

@ -253,6 +253,8 @@ gpui::actions!(
TabPrev,
ToggleGitBlame,
ToggleGitBlameInline,
ToggleHunkDiff,
ExpandAllHunkDiffs,
ToggleInlayHints,
ToggleLineNumbers,
ToggleSoftWrap,

View file

@ -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)| {

View file

@ -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.)

File diff suppressed because it is too large Load diff

View file

@ -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;
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;
}
}
paint_highlight(highlight_row_start, highlight_row_end, color);
highlight_row_start = 0;
highlight_row_end = 0;
if !paint_range_is_unfinished {
highlight_row_start = row;
last_row = Some((row, color));
}
} else {
if last_row.is_none() {
highlight_row_start = row;
} else {
highlight_row_end = row;
}
last_row = Some((row, color));
}
None => current_paint = Some((new_color, new_row..new_row)),
};
}
if let Some((row, hsla)) = last_row {
highlight_row_end = row;
paint_highlight(highlight_row_start, highlight_row_end, hsla);
if let Some((color, range)) = current_paint {
paint_highlight(range.start, range.end, color);
}
let scroll_left =
@ -2265,14 +2309,18 @@ impl EditorElement {
let scroll_top = scroll_position.y * line_height;
cx.set_cursor_style(CursorStyle::Arrow, &layout.gutter_hitbox);
for (_, hunk_hitbox) in &layout.display_hunks {
if let Some(hunk_hitbox) = hunk_hitbox {
cx.set_cursor_style(CursorStyle::PointingHand, hunk_hitbox);
}
}
let show_git_gutter = matches!(
ProjectSettings::get_global(cx).git.git_gutter,
Some(GitGutterSetting::TrackedFiles)
);
if show_git_gutter {
Self::paint_diff_hunks(layout, cx);
Self::paint_diff_hunks(layout.gutter_hitbox.bounds, layout, cx)
}
if layout.blamed_display_rows.is_some() {
@ -2303,113 +2351,135 @@ impl EditorElement {
if let Some(indicator) = layout.code_actions_indicator.as_mut() {
indicator.paint(cx);
}
})
});
}
fn paint_diff_hunks(layout: &EditorLayout, cx: &mut WindowContext) {
fn paint_diff_hunks(
gutter_bounds: Bounds<Pixels>,
layout: &EditorLayout,
cx: &mut WindowContext,
) {
if layout.display_hunks.is_empty() {
return;
}
let line_height = layout.position_map.line_height;
cx.paint_layer(layout.gutter_hitbox.bounds, |cx| {
for (hunk, hitbox) in &layout.display_hunks {
let hunk_to_paint = match hunk {
DisplayDiffHunk::Folded { .. } => {
let hunk_bounds = Self::diff_hunk_bounds(
&layout.position_map.snapshot,
line_height,
gutter_bounds,
&hunk,
);
Some((
hunk_bounds,
cx.theme().status().modified,
Corners::all(1. * line_height),
))
}
DisplayDiffHunk::Unfolded { status, .. } => {
hitbox.as_ref().map(|hunk_hitbox| match status {
DiffHunkStatus::Added => (
hunk_hitbox.bounds,
cx.theme().status().created,
Corners::all(0.05 * line_height),
),
DiffHunkStatus::Modified => (
hunk_hitbox.bounds,
cx.theme().status().modified,
Corners::all(0.05 * line_height),
),
DiffHunkStatus::Removed => (
hunk_hitbox.bounds,
cx.theme().status().deleted,
Corners::all(1. * line_height),
),
})
}
};
let scroll_position = layout.position_map.snapshot.scroll_position();
if let Some((hunk_bounds, background_color, corner_radii)) = hunk_to_paint {
cx.paint_quad(quad(
hunk_bounds,
corner_radii,
background_color,
Edges::default(),
transparent_black(),
));
}
}
});
}
fn diff_hunk_bounds(
snapshot: &EditorSnapshot,
line_height: Pixels,
bounds: Bounds<Pixels>,
hunk: &DisplayDiffHunk,
) -> Bounds<Pixels> {
let scroll_position = snapshot.scroll_position();
let scroll_top = scroll_position.y * line_height;
cx.paint_layer(layout.gutter_hitbox.bounds, |cx| {
for hunk in &layout.display_hunks {
let (display_row_range, status) = match hunk {
//TODO: This rendering is entirely a horrible hack
&DisplayDiffHunk::Folded { display_row: row } => {
let start_y = row as f32 * line_height - scroll_top;
let end_y = start_y + line_height;
let width = 0.275 * line_height;
let highlight_origin = layout.gutter_hitbox.origin + point(-width, start_y);
let highlight_size = size(width * 2., end_y - start_y);
let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
cx.paint_quad(quad(
highlight_bounds,
Corners::all(1. * line_height),
cx.theme().status().modified,
Edges::default(),
transparent_black(),
));
continue;
}
DisplayDiffHunk::Unfolded {
display_row_range,
status,
} => (display_row_range, status),
};
let color = match status {
DiffHunkStatus::Added => cx.theme().status().created,
DiffHunkStatus::Modified => cx.theme().status().modified,
//TODO: This rendering is entirely a horrible hack
DiffHunkStatus::Removed => {
let row = display_row_range.start;
let offset = line_height / 2.;
let start_y = row as f32 * line_height - offset - scroll_top;
let end_y = start_y + line_height;
let width = 0.275 * line_height;
let highlight_origin = layout.gutter_hitbox.origin + point(-width, start_y);
let highlight_size = size(width * 2., end_y - start_y);
let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
cx.paint_quad(quad(
highlight_bounds,
Corners::all(1. * line_height),
cx.theme().status().deleted,
Edges::default(),
transparent_black(),
));
continue;
}
};
let start_row = display_row_range.start;
let end_row = display_row_range.end;
// If we're in a multibuffer, row range span might include an
// excerpt header, so if we were to draw the marker straight away,
// the hunk might include the rows of that header.
// Making the range inclusive doesn't quite cut it, as we rely on the exclusivity for the soft wrap.
// Instead, we simply check whether the range we're dealing with includes
// any excerpt headers and if so, we stop painting the diff hunk on the first row of that header.
let end_row_in_current_excerpt = layout
.position_map
.snapshot
.blocks_in_range(start_row..end_row)
.find_map(|(start_row, block)| {
if matches!(block, TransformBlock::ExcerptHeader { .. }) {
Some(start_row)
} else {
None
}
})
.unwrap_or(end_row);
let start_y = start_row as f32 * line_height - scroll_top;
let end_y = end_row_in_current_excerpt as f32 * line_height - scroll_top;
match hunk {
DisplayDiffHunk::Folded { display_row, .. } => {
let start_y = *display_row as f32 * line_height - scroll_top;
let end_y = start_y + line_height;
let width = 0.275 * line_height;
let highlight_origin = layout.gutter_hitbox.origin + point(-width, start_y);
let highlight_origin = bounds.origin + point(-width, start_y);
let highlight_size = size(width * 2., end_y - start_y);
let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
cx.paint_quad(quad(
highlight_bounds,
Corners::all(0.05 * line_height),
color,
Edges::default(),
transparent_black(),
));
Bounds::new(highlight_origin, highlight_size)
}
})
DisplayDiffHunk::Unfolded {
display_row_range,
status,
..
} => match status {
DiffHunkStatus::Added | DiffHunkStatus::Modified => {
let start_row = display_row_range.start;
let end_row = display_row_range.end;
// If we're in a multibuffer, row range span might include an
// excerpt header, so if we were to draw the marker straight away,
// the hunk might include the rows of that header.
// Making the range inclusive doesn't quite cut it, as we rely on the exclusivity for the soft wrap.
// Instead, we simply check whether the range we're dealing with includes
// any excerpt headers and if so, we stop painting the diff hunk on the first row of that header.
let end_row_in_current_excerpt = snapshot
.blocks_in_range(start_row..end_row)
.find_map(|(start_row, block)| {
if matches!(block, TransformBlock::ExcerptHeader { .. }) {
Some(start_row)
} else {
None
}
})
.unwrap_or(end_row);
let start_y = start_row as f32 * line_height - scroll_top;
let end_y = end_row_in_current_excerpt as f32 * line_height - scroll_top;
let width = 0.275 * line_height;
let highlight_origin = bounds.origin + point(-width, start_y);
let highlight_size = size(width * 2., end_y - start_y);
Bounds::new(highlight_origin, highlight_size)
}
DiffHunkStatus::Removed => {
let row = display_row_range.start;
let offset = line_height / 2.;
let start_y = row as f32 * line_height - offset - scroll_top;
let end_y = start_y + line_height;
let width = 0.35 * line_height;
let highlight_origin = bounds.origin + point(-width, start_y);
let highlight_size = size(width * 2., end_y - start_y);
Bounds::new(highlight_origin, highlight_size)
}
},
}
}
fn paint_blamed_display_rows(&self, layout: &mut EditorLayout, cx: &mut WindowContext) {
@ -3009,14 +3079,22 @@ impl EditorElement {
}
};
let scroll_position = position_map.snapshot.scroll_position();
let x = (scroll_position.x * max_glyph_width
let current_scroll_position = position_map.snapshot.scroll_position();
let x = (current_scroll_position.x * max_glyph_width
- (delta.x * scroll_sensitivity))
/ max_glyph_width;
let y = (scroll_position.y * line_height - (delta.y * scroll_sensitivity))
let y = (current_scroll_position.y * line_height
- (delta.y * scroll_sensitivity))
/ line_height;
let scroll_position =
let mut scroll_position =
point(x, y).clamp(&point(0., 0.), &position_map.scroll_max);
let forbid_vertical_scroll = editor.scroll_manager.forbid_vertical_scroll();
if forbid_vertical_scroll {
scroll_position.y = current_scroll_position.y;
if scroll_position == current_scroll_position {
return;
}
}
editor.scroll(scroll_position, axis, cx);
cx.stop_propagation();
});
@ -3025,7 +3103,12 @@ impl EditorElement {
});
}
fn paint_mouse_listeners(&mut self, layout: &EditorLayout, cx: &mut WindowContext) {
fn paint_mouse_listeners(
&mut self,
layout: &EditorLayout,
hovered_hunk: Option<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]

View file

@ -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(),
}
}
}

View file

@ -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
}

View file

@ -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);
}

View file

@ -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 {

View file

@ -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()
}

View file

@ -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();
}

View file

@ -14,6 +14,7 @@ doctest = false
[dependencies]
anyhow.workspace = true
collections.workspace = true
editor.workspace = true
gpui.workspace = true
menu.workspace = true

View file

@ -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()
})
}

View file

@ -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();
}))

View file

@ -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

View file

@ -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"))]

View file

@ -13,6 +13,7 @@ path = "src/outline.rs"
doctest = false
[dependencies]
collections.workspace = true
editor.workspace = true
fuzzy.workspace = true
gpui.workspace = true

View file

@ -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()
})
}

View file

@ -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>,
}