From 185a80a21b30b6d70925be74f3a92980d2dd5485 Mon Sep 17 00:00:00 2001 From: David Barsky Date: Fri, 30 Jan 2026 01:14:36 -0800 Subject: [PATCH] cli: Teach `--diff` to recurse into directories and add a `MultiDiffView` (#45131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This branch: 1. teaches `--diff `command line option to to recurse into folders if provided. 2. Adds a `MultiDiffView` that shows _all_ changed files in a single, scrollable view. This is necessary to provide a smooth user experience for `jj` and Zed users who wish to use Zed as their jj difftool. I'm not fully sure how this change interacts with https://github.com/zed-industries/zed/pull/44936, or what plans y'all have in mind. Here's a screenshot of the resulting behavior: Screenshot 2025-12-17 at 9 10 52 AM I setup zed to handle jj diffs by adding the following to my jj config: ```toml [aliases] zdiff = ["diff", "--tool", "zed"] [merge-tools.zed] program = "/Users/dbarsky/Developer/zed/target/debug/cli" # omit diff-invocation-mode to keep the default (JJ passes two dirs) diff-args = [ "--zed", "/Users/dbarsky/Developer/zed/target/debug/zed", "--wait", "--new", "--diff", "$left", "$right", ] ``` Release Notes: - `--diff`, if provided with folders instead of files, will recurse into those directories. - Added a `MultiDiffView`, which will show all changed files within a single, scrollable view. **AI Disclosure**: Pretty much all of this code was written using Codex with GPT-5.1-Codex. I edited by hand and tested this. --------- Co-authored-by: Lukas Wirth --- Cargo.lock | 1 + crates/cli/Cargo.toml | 1 + crates/cli/src/cli.rs | 1 + crates/cli/src/main.rs | 128 ++++++- crates/git_ui/src/git_ui.rs | 1 + crates/git_ui/src/multi_diff_view.rs | 377 ++++++++++++++++++++ crates/zed/src/main.rs | 10 + crates/zed/src/zed/open_listener.rs | 37 +- crates/zed/src/zed/windows_only_instance.rs | 1 + 9 files changed, 550 insertions(+), 7 deletions(-) create mode 100644 crates/git_ui/src/multi_diff_view.rs diff --git a/Cargo.lock b/Cargo.lock index 42c7558a1d00735d1e653c4db82bc2fdf8cf5875..3fc440414653dfb9f1e403927292c22e083d7435 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3082,6 +3082,7 @@ dependencies = [ "serde_json", "tempfile", "util", + "walkdir", "windows 0.61.3", ] diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 63e99a3ed25fad919e1a86a3a1917e3617ac2737..c7a71f036a350b5ab4b8a7eb49fd1ba0aa7d7272 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -33,6 +33,7 @@ serde.workspace = true util.workspace = true tempfile.workspace = true rayon.workspace = true +walkdir = "2.5" [dev-dependencies] serde_json.workspace = true diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index fbd7e2693a74598f3840afa5f4a99c86e96f2357..8a2394372faf17281babf2cc9769648d64cd67be 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -14,6 +14,7 @@ pub enum CliRequest { paths: Vec, urls: Vec, diff_paths: Vec<[String; 2]>, + diff_all: bool, wsl: Option, wait: bool, open_new_workspace: Option, diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index e1a7a1481b56633364cb011f46cd55e616244f2c..32d727962e0331eb5d59b63bbf5aea6f67455d35 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -12,6 +12,7 @@ use clap::Parser; use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer}; use parking_lot::Mutex; use std::{ + collections::{BTreeMap, BTreeSet}, env, ffi::OsStr, fs, io, @@ -20,8 +21,9 @@ use std::{ sync::Arc, thread::{self, JoinHandle}, }; -use tempfile::NamedTempFile; +use tempfile::{NamedTempFile, TempDir}; use util::paths::PathWithPosition; +use walkdir::WalkDir; #[cfg(any(target_os = "linux", target_os = "freebsd"))] use std::io::IsTerminal; @@ -117,6 +119,7 @@ struct Args { #[arg(long)] system_specs: bool, /// Pairs of file paths to diff. Can be specified multiple times. + /// When directories are provided, recurses into them and shows all changed files in a single multi-diff view. #[arg(long, action = clap::ArgAction::Append, num_args = 2, value_names = ["OLD_PATH", "NEW_PATH"])] diff: Vec, /// Uninstall Zed from user system @@ -180,6 +183,104 @@ fn parse_path_with_position(argument_str: &str) -> anyhow::Result { .map(|path_with_pos| path_with_pos.to_string(|path| path.to_string_lossy().into_owned())) } +fn expand_directory_diff_pairs( + diff_pairs: Vec<[String; 2]>, +) -> anyhow::Result<(Vec<[String; 2]>, Vec)> { + let mut expanded = Vec::new(); + let mut temp_dirs = Vec::new(); + + for pair in diff_pairs { + let left = PathBuf::from(&pair[0]); + let right = PathBuf::from(&pair[1]); + + if left.is_dir() && right.is_dir() { + let (mut pairs, temp_dir) = expand_directory_pair(&left, &right)?; + expanded.append(&mut pairs); + if let Some(temp_dir) = temp_dir { + temp_dirs.push(temp_dir); + } + } else { + expanded.push(pair); + } + } + + Ok((expanded, temp_dirs)) +} + +fn expand_directory_pair( + left: &Path, + right: &Path, +) -> anyhow::Result<(Vec<[String; 2]>, Option)> { + let left_files = collect_files(left)?; + let right_files = collect_files(right)?; + + let mut rel_paths = BTreeSet::new(); + rel_paths.extend(left_files.keys().cloned()); + rel_paths.extend(right_files.keys().cloned()); + + let mut temp_dir = TempDir::new()?; + let mut temp_dir_used = false; + let mut pairs = Vec::new(); + + for rel in rel_paths { + match (left_files.get(&rel), right_files.get(&rel)) { + (Some(left_path), Some(right_path)) => { + pairs.push([ + left_path.to_string_lossy().into_owned(), + right_path.to_string_lossy().into_owned(), + ]); + } + (Some(left_path), None) => { + let stub = create_empty_stub(&mut temp_dir, &rel)?; + temp_dir_used = true; + pairs.push([ + left_path.to_string_lossy().into_owned(), + stub.to_string_lossy().into_owned(), + ]); + } + (None, Some(right_path)) => { + let stub = create_empty_stub(&mut temp_dir, &rel)?; + temp_dir_used = true; + pairs.push([ + stub.to_string_lossy().into_owned(), + right_path.to_string_lossy().into_owned(), + ]); + } + (None, None) => {} + } + } + + let temp_dir = if temp_dir_used { Some(temp_dir) } else { None }; + Ok((pairs, temp_dir)) +} + +fn collect_files(root: &Path) -> anyhow::Result> { + let mut files = BTreeMap::new(); + + for entry in WalkDir::new(root) { + let entry = entry?; + if entry.file_type().is_file() { + let rel = entry + .path() + .strip_prefix(root) + .context("stripping directory prefix")? + .to_path_buf(); + files.insert(rel, entry.into_path()); + } + } + + Ok(files) +} + +fn create_empty_stub(temp_dir: &mut TempDir, rel: &Path) -> anyhow::Result { + let stub_path = temp_dir.path().join(rel); + if let Some(parent) = stub_path.parent() { + fs::create_dir_all(parent)?; + } + fs::File::create(&stub_path)?; + Ok(stub_path) +} + #[cfg(test)] mod tests { use super::*; @@ -476,6 +577,12 @@ fn main() -> Result<()> { let mut stdin_tmp_file: Option = None; let mut anonymous_fd_tmp_files = vec![]; + // Check if any diff paths are directories to determine diff_all mode + let diff_all_mode = args + .diff + .chunks(2) + .any(|pair| Path::new(&pair[0]).is_dir() || Path::new(&pair[1]).is_dir()); + for path in args.diff.chunks(2) { diff_paths.push([ parse_path_with_position(&path[0])?, @@ -483,6 +590,16 @@ fn main() -> Result<()> { ]); } + let (expanded_diff_paths, temp_dirs) = expand_directory_diff_pairs(diff_paths)?; + diff_paths = expanded_diff_paths; + // Prevent automatic cleanup of temp directories containing empty stub files + // for directory diffs. The CLI process may exit before Zed has read these + // files (e.g., when RPC-ing into an already-running instance). The files + // live in the OS temp directory and will be cleaned up on reboot. + for temp_dir in temp_dirs { + let _ = temp_dir.keep(); + } + #[cfg(target_os = "windows")] let wsl = args.wsl.as_ref(); #[cfg(not(target_os = "windows"))] @@ -508,6 +625,14 @@ fn main() -> Result<()> { } } + // When only diff paths are provided (no regular paths), add the current + // working directory so the workspace opens with the right context. + if paths.is_empty() && urls.is_empty() && !diff_paths.is_empty() { + if let Ok(cwd) = env::current_dir() { + paths.push(cwd.to_string_lossy().into_owned()); + } + } + anyhow::ensure!( args.dev_server_token.is_none(), "Dev servers were removed in v0.157.x please upgrade to SSH remoting: https://zed.dev/docs/remote-development" @@ -538,6 +663,7 @@ fn main() -> Result<()> { paths, urls, diff_paths, + diff_all: diff_all_mode, wsl, wait: args.wait, open_new_workspace, diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 0c7da7e90e10260e7ce645716ce31da98f902252..a5c0278aa6c6799470e285a67364bd0fd3aa2b45 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -42,6 +42,7 @@ pub mod file_history_view; pub mod git_panel; mod git_panel_settings; pub mod git_picker; +pub mod multi_diff_view; pub mod onboarding; pub mod picker_prompt; pub mod project_diff; diff --git a/crates/git_ui/src/multi_diff_view.rs b/crates/git_ui/src/multi_diff_view.rs new file mode 100644 index 0000000000000000000000000000000000000000..8e3307823db6468dc18776524ec789024ed8a697 --- /dev/null +++ b/crates/git_ui/src/multi_diff_view.rs @@ -0,0 +1,377 @@ +use anyhow::Result; +use buffer_diff::BufferDiff; +use editor::{Editor, EditorEvent, MultiBuffer, multibuffer_context_lines}; +use gpui::{ + AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle, + Focusable, IntoElement, Render, SharedString, Task, Window, +}; +use language::{Buffer, Capability, OffsetRangeExt}; +use multi_buffer::PathKey; +use project::Project; +use std::{ + any::{Any, TypeId}, + path::{Path, PathBuf}, + sync::Arc, +}; +use theme; +use ui::{Color, Icon, IconName, Label, LabelCommon as _}; +use util::paths::PathStyle; +use util::rel_path::RelPath; +use workspace::{ + Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace, + item::{BreadcrumbText, ItemEvent, SaveOptions, TabContentParams}, + searchable::SearchableItemHandle, +}; + +pub struct MultiDiffView { + editor: Entity, + file_count: usize, +} + +struct Entry { + index: usize, + new_path: PathBuf, + new_buffer: Entity, + diff: Entity, +} + +async fn load_entries( + diff_pairs: Vec<[String; 2]>, + project: &Entity, + cx: &mut AsyncApp, +) -> Result<(Vec, Option)> { + let mut entries = Vec::with_capacity(diff_pairs.len()); + let mut all_paths = Vec::with_capacity(diff_pairs.len()); + + for (ix, pair) in diff_pairs.into_iter().enumerate() { + let old_path = PathBuf::from(&pair[0]); + let new_path = PathBuf::from(&pair[1]); + + 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 diff = build_buffer_diff(&old_buffer, &new_buffer, cx).await?; + + all_paths.push(new_path.clone()); + entries.push(Entry { + index: ix, + new_path, + new_buffer: new_buffer.clone(), + diff, + }); + } + + let common_root = common_prefix(&all_paths); + Ok((entries, common_root)) +} + +fn register_entry( + multibuffer: &Entity, + entry: Entry, + common_root: &Option, + context_lines: u32, + cx: &mut Context, +) { + let snapshot = entry.new_buffer.read(cx).snapshot(); + let diff_snapshot = entry.diff.read(cx).snapshot(cx); + + let ranges: Vec> = diff_snapshot + .hunks(&snapshot) + .map(|hunk| hunk.buffer_range.to_point(&snapshot)) + .collect(); + + let display_rel = common_root + .as_ref() + .and_then(|root| entry.new_path.strip_prefix(root).ok()) + .map(|rel| { + RelPath::new(rel, PathStyle::local()) + .map(|r| r.into_owned().into()) + .unwrap_or_else(|_| { + RelPath::new(Path::new("untitled"), PathStyle::Posix) + .unwrap() + .into_owned() + .into() + }) + }) + .unwrap_or_else(|| { + entry + .new_path + .file_name() + .and_then(|n| n.to_str()) + .and_then(|s| RelPath::new(Path::new(s), PathStyle::Posix).ok()) + .map(|r| r.into_owned().into()) + .unwrap_or_else(|| { + RelPath::new(Path::new("untitled"), PathStyle::Posix) + .unwrap() + .into_owned() + .into() + }) + }); + + let path_key = PathKey::with_sort_prefix(entry.index as u64, display_rel); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.set_excerpts_for_path( + path_key, + entry.new_buffer.clone(), + ranges, + context_lines, + cx, + ); + multibuffer.add_diff(entry.diff.clone(), cx); + }); +} + +fn common_prefix(paths: &[PathBuf]) -> Option { + let mut iter = paths.iter(); + let mut prefix = iter.next()?.clone(); + + for path in iter { + while !path.starts_with(&prefix) { + if !prefix.pop() { + return Some(PathBuf::new()); + } + } + } + + Some(prefix) +} + +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 = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot.text, cx)); + + let update = diff + .update(cx, |diff, cx| { + diff.update_diff( + new_buffer_snapshot.text.clone(), + Some(old_buffer_snapshot.text().into()), + Some(true), + new_buffer_snapshot.language().cloned(), + cx, + ) + }) + .await; + + diff.update(cx, |diff, cx| { + diff.set_snapshot(update, &new_buffer_snapshot.text, cx) + }) + .await; + + Ok(diff) +} + +impl MultiDiffView { + pub fn open( + diff_pairs: Vec<[String; 2]>, + workspace: &Workspace, + window: &mut Window, + cx: &mut App, + ) -> Task>> { + let project = workspace.project().clone(); + let workspace = workspace.weak_handle(); + let context_lines = multibuffer_context_lines(cx); + + window.spawn(cx, async move |cx| { + let (entries, common_root) = load_entries(diff_pairs, &project, cx).await?; + + workspace.update_in(cx, |workspace, window, cx| { + let multibuffer = cx.new(|cx| { + let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); + multibuffer.set_all_diff_hunks_expanded(cx); + multibuffer + }); + + let file_count = entries.len(); + for entry in entries { + register_entry(&multibuffer, entry, &common_root, context_lines, cx); + } + + let diff_view = cx.new(|cx| { + Self::new(multibuffer.clone(), project.clone(), file_count, 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); + }); + + // Hide the left dock (file explorer) for a cleaner diff view + workspace.left_dock().update(cx, |dock, cx| { + dock.set_open(false, window, cx); + }); + + diff_view + }) + }) + } + + fn new( + multibuffer: Entity, + project: Entity, + file_count: usize, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let editor = cx.new(|cx| { + let mut editor = + Editor::for_multibuffer(multibuffer, Some(project.clone()), 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 + }); + + Self { editor, file_count } + } + + fn title(&self) -> SharedString { + let suffix = if self.file_count == 1 { + "1 file".to_string() + } else { + format!("{} files", self.file_count) + }; + format!("Diff ({suffix})").into() + } +} + +impl EventEmitter for MultiDiffView {} + +impl Focusable for MultiDiffView { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.editor.focus_handle(cx) + } +} + +impl Item for MultiDiffView { + 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.title()) + .color(if params.selected { + Color::Default + } else { + Color::Muted + }) + .into_any_element() + } + + fn tab_tooltip_text(&self, _cx: &App) -> Option { + Some(self.title()) + } + + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + self.title() + } + + 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 act_as_type<'a>( + &'a self, + type_id: TypeId, + self_handle: &'a Entity, + _: &'a App, + ) -> Option { + if type_id == TypeId::of::() { + Some(self_handle.clone().into()) + } else if type_id == TypeId::of::() { + Some(self.editor.clone().into()) + } else { + None + } + } + + fn as_searchable(&self, _: &Entity, _: &App) -> Option> { + Some(Box::new(self.editor.clone())) + } + + 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: Arc, + 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) + }); + } + + fn can_save(&self, cx: &App) -> bool { + self.editor.read(cx).can_save(cx) + } + + fn save( + &mut self, + options: SaveOptions, + project: Entity, + window: &mut Window, + cx: &mut Context, + ) -> gpui::Task> { + self.editor + .update(cx, |editor, cx| editor.save(options, project, window, cx)) + } +} + +impl Render for MultiDiffView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + self.editor.clone() + } +} diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index d664b65641ce53f208472a09c84e827feb0573d5..3bac2b92198946118ed50d379cf0de611eeed8a1 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -765,6 +765,12 @@ fn main() { .map(|arg| parse_url_arg(arg, cx)) .collect(); + // Check if any diff paths are directories to determine diff_all mode + let diff_all_mode = args + .diff + .chunks(2) + .any(|pair| Path::new(&pair[0]).is_dir() || Path::new(&pair[1]).is_dir()); + let diff_paths: Vec<[String; 2]> = args .diff .chunks(2) @@ -781,6 +787,7 @@ fn main() { urls, diff_paths, wsl, + diff_all: diff_all_mode, }) } @@ -1052,6 +1059,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut let (workspace, _results) = open_paths_with_positions( &paths_with_position, &[], + false, app_state, workspace::OpenOptions::default(), cx, @@ -1113,6 +1121,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut let (_window, results) = open_paths_with_positions( &paths_with_position, &request.diff_paths, + request.diff_all, app_state, workspace::OpenOptions::default(), cx, @@ -1476,6 +1485,7 @@ struct Args { paths_or_urls: Vec, /// Pairs of file paths to diff. Can be specified multiple times. + /// When directories are provided, recurses into them and shows all changed files in a single multi-diff view. #[arg(long, action = clap::ArgAction::Append, num_args = 2, value_names = ["OLD_PATH", "NEW_PATH"])] diff: Vec, diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 06cf6a90fa17f3a5d33b538c4e27b666102e1f0e..86d35d558dc024931a901c479f26e502de381ca7 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -13,7 +13,7 @@ use futures::channel::{mpsc, oneshot}; use futures::future; use futures::future::join_all; use futures::{FutureExt, SinkExt, StreamExt}; -use git_ui::file_diff_view::FileDiffView; +use git_ui::{file_diff_view::FileDiffView, multi_diff_view::MultiDiffView}; use gpui::{App, AsyncApp, Global, WindowHandle}; use language::Point; use onboarding::FIRST_OPEN; @@ -37,6 +37,7 @@ pub struct OpenRequest { pub kind: Option, pub open_paths: Vec, pub diff_paths: Vec<[String; 2]>, + pub diff_all: bool, pub open_channel_notes: Vec<(u64, Option)>, pub join_channel: Option, pub remote_connection: Option, @@ -77,6 +78,7 @@ impl OpenRequest { let mut this = Self::default(); this.diff_paths = request.diff_paths; + this.diff_all = request.diff_all; if let Some(wsl) = request.wsl { let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') { if user.is_empty() { @@ -253,6 +255,7 @@ pub struct OpenListener(UnboundedSender); pub struct RawOpenRequest { pub urls: Vec, pub diff_paths: Vec<[String; 2]>, + pub diff_all: bool, pub wsl: Option, } @@ -329,6 +332,7 @@ fn connect_to_cli( pub async fn open_paths_with_positions( path_positions: &[PathWithPosition], diff_paths: &[[String; 2]], + diff_all: bool, app_state: Arc, open_options: workspace::OpenOptions, cx: &mut AsyncApp, @@ -357,14 +361,24 @@ pub async fn open_paths_with_positions( .update(|cx| workspace::open_paths(&paths, app_state, open_options, cx)) .await?; - 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 diff_all && !diff_paths.is_empty() { if let Ok(diff_view) = workspace.update(cx, |workspace, window, cx| { - FileDiffView::open(old_path, new_path, workspace, window, cx) + MultiDiffView::open(diff_paths.to_vec(), workspace, window, cx) }) { if let Some(diff_view) = diff_view.await.log_err() { - items.push(Some(Ok(Box::new(diff_view)))) + items.push(Some(Ok(Box::new(diff_view)))); + } + } + } else { + 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| { + FileDiffView::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)))) + } } } } @@ -405,6 +419,7 @@ pub async fn handle_cli_connection( urls, paths, diff_paths, + diff_all, wait, wsl, open_new_workspace, @@ -418,6 +433,7 @@ pub async fn handle_cli_connection( RawOpenRequest { urls, diff_paths, + diff_all, wsl, }, cx, @@ -442,6 +458,7 @@ pub async fn handle_cli_connection( let open_workspace_result = open_workspaces( paths, diff_paths, + diff_all, open_new_workspace, reuse, &responses, @@ -462,6 +479,7 @@ pub async fn handle_cli_connection( async fn open_workspaces( paths: Vec, diff_paths: Vec<[String; 2]>, + diff_all: bool, open_new_workspace: Option, reuse: bool, responses: &IpcSender, @@ -525,6 +543,7 @@ async fn open_workspaces( let workspace_failed_to_open = open_local_workspace( workspace_paths, diff_paths.clone(), + diff_all, open_new_workspace, reuse, wait, @@ -572,6 +591,7 @@ async fn open_workspaces( async fn open_local_workspace( workspace_paths: Vec, diff_paths: Vec<[String; 2]>, + diff_all: bool, open_new_workspace: Option, reuse: bool, wait: bool, @@ -596,6 +616,7 @@ async fn open_local_workspace( let (workspace, items) = match open_paths_with_positions( &paths_with_position, &diff_paths, + diff_all, app_state.clone(), workspace::OpenOptions { open_new_workspace, @@ -935,6 +956,7 @@ mod tests { let errored = open_local_workspace( workspace_paths, vec![], + false, None, false, true, @@ -1026,6 +1048,7 @@ mod tests { open_local_workspace( workspace_paths, vec![], + false, open_new_workspace, false, false, @@ -1099,6 +1122,7 @@ mod tests { open_local_workspace( workspace_paths, vec![], + false, None, false, false, @@ -1123,6 +1147,7 @@ mod tests { open_local_workspace( workspace_paths_reuse, vec![], + false, None, // open_new_workspace will be overridden by reuse logic true, // reuse = true false, diff --git a/crates/zed/src/zed/windows_only_instance.rs b/crates/zed/src/zed/windows_only_instance.rs index f3eab154415814d60e2b06f5823d47006b1c367c..14b08c8f6694a409af1f5f6b3e5dc78e51870e78 100644 --- a/crates/zed/src/zed/windows_only_instance.rs +++ b/crates/zed/src/zed/windows_only_instance.rs @@ -155,6 +155,7 @@ fn send_args_to_instance(args: &Args) -> anyhow::Result<()> { paths, urls, diff_paths, + diff_all: false, wait: false, wsl: args.wsl.clone(), open_new_workspace: None,