Something went wrong. Try again.
fork of gpui
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950//! 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, EditorSettings, MultiBuffer, RestoreOnlyUnstagedDiffHunkDelegate, SplittableEditor, 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, Capability, OffsetRangeExt, Point};use project::{Project, ProjectPath};use settings::Settings;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, ItemNavHistory, Workspace, item::{ItemEvent, SaveOptions, TabContentParams}, searchable::SearchableItemHandle,};
pub struct TextDiffView { diff_editor: Entity<SplittableEditor>, 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(); let multibuffer_snapshot = multibuffer.read(cx).snapshot(cx); let first_selection = editor.selections.newest_anchor();
let (source_buffer, buffer_range) = multibuffer_snapshot .anchor_range_to_buffer_anchor_range(first_selection.range())?; let max_point = source_buffer.max_point(); let buffer_range = buffer_range.to_point(source_buffer); let source_buffer = multibuffer.read(cx).buffer(source_buffer.remote_id())?;
if buffer_range.is_empty() { let full_range = Point::new(0, 0)..max_point; return Some((source_buffer, full_range)); }
let expanded_start = Point::new(buffer_range.start.row, 0); let expanded_end = if buffer_range.end.column > 0 { let next_row = buffer_range.end.row + 1; cmp::min(max_point, Point::new(next_row, 0)) } else { buffer_range.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| { let multibuffer = source_editor.buffer(); let mb_range = { let mb = multibuffer.read(cx); let start_anchor = mb.buffer_point_to_anchor(&source_buffer, expanded_selection_range.start, cx); let end_anchor = mb.buffer_point_to_anchor(&source_buffer, expanded_selection_range.end, cx); start_anchor.zip(end_anchor).map(|(s, e)| { let snapshot = mb.snapshot(cx); s.to_point(&snapshot)..e.to_point(&snapshot) }) };
if let Some(range) = mb_range { source_editor.change_selections(Default::default(), window, cx, |s| { s.select_ranges(vec![range]); }); } });
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 clipboard_buffer = build_clipboard_buffer( clipboard_text, &source_buffer, expanded_selection_range.clone(), cx, ); let diff_buffer = cx.new(|cx| { BufferDiff::new_with_base_text_buffer( &source_buffer_snapshot.text, clipboard_buffer.clone(), buffer_diff::DiffBaseKind::Custom, cx, ) });
let task = window.spawn(cx, async move |cx| { update_diff_buffer(&diff_buffer, &source_buffer, &clipboard_buffer, cx).await;
workspace.update_in(cx, |workspace, window, cx| { let project = workspace.project().clone(); let workspace_entity = cx.entity(); let diff_view = cx.new(|cx| { TextDiffView::new( clipboard_buffer, source_editor, source_buffer, expanded_selection_range, diff_buffer, project, workspace_entity, 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>, workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let multibuffer = cx.new(|cx| { let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
multibuffer.set_excerpts_for_buffer(source_buffer.clone(), [source_range], 0, cx);
multibuffer.add_diff(diff_buffer.clone(), cx); multibuffer }); let diff_editor = cx.new(|cx| { let splittable = SplittableEditor::new( EditorSettings::get_global(cx).diff_view_style, multibuffer, project, workspace, window, cx, ); splittable .set_diff_hunk_delegate(Some(Arc::new(RestoreOnlyUnstagedDiffHunkDelegate)), cx); splittable });
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(MultiBuffer::DEFAULT_TITLE.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(); if let Some(language_registry) = source_buffer.read(cx).language_registry() { buffer.set_language_registry(language_registry); } 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.set_capability(Capability::ReadOnly, cx);
buffer })}
async fn update_diff_buffer( diff: &Entity<BufferDiff>, source_buffer: &Entity<Buffer>, clipboard_buffer: &Entity<Buffer>, cx: &mut AsyncApp,) { let source_buffer_snapshot = source_buffer.read_with(cx, |buffer, _| buffer.snapshot()); let base_buffer_snapshot = clipboard_buffer.read_with(cx, |buffer, _| buffer.snapshot()); let base_text = Arc::<str>::from(base_buffer_snapshot.text());
let update = diff .update(cx, |diff, cx| { diff.update_diff( source_buffer_snapshot.text.clone(), &base_buffer_snapshot, Some(base_text.clone()), cx, ) }) .await;
diff.update(cx, |diff, cx| diff.set_snapshot(update, cx));}
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: &mut dyn 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>, cx: &'a App, ) -> Option<gpui::AnyEntity> { if type_id == TypeId::of::<Self>() { Some(self_handle.clone().into()) } else if type_id == TypeId::of::<SplittableEditor>() { Some(self.diff_editor.clone().into()) } else if type_id == TypeId::of::<Editor>() { Some(self.diff_editor.read(cx).rhs_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.read(cx).for_each_project_item(cx, f) }
fn active_project_path(&self, cx: &App) -> Option<ProjectPath> { self.diff_editor.read(cx).active_project_path(cx) }
fn set_nav_history( &mut self, nav_history: ItemNavHistory, _: &mut Window, cx: &mut Context<Self>, ) { let rhs = self.diff_editor.read(cx).rhs_editor().clone(); rhs.update(cx, |editor, _| { editor.set_nav_history(Some(nav_history)); }); }
fn navigate( &mut self, data: Arc<dyn Any + Send>, window: &mut Window, cx: &mut Context<Self>, ) -> bool { self.diff_editor .update(cx, |editor, cx| editor.navigate(data, window, 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, PathKey, test::editor_test_context::assert_state_with_diff}; use gpui::{BorrowAppContext, TestAppContext, VisualContext}; use language::Point; use project::{FakeFs, Project}; use serde_json::json; use settings::{DiffViewStyle, SettingsStore}; use unindent::unindent; use util::{path, test::marked_text_ranges}; use workspace::MultiWorkspace;
fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); cx.update_global::<SettingsStore, _>(|store, cx| { store.update_user_settings(cx, |settings| { settings.editor.diff_view_style = Some(DiffViewStyle::Unified); }); }); theme_settings::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; }
#[gpui::test] async fn test_diffing_clipboard_from_multibuffer_with_selection(cx: &mut TestAppContext) { init_test(cx);
let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/project"), json!({ "a.txt": "alpha\nbeta\ngamma", "b.txt": "one\ntwo\nthree" }), ) .await;
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let buffer_a = project .update(cx, |project, cx| { project.open_local_buffer(path!("/project/a.txt"), cx) }) .await .unwrap(); let buffer_b = project .update(cx, |project, cx| { project.open_local_buffer(path!("/project/b.txt"), cx) }) .await .unwrap();
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 editor = cx.new_window_entity(|window, cx| { let multibuffer = cx.new(|cx| { let mut mb = MultiBuffer::new(language::Capability::ReadWrite); mb.set_excerpts_for_path( PathKey::sorted(0), buffer_a.clone(), [Point::new(0, 0)..Point::new(2, 5)], 0, cx, ); mb.set_excerpts_for_path( PathKey::sorted(1), buffer_b.clone(), [Point::new(0, 0)..Point::new(2, 5)], 0, cx, ); mb });
let mut editor = Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx); // Select "beta" inside the first excerpt editor.change_selections(Default::default(), window, cx, |s| { s.select_ranges([MultiBufferOffset(6)..MultiBufferOffset(10)]); }); editor });
let diff_view = workspace .update_in(cx, |workspace, window, cx| { TextDiffView::open( &DiffClipboardWithSelectionData { clipboard_text: "REPLACED".to_string(), editor, }, workspace, window, cx, ) }) .unwrap() .await .unwrap();
cx.executor().run_until_parked();
diff_view.read_with(cx, |diff_view, _cx| { assert!( diff_view.title.contains("Clipboard"), "diff view should have opened with a clipboard diff title, got: {}", diff_view.title ); }); }
#[gpui::test] async fn test_diffing_clipboard_from_multibuffer_with_empty_selection(cx: &mut TestAppContext) { init_test(cx);
let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/project"), json!({ "a.txt": "alpha\nbeta\ngamma", "b.txt": "one\ntwo\nthree" }), ) .await;
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let buffer_a = project .update(cx, |project, cx| { project.open_local_buffer(path!("/project/a.txt"), cx) }) .await .unwrap(); let buffer_b = project .update(cx, |project, cx| { project.open_local_buffer(path!("/project/b.txt"), cx) }) .await .unwrap();
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 editor = cx.new_window_entity(|window, cx| { let multibuffer = cx.new(|cx| { let mut mb = MultiBuffer::new(language::Capability::ReadWrite); mb.set_excerpts_for_path( PathKey::sorted(0), buffer_a.clone(), [Point::new(0, 0)..Point::new(2, 5)], 0, cx, ); mb.set_excerpts_for_path( PathKey::sorted(1), buffer_b.clone(), [Point::new(0, 0)..Point::new(2, 5)], 0, cx, ); mb });
let mut editor = Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx); // Cursor inside the first excerpt (no selection) editor.change_selections(Default::default(), window, cx, |s| { s.select_ranges([MultiBufferOffset(6)..MultiBufferOffset(6)]); }); editor });
let diff_view = workspace .update_in(cx, |workspace, window, cx| { TextDiffView::open( &DiffClipboardWithSelectionData { clipboard_text: "REPLACED".to_string(), editor, }, workspace, window, cx, ) }) .unwrap() .await .unwrap();
cx.executor().run_until_parked();
// Empty selection should diff the full underlying buffer diff_view.read_with(cx, |diff_view, _cx| { assert!( diff_view.title.contains("Clipboard"), "diff view should have opened with a clipboard diff title, got: {}", diff_view.title ); }); }
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 (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 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, cx| { diff_view.diff_editor.read(cx).rhs_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 ); }); }}