From f53823c840cdb80c65e9c7fc5dd7a416d72d3ead Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 4 Mar 2024 16:08:47 -0700 Subject: [PATCH] Remove release channel from Zed URLs (#8863) Also adds a new command `cli: Register Zed Scheme` that will cause URLs to be opened in the current zed version, and we call this implicitly if you install the CLI Also add some status reporting to install cli Fixes: #8857 Release Notes: - Added success/error reporting to `cli: Install Cli` ([#8857](https://github.com/zed-industries/zed/issues/8857)). - Removed `zed-{preview,nightly,dev}:` url schemes (used by channel links) - Added `cli: Register Zed Scheme` to control which zed handles the `zed://` scheme (defaults to the most recently installed, or the version that you last used `cli: Install Cli` with) --- Cargo.lock | 1 - crates/channel/src/channel_store.rs | 21 ++++--- crates/client/src/client.rs | 47 +++++++------- crates/collab_ui/src/channel_view.rs | 2 +- crates/collab_ui/src/collab_panel.rs | 4 +- .../src/collab_panel/channel_modal.rs | 2 +- crates/command_palette/Cargo.toml | 1 - crates/command_palette/src/command_palette.rs | 6 +- crates/gpui/src/app.rs | 8 +++ crates/gpui/src/platform.rs | 2 + crates/gpui/src/platform/linux/platform.rs | 4 ++ crates/gpui/src/platform/mac/platform.rs | 43 +++++++++++++ crates/gpui/src/platform/test/platform.rs | 4 ++ crates/gpui/src/platform/windows/platform.rs | 4 ++ crates/install_cli/src/install_cli.rs | 12 ++-- crates/release_channel/src/lib.rs | 41 ------------ crates/zed/Cargo.toml | 6 +- crates/zed/src/main.rs | 16 ++--- crates/zed/src/open_listener.rs | 6 +- crates/zed/src/zed.rs | 62 ++++++++++++++++--- script/bundle | 1 + 21 files changed, 179 insertions(+), 114 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 47cf6cfed628ececc6f2b40b185920e9c264b587..aa6eb75f2c55f4b297ab775b416b38fc975d759f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2390,7 +2390,6 @@ dependencies = [ "picker", "postage", "project", - "release_channel", "serde", "serde_json", "settings", diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index dd7751c70ae598c70c6e431cb9c48bd9c917f647..0afd7e41b94a7b26e05794e362065c3429687221 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -3,7 +3,7 @@ mod channel_index; use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage}; use anyhow::{anyhow, Result}; use channel_index::ChannelIndex; -use client::{ChannelId, Client, Subscription, User, UserId, UserStore}; +use client::{ChannelId, Client, ClientSettings, Subscription, User, UserId, UserStore}; use collections::{hash_map, HashMap, HashSet}; use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; use gpui::{ @@ -11,11 +11,11 @@ use gpui::{ Task, WeakModel, }; use language::Capability; -use release_channel::RELEASE_CHANNEL; use rpc::{ proto::{self, ChannelRole, ChannelVisibility}, TypedEnvelope, }; +use settings::Settings; use std::{mem, sync::Arc, time::Duration}; use util::{async_maybe, maybe, ResultExt}; @@ -93,16 +93,17 @@ pub struct ChannelState { } impl Channel { - pub fn link(&self) -> String { - RELEASE_CHANNEL.link_prefix().to_owned() - + "channel/" - + &Self::slug(&self.name) - + "-" - + &self.id.to_string() + pub fn link(&self, cx: &AppContext) -> String { + format!( + "{}/channel/{}-{}", + ClientSettings::get_global(cx).server_url, + Self::slug(&self.name), + self.id + ) } - pub fn notes_link(&self, heading: Option) -> String { - self.link() + pub fn notes_link(&self, heading: Option, cx: &AppContext) -> String { + self.link(cx) + "/notes" + &heading .map(|h| format!("#{}", Self::slug(&h))) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 0f60f439d0b7ff213dbeede90e0cc628fd08c17e..754a47baa49537b4c484382555291e90670f9edf 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1437,21 +1437,29 @@ async fn delete_credentials_from_keychain(cx: &AsyncAppContext) -> Result<()> { .await } -const WORKTREE_URL_PREFIX: &str = "zed://worktrees/"; - -pub fn encode_worktree_url(id: u64, access_token: &str) -> String { - format!("{}{}/{}", WORKTREE_URL_PREFIX, id, access_token) -} - -pub fn decode_worktree_url(url: &str) -> Option<(u64, String)> { - let path = url.trim().strip_prefix(WORKTREE_URL_PREFIX)?; - let mut parts = path.split('/'); - let id = parts.next()?.parse::().ok()?; - let access_token = parts.next()?; - if access_token.is_empty() { - return None; +/// prefix for the zed:// url scheme +pub static ZED_URL_SCHEME: &str = "zed"; + +/// Parses the given link into a Zed link. +/// +/// Returns a [`Some`] containing the unprefixed link if the link is a Zed link. +/// Returns [`None`] otherwise. +pub fn parse_zed_link<'a>(link: &'a str, cx: &AppContext) -> Option<&'a str> { + let server_url = &ClientSettings::get_global(cx).server_url; + if let Some(stripped) = link + .strip_prefix(server_url) + .and_then(|result| result.strip_prefix('/')) + { + return Some(stripped); } - Some((id, access_token.to_string())) + if let Some(stripped) = link + .strip_prefix(ZED_URL_SCHEME) + .and_then(|result| result.strip_prefix("://")) + { + return Some(stripped); + } + + None } #[cfg(test)] @@ -1629,17 +1637,6 @@ mod tests { assert_eq!(*dropped_auth_count.lock(), 1); } - #[test] - fn test_encode_and_decode_worktree_url() { - let url = encode_worktree_url(5, "deadbeef"); - assert_eq!(decode_worktree_url(&url), Some((5, "deadbeef".to_string()))); - assert_eq!( - decode_worktree_url(&format!("\n {}\t", url)), - Some((5, "deadbeef".to_string())) - ); - assert_eq!(decode_worktree_url("not://the-right-format"), None); - } - #[gpui::test] async fn test_subscribing_to_entity(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 1cc48591c77738e859090a1c1f02d9df28acdeb7..ac0793715fad450f0be95425920d99d2a0ebe371 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -265,7 +265,7 @@ impl ChannelView { return; }; - let link = channel.notes_link(closest_heading.map(|heading| heading.text)); + let link = channel.notes_link(closest_heading.map(|heading| heading.text), cx); cx.write_to_clipboard(ClipboardItem::new(link)); self.workspace .update(cx, |workspace, cx| { diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 1df8e7e73bde433e1618cd2a21aedd03eb84207d..500c7affcf942668ff5ea7d701aca1244268bad8 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2023,7 +2023,7 @@ impl CollabPanel { let Some(channel) = channel_store.channel_for_id(channel_id) else { return; }; - let item = ClipboardItem::new(channel.link()); + let item = ClipboardItem::new(channel.link(cx)); cx.write_to_clipboard(item) } @@ -2206,7 +2206,7 @@ impl CollabPanel { let channel = self.channel_store.read(cx).channel_for_id(channel_id)?; - channel_link = Some(channel.link()); + channel_link = Some(channel.link(cx)); (channel_icon, channel_tooltip_text) = match channel.visibility { proto::ChannelVisibility::Public => { (Some("icons/public.svg"), Some("Copy public channel link.")) diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 125e8c64f38ac0da899c418984303e304814c8ab..4f9b18198b032460b434c7fc86d4686b9552f712 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -197,7 +197,7 @@ impl Render for ChannelModal { .read(cx) .channel_for_id(channel_id) { - let item = ClipboardItem::new(channel.link()); + let item = ClipboardItem::new(channel.link(cx)); cx.write_to_clipboard(item); } })), diff --git a/crates/command_palette/Cargo.toml b/crates/command_palette/Cargo.toml index 4d268545e8f28af0127d4d5c3408915230274b2c..565939e4a358c32eed611214302013ac0a6eb15b 100644 --- a/crates/command_palette/Cargo.toml +++ b/crates/command_palette/Cargo.toml @@ -18,7 +18,6 @@ gpui.workspace = true picker.workspace = true postage.workspace = true project.workspace = true -release_channel.workspace = true serde.workspace = true settings.workspace = true theme.workspace = true diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 197ad90586fd53c09a63639a9f1aeef4785d78f1..0cd7f581def160764f9408f05dca7db2cb3592ba 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -4,7 +4,7 @@ use std::{ time::Duration, }; -use client::telemetry::Telemetry; +use client::{parse_zed_link, telemetry::Telemetry}; use collections::HashMap; use command_palette_hooks::{ CommandInterceptResult, CommandPaletteFilter, CommandPaletteInterceptor, @@ -17,7 +17,6 @@ use gpui::{ use picker::{Picker, PickerDelegate}; use postage::{sink::Sink, stream::Stream}; -use release_channel::parse_zed_link; use ui::{h_flex, prelude::*, v_flex, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing}; use util::ResultExt; use workspace::{ModalView, Workspace}; @@ -26,6 +25,7 @@ use zed_actions::OpenZedUrl; actions!(command_palette, [Toggle]); pub fn init(cx: &mut AppContext) { + client::init_settings(cx); cx.set_global(HitCounts::default()); cx.set_global(CommandPaletteFilter::default()); cx.observe_new_views(CommandPalette::register).detach(); @@ -192,7 +192,7 @@ impl CommandPaletteDelegate { None }; - if parse_zed_link(&query).is_some() { + if parse_zed_link(&query, cx).is_some() { intercept_result = Some(CommandInterceptResult { action: OpenZedUrl { url: query.clone() }.boxed_clone(), string: query.clone(), diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index f30e5264f129762f84303a18b32e4b42625fbd4b..9373ad66e85a760695ed1501b1d6ececd85fa467 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -566,6 +566,14 @@ impl AppContext { self.platform.open_url(url); } + /// register_url_scheme requests that the given scheme (e.g. `zed` for `zed://` urls) + /// is opened by the current app. + /// On some platforms (e.g. macOS) you may be able to register URL schemes as part of app + /// distribution, but this method exists to let you register schemes at runtime. + pub fn register_url_scheme(&self, scheme: &str) -> Task> { + self.platform.register_url_scheme(scheme) + } + /// Returns the full pathname of the current app bundle. /// If the app is not being run from a bundle, returns an error. pub fn app_path(&self) -> Result { diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 8222b88fe1ea65b9b3e807dd45a0a185f5327c9d..1e1b66fffeb33090a28b9d32dcd37db12d7381e2 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -101,6 +101,8 @@ pub(crate) trait Platform: 'static { fn open_url(&self, url: &str); fn on_open_urls(&self, callback: Box)>); + fn register_url_scheme(&self, url: &str) -> Task>; + fn prompt_for_paths( &self, options: PathPromptOptions, diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 414c1507dc95139029a446da80162d9ad48b7e76..2a7b8bfb2ece400d8399afc53068934321ff8944 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -441,6 +441,10 @@ impl Platform for LinuxPlatform { fn window_appearance(&self) -> crate::WindowAppearance { crate::WindowAppearance::Light } + + fn register_url_scheme(&self, _: &str) -> Task> { + Task::ready(Err(anyhow!("register_url_scheme unimplemented"))) + } } #[cfg(test)] diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 0a60fd51723b074a44f0487ee557ad9326fe12a4..d293f4cf4023aa3375989305f60a6ba8e33b6231 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -525,6 +525,49 @@ impl Platform for MacPlatform { } } + fn register_url_scheme(&self, scheme: &str) -> Task> { + // API only available post Monterey + // https://developer.apple.com/documentation/appkit/nsworkspace/3753004-setdefaultapplicationaturl + let (done_tx, done_rx) = oneshot::channel(); + if self.os_version().ok() < Some(SemanticVersion::new(12, 0, 0)) { + return Task::ready(Err(anyhow!( + "macOS 12.0 or later is required to register URL schemes" + ))); + } + + let bundle_id = unsafe { + let bundle: id = msg_send![class!(NSBundle), mainBundle]; + let bundle_id: id = msg_send![bundle, bundleIdentifier]; + if bundle_id == nil { + return Task::ready(Err(anyhow!("Can only register URL scheme in bundled apps"))); + } + bundle_id + }; + + unsafe { + let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; + let scheme: id = ns_string(scheme); + let app: id = msg_send![workspace, URLForApplicationWithBundleIdentifier: bundle_id]; + let done_tx = Cell::new(Some(done_tx)); + let block = ConcreteBlock::new(move |error: id| { + let result = if error == nil { + Ok(()) + } else { + let msg: id = msg_send![error, localizedDescription]; + Err(anyhow!("Failed to register: {:?}", msg)) + }; + + if let Some(done_tx) = done_tx.take() { + let _ = done_tx.send(result); + } + }); + let _: () = msg_send![workspace, setDefaultApplicationAtURL: app toOpenURLsWithScheme: scheme completionHandler: block]; + } + + self.background_executor() + .spawn(async { crate::Flatten::flatten(done_rx.await.map_err(|e| anyhow!(e))) }) + } + fn on_open_urls(&self, callback: Box)>) { self.0.lock().open_urls = Some(callback); } diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index d97a4fc5abe62499049b333b085c13d577f8699d..5df416f9ab080985c88229f57cf40c19549a5636 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -298,4 +298,8 @@ impl Platform for TestPlatform { fn double_click_interval(&self) -> std::time::Duration { Duration::from_millis(500) } + + fn register_url_scheme(&self, _: &str) -> Task> { + unimplemented!() + } } diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index e7751579c9d5e358a772fb8f4363877af9e950bf..be3d789813e8319029a9bcacf19619c5ac42f501 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -314,4 +314,8 @@ impl Platform for WindowsPlatform { fn delete_credentials(&self, url: &str) -> Task> { Task::Ready(Some(Err(anyhow!("not implemented yet.")))) } + + fn register_url_scheme(&self, _: &str) -> Task> { + Task::ready(Err(anyhow!("register_url_scheme unimplemented"))) + } } diff --git a/crates/install_cli/src/install_cli.rs b/crates/install_cli/src/install_cli.rs index 61c5aa2fb90f2198b10a4860c2a1e494e747f3f8..506de309ef892ef66cb314f7cd3401908de7d32d 100644 --- a/crates/install_cli/src/install_cli.rs +++ b/crates/install_cli/src/install_cli.rs @@ -1,18 +1,18 @@ use anyhow::{anyhow, Result}; use gpui::{actions, AsyncAppContext}; -use std::path::Path; +use std::path::{Path, PathBuf}; use util::ResultExt; -actions!(cli, [Install]); +actions!(cli, [Install, RegisterZedScheme]); -pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> { +pub async fn install_cli(cx: &AsyncAppContext) -> Result { let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))??; let link_path = Path::new("/usr/local/bin/zed"); let bin_dir_path = link_path.parent().unwrap(); // Don't re-create symlink if it points to the same CLI binary. if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) { - return Ok(()); + return Ok(link_path.into()); } // If the symlink is not there or is outdated, first try replacing it @@ -26,7 +26,7 @@ pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> { .log_err() .is_some() { - return Ok(()); + return Ok(link_path.into()); } } @@ -51,7 +51,7 @@ pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> { .await? .status; if status.success() { - Ok(()) + Ok(link_path.into()) } else { Err(anyhow!("error running osascript")) } diff --git a/crates/release_channel/src/lib.rs b/crates/release_channel/src/lib.rs index 78b17ba9977acca6e4cef4e7620363792863fccf..864df387c008d33d7a843cbdbd7a47c56a413adb 100644 --- a/crates/release_channel/src/lib.rs +++ b/crates/release_channel/src/lib.rs @@ -139,26 +139,6 @@ impl ReleaseChannel { } } - /// Returns the URL scheme for this [`ReleaseChannel`]. - pub fn url_scheme(&self) -> &'static str { - match self { - ReleaseChannel::Dev => "zed-dev://", - ReleaseChannel::Nightly => "zed-nightly://", - ReleaseChannel::Preview => "zed-preview://", - ReleaseChannel::Stable => "zed://", - } - } - - /// Returns the link prefix for this [`ReleaseChannel`]. - pub fn link_prefix(&self) -> &'static str { - match self { - ReleaseChannel::Dev => "https://zed.dev/dev/", - ReleaseChannel::Nightly => "https://zed.dev/nightly/", - ReleaseChannel::Preview => "https://zed.dev/preview/", - ReleaseChannel::Stable => "https://zed.dev/", - } - } - /// Returns the query parameter for this [`ReleaseChannel`]. pub fn release_query_param(&self) -> Option<&'static str> { match self { @@ -169,24 +149,3 @@ impl ReleaseChannel { } } } - -/// Parses the given link into a Zed link. -/// -/// Returns a [`Some`] containing the unprefixed link if the link is a Zed link. -/// Returns [`None`] otherwise. -pub fn parse_zed_link(link: &str) -> Option<&str> { - for release in [ - ReleaseChannel::Dev, - ReleaseChannel::Nightly, - ReleaseChannel::Preview, - ReleaseChannel::Stable, - ] { - if let Some(stripped) = link.strip_prefix(release.link_prefix()) { - return Some(stripped); - } - if let Some(stripped) = link.strip_prefix(release.url_scheme()) { - return Some(stripped); - } - } - None -} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 4ad8549e689ba16532ae627904846ca7b282343b..2ad94a8bae62b3d548b73b03e4e6a7f932a165d2 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -107,7 +107,7 @@ identifier = "dev.zed.Zed-Dev" name = "Zed Dev" osx_minimum_system_version = "10.15.7" osx_info_plist_exts = ["resources/info/*"] -osx_url_schemes = ["zed-dev"] +osx_url_schemes = ["zed"] [package.metadata.bundle-nightly] icon = ["resources/app-icon-nightly@2x.png", "resources/app-icon-nightly.png"] @@ -115,7 +115,7 @@ identifier = "dev.zed.Zed-Nightly" name = "Zed Nightly" osx_minimum_system_version = "10.15.7" osx_info_plist_exts = ["resources/info/*"] -osx_url_schemes = ["zed-nightly"] +osx_url_schemes = ["zed"] [package.metadata.bundle-preview] icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"] @@ -123,7 +123,7 @@ identifier = "dev.zed.Zed-Preview" name = "Zed Preview" osx_minimum_system_version = "10.15.7" osx_info_plist_exts = ["resources/info/*"] -osx_url_schemes = ["zed-preview"] +osx_url_schemes = ["zed"] [package.metadata.bundle-stable] icon = ["resources/app-icon@2x.png", "resources/app-icon.png"] diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index c7356216a1bdc199db5771f1cf277ac72e9f5606..277d4dda03f9d569094994530dc5b8a6e648bec1 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -5,7 +5,7 @@ use anyhow::{anyhow, Context as _, Result}; use backtrace::Backtrace; use chrono::Utc; use cli::FORCE_CLI_MODE_ENV_VAR_NAME; -use client::{Client, UserStore}; +use client::{parse_zed_link, Client, UserStore}; use collab_ui::channel_view::ChannelView; use db::kvp::KEY_VALUE_STORE; use editor::Editor; @@ -23,7 +23,7 @@ use assets::Assets; use mimalloc::MiMalloc; use node_runtime::RealNodeRuntime; use parking_lot::Mutex; -use release_channel::{parse_zed_link, AppCommitSha, ReleaseChannel, RELEASE_CHANNEL}; +use release_channel::{AppCommitSha, ReleaseChannel, RELEASE_CHANNEL}; use serde::{Deserialize, Serialize}; use settings::{ default_settings, handle_settings_file_changes, watch_config_file, Settings, SettingsStore, @@ -106,7 +106,7 @@ fn main() { let (listener, mut open_rx) = OpenListener::new(); let listener = Arc::new(listener); let open_listener = listener.clone(); - app.on_open_urls(move |urls, _| open_listener.open_urls(&urls)); + app.on_open_urls(move |urls, cx| open_listener.open_urls(&urls, cx)); app.on_reopen(move |cx| { if let Some(app_state) = AppState::try_global(cx).and_then(|app_state| app_state.upgrade()) { @@ -271,9 +271,9 @@ fn main() { #[cfg(not(target_os = "linux"))] upload_panics_and_crashes(http.clone(), cx); cx.activate(true); - let urls = collect_url_args(); + let urls = collect_url_args(cx); if !urls.is_empty() { - listener.open_urls(&urls) + listener.open_urls(&urls, cx) } } else { upload_panics_and_crashes(http.clone(), cx); @@ -282,7 +282,7 @@ fn main() { if std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_some() && !listener.triggered.load(Ordering::Acquire) { - listener.open_urls(&collect_url_args()) + listener.open_urls(&collect_url_args(cx), cx) } } @@ -921,13 +921,13 @@ fn stdout_is_a_pty() -> bool { std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() && std::io::stdout().is_terminal() } -fn collect_url_args() -> Vec { +fn collect_url_args(cx: &AppContext) -> Vec { env::args() .skip(1) .filter_map(|arg| match std::fs::canonicalize(Path::new(&arg)) { Ok(path) => Some(format!("file://{}", path.to_string_lossy())), Err(error) => { - if let Some(_) = parse_zed_link(&arg) { + if let Some(_) = parse_zed_link(&arg, cx) { Some(arg) } else { log::error!("error parsing path argument: {}", error); diff --git a/crates/zed/src/open_listener.rs b/crates/zed/src/open_listener.rs index 4d3630a846f3cdbb2f14b3c21aea0a1c9d9a4047..41dfe7e4321ea7785f8604d00f60f5ee49155ae1 100644 --- a/crates/zed/src/open_listener.rs +++ b/crates/zed/src/open_listener.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, Context, Result}; use cli::{ipc, IpcHandshake}; use cli::{ipc::IpcSender, CliRequest, CliResponse}; +use client::parse_zed_link; use collections::HashMap; use editor::scroll::Autoscroll; use editor::Editor; @@ -10,7 +11,6 @@ use futures::{FutureExt, SinkExt, StreamExt}; use gpui::{AppContext, AsyncAppContext, Global}; use itertools::Itertools; use language::{Bias, Point}; -use release_channel::parse_zed_link; use std::path::Path; use std::sync::atomic::Ordering; use std::sync::Arc; @@ -66,13 +66,13 @@ impl OpenListener { ) } - pub fn open_urls(&self, urls: &[String]) { + pub fn open_urls(&self, urls: &[String], cx: &AppContext) { self.triggered.store(true, Ordering::Release); let request = if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) { self.handle_cli_connection(server_name) - } else if let Some(request_path) = urls.first().and_then(|url| parse_zed_link(url)) { + } else if let Some(request_path) = urls.first().and_then(|url| parse_zed_link(url, cx)) { self.handle_zed_url_scheme(request_path) } else { self.handle_file_urls(urls) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 7c47a6b6b5c7f5b973edbd4e4c90997eee4c1bfb..ca282674727669281440f415e887f606425ebd72 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -5,11 +5,12 @@ mod open_listener; pub use app_menus::*; use assistant::AssistantPanel; use breadcrumbs::Breadcrumbs; +use client::ZED_URL_SCHEME; use collections::VecDeque; use editor::{Editor, MultiBuffer}; use gpui::{ - actions, point, px, AppContext, Context, FocusableView, PromptLevel, TitlebarOptions, View, - ViewContext, VisualContext, WindowBounds, WindowKind, WindowOptions, + actions, point, px, AppContext, AsyncAppContext, Context, FocusableView, PromptLevel, + TitlebarOptions, View, ViewContext, VisualContext, WindowBounds, WindowKind, WindowOptions, }; pub use only_instance::*; pub use open_listener::*; @@ -38,11 +39,11 @@ use util::{ use uuid::Uuid; use vim::VimModeSetting; use welcome::BaseKeymap; -use workspace::Pane; use workspace::{ create_and_open_local_file, notifications::simple_message_notification::MessageNotification, - open_new, AppState, NewFile, NewWindow, Workspace, WorkspaceSettings, + open_new, AppState, NewFile, NewWindow, Toast, Workspace, WorkspaceSettings, }; +use workspace::{notifications::DetachAndPromptErr, Pane}; use zed_actions::{OpenBrowser, OpenSettings, OpenZedUrl, Quit}; actions!( @@ -232,7 +233,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { cx.toggle_full_screen(); }) .register_action(|_, action: &OpenZedUrl, cx| { - OpenListener::global(cx).open_urls(&[action.url.clone()]) + OpenListener::global(cx).open_urls(&[action.url.clone()], cx) }) .register_action(|_, action: &OpenBrowser, cx| cx.open_url(&action.url)) .register_action(move |_, _: &IncreaseBufferFontSize, cx| { @@ -243,12 +244,50 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { }) .register_action(move |_, _: &ResetBufferFontSize, cx| theme::reset_font_size(cx)) .register_action(|_, _: &install_cli::Install, cx| { - cx.spawn(|_, cx| async move { - install_cli::install_cli(cx.deref()) + cx.spawn(|workspace, mut cx| async move { + let path = install_cli::install_cli(cx.deref()) .await - .context("error creating CLI symlink") + .context("error creating CLI symlink")?; + workspace.update(&mut cx, |workspace, cx| { + workspace.show_toast( + Toast::new( + 0, + format!( + "Installed `zed` to {}. You can launch {} from your terminal.", + path.to_string_lossy(), + ReleaseChannel::global(cx).display_name() + ), + ), + cx, + ) + })?; + register_zed_scheme(&cx).await.log_err(); + Ok(()) + }) + .detach_and_prompt_err("Error installing zed cli", cx, |_, _| None); + }) + .register_action(|_, _: &install_cli::RegisterZedScheme, cx| { + cx.spawn(|workspace, mut cx| async move { + register_zed_scheme(&cx).await?; + workspace.update(&mut cx, |workspace, cx| { + workspace.show_toast( + Toast::new( + 0, + format!( + "zed:// links will now open in {}.", + ReleaseChannel::global(cx).display_name() + ), + ), + cx, + ) + })?; + Ok(()) }) - .detach_and_log_err(cx); + .detach_and_prompt_err( + "Error registering zed:// scheme", + cx, + |_, _| None, + ); }) .register_action(|workspace, _: &OpenLog, cx| { open_log_file(workspace, cx); @@ -2881,3 +2920,8 @@ mod tests { } } } + +async fn register_zed_scheme(cx: &AsyncAppContext) -> anyhow::Result<()> { + cx.update(|cx| cx.register_url_scheme(ZED_URL_SCHEME))? + .await +} diff --git a/script/bundle b/script/bundle index a6414febcb8c55cae553b0ac1858b14f4b2d69bf..d90ed2a014bd445c5f8f40c725112c2bfacc8f0f 100755 --- a/script/bundle +++ b/script/bundle @@ -173,6 +173,7 @@ if [ "$local_arch" = false ]; then cp -R target/${local_target_triple}/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/" else cp -R target/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/" + cp -R target/${target_dir}/cli "${app_path}/Contents/MacOS/" fi # Note: The app identifier for our development builds is the same as the app identifier for nightly.