cli: Add first-run prompt for default open behavior and abstract IPC transport (#53663)
This PR adds the `cli_default_open_behavior` setting and a first-run TUI prompt that appears when `zed <path>` is invoked without flags while existing windows are open and the setting hasn't been configured yet. ## What it does ### Setting and prompt - Adds a new `cli_default_open_behavior` workspace setting with two values: `existing_window` (default) and `new_window`. - When the user runs `zed <path>` for the first time with existing Zed windows open, a `dialoguer::Select` prompt in the CLI asks them to choose their preferred behavior. The choice is persisted to `settings.json`. - The prompt is skipped when: - An explicit flag (`-n`, `-e`, `-a`) is given - No existing Zed windows are open - The setting is already configured in `settings.json` - The paths being opened are already contained in an existing workspace ### IPC transport abstraction - Introduces a `CliResponseSink` trait in the `cli` crate that abstracts `IpcSender<CliResponse>`, with an implementation for the real IPC sender. - Replaces `IpcSender<CliResponse>` with `Box<dyn CliResponseSink>` / `&dyn CliResponseSink` across all signatures in `open_listener.rs`: `OpenRequestKind::CliConnection`, `handle_cli_connection`, `maybe_prompt_open_behavior`, `open_workspaces`, `open_local_workspace`. - Extracts the inline CLI response loop from `main.rs` into a testable `cli::run_cli_response_loop` function. - Switches the request channel from bounded `mpsc::channel(16)` to `mpsc::unbounded()`, eliminating `smol::block_on` in the bridge thread. ### End-to-end tests Seven new tests exercise both the CLI-side response loop and the Zed-side handler connected through in-memory channels, using `allow_parking()` so the real `cli::run_cli_response_loop` runs on an OS thread while the GPUI executor drives the Zed handler: - No flags, no windows → no prompt, opens new window - No flags, existing windows, user picks "existing window" → prompt, setting persisted - No flags, existing windows, user picks "new window" → prompt, setting persisted - Setting already configured → no prompt - Paths already in existing workspace → no prompt - Explicit `-e` flag → no prompt - Explicit `-n` flag → no prompt Existing tests that previously used `ipc::channel()` now use a `DiscardResponseSink`, removing OS-level IPC from all tests. Release Notes: - Added a first-run prompt when using `zed <path>` to choose between opening in an existing window or a new window. The choice is saved to settings and can be changed later via the `cli_default_open_behavior` setting. --------- Co-authored-by: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
1 parent
52aa990dcd
commit
afed924a56
16 files changed
+972
-85
No files matched your search
Generated
+42
-12
@@ -2876,8 +2876,10 @@ dependencies = [
|
||||
"askpass",
|
||||
"clap",
|
||||
"collections",
|
||||
"console",
|
||||
"core-foundation 0.10.0",
|
||||
"core-services",
|
||||
"dialoguer",
|
||||
"exec",
|
||||
"fork",
|
||||
"ipc-channel",
|
||||
@@ -3418,6 +3420,18 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.16.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"unicode-width",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console_error_panic_hook"
|
||||
version = "0.1.7"
|
||||
@@ -4839,6 +4853,16 @@ dependencies = [
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dialoguer"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96"
|
||||
dependencies = [
|
||||
"console",
|
||||
"shell-words",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
@@ -4925,7 +4949,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.5.2",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5520,6 +5544,12 @@ dependencies = [
|
||||
"phf 0.11.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
@@ -5691,7 +5721,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7116,7 +7146,7 @@ dependencies = [
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps 7.0.7",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8445,7 +8475,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.57.0",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -11089,7 +11119,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13535,7 +13565,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4"
|
||||
dependencies = [
|
||||
"bytes 1.11.1",
|
||||
"heck 0.4.1",
|
||||
"heck 0.5.0",
|
||||
"itertools 0.12.1",
|
||||
"log",
|
||||
"multimap",
|
||||
@@ -13841,7 +13871,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"socket2 0.6.1",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -15033,7 +15063,7 @@ dependencies = [
|
||||
"errno 0.3.14",
|
||||
"libc",
|
||||
"linux-raw-sys 0.11.0",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -16286,7 +16316,7 @@ version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
@@ -17500,7 +17530,7 @@ dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix 1.1.2",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -18419,7 +18449,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fb391ac70462b3097a755618fbf9c8f95ecc1eb379a414f7b46f202ed10db1f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"windows-targets 0.48.5",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -20531,7 +20561,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -295,6 +295,7 @@ command_palette_hooks = { path = "crates/command_palette_hooks" }
|
||||
compliance = { path = "tooling/compliance" }
|
||||
component = { path = "crates/component" }
|
||||
component_preview = { path = "crates/component_preview" }
|
||||
console = "0.16"
|
||||
context_server = { path = "crates/context_server" }
|
||||
copilot = { path = "crates/copilot" }
|
||||
copilot_chat = { path = "crates/copilot_chat" }
|
||||
@@ -313,6 +314,7 @@ deepseek = { path = "crates/deepseek" }
|
||||
derive_refineable = { path = "crates/refineable/derive_refineable" }
|
||||
dev_container = { path = "crates/dev_container" }
|
||||
diagnostics = { path = "crates/diagnostics" }
|
||||
dialoguer = { version = "0.12", default-features = false }
|
||||
editor = { path = "crates/editor" }
|
||||
encoding_selector = { path = "crates/encoding_selector" }
|
||||
env_var = { path = "crates/env_var" }
|
||||
|
||||
@@ -134,6 +134,15 @@
|
||||
// 3. Do not restore previous workspaces
|
||||
// "restore_on_startup": "none",
|
||||
"restore_on_startup": "last_session",
|
||||
// The default behavior when opening paths from the CLI without
|
||||
// an explicit `-e` (existing window) or `-n` (new window) flag.
|
||||
//
|
||||
// May take 2 values:
|
||||
// 1. Add to the existing Zed window
|
||||
// "cli_default_open_behavior": "existing_window"
|
||||
// 2. Open a new Zed window
|
||||
// "cli_default_open_behavior": "new_window"
|
||||
"cli_default_open_behavior": "existing_window",
|
||||
// Whether to attempt to restore previous file's state when opening it again.
|
||||
// The state is stored per pane.
|
||||
// When disabled, defaults are applied instead of the state restoration.
|
||||
|
||||
@@ -25,6 +25,8 @@ anyhow.workspace = true
|
||||
askpass.workspace = true
|
||||
clap.workspace = true
|
||||
collections.workspace = true
|
||||
console.workspace = true
|
||||
dialoguer.workspace = true
|
||||
ipc-channel = "0.19"
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
pub use ipc_channel::ipc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -8,6 +9,13 @@ pub struct IpcHandshake {
|
||||
pub responses: ipc::IpcReceiver<CliResponse>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CliOpenBehavior {
|
||||
ExistingWindow,
|
||||
NewWindow,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum CliRequest {
|
||||
Open {
|
||||
@@ -18,11 +26,16 @@ pub enum CliRequest {
|
||||
wsl: Option<String>,
|
||||
wait: bool,
|
||||
open_new_workspace: Option<bool>,
|
||||
#[serde(default)]
|
||||
force_existing_window: bool,
|
||||
reuse: bool,
|
||||
env: Option<HashMap<String, String>>,
|
||||
user_data_dir: Option<String>,
|
||||
dev_container: bool,
|
||||
},
|
||||
SetOpenBehavior {
|
||||
behavior: CliOpenBehavior,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -31,6 +44,7 @@ pub enum CliResponse {
|
||||
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),
|
||||
@@ -39,3 +53,17 @@ pub enum CliResponse {
|
||||
/// 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}"))
|
||||
}
|
||||
}
|
||||
+52
-6
@@ -25,7 +25,6 @@ use tempfile::{NamedTempFile, TempDir};
|
||||
use util::paths::PathWithPosition;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
use std::io::IsTerminal;
|
||||
|
||||
const URL_PREFIX: [&'static str; 5] = ["zed://", "http://", "https://", "file://", "ssh://"];
|
||||
@@ -68,14 +67,17 @@ struct Args {
|
||||
#[arg(short, long)]
|
||||
wait: bool,
|
||||
/// Add files to the currently open workspace
|
||||
#[arg(short, long, overrides_with_all = ["new", "reuse"])]
|
||||
#[arg(short, long, overrides_with_all = ["new", "reuse", "existing"])]
|
||||
add: bool,
|
||||
/// Create a new workspace
|
||||
#[arg(short, long, overrides_with_all = ["add", "reuse"])]
|
||||
#[arg(short, long, overrides_with_all = ["add", "reuse", "existing"])]
|
||||
new: bool,
|
||||
/// Reuse an existing window, replacing its workspace
|
||||
#[arg(short, long, overrides_with_all = ["add", "new"])]
|
||||
#[arg(short, long, overrides_with_all = ["add", "new", "existing"])]
|
||||
reuse: bool,
|
||||
/// Open in existing Zed window
|
||||
#[arg(short = 'e', long = "existing", overrides_with_all = ["add", "new", "reuse"])]
|
||||
existing: bool,
|
||||
/// Sets a custom directory for all user data (e.g., database, extensions, logs).
|
||||
/// This overrides the default platform-specific data directory location:
|
||||
#[cfg_attr(target_os = "macos", doc = "`~/Library/Application Support/Zed`.")]
|
||||
@@ -544,6 +546,8 @@ fn main() -> Result<()> {
|
||||
None
|
||||
};
|
||||
|
||||
let force_existing_window = args.existing;
|
||||
|
||||
let env = {
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
{
|
||||
@@ -665,7 +669,7 @@ fn main() -> Result<()> {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let wsl = None;
|
||||
|
||||
tx.send(CliRequest::Open {
|
||||
let open_request = CliRequest::Open {
|
||||
paths,
|
||||
urls,
|
||||
diff_paths,
|
||||
@@ -673,11 +677,14 @@ fn main() -> Result<()> {
|
||||
wsl,
|
||||
wait: args.wait,
|
||||
open_new_workspace,
|
||||
force_existing_window,
|
||||
reuse: args.reuse,
|
||||
env,
|
||||
user_data_dir: user_data_dir_for_thread,
|
||||
dev_container: args.dev_container,
|
||||
})?;
|
||||
};
|
||||
|
||||
tx.send(open_request)?;
|
||||
|
||||
while let Ok(response) = rx.recv() {
|
||||
match response {
|
||||
@@ -688,6 +695,11 @@ fn main() -> Result<()> {
|
||||
exit_status.lock().replace(status);
|
||||
return Ok(());
|
||||
}
|
||||
CliResponse::PromptOpenBehavior => {
|
||||
let behavior = prompt_open_behavior()
|
||||
.unwrap_or(cli::CliOpenBehavior::ExistingWindow);
|
||||
tx.send(CliRequest::SetOpenBehavior { behavior })?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -781,6 +793,40 @@ fn anonymous_fd(path: &str) -> Option<fs::File> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows an interactive prompt asking the user to choose the default open
|
||||
/// behavior for `zed <path>`. Returns `None` if the prompt cannot be shown
|
||||
/// (e.g. stdin is not a terminal) or the user cancels.
|
||||
fn prompt_open_behavior() -> Option<cli::CliOpenBehavior> {
|
||||
if !std::io::stdin().is_terminal() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let blue = console::Style::new().blue();
|
||||
let items = [
|
||||
format!("Add to existing Zed window ({})", blue.apply_to("zed -e")),
|
||||
format!("Open a new window ({})", blue.apply_to("zed -n")),
|
||||
];
|
||||
|
||||
let prompt = format!(
|
||||
"Configure default behavior for {}\n{}",
|
||||
blue.apply_to("zed <path>"),
|
||||
console::style("You can change this later in Zed settings"),
|
||||
);
|
||||
|
||||
let selection = dialoguer::Select::new()
|
||||
.with_prompt(&prompt)
|
||||
.items(&items)
|
||||
.default(0)
|
||||
.interact()
|
||||
.ok()?;
|
||||
|
||||
Some(if selection == 0 {
|
||||
cli::CliOpenBehavior::ExistingWindow
|
||||
} else {
|
||||
cli::CliOpenBehavior::NewWindow
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
mod linux {
|
||||
use std::{
|
||||
|
||||
@@ -159,6 +159,7 @@ pub struct StyledText {
|
||||
text: SharedString,
|
||||
runs: Option<Vec<TextRun>>,
|
||||
delayed_highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
|
||||
delayed_font_family_overrides: Option<Vec<(Range<usize>, SharedString)>>,
|
||||
layout: TextLayout,
|
||||
}
|
||||
|
||||
@@ -169,6 +170,7 @@ impl StyledText {
|
||||
text: text.into(),
|
||||
runs: None,
|
||||
delayed_highlights: None,
|
||||
delayed_font_family_overrides: None,
|
||||
layout: TextLayout::default(),
|
||||
}
|
||||
}
|
||||
@@ -242,6 +244,51 @@ impl StyledText {
|
||||
runs
|
||||
}
|
||||
|
||||
/// Override the font family for specific byte ranges of the text.
|
||||
///
|
||||
/// This is resolved lazily at layout time, so the overrides are applied
|
||||
/// on top of the inherited text style from the parent element.
|
||||
/// Can be combined with [`with_highlights`](Self::with_highlights).
|
||||
///
|
||||
/// The overrides must be sorted by range start and non-overlapping.
|
||||
/// Each override range must fall on character boundaries.
|
||||
pub fn with_font_family_overrides(
|
||||
mut self,
|
||||
overrides: impl IntoIterator<Item = (Range<usize>, SharedString)>,
|
||||
) -> Self {
|
||||
self.delayed_font_family_overrides = Some(
|
||||
overrides
|
||||
.into_iter()
|
||||
.inspect(|(range, _)| {
|
||||
debug_assert!(self.text.is_char_boundary(range.start));
|
||||
debug_assert!(self.text.is_char_boundary(range.end));
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
fn apply_font_family_overrides(
|
||||
runs: &mut [TextRun],
|
||||
overrides: &[(Range<usize>, SharedString)],
|
||||
) {
|
||||
let mut byte_offset = 0;
|
||||
let mut override_idx = 0;
|
||||
for run in runs.iter_mut() {
|
||||
let run_end = byte_offset + run.len;
|
||||
while override_idx < overrides.len() && overrides[override_idx].0.end <= byte_offset {
|
||||
override_idx += 1;
|
||||
}
|
||||
if override_idx < overrides.len() {
|
||||
let (ref range, ref family) = overrides[override_idx];
|
||||
if byte_offset >= range.start && run_end <= range.end {
|
||||
run.font.family = family.clone();
|
||||
}
|
||||
}
|
||||
byte_offset = run_end;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the text runs for this piece of text.
|
||||
pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
|
||||
let mut text = &**self.text;
|
||||
@@ -278,12 +325,19 @@ impl Element for StyledText {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let runs = self.runs.take().or_else(|| {
|
||||
let font_family_overrides = self.delayed_font_family_overrides.take();
|
||||
let mut runs = self.runs.take().or_else(|| {
|
||||
self.delayed_highlights.take().map(|delayed_highlights| {
|
||||
Self::compute_runs(&self.text, &window.text_style(), delayed_highlights)
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(ref overrides) = font_family_overrides {
|
||||
let runs =
|
||||
runs.get_or_insert_with(|| vec![window.text_style().to_run(self.text.len())]);
|
||||
Self::apply_font_family_overrides(runs, overrides);
|
||||
}
|
||||
|
||||
let layout_id = self.layout.layout(self.text.clone(), runs, window, cx);
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
@@ -975,6 +975,7 @@ impl VsCodeSettings {
|
||||
}),
|
||||
bottom_dock_layout: None,
|
||||
centered_layout: None,
|
||||
cli_default_open_behavior: None,
|
||||
close_on_file_delete: None,
|
||||
close_panel_on_toggle: None,
|
||||
command_aliases: Default::default(),
|
||||
|
||||
@@ -49,6 +49,11 @@ pub struct WorkspaceSettingsContent {
|
||||
/// Values: empty_tab, last_workspace, last_session, launchpad
|
||||
/// Default: last_session
|
||||
pub restore_on_startup: Option<RestoreOnStartupBehavior>,
|
||||
/// The default behavior when opening paths from the CLI without
|
||||
/// an explicit `-e` or `-n` flag.
|
||||
///
|
||||
/// Default: existing_window
|
||||
pub cli_default_open_behavior: Option<CliDefaultOpenBehavior>,
|
||||
/// Whether to attempt to restore previous file's state when opening it again.
|
||||
/// The state is stored per pane.
|
||||
/// When disabled, defaults are applied instead of the state restoration.
|
||||
@@ -379,6 +384,31 @@ impl CloseWindowWhenNoItems {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
MergeFrom,
|
||||
Debug,
|
||||
strum::VariantArray,
|
||||
strum::VariantNames,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CliDefaultOpenBehavior {
|
||||
/// Add to the existing Zed window as a new workspace.
|
||||
#[default]
|
||||
#[strum(serialize = "Add to Existing Window")]
|
||||
ExistingWindow,
|
||||
/// Open a new Zed window.
|
||||
#[strum(serialize = "Open a New Window")]
|
||||
NewWindow,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
|
||||
@@ -80,7 +80,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
}
|
||||
|
||||
fn general_page() -> SettingsPage {
|
||||
fn general_settings_section() -> [SettingsPageItem; 8] {
|
||||
fn general_settings_section() -> [SettingsPageItem; 9] {
|
||||
[
|
||||
SettingsPageItem::SectionHeader("General Settings"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
@@ -140,6 +140,27 @@ fn general_page() -> SettingsPage {
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "CLI Default Open Behavior",
|
||||
description: "How `zed <path>` opens directories when no `-e` or `-n` flag is specified.",
|
||||
field: Box::new(SettingField {
|
||||
json_path: Some("cli_default_open_behavior"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.workspace
|
||||
.cli_default_open_behavior
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content.workspace.cli_default_open_behavior = value;
|
||||
},
|
||||
}),
|
||||
metadata: Some(Box::new(SettingsFieldMetadata {
|
||||
should_do_titlecase: Some(false),
|
||||
..Default::default()
|
||||
})),
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Use System Path Prompts",
|
||||
description: "Use native OS dialogs for 'Open' and 'Save As'.",
|
||||
|
||||
@@ -458,6 +458,7 @@ fn init_renderers(cx: &mut App) {
|
||||
.add_basic_renderer::<settings::RestoreOnStartupBehavior>(render_dropdown)
|
||||
.add_basic_renderer::<settings::BottomDockLayout>(render_dropdown)
|
||||
.add_basic_renderer::<settings::OnLastWindowClosed>(render_dropdown)
|
||||
.add_basic_renderer::<settings::CliDefaultOpenBehavior>(render_dropdown)
|
||||
.add_basic_renderer::<settings::CloseWindowWhenNoItems>(render_dropdown)
|
||||
.add_basic_renderer::<settings::TextRenderingMode>(render_dropdown)
|
||||
.add_basic_renderer::<settings::FontFamilyName>(render_font_picker)
|
||||
@@ -1213,7 +1214,8 @@ fn render_settings_item(
|
||||
.child(
|
||||
Label::new(SharedString::new_static(setting_item.description))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
.color(Color::Muted)
|
||||
.render_code_spans(),
|
||||
),
|
||||
)
|
||||
.child(control)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use crate::{LabelLike, prelude::*};
|
||||
use gpui::StyleRefinement;
|
||||
use gpui::{HighlightStyle, StyleRefinement, StyledText};
|
||||
|
||||
/// A struct representing a label element in the UI.
|
||||
///
|
||||
@@ -33,6 +35,7 @@ use gpui::StyleRefinement;
|
||||
pub struct Label {
|
||||
base: LabelLike,
|
||||
label: SharedString,
|
||||
render_code_spans: bool,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
@@ -49,9 +52,17 @@ impl Label {
|
||||
Self {
|
||||
base: LabelLike::new(),
|
||||
label: label.into(),
|
||||
render_code_spans: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// When enabled, text wrapped in backticks (e.g. `` `code` ``) will be
|
||||
/// rendered in the buffer (monospace) font.
|
||||
pub fn render_code_spans(mut self) -> Self {
|
||||
self.render_code_spans = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the text of the [`Label`].
|
||||
pub fn set_text(&mut self, text: impl Into<SharedString>) {
|
||||
self.label = text.into();
|
||||
@@ -233,11 +244,109 @@ impl LabelCommon for Label {
|
||||
}
|
||||
|
||||
impl RenderOnce for Label {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
if self.render_code_spans {
|
||||
if let Some((stripped, code_ranges)) = parse_backtick_spans(&self.label) {
|
||||
let buffer_font_family = theme::theme_settings(cx).buffer_font(cx).family.clone();
|
||||
let background_color = cx.theme().colors().element_background;
|
||||
|
||||
let highlights = code_ranges.iter().map(|range| {
|
||||
(
|
||||
range.clone(),
|
||||
HighlightStyle {
|
||||
background_color: Some(background_color),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
let font_overrides = code_ranges
|
||||
.iter()
|
||||
.map(|range| (range.clone(), buffer_font_family.clone()));
|
||||
|
||||
return self.base.child(
|
||||
StyledText::new(stripped)
|
||||
.with_highlights(highlights)
|
||||
.with_font_family_overrides(font_overrides),
|
||||
);
|
||||
}
|
||||
}
|
||||
self.base.child(self.label)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses backtick-delimited code spans from a string.
|
||||
///
|
||||
/// Returns `None` if there are no matched backtick pairs.
|
||||
/// Otherwise returns the text with backticks stripped and the byte ranges
|
||||
/// of the code spans in the stripped string.
|
||||
fn parse_backtick_spans(text: &str) -> Option<(SharedString, Vec<Range<usize>>)> {
|
||||
if !text.contains('`') {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut stripped = String::with_capacity(text.len());
|
||||
let mut code_ranges = Vec::new();
|
||||
let mut in_code = false;
|
||||
let mut code_start = 0;
|
||||
|
||||
for ch in text.chars() {
|
||||
if ch == '`' {
|
||||
if in_code {
|
||||
code_ranges.push(code_start..stripped.len());
|
||||
} else {
|
||||
code_start = stripped.len();
|
||||
}
|
||||
in_code = !in_code;
|
||||
} else {
|
||||
stripped.push(ch);
|
||||
}
|
||||
}
|
||||
|
||||
if code_ranges.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((SharedString::from(stripped), code_ranges))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_backtick_spans_no_backticks() {
|
||||
assert_eq!(parse_backtick_spans("plain text"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_backtick_spans_single_span() {
|
||||
let (text, ranges) = parse_backtick_spans("use `zed` to open").unwrap();
|
||||
assert_eq!(text.as_ref(), "use zed to open");
|
||||
assert_eq!(ranges, vec![4..7]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_backtick_spans_multiple_spans() {
|
||||
let (text, ranges) = parse_backtick_spans("flags `-e` or `-n`").unwrap();
|
||||
assert_eq!(text.as_ref(), "flags -e or -n");
|
||||
assert_eq!(ranges, vec![6..8, 12..14]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_backtick_spans_unmatched_backtick() {
|
||||
// A trailing unmatched backtick should not produce a code range
|
||||
assert_eq!(parse_backtick_spans("trailing `backtick"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_backtick_spans_empty_span() {
|
||||
let (text, ranges) = parse_backtick_spans("empty `` span").unwrap();
|
||||
assert_eq!(text.as_ref(), "empty span");
|
||||
assert_eq!(ranges, vec![6..6]);
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Label {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Typography
|
||||
|
||||
@@ -9317,6 +9317,7 @@ pub struct OpenOptions {
|
||||
pub visible: Option<OpenVisible>,
|
||||
pub focus: Option<bool>,
|
||||
pub open_new_workspace: Option<bool>,
|
||||
pub force_existing_window: bool,
|
||||
pub wait: bool,
|
||||
pub requesting_window: Option<WindowHandle<MultiWorkspace>>,
|
||||
pub open_mode: OpenMode,
|
||||
@@ -9501,31 +9502,42 @@ pub fn open_paths(
|
||||
}
|
||||
|
||||
// Fallback for directories: when no flag is specified and no existing
|
||||
// workspace matched, add the directory as a new workspace in the
|
||||
// active window's MultiWorkspace (instead of opening a new window).
|
||||
// workspace matched, check the user's setting to decide whether to add
|
||||
// the directory as a new workspace in the active window's MultiWorkspace
|
||||
// or open a new window.
|
||||
if open_options.open_new_workspace.is_none() && existing.is_none() {
|
||||
let target_window = cx.update(|cx| {
|
||||
let windows = workspace_windows_for_location(
|
||||
&SerializedWorkspaceLocation::Local,
|
||||
cx,
|
||||
);
|
||||
let window = cx
|
||||
.active_window()
|
||||
.and_then(|window| window.downcast::<MultiWorkspace>())
|
||||
.filter(|window| windows.contains(window))
|
||||
.or_else(|| windows.into_iter().next());
|
||||
window.filter(|window| {
|
||||
window.read(cx).is_ok_and(|mw| mw.multi_workspace_enabled(cx))
|
||||
})
|
||||
});
|
||||
let use_existing_window = open_options.force_existing_window
|
||||
|| cx.update(|cx| {
|
||||
WorkspaceSettings::get_global(cx).cli_default_open_behavior
|
||||
== settings::CliDefaultOpenBehavior::ExistingWindow
|
||||
});
|
||||
|
||||
if let Some(window) = target_window {
|
||||
open_options.requesting_window = Some(window);
|
||||
window
|
||||
.update(cx, |multi_workspace, _, cx| {
|
||||
multi_workspace.open_sidebar(cx);
|
||||
if use_existing_window {
|
||||
let target_window = cx.update(|cx| {
|
||||
let windows = workspace_windows_for_location(
|
||||
&SerializedWorkspaceLocation::Local,
|
||||
cx,
|
||||
);
|
||||
let window = cx
|
||||
.active_window()
|
||||
.and_then(|window| window.downcast::<MultiWorkspace>())
|
||||
.filter(|window| windows.contains(window))
|
||||
.or_else(|| windows.into_iter().next());
|
||||
window.filter(|window| {
|
||||
window
|
||||
.read(cx)
|
||||
.is_ok_and(|mw| mw.multi_workspace_enabled(cx))
|
||||
})
|
||||
.log_err();
|
||||
});
|
||||
|
||||
if let Some(window) = target_window {
|
||||
open_options.requesting_window = Some(window);
|
||||
window
|
||||
.update(cx, |multi_workspace, _, cx| {
|
||||
multi_workspace.open_sidebar(cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ pub struct WorkspaceSettings {
|
||||
pub show_call_status_icon: bool,
|
||||
pub autosave: AutosaveSetting,
|
||||
pub restore_on_startup: settings::RestoreOnStartupBehavior,
|
||||
pub cli_default_open_behavior: settings::CliDefaultOpenBehavior,
|
||||
pub restore_on_file_reopen: bool,
|
||||
pub drop_target_size: f32,
|
||||
pub use_system_path_prompts: bool,
|
||||
@@ -99,6 +100,7 @@ impl Settings for WorkspaceSettings {
|
||||
show_call_status_icon: workspace.show_call_status_icon.unwrap(),
|
||||
autosave: workspace.autosave.unwrap(),
|
||||
restore_on_startup: workspace.restore_on_startup.unwrap(),
|
||||
cli_default_open_behavior: workspace.cli_default_open_behavior.unwrap(),
|
||||
restore_on_file_reopen: workspace.restore_on_file_reopen.unwrap(),
|
||||
drop_target_size: workspace.drop_target_size.unwrap(),
|
||||
use_system_path_prompts: workspace.use_system_path_prompts.unwrap(),
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::handle_open_request;
|
||||
use crate::restore_or_create_workspace;
|
||||
use agent_ui::ExternalSourcePrompt;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use cli::{CliRequest, CliResponse, ipc::IpcSender};
|
||||
use cli::{CliRequest, CliResponse, CliResponseSink};
|
||||
use cli::{IpcHandshake, ipc};
|
||||
use client::{ZedLink, parse_zed_link};
|
||||
use db::kvp::KeyValueStore;
|
||||
@@ -12,7 +12,7 @@ use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use futures::future;
|
||||
|
||||
use futures::{FutureExt, SinkExt, StreamExt};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use git_ui::{file_diff_view::FileDiffView, multi_diff_view::MultiDiffView};
|
||||
use gpui::{App, AsyncApp, Global, WindowHandle};
|
||||
use onboarding::FIRST_OPEN;
|
||||
@@ -26,6 +26,7 @@ use std::thread;
|
||||
use std::time::Duration;
|
||||
use ui::SharedString;
|
||||
use util::ResultExt;
|
||||
use util::debug_panic;
|
||||
use util::paths::PathWithPosition;
|
||||
use workspace::PathList;
|
||||
use workspace::item::ItemHandle;
|
||||
@@ -43,9 +44,13 @@ pub struct OpenRequest {
|
||||
pub remote_connection: Option<RemoteConnectionOptions>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum OpenRequestKind {
|
||||
CliConnection((mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)),
|
||||
CliConnection(
|
||||
(
|
||||
mpsc::UnboundedReceiver<CliRequest>,
|
||||
Box<dyn CliResponseSink>,
|
||||
),
|
||||
),
|
||||
Extension {
|
||||
extension_id: String,
|
||||
},
|
||||
@@ -73,6 +78,45 @@ pub enum OpenRequestKind {
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for OpenRequestKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::CliConnection(_) => write!(f, "CliConnection(..)"),
|
||||
Self::Extension { extension_id } => f
|
||||
.debug_struct("Extension")
|
||||
.field("extension_id", extension_id)
|
||||
.finish(),
|
||||
Self::AgentPanel {
|
||||
external_source_prompt,
|
||||
} => f
|
||||
.debug_struct("AgentPanel")
|
||||
.field("external_source_prompt", external_source_prompt)
|
||||
.finish(),
|
||||
Self::SharedAgentThread { session_id } => f
|
||||
.debug_struct("SharedAgentThread")
|
||||
.field("session_id", session_id)
|
||||
.finish(),
|
||||
Self::DockMenuAction { index } => f
|
||||
.debug_struct("DockMenuAction")
|
||||
.field("index", index)
|
||||
.finish(),
|
||||
Self::BuiltinJsonSchema { schema_path } => f
|
||||
.debug_struct("BuiltinJsonSchema")
|
||||
.field("schema_path", schema_path)
|
||||
.finish(),
|
||||
Self::Setting { setting_path } => f
|
||||
.debug_struct("Setting")
|
||||
.field("setting_path", setting_path)
|
||||
.finish(),
|
||||
Self::GitClone { repo_url } => f
|
||||
.debug_struct("GitClone")
|
||||
.field("repo_url", repo_url)
|
||||
.finish(),
|
||||
Self::GitCommit { sha } => f.debug_struct("GitCommit").field("sha", sha).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenRequest {
|
||||
pub fn parse(request: RawOpenRequest, cx: &App) -> Result<Self> {
|
||||
let mut this = Self::default();
|
||||
@@ -305,8 +349,11 @@ pub fn listen_for_cli_connections(opener: OpenListener) -> Result<()> {
|
||||
|
||||
fn connect_to_cli(
|
||||
server_name: &str,
|
||||
) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
|
||||
let handshake_tx = cli::ipc::IpcSender::<IpcHandshake>::connect(server_name.to_string())
|
||||
) -> Result<(
|
||||
mpsc::UnboundedReceiver<CliRequest>,
|
||||
Box<dyn CliResponseSink>,
|
||||
)> {
|
||||
let handshake_tx = ipc::IpcSender::<IpcHandshake>::connect(server_name.to_string())
|
||||
.context("error connecting to cli")?;
|
||||
let (request_tx, request_rx) = ipc::channel::<CliRequest>()?;
|
||||
let (response_tx, response_rx) = ipc::channel::<CliResponse>()?;
|
||||
@@ -318,18 +365,17 @@ fn connect_to_cli(
|
||||
})
|
||||
.context("error sending ipc handshake")?;
|
||||
|
||||
let (mut async_request_tx, async_request_rx) =
|
||||
futures::channel::mpsc::channel::<CliRequest>(16);
|
||||
let (async_request_tx, async_request_rx) = futures::channel::mpsc::unbounded::<CliRequest>();
|
||||
thread::spawn(move || {
|
||||
while let Ok(cli_request) = request_rx.recv() {
|
||||
if smol::block_on(async_request_tx.send(cli_request)).is_err() {
|
||||
if async_request_tx.unbounded_send(cli_request).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
Ok((async_request_rx, response_tx))
|
||||
Ok((async_request_rx, Box::new(response_tx)))
|
||||
}
|
||||
|
||||
pub async fn open_paths_with_positions(
|
||||
@@ -399,7 +445,10 @@ pub async fn open_paths_with_positions(
|
||||
}
|
||||
|
||||
pub async fn handle_cli_connection(
|
||||
(mut requests, responses): (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
|
||||
(mut requests, responses): (
|
||||
mpsc::UnboundedReceiver<CliRequest>,
|
||||
Box<dyn CliResponseSink>,
|
||||
),
|
||||
app_state: Arc<AppState>,
|
||||
cx: &mut AsyncApp,
|
||||
) {
|
||||
@@ -412,7 +461,8 @@ pub async fn handle_cli_connection(
|
||||
diff_all,
|
||||
wait,
|
||||
wsl,
|
||||
open_new_workspace,
|
||||
mut open_new_workspace,
|
||||
mut force_existing_window,
|
||||
reuse,
|
||||
env,
|
||||
user_data_dir: _,
|
||||
@@ -447,13 +497,36 @@ pub async fn handle_cli_connection(
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(behavior) = maybe_prompt_open_behavior(
|
||||
open_new_workspace,
|
||||
force_existing_window,
|
||||
reuse,
|
||||
&paths,
|
||||
&app_state,
|
||||
responses.as_ref(),
|
||||
&mut requests,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
{
|
||||
match behavior {
|
||||
settings::CliDefaultOpenBehavior::ExistingWindow => {
|
||||
force_existing_window = true;
|
||||
}
|
||||
settings::CliDefaultOpenBehavior::NewWindow => {
|
||||
open_new_workspace = Some(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let open_workspace_result = open_workspaces(
|
||||
paths,
|
||||
diff_paths,
|
||||
diff_all,
|
||||
open_new_workspace,
|
||||
force_existing_window,
|
||||
reuse,
|
||||
&responses,
|
||||
responses.as_ref(),
|
||||
wait,
|
||||
dev_container,
|
||||
app_state.clone(),
|
||||
@@ -465,17 +538,127 @@ pub async fn handle_cli_connection(
|
||||
let status = if open_workspace_result.is_err() { 1 } else { 0 };
|
||||
responses.send(CliResponse::Exit { status }).log_err();
|
||||
}
|
||||
CliRequest::SetOpenBehavior { .. } => {
|
||||
// We handle this case in a situation-specific way in
|
||||
// maybe_prompt_open_behavior
|
||||
debug_panic!("unexpected SetOpenBehavior message");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks whether the CLI user should be prompted to configure their default
|
||||
/// open behavior. Sends `CliResponse::PromptOpenBehavior` and waits for the
|
||||
/// CLI's response if all of these are true:
|
||||
/// - No explicit flag was given (`-n`, `-e`, `-a`)
|
||||
/// - There is at least one existing Zed window
|
||||
/// - The user has not yet configured `cli_default_open_behavior` in settings
|
||||
///
|
||||
/// Returns the user's choice, or `None` if no prompt was needed or the CLI
|
||||
/// didn't respond.
|
||||
async fn maybe_prompt_open_behavior(
|
||||
open_new_workspace: Option<bool>,
|
||||
force_existing_window: bool,
|
||||
reuse: bool,
|
||||
paths: &[String],
|
||||
app_state: &Arc<AppState>,
|
||||
responses: &dyn CliResponseSink,
|
||||
requests: &mut mpsc::UnboundedReceiver<CliRequest>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Option<settings::CliDefaultOpenBehavior> {
|
||||
if open_new_workspace.is_some() || force_existing_window || reuse {
|
||||
return None;
|
||||
}
|
||||
|
||||
let has_existing_windows = cx.update(|cx| {
|
||||
cx.windows()
|
||||
.iter()
|
||||
.any(|window| window.downcast::<MultiWorkspace>().is_some())
|
||||
});
|
||||
|
||||
if !has_existing_windows {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !paths.is_empty() {
|
||||
let paths_as_pathbufs: Vec<PathBuf> = paths.iter().map(PathBuf::from).collect();
|
||||
let paths_in_existing_workspace = cx.update(|cx| {
|
||||
for window in cx.windows() {
|
||||
if let Some(multi_workspace) = window.downcast::<MultiWorkspace>() {
|
||||
if let Ok(multi_workspace) = multi_workspace.read(cx) {
|
||||
for workspace in multi_workspace.workspaces() {
|
||||
let project = workspace.read(cx).project().read(cx);
|
||||
if project
|
||||
.visibility_for_paths(&paths_as_pathbufs, false, cx)
|
||||
.is_some()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
});
|
||||
|
||||
if paths_in_existing_workspace {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
if !paths.is_empty() {
|
||||
let has_directory =
|
||||
futures::future::join_all(paths.iter().map(|p| app_state.fs.is_dir(Path::new(p))))
|
||||
.await
|
||||
.into_iter()
|
||||
.any(|is_dir| is_dir);
|
||||
|
||||
if !has_directory {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let settings_text = app_state
|
||||
.fs
|
||||
.load(paths::settings_file())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if settings_text.contains("cli_default_open_behavior") {
|
||||
return None;
|
||||
}
|
||||
|
||||
responses.send(CliResponse::PromptOpenBehavior).log_err()?;
|
||||
|
||||
if let Some(CliRequest::SetOpenBehavior { behavior }) = requests.next().await {
|
||||
let behavior = match behavior {
|
||||
cli::CliOpenBehavior::ExistingWindow => {
|
||||
settings::CliDefaultOpenBehavior::ExistingWindow
|
||||
}
|
||||
cli::CliOpenBehavior::NewWindow => settings::CliDefaultOpenBehavior::NewWindow,
|
||||
};
|
||||
|
||||
let fs = app_state.fs.clone();
|
||||
cx.update(|cx| {
|
||||
settings::update_settings_file(fs, cx, move |content, _cx| {
|
||||
content.workspace.cli_default_open_behavior = Some(behavior);
|
||||
});
|
||||
});
|
||||
|
||||
return Some(behavior);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
async fn open_workspaces(
|
||||
paths: Vec<String>,
|
||||
diff_paths: Vec<[String; 2]>,
|
||||
diff_all: bool,
|
||||
open_new_workspace: Option<bool>,
|
||||
force_existing_window: bool,
|
||||
reuse: bool,
|
||||
responses: &IpcSender<CliResponse>,
|
||||
responses: &dyn CliResponseSink,
|
||||
wait: bool,
|
||||
dev_container: bool,
|
||||
app_state: Arc<AppState>,
|
||||
@@ -536,6 +719,7 @@ async fn open_workspaces(
|
||||
};
|
||||
let open_options = workspace::OpenOptions {
|
||||
open_new_workspace,
|
||||
force_existing_window,
|
||||
requesting_window: replace_window,
|
||||
wait,
|
||||
env: env.clone(),
|
||||
@@ -600,7 +784,7 @@ async fn open_local_workspace(
|
||||
diff_paths: Vec<[String; 2]>,
|
||||
diff_all: bool,
|
||||
open_options: workspace::OpenOptions,
|
||||
responses: &IpcSender<CliResponse>,
|
||||
responses: &dyn CliResponseSink,
|
||||
app_state: &Arc<AppState>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> bool {
|
||||
@@ -742,10 +926,7 @@ pub async fn derive_paths_with_position(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::zed::{open_listener::open_local_workspace, tests::init_test};
|
||||
use cli::{
|
||||
CliResponse,
|
||||
ipc::{self},
|
||||
};
|
||||
use cli::CliResponse;
|
||||
use editor::Editor;
|
||||
use futures::poll;
|
||||
use gpui::{AppContext as _, TestAppContext};
|
||||
@@ -757,6 +938,24 @@ mod tests {
|
||||
use util::path;
|
||||
use workspace::{AppState, MultiWorkspace};
|
||||
|
||||
struct DiscardResponseSink;
|
||||
|
||||
impl CliResponseSink for DiscardResponseSink {
|
||||
fn send(&self, _response: CliResponse) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct SyncResponseSender(std::sync::mpsc::Sender<CliResponse>);
|
||||
|
||||
impl CliResponseSink for SyncResponseSender {
|
||||
fn send(&self, response: CliResponse) -> anyhow::Result<()> {
|
||||
self.0
|
||||
.send(response)
|
||||
.map_err(|error| anyhow::anyhow!("{error}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_parse_ssh_url(cx: &mut TestAppContext) {
|
||||
let _app_state = init_test(cx);
|
||||
@@ -1072,7 +1271,7 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
|
||||
let (response_tx, _) = ipc::channel::<CliResponse>().unwrap();
|
||||
let response_sink = DiscardResponseSink;
|
||||
let workspace_paths = vec![path!("/root/dir1").to_owned()];
|
||||
|
||||
let (done_tx, mut done_rx) = futures::channel::oneshot::channel();
|
||||
@@ -1087,7 +1286,7 @@ mod tests {
|
||||
wait: true,
|
||||
..Default::default()
|
||||
},
|
||||
&response_tx,
|
||||
&response_sink,
|
||||
&app_state,
|
||||
&mut cx,
|
||||
)
|
||||
@@ -1171,7 +1370,7 @@ mod tests {
|
||||
app_state: Arc<AppState>,
|
||||
cx: &TestAppContext,
|
||||
) {
|
||||
let (response_tx, _) = ipc::channel::<CliResponse>().unwrap();
|
||||
let response_sink = DiscardResponseSink;
|
||||
|
||||
let workspace_paths = vec![path.to_owned()];
|
||||
|
||||
@@ -1185,7 +1384,7 @@ mod tests {
|
||||
open_new_workspace,
|
||||
..Default::default()
|
||||
},
|
||||
&response_tx,
|
||||
&response_sink,
|
||||
&app_state,
|
||||
&mut cx,
|
||||
)
|
||||
@@ -1243,20 +1442,19 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
// First, open a workspace normally
|
||||
let (response_tx, _response_rx) = ipc::channel::<CliResponse>().unwrap();
|
||||
let response_sink = DiscardResponseSink;
|
||||
let workspace_paths = vec![file1_path.to_string()];
|
||||
|
||||
let _errored = cx
|
||||
.spawn({
|
||||
let app_state = app_state.clone();
|
||||
let response_tx = response_tx.clone();
|
||||
|mut cx| async move {
|
||||
open_local_workspace(
|
||||
workspace_paths,
|
||||
vec![],
|
||||
false,
|
||||
workspace::OpenOptions::default(),
|
||||
&response_tx,
|
||||
&response_sink,
|
||||
&app_state,
|
||||
&mut cx,
|
||||
)
|
||||
@@ -1282,8 +1480,8 @@ mod tests {
|
||||
let errored_reuse = cx
|
||||
.spawn({
|
||||
let app_state = app_state.clone();
|
||||
let response_tx = response_tx.clone();
|
||||
|mut cx| async move {
|
||||
let response_sink = DiscardResponseSink;
|
||||
open_local_workspace(
|
||||
workspace_paths_reuse,
|
||||
vec![],
|
||||
@@ -1292,7 +1490,7 @@ mod tests {
|
||||
requesting_window: Some(window_to_replace),
|
||||
..Default::default()
|
||||
},
|
||||
&response_tx,
|
||||
&response_sink,
|
||||
&app_state,
|
||||
&mut cx,
|
||||
)
|
||||
@@ -1426,21 +1624,19 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (response_tx, _response_rx) = ipc::channel::<CliResponse>().unwrap();
|
||||
|
||||
// Open first workspace
|
||||
let workspace_paths_1 = vec![file1_path.to_string()];
|
||||
let _errored = cx
|
||||
.spawn({
|
||||
let app_state = app_state.clone();
|
||||
let response_tx = response_tx.clone();
|
||||
|mut cx| async move {
|
||||
let response_sink = DiscardResponseSink;
|
||||
open_local_workspace(
|
||||
workspace_paths_1,
|
||||
Vec::new(),
|
||||
false,
|
||||
workspace::OpenOptions::default(),
|
||||
&response_tx,
|
||||
&response_sink,
|
||||
&app_state,
|
||||
&mut cx,
|
||||
)
|
||||
@@ -1457,8 +1653,8 @@ mod tests {
|
||||
let _errored = cx
|
||||
.spawn({
|
||||
let app_state = app_state.clone();
|
||||
let response_tx = response_tx.clone();
|
||||
|mut cx| async move {
|
||||
let response_sink = DiscardResponseSink;
|
||||
open_local_workspace(
|
||||
workspace_paths_2,
|
||||
Vec::new(),
|
||||
@@ -1467,7 +1663,7 @@ mod tests {
|
||||
open_new_workspace: Some(true), // Force new window
|
||||
..Default::default()
|
||||
},
|
||||
&response_tx,
|
||||
&response_sink,
|
||||
&app_state,
|
||||
&mut cx,
|
||||
)
|
||||
@@ -1503,8 +1699,8 @@ mod tests {
|
||||
let _errored = cx
|
||||
.spawn({
|
||||
let app_state = app_state.clone();
|
||||
let response_tx = response_tx.clone();
|
||||
|mut cx| async move {
|
||||
let response_sink = DiscardResponseSink;
|
||||
open_local_workspace(
|
||||
workspace_paths_add,
|
||||
Vec::new(),
|
||||
@@ -1513,7 +1709,7 @@ mod tests {
|
||||
open_new_workspace: Some(false), // --add flag
|
||||
..Default::default()
|
||||
},
|
||||
&response_tx,
|
||||
&response_sink,
|
||||
&app_state,
|
||||
&mut cx,
|
||||
)
|
||||
@@ -1564,11 +1760,11 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
|
||||
let (response_tx, _) = ipc::channel::<CliResponse>().unwrap();
|
||||
let errored = cx
|
||||
.spawn({
|
||||
let app_state = app_state.clone();
|
||||
|mut cx| async move {
|
||||
let response_sink = DiscardResponseSink;
|
||||
open_local_workspace(
|
||||
vec![path!("/project").to_owned()],
|
||||
vec![],
|
||||
@@ -1577,7 +1773,7 @@ mod tests {
|
||||
open_in_dev_container: true,
|
||||
..Default::default()
|
||||
},
|
||||
&response_tx,
|
||||
&response_sink,
|
||||
&app_state,
|
||||
&mut cx,
|
||||
)
|
||||
@@ -1618,11 +1814,11 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
|
||||
let (response_tx, _) = ipc::channel::<CliResponse>().unwrap();
|
||||
let errored = cx
|
||||
.spawn({
|
||||
let app_state = app_state.clone();
|
||||
|mut cx| async move {
|
||||
let response_sink = DiscardResponseSink;
|
||||
open_local_workspace(
|
||||
vec![path!("/project").to_owned()],
|
||||
vec![],
|
||||
@@ -1631,7 +1827,7 @@ mod tests {
|
||||
open_in_dev_container: true,
|
||||
..Default::default()
|
||||
},
|
||||
&response_tx,
|
||||
&response_sink,
|
||||
&app_state,
|
||||
&mut cx,
|
||||
)
|
||||
@@ -1661,4 +1857,341 @@ mod tests {
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn make_cli_open_request(
|
||||
paths: Vec<String>,
|
||||
open_new_workspace: Option<bool>,
|
||||
force_existing_window: bool,
|
||||
) -> CliRequest {
|
||||
CliRequest::Open {
|
||||
paths,
|
||||
urls: vec![],
|
||||
diff_paths: vec![],
|
||||
diff_all: false,
|
||||
wsl: None,
|
||||
wait: false,
|
||||
open_new_workspace,
|
||||
force_existing_window,
|
||||
reuse: false,
|
||||
env: None,
|
||||
user_data_dir: None,
|
||||
dev_container: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the real [`cli::run_cli_response_loop`] on an OS thread against
|
||||
/// the Zed-side `handle_cli_connection` on the GPUI foreground executor,
|
||||
/// using `allow_parking` so the test scheduler tolerates cross-thread
|
||||
/// wakeups.
|
||||
///
|
||||
/// Returns `(exit_status, prompt_was_shown)`.
|
||||
fn run_cli_with_zed_handler(
|
||||
cx: &mut TestAppContext,
|
||||
app_state: Arc<AppState>,
|
||||
open_request: CliRequest,
|
||||
prompt_response: Option<cli::CliOpenBehavior>,
|
||||
) -> (i32, bool) {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let (request_tx, request_rx) = mpsc::unbounded::<CliRequest>();
|
||||
let (response_tx, response_rx) = std::sync::mpsc::channel::<CliResponse>();
|
||||
let response_sink: Box<dyn CliResponseSink> = Box::new(SyncResponseSender(response_tx));
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
handle_cli_connection((request_rx, response_sink), app_state, &mut cx).await;
|
||||
})
|
||||
.detach();
|
||||
|
||||
let prompt_called = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let prompt_called_for_thread = prompt_called.clone();
|
||||
|
||||
let cli_thread = std::thread::spawn(move || -> anyhow::Result<i32> {
|
||||
request_tx
|
||||
.unbounded_send(open_request)
|
||||
.map_err(|error| anyhow::anyhow!("{error}"))?;
|
||||
|
||||
while let Ok(response) = response_rx.recv() {
|
||||
match response {
|
||||
CliResponse::Ping => {}
|
||||
CliResponse::Stdout { .. } | CliResponse::Stderr { .. } => {}
|
||||
CliResponse::Exit { status } => return Ok(status),
|
||||
CliResponse::PromptOpenBehavior => {
|
||||
prompt_called_for_thread.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
let behavior =
|
||||
prompt_response.unwrap_or(cli::CliOpenBehavior::ExistingWindow);
|
||||
request_tx
|
||||
.unbounded_send(CliRequest::SetOpenBehavior { behavior })
|
||||
.map_err(|error| anyhow::anyhow!("{error}"))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::bail!("CLI response channel closed without Exit")
|
||||
});
|
||||
|
||||
while !cli_thread.is_finished() {
|
||||
cx.run_until_parked();
|
||||
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||
}
|
||||
|
||||
let exit_status = cli_thread.join().unwrap().expect("CLI loop failed");
|
||||
let prompt_shown = prompt_called.load(std::sync::atomic::Ordering::SeqCst);
|
||||
|
||||
// Flush any remaining async work (e.g. settings file writes).
|
||||
cx.run_until_parked();
|
||||
|
||||
(exit_status, prompt_shown)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_e2e_no_flags_no_windows_no_prompt(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(path!("/project"), json!({ "file.txt": "content" }))
|
||||
.await;
|
||||
|
||||
assert_eq!(cx.windows().len(), 0);
|
||||
|
||||
let (status, prompt_shown) = run_cli_with_zed_handler(
|
||||
cx,
|
||||
app_state,
|
||||
make_cli_open_request(vec![path!("/project/file.txt").to_string()], None, false),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(status, 0);
|
||||
assert!(
|
||||
!prompt_shown,
|
||||
"no prompt should be shown when no windows exist"
|
||||
);
|
||||
assert_eq!(cx.windows().len(), 1);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_e2e_prompt_user_picks_existing_window(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(path!("/project_a"), json!({ "file.txt": "content" }))
|
||||
.await;
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(path!("/project_b"), json!({ "file.txt": "content" }))
|
||||
.await;
|
||||
|
||||
// Create an existing window so the prompt triggers
|
||||
open_workspace_file(path!("/project_a"), None, app_state.clone(), cx).await;
|
||||
assert_eq!(cx.windows().len(), 1);
|
||||
|
||||
let (status, prompt_shown) = run_cli_with_zed_handler(
|
||||
cx,
|
||||
app_state.clone(),
|
||||
make_cli_open_request(vec![path!("/project_b").to_string()], None, false),
|
||||
Some(cli::CliOpenBehavior::ExistingWindow),
|
||||
);
|
||||
|
||||
assert_eq!(status, 0);
|
||||
assert!(prompt_shown, "prompt should be shown");
|
||||
assert_eq!(cx.windows().len(), 1);
|
||||
|
||||
let settings_text = app_state
|
||||
.fs
|
||||
.load(paths::settings_file())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
assert!(
|
||||
settings_text.contains("existing_window"),
|
||||
"settings should contain 'existing_window', got: {settings_text}"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_e2e_prompt_user_picks_new_window(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(path!("/project_a"), json!({ "file.txt": "content" }))
|
||||
.await;
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(path!("/project_b"), json!({ "file.txt": "content" }))
|
||||
.await;
|
||||
|
||||
// Create an existing window with project_a
|
||||
open_workspace_file(path!("/project_a"), None, app_state.clone(), cx).await;
|
||||
assert_eq!(cx.windows().len(), 1);
|
||||
|
||||
let (status, prompt_shown) = run_cli_with_zed_handler(
|
||||
cx,
|
||||
app_state.clone(),
|
||||
make_cli_open_request(vec![path!("/project_b").to_string()], None, false),
|
||||
Some(cli::CliOpenBehavior::NewWindow),
|
||||
);
|
||||
|
||||
assert_eq!(status, 0);
|
||||
assert!(prompt_shown, "prompt should be shown");
|
||||
assert_eq!(cx.windows().len(), 2);
|
||||
|
||||
let settings_text = app_state
|
||||
.fs
|
||||
.load(paths::settings_file())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
assert!(
|
||||
settings_text.contains("new_window"),
|
||||
"settings should contain 'new_window', got: {settings_text}"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_e2e_setting_already_configured_no_prompt(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(path!("/project"), json!({ "file.txt": "content" }))
|
||||
.await;
|
||||
|
||||
// Pre-configure the setting in settings.json
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
paths::config_dir(),
|
||||
json!({
|
||||
"settings.json": r#"{"cli_default_open_behavior": "existing_window"}"#
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Create an existing window
|
||||
open_workspace_file(path!("/project"), None, app_state.clone(), cx).await;
|
||||
assert_eq!(cx.windows().len(), 1);
|
||||
|
||||
let (status, prompt_shown) = run_cli_with_zed_handler(
|
||||
cx,
|
||||
app_state,
|
||||
make_cli_open_request(vec![path!("/project/file.txt").to_string()], None, false),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(status, 0);
|
||||
assert!(
|
||||
!prompt_shown,
|
||||
"no prompt should be shown when setting already configured"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_e2e_explicit_existing_flag_no_prompt(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(path!("/project"), json!({ "file.txt": "content" }))
|
||||
.await;
|
||||
|
||||
// Create an existing window
|
||||
open_workspace_file(path!("/project"), None, app_state.clone(), cx).await;
|
||||
assert_eq!(cx.windows().len(), 1);
|
||||
|
||||
let (status, prompt_shown) = run_cli_with_zed_handler(
|
||||
cx,
|
||||
app_state,
|
||||
make_cli_open_request(
|
||||
vec![path!("/project/file.txt").to_string()],
|
||||
None,
|
||||
true, // -e flag: force existing window
|
||||
),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(status, 0);
|
||||
assert!(!prompt_shown, "no prompt should be shown with -e flag");
|
||||
assert_eq!(cx.windows().len(), 1);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_e2e_explicit_new_flag_no_prompt(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(path!("/project_a"), json!({ "file.txt": "content" }))
|
||||
.await;
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(path!("/project_b"), json!({ "file.txt": "content" }))
|
||||
.await;
|
||||
|
||||
// Create an existing window
|
||||
open_workspace_file(path!("/project_a"), None, app_state.clone(), cx).await;
|
||||
assert_eq!(cx.windows().len(), 1);
|
||||
|
||||
let (status, prompt_shown) = run_cli_with_zed_handler(
|
||||
cx,
|
||||
app_state,
|
||||
make_cli_open_request(
|
||||
vec![path!("/project_b/file.txt").to_string()],
|
||||
Some(true), // -n flag: force new window
|
||||
false,
|
||||
),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(status, 0);
|
||||
assert!(!prompt_shown, "no prompt should be shown with -n flag");
|
||||
assert_eq!(cx.windows().len(), 2);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_e2e_paths_in_existing_workspace_no_prompt(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
"src": {
|
||||
"main.rs": "fn main() {}",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Open the project directory as a workspace
|
||||
open_workspace_file(path!("/project"), None, app_state.clone(), cx).await;
|
||||
assert_eq!(cx.windows().len(), 1);
|
||||
|
||||
// Opening a file inside the already-open workspace should not prompt
|
||||
let (status, prompt_shown) = run_cli_with_zed_handler(
|
||||
cx,
|
||||
app_state,
|
||||
make_cli_open_request(vec![path!("/project/src/main.rs").to_string()], None, false),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(status, 0);
|
||||
assert!(
|
||||
!prompt_shown,
|
||||
"no prompt should be shown when paths are in an existing workspace"
|
||||
);
|
||||
// File opened in existing window
|
||||
assert_eq!(cx.windows().len(), 1);
|
||||
}
|
||||
}
|
||||
@@ -159,6 +159,7 @@ fn send_args_to_instance(args: &Args) -> anyhow::Result<()> {
|
||||
wait: false,
|
||||
wsl: args.wsl.clone(),
|
||||
open_new_workspace: None,
|
||||
force_existing_window: false,
|
||||
reuse: false,
|
||||
env: None,
|
||||
user_data_dir: args.user_data_dir.clone(),
|
||||
@@ -186,6 +187,11 @@ fn send_args_to_instance(args: &Args) -> anyhow::Result<()> {
|
||||
exit_status.lock().replace(status);
|
||||
return Ok(());
|
||||
}
|
||||
CliResponse::PromptOpenBehavior => {
|
||||
tx.send(CliRequest::SetOpenBehavior {
|
||||
behavior: cli::CliOpenBehavior::ExistingWindow,
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
Reference in new issue
Block a user