Compare commits
3 commits
main
...
cli-no-fil
Author | SHA1 | Date | |
---|---|---|---|
![]() |
df75aacd49 | ||
![]() |
f00a0e15bb | ||
![]() |
758e707f3b |
12 changed files with 608 additions and 2702 deletions
2521
Cargo.lock
generated
2521
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
186
Cargo.toml
186
Cargo.toml
|
@ -1,102 +1,102 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/ai",
|
||||
"crates/assets",
|
||||
"crates/assistant",
|
||||
"crates/audio",
|
||||
"crates/auto_update",
|
||||
"crates/breadcrumbs",
|
||||
"crates/call",
|
||||
"crates/channel",
|
||||
"crates/cli",
|
||||
"crates/client",
|
||||
"crates/clock",
|
||||
"crates/collab",
|
||||
"crates/collab_ui",
|
||||
"crates/collections",
|
||||
"crates/command_palette",
|
||||
"crates/command_palette_hooks",
|
||||
"crates/copilot",
|
||||
"crates/copilot_ui",
|
||||
"crates/db",
|
||||
"crates/diagnostics",
|
||||
"crates/editor",
|
||||
"crates/extension",
|
||||
"crates/extension_api",
|
||||
"crates/extensions_ui",
|
||||
"crates/feature_flags",
|
||||
"crates/feedback",
|
||||
"crates/file_finder",
|
||||
"crates/fs",
|
||||
"crates/fsevent",
|
||||
"crates/fuzzy",
|
||||
"crates/git",
|
||||
"crates/go_to_line",
|
||||
"crates/gpui",
|
||||
"crates/gpui_macros",
|
||||
"crates/install_cli",
|
||||
"crates/journal",
|
||||
"crates/language",
|
||||
"crates/language_selector",
|
||||
"crates/language_tools",
|
||||
"crates/languages",
|
||||
"crates/live_kit_client",
|
||||
"crates/live_kit_server",
|
||||
"crates/lsp",
|
||||
"crates/markdown_preview",
|
||||
"crates/media",
|
||||
"crates/menu",
|
||||
"crates/multi_buffer",
|
||||
"crates/node_runtime",
|
||||
"crates/notifications",
|
||||
"crates/outline",
|
||||
"crates/picker",
|
||||
"crates/prettier",
|
||||
"crates/project",
|
||||
"crates/project_core",
|
||||
"crates/project_panel",
|
||||
"crates/project_symbols",
|
||||
"crates/quick_action_bar",
|
||||
"crates/recent_projects",
|
||||
"crates/refineable",
|
||||
"crates/refineable/derive_refineable",
|
||||
"crates/release_channel",
|
||||
"crates/rich_text",
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
"crates/task",
|
||||
"crates/tasks_ui",
|
||||
"crates/search",
|
||||
"crates/semantic_index",
|
||||
"crates/settings",
|
||||
"crates/snippet",
|
||||
"crates/sqlez",
|
||||
"crates/sqlez_macros",
|
||||
"crates/story",
|
||||
"crates/storybook",
|
||||
"crates/sum_tree",
|
||||
"crates/terminal",
|
||||
"crates/terminal_view",
|
||||
"crates/text",
|
||||
"crates/theme",
|
||||
"crates/theme_importer",
|
||||
"crates/theme_selector",
|
||||
"crates/telemetry_events",
|
||||
"crates/time_format",
|
||||
"crates/ui",
|
||||
"crates/util",
|
||||
"crates/vcs_menu",
|
||||
"crates/vim",
|
||||
"crates/welcome",
|
||||
# "crates/activity_indicator",
|
||||
# "crates/ai",
|
||||
# "crates/assets",
|
||||
# "crates/assistant",
|
||||
# "crates/audio",
|
||||
# "crates/auto_update",
|
||||
# "crates/breadcrumbs",
|
||||
# "crates/call",
|
||||
# "crates/channel",
|
||||
# "crates/cli",
|
||||
# "crates/client",
|
||||
# "crates/clock",
|
||||
# "crates/collab",
|
||||
# "crates/collab_ui",
|
||||
# "crates/collections",
|
||||
# "crates/command_palette",
|
||||
# "crates/command_palette_hooks",
|
||||
# "crates/copilot",
|
||||
# "crates/copilot_ui",
|
||||
# "crates/db",
|
||||
# "crates/diagnostics",
|
||||
# "crates/editor",
|
||||
# "crates/extension",
|
||||
# "crates/extension_api",
|
||||
# "crates/extensions_ui",
|
||||
# "crates/feature_flags",
|
||||
# "crates/feedback",
|
||||
# "crates/file_finder",
|
||||
# "crates/fs",
|
||||
# "crates/fsevent",
|
||||
# "crates/fuzzy",
|
||||
# "crates/git",
|
||||
# "crates/go_to_line",
|
||||
# "crates/gpui",
|
||||
# "crates/gpui_macros",
|
||||
# "crates/install_cli",
|
||||
# "crates/journal",
|
||||
# "crates/language",
|
||||
# "crates/language_selector",
|
||||
# "crates/language_tools",
|
||||
# "crates/languages",
|
||||
# "crates/live_kit_client",
|
||||
# "crates/live_kit_server",
|
||||
# "crates/lsp",
|
||||
# "crates/markdown_preview",
|
||||
# "crates/media",
|
||||
# "crates/menu",
|
||||
# "crates/multi_buffer",
|
||||
# "crates/node_runtime",
|
||||
# "crates/notifications",
|
||||
# "crates/outline",
|
||||
# "crates/picker",
|
||||
# "crates/prettier",
|
||||
# "crates/project",
|
||||
# "crates/project_core",
|
||||
# "crates/project_panel",
|
||||
# "crates/project_symbols",
|
||||
# "crates/quick_action_bar",
|
||||
# "crates/recent_projects",
|
||||
# "crates/refineable",
|
||||
# "crates/refineable/derive_refineable",
|
||||
# "crates/release_channel",
|
||||
# "crates/rich_text",
|
||||
# "crates/rope",
|
||||
# "crates/rpc",
|
||||
# "crates/task",
|
||||
# "crates/tasks_ui",
|
||||
# "crates/search",
|
||||
# "crates/semantic_index",
|
||||
# "crates/settings",
|
||||
# "crates/snippet",
|
||||
# "crates/sqlez",
|
||||
# "crates/sqlez_macros",
|
||||
# "crates/story",
|
||||
# "crates/storybook",
|
||||
# "crates/sum_tree",
|
||||
# "crates/terminal",
|
||||
# "crates/terminal_view",
|
||||
# "crates/text",
|
||||
# "crates/theme",
|
||||
# "crates/theme_importer",
|
||||
# "crates/theme_selector",
|
||||
# "crates/telemetry_events",
|
||||
# "crates/time_format",
|
||||
# "crates/ui",
|
||||
# "crates/util",
|
||||
# "crates/vcs_menu",
|
||||
# "crates/vim",
|
||||
# "crates/welcome",
|
||||
"crates/workspace",
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
# "crates/zed_actions",
|
||||
|
||||
"extensions/gleam",
|
||||
"extensions/uiua",
|
||||
# "extensions/gleam",
|
||||
# "extensions/uiua",
|
||||
|
||||
"tooling/xtask",
|
||||
# "tooling/xtask",
|
||||
]
|
||||
default-members = ["crates/zed"]
|
||||
resolver = "2"
|
||||
|
|
|
@ -9,12 +9,11 @@ pub struct IpcHandshake {
|
|||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum CliRequest {
|
||||
// The filed is named `path` for compatibility, but now CLI can request
|
||||
// opening a path at a certain row and/or column: `some/path:123` and `some/path:123:456`.
|
||||
//
|
||||
// Since Zed CLI has to be installed separately, there can be situations when old CLI is
|
||||
// querying new Zed editors, support both formats by using `String` here and parsing it on Zed side later.
|
||||
Open { paths: Vec<String>, wait: bool },
|
||||
Open {
|
||||
paths: Vec<String>,
|
||||
wait: bool,
|
||||
open_new_workspace: Option<bool>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
#![cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use clap::Parser;
|
||||
use clap::{Error, ErrorKind, Parser};
|
||||
use cli::{CliRequest, CliResponse};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
env,
|
||||
ffi::OsStr,
|
||||
fs::{self, OpenOptions},
|
||||
io,
|
||||
|
@ -12,12 +13,18 @@ use std::{
|
|||
};
|
||||
use util::paths::PathLikeWithPosition;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))]
|
||||
struct Args {
|
||||
/// Wait for all of the given paths to be opened/closed before exiting.
|
||||
#[clap(short, long)]
|
||||
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.
|
||||
///
|
||||
/// Use `path:line:row` syntax to open a file at a specific location.
|
||||
|
@ -56,32 +63,54 @@ fn main() -> Result<()> {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
for path in args
|
||||
.paths_with_position
|
||||
.iter()
|
||||
.map(|path_with_position| &path_with_position.path_like)
|
||||
{
|
||||
if !path.exists() {
|
||||
touch(path.as_path())?;
|
||||
}
|
||||
}
|
||||
// for path in args
|
||||
// .paths_with_position
|
||||
// .iter()
|
||||
// .map(|path_with_position| &path_with_position.path_like)
|
||||
// {
|
||||
// if !path.exists() {
|
||||
// touch(path.as_path())?;
|
||||
// }
|
||||
// }
|
||||
|
||||
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_with_position
|
||||
.into_iter()
|
||||
.map(|path_with_position| {
|
||||
let path_with_position = path_with_position.map_path_like(|path| {
|
||||
fs::canonicalize(&path)
|
||||
.with_context(|| format!("path {path:?} canonicalization"))
|
||||
})?;
|
||||
let path_with_position =
|
||||
path_with_position.map_path_like(|path| match fs::canonicalize(&path) {
|
||||
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()))
|
||||
})
|
||||
.collect::<Result<_>>()?,
|
||||
wait: args.wait,
|
||||
})?;
|
||||
open_new_workspace,
|
||||
}))?;
|
||||
|
||||
while let Ok(response) = rx.recv() {
|
||||
match response {
|
||||
|
|
|
@ -500,7 +500,12 @@ impl FakeFsState {
|
|||
fn read_path(&self, target: &Path) -> Result<Arc<Mutex<FakeFsEntry>>> {
|
||||
Ok(self
|
||||
.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)
|
||||
}
|
||||
|
||||
|
|
|
@ -102,7 +102,14 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut WindowContext) {
|
|||
cx.spawn(|mut cx| async move {
|
||||
let (journal_dir, entry_path) = create_entry.await?;
|
||||
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?;
|
||||
|
||||
let opened = workspace
|
||||
|
|
|
@ -74,7 +74,7 @@ use std::{
|
|||
env,
|
||||
ffi::OsStr,
|
||||
hash::Hash,
|
||||
mem,
|
||||
io, mem,
|
||||
num::NonZeroU32,
|
||||
ops::Range,
|
||||
path::{self, Component, Path, PathBuf},
|
||||
|
@ -1136,18 +1136,24 @@ impl Project {
|
|||
.map(|worktree| worktree.read(cx).id())
|
||||
}
|
||||
|
||||
pub fn contains_paths(&self, paths: &[PathBuf], cx: &AppContext) -> bool {
|
||||
paths.iter().all(|path| self.contains_path(path, cx))
|
||||
pub fn visibility_for_paths(&self, paths: &[PathBuf], cx: &AppContext) -> Option<bool> {
|
||||
paths
|
||||
.iter()
|
||||
.map(|path| self.visibility_for_path(path, cx))
|
||||
.max()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub fn contains_path(&self, path: &Path, cx: &AppContext) -> bool {
|
||||
for worktree in self.worktrees() {
|
||||
let worktree = worktree.read(cx).as_local();
|
||||
if worktree.map_or(false, |w| w.contains_abs_path(path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
pub fn visibility_for_path(&self, path: &Path, cx: &AppContext) -> Option<bool> {
|
||||
self.worktrees()
|
||||
.filter_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
worktree
|
||||
.as_local()?
|
||||
.contains_abs_path(path)
|
||||
.then(|| worktree.is_visible())
|
||||
})
|
||||
.max()
|
||||
}
|
||||
|
||||
pub fn create_entry(
|
||||
|
@ -1822,12 +1828,28 @@ impl Project {
|
|||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Model<Buffer>>> {
|
||||
let buffer_id = self.next_buffer_id.next();
|
||||
let path2 = path.clone();
|
||||
let load_buffer = worktree.update(cx, |worktree, cx| {
|
||||
let worktree = worktree.as_local_mut().unwrap();
|
||||
worktree.load_buffer(buffer_id, path, cx)
|
||||
});
|
||||
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))??;
|
||||
Ok(buffer)
|
||||
})
|
||||
|
|
|
@ -493,9 +493,16 @@ mod tests {
|
|||
}),
|
||||
)
|
||||
.await;
|
||||
cx.update(|cx| open_paths(&[PathBuf::from("/dir/main.ts")], app_state, None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|cx| {
|
||||
open_paths(
|
||||
&[PathBuf::from("/dir/main.ts")],
|
||||
app_state,
|
||||
workspace::OpenOptions::default(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
|
||||
|
||||
let workspace = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
|
||||
|
|
|
@ -262,7 +262,8 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
|||
cx.spawn(move |cx| async move {
|
||||
if let Some(paths) = paths.await.log_err().flatten() {
|
||||
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();
|
||||
}
|
||||
|
@ -794,6 +795,7 @@ impl Workspace {
|
|||
app_state.fs.clone(),
|
||||
cx,
|
||||
);
|
||||
dbg!("new_local", &abs_paths);
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
let serialized_workspace: Option<SerializedWorkspace> =
|
||||
|
@ -813,12 +815,15 @@ impl Workspace {
|
|||
.await
|
||||
.log_err()
|
||||
{
|
||||
dbg!("got project_path_for_path");
|
||||
worktree_roots.extend(worktree.update(&mut cx, |tree, _| tree.abs_path()).ok());
|
||||
project_paths.push((path, Some(project_entry)));
|
||||
} else {
|
||||
dbg!("NO project_path_for_path");
|
||||
project_paths.push((path, None));
|
||||
}
|
||||
}
|
||||
dbg!(&project_paths);
|
||||
|
||||
let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
|
||||
serialized_workspace.id
|
||||
|
@ -1414,8 +1419,18 @@ impl Workspace {
|
|||
let app_state = self.app_state.clone();
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
cx.update(|cx| open_paths(&paths, app_state, window_to_replace, cx))?
|
||||
.await?;
|
||||
cx.update(|cx| {
|
||||
open_paths(
|
||||
&paths,
|
||||
app_state,
|
||||
OpenOptions {
|
||||
replace_window: window_to_replace,
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
@ -3729,19 +3744,21 @@ fn open_items(
|
|||
let fs = app_state.fs.clone();
|
||||
async move {
|
||||
let file_project_path = project_path?;
|
||||
if fs.is_file(&abs_path).await {
|
||||
Some((
|
||||
ix,
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.open_path(file_project_path, None, true, cx)
|
||||
})
|
||||
.log_err()?
|
||||
.await,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
// if fs.metadata(&abs_path).await.is_ok_and(|metadata| {
|
||||
// metadata.is_none() || !metadata.unwrap().is_dir
|
||||
// }) {
|
||||
Some((
|
||||
ix,
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.open_path(file_project_path, None, true, cx)
|
||||
})
|
||||
.log_err()?
|
||||
.await,
|
||||
))
|
||||
// } else {
|
||||
// None
|
||||
// }
|
||||
}
|
||||
})
|
||||
});
|
||||
|
@ -4360,6 +4377,13 @@ pub async fn get_any_active_workspace(
|
|||
|
||||
fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
|
||||
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() {
|
||||
if let Some(workspace_window) = window.downcast::<Workspace>() {
|
||||
workspace_window
|
||||
|
@ -4374,11 +4398,17 @@ fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandl
|
|||
.flatten()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct OpenOptions {
|
||||
pub open_new_workspace: Option<bool>,
|
||||
pub replace_window: Option<WindowHandle<Workspace>>,
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn open_paths(
|
||||
abs_paths: &[PathBuf],
|
||||
app_state: Arc<AppState>,
|
||||
requesting_window: Option<WindowHandle<Workspace>>,
|
||||
open_options: OpenOptions,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<
|
||||
anyhow::Result<(
|
||||
|
@ -4387,24 +4417,62 @@ pub fn open_paths(
|
|||
)>,
|
||||
> {
|
||||
let abs_paths = abs_paths.to_vec();
|
||||
// Open paths in existing workspace if possible
|
||||
let existing = activate_workspace_for_project(cx, {
|
||||
let abs_paths = abs_paths.clone();
|
||||
move |project, cx| project.contains_paths(&abs_paths, cx)
|
||||
});
|
||||
let mut existing = None;
|
||||
let mut best_match = None;
|
||||
let mut open_visible = OpenVisible::All;
|
||||
|
||||
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 {
|
||||
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 {
|
||||
Ok((
|
||||
existing,
|
||||
existing
|
||||
.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,
|
||||
))
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -264,24 +264,14 @@ fn main() {
|
|||
cx.set_menus(app_menus());
|
||||
initialize_workspace(app_state.clone(), cx);
|
||||
|
||||
if stdout_is_a_pty() {
|
||||
// todo(linux): unblock this
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
upload_panics_and_crashes(http.clone(), cx);
|
||||
cx.activate(true);
|
||||
let urls = collect_url_args(cx);
|
||||
if !urls.is_empty() {
|
||||
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))
|
||||
}
|
||||
// todo(linux): unblock this
|
||||
upload_panics_and_crashes(http.clone(), cx);
|
||||
|
||||
cx.activate(true);
|
||||
|
||||
let urls = collect_url_args(cx);
|
||||
if !urls.is_empty() {
|
||||
listener.open_urls(urls)
|
||||
}
|
||||
|
||||
let mut triggered_authentication = false;
|
||||
|
@ -339,8 +329,13 @@ fn handle_open_request(
|
|||
if !request.open_paths.is_empty() {
|
||||
let app_state = app_state.clone();
|
||||
task = Some(cx.spawn(|mut cx| async move {
|
||||
let (_window, results) =
|
||||
open_paths_with_positions(&request.open_paths, app_state, &mut cx).await?;
|
||||
let (_window, results) = open_paths_with_positions(
|
||||
&request.open_paths,
|
||||
app_state,
|
||||
workspace::OpenOptions::default(),
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
for result in results.into_iter().flatten() {
|
||||
if let Err(err) = result {
|
||||
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_maybe!({
|
||||
if let Some(location) = workspace::last_opened_workspace_paths().await {
|
||||
cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))?
|
||||
.await
|
||||
.log_err();
|
||||
cx.update(|cx| {
|
||||
workspace::open_paths(
|
||||
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)) {
|
||||
cx.update(|cx| show_welcome_view(app_state, cx)).log_err();
|
||||
} else {
|
||||
|
@ -901,7 +903,7 @@ fn collect_url_args(cx: &AppContext) -> Vec<String> {
|
|||
.filter_map(|arg| match std::fs::canonicalize(Path::new(&arg)) {
|
||||
Ok(path) => Some(format!("file://{}", path.to_string_lossy())),
|
||||
Err(error) => {
|
||||
if arg.starts_with("file://") {
|
||||
if arg.starts_with("file://") || arg.starts_with("zed-cli://") {
|
||||
Some(arg)
|
||||
} else if let Some(_) = parse_zed_link(&arg, cx) {
|
||||
Some(arg)
|
||||
|
|
|
@ -11,11 +11,10 @@ use futures::{FutureExt, SinkExt, StreamExt};
|
|||
use gpui::{AppContext, AsyncAppContext, Global, WindowHandle};
|
||||
use language::{Bias, Point};
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::{path::PathBuf, sync::atomic::AtomicBool};
|
||||
use util::paths::PathLikeWithPosition;
|
||||
use util::ResultExt;
|
||||
use workspace::item::ItemHandle;
|
||||
|
@ -89,7 +88,6 @@ impl OpenRequest {
|
|||
|
||||
pub struct OpenListener {
|
||||
tx: UnboundedSender<Vec<String>>,
|
||||
pub triggered: AtomicBool,
|
||||
}
|
||||
|
||||
struct GlobalOpenListener(Arc<OpenListener>);
|
||||
|
@ -107,17 +105,10 @@ impl OpenListener {
|
|||
|
||||
pub fn new() -> (Self, UnboundedReceiver<Vec<String>>) {
|
||||
let (tx, rx) = mpsc::unbounded();
|
||||
(
|
||||
OpenListener {
|
||||
tx,
|
||||
triggered: AtomicBool::new(false),
|
||||
},
|
||||
rx,
|
||||
)
|
||||
(OpenListener { tx }, rx)
|
||||
}
|
||||
|
||||
pub fn open_urls(&self, urls: Vec<String>) {
|
||||
self.triggered.store(true, Ordering::Release);
|
||||
self.tx
|
||||
.unbounded_send(urls)
|
||||
.map_err(|_| anyhow!("no listener for open requests"))
|
||||
|
@ -157,6 +148,7 @@ fn connect_to_cli(
|
|||
pub async fn open_paths_with_positions(
|
||||
path_likes: &Vec<PathLikeWithPosition<PathBuf>>,
|
||||
app_state: Arc<AppState>,
|
||||
open_options: workspace::OpenOptions,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<(
|
||||
WindowHandle<Workspace>,
|
||||
|
@ -180,7 +172,7 @@ pub async fn open_paths_with_positions(
|
|||
.collect::<Vec<_>>();
|
||||
|
||||
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?;
|
||||
|
||||
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 {
|
||||
match request {
|
||||
CliRequest::Open { paths, wait } => {
|
||||
CliRequest::Open {
|
||||
paths,
|
||||
wait,
|
||||
open_new_workspace,
|
||||
} => {
|
||||
let paths = if paths.is_empty() {
|
||||
workspace::last_opened_workspace_paths()
|
||||
.await
|
||||
.map(|location| {
|
||||
location
|
||||
.paths()
|
||||
.iter()
|
||||
.map(|path| PathLikeWithPosition {
|
||||
path_like: path.clone(),
|
||||
row: None,
|
||||
column: None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
if open_new_workspace == Some(true) {
|
||||
vec![]
|
||||
} else {
|
||||
workspace::last_opened_workspace_paths()
|
||||
.await
|
||||
.map(|location| {
|
||||
location
|
||||
.paths()
|
||||
.iter()
|
||||
.map(|path| PathLikeWithPosition {
|
||||
path_like: path.clone(),
|
||||
row: None,
|
||||
column: None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
} else {
|
||||
paths
|
||||
.into_iter()
|
||||
|
@ -250,7 +250,17 @@ pub async fn handle_cli_connection(
|
|||
|
||||
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)) => {
|
||||
let mut item_release_futures = Vec::new();
|
||||
|
||||
|
|
|
@ -877,6 +877,41 @@ mod tests {
|
|||
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]
|
||||
async fn test_open_paths_action(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
@ -910,7 +945,7 @@ mod tests {
|
|||
open_paths(
|
||||
&[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
|
||||
app_state.clone(),
|
||||
None,
|
||||
workspace::OpenOptions::default(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
|
@ -918,9 +953,16 @@ mod tests {
|
|||
.unwrap();
|
||||
assert_eq!(cx.read(|cx| cx.windows().len()), 1);
|
||||
|
||||
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], app_state.clone(), None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|cx| {
|
||||
open_paths(
|
||||
&[PathBuf::from("/root/a")],
|
||||
app_state.clone(),
|
||||
workspace::OpenOptions::default(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(cx.read(|cx| cx.windows().len()), 1);
|
||||
let workspace_1 = cx
|
||||
.read(|cx| cx.windows()[0].downcast::<Workspace>())
|
||||
|
@ -941,7 +983,7 @@ mod tests {
|
|||
open_paths(
|
||||
&[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
|
||||
app_state.clone(),
|
||||
None,
|
||||
workspace::OpenOptions::default(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
|
@ -957,7 +999,10 @@ mod tests {
|
|||
open_paths(
|
||||
&[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
|
||||
app_state,
|
||||
Some(window),
|
||||
workspace::OpenOptions {
|
||||
replace_window: Some(window),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
|
@ -983,6 +1028,123 @@ mod tests {
|
|||
.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]
|
||||
async fn test_window_edit_state(cx: &mut TestAppContext) {
|
||||
let executor = cx.executor();
|
||||
|
@ -993,9 +1155,16 @@ mod tests {
|
|||
.insert_tree("/root", json!({"a": "hey"}))
|
||||
.await;
|
||||
|
||||
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], app_state.clone(), None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|cx| {
|
||||
open_paths(
|
||||
&[PathBuf::from("/root/a")],
|
||||
app_state.clone(),
|
||||
workspace::OpenOptions::default(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
|
||||
|
||||
// When opening the workspace, the window is not in a edited state.
|
||||
|
@ -1060,9 +1229,16 @@ mod tests {
|
|||
assert!(!window_is_edited(window, cx));
|
||||
|
||||
// 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))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|cx| {
|
||||
open_paths(
|
||||
&[PathBuf::from("/root/a")],
|
||||
app_state,
|
||||
workspace::OpenOptions::default(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let editor = window
|
||||
.read_with(cx, |workspace, cx| {
|
||||
workspace
|
||||
|
@ -1289,9 +1465,16 @@ mod tests {
|
|||
)
|
||||
.await;
|
||||
|
||||
cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], app_state, None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|cx| {
|
||||
open_paths(
|
||||
&[PathBuf::from("/dir1/")],
|
||||
app_state,
|
||||
workspace::OpenOptions::default(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
|
||||
let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
|
||||
let workspace = window.root(cx).unwrap();
|
||||
|
@ -1523,7 +1706,14 @@ mod tests {
|
|||
Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(),
|
||||
];
|
||||
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
|
||||
.unwrap();
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue