cli: Teach --diff to recurse into directories and add a MultiDiffView (#45131)

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>
This commit is contained in:
David Barsky 2026-01-30 01:14:36 -08:00 committed by GitHub
parent 7053561b63
commit 185a80a21b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 550 additions and 7 deletions

1
Cargo.lock generated
View file

@ -3082,6 +3082,7 @@ dependencies = [
"serde_json",
"tempfile",
"util",
"walkdir",
"windows 0.61.3",
]

View file

@ -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

View file

@ -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>,

View file

@ -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,

View file

@ -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;

View file

@ -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()
}
}

View file

@ -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>,

View file

@ -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,

View file

@ -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,