diff --git a/Cargo.lock b/Cargo.lock index 47cf6cfed6..aa6eb75f2c 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 dd7751c70a..0afd7e41b9 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 0f60f439d0..754a47baa4 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/"; +/// prefix for the zed:// url scheme +pub static ZED_URL_SCHEME: &str = "zed"; -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; +/// 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 1cc48591c7..ac0793715f 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 1df8e7e73b..500c7affcf 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 125e8c64f3..4f9b18198b 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 4d268545e8..565939e4a3 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 197ad90586..0cd7f581de 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 f30e5264f1..9373ad66e8 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 8222b88fe1..1e1b66fffe 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 414c1507dc..2a7b8bfb2e 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 0a60fd5172..d293f4cf40 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 d97a4fc5ab..5df416f9ab 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 e7751579c9..be3d789813 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 61c5aa2fb9..506de309ef 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 78b17ba997..864df387c0 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 4ad8549e68..2ad94a8bae 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 c7356216a1..277d4dda03 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 4d3630a846..41dfe7e432 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 7c47a6b6b5..ca28267472 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_log_err(cx); + .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_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 a6414febcb..d90ed2a014 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.