Cargo.lock 🔗
@@ -3082,6 +3082,7 @@ dependencies = [
"serde_json",
"tempfile",
"util",
+ "walkdir",
"windows 0.61.3",
]
David Barsky and Lukas Wirth created
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:
<img width="1090" height="950" alt="Screenshot 2025-12-17 at 9 10 52 AM"
src="https://github.com/user-attachments/assets/8efd09b4-974f-4059-9f94-539c484c6d4a"
/>
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 <lukas@zed.dev>
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(-)
@@ -3082,6 +3082,7 @@ dependencies = [
"serde_json",
"tempfile",
"util",
+ "walkdir",
"windows 0.61.3",
]
@@ -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
@@ -14,6 +14,7 @@ pub enum CliRequest {
paths: Vec<String>,
urls: Vec<String>,
diff_paths: Vec<[String; 2]>,
+ diff_all: bool,
wsl: Option<String>,
wait: bool,
open_new_workspace: Option<bool>,
@@ -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<String>,
/// Uninstall Zed from user system
@@ -180,6 +183,104 @@ fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
.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<TempDir>)> {
+ 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<TempDir>)> {
+ 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<BTreeMap<PathBuf, PathBuf>> {
+ 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<PathBuf> {
+ 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<fs::File> = 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,
@@ -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;
@@ -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<Editor>,
+ file_count: usize,
+}
+
+struct Entry {
+ index: usize,
+ new_path: PathBuf,
+ new_buffer: Entity<Buffer>,
+ diff: Entity<BufferDiff>,
+}
+
+async fn load_entries(
+ diff_pairs: Vec<[String; 2]>,
+ project: &Entity<Project>,
+ cx: &mut AsyncApp,
+) -> Result<(Vec<Entry>, Option<PathBuf>)> {
+ 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<MultiBuffer>,
+ entry: Entry,
+ common_root: &Option<PathBuf>,
+ context_lines: u32,
+ cx: &mut Context<Workspace>,
+) {
+ let snapshot = entry.new_buffer.read(cx).snapshot();
+ let diff_snapshot = entry.diff.read(cx).snapshot(cx);
+
+ let ranges: Vec<std::ops::Range<language::Point>> = 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<PathBuf> {
+ 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<Buffer>,
+ new_buffer: &Entity<Buffer>,
+ cx: &mut AsyncApp,
+) -> Result<Entity<BufferDiff>> {
+ 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<Result<Entity<Self>>> {
+ 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<MultiBuffer>,
+ project: Entity<Project>,
+ file_count: usize,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> 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<EditorEvent> 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<Icon> {
+ 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<ui::SharedString> {
+ 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>) {
+ self.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.editor.clone().into())
+ } else {
+ None
+ }
+ }
+
+ fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
+ Some(Box::new(self.editor.clone()))
+ }
+
+ fn set_nav_history(
+ &mut self,
+ nav_history: ItemNavHistory,
+ _: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.editor.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.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.editor.breadcrumbs(theme, cx)
+ }
+
+ fn added_to_workspace(
+ &mut self,
+ workspace: &mut Workspace,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ 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<Project>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> gpui::Task<Result<()>> {
+ 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<Self>) -> impl IntoElement {
+ self.editor.clone()
+ }
+}
@@ -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<AppState>, 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<AppState>, 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<String>,
/// 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<String>,
@@ -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<OpenRequestKind>,
pub open_paths: Vec<String>,
pub diff_paths: Vec<[String; 2]>,
+ pub diff_all: bool,
pub open_channel_notes: Vec<(u64, Option<String>)>,
pub join_channel: Option<u64>,
pub remote_connection: Option<RemoteConnectionOptions>,
@@ -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<RawOpenRequest>);
pub struct RawOpenRequest {
pub urls: Vec<String>,
pub diff_paths: Vec<[String; 2]>,
+ pub diff_all: bool,
pub wsl: Option<String>,
}
@@ -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<AppState>,
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<String>,
diff_paths: Vec<[String; 2]>,
+ diff_all: bool,
open_new_workspace: Option<bool>,
reuse: bool,
responses: &IpcSender<CliResponse>,
@@ -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<String>,
diff_paths: Vec<[String; 2]>,
+ diff_all: bool,
open_new_workspace: Option<bool>,
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,
@@ -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,