Compare commits

...
Sign in to create a new pull request.

3 commits

Author SHA1 Message Date
Conrad Irwin
df75aacd49 TEMP 2024-03-12 09:56:59 -06:00
Conrad Irwin
f00a0e15bb TEMP 2024-03-12 08:48:49 -06:00
Conrad Irwin
758e707f3b Add --add/--new to control CLI behaviour
When neither is specified, if you open a directory you get a new
workspace, otherwise files are added to your existing workspace.

With --new files are always opened in a new workspace
With --add directories are always added to an existing workspace

Fixes #9076
Fixes #4861
Fixes #5370
2024-03-11 16:30:43 -06:00
12 changed files with 608 additions and 2702 deletions

2521
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,102 +1,102 @@
[workspace] [workspace]
members = [ members = [
"crates/activity_indicator", # "crates/activity_indicator",
"crates/ai", # "crates/ai",
"crates/assets", # "crates/assets",
"crates/assistant", # "crates/assistant",
"crates/audio", # "crates/audio",
"crates/auto_update", # "crates/auto_update",
"crates/breadcrumbs", # "crates/breadcrumbs",
"crates/call", # "crates/call",
"crates/channel", # "crates/channel",
"crates/cli", # "crates/cli",
"crates/client", # "crates/client",
"crates/clock", # "crates/clock",
"crates/collab", # "crates/collab",
"crates/collab_ui", # "crates/collab_ui",
"crates/collections", # "crates/collections",
"crates/command_palette", # "crates/command_palette",
"crates/command_palette_hooks", # "crates/command_palette_hooks",
"crates/copilot", # "crates/copilot",
"crates/copilot_ui", # "crates/copilot_ui",
"crates/db", # "crates/db",
"crates/diagnostics", # "crates/diagnostics",
"crates/editor", # "crates/editor",
"crates/extension", # "crates/extension",
"crates/extension_api", # "crates/extension_api",
"crates/extensions_ui", # "crates/extensions_ui",
"crates/feature_flags", # "crates/feature_flags",
"crates/feedback", # "crates/feedback",
"crates/file_finder", # "crates/file_finder",
"crates/fs", # "crates/fs",
"crates/fsevent", # "crates/fsevent",
"crates/fuzzy", # "crates/fuzzy",
"crates/git", # "crates/git",
"crates/go_to_line", # "crates/go_to_line",
"crates/gpui", # "crates/gpui",
"crates/gpui_macros", # "crates/gpui_macros",
"crates/install_cli", # "crates/install_cli",
"crates/journal", # "crates/journal",
"crates/language", # "crates/language",
"crates/language_selector", # "crates/language_selector",
"crates/language_tools", # "crates/language_tools",
"crates/languages", # "crates/languages",
"crates/live_kit_client", # "crates/live_kit_client",
"crates/live_kit_server", # "crates/live_kit_server",
"crates/lsp", # "crates/lsp",
"crates/markdown_preview", # "crates/markdown_preview",
"crates/media", # "crates/media",
"crates/menu", # "crates/menu",
"crates/multi_buffer", # "crates/multi_buffer",
"crates/node_runtime", # "crates/node_runtime",
"crates/notifications", # "crates/notifications",
"crates/outline", # "crates/outline",
"crates/picker", # "crates/picker",
"crates/prettier", # "crates/prettier",
"crates/project", # "crates/project",
"crates/project_core", # "crates/project_core",
"crates/project_panel", # "crates/project_panel",
"crates/project_symbols", # "crates/project_symbols",
"crates/quick_action_bar", # "crates/quick_action_bar",
"crates/recent_projects", # "crates/recent_projects",
"crates/refineable", # "crates/refineable",
"crates/refineable/derive_refineable", # "crates/refineable/derive_refineable",
"crates/release_channel", # "crates/release_channel",
"crates/rich_text", # "crates/rich_text",
"crates/rope", # "crates/rope",
"crates/rpc", # "crates/rpc",
"crates/task", # "crates/task",
"crates/tasks_ui", # "crates/tasks_ui",
"crates/search", # "crates/search",
"crates/semantic_index", # "crates/semantic_index",
"crates/settings", # "crates/settings",
"crates/snippet", # "crates/snippet",
"crates/sqlez", # "crates/sqlez",
"crates/sqlez_macros", # "crates/sqlez_macros",
"crates/story", # "crates/story",
"crates/storybook", # "crates/storybook",
"crates/sum_tree", # "crates/sum_tree",
"crates/terminal", # "crates/terminal",
"crates/terminal_view", # "crates/terminal_view",
"crates/text", # "crates/text",
"crates/theme", # "crates/theme",
"crates/theme_importer", # "crates/theme_importer",
"crates/theme_selector", # "crates/theme_selector",
"crates/telemetry_events", # "crates/telemetry_events",
"crates/time_format", # "crates/time_format",
"crates/ui", # "crates/ui",
"crates/util", # "crates/util",
"crates/vcs_menu", # "crates/vcs_menu",
"crates/vim", # "crates/vim",
"crates/welcome", # "crates/welcome",
"crates/workspace", "crates/workspace",
"crates/zed", "crates/zed",
"crates/zed_actions", # "crates/zed_actions",
"extensions/gleam", # "extensions/gleam",
"extensions/uiua", # "extensions/uiua",
"tooling/xtask", # "tooling/xtask",
] ]
default-members = ["crates/zed"] default-members = ["crates/zed"]
resolver = "2" resolver = "2"

