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

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>

Change summary

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

Detailed changes

Cargo.lock 🔗

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

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

crates/cli/src/cli.rs 🔗

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

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

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;

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

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

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

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,