Both cloning and dropping of these has quite a bit of overhead (despite them being snapshots), so avoid where possible, especially in display map syncing Release Notes: - N/A or Added/Fixed/Improved ...
6353 lines
191 KiB
Rust
6353 lines
191 KiB
Rust
use std::{
|
|
ops::{Range, RangeInclusive},
|
|
sync::Arc,
|
|
};
|
|
|
|
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
|
|
use collections::HashMap;
|
|
|
|
use gpui::{
|
|
Action, AppContext as _, Entity, EventEmitter, Focusable, Font, Pixels, Subscription,
|
|
WeakEntity, canvas,
|
|
};
|
|
use itertools::Itertools;
|
|
use language::{Buffer, Capability, HighlightedText};
|
|
use multi_buffer::{
|
|
Anchor, AnchorRangeExt as _, BufferOffset, ExcerptRange, ExpandExcerptDirection, MultiBuffer,
|
|
MultiBufferDiffHunk, MultiBufferPoint, MultiBufferSnapshot, PathKey,
|
|
};
|
|
use project::Project;
|
|
use rope::Point;
|
|
use settings::{DiffViewStyle, SeedQuerySetting, Settings, SettingsStore};
|
|
use text::{Bias, BufferId, OffsetRangeExt as _, Patch, ToPoint as _};
|
|
use ui::{
|
|
App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
|
|
Styled as _, Window, div,
|
|
};
|
|
|
|
use crate::{
|
|
display_map::CompanionExcerptPatch,
|
|
element::SplitSide,
|
|
split_editor_view::{SplitEditorState, SplitEditorView},
|
|
};
|
|
use workspace::{
|
|
ActivatePaneLeft, ActivatePaneRight, Item, ToolbarItemLocation, Workspace,
|
|
item::{ItemBufferKind, ItemEvent, SaveOptions, TabContentParams},
|
|
searchable::{SearchEvent, SearchToken, SearchableItem, SearchableItemHandle},
|
|
};
|
|
|
|
use crate::{
|
|
Autoscroll, Editor, EditorEvent, EditorSettings, RenderDiffHunkControlsFn, ToggleSoftWrap,
|
|
actions::{DisableBreakpoint, EditLogBreakpoint, EnableBreakpoint, ToggleBreakpoint},
|
|
display_map::Companion,
|
|
};
|
|
use zed_actions::assistant::InlineAssist;
|
|
|
|
pub(crate) fn patches_for_lhs_range(
|
|
rhs_snapshot: &MultiBufferSnapshot,
|
|
lhs_snapshot: &MultiBufferSnapshot,
|
|
lhs_bounds: Range<MultiBufferPoint>,
|
|
) -> Vec<CompanionExcerptPatch> {
|
|
patches_for_range(
|
|
lhs_snapshot,
|
|
rhs_snapshot,
|
|
lhs_bounds,
|
|
|diff, range, buffer| diff.patch_for_base_text_range(range, buffer),
|
|
)
|
|
}
|
|
|
|
pub(crate) fn patches_for_rhs_range(
|
|
lhs_snapshot: &MultiBufferSnapshot,
|
|
rhs_snapshot: &MultiBufferSnapshot,
|
|
rhs_bounds: Range<MultiBufferPoint>,
|
|
) -> Vec<CompanionExcerptPatch> {
|
|
patches_for_range(
|
|
rhs_snapshot,
|
|
lhs_snapshot,
|
|
rhs_bounds,
|
|
|diff, range, buffer| diff.patch_for_buffer_range(range, buffer),
|
|
)
|
|
}
|
|
|
|
fn buffer_range_to_base_text_range(
|
|
rhs_range: &Range<Point>,
|
|
diff_snapshot: &BufferDiffSnapshot,
|
|
rhs_buffer_snapshot: &text::BufferSnapshot,
|
|
) -> Range<Point> {
|
|
let start = diff_snapshot
|
|
.buffer_point_to_base_text_range(Point::new(rhs_range.start.row, 0), rhs_buffer_snapshot)
|
|
.start;
|
|
let end = diff_snapshot
|
|
.buffer_point_to_base_text_range(Point::new(rhs_range.end.row, 0), rhs_buffer_snapshot)
|
|
.end;
|
|
let end_column = diff_snapshot.base_text().line_len(end.row);
|
|
Point::new(start.row, 0)..Point::new(end.row, end_column)
|
|
}
|
|
|
|
fn translate_lhs_selections_to_rhs(
|
|
selections_by_buffer: &HashMap<BufferId, (Vec<Range<BufferOffset>>, Option<u32>)>,
|
|
splittable: &SplittableEditor,
|
|
cx: &App,
|
|
) -> HashMap<Entity<Buffer>, (Vec<Range<BufferOffset>>, Option<u32>)> {
|
|
let Some(lhs) = &splittable.lhs else {
|
|
return HashMap::default();
|
|
};
|
|
let lhs_snapshot = lhs.multibuffer.read(cx).snapshot(cx);
|
|
|
|
let mut translated: HashMap<Entity<Buffer>, (Vec<Range<BufferOffset>>, Option<u32>)> =
|
|
HashMap::default();
|
|
|
|
for (lhs_buffer_id, (ranges, scroll_offset)) in selections_by_buffer {
|
|
let Some(diff) = lhs_snapshot.diff_for_buffer_id(*lhs_buffer_id) else {
|
|
continue;
|
|
};
|
|
let rhs_buffer_id = diff.buffer_id();
|
|
|
|
let Some(rhs_buffer) = splittable
|
|
.rhs_editor
|
|
.read(cx)
|
|
.buffer()
|
|
.read(cx)
|
|
.buffer(rhs_buffer_id)
|
|
else {
|
|
continue;
|
|
};
|
|
|
|
let Some(diff) = splittable
|
|
.rhs_editor
|
|
.read(cx)
|
|
.buffer()
|
|
.read(cx)
|
|
.diff_for(rhs_buffer_id)
|
|
else {
|
|
continue;
|
|
};
|
|
|
|
let diff_snapshot = diff.read(cx).snapshot(cx);
|
|
let rhs_buffer_snapshot = rhs_buffer.read(cx).snapshot();
|
|
let base_text_buffer = diff.read(cx).base_text_buffer();
|
|
let base_text_snapshot = base_text_buffer.read(cx).snapshot();
|
|
|
|
let translated_ranges: Vec<Range<BufferOffset>> = ranges
|
|
.iter()
|
|
.map(|range| {
|
|
let start_point = base_text_snapshot.offset_to_point(range.start.0);
|
|
let end_point = base_text_snapshot.offset_to_point(range.end.0);
|
|
|
|
let rhs_start = diff_snapshot
|
|
.base_text_point_to_buffer_point(start_point, &rhs_buffer_snapshot);
|
|
let rhs_end =
|
|
diff_snapshot.base_text_point_to_buffer_point(end_point, &rhs_buffer_snapshot);
|
|
|
|
BufferOffset(rhs_buffer_snapshot.point_to_offset(rhs_start))
|
|
..BufferOffset(rhs_buffer_snapshot.point_to_offset(rhs_end))
|
|
})
|
|
.collect();
|
|
|
|
translated.insert(rhs_buffer, (translated_ranges, *scroll_offset));
|
|
}
|
|
|
|
translated
|
|
}
|
|
|
|
fn translate_lhs_hunks_to_rhs(
|
|
lhs_hunks: &[MultiBufferDiffHunk],
|
|
splittable: &SplittableEditor,
|
|
cx: &App,
|
|
) -> Vec<MultiBufferDiffHunk> {
|
|
let Some(lhs) = &splittable.lhs else {
|
|
return vec![];
|
|
};
|
|
let lhs_snapshot = lhs.multibuffer.read(cx).snapshot(cx);
|
|
let rhs_snapshot = splittable.rhs_multibuffer.read(cx).snapshot(cx);
|
|
let rhs_hunks: Vec<MultiBufferDiffHunk> = rhs_snapshot.diff_hunks().collect();
|
|
|
|
let mut translated = Vec::new();
|
|
for lhs_hunk in lhs_hunks {
|
|
let Some(diff) = lhs_snapshot.diff_for_buffer_id(lhs_hunk.buffer_id) else {
|
|
continue;
|
|
};
|
|
let rhs_buffer_id = diff.buffer_id();
|
|
if let Some(rhs_hunk) = rhs_hunks.iter().find(|rhs_hunk| {
|
|
rhs_hunk.buffer_id == rhs_buffer_id
|
|
&& rhs_hunk.diff_base_byte_range == lhs_hunk.diff_base_byte_range
|
|
}) {
|
|
translated.push(rhs_hunk.clone());
|
|
}
|
|
}
|
|
translated
|
|
}
|
|
|
|
fn patches_for_range<F>(
|
|
source_snapshot: &MultiBufferSnapshot,
|
|
target_snapshot: &MultiBufferSnapshot,
|
|
source_bounds: Range<MultiBufferPoint>,
|
|
translate_fn: F,
|
|
) -> Vec<CompanionExcerptPatch>
|
|
where
|
|
F: Fn(&BufferDiffSnapshot, RangeInclusive<Point>, &text::BufferSnapshot) -> Patch<Point>,
|
|
{
|
|
struct PendingExcerpt<'a> {
|
|
source_buffer_snapshot: &'a language::BufferSnapshot,
|
|
source_excerpt_range: ExcerptRange<text::Anchor>,
|
|
buffer_point_range: Range<Point>,
|
|
}
|
|
|
|
let mut result = Vec::new();
|
|
let mut current_buffer_id: Option<BufferId> = None;
|
|
let mut pending_excerpts: Vec<PendingExcerpt<'_>> = Vec::new();
|
|
let mut union_context_start: Option<Point> = None;
|
|
let mut union_context_end: Option<Point> = None;
|
|
|
|
let flush_buffer = |pending: &mut Vec<PendingExcerpt>,
|
|
union_start: Point,
|
|
union_end: Point,
|
|
result: &mut Vec<CompanionExcerptPatch>| {
|
|
let Some(first) = pending.first() else {
|
|
return;
|
|
};
|
|
|
|
let source_buffer_id = first.source_buffer_snapshot.remote_id();
|
|
let Some(diff) = source_snapshot.diff_for_buffer_id(source_buffer_id) else {
|
|
pending.clear();
|
|
return;
|
|
};
|
|
let source_is_lhs = source_buffer_id == diff.base_text().remote_id();
|
|
let target_buffer_id = if source_is_lhs {
|
|
diff.buffer_id()
|
|
} else {
|
|
diff.base_text().remote_id()
|
|
};
|
|
let Some(target_buffer) = target_snapshot.buffer_for_id(target_buffer_id) else {
|
|
pending.clear();
|
|
return;
|
|
};
|
|
let rhs_buffer = if source_is_lhs {
|
|
target_buffer
|
|
} else {
|
|
&first.source_buffer_snapshot
|
|
};
|
|
|
|
let patch = translate_fn(diff, union_start..=union_end, rhs_buffer);
|
|
|
|
let mut source_excerpts = source_snapshot
|
|
.excerpts_for_buffer(source_buffer_id)
|
|
.peekable();
|
|
let mut target_excerpts = target_snapshot
|
|
.excerpts_for_buffer(target_buffer_id)
|
|
.peekable();
|
|
|
|
for excerpt in pending.drain(..) {
|
|
while let Some(source_excerpt_range) = source_excerpts.peek()
|
|
&& source_excerpt_range != &excerpt.source_excerpt_range
|
|
{
|
|
source_excerpts.next();
|
|
target_excerpts.next();
|
|
}
|
|
if let Some(source_excerpt_range) = source_excerpts.peek()
|
|
&& let Some(target_excerpt_range) = target_excerpts.peek()
|
|
{
|
|
result.push(patch_for_excerpt(
|
|
source_snapshot,
|
|
target_snapshot,
|
|
&excerpt.source_buffer_snapshot,
|
|
target_buffer,
|
|
source_excerpt_range.clone(),
|
|
target_excerpt_range.clone(),
|
|
&patch,
|
|
excerpt.buffer_point_range,
|
|
));
|
|
}
|
|
}
|
|
};
|
|
|
|
for (buffer_snapshot, source_range, source_excerpt_range) in
|
|
source_snapshot.range_to_buffer_ranges(source_bounds)
|
|
{
|
|
let buffer_id = buffer_snapshot.remote_id();
|
|
|
|
if current_buffer_id != Some(buffer_id) {
|
|
if let (Some(start), Some(end)) = (union_context_start.take(), union_context_end.take())
|
|
{
|
|
flush_buffer(&mut pending_excerpts, start, end, &mut result);
|
|
}
|
|
current_buffer_id = Some(buffer_id);
|
|
}
|
|
|
|
let buffer_point_range = source_range.to_point(&buffer_snapshot);
|
|
let source_context_range = source_excerpt_range.context.to_point(&buffer_snapshot);
|
|
|
|
union_context_start = Some(union_context_start.map_or(source_context_range.start, |s| {
|
|
s.min(source_context_range.start)
|
|
}));
|
|
union_context_end = Some(union_context_end.map_or(source_context_range.end, |e| {
|
|
e.max(source_context_range.end)
|
|
}));
|
|
|
|
pending_excerpts.push(PendingExcerpt {
|
|
source_buffer_snapshot: buffer_snapshot,
|
|
source_excerpt_range,
|
|
buffer_point_range,
|
|
});
|
|
}
|
|
|
|
if let (Some(start), Some(end)) = (union_context_start, union_context_end) {
|
|
flush_buffer(&mut pending_excerpts, start, end, &mut result);
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
fn patch_for_excerpt(
|
|
source_snapshot: &MultiBufferSnapshot,
|
|
target_snapshot: &MultiBufferSnapshot,
|
|
source_buffer_snapshot: &language::BufferSnapshot,
|
|
target_buffer_snapshot: &language::BufferSnapshot,
|
|
source_excerpt_range: ExcerptRange<text::Anchor>,
|
|
target_excerpt_range: ExcerptRange<text::Anchor>,
|
|
patch: &Patch<Point>,
|
|
source_edited_range: Range<Point>,
|
|
) -> CompanionExcerptPatch {
|
|
let source_buffer_range = source_excerpt_range
|
|
.context
|
|
.to_point(source_buffer_snapshot);
|
|
let source_multibuffer_range = (source_snapshot
|
|
.anchor_in_buffer(source_excerpt_range.context.start)
|
|
.expect("buffer should exist in multibuffer")
|
|
..source_snapshot
|
|
.anchor_in_buffer(source_excerpt_range.context.end)
|
|
.expect("buffer should exist in multibuffer"))
|
|
.to_point(source_snapshot);
|
|
let target_buffer_range = target_excerpt_range
|
|
.context
|
|
.to_point(target_buffer_snapshot);
|
|
let target_multibuffer_range = (target_snapshot
|
|
.anchor_in_buffer(target_excerpt_range.context.start)
|
|
.expect("buffer should exist in multibuffer")
|
|
..target_snapshot
|
|
.anchor_in_buffer(target_excerpt_range.context.end)
|
|
.expect("buffer should exist in multibuffer"))
|
|
.to_point(target_snapshot);
|
|
|
|
let edits = patch
|
|
.edits()
|
|
.iter()
|
|
.skip_while(|edit| edit.old.end < source_buffer_range.start)
|
|
.take_while(|edit| edit.old.start <= source_buffer_range.end)
|
|
.map(|edit| {
|
|
let clamped_source_start = edit.old.start.max(source_buffer_range.start);
|
|
let clamped_source_end = edit.old.end.min(source_buffer_range.end);
|
|
let source_multibuffer_start =
|
|
source_multibuffer_range.start + (clamped_source_start - source_buffer_range.start);
|
|
let source_multibuffer_end =
|
|
source_multibuffer_range.start + (clamped_source_end - source_buffer_range.start);
|
|
let clamped_target_start = edit
|
|
.new
|
|
.start
|
|
.max(target_buffer_range.start)
|
|
.min(target_buffer_range.end);
|
|
let clamped_target_end = edit
|
|
.new
|
|
.end
|
|
.max(target_buffer_range.start)
|
|
.min(target_buffer_range.end);
|
|
let target_multibuffer_start =
|
|
target_multibuffer_range.start + (clamped_target_start - target_buffer_range.start);
|
|
let target_multibuffer_end =
|
|
target_multibuffer_range.start + (clamped_target_end - target_buffer_range.start);
|
|
text::Edit {
|
|
old: source_multibuffer_start..source_multibuffer_end,
|
|
new: target_multibuffer_start..target_multibuffer_end,
|
|
}
|
|
});
|
|
|
|
let edits = [text::Edit {
|
|
old: source_multibuffer_range.start..source_multibuffer_range.start,
|
|
new: target_multibuffer_range.start..target_multibuffer_range.start,
|
|
}]
|
|
.into_iter()
|
|
.chain(edits);
|
|
|
|
let mut merged_edits: Vec<text::Edit<Point>> = Vec::new();
|
|
for edit in edits {
|
|
if let Some(last) = merged_edits.last_mut() {
|
|
if edit.new.start <= last.new.end || edit.old.start <= last.old.end {
|
|
last.old.end = last.old.end.max(edit.old.end);
|
|
last.new.end = last.new.end.max(edit.new.end);
|
|
continue;
|
|
}
|
|
}
|
|
merged_edits.push(edit);
|
|
}
|
|
|
|
let edited_range = source_multibuffer_range.start
|
|
+ (source_edited_range.start - source_buffer_range.start)
|
|
..source_multibuffer_range.start + (source_edited_range.end - source_buffer_range.start);
|
|
|
|
let source_excerpt_end =
|
|
source_multibuffer_range.start + (source_buffer_range.end - source_buffer_range.start);
|
|
let target_excerpt_end =
|
|
target_multibuffer_range.start + (target_buffer_range.end - target_buffer_range.start);
|
|
|
|
CompanionExcerptPatch {
|
|
patch: Patch::new(merged_edits),
|
|
edited_range,
|
|
source_excerpt_range: source_multibuffer_range.start..source_excerpt_end,
|
|
target_excerpt_range: target_multibuffer_range.start..target_excerpt_end,
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
|
|
#[action(namespace = editor)]
|
|
pub struct ToggleSplitDiff;
|
|
|
|
pub struct SplittableEditor {
|
|
rhs_multibuffer: Entity<MultiBuffer>,
|
|
rhs_editor: Entity<Editor>,
|
|
lhs: Option<LhsEditor>,
|
|
workspace: WeakEntity<Workspace>,
|
|
split_state: Entity<SplitEditorState>,
|
|
searched_side: Option<SplitSide>,
|
|
/// The preferred diff style.
|
|
diff_view_style: DiffViewStyle,
|
|
/// True when the current width is below the minimum threshold for split
|
|
/// mode, regardless of the current diff view style setting.
|
|
too_narrow_for_split: bool,
|
|
last_width: Option<Pixels>,
|
|
_subscriptions: Vec<Subscription>,
|
|
}
|
|
|
|
struct LhsEditor {
|
|
multibuffer: Entity<MultiBuffer>,
|
|
editor: Entity<Editor>,
|
|
was_last_focused: bool,
|
|
_subscriptions: Vec<Subscription>,
|
|
}
|
|
|
|
impl SplittableEditor {
|
|
pub fn rhs_editor(&self) -> &Entity<Editor> {
|
|
&self.rhs_editor
|
|
}
|
|
|
|
pub fn lhs_editor(&self) -> Option<&Entity<Editor>> {
|
|
self.lhs.as_ref().map(|s| &s.editor)
|
|
}
|
|
|
|
pub fn update_editors(
|
|
&self,
|
|
cx: &mut Context<Self>,
|
|
f: impl Fn(&mut Editor, &mut Context<Editor>),
|
|
) {
|
|
if let Some(lhs) = &self.lhs {
|
|
lhs.editor.update(cx, &f);
|
|
}
|
|
self.rhs_editor.update(cx, &f);
|
|
}
|
|
|
|
pub fn diff_view_style(&self) -> DiffViewStyle {
|
|
self.diff_view_style
|
|
}
|
|
|
|
pub fn is_split(&self) -> bool {
|
|
self.lhs.is_some()
|
|
}
|
|
|
|
pub fn set_render_diff_hunk_controls(
|
|
&self,
|
|
render_diff_hunk_controls: RenderDiffHunkControlsFn,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.update_editors(cx, |editor, cx| {
|
|
editor.set_render_diff_hunk_controls(render_diff_hunk_controls.clone(), cx);
|
|
});
|
|
}
|
|
|
|
pub fn disable_diff_hunk_controls(&self, cx: &mut Context<Self>) {
|
|
let empty_controls = Arc::new(|_, _: &_, _, _, _, _: &_, _: &mut _, _: &mut _| {
|
|
gpui::Empty.into_any_element()
|
|
});
|
|
self.update_editors(cx, |editor, cx| {
|
|
editor.set_render_diff_hunk_controls(empty_controls.clone(), cx);
|
|
});
|
|
}
|
|
|
|
pub fn set_render_diff_hunks_as_unstaged(&self, cx: &mut Context<Self>) {
|
|
self.update_editors(cx, |editor, cx| {
|
|
editor.set_render_diff_hunks_as_unstaged(true, cx);
|
|
});
|
|
}
|
|
|
|
fn focused_side(&self) -> SplitSide {
|
|
if let Some(lhs) = &self.lhs
|
|
&& lhs.was_last_focused
|
|
{
|
|
SplitSide::Left
|
|
} else {
|
|
SplitSide::Right
|
|
}
|
|
}
|
|
|
|
pub fn focused_editor(&self) -> &Entity<Editor> {
|
|
if let Some(lhs) = &self.lhs
|
|
&& lhs.was_last_focused
|
|
{
|
|
&lhs.editor
|
|
} else {
|
|
&self.rhs_editor
|
|
}
|
|
}
|
|
|
|
pub fn new(
|
|
style: DiffViewStyle,
|
|
rhs_multibuffer: Entity<MultiBuffer>,
|
|
project: Entity<Project>,
|
|
workspace: Entity<Workspace>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Self {
|
|
let rhs_editor = cx.new(|cx| {
|
|
let mut editor =
|
|
Editor::for_multibuffer(rhs_multibuffer.clone(), Some(project.clone()), window, cx);
|
|
editor.set_expand_all_diff_hunks(cx);
|
|
editor.disable_runnables();
|
|
editor.disable_code_lens(cx);
|
|
editor.disable_inline_diagnostics();
|
|
editor.disable_mouse_wheel_zoom();
|
|
editor.set_minimap_visibility(crate::MinimapVisibility::Disabled, window, cx);
|
|
editor.start_temporary_diff_override();
|
|
editor
|
|
});
|
|
// TODO(split-diff) we might want to tag editor events with whether they came from rhs/lhs
|
|
let subscriptions = vec![
|
|
cx.subscribe(
|
|
&rhs_editor,
|
|
|this, _, event: &EditorEvent, cx| match event {
|
|
EditorEvent::ExpandExcerptsRequested {
|
|
excerpt_anchors,
|
|
lines,
|
|
direction,
|
|
} => {
|
|
this.expand_excerpts(
|
|
excerpt_anchors.iter().copied(),
|
|
*lines,
|
|
*direction,
|
|
cx,
|
|
);
|
|
}
|
|
_ => cx.emit(event.clone()),
|
|
},
|
|
),
|
|
cx.subscribe(&rhs_editor, |this, _, event: &SearchEvent, cx| {
|
|
if this.searched_side.is_none() || this.searched_side == Some(SplitSide::Right) {
|
|
cx.emit(event.clone());
|
|
}
|
|
}),
|
|
cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| {
|
|
let diff_view_style = EditorSettings::get_global(cx).diff_view_style;
|
|
if this.diff_view_style() != diff_view_style {
|
|
this.toggle_split(&ToggleSplitDiff, window, cx);
|
|
cx.notify();
|
|
}
|
|
}),
|
|
];
|
|
|
|
let this = cx.weak_entity();
|
|
window.defer(cx, {
|
|
let workspace = workspace.downgrade();
|
|
let rhs_editor = rhs_editor.downgrade();
|
|
move |window, cx| {
|
|
workspace
|
|
.update(cx, |workspace, cx| {
|
|
rhs_editor
|
|
.update(cx, |editor, cx| {
|
|
editor.added_to_workspace(workspace, window, cx);
|
|
})
|
|
.ok();
|
|
})
|
|
.ok();
|
|
if style == DiffViewStyle::Split {
|
|
this.update(cx, |this, cx| {
|
|
this.split(window, cx);
|
|
})
|
|
.ok();
|
|
}
|
|
}
|
|
});
|
|
let split_state = cx.new(|cx| SplitEditorState::new(cx));
|
|
Self {
|
|
diff_view_style: style,
|
|
rhs_editor,
|
|
rhs_multibuffer,
|
|
lhs: None,
|
|
workspace: workspace.downgrade(),
|
|
split_state,
|
|
searched_side: None,
|
|
too_narrow_for_split: false,
|
|
last_width: None,
|
|
_subscriptions: subscriptions,
|
|
}
|
|
}
|
|
|
|
pub fn split(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
if self.lhs.is_some() {
|
|
return;
|
|
}
|
|
let Some(workspace) = self.workspace.upgrade() else {
|
|
return;
|
|
};
|
|
let project = workspace.read(cx).project().clone();
|
|
let all_paths = self.diff_paths(cx);
|
|
if all_paths.is_empty() && !self.rhs_multibuffer.read(cx).is_empty() {
|
|
return;
|
|
}
|
|
|
|
let is_rhs_singleton = self.rhs_multibuffer.read(cx).is_singleton();
|
|
let lhs_multibuffer = cx.new(|cx| {
|
|
let mut multibuffer = if is_rhs_singleton {
|
|
MultiBuffer::without_headers(Capability::ReadOnly)
|
|
} else {
|
|
MultiBuffer::new(Capability::ReadOnly)
|
|
};
|
|
multibuffer.set_all_diff_hunks_expanded(cx);
|
|
multibuffer
|
|
});
|
|
|
|
let render_diff_hunk_controls = self.rhs_editor.read(cx).render_diff_hunk_controls.clone();
|
|
let render_diff_hunks_as_unstaged = self.rhs_editor.read(cx).render_diff_hunks_as_unstaged;
|
|
let lhs_editor = cx.new(|cx| {
|
|
let mut editor =
|
|
Editor::for_multibuffer(lhs_multibuffer.clone(), Some(project.clone()), window, cx);
|
|
editor.set_render_diff_hunks_as_unstaged(render_diff_hunks_as_unstaged, cx);
|
|
editor.set_number_deleted_lines(true, cx);
|
|
editor.set_delegate_expand_excerpts(true);
|
|
editor.set_delegate_stage_and_restore(true);
|
|
editor.set_delegate_open_excerpts(true);
|
|
editor.set_show_vertical_scrollbar(false, cx);
|
|
editor.disable_lsp_data();
|
|
editor.disable_runnables();
|
|
editor.disable_diagnostics(cx);
|
|
editor.disable_mouse_wheel_zoom();
|
|
editor.set_minimap_visibility(crate::MinimapVisibility::Disabled, window, cx);
|
|
editor
|
|
});
|
|
|
|
lhs_editor.update(cx, |editor, cx| {
|
|
editor.set_render_diff_hunk_controls(render_diff_hunk_controls, cx);
|
|
});
|
|
|
|
let mut subscriptions = vec![cx.subscribe_in(
|
|
&lhs_editor,
|
|
window,
|
|
|this, _, event: &EditorEvent, window, cx| match event {
|
|
EditorEvent::ExpandExcerptsRequested {
|
|
excerpt_anchors,
|
|
lines,
|
|
direction,
|
|
} => {
|
|
if let Some(lhs) = &this.lhs {
|
|
let rhs_snapshot = this.rhs_multibuffer.read(cx).snapshot(cx);
|
|
let lhs_snapshot = lhs.multibuffer.read(cx).snapshot(cx);
|
|
let rhs_anchors = excerpt_anchors
|
|
.iter()
|
|
.filter_map(|anchor| {
|
|
let (anchor, lhs_buffer) =
|
|
lhs_snapshot.anchor_to_buffer_anchor(*anchor)?;
|
|
let diff = lhs_snapshot.diff_for_buffer_id(anchor.buffer_id)?;
|
|
let rhs_buffer_id = diff.buffer_id();
|
|
let rhs_buffer = rhs_snapshot.buffer_for_id(rhs_buffer_id)?;
|
|
let rhs_point = diff.base_text_point_to_buffer_point(
|
|
anchor.to_point(&lhs_buffer),
|
|
&rhs_buffer,
|
|
);
|
|
rhs_snapshot.anchor_in_excerpt(rhs_buffer.anchor_before(rhs_point))
|
|
})
|
|
.collect::<Vec<_>>();
|
|
this.expand_excerpts(rhs_anchors.into_iter(), *lines, *direction, cx);
|
|
}
|
|
}
|
|
EditorEvent::StageOrUnstageRequested { stage, hunks } => {
|
|
if this.lhs.is_some() {
|
|
let translated = translate_lhs_hunks_to_rhs(hunks, this, cx);
|
|
if !translated.is_empty() {
|
|
let stage = *stage;
|
|
this.rhs_editor.update(cx, |editor, cx| {
|
|
let chunk_by = translated.into_iter().chunk_by(|h| h.buffer_id);
|
|
for (buffer_id, hunks) in &chunk_by {
|
|
editor.do_stage_or_unstage(stage, buffer_id, hunks, cx);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
EditorEvent::RestoreRequested { hunks } => {
|
|
if this.lhs.is_some() {
|
|
let translated = translate_lhs_hunks_to_rhs(hunks, this, cx);
|
|
if !translated.is_empty() {
|
|
this.rhs_editor.update(cx, |editor, cx| {
|
|
editor.restore_diff_hunks(translated, cx);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
EditorEvent::OpenExcerptsRequested {
|
|
selections_by_buffer,
|
|
split,
|
|
} => {
|
|
if this.lhs.is_some() {
|
|
let translated =
|
|
translate_lhs_selections_to_rhs(selections_by_buffer, this, cx);
|
|
if !translated.is_empty() {
|
|
let workspace = this.workspace.clone();
|
|
let split = *split;
|
|
Editor::open_buffers_in_workspace(
|
|
workspace, translated, split, window, cx,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
_ => cx.emit(event.clone()),
|
|
},
|
|
)];
|
|
|
|
subscriptions.push(
|
|
cx.subscribe(&lhs_editor, |this, _, event: &SearchEvent, cx| {
|
|
if this.searched_side == Some(SplitSide::Left) {
|
|
cx.emit(event.clone());
|
|
}
|
|
}),
|
|
);
|
|
|
|
let lhs_focus_handle = lhs_editor.read(cx).focus_handle(cx);
|
|
subscriptions.push(
|
|
cx.on_focus_in(&lhs_focus_handle, window, |this, _window, cx| {
|
|
if let Some(lhs) = &mut this.lhs {
|
|
if !lhs.was_last_focused {
|
|
lhs.was_last_focused = true;
|
|
cx.notify();
|
|
}
|
|
}
|
|
}),
|
|
);
|
|
|
|
let rhs_focus_handle = self.rhs_editor.read(cx).focus_handle(cx);
|
|
subscriptions.push(
|
|
cx.on_focus_in(&rhs_focus_handle, window, |this, _window, cx| {
|
|
if let Some(lhs) = &mut this.lhs {
|
|
if lhs.was_last_focused {
|
|
lhs.was_last_focused = false;
|
|
cx.notify();
|
|
}
|
|
}
|
|
}),
|
|
);
|
|
|
|
let rhs_display_map = self.rhs_editor.read(cx).display_map.clone();
|
|
let lhs_display_map = lhs_editor.read(cx).display_map.clone();
|
|
let rhs_display_map_id = rhs_display_map.entity_id();
|
|
let companion = cx.new(|_| Companion::new(rhs_display_map_id));
|
|
let lhs = LhsEditor {
|
|
editor: lhs_editor,
|
|
multibuffer: lhs_multibuffer,
|
|
was_last_focused: false,
|
|
_subscriptions: subscriptions,
|
|
};
|
|
|
|
self.rhs_editor.update(cx, |editor, cx| {
|
|
editor.set_delegate_expand_excerpts(true);
|
|
editor.buffer().update(cx, |rhs_multibuffer, cx| {
|
|
rhs_multibuffer.set_show_deleted_hunks(false, cx);
|
|
rhs_multibuffer.set_use_extended_diff_range(true, cx);
|
|
})
|
|
});
|
|
|
|
self.lhs = Some(lhs);
|
|
|
|
self.sync_lhs_for_paths(all_paths, cx);
|
|
|
|
rhs_display_map.update(cx, |dm, cx| {
|
|
dm.set_companion(Some((lhs_display_map, companion.clone())), cx);
|
|
});
|
|
|
|
let lhs = self.lhs.as_ref().unwrap();
|
|
|
|
let shared_scroll_anchor = self
|
|
.rhs_editor
|
|
.read(cx)
|
|
.scroll_manager
|
|
.scroll_anchor_entity();
|
|
lhs.editor.update(cx, |editor, _cx| {
|
|
editor
|
|
.scroll_manager
|
|
.set_shared_scroll_anchor(shared_scroll_anchor);
|
|
});
|
|
|
|
let this = cx.entity().downgrade();
|
|
self.rhs_editor.update(cx, |editor, _cx| {
|
|
let this = this.clone();
|
|
editor.set_on_local_selections_changed(Some(Box::new(
|
|
move |cursor_position, window, cx| {
|
|
let this = this.clone();
|
|
window.defer(cx, move |window, cx| {
|
|
this.update(cx, |this, cx| {
|
|
this.sync_cursor_to_other_side(true, cursor_position, window, cx);
|
|
})
|
|
.ok();
|
|
})
|
|
},
|
|
)));
|
|
});
|
|
lhs.editor.update(cx, |editor, _cx| {
|
|
let this = this.clone();
|
|
editor.set_on_local_selections_changed(Some(Box::new(
|
|
move |cursor_position, window, cx| {
|
|
let this = this.clone();
|
|
window.defer(cx, move |window, cx| {
|
|
this.update(cx, |this, cx| {
|
|
this.sync_cursor_to_other_side(false, cursor_position, window, cx);
|
|
})
|
|
.ok();
|
|
})
|
|
},
|
|
)));
|
|
});
|
|
|
|
// Copy soft wrap state from rhs (source of truth) to lhs
|
|
let rhs_soft_wrap_override = self.rhs_editor.read(cx).soft_wrap_mode_override;
|
|
lhs.editor.update(cx, |editor, cx| {
|
|
editor.soft_wrap_mode_override = rhs_soft_wrap_override;
|
|
cx.notify();
|
|
});
|
|
|
|
cx.notify();
|
|
}
|
|
|
|
fn diff_paths(&self, cx: &App) -> Vec<(PathKey, Entity<BufferDiff>)> {
|
|
let rhs_multibuffer = self.rhs_multibuffer.read(cx);
|
|
let rhs_multibuffer_snapshot = rhs_multibuffer.snapshot(cx);
|
|
rhs_multibuffer_snapshot
|
|
.buffers_with_paths()
|
|
.filter_map(|(buffer, path)| {
|
|
let diff = rhs_multibuffer.diff_for(buffer.remote_id())?;
|
|
Some((path.clone(), diff))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn activate_pane_left(
|
|
&mut self,
|
|
_: &ActivatePaneLeft,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if let Some(lhs) = &self.lhs {
|
|
if !lhs.was_last_focused {
|
|
lhs.editor.read(cx).focus_handle(cx).focus(window, cx);
|
|
lhs.editor.update(cx, |editor, cx| {
|
|
editor.request_autoscroll(Autoscroll::fit(), cx);
|
|
});
|
|
} else {
|
|
cx.propagate();
|
|
}
|
|
} else {
|
|
cx.propagate();
|
|
}
|
|
}
|
|
|
|
fn activate_pane_right(
|
|
&mut self,
|
|
_: &ActivatePaneRight,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if let Some(lhs) = &self.lhs {
|
|
if lhs.was_last_focused {
|
|
self.rhs_editor.read(cx).focus_handle(cx).focus(window, cx);
|
|
self.rhs_editor.update(cx, |editor, cx| {
|
|
editor.request_autoscroll(Autoscroll::fit(), cx);
|
|
});
|
|
} else {
|
|
cx.propagate();
|
|
}
|
|
} else {
|
|
cx.propagate();
|
|
}
|
|
}
|
|
|
|
fn sync_cursor_to_other_side(
|
|
&mut self,
|
|
from_rhs: bool,
|
|
source_point: Point,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let Some(lhs) = &self.lhs else {
|
|
return;
|
|
};
|
|
|
|
let (source_editor, target_editor) = if from_rhs {
|
|
(&self.rhs_editor, &lhs.editor)
|
|
} else {
|
|
(&lhs.editor, &self.rhs_editor)
|
|
};
|
|
|
|
let source_snapshot = source_editor.update(cx, |editor, cx| editor.snapshot(window, cx));
|
|
let target_snapshot = target_editor.update(cx, |editor, cx| editor.snapshot(window, cx));
|
|
|
|
let display_point = source_snapshot
|
|
.display_snapshot
|
|
.point_to_display_point(source_point, Bias::Right);
|
|
let display_point = target_snapshot.clip_point(display_point, Bias::Right);
|
|
let target_point = target_snapshot.display_point_to_point(display_point, Bias::Right);
|
|
|
|
target_editor.update(cx, |editor, cx| {
|
|
editor.set_suppress_selection_callback(true);
|
|
editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select_ranges([target_point..target_point]);
|
|
});
|
|
editor.set_suppress_selection_callback(false);
|
|
});
|
|
}
|
|
|
|
pub fn toggle_split(
|
|
&mut self,
|
|
_: &ToggleSplitDiff,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
match self.diff_view_style {
|
|
DiffViewStyle::Unified => {
|
|
self.diff_view_style = DiffViewStyle::Split;
|
|
if !self.too_narrow_for_split {
|
|
self.split(window, cx);
|
|
}
|
|
}
|
|
DiffViewStyle::Split => {
|
|
self.diff_view_style = DiffViewStyle::Unified;
|
|
if self.is_split() {
|
|
self.unsplit(window, cx);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn intercept_toggle_breakpoint(
|
|
&mut self,
|
|
_: &ToggleBreakpoint,
|
|
_window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
// Only block breakpoint actions when the left (lhs) editor has focus
|
|
if let Some(lhs) = &self.lhs {
|
|
if lhs.was_last_focused {
|
|
cx.stop_propagation();
|
|
} else {
|
|
cx.propagate();
|
|
}
|
|
} else {
|
|
cx.propagate();
|
|
}
|
|
}
|
|
|
|
fn intercept_enable_breakpoint(
|
|
&mut self,
|
|
_: &EnableBreakpoint,
|
|
_window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
// Only block breakpoint actions when the left (lhs) editor has focus
|
|
if let Some(lhs) = &self.lhs {
|
|
if lhs.was_last_focused {
|
|
cx.stop_propagation();
|
|
} else {
|
|
cx.propagate();
|
|
}
|
|
} else {
|
|
cx.propagate();
|
|
}
|
|
}
|
|
|
|
fn intercept_disable_breakpoint(
|
|
&mut self,
|
|
_: &DisableBreakpoint,
|
|
_window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
// Only block breakpoint actions when the left (lhs) editor has focus
|
|
if let Some(lhs) = &self.lhs {
|
|
if lhs.was_last_focused {
|
|
cx.stop_propagation();
|
|
} else {
|
|
cx.propagate();
|
|
}
|
|
} else {
|
|
cx.propagate();
|
|
}
|
|
}
|
|
|
|
fn intercept_edit_log_breakpoint(
|
|
&mut self,
|
|
_: &EditLogBreakpoint,
|
|
_window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
// Only block breakpoint actions when the left (lhs) editor has focus
|
|
if let Some(lhs) = &self.lhs {
|
|
if lhs.was_last_focused {
|
|
cx.stop_propagation();
|
|
} else {
|
|
cx.propagate();
|
|
}
|
|
} else {
|
|
cx.propagate();
|
|
}
|
|
}
|
|
|
|
fn intercept_inline_assist(
|
|
&mut self,
|
|
_: &InlineAssist,
|
|
_window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if self.lhs.is_some() {
|
|
cx.stop_propagation();
|
|
} else {
|
|
cx.propagate();
|
|
}
|
|
}
|
|
|
|
fn toggle_soft_wrap(
|
|
&mut self,
|
|
_: &ToggleSoftWrap,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if let Some(lhs) = &self.lhs {
|
|
cx.stop_propagation();
|
|
|
|
let is_lhs_focused = lhs.was_last_focused;
|
|
let (focused_editor, other_editor) = if is_lhs_focused {
|
|
(&lhs.editor, &self.rhs_editor)
|
|
} else {
|
|
(&self.rhs_editor, &lhs.editor)
|
|
};
|
|
|
|
// Toggle the focused editor
|
|
focused_editor.update(cx, |editor, cx| {
|
|
editor.toggle_soft_wrap(&ToggleSoftWrap, window, cx);
|
|
});
|
|
|
|
// Copy the soft wrap state from the focused editor to the other editor
|
|
let soft_wrap_override = focused_editor.read(cx).soft_wrap_mode_override;
|
|
other_editor.update(cx, |editor, cx| {
|
|
editor.soft_wrap_mode_override = soft_wrap_override;
|
|
cx.notify();
|
|
});
|
|
} else {
|
|
cx.propagate();
|
|
}
|
|
}
|
|
|
|
fn unsplit(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
|
let Some(lhs) = self.lhs.take() else {
|
|
return;
|
|
};
|
|
|
|
// Detach the stale lhs editor from the shared scroll anchor while the split companion still exists,
|
|
// so its anchor can be converted to lhs native before rhs tears down split specific state.
|
|
lhs.editor.update(cx, |editor, cx| {
|
|
let lhs_snapshot = editor.display_map.update(cx, |dm, cx| dm.snapshot(cx));
|
|
editor
|
|
.scroll_manager
|
|
.unshare_scroll_anchor(&lhs_snapshot, cx);
|
|
editor.set_on_local_selections_changed(None);
|
|
});
|
|
|
|
self.rhs_editor.update(cx, |rhs, cx| {
|
|
let rhs_snapshot = rhs.display_map.update(cx, |dm, cx| dm.snapshot(cx));
|
|
let native_anchor = rhs.scroll_manager.native_anchor(&rhs_snapshot, cx);
|
|
let rhs_display_map_id = rhs_snapshot.display_map_id;
|
|
rhs.scroll_manager
|
|
.scroll_anchor_entity()
|
|
.update(cx, |shared, _| {
|
|
shared.scroll_anchor = native_anchor;
|
|
shared.display_map_id = Some(rhs_display_map_id);
|
|
});
|
|
|
|
rhs.set_on_local_selections_changed(None);
|
|
rhs.set_delegate_expand_excerpts(false);
|
|
rhs.buffer().update(cx, |buffer, cx| {
|
|
buffer.set_show_deleted_hunks(true, cx);
|
|
buffer.set_use_extended_diff_range(false, cx);
|
|
});
|
|
rhs.display_map.update(cx, |dm, cx| {
|
|
dm.set_companion(None, cx);
|
|
});
|
|
});
|
|
cx.notify();
|
|
}
|
|
|
|
pub fn update_excerpts_for_path(
|
|
&mut self,
|
|
path: PathKey,
|
|
buffer: Entity<Buffer>,
|
|
ranges: impl IntoIterator<Item = Range<Point>> + Clone,
|
|
context_line_count: u32,
|
|
diff: Entity<BufferDiff>,
|
|
cx: &mut Context<Self>,
|
|
) -> bool {
|
|
let has_ranges = ranges.clone().into_iter().next().is_some();
|
|
if self.lhs.is_none() {
|
|
return self.rhs_multibuffer.update(cx, |rhs_multibuffer, cx| {
|
|
let added_a_new_excerpt = rhs_multibuffer.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
ranges,
|
|
context_line_count,
|
|
cx,
|
|
);
|
|
if has_ranges
|
|
&& rhs_multibuffer
|
|
.diff_for(buffer.read(cx).remote_id())
|
|
.is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
|
|
{
|
|
rhs_multibuffer.add_diff(diff, cx);
|
|
}
|
|
added_a_new_excerpt
|
|
});
|
|
}
|
|
|
|
let result = self.rhs_multibuffer.update(cx, |rhs_multibuffer, cx| {
|
|
let added_a_new_excerpt = rhs_multibuffer.update_excerpts_for_path(
|
|
path.clone(),
|
|
buffer.clone(),
|
|
ranges,
|
|
context_line_count,
|
|
cx,
|
|
);
|
|
if has_ranges
|
|
&& rhs_multibuffer
|
|
.diff_for(buffer.read(cx).remote_id())
|
|
.is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
|
|
{
|
|
rhs_multibuffer.add_diff(diff.clone(), cx);
|
|
}
|
|
added_a_new_excerpt
|
|
});
|
|
|
|
self.sync_lhs_for_paths(vec![(path, diff)], cx);
|
|
result
|
|
}
|
|
|
|
fn expand_excerpts(
|
|
&mut self,
|
|
excerpt_anchors: impl Iterator<Item = Anchor> + Clone,
|
|
lines: u32,
|
|
direction: ExpandExcerptDirection,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if self.lhs.is_none() {
|
|
self.rhs_multibuffer.update(cx, |rhs_multibuffer, cx| {
|
|
rhs_multibuffer.expand_excerpts(excerpt_anchors, lines, direction, cx);
|
|
});
|
|
return;
|
|
}
|
|
|
|
let paths: Vec<_> = self.rhs_multibuffer.update(cx, |rhs_multibuffer, cx| {
|
|
let snapshot = rhs_multibuffer.snapshot(cx);
|
|
let paths = excerpt_anchors
|
|
.clone()
|
|
.filter_map(|anchor| {
|
|
let (anchor, _) = snapshot.anchor_to_buffer_anchor(anchor)?;
|
|
let path = snapshot.path_for_buffer(anchor.buffer_id)?;
|
|
let diff = rhs_multibuffer.diff_for(anchor.buffer_id)?;
|
|
Some((path.clone(), diff))
|
|
})
|
|
.collect::<HashMap<_, _>>()
|
|
.into_iter()
|
|
.collect();
|
|
rhs_multibuffer.expand_excerpts(excerpt_anchors, lines, direction, cx);
|
|
paths
|
|
});
|
|
|
|
self.sync_lhs_for_paths(paths, cx);
|
|
}
|
|
|
|
pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
|
|
self.rhs_multibuffer.update(cx, |rhs_multibuffer, cx| {
|
|
rhs_multibuffer.remove_excerpts(path.clone(), cx);
|
|
});
|
|
|
|
if let Some(lhs) = &self.lhs {
|
|
lhs.multibuffer.update(cx, |lhs_multibuffer, cx| {
|
|
lhs_multibuffer.remove_excerpts(path, cx);
|
|
});
|
|
}
|
|
}
|
|
|
|
fn search_token(&self) -> SearchToken {
|
|
SearchToken::new(self.focused_side() as u64)
|
|
}
|
|
|
|
fn editor_for_token(&self, token: SearchToken) -> Option<&Entity<Editor>> {
|
|
if token.value() == SplitSide::Left as u64 {
|
|
return self.lhs.as_ref().map(|lhs| &lhs.editor);
|
|
}
|
|
Some(&self.rhs_editor)
|
|
}
|
|
|
|
fn sync_lhs_for_paths(
|
|
&self,
|
|
paths: Vec<(PathKey, Entity<BufferDiff>)>,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let Some(lhs) = &self.lhs else { return };
|
|
|
|
self.rhs_multibuffer.update(cx, |rhs_multibuffer, cx| {
|
|
for (path, diff) in paths {
|
|
let main_buffer_id = diff.read(cx).buffer_id;
|
|
let Some(main_buffer) = rhs_multibuffer.buffer(diff.read(cx).buffer_id) else {
|
|
lhs.multibuffer.update(cx, |lhs_multibuffer, lhs_cx| {
|
|
lhs_multibuffer.remove_excerpts(path, lhs_cx);
|
|
});
|
|
continue;
|
|
};
|
|
let main_buffer_snapshot = main_buffer.read(cx).snapshot();
|
|
|
|
let base_text_buffer = diff.read(cx).base_text_buffer().clone();
|
|
let diff_snapshot = diff.read(cx).snapshot(cx);
|
|
let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
|
|
|
|
let mut paired_ranges: Vec<(Range<Point>, ExcerptRange<text::Anchor>)> = Vec::new();
|
|
|
|
let mut have_excerpt = false;
|
|
let mut did_merge = false;
|
|
let rhs_multibuffer_snapshot = rhs_multibuffer.snapshot(cx);
|
|
for info in rhs_multibuffer_snapshot.excerpts_for_buffer(main_buffer_id) {
|
|
have_excerpt = true;
|
|
let rhs_context = info.context.to_point(&main_buffer_snapshot);
|
|
let lhs_context = buffer_range_to_base_text_range(
|
|
&rhs_context,
|
|
&diff_snapshot,
|
|
&main_buffer_snapshot,
|
|
);
|
|
|
|
if let Some((prev_lhs_context, prev_rhs_range)) = paired_ranges.last_mut()
|
|
&& prev_lhs_context.end >= lhs_context.start
|
|
{
|
|
did_merge = true;
|
|
prev_lhs_context.end = lhs_context.end;
|
|
prev_rhs_range.context.end = info.context.end;
|
|
continue;
|
|
}
|
|
|
|
paired_ranges.push((lhs_context, info));
|
|
}
|
|
|
|
let (lhs_ranges, rhs_ranges): (Vec<_>, Vec<_>) = paired_ranges.into_iter().unzip();
|
|
let lhs_ranges = lhs_ranges
|
|
.into_iter()
|
|
.map(|range| {
|
|
ExcerptRange::new(base_text_buffer_snapshot.anchor_range_outside(range))
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
lhs.multibuffer.update(cx, |lhs_multibuffer, lhs_cx| {
|
|
lhs_multibuffer.update_path_excerpts(
|
|
path.clone(),
|
|
base_text_buffer,
|
|
&base_text_buffer_snapshot,
|
|
&lhs_ranges,
|
|
lhs_cx,
|
|
);
|
|
if have_excerpt
|
|
&& lhs_multibuffer
|
|
.diff_for(base_text_buffer_snapshot.remote_id())
|
|
.is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
|
|
{
|
|
lhs_multibuffer.add_inverted_diff(
|
|
diff.clone(),
|
|
main_buffer.clone(),
|
|
lhs_cx,
|
|
);
|
|
}
|
|
});
|
|
|
|
if did_merge {
|
|
rhs_multibuffer.update_path_excerpts(
|
|
path,
|
|
main_buffer,
|
|
&main_buffer_snapshot,
|
|
&rhs_ranges,
|
|
cx,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
fn width_changed(&mut self, width: Pixels, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.last_width = Some(width);
|
|
|
|
let min_ems = EditorSettings::get_global(cx).minimum_split_diff_width;
|
|
|
|
let style = self.rhs_editor.read(cx).create_style(cx);
|
|
let font_id = window.text_system().resolve_font(&style.text.font());
|
|
let font_size = style.text.font_size.to_pixels(window.rem_size());
|
|
let em_advance = window
|
|
.text_system()
|
|
.em_advance(font_id, font_size)
|
|
.unwrap_or(font_size);
|
|
let min_width = em_advance * min_ems;
|
|
let is_split = self.lhs.is_some();
|
|
|
|
self.too_narrow_for_split = min_ems > 0.0 && width < min_width;
|
|
|
|
match self.diff_view_style {
|
|
DiffViewStyle::Unified => {}
|
|
DiffViewStyle::Split => {
|
|
if self.too_narrow_for_split && is_split {
|
|
self.unsplit(window, cx);
|
|
} else if !self.too_narrow_for_split && !is_split {
|
|
self.split(window, cx);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn remove_excerpts_for_buffer(
|
|
&mut self,
|
|
buffer_id: BufferId,
|
|
cx: &mut Context<'_, SplittableEditor>,
|
|
) {
|
|
let snapshot = self.rhs_multibuffer.read(cx).snapshot(cx);
|
|
let Some(path) = snapshot.path_for_buffer(buffer_id) else {
|
|
return;
|
|
};
|
|
self.remove_excerpts_for_path(path.clone(), cx);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
impl SplittableEditor {
|
|
fn check_invariants(&self, quiesced: bool, cx: &mut App) {
|
|
use text::Bias;
|
|
|
|
use crate::display_map::Block;
|
|
use crate::display_map::DisplayRow;
|
|
|
|
let rhs_snapshot = self
|
|
.rhs_editor
|
|
.update(cx, |editor, cx| editor.display_snapshot(cx));
|
|
|
|
let Some(lhs) = self.lhs.as_ref() else {
|
|
assert!(
|
|
rhs_snapshot.companion_snapshot().is_none(),
|
|
"rhs display snapshot should not have a companion when unsplit"
|
|
);
|
|
|
|
let shared_scroll_anchor = self
|
|
.rhs_editor
|
|
.read(cx)
|
|
.scroll_manager
|
|
.shared_scroll_anchor(cx);
|
|
if let Some(display_map_id) = shared_scroll_anchor.display_map_id {
|
|
assert_eq!(
|
|
display_map_id, rhs_snapshot.display_map_id,
|
|
"unsplit editor should not retain a scroll anchor native to a torn-down split companion"
|
|
);
|
|
}
|
|
|
|
let _ = self
|
|
.rhs_editor
|
|
.read(cx)
|
|
.scroll_manager
|
|
.native_anchor(&rhs_snapshot, cx);
|
|
return;
|
|
};
|
|
|
|
self.debug_print(cx);
|
|
self.check_excerpt_invariants(quiesced, cx);
|
|
|
|
let lhs_snapshot = lhs
|
|
.editor
|
|
.update(cx, |editor, cx| editor.display_snapshot(cx));
|
|
|
|
let lhs_companion = lhs_snapshot
|
|
.companion_snapshot()
|
|
.expect("lhs display snapshot should have rhs companion while split");
|
|
assert_eq!(
|
|
lhs_companion.display_map_id, rhs_snapshot.display_map_id,
|
|
"lhs display snapshot companion should point to rhs display map"
|
|
);
|
|
assert!(
|
|
lhs_companion.companion_snapshot().is_none(),
|
|
"embedded companion snapshot should not recursively contain another companion"
|
|
);
|
|
|
|
let rhs_companion = rhs_snapshot
|
|
.companion_snapshot()
|
|
.expect("rhs display snapshot should have lhs companion while split");
|
|
assert_eq!(
|
|
rhs_companion.display_map_id, lhs_snapshot.display_map_id,
|
|
"rhs display snapshot companion should point to lhs display map"
|
|
);
|
|
assert!(
|
|
rhs_companion.companion_snapshot().is_none(),
|
|
"embedded companion snapshot should not recursively contain another companion"
|
|
);
|
|
|
|
let lhs_scroll_anchor_entity_id = lhs
|
|
.editor
|
|
.read(cx)
|
|
.scroll_manager
|
|
.scroll_anchor_entity()
|
|
.entity_id();
|
|
let rhs_scroll_anchor_entity_id = self
|
|
.rhs_editor
|
|
.read(cx)
|
|
.scroll_manager
|
|
.scroll_anchor_entity()
|
|
.entity_id();
|
|
assert_eq!(
|
|
lhs_scroll_anchor_entity_id, rhs_scroll_anchor_entity_id,
|
|
"split editors should share a scroll anchor entity"
|
|
);
|
|
|
|
let shared_scroll_anchor = self
|
|
.rhs_editor
|
|
.read(cx)
|
|
.scroll_manager
|
|
.shared_scroll_anchor(cx);
|
|
if let Some(display_map_id) = shared_scroll_anchor.display_map_id {
|
|
assert!(
|
|
display_map_id == lhs_snapshot.display_map_id
|
|
|| display_map_id == rhs_snapshot.display_map_id,
|
|
"shared scroll anchor should be native to one side of the split"
|
|
);
|
|
}
|
|
let _ = lhs
|
|
.editor
|
|
.read(cx)
|
|
.scroll_manager
|
|
.native_anchor(&lhs_snapshot, cx);
|
|
let _ = self
|
|
.rhs_editor
|
|
.read(cx)
|
|
.scroll_manager
|
|
.native_anchor(&rhs_snapshot, cx);
|
|
|
|
if quiesced {
|
|
let lhs_max_row = lhs_snapshot.max_point().row();
|
|
let rhs_max_row = rhs_snapshot.max_point().row();
|
|
assert_eq!(lhs_max_row, rhs_max_row, "mismatch in display row count");
|
|
|
|
let lhs_excerpt_block_rows = lhs_snapshot
|
|
.blocks_in_range(DisplayRow(0)..lhs_max_row + 1)
|
|
.filter(|(_, block)| {
|
|
matches!(
|
|
block,
|
|
Block::BufferHeader { .. } | Block::ExcerptBoundary { .. }
|
|
)
|
|
})
|
|
.map(|(row, _)| row)
|
|
.collect::<Vec<_>>();
|
|
let rhs_excerpt_block_rows = rhs_snapshot
|
|
.blocks_in_range(DisplayRow(0)..rhs_max_row + 1)
|
|
.filter(|(_, block)| {
|
|
matches!(
|
|
block,
|
|
Block::BufferHeader { .. } | Block::ExcerptBoundary { .. }
|
|
)
|
|
})
|
|
.map(|(row, _)| row)
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(lhs_excerpt_block_rows, rhs_excerpt_block_rows);
|
|
|
|
for (lhs_hunk, rhs_hunk) in lhs_snapshot.diff_hunks().zip(rhs_snapshot.diff_hunks()) {
|
|
assert_eq!(
|
|
lhs_hunk.diff_base_byte_range, rhs_hunk.diff_base_byte_range,
|
|
"mismatch in hunks"
|
|
);
|
|
assert_eq!(
|
|
lhs_hunk.status, rhs_hunk.status,
|
|
"mismatch in hunk statuses"
|
|
);
|
|
|
|
let (lhs_point, rhs_point) =
|
|
if lhs_hunk.row_range.is_empty() || rhs_hunk.row_range.is_empty() {
|
|
use multi_buffer::ToPoint as _;
|
|
|
|
let lhs_end = Point::new(lhs_hunk.row_range.end.0, 0);
|
|
let rhs_end = Point::new(rhs_hunk.row_range.end.0, 0);
|
|
|
|
let lhs_excerpt_end = lhs_snapshot
|
|
.anchor_in_excerpt(lhs_hunk.excerpt_range.context.end)
|
|
.unwrap()
|
|
.to_point(&lhs_snapshot);
|
|
let lhs_exceeds = lhs_end >= lhs_excerpt_end;
|
|
let rhs_excerpt_end = rhs_snapshot
|
|
.anchor_in_excerpt(rhs_hunk.excerpt_range.context.end)
|
|
.unwrap()
|
|
.to_point(&rhs_snapshot);
|
|
let rhs_exceeds = rhs_end >= rhs_excerpt_end;
|
|
if lhs_exceeds != rhs_exceeds {
|
|
continue;
|
|
}
|
|
|
|
(lhs_end, rhs_end)
|
|
} else {
|
|
(
|
|
Point::new(lhs_hunk.row_range.start.0, 0),
|
|
Point::new(rhs_hunk.row_range.start.0, 0),
|
|
)
|
|
};
|
|
let lhs_point = lhs_snapshot.point_to_display_point(lhs_point, Bias::Left);
|
|
let rhs_point = rhs_snapshot.point_to_display_point(rhs_point, Bias::Left);
|
|
assert_eq!(
|
|
lhs_point.row(),
|
|
rhs_point.row(),
|
|
"mismatch in hunk position"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn debug_print(&self, cx: &mut App) {
|
|
use crate::DisplayRow;
|
|
use crate::display_map::Block;
|
|
use buffer_diff::DiffHunkStatusKind;
|
|
|
|
assert!(
|
|
self.lhs.is_some(),
|
|
"debug_print is only useful when lhs editor exists"
|
|
);
|
|
|
|
let lhs = self.lhs.as_ref().unwrap();
|
|
|
|
// Get terminal width, default to 80 if unavailable
|
|
let terminal_width = std::env::var("COLUMNS")
|
|
.ok()
|
|
.and_then(|s| s.parse::<usize>().ok())
|
|
.unwrap_or(80);
|
|
|
|
// Each side gets half the terminal width minus the separator
|
|
let separator = " │ ";
|
|
let side_width = (terminal_width - separator.len()) / 2;
|
|
|
|
// Get display snapshots for both editors
|
|
let lhs_snapshot = lhs.editor.update(cx, |editor, cx| {
|
|
editor.display_map.update(cx, |map, cx| map.snapshot(cx))
|
|
});
|
|
let rhs_snapshot = self.rhs_editor.update(cx, |editor, cx| {
|
|
editor.display_map.update(cx, |map, cx| map.snapshot(cx))
|
|
});
|
|
|
|
let lhs_max_row = lhs_snapshot.max_point().row().0;
|
|
let rhs_max_row = rhs_snapshot.max_point().row().0;
|
|
let max_row = lhs_max_row.max(rhs_max_row);
|
|
|
|
// Build a map from display row -> block type string
|
|
// Each row of a multi-row block gets an entry with the same block type
|
|
// For spacers, the ID is included in brackets
|
|
fn build_block_map(
|
|
snapshot: &crate::DisplaySnapshot,
|
|
max_row: u32,
|
|
) -> std::collections::HashMap<u32, String> {
|
|
let mut block_map = std::collections::HashMap::new();
|
|
for (start_row, block) in
|
|
snapshot.blocks_in_range(DisplayRow(0)..DisplayRow(max_row + 1))
|
|
{
|
|
let (block_type, height) = match block {
|
|
Block::Spacer {
|
|
id,
|
|
height,
|
|
is_below: _,
|
|
} => (format!("SPACER[{}]", id.0), *height),
|
|
Block::ExcerptBoundary { height, .. } => {
|
|
("EXCERPT_BOUNDARY".to_string(), *height)
|
|
}
|
|
Block::BufferHeader { height, .. } => ("BUFFER_HEADER".to_string(), *height),
|
|
Block::FoldedBuffer { height, .. } => ("FOLDED_BUFFER".to_string(), *height),
|
|
Block::Custom(custom) => {
|
|
("CUSTOM_BLOCK".to_string(), custom.height.unwrap_or(1))
|
|
}
|
|
};
|
|
for offset in 0..height {
|
|
block_map.insert(start_row.0 + offset, block_type.clone());
|
|
}
|
|
}
|
|
block_map
|
|
}
|
|
|
|
let lhs_blocks = build_block_map(&lhs_snapshot, lhs_max_row);
|
|
let rhs_blocks = build_block_map(&rhs_snapshot, rhs_max_row);
|
|
|
|
fn display_width(s: &str) -> usize {
|
|
unicode_width::UnicodeWidthStr::width(s)
|
|
}
|
|
|
|
fn truncate_line(line: &str, max_width: usize) -> String {
|
|
let line_width = display_width(line);
|
|
if line_width <= max_width {
|
|
return line.to_string();
|
|
}
|
|
if max_width < 9 {
|
|
let mut result = String::new();
|
|
let mut width = 0;
|
|
for c in line.chars() {
|
|
let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
|
|
if width + c_width > max_width {
|
|
break;
|
|
}
|
|
result.push(c);
|
|
width += c_width;
|
|
}
|
|
return result;
|
|
}
|
|
let ellipsis = "...";
|
|
let target_prefix_width = 3;
|
|
let target_suffix_width = 3;
|
|
|
|
let mut prefix = String::new();
|
|
let mut prefix_width = 0;
|
|
for c in line.chars() {
|
|
let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
|
|
if prefix_width + c_width > target_prefix_width {
|
|
break;
|
|
}
|
|
prefix.push(c);
|
|
prefix_width += c_width;
|
|
}
|
|
|
|
let mut suffix_chars: Vec<char> = Vec::new();
|
|
let mut suffix_width = 0;
|
|
for c in line.chars().rev() {
|
|
let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
|
|
if suffix_width + c_width > target_suffix_width {
|
|
break;
|
|
}
|
|
suffix_chars.push(c);
|
|
suffix_width += c_width;
|
|
}
|
|
suffix_chars.reverse();
|
|
let suffix: String = suffix_chars.into_iter().collect();
|
|
|
|
format!("{}{}{}", prefix, ellipsis, suffix)
|
|
}
|
|
|
|
fn pad_to_width(s: &str, target_width: usize) -> String {
|
|
let current_width = display_width(s);
|
|
if current_width >= target_width {
|
|
s.to_string()
|
|
} else {
|
|
format!("{}{}", s, " ".repeat(target_width - current_width))
|
|
}
|
|
}
|
|
|
|
// Helper to format a single row for one side
|
|
// Format: "ln# diff bytes(cumul) text" or block info
|
|
// Line numbers come from buffer_row in RowInfo (1-indexed for display)
|
|
fn format_row(
|
|
row: u32,
|
|
max_row: u32,
|
|
snapshot: &crate::DisplaySnapshot,
|
|
blocks: &std::collections::HashMap<u32, String>,
|
|
row_infos: &[multi_buffer::RowInfo],
|
|
cumulative_bytes: &[usize],
|
|
side_width: usize,
|
|
) -> String {
|
|
// Get row info if available
|
|
let row_info = row_infos.get(row as usize);
|
|
|
|
// Line number prefix (3 chars + space)
|
|
// Use buffer_row from RowInfo, which is None for block rows
|
|
let line_prefix = if row > max_row {
|
|
" ".to_string()
|
|
} else if let Some(buffer_row) = row_info.and_then(|info| info.buffer_row) {
|
|
format!("{:>3} ", buffer_row + 1) // 1-indexed for display
|
|
} else {
|
|
" ".to_string() // block rows have no line number
|
|
};
|
|
let content_width = side_width.saturating_sub(line_prefix.len());
|
|
|
|
if row > max_row {
|
|
return format!("{}{}", line_prefix, " ".repeat(content_width));
|
|
}
|
|
|
|
// Check if this row is a block row
|
|
if let Some(block_type) = blocks.get(&row) {
|
|
let block_str = format!("~~~[{}]~~~", block_type);
|
|
let formatted = format!("{:^width$}", block_str, width = content_width);
|
|
return format!(
|
|
"{}{}",
|
|
line_prefix,
|
|
truncate_line(&formatted, content_width)
|
|
);
|
|
}
|
|
|
|
// Get line text
|
|
let line_text = snapshot.line(DisplayRow(row));
|
|
let line_bytes = line_text.len();
|
|
|
|
// Diff status marker
|
|
let diff_marker = match row_info.and_then(|info| info.diff_status.as_ref()) {
|
|
Some(status) => match status.kind {
|
|
DiffHunkStatusKind::Added => "+",
|
|
DiffHunkStatusKind::Deleted => "-",
|
|
DiffHunkStatusKind::Modified => "~",
|
|
},
|
|
None => " ",
|
|
};
|
|
|
|
// Cumulative bytes
|
|
let cumulative = cumulative_bytes.get(row as usize).copied().unwrap_or(0);
|
|
|
|
// Format: "diff bytes(cumul) text" - use 3 digits for bytes, 4 for cumulative
|
|
let info_prefix = format!("{}{:>3}({:>4}) ", diff_marker, line_bytes, cumulative);
|
|
let text_width = content_width.saturating_sub(info_prefix.len());
|
|
let truncated_text = truncate_line(&line_text, text_width);
|
|
|
|
let text_part = pad_to_width(&truncated_text, text_width);
|
|
format!("{}{}{}", line_prefix, info_prefix, text_part)
|
|
}
|
|
|
|
// Collect row infos for both sides
|
|
let lhs_row_infos: Vec<_> = lhs_snapshot
|
|
.row_infos(DisplayRow(0))
|
|
.take((lhs_max_row + 1) as usize)
|
|
.collect();
|
|
let rhs_row_infos: Vec<_> = rhs_snapshot
|
|
.row_infos(DisplayRow(0))
|
|
.take((rhs_max_row + 1) as usize)
|
|
.collect();
|
|
|
|
// Calculate cumulative bytes for each side (only counting non-block rows)
|
|
let mut lhs_cumulative = Vec::with_capacity((lhs_max_row + 1) as usize);
|
|
let mut cumulative = 0usize;
|
|
for row in 0..=lhs_max_row {
|
|
if !lhs_blocks.contains_key(&row) {
|
|
cumulative += lhs_snapshot.line(DisplayRow(row)).len() + 1; // +1 for newline
|
|
}
|
|
lhs_cumulative.push(cumulative);
|
|
}
|
|
|
|
let mut rhs_cumulative = Vec::with_capacity((rhs_max_row + 1) as usize);
|
|
cumulative = 0;
|
|
for row in 0..=rhs_max_row {
|
|
if !rhs_blocks.contains_key(&row) {
|
|
cumulative += rhs_snapshot.line(DisplayRow(row)).len() + 1;
|
|
}
|
|
rhs_cumulative.push(cumulative);
|
|
}
|
|
|
|
// Print header
|
|
eprintln!();
|
|
eprintln!("{}", "═".repeat(terminal_width));
|
|
let header_left = format!("{:^width$}", "(LHS)", width = side_width);
|
|
let header_right = format!("{:^width$}", "(RHS)", width = side_width);
|
|
eprintln!("{}{}{}", header_left, separator, header_right);
|
|
eprintln!(
|
|
"{:^width$}{}{:^width$}",
|
|
"ln# diff len(cum) text",
|
|
separator,
|
|
"ln# diff len(cum) text",
|
|
width = side_width
|
|
);
|
|
eprintln!("{}", "─".repeat(terminal_width));
|
|
|
|
// Print each row
|
|
for row in 0..=max_row {
|
|
let left = format_row(
|
|
row,
|
|
lhs_max_row,
|
|
&lhs_snapshot,
|
|
&lhs_blocks,
|
|
&lhs_row_infos,
|
|
&lhs_cumulative,
|
|
side_width,
|
|
);
|
|
let right = format_row(
|
|
row,
|
|
rhs_max_row,
|
|
&rhs_snapshot,
|
|
&rhs_blocks,
|
|
&rhs_row_infos,
|
|
&rhs_cumulative,
|
|
side_width,
|
|
);
|
|
eprintln!("{}{}{}", left, separator, right);
|
|
}
|
|
|
|
eprintln!("{}", "═".repeat(terminal_width));
|
|
eprintln!("Legend: + added, - deleted, ~ modified, ~~~ block/spacer row");
|
|
eprintln!();
|
|
}
|
|
|
|
fn check_excerpt_invariants(&self, quiesced: bool, cx: &gpui::App) {
|
|
let lhs = self.lhs.as_ref().expect("should have lhs editor");
|
|
|
|
let rhs_snapshot = self.rhs_multibuffer.read(cx).snapshot(cx);
|
|
let rhs_excerpts = rhs_snapshot.excerpts().collect::<Vec<_>>();
|
|
let lhs_snapshot = lhs.multibuffer.read(cx).snapshot(cx);
|
|
let lhs_excerpts = lhs_snapshot.excerpts().collect::<Vec<_>>();
|
|
assert_eq!(lhs_excerpts.len(), rhs_excerpts.len());
|
|
|
|
for (lhs_excerpt, rhs_excerpt) in lhs_excerpts.into_iter().zip(rhs_excerpts) {
|
|
assert_eq!(
|
|
lhs_snapshot
|
|
.path_for_buffer(lhs_excerpt.context.start.buffer_id)
|
|
.unwrap(),
|
|
rhs_snapshot
|
|
.path_for_buffer(rhs_excerpt.context.start.buffer_id)
|
|
.unwrap(),
|
|
"corresponding excerpts should have the same path"
|
|
);
|
|
let diff = self
|
|
.rhs_multibuffer
|
|
.read(cx)
|
|
.diff_for(rhs_excerpt.context.start.buffer_id)
|
|
.expect("missing diff");
|
|
assert_eq!(
|
|
lhs_excerpt.context.start.buffer_id,
|
|
diff.read(cx).base_text(cx).remote_id(),
|
|
"corresponding lhs excerpt should show diff base text"
|
|
);
|
|
|
|
if quiesced {
|
|
let diff_snapshot = diff.read(cx).snapshot(cx);
|
|
let lhs_buffer_snapshot = lhs_snapshot
|
|
.buffer_for_id(lhs_excerpt.context.start.buffer_id)
|
|
.unwrap();
|
|
let rhs_buffer_snapshot = rhs_snapshot
|
|
.buffer_for_id(rhs_excerpt.context.start.buffer_id)
|
|
.unwrap();
|
|
let lhs_range = lhs_excerpt.context.to_point(&lhs_buffer_snapshot);
|
|
let rhs_range = rhs_excerpt.context.to_point(&rhs_buffer_snapshot);
|
|
let expected_lhs_range = buffer_range_to_base_text_range(
|
|
&rhs_range,
|
|
&diff_snapshot,
|
|
&rhs_buffer_snapshot,
|
|
);
|
|
assert_eq!(
|
|
lhs_range, expected_lhs_range,
|
|
"corresponding lhs excerpt should have a matching range"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Item for SplittableEditor {
|
|
type Event = EditorEvent;
|
|
|
|
fn tab_content_text(&self, detail: usize, cx: &App) -> ui::SharedString {
|
|
self.rhs_editor.read(cx).tab_content_text(detail, cx)
|
|
}
|
|
|
|
fn tab_tooltip_text(&self, cx: &App) -> Option<ui::SharedString> {
|
|
self.rhs_editor.read(cx).tab_tooltip_text(cx)
|
|
}
|
|
|
|
fn tab_icon(&self, window: &Window, cx: &App) -> Option<ui::Icon> {
|
|
self.rhs_editor.read(cx).tab_icon(window, cx)
|
|
}
|
|
|
|
fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> gpui::AnyElement {
|
|
self.rhs_editor.read(cx).tab_content(params, window, cx)
|
|
}
|
|
|
|
fn to_item_events(event: &EditorEvent, f: &mut dyn FnMut(ItemEvent)) {
|
|
Editor::to_item_events(event, f)
|
|
}
|
|
|
|
fn for_each_project_item(
|
|
&self,
|
|
cx: &App,
|
|
f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
|
|
) {
|
|
self.rhs_editor.read(cx).for_each_project_item(cx, f)
|
|
}
|
|
|
|
fn buffer_kind(&self, cx: &App) -> ItemBufferKind {
|
|
self.rhs_editor.read(cx).buffer_kind(cx)
|
|
}
|
|
|
|
fn active_project_path(&self, cx: &App) -> Option<project::ProjectPath> {
|
|
self.rhs_editor.read(cx).active_project_path(cx)
|
|
}
|
|
|
|
fn is_dirty(&self, cx: &App) -> bool {
|
|
self.rhs_editor.read(cx).is_dirty(cx)
|
|
}
|
|
|
|
fn has_conflict(&self, cx: &App) -> bool {
|
|
self.rhs_editor.read(cx).has_conflict(cx)
|
|
}
|
|
|
|
fn has_deleted_file(&self, cx: &App) -> bool {
|
|
self.rhs_editor.read(cx).has_deleted_file(cx)
|
|
}
|
|
|
|
fn capability(&self, cx: &App) -> language::Capability {
|
|
self.rhs_editor.read(cx).capability(cx)
|
|
}
|
|
|
|
fn can_save(&self, cx: &App) -> bool {
|
|
self.rhs_editor.read(cx).can_save(cx)
|
|
}
|
|
|
|
fn can_save_as(&self, cx: &App) -> bool {
|
|
self.rhs_editor.read(cx).can_save_as(cx)
|
|
}
|
|
|
|
fn save(
|
|
&mut self,
|
|
options: SaveOptions,
|
|
project: Entity<Project>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> gpui::Task<anyhow::Result<()>> {
|
|
self.rhs_editor
|
|
.update(cx, |editor, cx| editor.save(options, project, window, cx))
|
|
}
|
|
|
|
fn save_as(
|
|
&mut self,
|
|
project: Entity<Project>,
|
|
path: project::ProjectPath,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> gpui::Task<anyhow::Result<()>> {
|
|
self.rhs_editor
|
|
.update(cx, |editor, cx| editor.save_as(project, path, window, cx))
|
|
}
|
|
|
|
fn reload(
|
|
&mut self,
|
|
project: Entity<Project>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> gpui::Task<anyhow::Result<()>> {
|
|
self.rhs_editor
|
|
.update(cx, |editor, cx| editor.reload(project, window, cx))
|
|
}
|
|
|
|
fn navigate(
|
|
&mut self,
|
|
data: Arc<dyn std::any::Any + Send>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> bool {
|
|
self.focused_editor()
|
|
.update(cx, |editor, cx| editor.navigate(data, window, cx))
|
|
}
|
|
|
|
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.focused_editor().update(cx, |editor, cx| {
|
|
editor.deactivated(window, cx);
|
|
});
|
|
}
|
|
|
|
fn added_to_workspace(
|
|
&mut self,
|
|
workspace: &mut Workspace,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.workspace = workspace.weak_handle();
|
|
self.rhs_editor.update(cx, |rhs_editor, cx| {
|
|
rhs_editor.added_to_workspace(workspace, window, cx);
|
|
});
|
|
if let Some(lhs) = &self.lhs {
|
|
lhs.editor.update(cx, |lhs_editor, cx| {
|
|
lhs_editor.added_to_workspace(workspace, window, cx);
|
|
});
|
|
}
|
|
}
|
|
|
|
fn as_searchable(
|
|
&self,
|
|
handle: &Entity<Self>,
|
|
_: &App,
|
|
) -> Option<Box<dyn SearchableItemHandle>> {
|
|
Some(Box::new(handle.clone()))
|
|
}
|
|
|
|
fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation {
|
|
self.rhs_editor.read(cx).breadcrumb_location(cx)
|
|
}
|
|
|
|
fn breadcrumbs(&self, cx: &App) -> Option<(Vec<HighlightedText>, Option<Font>)> {
|
|
self.rhs_editor.read(cx).breadcrumbs(cx)
|
|
}
|
|
|
|
fn pixel_position_of_cursor(&self, cx: &App) -> Option<gpui::Point<gpui::Pixels>> {
|
|
self.focused_editor().read(cx).pixel_position_of_cursor(cx)
|
|
}
|
|
|
|
fn act_as_type<'a>(
|
|
&'a self,
|
|
type_id: std::any::TypeId,
|
|
self_handle: &'a Entity<Self>,
|
|
_: &'a App,
|
|
) -> Option<gpui::AnyEntity> {
|
|
if type_id == std::any::TypeId::of::<Self>() {
|
|
Some(self_handle.clone().into())
|
|
} else if type_id == std::any::TypeId::of::<Editor>() {
|
|
Some(self.rhs_editor.clone().into())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
impl SearchableItem for SplittableEditor {
|
|
type Match = Range<Anchor>;
|
|
|
|
fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.rhs_editor.update(cx, |editor, cx| {
|
|
editor.clear_matches(window, cx);
|
|
});
|
|
if let Some(lhs_editor) = self.lhs_editor() {
|
|
lhs_editor.update(cx, |editor, cx| {
|
|
editor.clear_matches(window, cx);
|
|
})
|
|
}
|
|
}
|
|
|
|
fn update_matches(
|
|
&mut self,
|
|
matches: &[Self::Match],
|
|
active_match_index: Option<usize>,
|
|
token: SearchToken,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let Some(target) = self.editor_for_token(token) else {
|
|
return;
|
|
};
|
|
target.update(cx, |editor, cx| {
|
|
editor.update_matches(matches, active_match_index, token, window, cx);
|
|
});
|
|
}
|
|
|
|
fn search_bar_visibility_changed(
|
|
&mut self,
|
|
visible: bool,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if visible {
|
|
let side = self.focused_side();
|
|
self.searched_side = Some(side);
|
|
match side {
|
|
SplitSide::Left => {
|
|
self.rhs_editor.update(cx, |editor, cx| {
|
|
editor.clear_matches(window, cx);
|
|
});
|
|
}
|
|
SplitSide::Right => {
|
|
if let Some(lhs) = &self.lhs {
|
|
lhs.editor.update(cx, |editor, cx| {
|
|
editor.clear_matches(window, cx);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
self.searched_side = None;
|
|
}
|
|
}
|
|
|
|
fn query_suggestion(
|
|
&mut self,
|
|
seed_query_override: Option<SeedQuerySetting>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> String {
|
|
self.focused_editor().update(cx, |editor, cx| {
|
|
editor.query_suggestion(seed_query_override, window, cx)
|
|
})
|
|
}
|
|
|
|
fn activate_match(
|
|
&mut self,
|
|
index: usize,
|
|
matches: &[Self::Match],
|
|
token: SearchToken,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let Some(target) = self.editor_for_token(token) else {
|
|
return;
|
|
};
|
|
target.update(cx, |editor, cx| {
|
|
editor.activate_match(index, matches, token, window, cx);
|
|
});
|
|
}
|
|
|
|
fn select_matches(
|
|
&mut self,
|
|
matches: &[Self::Match],
|
|
token: SearchToken,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let Some(target) = self.editor_for_token(token) else {
|
|
return;
|
|
};
|
|
target.update(cx, |editor, cx| {
|
|
editor.select_matches(matches, token, window, cx);
|
|
});
|
|
}
|
|
|
|
fn replace(
|
|
&mut self,
|
|
identifier: &Self::Match,
|
|
query: &project::search::SearchQuery,
|
|
token: SearchToken,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let Some(target) = self.editor_for_token(token) else {
|
|
return;
|
|
};
|
|
target.update(cx, |editor, cx| {
|
|
editor.replace(identifier, query, token, window, cx);
|
|
});
|
|
}
|
|
|
|
fn find_matches(
|
|
&mut self,
|
|
query: Arc<project::search::SearchQuery>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> gpui::Task<Vec<Self::Match>> {
|
|
self.focused_editor()
|
|
.update(cx, |editor, cx| editor.find_matches(query, window, cx))
|
|
}
|
|
|
|
fn find_matches_with_token(
|
|
&mut self,
|
|
query: Arc<project::search::SearchQuery>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> gpui::Task<(Vec<Self::Match>, SearchToken)> {
|
|
let token = self.search_token();
|
|
let editor = self.focused_editor().downgrade();
|
|
cx.spawn_in(window, async move |_, cx| {
|
|
let Some(matches) = editor
|
|
.update_in(cx, |editor, window, cx| {
|
|
editor.find_matches(query, window, cx)
|
|
})
|
|
.ok()
|
|
else {
|
|
return (Vec::new(), token);
|
|
};
|
|
(matches.await, token)
|
|
})
|
|
}
|
|
|
|
fn active_match_index(
|
|
&mut self,
|
|
direction: workspace::searchable::Direction,
|
|
matches: &[Self::Match],
|
|
token: SearchToken,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Option<usize> {
|
|
self.editor_for_token(token)?.update(cx, |editor, cx| {
|
|
editor.active_match_index(direction, matches, token, window, cx)
|
|
})
|
|
}
|
|
}
|
|
|
|
impl EventEmitter<EditorEvent> for SplittableEditor {}
|
|
impl EventEmitter<SearchEvent> for SplittableEditor {}
|
|
impl Focusable for SplittableEditor {
|
|
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
|
|
self.focused_editor().read(cx).focus_handle(cx)
|
|
}
|
|
}
|
|
|
|
impl Render for SplittableEditor {
|
|
fn render(
|
|
&mut self,
|
|
_window: &mut ui::Window,
|
|
cx: &mut ui::Context<Self>,
|
|
) -> impl ui::IntoElement {
|
|
let is_split = self.lhs.is_some();
|
|
let inner = if is_split {
|
|
let style = self.rhs_editor.read(cx).create_style(cx);
|
|
SplitEditorView::new(cx.entity(), style, self.split_state.clone()).into_any_element()
|
|
} else {
|
|
self.rhs_editor.clone().into_any_element()
|
|
};
|
|
|
|
let this = cx.entity().downgrade();
|
|
let last_width = self.last_width;
|
|
|
|
div()
|
|
.id("splittable-editor")
|
|
.on_action(cx.listener(Self::toggle_split))
|
|
.on_action(cx.listener(Self::activate_pane_left))
|
|
.on_action(cx.listener(Self::activate_pane_right))
|
|
.on_action(cx.listener(Self::intercept_toggle_breakpoint))
|
|
.on_action(cx.listener(Self::intercept_enable_breakpoint))
|
|
.on_action(cx.listener(Self::intercept_disable_breakpoint))
|
|
.on_action(cx.listener(Self::intercept_edit_log_breakpoint))
|
|
.on_action(cx.listener(Self::intercept_inline_assist))
|
|
.capture_action(cx.listener(Self::toggle_soft_wrap))
|
|
.size_full()
|
|
.child(inner)
|
|
.child(
|
|
canvas(
|
|
move |bounds, window, cx| {
|
|
let width = bounds.size.width;
|
|
if last_width == Some(width) {
|
|
return;
|
|
}
|
|
window.defer(cx, move |window, cx| {
|
|
this.update(cx, |this, cx| {
|
|
this.width_changed(width, window, cx);
|
|
})
|
|
.ok();
|
|
});
|
|
},
|
|
|_, _, _, _| {},
|
|
)
|
|
.absolute()
|
|
.size_full(),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::{any::TypeId, sync::Arc};
|
|
|
|
use buffer_diff::BufferDiff;
|
|
use collections::{HashMap, HashSet};
|
|
use fs::FakeFs;
|
|
use gpui::{AppContext as _, Entity, Pixels, VisualTestContext};
|
|
use gpui::{BorrowAppContext as _, Element as _};
|
|
use language::language_settings::SoftWrap;
|
|
use language::{Buffer, Capability};
|
|
use multi_buffer::{MultiBuffer, PathKey};
|
|
use pretty_assertions::assert_eq;
|
|
use project::Project;
|
|
use rand::rngs::StdRng;
|
|
use settings::{DiffViewStyle, SettingsStore};
|
|
use ui::{VisualContext as _, div, px};
|
|
use util::rel_path::rel_path;
|
|
use workspace::{Item, MultiWorkspace};
|
|
|
|
use crate::display_map::{
|
|
BlockPlacement, BlockProperties, BlockStyle, Crease, FoldPlaceholder,
|
|
};
|
|
use crate::inlays::Inlay;
|
|
use crate::test::{editor_content_with_blocks_and_width, set_block_content_for_tests};
|
|
use crate::{Editor, SplittableEditor};
|
|
use multi_buffer::MultiBufferOffset;
|
|
|
|
async fn init_test(
|
|
cx: &mut gpui::TestAppContext,
|
|
soft_wrap: SoftWrap,
|
|
style: DiffViewStyle,
|
|
) -> (Entity<SplittableEditor>, &mut VisualTestContext) {
|
|
cx.update(|cx| {
|
|
let store = SettingsStore::test(cx);
|
|
cx.set_global(store);
|
|
cx.update_global::<SettingsStore, _>(|store, cx| {
|
|
store.update_user_settings(cx, |settings| {
|
|
settings.editor.diff_view_style = Some(style);
|
|
});
|
|
});
|
|
theme_settings::init(theme::LoadThemes::JustBase, cx);
|
|
crate::init(cx);
|
|
});
|
|
let fs = FakeFs::new(cx.executor());
|
|
let project = Project::test(fs as Arc<dyn fs::Fs>, [], cx).await;
|
|
let (multi_workspace, cx) =
|
|
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
|
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
|
|
let rhs_multibuffer = cx.new(|cx| {
|
|
let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
|
|
multibuffer.set_all_diff_hunks_expanded(cx);
|
|
multibuffer
|
|
});
|
|
let editor = cx.new_window_entity(|window, cx| {
|
|
let editor = SplittableEditor::new(
|
|
style,
|
|
rhs_multibuffer.clone(),
|
|
project.clone(),
|
|
workspace,
|
|
window,
|
|
cx,
|
|
);
|
|
editor.update_editors(cx, |editor, cx| {
|
|
editor.set_soft_wrap_mode(soft_wrap, cx);
|
|
});
|
|
editor
|
|
});
|
|
(editor, cx)
|
|
}
|
|
|
|
fn buffer_with_diff(
|
|
base_text: &str,
|
|
current_text: &str,
|
|
cx: &mut VisualTestContext,
|
|
) -> (Entity<Buffer>, Entity<BufferDiff>) {
|
|
let buffer = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
|
|
let diff = cx.new(|cx| {
|
|
BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
|
|
});
|
|
(buffer, diff)
|
|
}
|
|
|
|
#[track_caller]
|
|
fn assert_split_content(
|
|
editor: &Entity<SplittableEditor>,
|
|
expected_rhs: String,
|
|
expected_lhs: String,
|
|
cx: &mut VisualTestContext,
|
|
) {
|
|
assert_split_content_with_widths(
|
|
editor,
|
|
px(3000.0),
|
|
px(3000.0),
|
|
expected_rhs,
|
|
expected_lhs,
|
|
cx,
|
|
);
|
|
}
|
|
|
|
#[track_caller]
|
|
fn assert_split_content_with_widths(
|
|
editor: &Entity<SplittableEditor>,
|
|
rhs_width: Pixels,
|
|
lhs_width: Pixels,
|
|
expected_rhs: String,
|
|
expected_lhs: String,
|
|
cx: &mut VisualTestContext,
|
|
) {
|
|
let (rhs_editor, lhs_editor) = editor.update(cx, |editor, _cx| {
|
|
let lhs = editor.lhs.as_ref().expect("should have lhs editor");
|
|
(editor.rhs_editor.clone(), lhs.editor.clone())
|
|
});
|
|
|
|
// Make sure both sides learn if the other has soft-wrapped
|
|
let _ = editor_content_with_blocks_and_width(&rhs_editor, rhs_width, cx);
|
|
cx.run_until_parked();
|
|
let _ = editor_content_with_blocks_and_width(&lhs_editor, lhs_width, cx);
|
|
cx.run_until_parked();
|
|
|
|
let rhs_content = editor_content_with_blocks_and_width(&rhs_editor, rhs_width, cx);
|
|
let lhs_content = editor_content_with_blocks_and_width(&lhs_editor, lhs_width, cx);
|
|
|
|
if rhs_content != expected_rhs || lhs_content != expected_lhs {
|
|
editor.update(cx, |editor, cx| editor.debug_print(cx));
|
|
}
|
|
|
|
assert_eq!(rhs_content, expected_rhs, "rhs");
|
|
assert_eq!(lhs_content, expected_lhs, "lhs");
|
|
}
|
|
|
|
#[gpui::test(iterations = 25)]
|
|
async fn test_random_split_editor(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
|
|
use multi_buffer::ExpandExcerptDirection;
|
|
use rand::prelude::*;
|
|
use util::RandomCharIter;
|
|
|
|
let (editor, cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
|
|
let operations = std::env::var("OPERATIONS")
|
|
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
|
.unwrap_or(10);
|
|
let rng = &mut rng;
|
|
for _ in 0..operations {
|
|
let buffers = editor.update(cx, |editor, cx| {
|
|
editor.rhs_editor.read(cx).buffer().read(cx).all_buffers()
|
|
});
|
|
|
|
if buffers.is_empty() {
|
|
log::info!("creating initial buffer");
|
|
let len = rng.random_range(200..1000);
|
|
let base_text: String = RandomCharIter::new(&mut *rng).take(len).collect();
|
|
let buffer = cx.new(|cx| Buffer::local(base_text.clone(), cx));
|
|
let buffer_snapshot = buffer.read_with(cx, |b, _| b.text_snapshot());
|
|
let diff =
|
|
cx.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer_snapshot, cx));
|
|
let edit_count = rng.random_range(3..8);
|
|
buffer.update(cx, |buffer, cx| {
|
|
buffer.randomly_edit(rng, edit_count, cx);
|
|
});
|
|
let buffer_snapshot = buffer.read_with(cx, |b, _| b.text_snapshot());
|
|
diff.update(cx, |diff, cx| {
|
|
diff.recalculate_diff_sync(&buffer_snapshot, cx);
|
|
});
|
|
let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
|
|
let ranges = diff_snapshot
|
|
.hunks(&buffer_snapshot)
|
|
.map(|hunk| hunk.range)
|
|
.collect::<Vec<_>>();
|
|
let context_lines = rng.random_range(0..2);
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::for_buffer(&buffer, cx);
|
|
editor.update_excerpts_for_path(path, buffer, ranges, context_lines, diff, cx);
|
|
});
|
|
editor.update(cx, |editor, cx| {
|
|
editor.check_invariants(true, cx);
|
|
});
|
|
continue;
|
|
}
|
|
|
|
let mut quiesced = false;
|
|
|
|
match rng.random_range(0..100) {
|
|
0..=14 if buffers.len() < 6 => {
|
|
log::info!("creating new buffer and setting excerpts");
|
|
let len = rng.random_range(200..1000);
|
|
let base_text: String = RandomCharIter::new(&mut *rng).take(len).collect();
|
|
let buffer = cx.new(|cx| Buffer::local(base_text.clone(), cx));
|
|
let buffer_snapshot = buffer.read_with(cx, |b, _| b.text_snapshot());
|
|
let diff = cx
|
|
.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer_snapshot, cx));
|
|
let edit_count = rng.random_range(3..8);
|
|
buffer.update(cx, |buffer, cx| {
|
|
buffer.randomly_edit(rng, edit_count, cx);
|
|
});
|
|
let buffer_snapshot = buffer.read_with(cx, |b, _| b.text_snapshot());
|
|
diff.update(cx, |diff, cx| {
|
|
diff.recalculate_diff_sync(&buffer_snapshot, cx);
|
|
});
|
|
let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
|
|
let ranges = diff_snapshot
|
|
.hunks(&buffer_snapshot)
|
|
.map(|hunk| hunk.range)
|
|
.collect::<Vec<_>>();
|
|
let context_lines = rng.random_range(0..2);
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::for_buffer(&buffer, cx);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer,
|
|
ranges,
|
|
context_lines,
|
|
diff,
|
|
cx,
|
|
);
|
|
});
|
|
}
|
|
15..=29 => {
|
|
log::info!("randomly editing multibuffer");
|
|
let edit_count = rng.random_range(1..5);
|
|
editor.update(cx, |editor, cx| {
|
|
editor.rhs_multibuffer.update(cx, |multibuffer, cx| {
|
|
multibuffer.randomly_edit(rng, edit_count, cx);
|
|
});
|
|
});
|
|
}
|
|
30..=44 => {
|
|
log::info!("randomly editing individual buffer");
|
|
let buffer = buffers.iter().choose(rng).unwrap();
|
|
let edit_count = rng.random_range(1..3);
|
|
buffer.update(cx, |buffer, cx| {
|
|
buffer.randomly_edit(rng, edit_count, cx);
|
|
});
|
|
}
|
|
45..=54 => {
|
|
log::info!("recalculating diff and resetting excerpts for single buffer");
|
|
let buffer = buffers.iter().choose(rng).unwrap();
|
|
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
|
let diff = editor.update(cx, |editor, cx| {
|
|
editor
|
|
.rhs_multibuffer
|
|
.read(cx)
|
|
.diff_for(buffer.read(cx).remote_id())
|
|
.unwrap()
|
|
});
|
|
diff.update(cx, |diff, cx| {
|
|
diff.recalculate_diff_sync(&buffer_snapshot, cx);
|
|
});
|
|
cx.run_until_parked();
|
|
let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
|
|
let ranges = diff_snapshot
|
|
.hunks(&buffer_snapshot)
|
|
.map(|hunk| hunk.range)
|
|
.collect::<Vec<_>>();
|
|
let context_lines = rng.random_range(0..2);
|
|
let buffer = buffer.clone();
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::for_buffer(&buffer, cx);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer,
|
|
ranges,
|
|
context_lines,
|
|
diff,
|
|
cx,
|
|
);
|
|
});
|
|
}
|
|
55..=64 => {
|
|
log::info!("randomly undoing/redoing in single buffer");
|
|
let buffer = buffers.iter().choose(rng).unwrap();
|
|
buffer.update(cx, |buffer, cx| {
|
|
buffer.randomly_undo_redo(rng, cx);
|
|
});
|
|
}
|
|
65..=74 => {
|
|
log::info!("removing excerpts for a random path");
|
|
let ids = editor.update(cx, |editor, cx| {
|
|
let snapshot = editor.rhs_multibuffer.read(cx).snapshot(cx);
|
|
snapshot.all_buffer_ids().collect::<Vec<_>>()
|
|
});
|
|
if let Some(id) = ids.choose(rng) {
|
|
editor.update(cx, |editor, cx| {
|
|
let snapshot = editor.rhs_multibuffer.read(cx).snapshot(cx);
|
|
let path = snapshot.path_for_buffer(*id).unwrap();
|
|
editor.remove_excerpts_for_path(path.clone(), cx);
|
|
});
|
|
}
|
|
}
|
|
75..=79 => {
|
|
log::info!("unsplit, scroll stale lhs, and resplit");
|
|
let Some(lhs_editor) = editor.update(cx, |editor, _cx| {
|
|
editor.lhs.as_ref().map(|lhs| lhs.editor.clone())
|
|
}) else {
|
|
continue;
|
|
};
|
|
let lhs_max_row = lhs_editor.update(cx, |editor, cx| {
|
|
editor.display_snapshot(cx).max_point().row().0
|
|
});
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
editor.unsplit(window, cx);
|
|
});
|
|
cx.run_until_parked();
|
|
|
|
if lhs_max_row > 0 {
|
|
lhs_editor.update_in(cx, |editor, window, cx| {
|
|
editor.set_scroll_position(gpui::Point::new(0., 1.), window, cx);
|
|
});
|
|
editor.update(cx, |editor, cx| {
|
|
editor.check_invariants(false, cx);
|
|
});
|
|
}
|
|
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
editor.split(window, cx);
|
|
});
|
|
}
|
|
80..=89 => {
|
|
let snapshot = editor.update(cx, |editor, cx| {
|
|
editor.rhs_multibuffer.read(cx).snapshot(cx)
|
|
});
|
|
let excerpts = snapshot.excerpts().collect::<Vec<_>>();
|
|
if !excerpts.is_empty() {
|
|
let count = rng.random_range(1..=excerpts.len().min(3));
|
|
let chosen: Vec<_> =
|
|
excerpts.choose_multiple(rng, count).cloned().collect();
|
|
let line_count = rng.random_range(1..5);
|
|
log::info!("expanding {count} excerpts by {line_count} lines");
|
|
editor.update(cx, |editor, cx| {
|
|
editor.expand_excerpts(
|
|
chosen.into_iter().map(|excerpt| {
|
|
snapshot.anchor_in_excerpt(excerpt.context.start).unwrap()
|
|
}),
|
|
line_count,
|
|
ExpandExcerptDirection::UpAndDown,
|
|
cx,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
_ => {
|
|
log::info!("quiescing");
|
|
for buffer in buffers {
|
|
let buffer_snapshot =
|
|
buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
|
let diff = editor.update(cx, |editor, cx| {
|
|
editor
|
|
.rhs_multibuffer
|
|
.read(cx)
|
|
.diff_for(buffer.read(cx).remote_id())
|
|
.unwrap()
|
|
});
|
|
diff.update(cx, |diff, cx| {
|
|
diff.recalculate_diff_sync(&buffer_snapshot, cx);
|
|
});
|
|
cx.run_until_parked();
|
|
let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
|
|
let ranges = diff_snapshot
|
|
.hunks(&buffer_snapshot)
|
|
.map(|hunk| hunk.range)
|
|
.collect::<Vec<_>>();
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::for_buffer(&buffer, cx);
|
|
editor.update_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
|
|
});
|
|
}
|
|
quiesced = true;
|
|
}
|
|
}
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
editor.check_invariants(quiesced, cx);
|
|
});
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_expand_excerpt_with_hunk_before_excerpt_start(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
|
|
let (editor, cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "aaaaaaa rest_of_line\nsecond_line\nthird_line\nfourth_line";
|
|
let current_text = "aaaaaaa rest_of_line\nsecond_line\nMODIFIED\nfourth_line";
|
|
let (buffer, diff) = buffer_with_diff(base_text, current_text, cx);
|
|
|
|
let buffer_snapshot = buffer.read_with(cx, |b, _| b.text_snapshot());
|
|
diff.update(cx, |diff, cx| {
|
|
diff.recalculate_diff_sync(&buffer_snapshot, cx);
|
|
});
|
|
cx.run_until_parked();
|
|
|
|
let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
|
|
let ranges = diff_snapshot
|
|
.hunks(&buffer_snapshot)
|
|
.map(|hunk| hunk.range)
|
|
.collect::<Vec<_>>();
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(path, buffer.clone(), ranges, 0, diff.clone(), cx);
|
|
});
|
|
cx.run_until_parked();
|
|
|
|
buffer.update(cx, |buffer, cx| {
|
|
buffer.edit(
|
|
[(Point::new(0, 7)..Point::new(1, 7), "\nnew_line\n")],
|
|
None,
|
|
cx,
|
|
);
|
|
});
|
|
|
|
let excerpts = editor.update(cx, |editor, cx| {
|
|
let snapshot = editor.rhs_multibuffer.read(cx).snapshot(cx);
|
|
snapshot
|
|
.excerpts()
|
|
.map(|excerpt| snapshot.anchor_in_excerpt(excerpt.context.start).unwrap())
|
|
.collect::<Vec<_>>()
|
|
});
|
|
editor.update(cx, |editor, cx| {
|
|
editor.expand_excerpts(
|
|
excerpts.into_iter(),
|
|
2,
|
|
multi_buffer::ExpandExcerptDirection::UpAndDown,
|
|
cx,
|
|
);
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_basic_alignment(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee
|
|
fff
|
|
"
|
|
.unindent();
|
|
let current_text = "
|
|
aaa
|
|
ddd
|
|
eee
|
|
fff
|
|
"
|
|
.unindent();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..buffer.read(cx).max_point()],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
§ spacer
|
|
§ spacer
|
|
ddd
|
|
eee
|
|
fff"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee
|
|
fff"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
buffer.update(cx, |buffer, cx| {
|
|
buffer.edit([(Point::new(3, 0)..Point::new(3, 3), "FFF")], None, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
§ spacer
|
|
§ spacer
|
|
ddd
|
|
eee
|
|
FFF"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee
|
|
fff"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
|
diff.update(cx, |diff, cx| {
|
|
diff.recalculate_diff_sync(&buffer_snapshot, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
§ spacer
|
|
§ spacer
|
|
ddd
|
|
eee
|
|
FFF"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee
|
|
fff"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_deleting_unmodified_lines(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
|
|
|
|
let base_text1 = "
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee"
|
|
.unindent();
|
|
|
|
let base_text2 = "
|
|
fff
|
|
ggg
|
|
hhh
|
|
iii
|
|
jjj"
|
|
.unindent();
|
|
|
|
let (buffer1, diff1) = buffer_with_diff(&base_text1, &base_text1, &mut cx);
|
|
let (buffer2, diff2) = buffer_with_diff(&base_text2, &base_text2, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path1 = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path1,
|
|
buffer1.clone(),
|
|
vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
|
|
0,
|
|
diff1.clone(),
|
|
cx,
|
|
);
|
|
let path2 = PathKey::sorted(1);
|
|
editor.update_excerpts_for_path(
|
|
path2,
|
|
buffer2.clone(),
|
|
vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
|
|
1,
|
|
diff2.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
buffer1.update(cx, |buffer, cx| {
|
|
buffer.edit(
|
|
[
|
|
(Point::new(0, 0)..Point::new(1, 0), ""),
|
|
(Point::new(3, 0)..Point::new(4, 0), ""),
|
|
],
|
|
None,
|
|
cx,
|
|
);
|
|
});
|
|
buffer2.update(cx, |buffer, cx| {
|
|
buffer.edit(
|
|
[
|
|
(Point::new(0, 0)..Point::new(1, 0), ""),
|
|
(Point::new(3, 0)..Point::new(4, 0), ""),
|
|
],
|
|
None,
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
bbb
|
|
ccc
|
|
§ spacer
|
|
eee
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
ggg
|
|
hhh
|
|
§ spacer
|
|
jjj"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee
|
|
§ <no file>
|
|
§ -----
|
|
fff
|
|
ggg
|
|
hhh
|
|
iii
|
|
jjj"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
let buffer1_snapshot = buffer1.read_with(cx, |buffer, _| buffer.text_snapshot());
|
|
diff1.update(cx, |diff, cx| {
|
|
diff.recalculate_diff_sync(&buffer1_snapshot, cx);
|
|
});
|
|
let buffer2_snapshot = buffer2.read_with(cx, |buffer, _| buffer.text_snapshot());
|
|
diff2.update(cx, |diff, cx| {
|
|
diff.recalculate_diff_sync(&buffer2_snapshot, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
bbb
|
|
ccc
|
|
§ spacer
|
|
eee
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
ggg
|
|
hhh
|
|
§ spacer
|
|
jjj"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee
|
|
§ <no file>
|
|
§ -----
|
|
fff
|
|
ggg
|
|
hhh
|
|
iii
|
|
jjj"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_deleting_added_line(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
"
|
|
.unindent();
|
|
|
|
let current_text = "
|
|
aaa
|
|
NEW1
|
|
NEW2
|
|
ccc
|
|
ddd
|
|
"
|
|
.unindent();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..buffer.read(cx).max_point()],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
NEW1
|
|
NEW2
|
|
ccc
|
|
ddd"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
§ spacer
|
|
ccc
|
|
ddd"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
buffer.update(cx, |buffer, cx| {
|
|
buffer.edit([(Point::new(2, 0)..Point::new(3, 0), "")], None, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
NEW1
|
|
ccc
|
|
ddd"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
|
diff.update(cx, |diff, cx| {
|
|
diff.recalculate_diff_sync(&buffer_snapshot, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
NEW1
|
|
ccc
|
|
ddd"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_inserting_consecutive_blank_line(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "
|
|
aaa
|
|
bbb
|
|
|
|
|
|
|
|
|
|
|
|
ccc
|
|
ddd
|
|
"
|
|
.unindent();
|
|
let current_text = "
|
|
aaa
|
|
bbb
|
|
|
|
|
|
|
|
|
|
|
|
CCC
|
|
ddd
|
|
"
|
|
.unindent();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..buffer.read(cx).max_point()],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
buffer.update(cx, |buffer, cx| {
|
|
buffer.edit([(Point::new(1, 3)..Point::new(1, 3), "\n")], None, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CCC
|
|
ddd"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
§ spacer
|
|
|
|
|
|
|
|
|
|
|
|
ccc
|
|
ddd"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
|
diff.update(cx, |diff, cx| {
|
|
diff.recalculate_diff_sync(&buffer_snapshot, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CCC
|
|
ddd"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
|
|
|
|
|
|
|
|
|
|
ccc
|
|
§ spacer
|
|
ddd"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_reverting_deletion_hunk(cx: &mut gpui::TestAppContext) {
|
|
use git::Restore;
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee
|
|
"
|
|
.unindent();
|
|
let current_text = "
|
|
aaa
|
|
ddd
|
|
eee
|
|
"
|
|
.unindent();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..buffer.read(cx).max_point()],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
§ spacer
|
|
§ spacer
|
|
ddd
|
|
eee"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
let rhs_editor = editor.update(cx, |editor, _cx| editor.rhs_editor.clone());
|
|
cx.update_window_entity(&rhs_editor, |editor, window, cx| {
|
|
editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select_ranges([Point::new(1, 0)..Point::new(1, 0)]);
|
|
});
|
|
editor.git_restore(&Restore, window, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
|
diff.update(cx, |diff, cx| {
|
|
diff.recalculate_diff_sync(&buffer_snapshot, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_deleting_added_lines(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "
|
|
aaa
|
|
old1
|
|
old2
|
|
old3
|
|
old4
|
|
zzz
|
|
"
|
|
.unindent();
|
|
|
|
let current_text = "
|
|
aaa
|
|
new1
|
|
new2
|
|
new3
|
|
new4
|
|
zzz
|
|
"
|
|
.unindent();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..buffer.read(cx).max_point()],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
buffer.update(cx, |buffer, cx| {
|
|
buffer.edit(
|
|
[
|
|
(Point::new(2, 0)..Point::new(3, 0), ""),
|
|
(Point::new(4, 0)..Point::new(5, 0), ""),
|
|
],
|
|
None,
|
|
cx,
|
|
);
|
|
});
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
new1
|
|
new3
|
|
§ spacer
|
|
§ spacer
|
|
zzz"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
old1
|
|
old2
|
|
old3
|
|
old4
|
|
zzz"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
|
diff.update(cx, |diff, cx| {
|
|
diff.recalculate_diff_sync(&buffer_snapshot, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
new1
|
|
new3
|
|
§ spacer
|
|
§ spacer
|
|
zzz"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
old1
|
|
old2
|
|
old3
|
|
old4
|
|
zzz"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_soft_wrap_at_end_of_excerpt(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
|
|
|
|
let text = "aaaa bbbb cccc dddd eeee ffff";
|
|
|
|
let (buffer1, diff1) = buffer_with_diff(text, text, &mut cx);
|
|
let (buffer2, diff2) = buffer_with_diff(text, text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let end = Point::new(0, text.len() as u32);
|
|
let path1 = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path1,
|
|
buffer1.clone(),
|
|
vec![Point::new(0, 0)..end],
|
|
0,
|
|
diff1.clone(),
|
|
cx,
|
|
);
|
|
let path2 = PathKey::sorted(1);
|
|
editor.update_excerpts_for_path(
|
|
path2,
|
|
buffer2.clone(),
|
|
vec![Point::new(0, 0)..end],
|
|
0,
|
|
diff2.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content_with_widths(
|
|
&editor,
|
|
px(200.0),
|
|
px(400.0),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaaa bbbb\x20
|
|
cccc dddd\x20
|
|
eeee ffff
|
|
§ <no file>
|
|
§ -----
|
|
aaaa bbbb\x20
|
|
cccc dddd\x20
|
|
eeee ffff"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaaa bbbb cccc dddd eeee ffff
|
|
§ spacer
|
|
§ spacer
|
|
§ <no file>
|
|
§ -----
|
|
aaaa bbbb cccc dddd eeee ffff
|
|
§ spacer
|
|
§ spacer"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_soft_wrap_before_modification_hunk(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "
|
|
aaaa bbbb cccc dddd eeee ffff
|
|
old line one
|
|
old line two
|
|
"
|
|
.unindent();
|
|
|
|
let current_text = "
|
|
aaaa bbbb cccc dddd eeee ffff
|
|
new line
|
|
"
|
|
.unindent();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..buffer.read(cx).max_point()],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content_with_widths(
|
|
&editor,
|
|
px(200.0),
|
|
px(400.0),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaaa bbbb\x20
|
|
cccc dddd\x20
|
|
eeee ffff
|
|
new line
|
|
§ spacer"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaaa bbbb cccc dddd eeee ffff
|
|
§ spacer
|
|
§ spacer
|
|
old line one
|
|
old line two"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_soft_wrap_before_deletion_hunk(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "
|
|
aaaa bbbb cccc dddd eeee ffff
|
|
deleted line one
|
|
deleted line two
|
|
after
|
|
"
|
|
.unindent();
|
|
|
|
let current_text = "
|
|
aaaa bbbb cccc dddd eeee ffff
|
|
after
|
|
"
|
|
.unindent();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..buffer.read(cx).max_point()],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content_with_widths(
|
|
&editor,
|
|
px(400.0),
|
|
px(200.0),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaaa bbbb cccc dddd eeee ffff
|
|
§ spacer
|
|
§ spacer
|
|
§ spacer
|
|
§ spacer
|
|
§ spacer
|
|
§ spacer
|
|
after"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaaa bbbb\x20
|
|
cccc dddd\x20
|
|
eeee ffff
|
|
deleted line\x20
|
|
one
|
|
deleted line\x20
|
|
two
|
|
after"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_soft_wrap_spacer_after_editing_second_line(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
|
|
|
|
let text = "
|
|
aaaa bbbb cccc dddd eeee ffff
|
|
short
|
|
"
|
|
.unindent();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&text, &text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..buffer.read(cx).max_point()],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content_with_widths(
|
|
&editor,
|
|
px(400.0),
|
|
px(200.0),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaaa bbbb cccc dddd eeee ffff
|
|
§ spacer
|
|
§ spacer
|
|
short"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaaa bbbb\x20
|
|
cccc dddd\x20
|
|
eeee ffff
|
|
short"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
buffer.update(cx, |buffer, cx| {
|
|
buffer.edit([(Point::new(1, 0)..Point::new(1, 5), "modified")], None, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content_with_widths(
|
|
&editor,
|
|
px(400.0),
|
|
px(200.0),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaaa bbbb cccc dddd eeee ffff
|
|
§ spacer
|
|
§ spacer
|
|
modified"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaaa bbbb\x20
|
|
cccc dddd\x20
|
|
eeee ffff
|
|
short"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
|
diff.update(cx, |diff, cx| {
|
|
diff.recalculate_diff_sync(&buffer_snapshot, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content_with_widths(
|
|
&editor,
|
|
px(400.0),
|
|
px(200.0),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaaa bbbb cccc dddd eeee ffff
|
|
§ spacer
|
|
§ spacer
|
|
modified"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaaa bbbb\x20
|
|
cccc dddd\x20
|
|
eeee ffff
|
|
short"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_no_base_text(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
|
|
|
|
let (buffer1, diff1) = buffer_with_diff("xxx\nyyy", "xxx\nyyy", &mut cx);
|
|
|
|
let current_text = "
|
|
aaa
|
|
bbb
|
|
ccc
|
|
"
|
|
.unindent();
|
|
|
|
let buffer2 = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
|
|
let diff2 = cx.new(|cx| BufferDiff::new(&buffer2.read(cx).text_snapshot(), None, None, cx));
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path1 = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path1,
|
|
buffer1.clone(),
|
|
vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
|
|
0,
|
|
diff1.clone(),
|
|
cx,
|
|
);
|
|
|
|
let path2 = PathKey::sorted(1);
|
|
editor.update_excerpts_for_path(
|
|
path2,
|
|
buffer2.clone(),
|
|
vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
|
|
1,
|
|
diff2.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
xxx
|
|
yyy
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
xxx
|
|
yyy
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
§ spacer
|
|
§ spacer"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
buffer1.update(cx, |buffer, cx| {
|
|
buffer.edit([(Point::new(0, 3)..Point::new(0, 3), "z")], None, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
xxxz
|
|
yyy
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
xxx
|
|
yyy
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
§ spacer
|
|
§ spacer"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_deleting_char_in_added_line(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "
|
|
aaa
|
|
bbb
|
|
ccc
|
|
"
|
|
.unindent();
|
|
|
|
let current_text = "
|
|
NEW1
|
|
NEW2
|
|
ccc
|
|
"
|
|
.unindent();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..buffer.read(cx).max_point()],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
NEW1
|
|
NEW2
|
|
ccc"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
buffer.update(cx, |buffer, cx| {
|
|
buffer.edit([(Point::new(1, 3)..Point::new(1, 4), "")], None, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
NEW1
|
|
NEW
|
|
ccc"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_soft_wrap_spacer_before_added_line(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "aaaa bbbb cccc dddd eeee ffff\n";
|
|
|
|
let current_text = "
|
|
aaaa bbbb cccc dddd eeee ffff
|
|
added line
|
|
"
|
|
.unindent();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..buffer.read(cx).max_point()],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content_with_widths(
|
|
&editor,
|
|
px(400.0),
|
|
px(200.0),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaaa bbbb cccc dddd eeee ffff
|
|
§ spacer
|
|
§ spacer
|
|
added line"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaaa bbbb\x20
|
|
cccc dddd\x20
|
|
eeee ffff
|
|
§ spacer"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
assert_split_content_with_widths(
|
|
&editor,
|
|
px(200.0),
|
|
px(400.0),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaaa bbbb\x20
|
|
cccc dddd\x20
|
|
eeee ffff
|
|
added line"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaaa bbbb cccc dddd eeee ffff
|
|
§ spacer
|
|
§ spacer
|
|
§ spacer"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
#[ignore]
|
|
async fn test_joining_added_line_with_unmodified_line(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee
|
|
"
|
|
.unindent();
|
|
|
|
let current_text = "
|
|
aaa
|
|
NEW
|
|
eee
|
|
"
|
|
.unindent();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..buffer.read(cx).max_point()],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
NEW
|
|
§ spacer
|
|
§ spacer
|
|
eee"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
buffer.update(cx, |buffer, cx| {
|
|
buffer.edit([(Point::new(1, 3)..Point::new(2, 0), "")], None, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
§ spacer
|
|
§ spacer
|
|
§ spacer
|
|
NEWeee"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
|
diff.update(cx, |diff, cx| {
|
|
diff.recalculate_diff_sync(&buffer_snapshot, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
NEWeee
|
|
§ spacer
|
|
§ spacer
|
|
§ spacer"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_added_file_at_end(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "";
|
|
let current_text = "
|
|
aaaa bbbb cccc dddd eeee ffff
|
|
bbb
|
|
ccc
|
|
"
|
|
.unindent();
|
|
|
|
let (buffer, diff) = buffer_with_diff(base_text, ¤t_text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..buffer.read(cx).max_point()],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaaa bbbb cccc dddd eeee ffff
|
|
bbb
|
|
ccc"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
§ spacer
|
|
§ spacer"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
assert_split_content_with_widths(
|
|
&editor,
|
|
px(200.0),
|
|
px(200.0),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaaa bbbb\x20
|
|
cccc dddd\x20
|
|
eeee ffff
|
|
bbb
|
|
ccc"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
§ spacer
|
|
§ spacer
|
|
§ spacer
|
|
§ spacer"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_adding_line_to_addition_hunk(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "
|
|
aaa
|
|
bbb
|
|
ccc
|
|
"
|
|
.unindent();
|
|
|
|
let current_text = "
|
|
aaa
|
|
bbb
|
|
xxx
|
|
yyy
|
|
ccc
|
|
"
|
|
.unindent();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..buffer.read(cx).max_point()],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
xxx
|
|
yyy
|
|
ccc"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
§ spacer
|
|
§ spacer
|
|
ccc"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
buffer.update(cx, |buffer, cx| {
|
|
buffer.edit([(Point::new(3, 3)..Point::new(3, 3), "\nzzz")], None, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
xxx
|
|
yyy
|
|
zzz
|
|
ccc"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
§ spacer
|
|
§ spacer
|
|
§ spacer
|
|
ccc"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_scrolling(cx: &mut gpui::TestAppContext) {
|
|
use crate::test::editor_content_with_blocks_and_size;
|
|
use gpui::size;
|
|
use rope::Point;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
|
|
|
|
let long_line = "x".repeat(200);
|
|
let mut lines: Vec<String> = (0..50).map(|i| format!("line {i}")).collect();
|
|
lines[25] = long_line;
|
|
let content = lines.join("\n");
|
|
|
|
let (buffer, diff) = buffer_with_diff(&content, &content, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..buffer.read(cx).max_point()],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
let (rhs_editor, lhs_editor) = editor.update(cx, |editor, _cx| {
|
|
let lhs = editor.lhs.as_ref().expect("should have lhs editor");
|
|
(editor.rhs_editor.clone(), lhs.editor.clone())
|
|
});
|
|
|
|
rhs_editor.update_in(cx, |e, window, cx| {
|
|
e.set_scroll_position(gpui::Point::new(0., 10.), window, cx);
|
|
});
|
|
|
|
let rhs_pos =
|
|
rhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
|
|
let lhs_pos =
|
|
lhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
|
|
assert_eq!(rhs_pos.y, 10., "RHS should be scrolled to row 10");
|
|
assert_eq!(
|
|
lhs_pos.y, rhs_pos.y,
|
|
"LHS should have same scroll position as RHS after set_scroll_position"
|
|
);
|
|
|
|
let draw_size = size(px(300.), px(300.));
|
|
|
|
rhs_editor.update_in(cx, |e, window, cx| {
|
|
e.change_selections(Some(crate::Autoscroll::fit()).into(), window, cx, |s| {
|
|
s.select_ranges([Point::new(25, 150)..Point::new(25, 150)]);
|
|
});
|
|
});
|
|
|
|
let _ = editor_content_with_blocks_and_size(&rhs_editor, draw_size, &mut cx);
|
|
cx.run_until_parked();
|
|
let _ = editor_content_with_blocks_and_size(&lhs_editor, draw_size, &mut cx);
|
|
cx.run_until_parked();
|
|
|
|
let rhs_pos =
|
|
rhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
|
|
let lhs_pos =
|
|
lhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
|
|
|
|
assert!(
|
|
rhs_pos.y > 0.,
|
|
"RHS should have scrolled vertically to show cursor at row 25"
|
|
);
|
|
assert!(
|
|
rhs_pos.x > 0.,
|
|
"RHS should have scrolled horizontally to show cursor at column 150"
|
|
);
|
|
assert_eq!(
|
|
lhs_pos.y, rhs_pos.y,
|
|
"LHS should have same vertical scroll position as RHS after autoscroll"
|
|
);
|
|
assert_eq!(
|
|
lhs_pos.x, rhs_pos.x,
|
|
"LHS should have same horizontal scroll position as RHS after autoscroll"
|
|
)
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_edit_line_before_soft_wrapped_line_preceding_hunk(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "
|
|
first line
|
|
aaaa bbbb cccc dddd eeee ffff
|
|
original
|
|
"
|
|
.unindent();
|
|
|
|
let current_text = "
|
|
first line
|
|
aaaa bbbb cccc dddd eeee ffff
|
|
modified
|
|
"
|
|
.unindent();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..buffer.read(cx).max_point()],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content_with_widths(
|
|
&editor,
|
|
px(400.0),
|
|
px(200.0),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
first line
|
|
aaaa bbbb cccc dddd eeee ffff
|
|
§ spacer
|
|
§ spacer
|
|
modified"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
first line
|
|
aaaa bbbb\x20
|
|
cccc dddd\x20
|
|
eeee ffff
|
|
original"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
buffer.update(cx, |buffer, cx| {
|
|
buffer.edit(
|
|
[(Point::new(0, 0)..Point::new(0, 10), "edited first")],
|
|
None,
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content_with_widths(
|
|
&editor,
|
|
px(400.0),
|
|
px(200.0),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
edited first
|
|
aaaa bbbb cccc dddd eeee ffff
|
|
§ spacer
|
|
§ spacer
|
|
modified"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
first line
|
|
aaaa bbbb\x20
|
|
cccc dddd\x20
|
|
eeee ffff
|
|
original"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
|
diff.update(cx, |diff, cx| {
|
|
diff.recalculate_diff_sync(&buffer_snapshot, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content_with_widths(
|
|
&editor,
|
|
px(400.0),
|
|
px(200.0),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
edited first
|
|
aaaa bbbb cccc dddd eeee ffff
|
|
§ spacer
|
|
§ spacer
|
|
modified"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
first line
|
|
aaaa bbbb\x20
|
|
cccc dddd\x20
|
|
eeee ffff
|
|
original"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_custom_block_sync_between_split_views(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "
|
|
bbb
|
|
ccc
|
|
"
|
|
.unindent();
|
|
let current_text = "
|
|
aaa
|
|
bbb
|
|
ccc
|
|
"
|
|
.unindent();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..buffer.read(cx).max_point()],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
bbb
|
|
ccc"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
let block_ids = editor.update(cx, |splittable_editor, cx| {
|
|
splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
|
|
let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
|
|
let anchor = snapshot.anchor_before(Point::new(2, 0));
|
|
rhs_editor.insert_blocks(
|
|
[BlockProperties {
|
|
placement: BlockPlacement::Above(anchor),
|
|
height: Some(1),
|
|
style: BlockStyle::Fixed,
|
|
render: Arc::new(|_| div().into_any()),
|
|
priority: 0,
|
|
}],
|
|
None,
|
|
cx,
|
|
)
|
|
})
|
|
});
|
|
|
|
let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
|
|
let lhs_editor =
|
|
editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
|
|
|
|
cx.update(|_, cx| {
|
|
set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
|
|
"custom block".to_string()
|
|
});
|
|
});
|
|
|
|
let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
|
|
let display_map = lhs_editor.display_map.read(cx);
|
|
let companion = display_map.companion().unwrap().read(cx);
|
|
let mapping = companion
|
|
.custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
|
|
*mapping.borrow().get(&block_ids[0]).unwrap()
|
|
});
|
|
|
|
cx.update(|_, cx| {
|
|
set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
|
|
"custom block".to_string()
|
|
});
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
§ custom block
|
|
ccc"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
bbb
|
|
§ custom block
|
|
ccc"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
editor.update(cx, |splittable_editor, cx| {
|
|
splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
|
|
rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
|
|
});
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
bbb
|
|
ccc"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_custom_block_deletion_and_resplit_sync(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "
|
|
bbb
|
|
ccc
|
|
"
|
|
.unindent();
|
|
let current_text = "
|
|
aaa
|
|
bbb
|
|
ccc
|
|
"
|
|
.unindent();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..buffer.read(cx).max_point()],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
bbb
|
|
ccc"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
let block_ids = editor.update(cx, |splittable_editor, cx| {
|
|
splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
|
|
let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
|
|
let anchor1 = snapshot.anchor_before(Point::new(2, 0));
|
|
let anchor2 = snapshot.anchor_before(Point::new(3, 0));
|
|
rhs_editor.insert_blocks(
|
|
[
|
|
BlockProperties {
|
|
placement: BlockPlacement::Above(anchor1),
|
|
height: Some(1),
|
|
style: BlockStyle::Fixed,
|
|
render: Arc::new(|_| div().into_any()),
|
|
priority: 0,
|
|
},
|
|
BlockProperties {
|
|
placement: BlockPlacement::Above(anchor2),
|
|
height: Some(1),
|
|
style: BlockStyle::Fixed,
|
|
render: Arc::new(|_| div().into_any()),
|
|
priority: 0,
|
|
},
|
|
],
|
|
None,
|
|
cx,
|
|
)
|
|
})
|
|
});
|
|
|
|
let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
|
|
let lhs_editor =
|
|
editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
|
|
|
|
cx.update(|_, cx| {
|
|
set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
|
|
"custom block 1".to_string()
|
|
});
|
|
set_block_content_for_tests(&rhs_editor, block_ids[1], cx, |_| {
|
|
"custom block 2".to_string()
|
|
});
|
|
});
|
|
|
|
let (lhs_block_id_1, lhs_block_id_2) = lhs_editor.read_with(cx, |lhs_editor, cx| {
|
|
let display_map = lhs_editor.display_map.read(cx);
|
|
let companion = display_map.companion().unwrap().read(cx);
|
|
let mapping = companion
|
|
.custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
|
|
(
|
|
*mapping.borrow().get(&block_ids[0]).unwrap(),
|
|
*mapping.borrow().get(&block_ids[1]).unwrap(),
|
|
)
|
|
});
|
|
|
|
cx.update(|_, cx| {
|
|
set_block_content_for_tests(&lhs_editor, lhs_block_id_1, cx, |_| {
|
|
"custom block 1".to_string()
|
|
});
|
|
set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
|
|
"custom block 2".to_string()
|
|
});
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
§ custom block 1
|
|
ccc
|
|
§ custom block 2"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
bbb
|
|
§ custom block 1
|
|
ccc
|
|
§ custom block 2"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
editor.update(cx, |splittable_editor, cx| {
|
|
splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
|
|
rhs_editor.remove_blocks(HashSet::from_iter([block_ids[0]]), None, cx);
|
|
});
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
§ custom block 2"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
bbb
|
|
ccc
|
|
§ custom block 2"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
editor.update_in(cx, |splittable_editor, window, cx| {
|
|
splittable_editor.unsplit(window, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
editor.update_in(cx, |splittable_editor, window, cx| {
|
|
splittable_editor.split(window, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
let lhs_editor =
|
|
editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
|
|
|
|
let lhs_block_id_2 = lhs_editor.read_with(cx, |lhs_editor, cx| {
|
|
let display_map = lhs_editor.display_map.read(cx);
|
|
let companion = display_map.companion().unwrap().read(cx);
|
|
let mapping = companion
|
|
.custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
|
|
*mapping.borrow().get(&block_ids[1]).unwrap()
|
|
});
|
|
|
|
cx.update(|_, cx| {
|
|
set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
|
|
"custom block 2".to_string()
|
|
});
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
§ custom block 2"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
bbb
|
|
ccc
|
|
§ custom block 2"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_custom_block_sync_with_unsplit_start(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "
|
|
bbb
|
|
ccc
|
|
"
|
|
.unindent();
|
|
let current_text = "
|
|
aaa
|
|
bbb
|
|
ccc
|
|
"
|
|
.unindent();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..buffer.read(cx).max_point()],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
editor.update_in(cx, |splittable_editor, window, cx| {
|
|
splittable_editor.unsplit(window, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
|
|
|
|
let block_ids = editor.update(cx, |splittable_editor, cx| {
|
|
splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
|
|
let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
|
|
let anchor1 = snapshot.anchor_before(Point::new(2, 0));
|
|
let anchor2 = snapshot.anchor_before(Point::new(3, 0));
|
|
rhs_editor.insert_blocks(
|
|
[
|
|
BlockProperties {
|
|
placement: BlockPlacement::Above(anchor1),
|
|
height: Some(1),
|
|
style: BlockStyle::Fixed,
|
|
render: Arc::new(|_| div().into_any()),
|
|
priority: 0,
|
|
},
|
|
BlockProperties {
|
|
placement: BlockPlacement::Above(anchor2),
|
|
height: Some(1),
|
|
style: BlockStyle::Fixed,
|
|
render: Arc::new(|_| div().into_any()),
|
|
priority: 0,
|
|
},
|
|
],
|
|
None,
|
|
cx,
|
|
)
|
|
})
|
|
});
|
|
|
|
cx.update(|_, cx| {
|
|
set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
|
|
"custom block 1".to_string()
|
|
});
|
|
set_block_content_for_tests(&rhs_editor, block_ids[1], cx, |_| {
|
|
"custom block 2".to_string()
|
|
});
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
let rhs_content = editor_content_with_blocks_and_width(&rhs_editor, px(3000.0), &mut cx);
|
|
assert_eq!(
|
|
rhs_content,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
§ custom block 1
|
|
ccc
|
|
§ custom block 2"
|
|
.unindent(),
|
|
"rhs content before split"
|
|
);
|
|
|
|
editor.update_in(cx, |splittable_editor, window, cx| {
|
|
splittable_editor.split(window, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
let lhs_editor =
|
|
editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
|
|
|
|
let (lhs_block_id_1, lhs_block_id_2) = lhs_editor.read_with(cx, |lhs_editor, cx| {
|
|
let display_map = lhs_editor.display_map.read(cx);
|
|
let companion = display_map.companion().unwrap().read(cx);
|
|
let mapping = companion
|
|
.custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
|
|
(
|
|
*mapping.borrow().get(&block_ids[0]).unwrap(),
|
|
*mapping.borrow().get(&block_ids[1]).unwrap(),
|
|
)
|
|
});
|
|
|
|
cx.update(|_, cx| {
|
|
set_block_content_for_tests(&lhs_editor, lhs_block_id_1, cx, |_| {
|
|
"custom block 1".to_string()
|
|
});
|
|
set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
|
|
"custom block 2".to_string()
|
|
});
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
§ custom block 1
|
|
ccc
|
|
§ custom block 2"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
bbb
|
|
§ custom block 1
|
|
ccc
|
|
§ custom block 2"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
editor.update(cx, |splittable_editor, cx| {
|
|
splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
|
|
rhs_editor.remove_blocks(HashSet::from_iter([block_ids[0]]), None, cx);
|
|
});
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
§ custom block 2"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
bbb
|
|
ccc
|
|
§ custom block 2"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
editor.update_in(cx, |splittable_editor, window, cx| {
|
|
splittable_editor.unsplit(window, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
editor.update_in(cx, |splittable_editor, window, cx| {
|
|
splittable_editor.split(window, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
let lhs_editor =
|
|
editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
|
|
|
|
let lhs_block_id_2 = lhs_editor.read_with(cx, |lhs_editor, cx| {
|
|
let display_map = lhs_editor.display_map.read(cx);
|
|
let companion = display_map.companion().unwrap().read(cx);
|
|
let mapping = companion
|
|
.custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
|
|
*mapping.borrow().get(&block_ids[1]).unwrap()
|
|
});
|
|
|
|
cx.update(|_, cx| {
|
|
set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
|
|
"custom block 2".to_string()
|
|
});
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
§ custom block 2"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
bbb
|
|
ccc
|
|
§ custom block 2"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
let new_block_ids = editor.update(cx, |splittable_editor, cx| {
|
|
splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
|
|
let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
|
|
let anchor = snapshot.anchor_before(Point::new(2, 0));
|
|
rhs_editor.insert_blocks(
|
|
[BlockProperties {
|
|
placement: BlockPlacement::Above(anchor),
|
|
height: Some(1),
|
|
style: BlockStyle::Fixed,
|
|
render: Arc::new(|_| div().into_any()),
|
|
priority: 0,
|
|
}],
|
|
None,
|
|
cx,
|
|
)
|
|
})
|
|
});
|
|
|
|
cx.update(|_, cx| {
|
|
set_block_content_for_tests(&rhs_editor, new_block_ids[0], cx, |_| {
|
|
"custom block 3".to_string()
|
|
});
|
|
});
|
|
|
|
let lhs_block_id_3 = lhs_editor.read_with(cx, |lhs_editor, cx| {
|
|
let display_map = lhs_editor.display_map.read(cx);
|
|
let companion = display_map.companion().unwrap().read(cx);
|
|
let mapping = companion
|
|
.custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
|
|
*mapping.borrow().get(&new_block_ids[0]).unwrap()
|
|
});
|
|
|
|
cx.update(|_, cx| {
|
|
set_block_content_for_tests(&lhs_editor, lhs_block_id_3, cx, |_| {
|
|
"custom block 3".to_string()
|
|
});
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
§ custom block 3
|
|
ccc
|
|
§ custom block 2"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
bbb
|
|
§ custom block 3
|
|
ccc
|
|
§ custom block 2"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
editor.update(cx, |splittable_editor, cx| {
|
|
splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
|
|
rhs_editor.remove_blocks(HashSet::from_iter([new_block_ids[0]]), None, cx);
|
|
});
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
§ custom block 2"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
bbb
|
|
ccc
|
|
§ custom block 2"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_buffer_folding_sync(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Unified).await;
|
|
|
|
let base_text1 = "
|
|
aaa
|
|
bbb
|
|
ccc"
|
|
.unindent();
|
|
let current_text1 = "
|
|
aaa
|
|
bbb
|
|
ccc"
|
|
.unindent();
|
|
|
|
let base_text2 = "
|
|
ddd
|
|
eee
|
|
fff"
|
|
.unindent();
|
|
let current_text2 = "
|
|
ddd
|
|
eee
|
|
fff"
|
|
.unindent();
|
|
|
|
let (buffer1, diff1) = buffer_with_diff(&base_text1, ¤t_text1, &mut cx);
|
|
let (buffer2, diff2) = buffer_with_diff(&base_text2, ¤t_text2, &mut cx);
|
|
|
|
let buffer1_id = buffer1.read_with(cx, |buffer, _| buffer.remote_id());
|
|
let buffer2_id = buffer2.read_with(cx, |buffer, _| buffer.remote_id());
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
editor.update_excerpts_for_path(
|
|
PathKey::sorted(0),
|
|
buffer1.clone(),
|
|
vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
|
|
0,
|
|
diff1.clone(),
|
|
cx,
|
|
);
|
|
editor.update_excerpts_for_path(
|
|
PathKey::sorted(1),
|
|
buffer2.clone(),
|
|
vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
|
|
1,
|
|
diff2.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
editor.rhs_editor.update(cx, |rhs_editor, cx| {
|
|
rhs_editor.fold_buffer(buffer1_id, cx);
|
|
});
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
let rhs_buffer1_folded = editor.read_with(cx, |editor, cx| {
|
|
editor.rhs_editor.read(cx).is_buffer_folded(buffer1_id, cx)
|
|
});
|
|
assert!(
|
|
rhs_buffer1_folded,
|
|
"buffer1 should be folded in rhs before split"
|
|
);
|
|
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
editor.split(window, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
let (rhs_editor, lhs_editor) = editor.read_with(cx, |editor, _cx| {
|
|
(
|
|
editor.rhs_editor.clone(),
|
|
editor.lhs.as_ref().unwrap().editor.clone(),
|
|
)
|
|
});
|
|
|
|
let rhs_buffer1_folded =
|
|
rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer1_id, cx));
|
|
assert!(
|
|
rhs_buffer1_folded,
|
|
"buffer1 should be folded in rhs after split"
|
|
);
|
|
|
|
let base_buffer1_id = diff1.read_with(cx, |diff, cx| diff.base_text(cx).remote_id());
|
|
let lhs_buffer1_folded = lhs_editor.read_with(cx, |editor, cx| {
|
|
editor.is_buffer_folded(base_buffer1_id, cx)
|
|
});
|
|
assert!(
|
|
lhs_buffer1_folded,
|
|
"buffer1 should be folded in lhs after split"
|
|
);
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ <no file>
|
|
§ -----
|
|
ddd
|
|
eee
|
|
fff"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ <no file>
|
|
§ -----
|
|
ddd
|
|
eee
|
|
fff"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
editor.rhs_editor.update(cx, |rhs_editor, cx| {
|
|
rhs_editor.fold_buffer(buffer2_id, cx);
|
|
});
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
let rhs_buffer2_folded =
|
|
rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer2_id, cx));
|
|
assert!(rhs_buffer2_folded, "buffer2 should be folded in rhs");
|
|
|
|
let base_buffer2_id = diff2.read_with(cx, |diff, cx| diff.base_text(cx).remote_id());
|
|
let lhs_buffer2_folded = lhs_editor.read_with(cx, |editor, cx| {
|
|
editor.is_buffer_folded(base_buffer2_id, cx)
|
|
});
|
|
assert!(lhs_buffer2_folded, "buffer2 should be folded in lhs");
|
|
|
|
let rhs_buffer1_still_folded =
|
|
rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer1_id, cx));
|
|
assert!(
|
|
rhs_buffer1_still_folded,
|
|
"buffer1 should still be folded in rhs"
|
|
);
|
|
|
|
let lhs_buffer1_still_folded = lhs_editor.read_with(cx, |editor, cx| {
|
|
editor.is_buffer_folded(base_buffer1_id, cx)
|
|
});
|
|
assert!(
|
|
lhs_buffer1_still_folded,
|
|
"buffer1 should still be folded in lhs"
|
|
);
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ <no file>
|
|
§ -----"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ <no file>
|
|
§ -----"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_custom_block_in_middle_of_added_hunk(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "
|
|
ddd
|
|
eee
|
|
"
|
|
.unindent();
|
|
let current_text = "
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee
|
|
"
|
|
.unindent();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..buffer.read(cx).max_point()],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
§ spacer
|
|
§ spacer
|
|
ddd
|
|
eee"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
let block_ids = editor.update(cx, |splittable_editor, cx| {
|
|
splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
|
|
let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
|
|
let anchor = snapshot.anchor_before(Point::new(2, 0));
|
|
rhs_editor.insert_blocks(
|
|
[BlockProperties {
|
|
placement: BlockPlacement::Above(anchor),
|
|
height: Some(1),
|
|
style: BlockStyle::Fixed,
|
|
render: Arc::new(|_| div().into_any()),
|
|
priority: 0,
|
|
}],
|
|
None,
|
|
cx,
|
|
)
|
|
})
|
|
});
|
|
|
|
let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
|
|
let lhs_editor =
|
|
editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
|
|
|
|
cx.update(|_, cx| {
|
|
set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
|
|
"custom block".to_string()
|
|
});
|
|
});
|
|
|
|
let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
|
|
let display_map = lhs_editor.display_map.read(cx);
|
|
let companion = display_map.companion().unwrap().read(cx);
|
|
let mapping = companion
|
|
.custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
|
|
*mapping.borrow().get(&block_ids[0]).unwrap()
|
|
});
|
|
|
|
cx.update(|_, cx| {
|
|
set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
|
|
"custom block".to_string()
|
|
});
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
§ custom block
|
|
ccc
|
|
ddd
|
|
eee"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
§ spacer
|
|
§ spacer
|
|
§ custom block
|
|
ddd
|
|
eee"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
editor.update(cx, |splittable_editor, cx| {
|
|
splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
|
|
rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
|
|
});
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
§ spacer
|
|
§ spacer
|
|
ddd
|
|
eee"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_custom_block_below_in_middle_of_added_hunk(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "
|
|
ddd
|
|
eee
|
|
"
|
|
.unindent();
|
|
let current_text = "
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee
|
|
"
|
|
.unindent();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..buffer.read(cx).max_point()],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
§ spacer
|
|
§ spacer
|
|
ddd
|
|
eee"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
let block_ids = editor.update(cx, |splittable_editor, cx| {
|
|
splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
|
|
let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
|
|
let anchor = snapshot.anchor_after(Point::new(1, 3));
|
|
rhs_editor.insert_blocks(
|
|
[BlockProperties {
|
|
placement: BlockPlacement::Below(anchor),
|
|
height: Some(1),
|
|
style: BlockStyle::Fixed,
|
|
render: Arc::new(|_| div().into_any()),
|
|
priority: 0,
|
|
}],
|
|
None,
|
|
cx,
|
|
)
|
|
})
|
|
});
|
|
|
|
let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
|
|
let lhs_editor =
|
|
editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
|
|
|
|
cx.update(|_, cx| {
|
|
set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
|
|
"custom block".to_string()
|
|
});
|
|
});
|
|
|
|
let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
|
|
let display_map = lhs_editor.display_map.read(cx);
|
|
let companion = display_map.companion().unwrap().read(cx);
|
|
let mapping = companion
|
|
.custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
|
|
*mapping.borrow().get(&block_ids[0]).unwrap()
|
|
});
|
|
|
|
cx.update(|_, cx| {
|
|
set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
|
|
"custom block".to_string()
|
|
});
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
§ custom block
|
|
ccc
|
|
ddd
|
|
eee"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
§ spacer
|
|
§ spacer
|
|
§ custom block
|
|
ddd
|
|
eee"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
editor.update(cx, |splittable_editor, cx| {
|
|
splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
|
|
rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
|
|
});
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
§ spacer
|
|
§ spacer
|
|
§ spacer
|
|
ddd
|
|
eee"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_custom_block_resize_syncs_balancing_block(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "
|
|
bbb
|
|
ccc
|
|
"
|
|
.unindent();
|
|
let current_text = "
|
|
aaa
|
|
bbb
|
|
ccc
|
|
"
|
|
.unindent();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..buffer.read(cx).max_point()],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
let block_ids = editor.update(cx, |splittable_editor, cx| {
|
|
splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
|
|
let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
|
|
let anchor = snapshot.anchor_before(Point::new(2, 0));
|
|
rhs_editor.insert_blocks(
|
|
[BlockProperties {
|
|
placement: BlockPlacement::Above(anchor),
|
|
height: Some(1),
|
|
style: BlockStyle::Fixed,
|
|
render: Arc::new(|_| div().into_any()),
|
|
priority: 0,
|
|
}],
|
|
None,
|
|
cx,
|
|
)
|
|
})
|
|
});
|
|
|
|
let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
|
|
let lhs_editor =
|
|
editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
|
|
|
|
let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
|
|
let display_map = lhs_editor.display_map.read(cx);
|
|
let companion = display_map.companion().unwrap().read(cx);
|
|
let mapping = companion
|
|
.custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
|
|
*mapping.borrow().get(&block_ids[0]).unwrap()
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
let get_block_height = |editor: &Entity<crate::Editor>,
|
|
block_id: crate::CustomBlockId,
|
|
cx: &mut VisualTestContext| {
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
let snapshot = editor.snapshot(window, cx);
|
|
snapshot
|
|
.block_for_id(crate::BlockId::Custom(block_id))
|
|
.map(|block| block.height())
|
|
})
|
|
};
|
|
|
|
assert_eq!(
|
|
get_block_height(&rhs_editor, block_ids[0], &mut cx),
|
|
Some(1)
|
|
);
|
|
assert_eq!(
|
|
get_block_height(&lhs_editor, lhs_block_id, &mut cx),
|
|
Some(1)
|
|
);
|
|
|
|
editor.update(cx, |splittable_editor, cx| {
|
|
splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
|
|
let mut heights = HashMap::default();
|
|
heights.insert(block_ids[0], 3);
|
|
rhs_editor.resize_blocks(heights, None, cx);
|
|
});
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_eq!(
|
|
get_block_height(&rhs_editor, block_ids[0], &mut cx),
|
|
Some(3)
|
|
);
|
|
assert_eq!(
|
|
get_block_height(&lhs_editor, lhs_block_id, &mut cx),
|
|
Some(3)
|
|
);
|
|
|
|
editor.update(cx, |splittable_editor, cx| {
|
|
splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
|
|
let mut heights = HashMap::default();
|
|
heights.insert(block_ids[0], 5);
|
|
rhs_editor.resize_blocks(heights, None, cx);
|
|
});
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_eq!(
|
|
get_block_height(&rhs_editor, block_ids[0], &mut cx),
|
|
Some(5)
|
|
);
|
|
assert_eq!(
|
|
get_block_height(&lhs_editor, lhs_block_id, &mut cx),
|
|
Some(5)
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_edit_spanning_excerpt_boundaries_then_resplit(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee
|
|
fff
|
|
ggg
|
|
hhh
|
|
iii
|
|
jjj
|
|
kkk
|
|
lll
|
|
"
|
|
.unindent();
|
|
let current_text = base_text.clone();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![
|
|
Point::new(0, 0)..Point::new(3, 3),
|
|
Point::new(5, 0)..Point::new(8, 3),
|
|
Point::new(10, 0)..Point::new(11, 3),
|
|
],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
buffer.update(cx, |buffer, cx| {
|
|
buffer.edit([(Point::new(1, 0)..Point::new(10, 0), "")], None, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
editor.update_in(cx, |splittable_editor, window, cx| {
|
|
splittable_editor.unsplit(window, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
editor.update_in(cx, |splittable_editor, window, cx| {
|
|
splittable_editor.split(window, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_range_folds_removed_on_split(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Unified).await;
|
|
|
|
let base_text = "
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee"
|
|
.unindent();
|
|
let current_text = "
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
eee"
|
|
.unindent();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..buffer.read(cx).max_point()],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
editor.rhs_editor.update(cx, |rhs_editor, cx| {
|
|
rhs_editor.fold_creases(
|
|
vec![Crease::simple(
|
|
Point::new(1, 0)..Point::new(3, 0),
|
|
FoldPlaceholder::test(),
|
|
)],
|
|
false,
|
|
window,
|
|
cx,
|
|
);
|
|
});
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
editor.split(window, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
let (rhs_editor, lhs_editor) = editor.read_with(cx, |editor, _cx| {
|
|
(
|
|
editor.rhs_editor.clone(),
|
|
editor.lhs.as_ref().unwrap().editor.clone(),
|
|
)
|
|
});
|
|
|
|
let rhs_has_folds_after_split = rhs_editor.update(cx, |editor, cx| {
|
|
let snapshot = editor.display_snapshot(cx);
|
|
snapshot
|
|
.folds_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
|
|
.next()
|
|
.is_some()
|
|
});
|
|
assert!(
|
|
!rhs_has_folds_after_split,
|
|
"rhs should not have range folds after split"
|
|
);
|
|
|
|
let lhs_has_folds = lhs_editor.update(cx, |editor, cx| {
|
|
let snapshot = editor.display_snapshot(cx);
|
|
snapshot
|
|
.folds_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
|
|
.next()
|
|
.is_some()
|
|
});
|
|
assert!(!lhs_has_folds, "lhs should not have any range folds");
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_multiline_inlays_create_spacers(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "
|
|
aaa
|
|
bbb
|
|
ccc
|
|
ddd
|
|
"
|
|
.unindent();
|
|
let current_text = base_text.clone();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..Point::new(3, 3)],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
let rhs_editor = editor.read_with(cx, |e, _| e.rhs_editor.clone());
|
|
rhs_editor.update(cx, |rhs_editor, cx| {
|
|
let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
|
|
rhs_editor.splice_inlays(
|
|
&[],
|
|
vec![
|
|
Inlay::edit_prediction(
|
|
0,
|
|
snapshot.anchor_after(Point::new(0, 3)),
|
|
"\nINLAY_WITHIN",
|
|
),
|
|
Inlay::edit_prediction(
|
|
1,
|
|
snapshot.anchor_after(Point::new(1, 3)),
|
|
"\nINLAY_MID_1\nINLAY_MID_2",
|
|
),
|
|
Inlay::edit_prediction(
|
|
2,
|
|
snapshot.anchor_after(Point::new(3, 3)),
|
|
"\nINLAY_END_1\nINLAY_END_2",
|
|
),
|
|
],
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
INLAY_WITHIN
|
|
bbb
|
|
INLAY_MID_1
|
|
INLAY_MID_2
|
|
ccc
|
|
ddd
|
|
INLAY_END_1
|
|
INLAY_END_2"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
§ spacer
|
|
bbb
|
|
§ spacer
|
|
§ spacer
|
|
ccc
|
|
ddd
|
|
§ spacer
|
|
§ spacer"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_split_after_removing_folded_buffer(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Unified).await;
|
|
|
|
let base_text_a = "
|
|
aaa
|
|
bbb
|
|
ccc
|
|
"
|
|
.unindent();
|
|
let current_text_a = "
|
|
aaa
|
|
bbb modified
|
|
ccc
|
|
"
|
|
.unindent();
|
|
|
|
let base_text_b = "
|
|
xxx
|
|
yyy
|
|
zzz
|
|
"
|
|
.unindent();
|
|
let current_text_b = "
|
|
xxx
|
|
yyy modified
|
|
zzz
|
|
"
|
|
.unindent();
|
|
|
|
let (buffer_a, diff_a) = buffer_with_diff(&base_text_a, ¤t_text_a, &mut cx);
|
|
let (buffer_b, diff_b) = buffer_with_diff(&base_text_b, ¤t_text_b, &mut cx);
|
|
|
|
let path_a = PathKey::sorted(0);
|
|
let path_b = PathKey::sorted(1);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
editor.update_excerpts_for_path(
|
|
path_a.clone(),
|
|
buffer_a.clone(),
|
|
vec![Point::new(0, 0)..buffer_a.read(cx).max_point()],
|
|
0,
|
|
diff_a.clone(),
|
|
cx,
|
|
);
|
|
editor.update_excerpts_for_path(
|
|
path_b.clone(),
|
|
buffer_b.clone(),
|
|
vec![Point::new(0, 0)..buffer_b.read(cx).max_point()],
|
|
0,
|
|
diff_b.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
let buffer_a_id = buffer_a.read_with(cx, |buffer, _| buffer.remote_id());
|
|
editor.update(cx, |editor, cx| {
|
|
editor.rhs_editor().update(cx, |right_editor, cx| {
|
|
right_editor.fold_buffer(buffer_a_id, cx)
|
|
});
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
editor.remove_excerpts_for_path(path_a.clone(), cx);
|
|
});
|
|
cx.run_until_parked();
|
|
|
|
editor.update_in(cx, |editor, window, cx| editor.split(window, cx));
|
|
cx.run_until_parked();
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
editor.update_excerpts_for_path(
|
|
path_a.clone(),
|
|
buffer_a.clone(),
|
|
vec![Point::new(0, 0)..buffer_a.read(cx).max_point()],
|
|
0,
|
|
diff_a.clone(),
|
|
cx,
|
|
);
|
|
assert!(
|
|
!editor
|
|
.lhs_editor()
|
|
.unwrap()
|
|
.read(cx)
|
|
.is_buffer_folded(buffer_a_id, cx)
|
|
);
|
|
assert!(
|
|
!editor
|
|
.rhs_editor()
|
|
.read(cx)
|
|
.is_buffer_folded(buffer_a_id, cx)
|
|
);
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_two_path_keys_for_one_buffer(cx: &mut gpui::TestAppContext) {
|
|
use multi_buffer::PathKey;
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "
|
|
aaa
|
|
bbb
|
|
ccc
|
|
"
|
|
.unindent();
|
|
let current_text = "
|
|
aaa
|
|
bbb modified
|
|
ccc
|
|
"
|
|
.unindent();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
|
|
|
let path_key_1 = PathKey {
|
|
sort_prefix: Some(0),
|
|
path: rel_path("file1.txt").into(),
|
|
};
|
|
let path_key_2 = PathKey {
|
|
sort_prefix: Some(1),
|
|
path: rel_path("file1.txt").into(),
|
|
};
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
editor.update_excerpts_for_path(
|
|
path_key_1.clone(),
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..Point::new(1, 0)],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
editor.update_excerpts_for_path(
|
|
path_key_2.clone(),
|
|
buffer.clone(),
|
|
vec![Point::new(1, 0)..buffer.read(cx).max_point()],
|
|
1,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_spacer_blocks_revert_after_temporary_edit(cx: &mut gpui::TestAppContext) {
|
|
use rope::Point;
|
|
use unindent::Unindent as _;
|
|
|
|
let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
|
|
|
|
let base_text = "
|
|
aaa
|
|
bbb
|
|
"
|
|
.unindent();
|
|
let current_text = "
|
|
aaa
|
|
bbb
|
|
ccc
|
|
"
|
|
.unindent();
|
|
|
|
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let path = PathKey::sorted(0);
|
|
editor.update_excerpts_for_path(
|
|
path,
|
|
buffer.clone(),
|
|
vec![Point::new(0, 0)..buffer.read(cx).max_point()],
|
|
0,
|
|
diff.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
§ spacer"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
|
|
buffer.edit([(Point::new(0, 3)..Point::new(0, 3), "\n")], None, cx);
|
|
buffer.text_snapshot()
|
|
});
|
|
diff.update(cx, |diff, cx| {
|
|
diff.recalculate_diff_sync(&buffer_snapshot, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
|
|
bbb
|
|
ccc"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
§ spacer
|
|
bbb
|
|
§ spacer"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
|
|
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
|
|
buffer.edit([(Point::new(0, 3)..Point::new(1, 0), "")], None, cx);
|
|
buffer.text_snapshot()
|
|
});
|
|
diff.update(cx, |diff, cx| {
|
|
diff.recalculate_diff_sync(&buffer_snapshot, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
assert_split_content(
|
|
&editor,
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
ccc"
|
|
.unindent(),
|
|
"
|
|
§ <no file>
|
|
§ -----
|
|
aaa
|
|
bbb
|
|
§ spacer"
|
|
.unindent(),
|
|
&mut cx,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_act_as_type(cx: &mut gpui::TestAppContext) {
|
|
let (splittable_editor, cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
|
|
let editor = splittable_editor.read_with(cx, |editor, cx| {
|
|
editor.act_as_type(TypeId::of::<Editor>(), &splittable_editor, cx)
|
|
});
|
|
|
|
assert!(
|
|
editor.is_some(),
|
|
"SplittableEditor should be able to act as Editor"
|
|
);
|
|
}
|
|
}
|