mirror of
https://github.com/zed-industries/zed.git
synced 2026-07-09 16:00:35 +00:00
git: Side-by-side diff UI (#47349)
This PR implements a UI for the side-by-side diff, using blocks to align the two sides and adding a coherent `SplitEditorElement`. Release Notes: - N/A --------- Co-authored-by: cameron <cameron.studdstreet@gmail.com> Co-authored-by: Anthony Eid <hello@anthonyeid.me> Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>
This commit is contained in:
parent
7adb97acb9
commit
618f848c1f
30 changed files with 7882 additions and 888 deletions
|
|
@ -38,3 +38,7 @@ slow-timeout = { period = "300s", terminate-after = 1 }
|
|||
[[profile.default.overrides]]
|
||||
filter = 'package(vim) and (test(test_command_read) or test(test_capital_f_and_capital_t) or test(test_f_and_t) or test(test_change_paragraph_object) or test(test_change_surrounding_character_objects) or test(test_change_word_object) or test(test_delete_paragraph_object) or test(test_delete_surrounding_character_objects) or test(test_delete_word_object))'
|
||||
slow-timeout = { period = "300s", terminate-after = 1 }
|
||||
|
||||
[[profile.default.overrides]]
|
||||
filter = 'package(editor) and test(test_random_split_editor)'
|
||||
slow-timeout = { period = "300s", terminate-after = 1 }
|
||||
|
|
|
|||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -5516,6 +5516,7 @@ dependencies = [
|
|||
"ui",
|
||||
"unicode-script",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
"unindent",
|
||||
"url",
|
||||
"util",
|
||||
|
|
|
|||
|
|
@ -707,6 +707,7 @@ tracing = "0.1.40"
|
|||
unicase = "2.6"
|
||||
unicode-script = "0.5.7"
|
||||
unicode-segmentation = "1.10"
|
||||
unicode-width = "0.2"
|
||||
unindent = "0.2.0"
|
||||
url = "2.2"
|
||||
urlencoding = "2.1.2"
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ impl PendingDiff {
|
|||
diff.update_diff(
|
||||
text_snapshot.clone(),
|
||||
Some(base_text.clone()),
|
||||
false,
|
||||
None,
|
||||
language,
|
||||
cx,
|
||||
)
|
||||
|
|
@ -399,7 +399,7 @@ async fn build_buffer_diff(
|
|||
secondary_diff.update_diff(
|
||||
text_snapshot.clone(),
|
||||
Some(old_text),
|
||||
true,
|
||||
Some(false),
|
||||
language.clone(),
|
||||
cx,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -408,7 +408,7 @@ impl ActionLog {
|
|||
diff.update_diff(
|
||||
buffer_snapshot.clone(),
|
||||
Some(new_base_text),
|
||||
true,
|
||||
Some(true),
|
||||
language,
|
||||
cx,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -302,7 +302,7 @@ impl CodegenAlternative {
|
|||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
|
||||
let (old_buffer, _, _) = snapshot
|
||||
.range_to_buffer_ranges(range.clone())
|
||||
.range_to_buffer_ranges(range.start..=range.end)
|
||||
.pop()
|
||||
.unwrap();
|
||||
let old_buffer = cx.new(|cx| {
|
||||
|
|
@ -679,7 +679,7 @@ impl CodegenAlternative {
|
|||
let language_name = {
|
||||
let multibuffer = self.buffer.read(cx);
|
||||
let snapshot = multibuffer.snapshot(cx);
|
||||
let ranges = snapshot.range_to_buffer_ranges(self.range.clone());
|
||||
let ranges = snapshot.range_to_buffer_ranges(self.range.start..=self.range.end);
|
||||
ranges
|
||||
.first()
|
||||
.and_then(|(buffer, _, _)| buffer.language())
|
||||
|
|
|
|||
|
|
@ -1074,7 +1074,8 @@ impl InlineAssistant {
|
|||
let language_name = assist.editor.upgrade().and_then(|editor| {
|
||||
let multibuffer = editor.read(cx).buffer().read(cx);
|
||||
let snapshot = multibuffer.snapshot(cx);
|
||||
let ranges = snapshot.range_to_buffer_ranges(assist.range.clone());
|
||||
let ranges =
|
||||
snapshot.range_to_buffer_ranges(assist.range.start..=assist.range.end);
|
||||
ranges
|
||||
.first()
|
||||
.and_then(|(buffer, _, _)| buffer.language())
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -329,7 +329,7 @@ impl RatePredictionsModal {
|
|||
let update = diff.update_diff(
|
||||
new_buffer_snapshot.text.clone(),
|
||||
Some(old_buffer_snapshot.text().into()),
|
||||
true,
|
||||
Some(true),
|
||||
language,
|
||||
cx,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ tree-sitter-typescript.workspace = true
|
|||
tree-sitter-yaml.workspace = true
|
||||
tree-sitter-bash.workspace = true
|
||||
tree-sitter-md.workspace = true
|
||||
unicode-width.workspace = true
|
||||
unindent.workspace = true
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -37,6 +37,7 @@ mod rust_analyzer_ext;
|
|||
pub mod scroll;
|
||||
mod selections_collection;
|
||||
mod split;
|
||||
pub mod split_editor_view;
|
||||
pub mod tasks;
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -57,8 +58,8 @@ pub use editor_settings::{
|
|||
HideMouseMode, ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap,
|
||||
};
|
||||
pub use element::{
|
||||
CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
|
||||
render_breadcrumb_text,
|
||||
CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, OverlayPainter,
|
||||
OverlayPainterData, PointForPosition, render_breadcrumb_text,
|
||||
};
|
||||
pub use git::blame::BlameRenderer;
|
||||
pub use hover_popover::hover_markdown_style;
|
||||
|
|
@ -71,7 +72,8 @@ pub use multi_buffer::{
|
|||
MultiBufferOffset, MultiBufferOffsetUtf16, MultiBufferSnapshot, PathKey, RowInfo, ToOffset,
|
||||
ToPoint,
|
||||
};
|
||||
pub use split::SplittableEditor;
|
||||
pub use split::{SplittableEditor, ToggleLockedCursors, ToggleSplitDiff};
|
||||
pub use split_editor_view::SplitEditorView;
|
||||
pub use text::Bias;
|
||||
|
||||
use ::git::{Restore, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus};
|
||||
|
|
@ -1310,6 +1312,10 @@ pub struct Editor {
|
|||
folding_newlines: Task<()>,
|
||||
select_next_is_case_sensitive: Option<bool>,
|
||||
pub lookup_key: Option<Box<dyn Any + Send + Sync>>,
|
||||
scroll_companion: Option<WeakEntity<Editor>>,
|
||||
on_local_selections_changed:
|
||||
Option<Box<dyn Fn(Point, &mut Window, &mut Context<Self>) + 'static>>,
|
||||
suppress_selection_callback: bool,
|
||||
applicable_language_settings: HashMap<Option<LanguageName>, LanguageSettings>,
|
||||
accent_data: Option<AccentData>,
|
||||
fetched_tree_sitter_chunks: HashMap<ExcerptId, HashSet<Range<BufferRow>>>,
|
||||
|
|
@ -1808,7 +1814,7 @@ pub(crate) struct FocusedBlock {
|
|||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum JumpData {
|
||||
pub enum JumpData {
|
||||
MultiBufferRow {
|
||||
row: MultiBufferRow,
|
||||
line_offset_from_top: u32,
|
||||
|
|
@ -2515,6 +2521,9 @@ impl Editor {
|
|||
folding_newlines: Task::ready(()),
|
||||
lookup_key: None,
|
||||
select_next_is_case_sensitive: None,
|
||||
scroll_companion: None,
|
||||
on_local_selections_changed: None,
|
||||
suppress_selection_callback: false,
|
||||
applicable_language_settings: HashMap::default(),
|
||||
accent_data: None,
|
||||
fetched_tree_sitter_chunks: HashMap::default(),
|
||||
|
|
@ -3522,6 +3531,14 @@ impl Editor {
|
|||
}
|
||||
|
||||
self.blink_manager.update(cx, BlinkManager::pause_blinking);
|
||||
|
||||
if local && !self.suppress_selection_callback {
|
||||
if let Some(callback) = self.on_local_selections_changed.as_ref() {
|
||||
let cursor_position = self.selections.newest::<Point>(&display_map).head();
|
||||
callback(cursor_position, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
cx.emit(EditorEvent::SelectionsChanged { local });
|
||||
|
||||
let selections = &self.selections.disjoint_anchors_arc();
|
||||
|
|
@ -5544,7 +5561,7 @@ impl Editor {
|
|||
Bias::Left,
|
||||
);
|
||||
multi_buffer_snapshot
|
||||
.range_to_buffer_ranges(multi_buffer_visible_start..multi_buffer_visible_end)
|
||||
.range_to_buffer_ranges(multi_buffer_visible_start..=multi_buffer_visible_end)
|
||||
.into_iter()
|
||||
.filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
|
||||
.filter_map(|(buffer, excerpt_visible_range, excerpt_id)| {
|
||||
|
|
@ -7346,7 +7363,9 @@ impl Editor {
|
|||
}
|
||||
let match_task = cx.background_spawn(async move {
|
||||
let buffer_ranges = multi_buffer_snapshot
|
||||
.range_to_buffer_ranges(multi_buffer_range_to_query)
|
||||
.range_to_buffer_ranges(
|
||||
multi_buffer_range_to_query.start..=multi_buffer_range_to_query.end,
|
||||
)
|
||||
.into_iter()
|
||||
.filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty());
|
||||
let mut match_ranges = Vec::new();
|
||||
|
|
@ -8522,7 +8541,7 @@ impl Editor {
|
|||
..snapshot.display_point_to_point(DisplayPoint::new(range.end, 0), Bias::Right);
|
||||
|
||||
for (buffer_snapshot, range, excerpt_id) in
|
||||
multi_buffer_snapshot.range_to_buffer_ranges(range)
|
||||
multi_buffer_snapshot.range_to_buffer_ranges(range.start..=range.end)
|
||||
{
|
||||
let Some(buffer) = project
|
||||
.read(cx)
|
||||
|
|
@ -18743,7 +18762,7 @@ impl Editor {
|
|||
BTreeMap::new();
|
||||
for selection_range in selection_ranges {
|
||||
for (buffer, buffer_range, _) in
|
||||
snapshot.range_to_buffer_ranges(selection_range)
|
||||
snapshot.range_to_buffer_ranges(selection_range.start..=selection_range.end)
|
||||
{
|
||||
let buffer_id = buffer.remote_id();
|
||||
let start = buffer.anchor_before(buffer_range.start);
|
||||
|
|
@ -20779,7 +20798,6 @@ impl Editor {
|
|||
pub fn set_soft_wrap_mode(
|
||||
&mut self,
|
||||
mode: language_settings::SoftWrap,
|
||||
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.soft_wrap_mode_override = Some(mode);
|
||||
|
|
@ -21021,6 +21039,25 @@ impl Editor {
|
|||
self.delegate_expand_excerpts = delegate;
|
||||
}
|
||||
|
||||
pub fn set_scroll_companion(&mut self, companion: Option<WeakEntity<Editor>>) {
|
||||
self.scroll_companion = companion;
|
||||
}
|
||||
|
||||
pub fn scroll_companion(&self) -> Option<&WeakEntity<Editor>> {
|
||||
self.scroll_companion.as_ref()
|
||||
}
|
||||
|
||||
pub fn set_on_local_selections_changed(
|
||||
&mut self,
|
||||
callback: Option<Box<dyn Fn(Point, &mut Window, &mut Context<Self>) + 'static>>,
|
||||
) {
|
||||
self.on_local_selections_changed = callback;
|
||||
}
|
||||
|
||||
pub fn set_suppress_selection_callback(&mut self, suppress: bool) {
|
||||
self.suppress_selection_callback = suppress;
|
||||
}
|
||||
|
||||
pub fn set_show_git_diff_gutter(&mut self, show_git_diff_gutter: bool, cx: &mut Context<Self>) {
|
||||
self.show_git_diff_gutter = Some(show_git_diff_gutter);
|
||||
cx.notify();
|
||||
|
|
@ -22524,7 +22561,8 @@ impl Editor {
|
|||
|
||||
let multi_buffer = self.buffer().read(cx);
|
||||
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
|
||||
let buffer_ranges = multi_buffer_snapshot.range_to_buffer_ranges(selection_range);
|
||||
let buffer_ranges = multi_buffer_snapshot
|
||||
.range_to_buffer_ranges(selection_range.start..=selection_range.end);
|
||||
|
||||
let (buffer, range, _) = if selection.reversed {
|
||||
buffer_ranges.first()
|
||||
|
|
@ -22542,15 +22580,19 @@ impl Editor {
|
|||
};
|
||||
|
||||
let buffer_diff_snapshot = buffer_diff.read(cx).snapshot(cx);
|
||||
let (mut translated, _, _) = buffer_diff_snapshot.points_to_base_text_points(
|
||||
[
|
||||
Point::new(start_row_in_buffer, 0),
|
||||
Point::new(end_row_in_buffer, 0),
|
||||
],
|
||||
buffer,
|
||||
);
|
||||
let start_row = translated.next().unwrap().start.row;
|
||||
let end_row = translated.next().unwrap().end.row;
|
||||
|
||||
Some((
|
||||
multi_buffer.buffer(buffer.remote_id()).unwrap(),
|
||||
buffer_diff_snapshot.row_to_base_text_row(start_row_in_buffer, Bias::Left, buffer)
|
||||
..buffer_diff_snapshot.row_to_base_text_row(
|
||||
end_row_in_buffer,
|
||||
Bias::Left,
|
||||
buffer,
|
||||
),
|
||||
start_row..end_row,
|
||||
))
|
||||
});
|
||||
|
||||
|
|
@ -23839,7 +23881,7 @@ impl Editor {
|
|||
self.open_excerpts_common(None, false, window, cx)
|
||||
}
|
||||
|
||||
fn open_excerpts_common(
|
||||
pub(crate) fn open_excerpts_common(
|
||||
&mut self,
|
||||
jump_data: Option<JumpData>,
|
||||
split: bool,
|
||||
|
|
@ -24331,6 +24373,7 @@ impl Editor {
|
|||
modifiers: window.modifiers(),
|
||||
},
|
||||
&position_map,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
|
@ -24794,7 +24837,7 @@ impl Editor {
|
|||
self.active_diagnostics == ActiveDiagnostic::All || !self.mode().is_full()
|
||||
}
|
||||
|
||||
fn create_style(&self, cx: &App) -> EditorStyle {
|
||||
pub(crate) fn create_style(&self, cx: &App) -> EditorStyle {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
|
||||
let mut text_style = match self.mode {
|
||||
|
|
@ -25480,7 +25523,10 @@ impl NewlineConfig {
|
|||
buffer: &MultiBufferSnapshot,
|
||||
range: Range<MultiBufferOffset>,
|
||||
) -> bool {
|
||||
let (buffer, range) = match buffer.range_to_buffer_ranges(range).as_slice() {
|
||||
let (buffer, range) = match buffer
|
||||
.range_to_buffer_ranges(range.start..=range.end)
|
||||
.as_slice()
|
||||
{
|
||||
[(buffer, range, _)] => (*buffer, range.clone()),
|
||||
_ => return false,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ use gpui::{
|
|||
Pixels, PressureStage, ScrollDelta, ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString,
|
||||
Size, StatefulInteractiveElement, Style, Styled, StyledText, TextAlign, TextRun,
|
||||
TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, linear_color_stop,
|
||||
linear_gradient, outline, point, px, quad, relative, size, solid_background, transparent_black,
|
||||
linear_gradient, outline, pattern_slash, point, px, quad, relative, rgba, size,
|
||||
solid_background, transparent_black,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{IndentGuideSettings, language_settings::ShowWhitespaceSetting};
|
||||
|
|
@ -191,9 +192,29 @@ struct RenderBlocksOutput {
|
|||
resized_blocks: Option<HashMap<CustomBlockId, u32>>,
|
||||
}
|
||||
|
||||
/// Data passed to overlay painters during the paint phase.
|
||||
pub struct OverlayPainterData<'a> {
|
||||
pub editor: &'a Entity<Editor>,
|
||||
pub snapshot: &'a EditorSnapshot,
|
||||
pub scroll_position: gpui::Point<ScrollOffset>,
|
||||
pub line_height: Pixels,
|
||||
pub visible_row_range: Range<DisplayRow>,
|
||||
pub hitbox: &'a Hitbox,
|
||||
}
|
||||
|
||||
pub type OverlayPainter = Box<dyn FnOnce(OverlayPainterData<'_>, &mut Window, &mut App)>;
|
||||
|
||||
pub struct EditorElement {
|
||||
editor: Entity<Editor>,
|
||||
style: EditorStyle,
|
||||
split_side: Option<SplitSide>,
|
||||
overlay_painter: Option<OverlayPainter>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SplitSide {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl EditorElement {
|
||||
|
|
@ -203,9 +224,23 @@ impl EditorElement {
|
|||
Self {
|
||||
editor: editor.clone(),
|
||||
style,
|
||||
split_side: None,
|
||||
overlay_painter: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_split_side(&mut self, side: SplitSide) {
|
||||
self.split_side = Some(side);
|
||||
}
|
||||
|
||||
pub fn set_overlay_painter(&mut self, painter: OverlayPainter) {
|
||||
self.overlay_painter = Some(painter);
|
||||
}
|
||||
|
||||
fn should_show_buffer_headers(&self) -> bool {
|
||||
self.split_side.is_none()
|
||||
}
|
||||
|
||||
fn register_actions(&self, window: &mut Window, cx: &mut App) {
|
||||
let editor = &self.editor;
|
||||
editor.update(cx, |editor, cx| {
|
||||
|
|
@ -1201,6 +1236,7 @@ impl EditorElement {
|
|||
editor: &mut Editor,
|
||||
event: &MouseMoveEvent,
|
||||
position_map: &PositionMap,
|
||||
split_side: Option<SplitSide>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
|
|
@ -1328,7 +1364,10 @@ impl EditorElement {
|
|||
indicator.is_active && indicator.display_row == valid_point.row()
|
||||
});
|
||||
|
||||
let breakpoint_indicator = if gutter_hovered && !is_on_diff_review_button_row {
|
||||
let breakpoint_indicator = if gutter_hovered
|
||||
&& !is_on_diff_review_button_row
|
||||
&& split_side != Some(SplitSide::Left)
|
||||
{
|
||||
let buffer_anchor = position_map
|
||||
.snapshot
|
||||
.display_point_to_anchor(valid_point, Bias::Left);
|
||||
|
|
@ -1908,6 +1947,10 @@ impl EditorElement {
|
|||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<EditorScrollbars> {
|
||||
if self.split_side == Some(SplitSide::Left) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let show_scrollbars = self.editor.read(cx).show_scrollbars;
|
||||
if (!show_scrollbars.horizontal && !show_scrollbars.vertical)
|
||||
|| self.style.scrollbar_width.is_zero()
|
||||
|
|
@ -2466,6 +2509,11 @@ impl EditorElement {
|
|||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyElement> {
|
||||
// Don't show code actions in split diff view
|
||||
if self.split_side.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !snapshot
|
||||
.show_code_actions
|
||||
.unwrap_or(EditorSettings::get_global(cx).inline_code_actions)
|
||||
|
|
@ -3045,6 +3093,10 @@ impl EditorElement {
|
|||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Vec<AnyElement> {
|
||||
if self.split_side == Some(SplitSide::Left) {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
breakpoints
|
||||
.into_iter()
|
||||
|
|
@ -3145,6 +3197,10 @@ impl EditorElement {
|
|||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Vec<AnyElement> {
|
||||
if self.split_side == Some(SplitSide::Left) {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let active_task_indicator_row =
|
||||
// TODO: add edit button on the right side of each row in the context menu
|
||||
|
|
@ -3848,18 +3904,18 @@ impl EditorElement {
|
|||
height,
|
||||
..
|
||||
} => {
|
||||
let selected = selected_buffer_ids.contains(&first_excerpt.buffer_id);
|
||||
let result = v_flex().id(block_id).w_full().pr(editor_margins.right);
|
||||
let mut result = v_flex().id(block_id).w_full().pr(editor_margins.right);
|
||||
|
||||
let jump_data = header_jump_data(
|
||||
snapshot,
|
||||
block_row_start,
|
||||
*height,
|
||||
first_excerpt,
|
||||
latest_selection_anchors,
|
||||
);
|
||||
result
|
||||
.child(self.render_buffer_header(
|
||||
if self.should_show_buffer_headers() {
|
||||
let selected = selected_buffer_ids.contains(&first_excerpt.buffer_id);
|
||||
let jump_data = header_jump_data(
|
||||
snapshot,
|
||||
block_row_start,
|
||||
*height,
|
||||
first_excerpt,
|
||||
latest_selection_anchors,
|
||||
);
|
||||
result = result.child(self.render_buffer_header(
|
||||
first_excerpt,
|
||||
true,
|
||||
selected,
|
||||
|
|
@ -3867,8 +3923,13 @@ impl EditorElement {
|
|||
jump_data,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.into_any_element()
|
||||
));
|
||||
} else {
|
||||
result =
|
||||
result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height()));
|
||||
}
|
||||
|
||||
result.into_any_element()
|
||||
}
|
||||
|
||||
Block::ExcerptBoundary { .. } => {
|
||||
|
|
@ -3892,22 +3953,27 @@ impl EditorElement {
|
|||
Block::BufferHeader { excerpt, height } => {
|
||||
let mut result = v_flex().id(block_id).w_full();
|
||||
|
||||
let jump_data = header_jump_data(
|
||||
snapshot,
|
||||
block_row_start,
|
||||
*height,
|
||||
excerpt,
|
||||
latest_selection_anchors,
|
||||
);
|
||||
if self.should_show_buffer_headers() {
|
||||
let jump_data = header_jump_data(
|
||||
snapshot,
|
||||
block_row_start,
|
||||
*height,
|
||||
excerpt,
|
||||
latest_selection_anchors,
|
||||
);
|
||||
|
||||
if sticky_header_excerpt_id != Some(excerpt.id) {
|
||||
let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
|
||||
if sticky_header_excerpt_id != Some(excerpt.id) {
|
||||
let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
|
||||
|
||||
result = result.child(div().pr(editor_margins.right).child(
|
||||
self.render_buffer_header(
|
||||
excerpt, false, selected, false, jump_data, window, cx,
|
||||
),
|
||||
));
|
||||
result = result.child(div().pr(editor_margins.right).child(
|
||||
self.render_buffer_header(
|
||||
excerpt, false, selected, false, jump_data, window, cx,
|
||||
),
|
||||
));
|
||||
} else {
|
||||
result =
|
||||
result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height()));
|
||||
}
|
||||
} else {
|
||||
result =
|
||||
result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height()));
|
||||
|
|
@ -3915,6 +3981,13 @@ impl EditorElement {
|
|||
|
||||
result.into_any()
|
||||
}
|
||||
|
||||
Block::Spacer { height, .. } => div()
|
||||
.id(block_id)
|
||||
.w_full()
|
||||
.h((*height as f32) * line_height)
|
||||
.bg(pattern_slash(rgba(0xFFFFFF10), 8.0, 8.0))
|
||||
.into_any(),
|
||||
};
|
||||
|
||||
// Discover the element's content height, then round up to the nearest multiple of line height.
|
||||
|
|
@ -6613,7 +6686,7 @@ impl EditorElement {
|
|||
GitGutterSetting::TrackedFiles
|
||||
)
|
||||
});
|
||||
if show_git_gutter {
|
||||
if show_git_gutter && self.split_side.is_none() {
|
||||
Self::paint_gutter_diff_hunks(layout, window, cx)
|
||||
}
|
||||
|
||||
|
|
@ -7949,6 +8022,7 @@ impl EditorElement {
|
|||
window.on_mouse_event({
|
||||
let position_map = layout.position_map.clone();
|
||||
let editor = self.editor.clone();
|
||||
let split_side = self.split_side;
|
||||
|
||||
move |event: &MouseMoveEvent, phase, window, cx| {
|
||||
if phase == DispatchPhase::Bubble {
|
||||
|
|
@ -7962,7 +8036,7 @@ impl EditorElement {
|
|||
Self::mouse_dragged(editor, event, &position_map, window, cx)
|
||||
}
|
||||
|
||||
Self::mouse_moved(editor, event, &position_map, window, cx)
|
||||
Self::mouse_moved(editor, event, &position_map, split_side, window, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -8013,7 +8087,7 @@ impl EditorElement {
|
|||
}
|
||||
let buffer_snapshot = &display_snapshot.buffer_snapshot();
|
||||
for (buffer, buffer_range, excerpt_id) in
|
||||
buffer_snapshot.range_to_buffer_ranges(anchor_range)
|
||||
buffer_snapshot.range_to_buffer_ranges(anchor_range.start..=anchor_range.end)
|
||||
{
|
||||
let buffer_range =
|
||||
buffer.anchor_after(buffer_range.start)..buffer.anchor_before(buffer_range.end);
|
||||
|
|
@ -8264,7 +8338,7 @@ fn file_status_label_color(file_status: Option<FileStatus>) -> Color {
|
|||
})
|
||||
}
|
||||
|
||||
fn header_jump_data(
|
||||
pub(crate) fn header_jump_data(
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
block_row_start: DisplayRow,
|
||||
height: u32,
|
||||
|
|
@ -9528,6 +9602,38 @@ impl Element for EditorElement {
|
|||
}
|
||||
};
|
||||
|
||||
// When jumping from one side of a side-by-side diff to the
|
||||
// other, we autoscroll autoscroll to keep the target range in view.
|
||||
//
|
||||
// If our scroll companion has a pending autoscroll request, process it
|
||||
// first so that both editors render with synchronized scroll positions.
|
||||
// This is important for split diff views where one editor may prepaint
|
||||
// before the other.
|
||||
if let Some(companion) = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.scroll_companion()
|
||||
.and_then(|c| c.upgrade())
|
||||
{
|
||||
if companion.read(cx).scroll_manager.has_autoscroll_request() {
|
||||
companion.update(cx, |companion_editor, cx| {
|
||||
let companion_autoscroll_request =
|
||||
companion_editor.scroll_manager.take_autoscroll_request();
|
||||
companion_editor.autoscroll_vertically(
|
||||
bounds,
|
||||
line_height,
|
||||
max_scroll_top,
|
||||
companion_autoscroll_request,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
snapshot = self
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.snapshot(window, cx));
|
||||
}
|
||||
}
|
||||
|
||||
let (
|
||||
autoscroll_request,
|
||||
autoscroll_containing_element,
|
||||
|
|
@ -10070,23 +10176,27 @@ impl Element for EditorElement {
|
|||
}
|
||||
}
|
||||
|
||||
let sticky_buffer_header = sticky_header_excerpt.map(|sticky_header_excerpt| {
|
||||
window.with_element_namespace("blocks", |window| {
|
||||
self.layout_sticky_buffer_header(
|
||||
sticky_header_excerpt,
|
||||
scroll_position,
|
||||
line_height,
|
||||
right_margin,
|
||||
&snapshot,
|
||||
&hitbox,
|
||||
&selected_buffer_ids,
|
||||
&blocks,
|
||||
&latest_selection_anchors,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
let sticky_buffer_header = if self.should_show_buffer_headers() {
|
||||
sticky_header_excerpt.map(|sticky_header_excerpt| {
|
||||
window.with_element_namespace("blocks", |window| {
|
||||
self.layout_sticky_buffer_header(
|
||||
sticky_header_excerpt,
|
||||
scroll_position,
|
||||
line_height,
|
||||
right_margin,
|
||||
&snapshot,
|
||||
&hitbox,
|
||||
&selected_buffer_ids,
|
||||
&blocks,
|
||||
&latest_selection_anchors,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
});
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let start_buffer_row =
|
||||
MultiBufferRow(start_anchor.to_point(&snapshot.buffer_snapshot()).row);
|
||||
|
|
@ -10753,6 +10863,18 @@ impl Element for EditorElement {
|
|||
self.paint_scrollbars(layout, window, cx);
|
||||
self.paint_edit_prediction_popover(layout, window, cx);
|
||||
self.paint_mouse_context_menu(layout, window, cx);
|
||||
|
||||
if let Some(overlay_painter) = self.overlay_painter.take() {
|
||||
let data = OverlayPainterData {
|
||||
editor: &self.editor,
|
||||
snapshot: &layout.position_map.snapshot,
|
||||
scroll_position: layout.position_map.snapshot.scroll_position(),
|
||||
line_height: layout.position_map.line_height,
|
||||
visible_row_range: layout.visible_display_row_range.clone(),
|
||||
hitbox: &layout.hitbox,
|
||||
};
|
||||
overlay_painter(data, window, cx);
|
||||
}
|
||||
});
|
||||
})
|
||||
})
|
||||
|
|
@ -11563,15 +11685,15 @@ impl PositionMap {
|
|||
}
|
||||
}
|
||||
|
||||
struct BlockLayout {
|
||||
id: BlockId,
|
||||
x_offset: Pixels,
|
||||
row: Option<DisplayRow>,
|
||||
element: AnyElement,
|
||||
available_space: Size<AvailableSpace>,
|
||||
style: BlockStyle,
|
||||
overlaps_gutter: bool,
|
||||
is_buffer_header: bool,
|
||||
pub(crate) struct BlockLayout {
|
||||
pub(crate) id: BlockId,
|
||||
pub(crate) x_offset: Pixels,
|
||||
pub(crate) row: Option<DisplayRow>,
|
||||
pub(crate) element: AnyElement,
|
||||
pub(crate) available_space: Size<AvailableSpace>,
|
||||
pub(crate) style: BlockStyle,
|
||||
pub(crate) overlaps_gutter: bool,
|
||||
pub(crate) is_buffer_header: bool,
|
||||
}
|
||||
|
||||
pub fn layout_line(
|
||||
|
|
|
|||
|
|
@ -365,6 +365,10 @@ impl ScrollManager {
|
|||
self.show_scrollbars
|
||||
}
|
||||
|
||||
pub fn has_autoscroll_request(&self) -> bool {
|
||||
self.autoscroll_request.is_some()
|
||||
}
|
||||
|
||||
pub fn take_autoscroll_request(&mut self) -> Option<(Autoscroll, bool)> {
|
||||
self.autoscroll_request.take()
|
||||
}
|
||||
|
|
@ -588,14 +592,30 @@ impl Editor {
|
|||
cx: &mut Context<Self>,
|
||||
) -> WasScrolled {
|
||||
let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
self.set_scroll_position_taking_display_map(
|
||||
let was_scrolled = self.set_scroll_position_taking_display_map(
|
||||
scroll_position,
|
||||
local,
|
||||
autoscroll,
|
||||
map,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
);
|
||||
|
||||
if local && was_scrolled.0 {
|
||||
if let Some(companion) = self.scroll_companion.as_ref().and_then(|c| c.upgrade()) {
|
||||
companion.update(cx, |companion_editor, cx| {
|
||||
companion_editor.set_scroll_position_internal(
|
||||
scroll_position,
|
||||
false,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
was_scrolled
|
||||
}
|
||||
|
||||
fn set_scroll_position_taking_display_map(
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
1086
crates/editor/src/split_editor_view.rs
Normal file
1086
crates/editor/src/split_editor_view.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -176,7 +176,15 @@ pub fn block_content_for_tests(
|
|||
}
|
||||
|
||||
pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestContext) -> String {
|
||||
let draw_size = size(px(3000.0), px(3000.0));
|
||||
editor_content_with_blocks_and_width(editor, px(3000.), cx)
|
||||
}
|
||||
|
||||
pub fn editor_content_with_blocks_and_width(
|
||||
editor: &Entity<Editor>,
|
||||
width: Pixels,
|
||||
cx: &mut VisualTestContext,
|
||||
) -> String {
|
||||
let draw_size = size(width, px(3000.0));
|
||||
cx.simulate_resize(draw_size);
|
||||
cx.draw(gpui::Point::default(), draw_size, |_, _| editor.clone());
|
||||
let (snapshot, mut lines, blocks) = editor.update_in(cx, |editor, window, cx| {
|
||||
|
|
@ -224,7 +232,14 @@ pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestCo
|
|||
height,
|
||||
} => {
|
||||
lines[row.0 as usize].push_str(&cx.update(|_, cx| {
|
||||
format!("§ {}", first_excerpt.buffer.file().unwrap().file_name(cx))
|
||||
format!(
|
||||
"§ {}",
|
||||
first_excerpt
|
||||
.buffer
|
||||
.file()
|
||||
.map(|file| file.file_name(cx))
|
||||
.unwrap_or("<no file>")
|
||||
)
|
||||
}));
|
||||
for row in row.0 + 1..row.0 + height {
|
||||
lines[row as usize].push_str("§ -----");
|
||||
|
|
@ -236,15 +251,28 @@ pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestCo
|
|||
}
|
||||
}
|
||||
Block::BufferHeader { excerpt, height } => {
|
||||
lines[row.0 as usize].push_str(
|
||||
&cx.update(|_, cx| {
|
||||
format!("§ {}", excerpt.buffer.file().unwrap().file_name(cx))
|
||||
}),
|
||||
);
|
||||
lines[row.0 as usize].push_str(&cx.update(|_, cx| {
|
||||
format!(
|
||||
"§ {}",
|
||||
excerpt
|
||||
.buffer
|
||||
.file()
|
||||
.map(|file| file.file_name(cx))
|
||||
.unwrap_or("<no file>")
|
||||
)
|
||||
}));
|
||||
for row in row.0 + 1..row.0 + height {
|
||||
lines[row as usize].push_str("§ -----");
|
||||
}
|
||||
}
|
||||
Block::Spacer { height, .. } => {
|
||||
for row in row.0..row.0 + height {
|
||||
while lines.len() <= row as usize {
|
||||
lines.push(String::new());
|
||||
}
|
||||
lines[row as usize].push_str("§ spacer");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lines.join("\n")
|
||||
|
|
|
|||
|
|
@ -884,7 +884,7 @@ async fn build_buffer_diff(
|
|||
diff.update_diff(
|
||||
buffer.text.clone(),
|
||||
old_text.map(|old_text| Arc::from(old_text.as_str())),
|
||||
true,
|
||||
Some(true),
|
||||
language.clone(),
|
||||
cx,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ async fn build_buffer_diff(
|
|||
diff.update_diff(
|
||||
new_buffer_snapshot.text.clone(),
|
||||
Some(old_buffer_snapshot.text().into()),
|
||||
true,
|
||||
Some(true),
|
||||
new_buffer_snapshot.language().cloned(),
|
||||
cx,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use anyhow::{Context as _, Result, anyhow};
|
|||
use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
Addon, Editor, EditorEvent, SelectionEffects, SplittableEditor,
|
||||
Addon, Editor, EditorEvent, SelectionEffects, SplittableEditor, ToggleSplitDiff,
|
||||
actions::{GoToHunk, GoToPreviousHunk, SendReviewToAgent},
|
||||
multibuffer_context_lines,
|
||||
scroll::Autoscroll,
|
||||
|
|
@ -477,6 +477,7 @@ impl ProjectDiff {
|
|||
}
|
||||
|
||||
fn button_states(&self, cx: &App) -> ButtonStates {
|
||||
let is_split = self.editor.read(cx).is_split();
|
||||
let editor = self.editor.read(cx).primary_editor().read(cx);
|
||||
let snapshot = self.multibuffer.read(cx).snapshot(cx);
|
||||
let prev_next = snapshot.diff_hunks().nth(1).is_some();
|
||||
|
|
@ -537,6 +538,7 @@ impl ProjectDiff {
|
|||
selection,
|
||||
stage_all,
|
||||
unstage_all,
|
||||
is_split,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1293,6 +1295,7 @@ struct ButtonStates {
|
|||
selection: bool,
|
||||
stage_all: bool,
|
||||
unstage_all: bool,
|
||||
is_split: bool,
|
||||
}
|
||||
|
||||
impl Render for ProjectDiffToolbar {
|
||||
|
|
@ -1432,6 +1435,24 @@ impl Render for ProjectDiffToolbar {
|
|||
)
|
||||
},
|
||||
)
|
||||
.child(
|
||||
Button::new(
|
||||
"toggle-split",
|
||||
if button_states.is_split {
|
||||
"Stacked View"
|
||||
} else {
|
||||
"Split View"
|
||||
},
|
||||
)
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Toggle Split View",
|
||||
&ToggleSplitDiff,
|
||||
&focus_handle,
|
||||
))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(&ToggleSplitDiff, window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("commit", "Commit")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ async fn update_diff_buffer(
|
|||
diff.update_diff(
|
||||
source_buffer_snapshot.text.clone(),
|
||||
Some(Arc::from(base_text.as_str())),
|
||||
true,
|
||||
Some(true),
|
||||
language.clone(),
|
||||
cx,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -734,14 +734,14 @@ impl Default for Background {
|
|||
}
|
||||
|
||||
/// Creates a hash pattern background
|
||||
pub fn pattern_slash(color: Hsla, width: f32, interval: f32) -> Background {
|
||||
pub fn pattern_slash(color: impl Into<Hsla>, width: f32, interval: f32) -> Background {
|
||||
let width_scaled = (width * 255.0) as u32;
|
||||
let interval_scaled = (interval * 255.0) as u32;
|
||||
let height = ((width_scaled * 0xFFFF) + interval_scaled) as f32;
|
||||
|
||||
Background {
|
||||
tag: BackgroundTag::PatternSlash,
|
||||
solid: color,
|
||||
solid: color.into(),
|
||||
gradient_angle_or_pattern_height: height,
|
||||
..Default::default()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -586,7 +586,7 @@ pub struct Chunk<'a> {
|
|||
}
|
||||
|
||||
/// A set of edits to a given version of a buffer, computed asynchronously.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Diff {
|
||||
pub base_version: clock::Global,
|
||||
pub line_ending: LineEnding,
|
||||
|
|
@ -2148,11 +2148,15 @@ impl Buffer {
|
|||
|
||||
/// Spawns a background task that asynchronously computes a `Diff` between the buffer's text
|
||||
/// and the given new text.
|
||||
pub fn diff(&self, mut new_text: String, cx: &App) -> Task<Diff> {
|
||||
pub fn diff<T>(&self, new_text: T, cx: &App) -> Task<Diff>
|
||||
where
|
||||
T: AsRef<str> + Send + 'static,
|
||||
{
|
||||
let old_text = self.as_rope().clone();
|
||||
let base_version = self.version();
|
||||
cx.background_spawn(async move {
|
||||
let old_text = old_text.to_string();
|
||||
let mut new_text = new_text.as_ref().to_owned();
|
||||
let line_ending = LineEnding::detect(&new_text);
|
||||
LineEnding::normalize(&mut new_text);
|
||||
let edits = text_diff(&old_text, &new_text);
|
||||
|
|
|
|||
|
|
@ -259,7 +259,7 @@ impl SyntaxTreeView {
|
|||
let multi_buffer = editor.buffer().read(cx);
|
||||
let (buffer, range, excerpt_id) = snapshot
|
||||
.buffer_snapshot()
|
||||
.range_to_buffer_ranges(selection_range)
|
||||
.range_to_buffer_ranges(selection_range.start..=selection_range.end)
|
||||
.pop()?;
|
||||
let buffer = multi_buffer.buffer(buffer.remote_id()).unwrap();
|
||||
Some((buffer, range, excerpt_id))
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ pub use anchor::{Anchor, AnchorRangeExt};
|
|||
|
||||
use anyhow::{Result, anyhow};
|
||||
use buffer_diff::{
|
||||
BufferDiff, BufferDiffEvent, BufferDiffSnapshot, DiffHunk, DiffHunkSecondaryStatus,
|
||||
DiffHunkStatus, DiffHunkStatusKind,
|
||||
BufferDiff, BufferDiffEvent, BufferDiffSnapshot, DiffChanged, DiffHunk,
|
||||
DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind,
|
||||
};
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, Bound, HashMap, HashSet};
|
||||
|
|
@ -537,12 +537,19 @@ impl DiffState {
|
|||
fn new(diff: Entity<BufferDiff>, cx: &mut Context<MultiBuffer>) -> Self {
|
||||
DiffState {
|
||||
_subscription: cx.subscribe(&diff, |this, diff, event, cx| match event {
|
||||
BufferDiffEvent::DiffChanged {
|
||||
BufferDiffEvent::DiffChanged(DiffChanged {
|
||||
changed_range,
|
||||
base_text_changed_range: _,
|
||||
} => {
|
||||
if let Some(changed_range) = changed_range.clone() {
|
||||
this.buffer_diff_changed(diff, changed_range, cx)
|
||||
extended_range,
|
||||
}) => {
|
||||
let use_extended = this.snapshot.borrow().use_extended_diff_range;
|
||||
let range = if use_extended {
|
||||
extended_range.clone()
|
||||
} else {
|
||||
changed_range.clone()
|
||||
};
|
||||
if let Some(range) = range {
|
||||
this.buffer_diff_changed(diff, range, cx)
|
||||
}
|
||||
cx.emit(Event::BufferDiffChanged);
|
||||
}
|
||||
|
|
@ -564,10 +571,11 @@ impl DiffState {
|
|||
_subscription: cx.subscribe(&diff, {
|
||||
let main_buffer = main_buffer.clone();
|
||||
move |this, diff, event, cx| match event {
|
||||
BufferDiffEvent::DiffChanged {
|
||||
BufferDiffEvent::DiffChanged(DiffChanged {
|
||||
changed_range: _,
|
||||
base_text_changed_range,
|
||||
} => {
|
||||
extended_range: _,
|
||||
}) => {
|
||||
if let Some(base_text_changed_range) = base_text_changed_range.clone() {
|
||||
this.inverted_buffer_diff_changed(
|
||||
diff,
|
||||
|
|
@ -609,6 +617,7 @@ pub struct MultiBufferSnapshot {
|
|||
trailing_excerpt_update_count: usize,
|
||||
all_diff_hunks_expanded: bool,
|
||||
show_deleted_hunks: bool,
|
||||
use_extended_diff_range: bool,
|
||||
show_headers: bool,
|
||||
}
|
||||
|
||||
|
|
@ -1903,6 +1912,7 @@ impl MultiBuffer {
|
|||
trailing_excerpt_update_count,
|
||||
all_diff_hunks_expanded: _,
|
||||
show_deleted_hunks: _,
|
||||
use_extended_diff_range: _,
|
||||
show_headers: _,
|
||||
} = self.snapshot.get_mut();
|
||||
let start = ExcerptDimension(MultiBufferOffset::ZERO);
|
||||
|
|
@ -2367,7 +2377,6 @@ impl MultiBuffer {
|
|||
.get(&buffer_id)
|
||||
.is_none_or(|old_diff| !new_diff.base_texts_definitely_eq(old_diff));
|
||||
snapshot.diffs.insert_or_replace(buffer_id, new_diff);
|
||||
self.buffer_changed_since_sync.replace(true);
|
||||
|
||||
let buffer = buffer_state.buffer.read(cx);
|
||||
let diff_change_range = range.to_offset(buffer);
|
||||
|
|
@ -2648,6 +2657,10 @@ impl MultiBuffer {
|
|||
self.expand_or_collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], true, cx);
|
||||
}
|
||||
|
||||
pub fn set_use_extended_diff_range(&mut self, use_extended: bool, _cx: &mut Context<Self>) {
|
||||
self.snapshot.get_mut().use_extended_diff_range = use_extended;
|
||||
}
|
||||
|
||||
pub fn has_multiple_hunks(&self, cx: &App) -> bool {
|
||||
self.read(cx)
|
||||
.diff_hunks_in_range(Anchor::min()..Anchor::max())
|
||||
|
|
@ -2988,6 +3001,7 @@ impl MultiBuffer {
|
|||
trailing_excerpt_update_count: _,
|
||||
all_diff_hunks_expanded: _,
|
||||
show_deleted_hunks: _,
|
||||
use_extended_diff_range: _,
|
||||
show_headers: _,
|
||||
} = snapshot;
|
||||
*is_dirty = false;
|
||||
|
|
@ -3117,6 +3131,9 @@ impl MultiBuffer {
|
|||
&& let Some((old_diff, old_main_buffer, new_main_buffer)) =
|
||||
inverted_diff_touch_info.get(locator)
|
||||
{
|
||||
// TODO(split-diff) this iterates over all excerpts for all edited buffers;
|
||||
// it would be nice to be able to skip excerpts that weren't edited using
|
||||
// new_main_buffer.has_edits_since_in_range.
|
||||
let excerpt_buffer_start = old_excerpt
|
||||
.range
|
||||
.context
|
||||
|
|
@ -3124,13 +3141,27 @@ impl MultiBuffer {
|
|||
.to_offset(&old_excerpt.buffer);
|
||||
let excerpt_buffer_end = excerpt_buffer_start + old_excerpt.text_summary.len;
|
||||
|
||||
for hunk in old_diff.hunks_intersecting_base_text_range(
|
||||
excerpt_buffer_start..excerpt_buffer_end,
|
||||
old_main_buffer,
|
||||
) {
|
||||
if hunk.buffer_range.start.is_valid(new_main_buffer) {
|
||||
continue;
|
||||
}
|
||||
let mut hunks = old_diff
|
||||
.hunks_intersecting_base_text_range(
|
||||
excerpt_buffer_start..excerpt_buffer_end,
|
||||
old_main_buffer,
|
||||
)
|
||||
.chain(old_diff.hunks_intersecting_base_text_range(
|
||||
excerpt_buffer_start..excerpt_buffer_end,
|
||||
new_main_buffer,
|
||||
))
|
||||
.filter(|hunk| {
|
||||
hunk.buffer_range.start.is_valid(&old_main_buffer)
|
||||
!= hunk.buffer_range.start.is_valid(&new_main_buffer)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
hunks.sort_by(|l, r| {
|
||||
l.diff_base_byte_range
|
||||
.start
|
||||
.cmp(&r.diff_base_byte_range.start)
|
||||
});
|
||||
|
||||
for hunk in hunks {
|
||||
let hunk_buffer_start = hunk.diff_base_byte_range.start;
|
||||
if hunk_buffer_start >= excerpt_buffer_start
|
||||
&& hunk_buffer_start <= excerpt_buffer_end
|
||||
|
|
@ -3406,6 +3437,7 @@ impl MultiBuffer {
|
|||
edit_buffer_start..edit_buffer_end,
|
||||
main_buffer,
|
||||
) {
|
||||
did_expand_hunks = true;
|
||||
let hunk_buffer_range = hunk.diff_base_byte_range.clone();
|
||||
if hunk_buffer_range.start < excerpt_buffer_start {
|
||||
log::trace!("skipping hunk that starts before excerpt");
|
||||
|
|
@ -4081,27 +4113,52 @@ impl MultiBufferSnapshot {
|
|||
&self,
|
||||
ranges: impl Iterator<Item = Range<T>>,
|
||||
) -> impl Iterator<Item = (&BufferSnapshot, Range<BufferOffset>, ExcerptId)> {
|
||||
ranges.flat_map(|range| self.range_to_buffer_ranges(range).into_iter())
|
||||
ranges.flat_map(|range| {
|
||||
self.range_to_buffer_ranges((Bound::Included(range.start), Bound::Included(range.end)))
|
||||
.into_iter()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn range_to_buffer_ranges<T: ToOffset>(
|
||||
pub fn range_to_buffer_ranges<R, T>(
|
||||
&self,
|
||||
range: Range<T>,
|
||||
) -> Vec<(&BufferSnapshot, Range<BufferOffset>, ExcerptId)> {
|
||||
let start = range.start.to_offset(self);
|
||||
let end = range.end.to_offset(self);
|
||||
range: R,
|
||||
) -> Vec<(&BufferSnapshot, Range<BufferOffset>, ExcerptId)>
|
||||
where
|
||||
R: RangeBounds<T>,
|
||||
T: ToOffset,
|
||||
{
|
||||
let start = match range.start_bound() {
|
||||
Bound::Included(start) => start.to_offset(self),
|
||||
Bound::Excluded(_) => panic!("excluded start bound not supported"),
|
||||
Bound::Unbounded => MultiBufferOffset::ZERO,
|
||||
};
|
||||
let end_bound = match range.end_bound() {
|
||||
Bound::Included(end) => Bound::Included(end.to_offset(self)),
|
||||
Bound::Excluded(end) => Bound::Excluded(end.to_offset(self)),
|
||||
Bound::Unbounded => Bound::Unbounded,
|
||||
};
|
||||
let bounds = (Bound::Included(start), end_bound);
|
||||
|
||||
let mut cursor = self.cursor::<MultiBufferOffset, BufferOffset>();
|
||||
cursor.seek(&start);
|
||||
|
||||
let mut result: Vec<(&BufferSnapshot, Range<BufferOffset>, ExcerptId)> = Vec::new();
|
||||
while let Some(region) = cursor.region() {
|
||||
if region.range.start > end {
|
||||
let dominated_by_end_bound = match end_bound {
|
||||
Bound::Included(end) => region.range.start > end,
|
||||
Bound::Excluded(end) => region.range.start >= end,
|
||||
Bound::Unbounded => false,
|
||||
};
|
||||
if dominated_by_end_bound {
|
||||
break;
|
||||
}
|
||||
if region.is_main_buffer {
|
||||
let start_overshoot = start.saturating_sub(region.range.start);
|
||||
let end_overshoot = end.saturating_sub(region.range.start);
|
||||
let end_offset = match end_bound {
|
||||
Bound::Included(end) | Bound::Excluded(end) => end,
|
||||
Bound::Unbounded => region.range.end,
|
||||
};
|
||||
let end_overshoot = end_offset.saturating_sub(region.range.start);
|
||||
let start = region
|
||||
.buffer_range
|
||||
.end
|
||||
|
|
@ -4120,6 +4177,20 @@ impl MultiBufferSnapshot {
|
|||
}
|
||||
cursor.next();
|
||||
}
|
||||
|
||||
if let Some(excerpt) = cursor.excerpt() {
|
||||
let dominated_by_prev_excerpt =
|
||||
result.last().is_some_and(|(_, _, id)| *id == excerpt.id);
|
||||
if !dominated_by_prev_excerpt && excerpt.text_summary.len == 0 {
|
||||
let excerpt_position = self.len();
|
||||
if bounds.contains(&excerpt_position) {
|
||||
let buffer_offset =
|
||||
BufferOffset(excerpt.range.context.start.to_offset(&excerpt.buffer));
|
||||
result.push((&excerpt.buffer, buffer_offset..buffer_offset, excerpt.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
|
|
@ -6740,6 +6811,15 @@ impl MultiBufferSnapshot {
|
|||
self.diffs.get(&buffer_id).map(|diff| &diff.diff)
|
||||
}
|
||||
|
||||
/// For inverted diffs (used in side-by-side diff view), returns the main buffer
|
||||
/// snapshot that the diff's anchors refer to. Returns `None` if the diff is not
|
||||
/// inverted or if there's no diff for the given buffer ID.
|
||||
pub fn inverted_diff_main_buffer(&self, buffer_id: BufferId) -> Option<&text::BufferSnapshot> {
|
||||
self.diffs
|
||||
.get(&buffer_id)
|
||||
.and_then(|diff| diff.main_buffer.as_ref())
|
||||
}
|
||||
|
||||
/// Visually annotates a position or range with the `Debug` representation of a value. The
|
||||
/// callsite of this function is used as a key - previous annotations will be removed.
|
||||
#[cfg(debug_assertions)]
|
||||
|
|
@ -6767,11 +6847,11 @@ impl MultiBufferSnapshot {
|
|||
.to_multi_buffer_debug_ranges(self)
|
||||
.into_iter()
|
||||
.flat_map(|range| {
|
||||
self.range_to_buffer_ranges(range).into_iter().map(
|
||||
|(buffer, range, _excerpt_id)| {
|
||||
self.range_to_buffer_ranges(range.start..=range.end)
|
||||
.into_iter()
|
||||
.map(|(buffer, range, _excerpt_id)| {
|
||||
buffer.anchor_after(range.start)..buffer.anchor_before(range.end)
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
text::debug::GlobalDebugRanges::with_locked(|debug_ranges| {
|
||||
|
|
|
|||
|
|
@ -4012,7 +4012,7 @@ async fn test_singleton_with_inverted_diff(cx: &mut TestAppContext) {
|
|||
diff.update_diff(
|
||||
buffer.read(cx).text_snapshot(),
|
||||
Some(base_text.into()),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
|
|
@ -4050,7 +4050,7 @@ async fn test_singleton_with_inverted_diff(cx: &mut TestAppContext) {
|
|||
diff.update_diff(
|
||||
buffer.read(cx).text_snapshot(),
|
||||
Some(base_text.into()),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
|
|
@ -4890,3 +4890,122 @@ fn test_excerpts_containment_functions(cx: &mut App) {
|
|||
"excerpt_containing should return None for ranges spanning multiple excerpts"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_range_to_buffer_ranges_with_range_bounds(cx: &mut App) {
|
||||
use std::ops::Bound;
|
||||
|
||||
let buffer_1 = cx.new(|cx| Buffer::local("aaa\nbbb", cx));
|
||||
let buffer_2 = cx.new(|cx| Buffer::local("ccc", cx));
|
||||
|
||||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||||
let (excerpt_1_id, excerpt_2_id) = multibuffer.update(cx, |multibuffer, cx| {
|
||||
let excerpt_1_id = multibuffer.push_excerpts(
|
||||
buffer_1.clone(),
|
||||
[ExcerptRange::new(Point::new(0, 0)..Point::new(1, 3))],
|
||||
cx,
|
||||
)[0];
|
||||
|
||||
let excerpt_2_id = multibuffer.push_excerpts(
|
||||
buffer_2.clone(),
|
||||
[ExcerptRange::new(Point::new(0, 0)..Point::new(0, 3))],
|
||||
cx,
|
||||
)[0];
|
||||
|
||||
(excerpt_1_id, excerpt_2_id)
|
||||
});
|
||||
|
||||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||||
assert_eq!(snapshot.text(), "aaa\nbbb\nccc");
|
||||
|
||||
let excerpt_2_start = Point::new(2, 0);
|
||||
|
||||
let ranges_half_open = snapshot.range_to_buffer_ranges(Point::zero()..excerpt_2_start);
|
||||
assert_eq!(
|
||||
ranges_half_open.len(),
|
||||
1,
|
||||
"Half-open range ending at excerpt start should EXCLUDE that excerpt"
|
||||
);
|
||||
assert_eq!(ranges_half_open[0].2, excerpt_1_id);
|
||||
|
||||
let ranges_inclusive = snapshot.range_to_buffer_ranges(Point::zero()..=excerpt_2_start);
|
||||
assert_eq!(
|
||||
ranges_inclusive.len(),
|
||||
2,
|
||||
"Inclusive range ending at excerpt start should INCLUDE that excerpt"
|
||||
);
|
||||
assert_eq!(ranges_inclusive[0].2, excerpt_1_id);
|
||||
assert_eq!(ranges_inclusive[1].2, excerpt_2_id);
|
||||
|
||||
let ranges_unbounded =
|
||||
snapshot.range_to_buffer_ranges((Bound::Included(Point::zero()), Bound::Unbounded));
|
||||
assert_eq!(
|
||||
ranges_unbounded.len(),
|
||||
2,
|
||||
"Unbounded end should include all excerpts"
|
||||
);
|
||||
assert_eq!(ranges_unbounded[0].2, excerpt_1_id);
|
||||
assert_eq!(ranges_unbounded[1].2, excerpt_2_id);
|
||||
|
||||
let ranges_excluded_end = snapshot.range_to_buffer_ranges((
|
||||
Bound::Included(Point::zero()),
|
||||
Bound::Excluded(excerpt_2_start),
|
||||
));
|
||||
assert_eq!(
|
||||
ranges_excluded_end.len(),
|
||||
1,
|
||||
"Excluded end bound should exclude excerpt starting at that point"
|
||||
);
|
||||
assert_eq!(ranges_excluded_end[0].2, excerpt_1_id);
|
||||
|
||||
let buffer_empty = cx.new(|cx| Buffer::local("", cx));
|
||||
let multibuffer_trailing_empty = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||||
let (te_excerpt_1_id, te_excerpt_2_id) =
|
||||
multibuffer_trailing_empty.update(cx, |multibuffer, cx| {
|
||||
let excerpt_1_id = multibuffer.push_excerpts(
|
||||
buffer_1.clone(),
|
||||
[ExcerptRange::new(Point::new(0, 0)..Point::new(1, 3))],
|
||||
cx,
|
||||
)[0];
|
||||
|
||||
let excerpt_2_id = multibuffer.push_excerpts(
|
||||
buffer_empty.clone(),
|
||||
[ExcerptRange::new(Point::new(0, 0)..Point::new(0, 0))],
|
||||
cx,
|
||||
)[0];
|
||||
|
||||
(excerpt_1_id, excerpt_2_id)
|
||||
});
|
||||
|
||||
let snapshot_trailing = multibuffer_trailing_empty.read(cx).snapshot(cx);
|
||||
assert_eq!(snapshot_trailing.text(), "aaa\nbbb\n");
|
||||
|
||||
let max_point = snapshot_trailing.max_point();
|
||||
|
||||
let ranges_half_open_max = snapshot_trailing.range_to_buffer_ranges(Point::zero()..max_point);
|
||||
assert_eq!(
|
||||
ranges_half_open_max.len(),
|
||||
1,
|
||||
"Half-open range to max_point should EXCLUDE trailing empty excerpt at max_point"
|
||||
);
|
||||
assert_eq!(ranges_half_open_max[0].2, te_excerpt_1_id);
|
||||
|
||||
let ranges_inclusive_max = snapshot_trailing.range_to_buffer_ranges(Point::zero()..=max_point);
|
||||
assert_eq!(
|
||||
ranges_inclusive_max.len(),
|
||||
2,
|
||||
"Inclusive range to max_point should INCLUDE trailing empty excerpt"
|
||||
);
|
||||
assert_eq!(ranges_inclusive_max[0].2, te_excerpt_1_id);
|
||||
assert_eq!(ranges_inclusive_max[1].2, te_excerpt_2_id);
|
||||
|
||||
let ranges_unbounded_trailing = snapshot_trailing
|
||||
.range_to_buffer_ranges((Bound::Included(Point::zero()), Bound::Unbounded));
|
||||
assert_eq!(
|
||||
ranges_unbounded_trailing.len(),
|
||||
2,
|
||||
"Unbounded end should include trailing empty excerpt"
|
||||
);
|
||||
assert_eq!(ranges_unbounded_trailing[0].2, te_excerpt_1_id);
|
||||
assert_eq!(ranges_unbounded_trailing[1].2, te_excerpt_2_id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3115,7 +3115,7 @@ impl BufferGitState {
|
|||
unstaged_diff.read(cx).update_diff(
|
||||
buffer.clone(),
|
||||
index,
|
||||
index_changed,
|
||||
index_changed.then_some(false),
|
||||
language.clone(),
|
||||
cx,
|
||||
)
|
||||
|
|
@ -3138,7 +3138,7 @@ impl BufferGitState {
|
|||
uncommitted_diff.read(cx).update_diff(
|
||||
buffer.clone(),
|
||||
head,
|
||||
head_changed,
|
||||
head_changed.then_some(true),
|
||||
language.clone(),
|
||||
cx,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ use crate::{
|
|||
};
|
||||
use async_trait::async_trait;
|
||||
use buffer_diff::{
|
||||
BufferDiffEvent, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind, assert_hunks,
|
||||
BufferDiffEvent, DiffChanged, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind,
|
||||
assert_hunks,
|
||||
};
|
||||
use fs::FakeFs;
|
||||
use futures::{StreamExt, future};
|
||||
|
|
@ -8159,10 +8160,11 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
|
|||
BufferDiffEvent::HunksStagedOrUnstaged(_)
|
||||
));
|
||||
let event = diff_events.next().await.unwrap();
|
||||
if let BufferDiffEvent::DiffChanged {
|
||||
if let BufferDiffEvent::DiffChanged(DiffChanged {
|
||||
changed_range: Some(changed_range),
|
||||
base_text_changed_range: _,
|
||||
} = event
|
||||
extended_range: _,
|
||||
}) = event
|
||||
{
|
||||
let changed_range = changed_range.to_point(&snapshot);
|
||||
assert_eq!(changed_range, Point::new(1, 0)..Point::new(2, 0));
|
||||
|
|
@ -8202,10 +8204,11 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
|
|||
|
||||
// The diff emits a change event for the changed index text.
|
||||
let event = diff_events.next().await.unwrap();
|
||||
if let BufferDiffEvent::DiffChanged {
|
||||
if let BufferDiffEvent::DiffChanged(DiffChanged {
|
||||
changed_range: Some(changed_range),
|
||||
base_text_changed_range: _,
|
||||
} = event
|
||||
extended_range: _,
|
||||
}) = event
|
||||
{
|
||||
let changed_range = changed_range.to_point(&snapshot);
|
||||
assert_eq!(changed_range, Point::new(0, 0)..Point::new(4, 0));
|
||||
|
|
@ -8260,10 +8263,11 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
|
|||
BufferDiffEvent::HunksStagedOrUnstaged(_)
|
||||
));
|
||||
let event = diff_events.next().await.unwrap();
|
||||
if let BufferDiffEvent::DiffChanged {
|
||||
if let BufferDiffEvent::DiffChanged(DiffChanged {
|
||||
changed_range: Some(changed_range),
|
||||
base_text_changed_range: _,
|
||||
} = event
|
||||
extended_range: _,
|
||||
}) = event
|
||||
{
|
||||
let changed_range = changed_range.to_point(&snapshot);
|
||||
assert_eq!(changed_range, Point::new(3, 0)..Point::new(4, 0));
|
||||
|
|
@ -8302,10 +8306,11 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
|
|||
});
|
||||
|
||||
let event = diff_events.next().await.unwrap();
|
||||
if let BufferDiffEvent::DiffChanged {
|
||||
if let BufferDiffEvent::DiffChanged(DiffChanged {
|
||||
changed_range: Some(changed_range),
|
||||
base_text_changed_range: _,
|
||||
} = event
|
||||
extended_range: _,
|
||||
}) = event
|
||||
{
|
||||
let changed_range = changed_range.to_point(&snapshot);
|
||||
assert_eq!(changed_range, Point::new(0, 0)..Point::new(5, 0));
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
pub fn reset(&mut self) {
|
||||
self.did_seek = false;
|
||||
self.at_end = self.tree.is_empty();
|
||||
self.stack.truncate(0);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue