mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +03:00
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:
parent
5831d80f51
commit
caa0d35b8b
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -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.)
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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]
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
623
crates/editor/src/hunk_diff.rs
Normal file
623
crates/editor/src/hunk_diff.rs
Normal 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
|
||||
}
|
@ -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>,
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user