diff --git a/Cargo.lock b/Cargo.lock index 1669bf0ddb..2249a63758 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6218,6 +6218,7 @@ dependencies = [ "ui", "unindent", "util", + "watch", "windows 0.61.1", "workspace", "workspace-hack", diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index 25ca57c3e1..3812d48bf7 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -1028,7 +1028,11 @@ impl BufferDiff { let (base_text_changed, mut changed_range) = match (state.base_text_exists, new_state.base_text_exists) { (false, false) => (true, None), - (true, true) if state.base_text.remote_id() == new_state.base_text.remote_id() => { + (true, true) + if state.base_text.remote_id() == new_state.base_text.remote_id() + && state.base_text.syntax_update_count() + == new_state.base_text.syntax_update_count() => + { (false, new_state.compare(&state, buffer)) } _ => (true, Some(text::Anchor::MIN..text::Anchor::MAX)), diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 32d32dba50..6274f69035 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -13,6 +13,7 @@ pub enum CliRequest { Open { paths: Vec, urls: Vec, + diff_paths: Vec<[String; 2]>, wait: bool, open_new_workspace: Option, env: Option>, diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 75d083fcd9..752ecb2f01 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -89,6 +89,9 @@ struct Args { /// Will attempt to give the correct command to run #[arg(long)] system_specs: bool, + /// Pairs of file paths to diff. Can be specified multiple times. + #[arg(long, action = clap::ArgAction::Append, num_args = 2, value_names = ["OLD_PATH", "NEW_PATH"])] + diff: Vec, /// Uninstall Zed from user system #[cfg(all( any(target_os = "linux", target_os = "macos"), @@ -232,9 +235,17 @@ fn main() -> Result<()> { let exit_status = Arc::new(Mutex::new(None)); let mut paths = vec![]; let mut urls = vec![]; + let mut diff_paths = vec![]; let mut stdin_tmp_file: Option = None; let mut anonymous_fd_tmp_files = vec![]; + for path in args.diff.chunks(2) { + diff_paths.push([ + parse_path_with_position(&path[0])?, + parse_path_with_position(&path[1])?, + ]); + } + for path in args.paths_with_position.iter() { if path.starts_with("zed://") || path.starts_with("http://") @@ -273,6 +284,7 @@ fn main() -> Result<()> { tx.send(CliRequest::Open { paths, urls, + diff_paths, wait: args.wait, open_new_workspace, env, diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index cef6c4913f..6e04dcb656 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -57,6 +57,7 @@ time.workspace = true time_format.workspace = true ui.workspace = true util.workspace = true +watch.workspace = true workspace-hack.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/git_ui/src/diff_view.rs b/crates/git_ui/src/diff_view.rs new file mode 100644 index 0000000000..b5d697ae53 --- /dev/null +++ b/crates/git_ui/src/diff_view.rs @@ -0,0 +1,497 @@ +//! DiffView provides a UI for displaying differences between two buffers. + +use anyhow::Result; +use buffer_diff::{BufferDiff, BufferDiffSnapshot}; +use editor::{Editor, EditorEvent, MultiBuffer}; +use futures::{FutureExt, select_biased}; +use gpui::{ + AnyElement, AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, + FocusHandle, Focusable, IntoElement, Render, Task, Window, +}; +use language::Buffer; +use project::Project; +use std::{ + any::{Any, TypeId}, + path::PathBuf, + pin::pin, + sync::Arc, + time::Duration, +}; +use ui::{Color, Icon, IconName, Label, LabelCommon as _, SharedString}; +use util::paths::PathExt as _; +use workspace::{ + Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace, + item::{BreadcrumbText, ItemEvent, TabContentParams}, + searchable::SearchableItemHandle, +}; + +pub struct DiffView { + editor: Entity, + old_buffer: Entity, + new_buffer: Entity, + buffer_changes_tx: watch::Sender<()>, + _recalculate_diff_task: Task>, +} + +const RECALCULATE_DIFF_DEBOUNCE: Duration = Duration::from_millis(250); + +impl DiffView { + pub fn open( + old_path: PathBuf, + new_path: PathBuf, + workspace: &Workspace, + window: &mut Window, + cx: &mut App, + ) -> Task>> { + let workspace = workspace.weak_handle(); + window.spawn(cx, async move |cx| { + let project = workspace.update(cx, |workspace, _| workspace.project().clone())?; + let old_buffer = project + .update(cx, |project, cx| project.open_local_buffer(&old_path, cx))? + .await?; + let new_buffer = project + .update(cx, |project, cx| project.open_local_buffer(&new_path, cx))? + .await?; + + let buffer_diff = build_buffer_diff(&old_buffer, &new_buffer, cx).await?; + + workspace.update_in(cx, |workspace, window, cx| { + let diff_view = cx.new(|cx| { + DiffView::new( + old_buffer, + new_buffer, + buffer_diff, + project.clone(), + 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 + }) + }) + } + + pub fn new( + old_buffer: Entity, + new_buffer: Entity, + diff: Entity, + project: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let multibuffer = cx.new(|cx| { + let mut multibuffer = MultiBuffer::singleton(new_buffer.clone(), cx); + multibuffer.add_diff(diff.clone(), cx); + multibuffer + }); + let editor = cx.new(|cx| { + let mut editor = + Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx); + editor.start_temporary_diff_override(); + editor.disable_inline_diagnostics(); + 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(()); + + for buffer in [&old_buffer, &new_buffer] { + cx.subscribe(buffer, move |this, _, event, _| match event { + language::BufferEvent::Edited + | language::BufferEvent::LanguageChanged + | language::BufferEvent::Reparsed => { + this.buffer_changes_tx.send(()).ok(); + } + _ => {} + }) + .detach(); + } + + Self { + editor, + buffer_changes_tx, + old_buffer, + new_buffer, + _recalculate_diff_task: cx.spawn(async move |this, cx| { + while let Ok(_) = buffer_changes_rx.recv().await { + 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"); + let (old_snapshot, new_snapshot) = this.update(cx, |this, cx| { + ( + this.old_buffer.read(cx).snapshot(), + this.new_buffer.read(cx).snapshot(), + ) + })?; + let diff_snapshot = cx + .update(|cx| { + BufferDiffSnapshot::new_with_base_buffer( + new_snapshot.text.clone(), + Some(old_snapshot.text().into()), + old_snapshot, + cx, + ) + })? + .await; + diff.update(cx, |diff, cx| { + diff.set_snapshot(diff_snapshot, &new_snapshot, cx) + })?; + log::trace!("finish recalculating"); + } + Ok(()) + }), + } + } +} + +async fn build_buffer_diff( + old_buffer: &Entity, + new_buffer: &Entity, + cx: &mut AsyncApp, +) -> Result> { + let old_buffer_snapshot = old_buffer.read_with(cx, |buffer, _| buffer.snapshot())?; + let new_buffer_snapshot = new_buffer.read_with(cx, |buffer, _| buffer.snapshot())?; + + let diff_snapshot = cx + .update(|cx| { + BufferDiffSnapshot::new_with_base_buffer( + new_buffer_snapshot.text.clone(), + Some(old_buffer_snapshot.text().into()), + old_buffer_snapshot, + cx, + ) + })? + .await; + + cx.new(|cx| { + let mut diff = BufferDiff::new(&new_buffer_snapshot.text, cx); + diff.set_snapshot(diff_snapshot, &new_buffer_snapshot.text, cx); + diff + }) +} + +impl EventEmitter for DiffView {} + +impl Focusable for DiffView { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.editor.focus_handle(cx) + } +} + +impl Item for DiffView { + type Event = EditorEvent; + + fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { + 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, cx: &App) -> SharedString { + let old_filename = self + .old_buffer + .read(cx) + .file() + .and_then(|file| { + Some( + file.full_path(cx) + .file_name()? + .to_string_lossy() + .to_string(), + ) + }) + .unwrap_or_else(|| "untitled".into()); + let new_filename = self + .new_buffer + .read(cx) + .file() + .and_then(|file| { + Some( + file.full_path(cx) + .file_name()? + .to_string_lossy() + .to_string(), + ) + }) + .unwrap_or_else(|| "untitled".into()); + format!("{old_filename} ↔ {new_filename}").into() + } + + fn tab_tooltip_text(&self, cx: &App) -> Option { + let old_path = self + .old_buffer + .read(cx) + .file() + .map(|file| file.full_path(cx).compact().to_string_lossy().to_string()) + .unwrap_or_else(|| "untitled".into()); + let new_path = self + .new_buffer + .read(cx) + .file() + .map(|file| file.full_path(cx).compact().to_string_lossy().to_string()) + .unwrap_or_else(|| "untitled".into()); + Some(format!("{old_path} ↔ {new_path}").into()) + } + + 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("Diff View Opened") + } + + fn deactivated(&mut self, window: &mut Window, cx: &mut Context) { + self.editor + .update(cx, |editor, cx| editor.deactivated(window, cx)); + } + + fn is_singleton(&self, _: &App) -> bool { + false + } + + fn act_as_type<'a>( + &'a self, + type_id: TypeId, + self_handle: &'a Entity, + _: &'a App, + ) -> Option { + if type_id == TypeId::of::() { + Some(self_handle.to_any()) + } else if type_id == TypeId::of::() { + Some(self.editor.to_any()) + } else { + None + } + } + + fn as_searchable(&self, _: &Entity) -> Option> { + Some(Box::new(self.editor.clone())) + } + + fn for_each_project_item( + &self, + cx: &App, + f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem), + ) { + self.editor.for_each_project_item(cx, f) + } + + fn set_nav_history( + &mut self, + nav_history: ItemNavHistory, + _: &mut Window, + cx: &mut Context, + ) { + self.editor.update(cx, |editor, _| { + editor.set_nav_history(Some(nav_history)); + }); + } + + fn navigate( + &mut self, + data: Box, + window: &mut Window, + cx: &mut Context, + ) -> bool { + self.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> { + self.editor.breadcrumbs(theme, cx) + } + + fn added_to_workspace( + &mut self, + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, + ) { + self.editor.update(cx, |editor, cx| { + editor.added_to_workspace(workspace, window, cx) + }); + } +} + +impl Render for DiffView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + self.editor.clone() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use editor::test::editor_test_context::assert_state_with_diff; + use gpui::TestAppContext; + use project::{FakeFs, Fs, Project}; + use settings::{Settings, SettingsStore}; + use std::path::PathBuf; + use unindent::unindent; + use util::path; + use workspace::Workspace; + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + language::init(cx); + Project::init_settings(cx); + workspace::init_settings(cx); + editor::init_settings(cx); + theme::ThemeSettings::register(cx) + }); + } + + #[gpui::test] + async fn test_diff_view(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/test"), + serde_json::json!({ + "old_file.txt": "old line 1\nline 2\nold line 3\nline 4\n", + "new_file.txt": "new line 1\nline 2\nnew line 3\nline 4\n" + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; + + let (workspace, mut cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + + let diff_view = workspace + .update_in(cx, |workspace, window, cx| { + DiffView::open( + PathBuf::from(path!("/test/old_file.txt")), + PathBuf::from(path!("/test/new_file.txt")), + workspace, + window, + cx, + ) + }) + .await + .unwrap(); + + // Verify initial diff + assert_state_with_diff( + &diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()), + &mut cx, + &unindent( + " + - old line 1 + + ˇnew line 1 + line 2 + - old line 3 + + new line 3 + line 4 + ", + ), + ); + + // Modify the new file on disk + fs.save( + path!("/test/new_file.txt").as_ref(), + &unindent( + " + new line 1 + line 2 + new line 3 + line 4 + new line 5 + ", + ) + .into(), + Default::default(), + ) + .await + .unwrap(); + + // The diff now reflects the changes to the new file + cx.executor().advance_clock(RECALCULATE_DIFF_DEBOUNCE); + assert_state_with_diff( + &diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()), + &mut cx, + &unindent( + " + - old line 1 + + ˇnew line 1 + line 2 + - old line 3 + + new line 3 + line 4 + + new line 5 + ", + ), + ); + + // Modify the old file on disk + fs.save( + path!("/test/old_file.txt").as_ref(), + &unindent( + " + new line 1 + line 2 + old line 3 + line 4 + ", + ) + .into(), + Default::default(), + ) + .await + .unwrap(); + + // The diff now reflects the changes to the new file + cx.executor().advance_clock(RECALCULATE_DIFF_DEBOUNCE); + assert_state_with_diff( + &diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()), + &mut cx, + &unindent( + " + ˇnew line 1 + line 2 + - old line 3 + + new line 3 + line 4 + + new line 5 + ", + ), + ); + } +} diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index d0c6792ceb..1653902bbd 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -22,6 +22,7 @@ mod commit_modal; pub mod commit_tooltip; mod commit_view; mod conflict_view; +pub mod diff_view; pub mod git_panel; mod git_panel_settings; pub mod onboarding; diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 4e88f351c8..b362a2a982 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -4268,6 +4268,11 @@ impl BufferSnapshot { self.non_text_state_update_count } + /// An integer version that changes when the buffer's syntax changes. + pub fn syntax_update_count(&self) -> usize { + self.syntax.update_count() + } + /// Returns a snapshot of underlying file. pub fn file(&self) -> Option<&Arc> { self.file.as_ref() diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index 2dd00bb741..da05416e89 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -32,6 +32,7 @@ pub struct SyntaxSnapshot { parsed_version: clock::Global, interpolated_version: clock::Global, language_registry_version: usize, + update_count: usize, } #[derive(Default)] @@ -257,7 +258,9 @@ impl SyntaxMap { } pub fn clear(&mut self, text: &BufferSnapshot) { + let update_count = self.snapshot.update_count + 1; self.snapshot = SyntaxSnapshot::new(text); + self.snapshot.update_count = update_count; } } @@ -268,6 +271,7 @@ impl SyntaxSnapshot { parsed_version: clock::Global::default(), interpolated_version: clock::Global::default(), language_registry_version: 0, + update_count: 0, } } @@ -275,6 +279,10 @@ impl SyntaxSnapshot { self.layers.is_empty() } + pub fn update_count(&self) -> usize { + self.update_count + } + pub fn interpolate(&mut self, text: &BufferSnapshot) { let edits = text .anchored_edits_since::<(usize, Point)>(&self.interpolated_version) @@ -443,6 +451,8 @@ impl SyntaxSnapshot { self.language_registry_version = registry.version(); } } + + self.update_count += 1; } fn reparse_with_ranges( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5bc045ef9a..130e4c7a40 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2439,11 +2439,14 @@ impl Project { abs_path: impl AsRef, cx: &mut Context, ) -> Task>> { - if let Some((worktree, relative_path)) = self.find_worktree(abs_path.as_ref(), cx) { - self.open_buffer((worktree.read(cx).id(), relative_path), cx) - } else { - Task::ready(Err(anyhow!("no such path"))) - } + let worktree_task = self.find_or_create_worktree(abs_path.as_ref(), false, cx); + cx.spawn(async move |this, cx| { + let (worktree, relative_path) = worktree_task.await?; + this.update(cx, |this, cx| { + this.open_buffer((worktree.read(cx).id(), relative_path), cx) + })? + .await + }) } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 93afdb48c0..90b8e3e0b7 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -46,10 +46,10 @@ use uuid::Uuid; use welcome::{BaseKeymap, FIRST_OPEN, show_welcome_view}; use workspace::{AppState, SerializedWorkspaceLocation, WorkspaceSettings, WorkspaceStore}; use zed::{ - OpenListener, OpenRequest, app_menus, build_window_options, derive_paths_with_position, - handle_cli_connection, handle_keymap_file_changes, handle_settings_changed, - handle_settings_file_changes, initialize_workspace, inline_completion_registry, - open_paths_with_positions, + OpenListener, OpenRequest, RawOpenRequest, app_menus, build_window_options, + derive_paths_with_position, handle_cli_connection, handle_keymap_file_changes, + handle_settings_changed, handle_settings_file_changes, initialize_workspace, + inline_completion_registry, open_paths_with_positions, }; #[cfg(feature = "mimalloc")] @@ -329,7 +329,12 @@ pub fn main() { app.on_open_urls({ let open_listener = open_listener.clone(); - move |urls| open_listener.open_urls(urls) + move |urls| { + open_listener.open(RawOpenRequest { + urls, + diff_paths: Vec::new(), + }) + } }); app.on_reopen(move |cx| { if let Some(app_state) = AppState::try_global(cx).and_then(|app_state| app_state.upgrade()) @@ -658,15 +663,21 @@ pub fn main() { .filter_map(|arg| parse_url_arg(arg, cx).log_err()) .collect(); - if !urls.is_empty() { - open_listener.open_urls(urls) + let diff_paths: Vec<[String; 2]> = args + .diff + .chunks(2) + .map(|chunk| [chunk[0].clone(), chunk[1].clone()]) + .collect(); + + if !urls.is_empty() || !diff_paths.is_empty() { + open_listener.open(RawOpenRequest { urls, diff_paths }) } match open_rx .try_next() .ok() .flatten() - .and_then(|urls| OpenRequest::parse(urls, cx).log_err()) + .and_then(|request| OpenRequest::parse(request, cx).log_err()) { Some(request) => { handle_open_request(request, app_state.clone(), cx); @@ -733,13 +744,14 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut } let mut task = None; - if !request.open_paths.is_empty() { + if !request.open_paths.is_empty() || !request.diff_paths.is_empty() { let app_state = app_state.clone(); task = Some(cx.spawn(async move |mut cx| { let paths_with_position = derive_paths_with_position(app_state.fs.as_ref(), request.open_paths).await; let (_window, results) = open_paths_with_positions( &paths_with_position, + &request.diff_paths, app_state, workspace::OpenOptions::default(), &mut cx, @@ -1027,6 +1039,10 @@ struct Args { /// URLs can either be `file://` or `zed://` scheme, or relative to . paths_or_urls: Vec, + /// Pairs of file paths to diff. Can be specified multiple times. + #[arg(long, action = clap::ArgAction::Append, num_args = 2, value_names = ["OLD_PATH", "NEW_PATH"])] + diff: Vec, + /// Sets a custom directory for all user data (e.g., database, extensions, logs). /// This overrides the default platform-specific data directory location. /// On macOS, the default is `~/Library/Application Support/Zed`. diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index cf158c918f..0318d4c00b 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -570,7 +570,10 @@ fn register_actions( window.toggle_fullscreen(); }) .register_action(|_, action: &OpenZedUrl, _, cx| { - OpenListener::global(cx).open_urls(vec![action.url.clone()]) + OpenListener::global(cx).open(RawOpenRequest { + urls: vec![action.url.clone()], + ..Default::default() + }) }) .register_action(|_, action: &OpenBrowser, _window, cx| cx.open_url(&action.url)) .register_action(|workspace, _: &workspace::Open, window, cx| { diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index f7767a310f..8b41901335 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -1,6 +1,6 @@ use crate::handle_open_request; use crate::restorable_workspace_locations; -use anyhow::{Context as _, Result}; +use anyhow::{Context as _, Result, anyhow}; use cli::{CliRequest, CliResponse, ipc::IpcSender}; use cli::{IpcHandshake, ipc}; use client::parse_zed_link; @@ -12,6 +12,7 @@ use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures::channel::{mpsc, oneshot}; use futures::future::join_all; use futures::{FutureExt, SinkExt, StreamExt}; +use git_ui::diff_view::DiffView; use gpui::{App, AsyncApp, Global, WindowHandle}; use language::Point; use recent_projects::{SshSettings, open_ssh_project}; @@ -31,6 +32,7 @@ use workspace::{AppState, OpenOptions, SerializedWorkspaceLocation, Workspace}; pub struct OpenRequest { pub cli_connection: Option<(mpsc::Receiver, IpcSender)>, pub open_paths: Vec, + pub diff_paths: Vec<[String; 2]>, pub open_channel_notes: Vec<(u64, Option)>, pub join_channel: Option, pub ssh_connection: Option, @@ -38,9 +40,9 @@ pub struct OpenRequest { } impl OpenRequest { - pub fn parse(urls: Vec, cx: &App) -> Result { + pub fn parse(request: RawOpenRequest, cx: &App) -> Result { let mut this = Self::default(); - for url in urls { + for url in request.urls { if let Some(server_name) = url.strip_prefix("zed-cli://") { this.cli_connection = Some(connect_to_cli(server_name)?); } else if let Some(action_index) = url.strip_prefix("zed-dock-action://") { @@ -61,6 +63,8 @@ impl OpenRequest { } } + this.diff_paths = request.diff_paths; + Ok(this) } @@ -130,19 +134,25 @@ impl OpenRequest { } #[derive(Clone)] -pub struct OpenListener(UnboundedSender>); +pub struct OpenListener(UnboundedSender); + +#[derive(Default)] +pub struct RawOpenRequest { + pub urls: Vec, + pub diff_paths: Vec<[String; 2]>, +} impl Global for OpenListener {} impl OpenListener { - pub fn new() -> (Self, UnboundedReceiver>) { + pub fn new() -> (Self, UnboundedReceiver) { let (tx, rx) = mpsc::unbounded(); (OpenListener(tx), rx) } - pub fn open_urls(&self, urls: Vec) { + pub fn open(&self, request: RawOpenRequest) { self.0 - .unbounded_send(urls) + .unbounded_send(request) .context("no listener for open requests") .log_err(); } @@ -164,7 +174,10 @@ pub fn listen_for_cli_connections(opener: OpenListener) -> Result<()> { thread::spawn(move || { let mut buf = [0u8; 1024]; while let Ok(len) = listener.recv(&mut buf) { - opener.open_urls(vec![String::from_utf8_lossy(&buf[..len]).to_string()]); + opener.open(RawOpenRequest { + urls: vec![String::from_utf8_lossy(&buf[..len]).to_string()], + ..Default::default() + }); } }); Ok(()) @@ -201,6 +214,7 @@ fn connect_to_cli( pub async fn open_paths_with_positions( path_positions: &[PathWithPosition], + diff_paths: &[[String; 2]], app_state: Arc, open_options: workspace::OpenOptions, cx: &mut AsyncApp, @@ -225,11 +239,27 @@ pub async fn open_paths_with_positions( }) .collect::>(); - let (workspace, items) = cx + let (workspace, mut items) = cx .update(|cx| workspace::open_paths(&paths, app_state, open_options, cx))? .await?; - for (item, path) in items.iter().zip(&paths) { + for diff_pair in diff_paths { + let old_path = Path::new(&diff_pair[0]).canonicalize()?; + let new_path = Path::new(&diff_pair[1]).canonicalize()?; + if let Ok(diff_view) = workspace.update(cx, |workspace, window, cx| { + DiffView::open(old_path, new_path, workspace, window, cx) + }) { + if let Some(diff_view) = diff_view.await.log_err() { + items.push(Some(Ok(Box::new(diff_view)))) + } + } + } + + for (item, path) in items.iter_mut().zip(&paths) { + if let Some(Err(error)) = item { + *error = anyhow!("error opening {path:?}: {error}"); + continue; + } let Some(Ok(item)) = item else { continue; }; @@ -260,14 +290,15 @@ pub async fn handle_cli_connection( CliRequest::Open { urls, paths, + diff_paths, wait, open_new_workspace, env, - user_data_dir: _, // Ignore user_data_dir + user_data_dir: _, } => { if !urls.is_empty() { cx.update(|cx| { - match OpenRequest::parse(urls, cx) { + match OpenRequest::parse(RawOpenRequest { urls, diff_paths }, cx) { Ok(open_request) => { handle_open_request(open_request, app_state.clone(), cx); responses.send(CliResponse::Exit { status: 0 }).log_err(); @@ -288,6 +319,7 @@ pub async fn handle_cli_connection( let open_workspace_result = open_workspaces( paths, + diff_paths, open_new_workspace, &responses, wait, @@ -306,6 +338,7 @@ pub async fn handle_cli_connection( async fn open_workspaces( paths: Vec, + diff_paths: Vec<[String; 2]>, open_new_workspace: Option, responses: &IpcSender, wait: bool, @@ -362,6 +395,7 @@ async fn open_workspaces( let workspace_failed_to_open = open_local_workspace( workspace_paths, + diff_paths.clone(), open_new_workspace, wait, responses, @@ -411,6 +445,7 @@ async fn open_workspaces( async fn open_local_workspace( workspace_paths: Vec, + diff_paths: Vec<[String; 2]>, open_new_workspace: Option, wait: bool, responses: &IpcSender, @@ -424,6 +459,7 @@ async fn open_local_workspace( derive_paths_with_position(app_state.fs.as_ref(), workspace_paths).await; match open_paths_with_positions( &paths_with_position, + &diff_paths, app_state.clone(), workspace::OpenOptions { open_new_workspace, @@ -437,7 +473,7 @@ async fn open_local_workspace( Ok((workspace, items)) => { let mut item_release_futures = Vec::new(); - for (item, path) in items.into_iter().zip(&paths_with_position) { + for item in items { match item { Some(Ok(item)) => { cx.update(|cx| { @@ -456,7 +492,7 @@ async fn open_local_workspace( Some(Err(err)) => { responses .send(CliResponse::Stderr { - message: format!("error opening {path:?}: {err}"), + message: err.to_string(), }) .log_err(); errored = true; @@ -468,7 +504,7 @@ async fn open_local_workspace( if wait { let background = cx.background_executor().clone(); let wait = async move { - if paths_with_position.is_empty() { + if paths_with_position.is_empty() && diff_paths.is_empty() { let (done_tx, done_rx) = oneshot::channel(); let _subscription = workspace.update(cx, |_, _, cx| { cx.on_release(move |_, _| { @@ -549,8 +585,16 @@ mod tests { cx.update(|cx| { SshSettings::register(cx); }); - let request = - cx.update(|cx| OpenRequest::parse(vec!["ssh://me@localhost:/".into()], cx).unwrap()); + let request = cx.update(|cx| { + OpenRequest::parse( + RawOpenRequest { + urls: vec!["ssh://me@localhost:/".into()], + ..Default::default() + }, + cx, + ) + .unwrap() + }); assert_eq!( request.ssh_connection.unwrap(), SshConnectionOptions { @@ -692,6 +736,7 @@ mod tests { .spawn(|mut cx| async move { open_local_workspace( workspace_paths, + vec![], open_new_workspace, false, &response_tx, diff --git a/crates/zed/src/zed/windows_only_instance.rs b/crates/zed/src/zed/windows_only_instance.rs index 972cad38fe..277e8ee724 100644 --- a/crates/zed/src/zed/windows_only_instance.rs +++ b/crates/zed/src/zed/windows_only_instance.rs @@ -23,7 +23,7 @@ use windows::{ core::HSTRING, }; -use crate::{Args, OpenListener}; +use crate::{Args, OpenListener, RawOpenRequest}; pub fn is_first_instance() -> bool { unsafe { @@ -40,7 +40,14 @@ pub fn is_first_instance() -> bool { pub fn handle_single_instance(opener: OpenListener, args: &Args, is_first_instance: bool) -> bool { if is_first_instance { // We are the first instance, listen for messages sent from other instances - std::thread::spawn(move || with_pipe(|url| opener.open_urls(vec![url]))); + std::thread::spawn(move || { + with_pipe(|url| { + opener.open(RawOpenRequest { + urls: vec![url], + ..Default::default() + }) + }) + }); } else if !args.foreground { // We are not the first instance, send args to the first instance send_args_to_instance(args).log_err(); @@ -109,6 +116,7 @@ fn send_args_to_instance(args: &Args) -> anyhow::Result<()> { let request = { let mut paths = vec![]; let mut urls = vec![]; + let mut diff_paths = vec![]; for path in args.paths_or_urls.iter() { match std::fs::canonicalize(&path) { Ok(path) => paths.push(path.to_string_lossy().to_string()), @@ -126,9 +134,22 @@ fn send_args_to_instance(args: &Args) -> anyhow::Result<()> { } } } + + for path in args.diff.chunks(2) { + let old = std::fs::canonicalize(&path[0]).log_err(); + let new = std::fs::canonicalize(&path[1]).log_err(); + if let Some((old, new)) = old.zip(new) { + diff_paths.push([ + old.to_string_lossy().to_string(), + new.to_string_lossy().to_string(), + ]); + } + } + CliRequest::Open { paths, urls, + diff_paths, wait: false, open_new_workspace: None, env: None,