Files
zed/crates/cli/src/cli.rs
T
Kirill Bulatov a33b606ed1 Stop using wrong paths for zed --diff (#56380)
Closes https://github.com/zed-industries/zed/issues/56219

Contains 3 commist:
*
https://github.com/zed-industries/zed/pull/56380/changes/a8d55273d6cddb39868a1fd669c5069a1133a373
shows better errors when CLI or main binary fail early: no backtraces,
better error context, diff file paths shown

*
https://github.com/zed-industries/zed/pull/56380/changes/d389f7ee239dcf9bfa2bfe91729d96d2c1931663
stops opening Zed if cli `--diff` path argument(s) does not exist, also
switches over async fs API on the Zed side.
This is a behavior change, as before Zed tried to open or connect to an
instance — can be reverted if needed.

With `Path::exists` check CLI will do now:

<img width="669" height="55" alt="now"
src="https://github.com/user-attachments/assets/bdfbef2f-1b28-443d-8a01-0ff73ec0bba1"
/>

If I remove that bit, Zed will now open in the same cwd where the CLI is
invoked in:

<img width="1724" height="639" alt="reverted"
src="https://github.com/user-attachments/assets/69cd171b-aca3-445b-8647-5786f3360ce4"
/>


*
https://github.com/zed-industries/zed/pull/56380/changes/49787b7366336711b5bfc11ba3661f3ba70ce8ca
fixes an underlying bug leading to memory leak.

If on current `main`, I apply

```diff
diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs
index 18ea7c0869..5db22521f2 100644
--- a/crates/zed/src/zed/open_listener.rs
+++ b/crates/zed/src/zed/open_listener.rs
@@ -791,6 +791,7 @@ async fn open_local_workspace(
     // working directory so the workspace opens with the right context.
     if !user_provided_paths && !diff_paths.is_empty() {
         if let Ok(cwd) = std::env::current_dir() {
+            log::error!("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ {cwd:?}");
             workspace_paths.push(cwd.to_string_lossy().into_owned());
         }
     }
```

I see the following logs:

```
2026-05-11T09:52:35+03:00 INFO  [zed] ========== starting zed version 1.3.0+dev.7beb533da92b89ef0a8219e15a37c9ef6cddefa8, sha 7beb533 ==========
2026-05-11T09:52:36+03:00 INFO  [zed] Using git binary path: "/Applications/Zed Dev.app/Contents/MacOS/git"
2026-05-11T09:52:36+03:00 INFO  [util] set environment variables from shell:/bin/zsh, path:/opt/homebrew/opt/llvm/bin:/Applications/Postgres.app/Contents/Versions/16/bin/:/Users/someonetoignore/Developer/PlaydateSDK/bin/:/Users/someonetoignore/.docker/bin/:/opt/homebrew/opt/armv7-unknown-linux-gnueabihf/bin/:/opt/homebrew/opt/rustup/bin/:/opt/homebrew/opt/go@1.19/bin/:/usr/local/opt/llvm/bin/:/Users/someonetoignore/.jetbrains/bin/:/Users/someonetoignore/.cargo/bin/:/usr/local/git/bin/:/opt/homebrew/Cellar/openjdk@21/21.0.6//bin/:/Users/someonetoignore/.local/state/fnm_multishells/75815_1778482356501/bin:/opt/homebrew/opt/ruby/bin:/opt/homebrew/lib/ruby/gems/4.0.0/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/opt/pkg/env/active/bin:/opt/pmk/env/global/bin:/opt/X11/bin:/Library/Apple/usr/bin:/Applications/Wireshark.app/Contents/MacOS:/Library/Frameworks/Mono.framework/Versions/Current/Commands:/Users/someonetoignore/Library/pnpm:/opt/homebrew/opt/llvm/bin:/Applications/Postgres.app/Contents/Versions/16/bin/:/Users/someonetoignore/Developer/PlaydateSDK/bin/:/Users/someonetoignore/.docker/bin/:/opt/homebrew/opt/armv7-unknown-linux-gnueabihf/bin/:/opt/homebrew/opt/rustup/bin/:/opt/homebrew/opt/go@1.19/bin/:/usr/local/opt/llvm/bin/:/Users/someonetoignore/.jetbrains/bin/:/Users/someonetoignore/.cargo/bin/:/usr/local/git/bin/:/opt/homebrew/Cellar/openjdk@21/21.0.6//bin/:/Users/someonetoignore/.local/state/fnm_multishells/23413_1778480085615/bin:/opt/homebrew/opt/ruby/bin:/opt/homebrew/lib/ruby/gems/4.0.0/bin:/Users/someonetoignore/.cargo/bin:/Applications/iTerm.app/Contents/Resources/utilities:/Users/someonetoignore/.orbstack/bin:/Users/someonetoignore/.orbstack/bin
2026-05-11T09:52:36+03:00 INFO  [zed::reliability] Debug assertions enabled, skipping hang monitoring
2026-05-11T09:52:36+03:00 WARN  [zed::reliability] Minidump endpoint not set
2026-05-11T09:52:36+03:00 INFO  [extension_host] extensions updated. loading 21, reloading 0, unloading 0
2026-05-11T09:52:37+03:00 ERROR [crates/zed/src/main.rs:1936] canonicalizing "crates/grammars/src": No such file or directory (os error 2)
2026-05-11T09:52:37+03:00 INFO  [client] set status on client 0: Authenticating
2026-05-11T09:52:37+03:00 ERROR [zed::zed::open_listener] @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ "/"
2026-05-11T09:52:37+03:00 INFO  [project::trusted_worktrees] Worktree "/" is not trusted
2026-05-11T09:52:37+03:00 ERROR [worktree] error reading target of symlink "/.VolumeIcon.icns": canonicalizing "/.VolumeIcon.icns": No such file or directory (os error 2)
2026-05-11T09:52:37+03:00 ERROR [worktree] error reading target of symlink "/Users/someonetoignore/.gitconfig.zed": canonicalizing "/Users/someonetoignore/.gitconfig.zed": No such file or directory (os error 2)
2026-05-11T09:52:37+03:00 INFO  [workspace] Rendered first frame
2026-05-11T09:52:37+03:00 ERROR [worktree] error reading target of symlink "/usr/lib/libnetwork.dylib": canonicalizing "/usr/lib/libnetwork.dylib": No such file or directory (os error 2)
2026-05-11T09:52:37+03:00 WARN  [fs] Failed to read symlink target metadata for path "/usr/sbin/weakpass_edit": Permission denied (os error 13)
2026-05-11T09:52:37+03:00 ERROR [worktree] error reading target of symlink "/usr/sbin/weakpass_edit": canonicalizing "/usr/sbin/weakpass_edit": Permission denied (os error 13)
2026-05-11T09:52:37+03:00 ERROR [worktree] error reading target of symlink "/usr/lib/libz.1.2.12.dylib": canonicalizing "/usr/lib/libz.1.2.12.dylib": No such file or directory (os error 2)
2026-05-11T09:52:37+03:00 ERROR [worktree] error reading target of symlink "/usr/lib/libpcre2-8.dylib": canonicalizing "/usr/lib/libpcre2-8.dylib": No such file or directory (os error 2)
2026-05-11T09:52:38+03:00 ERROR [worktree] error reading target of symlink "/var/run/docker.sock": canonicalizing "/var/run/docker.sock": No such file or directory (os error 2)
2026-05-11T09:52:38+03:00 ERROR [worktree] error processing "/var/db/DifferentialPrivacy": Operation not permitted (os error 1)
2026-05-11T09:52:38+03:00 ERROR [worktree] error reading target of symlink "/usr/lib/libipconfig.dylib": canonicalizing "/usr/lib/libipconfig.dylib": No such file or directory (os error 2)
2026-05-11T09:52:38+03:00 ERROR [worktree] error reading target of symlink "/private/var/run/docker.sock": canonicalizing "/private/var/run/docker.sock": No such file or directory (os error 2)
2026-05-11T09:52:38+03:00 ERROR [worktree] error processing "/private/var/db/DifferentialPrivacy": Operation not permitted (os error 1)
2026-05-11T09:52:38+03:00 ERROR [worktree] error processing "/Library/Caches/com.apple.amsengagementd.classicdatavault": Operation not permitted (os error 1)
2026-05-11T09:52:38+03:00 ERROR [worktree] error reading target of symlink "/usr/lib/libpcre2-posix.dylib": canonicalizing "/usr/lib/libpcre2-posix.dylib": No such file or directory (os error 2)
2026-05-11T09:52:38+03:00 ERROR [worktree] error processing "/Library/Caches/com.apple.aneuserd": Operation not permitted (os error 1)
2026-05-11T09:52:38+03:00 ERROR [worktree] error processing "/Library/Caches/com.apple.aned": Operation not permitted (os error 1)
```

According to
https://apple.stackexchange.com/questions/284754/what-is-the-default-working-directory-of-a-script-run-via-launchd
, the current directory of a running macOS app could be `/` if started
the way similar to how we do it via the CLI:


https://github.com/zed-industries/zed/blob/7beb533da92b89ef0a8219e15a37c9ef6cddefa8/crates/cli/src/main.rs#L1306-L1329

This means that every `std::env::current_dir()` is potentially dangerous
currently, as e.g. diff code tries to open this `/` as a worktree and
index it fully.
It seems that we're "ok" for now: the dangerous code is mostly in
extensions (there we set the cwd) and cli tools, and 2 places in
"development" Zed's code are left after this one is fixed.
There's one in `fs.rs` but that one is cfg-gated to Windows only hence
should not be an issue, at least the related one.

I'm not sure if this is the best way to fix the issue: setting `/` as an
app's current directory seems also wrong and maybe that invocation CLI
code could be altered somehow?
Maybe, `open_local_workspace` could be reworked somehow? 
Seems that now we need a "shared directory" for both files we diff which
seems inevitable though, hence I've went on with passing the CLI's
current dir when opening items and that fixes the `/` issue along with
the OOM for now.


Release Notes:

- Fixed a memory leak with diffing non-existing files with Zed cli
2026-05-11 15:53:20 +00:00

107 lines
3.6 KiB
Rust

use std::path::PathBuf;
use anyhow::Result;
use collections::HashMap;
pub use ipc_channel::ipc;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct IpcHandshake {
pub requests: ipc::IpcSender<CliRequest>,
pub responses: ipc::IpcReceiver<CliResponse>,
}
/// Controls how CLI paths are opened — whether to reuse existing windows,
/// create new ones, or add to the sidebar.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum OpenBehavior {
/// Consult the user's `cli_default_open_behavior` setting to choose between
/// `ExistingWindow` or `Classic`.
#[default]
Default,
/// Always create a new window. No matching against existing worktrees.
/// Corresponds to `zed -n`.
AlwaysNew,
/// Match broadly including subdirectories, and fall back to any existing
/// window if no worktree matched. Corresponds to `zed -a`.
Add,
/// Open directories as a new workspace in the current Zed window's sidebar.
/// Reuse existing windows for files in open worktrees.
/// Corresponds to `zed -e`.
ExistingWindow,
/// New window for directories, reuse existing window for files in open
/// worktrees. The classic pre-sidebar behavior.
/// Corresponds to `zed --classic`.
Classic,
/// Replace the content of an existing window with a new workspace.
/// Corresponds to `zed -r`.
Reuse,
}
/// The setting-level enum for configuring default behavior. This only has
/// two values because the other modes are always explicitly requested via
/// CLI flags.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CliBehaviorSetting {
/// Open directories as a new workspace in the current Zed window's sidebar.
ExistingWindow,
/// Classic behavior: open directories in a new window, but reuse an
/// existing window when opening files that are already part of an open
/// project.
NewWindow,
}
#[derive(Debug, Serialize, Deserialize)]
pub enum CliRequest {
Open {
paths: Vec<String>,
urls: Vec<String>,
diff_paths: Vec<[String; 2]>,
diff_all: bool,
wsl: Option<String>,
wait: bool,
#[serde(default)]
open_behavior: OpenBehavior,
env: Option<HashMap<String, String>>,
user_data_dir: Option<String>,
dev_container: bool,
#[serde(default)]
cwd: Option<PathBuf>,
},
SetOpenBehavior {
behavior: CliBehaviorSetting,
},
}
#[derive(Debug, Serialize, Deserialize)]
pub enum CliResponse {
Ping,
Stdout { message: String },
Stderr { message: String },
Exit { status: i32 },
PromptOpenBehavior,
}
/// When Zed started not as an *.app but as a binary (e.g. local development),
/// there's a possibility to tell it to behave "regularly".
///
/// Note that in the main zed binary, this variable is unset after it's read for the first time,
/// therefore it should always be accessed through the `FORCE_CLI_MODE` static.
pub const FORCE_CLI_MODE_ENV_VAR_NAME: &str = "ZED_FORCE_CLI_MODE";
/// Abstracts the transport for sending CLI responses (Zed → CLI).
///
/// Production code uses `IpcSender<CliResponse>`. Tests can provide in-memory
/// implementations to avoid OS-level IPC.
pub trait CliResponseSink: Send + 'static {
fn send(&self, response: CliResponse) -> Result<()>;
}
impl CliResponseSink for ipc::IpcSender<CliResponse> {
fn send(&self, response: CliResponse) -> Result<()> {
ipc::IpcSender::send(self, response).map_err(|error| anyhow::anyhow!("{error}"))
}
}