From 0c6105992cc496976e3d27c0884e5b210074a3ca Mon Sep 17 00:00:00 2001 From: Cappy Ishihara Date: Wed, 17 Jul 2024 03:49:15 +0700 Subject: [PATCH] Open URIs from the CLI, support for the `zed://` URI scheme on Linux (#14104) Allows Zed to open custom `zed://` links (redirects from https://zed.dev/channels) on Linux used XDG MIME types. This PR also allows the CLI to be able to open Zed (`zed://`) URIs directly instead of executing the main executable in `/usr/libexec/zed-editor`. Release Notes: - Linux: Allow `zed.dev/channel` (`zed://`) URIs to open on Linux - CLI: Ability to open URIs from the command line --------- Co-authored-by: Conrad Irwin --- crates/cli/src/cli.rs | 1 + crates/cli/src/main.rs | 66 +++++++++++++++++------------ crates/zed/resources/zed.desktop.in | 4 +- crates/zed/src/zed/open_listener.rs | 24 ++++++++++- 4 files changed, 64 insertions(+), 31 deletions(-) diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 8b5faaf382..d6ea61d4d0 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -11,6 +11,7 @@ pub struct IpcHandshake { pub enum CliRequest { Open { paths: Vec, + urls: Vec, wait: bool, open_new_workspace: Option, dev_server_token: Option, diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index a5515f3be8..bb4bcd5beb 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -5,6 +5,7 @@ use clap::Parser; use cli::{ipc::IpcOneShotServer, CliRequest, CliResponse, IpcHandshake}; use parking_lot::Mutex; use std::{ + convert::Infallible, env, fs, io, path::{Path, PathBuf}, process::ExitStatus, @@ -37,8 +38,7 @@ struct Args { /// /// Use `path:line:row` syntax to open a file at a specific location. /// Non-existing paths and directories will ignore `:line:row` suffix. - #[arg(value_parser = parse_path_with_position)] - paths_with_position: Vec>, + paths_with_position: Vec, /// Print Zed's version and the app path. #[arg(short, long)] version: bool, @@ -53,12 +53,30 @@ struct Args { dev_server_token: Option, } -fn parse_path_with_position( - argument_str: &str, -) -> Result, std::convert::Infallible> { - PathLikeWithPosition::parse_str(argument_str, |_, path_str| { +fn parse_path_with_position(argument_str: &str) -> Result { + let path_like = PathLikeWithPosition::parse_str::(argument_str, |_, path_str| { Ok(Path::new(path_str).to_path_buf()) }) + .unwrap(); + let curdir = env::current_dir()?; + + let canonicalized = path_like.map_path_like(|path| match fs::canonicalize(&path) { + Ok(path) => Ok(path), + Err(e) => { + if let Some(mut parent) = path.parent() { + 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(canonicalized.to_string(|path| path.display().to_string())) } fn main() -> Result<()> { @@ -91,28 +109,6 @@ fn main() -> Result<()> { return Ok(()); } - let curdir = env::current_dir()?; - let mut paths = vec![]; - for path in args.paths_with_position { - let canonicalized = path.map_path_like(|path| match fs::canonicalize(&path) { - Ok(path) => Ok(path), - Err(e) => { - if let Some(mut parent) = path.parent() { - 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) - } - } - })?; - paths.push(canonicalized.to_string(|path| path.display().to_string())) - } - let (server, server_name) = IpcOneShotServer::::new().context("Handshake before Zed spawn")?; let url = format!("zed-cli://{server_name}"); @@ -126,6 +122,19 @@ fn main() -> Result<()> { }; let exit_status = Arc::new(Mutex::new(None)); + let mut paths = vec![]; + let mut urls = vec![]; + for path in args.paths_with_position.iter() { + if path.starts_with("zed://") + || path.starts_with("http://") + || path.starts_with("https://") + || path.starts_with("file://") + { + urls.push(path.to_string()); + } else { + paths.push(parse_path_with_position(path)?) + } + } let sender: JoinHandle> = thread::spawn({ let exit_status = exit_status.clone(); @@ -134,6 +143,7 @@ fn main() -> Result<()> { let (tx, rx) = (handshake.requests, handshake.responses); tx.send(CliRequest::Open { paths, + urls, wait: args.wait, open_new_workspace, dev_server_token: args.dev_server_token, diff --git a/crates/zed/resources/zed.desktop.in b/crates/zed/resources/zed.desktop.in index 43e3b7d0a2..cfc5236ea3 100644 --- a/crates/zed/resources/zed.desktop.in +++ b/crates/zed/resources/zed.desktop.in @@ -4,13 +4,13 @@ Type=Application Name=$APP_NAME GenericName=Text Editor Comment=A high-performance, multiplayer code editor. -TryExec=$APP_CLI +TryExec=$APP StartupNotify=$DO_STARTUP_NOTIFY Exec=$APP_CLI $APP_ARGS Icon=$APP_ICON Categories=Utility;TextEditor;Development;IDE; Keywords=zed; -MimeType=text/plain;inode/directory; +MimeType=text/plain;inode/directory;x-scheme-handler/zed; Actions=NewWorkspace; [Desktop Action NewWorkspace] diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 2a20de236b..b3bc209b07 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -22,7 +22,7 @@ use welcome::{show_welcome_view, FIRST_OPEN}; use workspace::item::ItemHandle; use workspace::{AppState, Workspace}; -use crate::{init_headless, init_ui}; +use crate::{handle_open_request, init_headless, init_ui}; #[derive(Default, Debug)] pub struct OpenRequest { @@ -223,6 +223,7 @@ pub async fn handle_cli_connection( if let Some(request) = requests.next().await { match request { CliRequest::Open { + urls, paths, wait, open_new_workspace, @@ -257,6 +258,27 @@ pub async fn handle_cli_connection( return; } + if !urls.is_empty() { + cx.update(|cx| { + match OpenRequest::parse(urls, cx) { + Ok(open_request) => { + handle_open_request(open_request, app_state.clone(), cx); + responses.send(CliResponse::Exit { status: 0 }).log_err(); + } + Err(e) => { + responses + .send(CliResponse::Stderr { + message: format!("{e}"), + }) + .log_err(); + responses.send(CliResponse::Exit { status: 1 }).log_err(); + } + }; + }) + .log_err(); + return; + } + if let Err(e) = cx .update(|cx| init_ui(app_state.clone(), cx)) .and_then(|r| r)