View file

@ -9,12 +9,11 @@ pub struct IpcHandshake {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub enum CliRequest { pub enum CliRequest {
// The filed is named `path` for compatibility, but now CLI can request Open {
// opening a path at a certain row and/or column: `some/path:123` and `some/path:123:456`. paths: Vec<String>,
// wait: bool,
// Since Zed CLI has to be installed separately, there can be situations when old CLI is open_new_workspace: Option<bool>,
// querying new Zed editors, support both formats by using `String` here and parsing it on Zed side later. },
Open { paths: Vec<String>, wait: bool },
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]

View file

@ -1,10 +1,11 @@
#![cfg_attr(target_os = "linux", allow(dead_code))] #![cfg_attr(target_os = "linux", allow(dead_code))]
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use clap::Parser; use clap::{Error, ErrorKind, Parser};
use cli::{CliRequest, CliResponse}; use cli::{CliRequest, CliResponse};
use serde::Deserialize; use serde::Deserialize;
use std::{ use std::{
env,
ffi::OsStr, ffi::OsStr,
fs::{self, OpenOptions}, fs::{self, OpenOptions},
io, io,
@ -12,12 +13,18 @@ use std::{
}; };
use util::paths::PathLikeWithPosition; use util::paths::PathLikeWithPosition;
#[derive(Parser)] #[derive(Parser, Debug)]
#[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))] #[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))]
struct Args { struct Args {
/// Wait for all of the given paths to be opened/closed before exiting. /// Wait for all of the given paths to be opened/closed before exiting.
#[clap(short, long)] #[clap(short, long)]
wait: bool, wait: bool,
/// Add files to the currently open workspace
#[clap(short, long, overrides_with = "new")]
add: bool,
/// Create a new workspace
#[clap(short, long, overrides_with = "add")]
new: bool,
/// A sequence of space-separated paths that you want to open. /// A sequence of space-separated paths that you want to open.
/// ///
/// Use `path:line:row` syntax to open a file at a specific location. /// Use `path:line:row` syntax to open a file at a specific location.
@ -56,32 +63,54 @@ fn main() -> Result<()> {
return Ok(()); return Ok(());
} }
for path in args // for path in args
.paths_with_position // .paths_with_position
.iter() // .iter()
.map(|path_with_position| &path_with_position.path_like) // .map(|path_with_position| &path_with_position.path_like)
{ // {
if !path.exists() { // if !path.exists() {
touch(path.as_path())?; // touch(path.as_path())?;
} // }
} // }
let (tx, rx) = bundle.launch()?; let (tx, rx) = bundle.launch()?;
let open_new_workspace = if args.new {
Some(true)
} else if args.add {
Some(false)
} else {
None
};
tx.send(CliRequest::Open { tx.send(dbg!(CliRequest::Open {
paths: args paths: args
.paths_with_position .paths_with_position
.into_iter() .into_iter()
.map(|path_with_position| { .map(|path_with_position| {
let path_with_position = path_with_position.map_path_like(|path| { let path_with_position =
fs::canonicalize(&path) path_with_position.map_path_like(|path| match fs::canonicalize(&path) {
.with_context(|| format!("path {path:?} canonicalization")) Ok(path) => Ok(path),
})?; Err(e) => {
if let Some(mut parent) = path.parent() {
let curdir = env::current_dir()?;
if parent == Path::new("") {
parent = &curdir;
}
match fs::canonicalize(parent) {
Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
Err(_) => Err(e),
}
} else {
Err(e)
}
}
})?;
Ok(path_with_position.to_string(|path| path.display().to_string())) Ok(path_with_position.to_string(|path| path.display().to_string()))
}) })
.collect::<Result<_>>()?, .collect::<Result<_>>()?,
wait: args.wait, wait: args.wait,
})?; open_new_workspace,
}))?;
while let Ok(response) = rx.recv() { while let Ok(response) = rx.recv() {
match response { match response {

View file

@ -500,7 +500,12 @@ impl FakeFsState {
fn read_path(&self, target: &Path) -> Result<Arc<Mutex<FakeFsEntry>>> { fn read_path(&self, target: &Path) -> Result<Arc<Mutex<FakeFsEntry>>> {
Ok(self Ok(self
.try_read_path(target, true) .try_read_path(target, true)
.ok_or_else(|| anyhow!("path does not exist: {}", target.display()))? .ok_or_else(|| {
anyhow!(io::Error::new(
io::ErrorKind::NotFound,
format!("not found: {}", target.display())
))
})?
.0) .0)
} }

View file

@ -102,7 +102,14 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut WindowContext) {
cx.spawn(|mut cx| async move { cx.spawn(|mut cx| async move {
let (journal_dir, entry_path) = create_entry.await?; let (journal_dir, entry_path) = create_entry.await?;
let (workspace, _) = cx let (workspace, _) = cx
.update(|cx| workspace::open_paths(&[journal_dir], app_state, None, cx))? .update(|cx| {
workspace::open_paths(
&[journal_dir],
app_state,
workspace::OpenOptions::default(),
cx,
)
})?
.await?; .await?;
let opened = workspace let opened = workspace

View file

@ -74,7 +74,7 @@ use std::{
env, env,
ffi::OsStr, ffi::OsStr,
hash::Hash, hash::Hash,
mem, io, mem,
num::NonZeroU32, num::NonZeroU32,
ops::Range, ops::Range,
path::{self, Component, Path, PathBuf}, path::{self, Component, Path, PathBuf},
@ -1136,18 +1136,24 @@ impl Project {
.map(|worktree| worktree.read(cx).id()) .map(|worktree| worktree.read(cx).id())
} }
pub fn contains_paths(&self, paths: &[PathBuf], cx: &AppContext) -> bool { pub fn visibility_for_paths(&self, paths: &[PathBuf], cx: &AppContext) -> Option<bool> {
paths.iter().all(|path| self.contains_path(path, cx)) paths
.iter()
.map(|path| self.visibility_for_path(path, cx))
.max()
.flatten()
} }
pub fn contains_path(&self, path: &Path, cx: &AppContext) -> bool { pub fn visibility_for_path(&self, path: &Path, cx: &AppContext) -> Option<bool> {
for worktree in self.worktrees() { self.worktrees()
let worktree = worktree.read(cx).as_local(); .filter_map(|worktree| {
if worktree.map_or(false, |w| w.contains_abs_path(path)) { let worktree = worktree.read(cx);
return true; worktree
} .as_local()?
} .contains_abs_path(path)
false .then(|| worktree.is_visible())
})
.max()
} }
pub fn create_entry( pub fn create_entry(
@ -1822,12 +1828,28 @@ impl Project {
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<Model<Buffer>>> { ) -> Task<Result<Model<Buffer>>> {
let buffer_id = self.next_buffer_id.next(); let buffer_id = self.next_buffer_id.next();
let path2 = path.clone();
let load_buffer = worktree.update(cx, |worktree, cx| { let load_buffer = worktree.update(cx, |worktree, cx| {
let worktree = worktree.as_local_mut().unwrap(); let worktree = worktree.as_local_mut().unwrap();
worktree.load_buffer(buffer_id, path, cx) worktree.load_buffer(buffer_id, path, cx)
}); });
cx.spawn(move |this, mut cx| async move { cx.spawn(move |this, mut cx| async move {
let buffer = load_buffer.await?; let buffer = match load_buffer.await {
Ok(buffer) => Ok(buffer),
Err(error) => {
dbg!(&path2, error.root_cause());
// if let Some(io_error) = error.root_cause().downcast_ref::<io::Error>() {
// if io_error.kind() == io::ErrorKind::NotFound {
let text_buffer = text::Buffer::new(0, buffer_id, "".into());
cx.new_model(|_| Buffer::build(text_buffer, None, None, Capability::ReadWrite))
// } else {
// Err(error)
// }
// } else {
// Err(error)
// }
}
}?;
this.update(&mut cx, |this, cx| this.register_buffer(&buffer, cx))??; this.update(&mut cx, |this, cx| this.register_buffer(&buffer, cx))??;
Ok(buffer) Ok(buffer)
}) })

View file

@ -493,9 +493,16 @@ mod tests {
}), }),
) )
.await; .await;
cx.update(|cx| open_paths(&[PathBuf::from("/dir/main.ts")], app_state, None, cx)) cx.update(|cx| {
.await open_paths(
.unwrap(); &[PathBuf::from("/dir/main.ts")],
app_state,
workspace::OpenOptions::default(),
cx,
)
})
.await
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 1); assert_eq!(cx.update(|cx| cx.windows().len()), 1);
let workspace = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap()); let workspace = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());

View file

@ -262,7 +262,8 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
cx.spawn(move |cx| async move { cx.spawn(move |cx| async move {
if let Some(paths) = paths.await.log_err().flatten() { if let Some(paths) = paths.await.log_err().flatten() {
cx.update(|cx| { cx.update(|cx| {
open_paths(&paths, app_state, None, cx).detach_and_log_err(cx) open_paths(&paths, app_state, OpenOptions::default(), cx)
.detach_and_log_err(cx)
}) })
.ok(); .ok();
} }
@ -794,6 +795,7 @@ impl Workspace {
app_state.fs.clone(), app_state.fs.clone(),
cx, cx,
); );
dbg!("new_local", &abs_paths);
cx.spawn(|mut cx| async move { cx.spawn(|mut cx| async move {
let serialized_workspace: Option<SerializedWorkspace> = let serialized_workspace: Option<SerializedWorkspace> =
@ -813,12 +815,15 @@ impl Workspace {
.await .await
.log_err() .log_err()
{ {
dbg!("got project_path_for_path");
worktree_roots.extend(worktree.update(&mut cx, |tree, _| tree.abs_path()).ok()); worktree_roots.extend(worktree.update(&mut cx, |tree, _| tree.abs_path()).ok());
project_paths.push((path, Some(project_entry))); project_paths.push((path, Some(project_entry)));
} else { } else {
dbg!("NO project_path_for_path");
project_paths.push((path, None)); project_paths.push((path, None));
} }
} }
dbg!(&project_paths);
let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() { let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
serialized_workspace.id serialized_workspace.id
@ -1414,8 +1419,18 @@ impl Workspace {
let app_state = self.app_state.clone(); let app_state = self.app_state.clone();
cx.spawn(|_, mut cx| async move { cx.spawn(|_, mut cx| async move {
cx.update(|cx| open_paths(&paths, app_state, window_to_replace, cx))? cx.update(|cx| {
.await?; open_paths(
&paths,
app_state,
OpenOptions {
replace_window: window_to_replace,
..Default::default()
},
cx,
)
})?
.await?;
Ok(()) Ok(())
}) })
} }
@ -3729,19 +3744,21 @@ fn open_items(
let fs = app_state.fs.clone(); let fs = app_state.fs.clone();
async move { async move {
let file_project_path = project_path?; let file_project_path = project_path?;
if fs.is_file(&abs_path).await { // if fs.metadata(&abs_path).await.is_ok_and(|metadata| {
Some(( // metadata.is_none() || !metadata.unwrap().is_dir
ix, // }) {
workspace Some((
.update(&mut cx, |workspace, cx| { ix,
workspace.open_path(file_project_path, None, true, cx) workspace
}) .update(&mut cx, |workspace, cx| {
.log_err()? workspace.open_path(file_project_path, None, true, cx)
.await, })
)) .log_err()?
} else { .await,
None ))
} // } else {
// None
// }
} }
}) })
}); });
@ -4360,6 +4377,13 @@ pub async fn get_any_active_workspace(
fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> { fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
cx.update(|cx| { cx.update(|cx| {
if let Some(workspace_window) = cx
.active_window()
.and_then(|window| window.downcast::<Workspace>())
{
return Some(workspace_window);
}
for window in cx.windows() { for window in cx.windows() {
if let Some(workspace_window) = window.downcast::<Workspace>() { if let Some(workspace_window) = window.downcast::<Workspace>() {
workspace_window workspace_window
@ -4374,11 +4398,17 @@ fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandl
.flatten() .flatten()
} }
#[derive(Default)]
pub struct OpenOptions {
pub open_new_workspace: Option<bool>,
pub replace_window: Option<WindowHandle<Workspace>>,
}
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
pub fn open_paths( pub fn open_paths(
abs_paths: &[PathBuf], abs_paths: &[PathBuf],
app_state: Arc<AppState>, app_state: Arc<AppState>,
requesting_window: Option<WindowHandle<Workspace>>, open_options: OpenOptions,
cx: &mut AppContext, cx: &mut AppContext,
) -> Task< ) -> Task<
anyhow::Result<( anyhow::Result<(
@ -4387,24 +4417,62 @@ pub fn open_paths(
)>, )>,
> { > {
let abs_paths = abs_paths.to_vec(); let abs_paths = abs_paths.to_vec();
// Open paths in existing workspace if possible let mut existing = None;
let existing = activate_workspace_for_project(cx, { let mut best_match = None;
let abs_paths = abs_paths.clone(); let mut open_visible = OpenVisible::All;
move |project, cx| project.contains_paths(&abs_paths, cx)
}); if open_options.open_new_workspace.unwrap_or(true) {
for window in cx.windows() {
let Some(handle) = window.downcast::<Workspace>() else {
continue;
};
if let Ok(workspace) = handle.read(cx) {
let m = workspace
.project()
.read(cx)
.visibility_for_paths(&abs_paths, cx);
if m > best_match {
existing = Some(handle);
best_match = m;
} else if best_match.is_none() && open_options.open_new_workspace == Some(true) {
existing = Some(handle)
}
}
}
}
cx.spawn(move |mut cx| async move { cx.spawn(move |mut cx| async move {
if open_options.open_new_workspace.is_none() && existing.is_none() {
let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
if futures::future::join_all(all_files)
.await
.into_iter()
.filter_map(|result| result.ok().flatten())
.all(|file| !file.is_dir)
{
existing = activate_any_workspace_window(&mut cx);
open_visible = OpenVisible::None;
}
}
if let Some(existing) = existing { if let Some(existing) = existing {
Ok(( Ok((
existing, existing,
existing existing
.update(&mut cx, |workspace, cx| { .update(&mut cx, |workspace, cx| {
workspace.open_paths(abs_paths, OpenVisible::All, None, cx) cx.activate_window();
workspace.open_paths(abs_paths, open_visible, None, cx)
})? })?
.await, .await,
)) ))
} else { } else {
cx.update(move |cx| { cx.update(move |cx| {
Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx) Workspace::new_local(
abs_paths,
app_state.clone(),
open_options.replace_window,
cx,
)
})? })?
.await .await
} }

View file

@ -264,24 +264,14 @@ fn main() {
cx.set_menus(app_menus()); cx.set_menus(app_menus());
initialize_workspace(app_state.clone(), cx); initialize_workspace(app_state.clone(), cx);
if stdout_is_a_pty() { // todo(linux): unblock this
// todo(linux): unblock this upload_panics_and_crashes(http.clone(), cx);
#[cfg(not(target_os = "linux"))]
upload_panics_and_crashes(http.clone(), cx); cx.activate(true);
cx.activate(true);
let urls = collect_url_args(cx); let urls = collect_url_args(cx);
if !urls.is_empty() { if !urls.is_empty() {
listener.open_urls(urls) listener.open_urls(urls)
}
} else {
upload_panics_and_crashes(http.clone(), cx);
// TODO Development mode that forces the CLI mode usually runs Zed binary as is instead
// of an *app, hence gets no specific callbacks run. Emulate them here, if needed.
if std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_some()
&& !listener.triggered.load(Ordering::Acquire)
{
listener.open_urls(collect_url_args(cx))
}
} }
let mut triggered_authentication = false; let mut triggered_authentication = false;
@ -339,8 +329,13 @@ fn handle_open_request(
if !request.open_paths.is_empty() { if !request.open_paths.is_empty() {
let app_state = app_state.clone(); let app_state = app_state.clone();
task = Some(cx.spawn(|mut cx| async move { task = Some(cx.spawn(|mut cx| async move {
let (_window, results) = let (_window, results) = open_paths_with_positions(
open_paths_with_positions(&request.open_paths, app_state, &mut cx).await?; &request.open_paths,
app_state,
workspace::OpenOptions::default(),
&mut cx,
)
.await?;
for result in results.into_iter().flatten() { for result in results.into_iter().flatten() {
if let Err(err) = result { if let Err(err) = result {
log::error!("Error opening path: {err}",); log::error!("Error opening path: {err}",);
@ -441,9 +436,16 @@ async fn installation_id() -> Result<(String, bool)> {
async fn restore_or_create_workspace(app_state: Arc<AppState>, cx: AsyncAppContext) { async fn restore_or_create_workspace(app_state: Arc<AppState>, cx: AsyncAppContext) {
async_maybe!({ async_maybe!({
if let Some(location) = workspace::last_opened_workspace_paths().await { if let Some(location) = workspace::last_opened_workspace_paths().await {
cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))? cx.update(|cx| {
.await workspace::open_paths(
.log_err(); location.paths().as_ref(),
app_state,
workspace::OpenOptions::default(),
cx,
)
})?
.await
.log_err();
} else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
cx.update(|cx| show_welcome_view(app_state, cx)).log_err(); cx.update(|cx| show_welcome_view(app_state, cx)).log_err();
} else { } else {
@ -901,7 +903,7 @@ fn collect_url_args(cx: &AppContext) -> Vec<String> {
.filter_map(|arg| match std::fs::canonicalize(Path::new(&arg)) { .filter_map(|arg| match std::fs::canonicalize(Path::new(&arg)) {
Ok(path) => Some(format!("file://{}", path.to_string_lossy())), Ok(path) => Some(format!("file://{}", path.to_string_lossy())),
Err(error) => { Err(error) => {
if arg.starts_with("file://") { if arg.starts_with("file://") || arg.starts_with("zed-cli://") {
Some(arg) Some(arg)
} else if let Some(_) = parse_zed_link(&arg, cx) { } else if let Some(_) = parse_zed_link(&arg, cx) {
Some(arg) Some(arg)

View file

@ -11,11 +11,10 @@ use futures::{FutureExt, SinkExt, StreamExt};
use gpui::{AppContext, AsyncAppContext, Global, WindowHandle}; use gpui::{AppContext, AsyncAppContext, Global, WindowHandle};
use language::{Bias, Point}; use language::{Bias, Point};
use std::path::Path; use std::path::Path;
use std::sync::atomic::Ordering; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use std::{path::PathBuf, sync::atomic::AtomicBool};
use util::paths::PathLikeWithPosition; use util::paths::PathLikeWithPosition;
use util::ResultExt; use util::ResultExt;
use workspace::item::ItemHandle; use workspace::item::ItemHandle;
@ -89,7 +88,6 @@ impl OpenRequest {
pub struct OpenListener { pub struct OpenListener {
tx: UnboundedSender<Vec<String>>, tx: UnboundedSender<Vec<String>>,
pub triggered: AtomicBool,
} }
struct GlobalOpenListener(Arc<OpenListener>); struct GlobalOpenListener(Arc<OpenListener>);
@ -107,17 +105,10 @@ impl OpenListener {
pub fn new() -> (Self, UnboundedReceiver<Vec<String>>) { pub fn new() -> (Self, UnboundedReceiver<Vec<String>>) {
let (tx, rx) = mpsc::unbounded(); let (tx, rx) = mpsc::unbounded();
( (OpenListener { tx }, rx)
OpenListener {
tx,
triggered: AtomicBool::new(false),
},
rx,
)
} }
pub fn open_urls(&self, urls: Vec<String>) { pub fn open_urls(&self, urls: Vec<String>) {
self.triggered.store(true, Ordering::Release);
self.tx self.tx
.unbounded_send(urls) .unbounded_send(urls)
.map_err(|_| anyhow!("no listener for open requests")) .map_err(|_| anyhow!("no listener for open requests"))
@ -157,6 +148,7 @@ fn connect_to_cli(
pub async fn open_paths_with_positions( pub async fn open_paths_with_positions(
path_likes: &Vec<PathLikeWithPosition<PathBuf>>, path_likes: &Vec<PathLikeWithPosition<PathBuf>>,
app_state: Arc<AppState>, app_state: Arc<AppState>,
open_options: workspace::OpenOptions,
cx: &mut AsyncAppContext, cx: &mut AsyncAppContext,
) -> Result<( ) -> Result<(
WindowHandle<Workspace>, WindowHandle<Workspace>,
@ -180,7 +172,7 @@ pub async fn open_paths_with_positions(
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let (workspace, items) = cx let (workspace, items) = cx
.update(|cx| workspace::open_paths(&paths, app_state, None, cx))? .update(|cx| workspace::open_paths(&paths, app_state, open_options, cx))?
.await?; .await?;
for (item, path) in items.iter().zip(&paths) { for (item, path) in items.iter().zip(&paths) {
@ -215,22 +207,30 @@ pub async fn handle_cli_connection(
) { ) {
if let Some(request) = requests.next().await { if let Some(request) = requests.next().await {
match request { match request {
CliRequest::Open { paths, wait } => { CliRequest::Open {
paths,
wait,
open_new_workspace,
} => {
let paths = if paths.is_empty() { let paths = if paths.is_empty() {
workspace::last_opened_workspace_paths() if open_new_workspace == Some(true) {
.await vec![]
.map(|location| { } else {
location workspace::last_opened_workspace_paths()
.paths() .await
.iter() .map(|location| {
.map(|path| PathLikeWithPosition { location
path_like: path.clone(), .paths()
row: None, .iter()
column: None, .map(|path| PathLikeWithPosition {
}) path_like: path.clone(),
.collect::<Vec<_>>() row: None,
}) column: None,
.unwrap_or_default() })
.collect::<Vec<_>>()
})
.unwrap_or_default()
}
} else { } else {
paths paths
.into_iter() .into_iter()
@ -250,7 +250,17 @@ pub async fn handle_cli_connection(
let mut errored = false; let mut errored = false;
match open_paths_with_positions(&paths, app_state, &mut cx).await { match open_paths_with_positions(
&paths,
app_state,
workspace::OpenOptions {
open_new_workspace,
..Default::default()
},
&mut cx,
)
.await
{
Ok((workspace, items)) => { Ok((workspace, items)) => {
let mut item_release_futures = Vec::new(); let mut item_release_futures = Vec::new();

View file

@ -877,6 +877,41 @@ mod tests {
WorkspaceHandle, WorkspaceHandle,
}; };
#[gpui::test]
async fn test_open_non_existing_file(cx: &mut TestAppContext) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/root",
json!({
"a": {
},
}),
)
.await;
cx.update(|cx| {
open_paths(
&[PathBuf::from("/root/a/new")],
app_state.clone(),
workspace::OpenOptions::default(),
cx,
)
})
.await
.unwrap();
assert_eq!(cx.read(|cx| cx.windows().len()), 1);
let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
workspace
.update(cx, |workspace, cx| {
assert!(workspace.active_item_as::<Editor>(cx).is_some())
})
.unwrap();
}
#[gpui::test] #[gpui::test]
async fn test_open_paths_action(cx: &mut TestAppContext) { async fn test_open_paths_action(cx: &mut TestAppContext) {
let app_state = init_test(cx); let app_state = init_test(cx);
@ -910,7 +945,7 @@ mod tests {
open_paths( open_paths(
&[PathBuf::from("/root/a"), PathBuf::from("/root/b")], &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
app_state.clone(), app_state.clone(),
None, workspace::OpenOptions::default(),
cx, cx,
) )
}) })
@ -918,9 +953,16 @@ mod tests {
.unwrap(); .unwrap();
assert_eq!(cx.read(|cx| cx.windows().len()), 1); assert_eq!(cx.read(|cx| cx.windows().len()), 1);
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], app_state.clone(), None, cx)) cx.update(|cx| {
.await open_paths(
.unwrap(); &[PathBuf::from("/root/a")],
app_state.clone(),
workspace::OpenOptions::default(),
cx,
)
})
.await
.unwrap();
assert_eq!(cx.read(|cx| cx.windows().len()), 1); assert_eq!(cx.read(|cx| cx.windows().len()), 1);
let workspace_1 = cx let workspace_1 = cx
.read(|cx| cx.windows()[0].downcast::<Workspace>()) .read(|cx| cx.windows()[0].downcast::<Workspace>())
@ -941,7 +983,7 @@ mod tests {
open_paths( open_paths(
&[PathBuf::from("/root/b"), PathBuf::from("/root/c")], &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
app_state.clone(), app_state.clone(),
None, workspace::OpenOptions::default(),
cx, cx,
) )
}) })
@ -957,7 +999,10 @@ mod tests {
open_paths( open_paths(
&[PathBuf::from("/root/c"), PathBuf::from("/root/d")], &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
app_state, app_state,
Some(window), workspace::OpenOptions {
replace_window: Some(window),
..Default::default()
},
cx, cx,
) )
}) })
@ -983,6 +1028,123 @@ mod tests {
.unwrap(); .unwrap();
} }
#[gpui::test]
async fn test_open_add_new(cx: &mut TestAppContext) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree("/root", json!({"a": "hey", "b": "", "dir": {"c": "f"}}))
.await;
cx.update(|cx| {
open_paths(
&[PathBuf::from("/root/dir")],
app_state.clone(),
workspace::OpenOptions::default(),
cx,
)
})
.await
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
cx.update(|cx| {
open_paths(
&[PathBuf::from("/root/a")],
app_state.clone(),
workspace::OpenOptions {
open_new_workspace: Some(false),
..Default::default()
},
cx,
)
})
.await
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
cx.update(|cx| {
open_paths(
&[PathBuf::from("/root/dir/c")],
app_state.clone(),
workspace::OpenOptions {
open_new_workspace: Some(true),
..Default::default()
},
cx,
)
})
.await
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 2);
}
#[gpui::test]
async fn test_open_file_in_many_spaces(cx: &mut TestAppContext) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree("/root", json!({"dir1": {"a": "b"}, "dir2": {"c": "d"}}))
.await;
cx.update(|cx| {
open_paths(
&[PathBuf::from("/root/dir1/a")],
app_state.clone(),
workspace::OpenOptions::default(),
cx,
)
})
.await
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
let window1 = cx.update(|cx| cx.active_window().unwrap());
cx.update(|cx| {
open_paths(
&[PathBuf::from("/root/dir2/c")],
app_state.clone(),
workspace::OpenOptions::default(),
cx,
)
})
.await
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
cx.update(|cx| {
open_paths(
&[PathBuf::from("/root/dir2")],
app_state.clone(),
workspace::OpenOptions::default(),
cx,
)
})
.await
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 2);
let window2 = cx.update(|cx| cx.active_window().unwrap());
assert!(window1 != window2);
cx.update_window(window1, |_, cx| cx.activate_window())
.unwrap();
cx.update(|cx| {
open_paths(
&[PathBuf::from("/root/dir2/c")],
app_state.clone(),
workspace::OpenOptions::default(),
cx,
)
})
.await
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 2);
// should have opened in window2 because that has dir2 visibly open (window1 has it open, but not in the project panel)
assert!(cx.update(|cx| cx.active_window().unwrap()) == window2);
}
#[gpui::test] #[gpui::test]
async fn test_window_edit_state(cx: &mut TestAppContext) { async fn test_window_edit_state(cx: &mut TestAppContext) {
let executor = cx.executor(); let executor = cx.executor();
@ -993,9 +1155,16 @@ mod tests {
.insert_tree("/root", json!({"a": "hey"})) .insert_tree("/root", json!({"a": "hey"}))
.await; .await;
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], app_state.clone(), None, cx)) cx.update(|cx| {
.await open_paths(
.unwrap(); &[PathBuf::from("/root/a")],
app_state.clone(),
workspace::OpenOptions::default(),
cx,
)
})
.await
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 1); assert_eq!(cx.update(|cx| cx.windows().len()), 1);
// When opening the workspace, the window is not in a edited state. // When opening the workspace, the window is not in a edited state.
@ -1060,9 +1229,16 @@ mod tests {
assert!(!window_is_edited(window, cx)); assert!(!window_is_edited(window, cx));
// Opening the buffer again doesn't impact the window's edited state. // Opening the buffer again doesn't impact the window's edited state.
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], app_state, None, cx)) cx.update(|cx| {
.await open_paths(
.unwrap(); &[PathBuf::from("/root/a")],
app_state,
workspace::OpenOptions::default(),
cx,
)
})
.await
.unwrap();
let editor = window let editor = window
.read_with(cx, |workspace, cx| { .read_with(cx, |workspace, cx| {
workspace workspace
@ -1289,9 +1465,16 @@ mod tests {
) )
.await; .await;
cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], app_state, None, cx)) cx.update(|cx| {
.await open_paths(
.unwrap(); &[PathBuf::from("/dir1/")],
app_state,
workspace::OpenOptions::default(),
cx,
)
})
.await
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 1); assert_eq!(cx.update(|cx| cx.windows().len()), 1);
let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap()); let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
let workspace = window.root(cx).unwrap(); let workspace = window.root(cx).unwrap();
@ -1523,7 +1706,14 @@ mod tests {
Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(), Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(),
]; ];
let (opened_workspace, new_items) = cx let (opened_workspace, new_items) = cx
.update(|cx| workspace::open_paths(&paths_to_open, app_state, None, cx)) .update(|cx| {
workspace::open_paths(
&paths_to_open,
app_state,
workspace::OpenOptions::default(),
cx,
)
})
.await .await
.unwrap(); .unwrap();