This PR reworks the (still feature-gated) side-by-side diff view to use a different approach to representing the multibuffers on the left- and right-hand sides. Previously, these two multibuffers used identical sets of buffers and excerpts, and were made to behave differently by adding a new knob to the multibuffer controlling how diffs are displayed. Specifically, the left-hand side multibuffer would filter out the added range of each hunk from the excerpts using a new `FilteredInsertedHunk` diff transform, and the right-hand side would simply not show the deleted sides of expanded hunks. This approach has some problems: - Line numbers, and actions that navigate by line number, behaved incorrectly for the left-hand side. - Syntax highlighting and other features that use the buffer syntax tree also behaved incorrectly for the left-hand side. In this PR, we've switched to using independent buffers to build the left-hand side. These buffers are constructed using the base texts for the corresponding diffs, and their lifecycle is managed by `BufferDiff`. The red "deleted" regions on the left-hand side are represented by `BufferContent` diff transforms, not `DeletedHunk` transforms. This means each excerpt on the left represents a contiguous slice of a single buffer, which fixes the above issues by construction. The tradeoff with this new approach is that we now have to manually synchronize excerpt ranges from the right side to the left, which we do using `BufferDiffSnapshot::row_to_base_text_row`. Release Notes: - N/A --------- Co-authored-by: cameron <cameron.studdstreet@gmail.com> Co-authored-by: HactarCE <6060305+HactarCE@users.noreply.github.com> Co-authored-by: Miguel Raz Guzmán Macedo <miguel@zed.dev> Co-authored-by: Anthony <anthony@zed.dev> Co-authored-by: Cameron <cameron@zed.dev>
739 lines
23 KiB
Rust
739 lines
23 KiB
Rust
//! TextDiffView currently provides a UI for displaying differences between the clipboard and selected text.
|
|
|
|
use anyhow::Result;
|
|
use buffer_diff::BufferDiff;
|
|
use editor::{Editor, EditorEvent, MultiBuffer, ToPoint, actions::DiffClipboardWithSelectionData};
|
|
use futures::{FutureExt, select_biased};
|
|
use gpui::{
|
|
AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle,
|
|
Focusable, IntoElement, Render, Task, Window,
|
|
};
|
|
use language::{self, Buffer, Point};
|
|
use project::Project;
|
|
use std::{
|
|
any::{Any, TypeId},
|
|
cmp,
|
|
ops::Range,
|
|
pin::pin,
|
|
sync::Arc,
|
|
time::Duration,
|
|
};
|
|
use ui::{Color, Icon, IconName, Label, LabelCommon as _, SharedString};
|
|
use util::paths::PathExt;
|
|
|
|
use workspace::{
|
|
Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace,
|
|
item::{BreadcrumbText, ItemEvent, SaveOptions, TabContentParams},
|
|
searchable::SearchableItemHandle,
|
|
};
|
|
|
|
pub struct TextDiffView {
|
|
diff_editor: Entity<Editor>,
|
|
title: SharedString,
|
|
path: Option<SharedString>,
|
|
buffer_changes_tx: watch::Sender<()>,
|
|
_recalculate_diff_task: Task<Result<()>>,
|
|
}
|
|
|
|
const RECALCULATE_DIFF_DEBOUNCE: Duration = Duration::from_millis(250);
|
|
|
|
impl TextDiffView {
|
|
pub fn open(
|
|
diff_data: &DiffClipboardWithSelectionData,
|
|
workspace: &Workspace,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> Option<Task<Result<Entity<Self>>>> {
|
|
let source_editor = diff_data.editor.clone();
|
|
|
|
let selection_data = source_editor.update(cx, |editor, cx| {
|
|
let multibuffer = editor.buffer().read(cx);
|
|
let source_buffer = multibuffer.as_singleton()?;
|
|
let selections = editor.selections.all::<Point>(&editor.display_snapshot(cx));
|
|
let buffer_snapshot = source_buffer.read(cx);
|
|
let first_selection = selections.first()?;
|
|
let max_point = buffer_snapshot.max_point();
|
|
|
|
if first_selection.is_empty() {
|
|
let full_range = Point::new(0, 0)..max_point;
|
|
return Some((source_buffer, full_range));
|
|
}
|
|
|
|
let start = first_selection.start;
|
|
let end = first_selection.end;
|
|
let expanded_start = Point::new(start.row, 0);
|
|
|
|
let expanded_end = if end.column > 0 {
|
|
let next_row = end.row + 1;
|
|
cmp::min(max_point, Point::new(next_row, 0))
|
|
} else {
|
|
end
|
|
};
|
|
Some((source_buffer, expanded_start..expanded_end))
|
|
});
|
|
|
|
let Some((source_buffer, expanded_selection_range)) = selection_data else {
|
|
log::warn!("There should always be at least one selection in Zed. This is a bug.");
|
|
return None;
|
|
};
|
|
|
|
source_editor.update(cx, |source_editor, cx| {
|
|
source_editor.change_selections(Default::default(), window, cx, |s| {
|
|
s.select_ranges(vec![
|
|
expanded_selection_range.start..expanded_selection_range.end,
|
|
]);
|
|
})
|
|
});
|
|
|
|
let source_buffer_snapshot = source_buffer.read(cx).snapshot();
|
|
let mut clipboard_text = diff_data.clipboard_text.clone();
|
|
|
|
if !clipboard_text.ends_with("\n") {
|
|
clipboard_text.push_str("\n");
|
|
}
|
|
|
|
let workspace = workspace.weak_handle();
|
|
let diff_buffer = cx.new(|cx| BufferDiff::new(&source_buffer_snapshot.text, cx));
|
|
let clipboard_buffer = build_clipboard_buffer(
|
|
clipboard_text,
|
|
&source_buffer,
|
|
expanded_selection_range.clone(),
|
|
cx,
|
|
);
|
|
|
|
let task = window.spawn(cx, async move |cx| {
|
|
let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
|
|
|
|
update_diff_buffer(&diff_buffer, &source_buffer, &clipboard_buffer, cx).await?;
|
|
|
|
workspace.update_in(cx, |workspace, window, cx| {
|
|
let diff_view = cx.new(|cx| {
|
|
TextDiffView::new(
|
|
clipboard_buffer,
|
|
source_editor,
|
|
source_buffer,
|
|
expanded_selection_range,
|
|
diff_buffer,
|
|
project,
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
|
|
let pane = workspace.active_pane();
|
|
pane.update(cx, |pane, cx| {
|
|
pane.add_item(Box::new(diff_view.clone()), true, true, None, window, cx);
|
|
});
|
|
|
|
diff_view
|
|
})
|
|
});
|
|
|
|
Some(task)
|
|
}
|
|
|
|
pub fn new(
|
|
clipboard_buffer: Entity<Buffer>,
|
|
source_editor: Entity<Editor>,
|
|
source_buffer: Entity<Buffer>,
|
|
source_range: Range<Point>,
|
|
diff_buffer: Entity<BufferDiff>,
|
|
project: Entity<Project>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Self {
|
|
let multibuffer = cx.new(|cx| {
|
|
let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
|
|
|
|
multibuffer.push_excerpts(
|
|
source_buffer.clone(),
|
|
[editor::ExcerptRange::new(source_range)],
|
|
cx,
|
|
);
|
|
|
|
multibuffer.add_diff(diff_buffer.clone(), cx);
|
|
multibuffer
|
|
});
|
|
let diff_editor = cx.new(|cx| {
|
|
let mut editor = Editor::for_multibuffer(multibuffer, Some(project), window, cx);
|
|
editor.start_temporary_diff_override();
|
|
editor.disable_diagnostics(cx);
|
|
editor.set_expand_all_diff_hunks(cx);
|
|
editor.set_render_diff_hunk_controls(
|
|
Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()),
|
|
cx,
|
|
);
|
|
editor
|
|
});
|
|
|
|
let (buffer_changes_tx, mut buffer_changes_rx) = watch::channel(());
|
|
|
|
cx.subscribe(&source_buffer, move |this, _, event, _| match event {
|
|
language::BufferEvent::Edited
|
|
| language::BufferEvent::LanguageChanged(_)
|
|
| language::BufferEvent::Reparsed => {
|
|
this.buffer_changes_tx.send(()).ok();
|
|
}
|
|
_ => {}
|
|
})
|
|
.detach();
|
|
|
|
let editor = source_editor.read(cx);
|
|
let title = editor.buffer().read(cx).title(cx).to_string();
|
|
let selection_location_text = selection_location_text(editor, cx);
|
|
let selection_location_title = selection_location_text
|
|
.as_ref()
|
|
.map(|text| format!("{} @ {}", title, text))
|
|
.unwrap_or(title);
|
|
|
|
let path = editor
|
|
.buffer()
|
|
.read(cx)
|
|
.as_singleton()
|
|
.and_then(|b| {
|
|
b.read(cx)
|
|
.file()
|
|
.map(|f| f.full_path(cx).compact().to_string_lossy().into_owned())
|
|
})
|
|
.unwrap_or("untitled".into());
|
|
|
|
let selection_location_path = selection_location_text
|
|
.map(|text| format!("{} @ {}", path, text))
|
|
.unwrap_or(path);
|
|
|
|
Self {
|
|
diff_editor,
|
|
title: format!("Clipboard ↔ {selection_location_title}").into(),
|
|
path: Some(format!("Clipboard ↔ {selection_location_path}").into()),
|
|
buffer_changes_tx,
|
|
_recalculate_diff_task: cx.spawn(async move |_, cx| {
|
|
while buffer_changes_rx.recv().await.is_ok() {
|
|
loop {
|
|
let mut timer = cx
|
|
.background_executor()
|
|
.timer(RECALCULATE_DIFF_DEBOUNCE)
|
|
.fuse();
|
|
let mut recv = pin!(buffer_changes_rx.recv().fuse());
|
|
select_biased! {
|
|
_ = timer => break,
|
|
_ = recv => continue,
|
|
}
|
|
}
|
|
|
|
log::trace!("start recalculating");
|
|
update_diff_buffer(&diff_buffer, &source_buffer, &clipboard_buffer, cx).await?;
|
|
log::trace!("finish recalculating");
|
|
}
|
|
Ok(())
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn build_clipboard_buffer(
|
|
text: String,
|
|
source_buffer: &Entity<Buffer>,
|
|
replacement_range: Range<Point>,
|
|
cx: &mut App,
|
|
) -> Entity<Buffer> {
|
|
let source_buffer_snapshot = source_buffer.read(cx).snapshot();
|
|
cx.new(|cx| {
|
|
let mut buffer = language::Buffer::local(source_buffer_snapshot.text(), cx);
|
|
let language = source_buffer.read(cx).language().cloned();
|
|
buffer.set_language(language, cx);
|
|
|
|
let range_start = source_buffer_snapshot.point_to_offset(replacement_range.start);
|
|
let range_end = source_buffer_snapshot.point_to_offset(replacement_range.end);
|
|
buffer.edit([(range_start..range_end, text)], None, cx);
|
|
|
|
buffer
|
|
})
|
|
}
|
|
|
|
async fn update_diff_buffer(
|
|
diff: &Entity<BufferDiff>,
|
|
source_buffer: &Entity<Buffer>,
|
|
clipboard_buffer: &Entity<Buffer>,
|
|
cx: &mut AsyncApp,
|
|
) -> Result<()> {
|
|
let source_buffer_snapshot = source_buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
|
|
let language = source_buffer_snapshot.language().cloned();
|
|
|
|
let base_buffer_snapshot = clipboard_buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
|
|
let base_text = base_buffer_snapshot.text();
|
|
|
|
let update = diff
|
|
.update(cx, |diff, cx| {
|
|
diff.update_diff(
|
|
source_buffer_snapshot.text.clone(),
|
|
Some(Arc::from(base_text.as_str())),
|
|
true,
|
|
language,
|
|
cx,
|
|
)
|
|
})?
|
|
.await;
|
|
|
|
diff.update(cx, |diff, cx| {
|
|
diff.set_snapshot(update, &source_buffer_snapshot.text, cx);
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
impl EventEmitter<EditorEvent> for TextDiffView {}
|
|
|
|
impl Focusable for TextDiffView {
|
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
|
self.diff_editor.focus_handle(cx)
|
|
}
|
|
}
|
|
|
|
impl Item for TextDiffView {
|
|
type Event = EditorEvent;
|
|
|
|
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
|
|
Some(Icon::new(IconName::Diff).color(Color::Muted))
|
|
}
|
|
|
|
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
|
|
Label::new(self.tab_content_text(params.detail.unwrap_or_default(), cx))
|
|
.color(if params.selected {
|
|
Color::Default
|
|
} else {
|
|
Color::Muted
|
|
})
|
|
.into_any_element()
|
|
}
|
|
|
|
fn tab_content_text(&self, _detail: usize, _: &App) -> SharedString {
|
|
self.title.clone()
|
|
}
|
|
|
|
fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
|
|
self.path.clone()
|
|
}
|
|
|
|
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
|
|
Editor::to_item_events(event, f)
|
|
}
|
|
|
|
fn telemetry_event_text(&self) -> Option<&'static str> {
|
|
Some("Selection Diff View Opened")
|
|
}
|
|
|
|
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.diff_editor
|
|
.update(cx, |editor, cx| editor.deactivated(window, cx));
|
|
}
|
|
|
|
fn act_as_type<'a>(
|
|
&'a self,
|
|
type_id: TypeId,
|
|
self_handle: &'a Entity<Self>,
|
|
_: &'a App,
|
|
) -> Option<gpui::AnyEntity> {
|
|
if type_id == TypeId::of::<Self>() {
|
|
Some(self_handle.clone().into())
|
|
} else if type_id == TypeId::of::<Editor>() {
|
|
Some(self.diff_editor.clone().into())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
|
|
Some(Box::new(self.diff_editor.clone()))
|
|
}
|
|
|
|
fn for_each_project_item(
|
|
&self,
|
|
cx: &App,
|
|
f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
|
|
) {
|
|
self.diff_editor.for_each_project_item(cx, f)
|
|
}
|
|
|
|
fn set_nav_history(
|
|
&mut self,
|
|
nav_history: ItemNavHistory,
|
|
_: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.diff_editor.update(cx, |editor, _| {
|
|
editor.set_nav_history(Some(nav_history));
|
|
});
|
|
}
|
|
|
|
fn navigate(
|
|
&mut self,
|
|
data: Box<dyn Any>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> bool {
|
|
self.diff_editor
|
|
.update(cx, |editor, cx| editor.navigate(data, window, cx))
|
|
}
|
|
|
|
fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
|
|
ToolbarItemLocation::PrimaryLeft
|
|
}
|
|
|
|
fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
|
|
self.diff_editor.breadcrumbs(theme, cx)
|
|
}
|
|
|
|
fn added_to_workspace(
|
|
&mut self,
|
|
workspace: &mut Workspace,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.diff_editor.update(cx, |editor, cx| {
|
|
editor.added_to_workspace(workspace, window, cx)
|
|
});
|
|
}
|
|
|
|
fn can_save(&self, cx: &App) -> bool {
|
|
// The editor handles the new buffer, so delegate to it
|
|
self.diff_editor.read(cx).can_save(cx)
|
|
}
|
|
|
|
fn save(
|
|
&mut self,
|
|
options: SaveOptions,
|
|
project: Entity<Project>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Task<Result<()>> {
|
|
// Delegate saving to the editor, which manages the new buffer
|
|
self.diff_editor
|
|
.update(cx, |editor, cx| editor.save(options, project, window, cx))
|
|
}
|
|
}
|
|
|
|
pub fn selection_location_text(editor: &Editor, cx: &App) -> Option<String> {
|
|
let buffer = editor.buffer().read(cx);
|
|
let buffer_snapshot = buffer.snapshot(cx);
|
|
let first_selection = editor.selections.disjoint_anchors().first()?;
|
|
|
|
let selection_start = first_selection.start.to_point(&buffer_snapshot);
|
|
let selection_end = first_selection.end.to_point(&buffer_snapshot);
|
|
|
|
let start_row = selection_start.row;
|
|
let start_column = selection_start.column;
|
|
let end_row = selection_end.row;
|
|
let end_column = selection_end.column;
|
|
|
|
let range_text = if start_row == end_row {
|
|
format!("L{}:{}-{}", start_row + 1, start_column + 1, end_column + 1)
|
|
} else {
|
|
format!(
|
|
"L{}:{}-L{}:{}",
|
|
start_row + 1,
|
|
start_column + 1,
|
|
end_row + 1,
|
|
end_column + 1
|
|
)
|
|
};
|
|
|
|
Some(range_text)
|
|
}
|
|
|
|
impl Render for TextDiffView {
|
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
|
self.diff_editor.clone()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use editor::{MultiBufferOffset, test::editor_test_context::assert_state_with_diff};
|
|
use gpui::{TestAppContext, VisualContext};
|
|
use project::{FakeFs, Project};
|
|
use serde_json::json;
|
|
use settings::SettingsStore;
|
|
use unindent::unindent;
|
|
use util::{path, test::marked_text_ranges};
|
|
|
|
fn init_test(cx: &mut TestAppContext) {
|
|
cx.update(|cx| {
|
|
let settings_store = SettingsStore::test(cx);
|
|
cx.set_global(settings_store);
|
|
theme::init(theme::LoadThemes::JustBase, cx);
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_diffing_clipboard_against_empty_selection_uses_full_buffer_selection(
|
|
cx: &mut TestAppContext,
|
|
) {
|
|
base_test(
|
|
path!("/test"),
|
|
path!("/test/text.txt"),
|
|
"def process_incoming_inventory(items, warehouse_id):\n pass\n",
|
|
"def process_outgoing_inventory(items, warehouse_id):\n passˇ\n",
|
|
&unindent(
|
|
"
|
|
- def process_incoming_inventory(items, warehouse_id):
|
|
+ ˇdef process_outgoing_inventory(items, warehouse_id):
|
|
pass
|
|
",
|
|
),
|
|
"Clipboard ↔ text.txt @ L1:1-L3:1",
|
|
&format!("Clipboard ↔ {} @ L1:1-L3:1", path!("test/text.txt")),
|
|
cx,
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_diffing_clipboard_against_multiline_selection_expands_to_full_lines(
|
|
cx: &mut TestAppContext,
|
|
) {
|
|
base_test(
|
|
path!("/test"),
|
|
path!("/test/text.txt"),
|
|
"def process_incoming_inventory(items, warehouse_id):\n pass\n",
|
|
"«def process_outgoing_inventory(items, warehouse_id):\n passˇ»\n",
|
|
&unindent(
|
|
"
|
|
- def process_incoming_inventory(items, warehouse_id):
|
|
+ ˇdef process_outgoing_inventory(items, warehouse_id):
|
|
pass
|
|
",
|
|
),
|
|
"Clipboard ↔ text.txt @ L1:1-L3:1",
|
|
&format!("Clipboard ↔ {} @ L1:1-L3:1", path!("test/text.txt")),
|
|
cx,
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_diffing_clipboard_against_single_line_selection(cx: &mut TestAppContext) {
|
|
base_test(
|
|
path!("/test"),
|
|
path!("/test/text.txt"),
|
|
"a",
|
|
"«bbˇ»",
|
|
&unindent(
|
|
"
|
|
- a
|
|
+ ˇbb",
|
|
),
|
|
"Clipboard ↔ text.txt @ L1:1-3",
|
|
&format!("Clipboard ↔ {} @ L1:1-3", path!("test/text.txt")),
|
|
cx,
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_diffing_clipboard_with_leading_whitespace_against_line(cx: &mut TestAppContext) {
|
|
base_test(
|
|
path!("/test"),
|
|
path!("/test/text.txt"),
|
|
" a",
|
|
"«bbˇ»",
|
|
&unindent(
|
|
"
|
|
- a
|
|
+ ˇbb",
|
|
),
|
|
"Clipboard ↔ text.txt @ L1:1-3",
|
|
&format!("Clipboard ↔ {} @ L1:1-3", path!("test/text.txt")),
|
|
cx,
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_diffing_clipboard_against_line_with_leading_whitespace(cx: &mut TestAppContext) {
|
|
base_test(
|
|
path!("/test"),
|
|
path!("/test/text.txt"),
|
|
"a",
|
|
" «bbˇ»",
|
|
&unindent(
|
|
"
|
|
- a
|
|
+ ˇ bb",
|
|
),
|
|
"Clipboard ↔ text.txt @ L1:1-7",
|
|
&format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")),
|
|
cx,
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_diffing_clipboard_against_line_with_leading_whitespace_included_in_selection(
|
|
cx: &mut TestAppContext,
|
|
) {
|
|
base_test(
|
|
path!("/test"),
|
|
path!("/test/text.txt"),
|
|
"a",
|
|
"« bbˇ»",
|
|
&unindent(
|
|
"
|
|
- a
|
|
+ ˇ bb",
|
|
),
|
|
"Clipboard ↔ text.txt @ L1:1-7",
|
|
&format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")),
|
|
cx,
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_diffing_clipboard_with_leading_whitespace_against_line_with_leading_whitespace(
|
|
cx: &mut TestAppContext,
|
|
) {
|
|
base_test(
|
|
path!("/test"),
|
|
path!("/test/text.txt"),
|
|
" a",
|
|
" «bbˇ»",
|
|
&unindent(
|
|
"
|
|
- a
|
|
+ ˇ bb",
|
|
),
|
|
"Clipboard ↔ text.txt @ L1:1-7",
|
|
&format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")),
|
|
cx,
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_diffing_clipboard_with_leading_whitespace_against_line_with_leading_whitespace_included_in_selection(
|
|
cx: &mut TestAppContext,
|
|
) {
|
|
base_test(
|
|
path!("/test"),
|
|
path!("/test/text.txt"),
|
|
" a",
|
|
"« bbˇ»",
|
|
&unindent(
|
|
"
|
|
- a
|
|
+ ˇ bb",
|
|
),
|
|
"Clipboard ↔ text.txt @ L1:1-7",
|
|
&format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")),
|
|
cx,
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_diffing_clipboard_against_partial_selection_expands_to_include_trailing_characters(
|
|
cx: &mut TestAppContext,
|
|
) {
|
|
base_test(
|
|
path!("/test"),
|
|
path!("/test/text.txt"),
|
|
"a",
|
|
"«bˇ»b",
|
|
&unindent(
|
|
"
|
|
- a
|
|
+ ˇbb",
|
|
),
|
|
"Clipboard ↔ text.txt @ L1:1-3",
|
|
&format!("Clipboard ↔ {} @ L1:1-3", path!("test/text.txt")),
|
|
cx,
|
|
)
|
|
.await;
|
|
}
|
|
|
|
async fn base_test(
|
|
project_root: &str,
|
|
file_path: &str,
|
|
clipboard_text: &str,
|
|
editor_text: &str,
|
|
expected_diff: &str,
|
|
expected_tab_title: &str,
|
|
expected_tab_tooltip: &str,
|
|
cx: &mut TestAppContext,
|
|
) {
|
|
init_test(cx);
|
|
|
|
let file_name = std::path::Path::new(file_path)
|
|
.file_name()
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap();
|
|
|
|
let fs = FakeFs::new(cx.executor());
|
|
fs.insert_tree(
|
|
project_root,
|
|
json!({
|
|
file_name: editor_text
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
let project = Project::test(fs, [project_root.as_ref()], cx).await;
|
|
|
|
let (workspace, cx) =
|
|
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
|
|
|
let buffer = project
|
|
.update(cx, |project, cx| project.open_local_buffer(file_path, cx))
|
|
.await
|
|
.unwrap();
|
|
|
|
let editor = cx.new_window_entity(|window, cx| {
|
|
let mut editor = Editor::for_buffer(buffer, None, window, cx);
|
|
let (unmarked_text, selection_ranges) = marked_text_ranges(editor_text, false);
|
|
editor.set_text(unmarked_text, window, cx);
|
|
editor.change_selections(Default::default(), window, cx, |s| {
|
|
s.select_ranges(
|
|
selection_ranges
|
|
.into_iter()
|
|
.map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
|
|
)
|
|
});
|
|
|
|
editor
|
|
});
|
|
|
|
let diff_view = workspace
|
|
.update_in(cx, |workspace, window, cx| {
|
|
TextDiffView::open(
|
|
&DiffClipboardWithSelectionData {
|
|
clipboard_text: clipboard_text.to_string(),
|
|
editor,
|
|
},
|
|
workspace,
|
|
window,
|
|
cx,
|
|
)
|
|
})
|
|
.unwrap()
|
|
.await
|
|
.unwrap();
|
|
|
|
cx.executor().run_until_parked();
|
|
|
|
assert_state_with_diff(
|
|
&diff_view.read_with(cx, |diff_view, _| diff_view.diff_editor.clone()),
|
|
cx,
|
|
expected_diff,
|
|
);
|
|
|
|
diff_view.read_with(cx, |diff_view, cx| {
|
|
assert_eq!(diff_view.tab_content_text(0, cx), expected_tab_title);
|
|
assert_eq!(
|
|
diff_view.tab_tooltip_text(cx).unwrap(),
|
|
expected_tab_tooltip
|
|
);
|
|
});
|
|
}
|
|
}
|