Allow pasting ZED urls in the command palette in development

Conrad Irwin created

Change summary

Cargo.lock                                    |   2 
crates/collab/src/db/queries/channels.rs      |   2 
crates/command_palette/Cargo.toml             |   1 
crates/command_palette/src/command_palette.rs |  17 +
crates/workspace/src/workspace.rs             |   1 
crates/zed-actions/Cargo.toml                 |   1 
crates/zed-actions/src/lib.rs                 |  15 +
crates/zed/src/main.rs                        | 206 --------------------
crates/zed/src/open_listener.rs               | 202 ++++++++++++++++++++
crates/zed/src/zed.rs                         |   6 
10 files changed, 245 insertions(+), 208 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1623,6 +1623,7 @@ dependencies = [
  "theme",
  "util",
  "workspace",
+ "zed-actions",
 ]
 
 [[package]]
@@ -10213,6 +10214,7 @@ name = "zed-actions"
 version = "0.1.0"
 dependencies = [
  "gpui",
+ "serde",
 ]
 
 [[package]]

crates/collab/src/db/queries/channels.rs 🔗

@@ -979,7 +979,7 @@ impl Database {
         })
     }
 
-    /// Returns the channel ancestors, include itself, deepest first
+    /// Returns the channel ancestors in arbitrary order
     pub async fn get_channel_ancestors(
         &self,
         channel_id: ChannelId,

crates/command_palette/Cargo.toml 🔗

@@ -19,6 +19,7 @@ settings = { path = "../settings" }
 util = { path = "../util" }
 theme = { path = "../theme" }
 workspace = { path = "../workspace" }
+zed-actions = { path = "../zed-actions" }
 
 [dev-dependencies]
 gpui = { path = "../gpui", features = ["test-support"] }

crates/command_palette/src/command_palette.rs 🔗

@@ -6,8 +6,12 @@ use gpui::{
 };
 use picker::{Picker, PickerDelegate, PickerEvent};
 use std::cmp::{self, Reverse};
-use util::ResultExt;
+use util::{
+    channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL, RELEASE_CHANNEL_NAME},
+    ResultExt,
+};
 use workspace::Workspace;
+use zed_actions::OpenZedURL;
 
 pub fn init(cx: &mut AppContext) {
     cx.add_action(toggle_command_palette);
@@ -167,13 +171,22 @@ impl PickerDelegate for CommandPaletteDelegate {
                 )
                 .await
             };
-            let intercept_result = cx.read(|cx| {
+            let mut intercept_result = cx.read(|cx| {
                 if cx.has_global::<CommandPaletteInterceptor>() {
                     cx.global::<CommandPaletteInterceptor>()(&query, cx)
                 } else {
                     None
                 }
             });
+            if *RELEASE_CHANNEL == ReleaseChannel::Dev {
+                if parse_zed_link(&query).is_some() {
+                    intercept_result = Some(CommandInterceptResult {
+                        action: OpenZedURL { url: query.clone() }.boxed_clone(),
+                        string: query.clone(),
+                        positions: vec![],
+                    })
+                }
+            }
             if let Some(CommandInterceptResult {
                 action,
                 string,

crates/workspace/src/workspace.rs 🔗

@@ -288,6 +288,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
     cx.add_global_action(restart);
     cx.add_async_action(Workspace::save_all);
     cx.add_action(Workspace::add_folder_to_project);
+
     cx.add_action(
         |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
             let pane = workspace.active_pane().clone();

crates/zed-actions/src/lib.rs 🔗

@@ -1,4 +1,7 @@
-use gpui::actions;
+use std::sync::Arc;
+
+use gpui::{actions, impl_actions};
+use serde::Deserialize;
 
 actions!(
     zed,
@@ -26,3 +29,13 @@ actions!(
         ResetDatabase,
     ]
 );
+
+#[derive(Deserialize, Clone, PartialEq)]
+pub struct OpenBrowser {
+    pub url: Arc<str>,
+}
+#[derive(Deserialize, Clone, PartialEq)]
+pub struct OpenZedURL {
+    pub url: String,
+}
+impl_actions!(zed, [OpenBrowser, OpenZedURL]);

crates/zed/src/main.rs 🔗

@@ -3,22 +3,16 @@
 
 use anyhow::{anyhow, Context, Result};
 use backtrace::Backtrace;
-use cli::{
-    ipc::{self, IpcSender},
-    CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME,
-};
+use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
 use client::{
     self, Client, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN,
 };
 use db::kvp::KEY_VALUE_STORE;
-use editor::{scroll::autoscroll::Autoscroll, Editor};
-use futures::{
-    channel::{mpsc, oneshot},
-    FutureExt, SinkExt, StreamExt,
-};
+use editor::Editor;
+use futures::StreamExt;
 use gpui::{Action, App, AppContext, AssetSource, AsyncAppContext, Task};
 use isahc::{config::Configurable, Request};
-use language::{LanguageRegistry, Point};
+use language::LanguageRegistry;
 use log::LevelFilter;
 use node_runtime::RealNodeRuntime;
 use parking_lot::Mutex;
@@ -28,7 +22,6 @@ use settings::{default_settings, handle_settings_file_changes, watch_config_file
 use simplelog::ConfigBuilder;
 use smol::process::Command;
 use std::{
-    collections::HashMap,
     env,
     ffi::OsStr,
     fs::OpenOptions,
@@ -42,11 +35,9 @@ use std::{
     thread,
     time::{Duration, SystemTime, UNIX_EPOCH},
 };
-use sum_tree::Bias;
 use util::{
     channel::{parse_zed_link, ReleaseChannel},
     http::{self, HttpClient},
-    paths::PathLikeWithPosition,
 };
 use uuid::Uuid;
 use welcome::{show_welcome_experience, FIRST_OPEN};
@@ -58,12 +49,9 @@ use zed::{
     assets::Assets,
     build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
     only_instance::{ensure_only_instance, IsOnlyInstance},
+    open_listener::{handle_cli_connection, OpenListener, OpenRequest},
 };
 
-use crate::open_listener::{OpenListener, OpenRequest};
-
-mod open_listener;
-
 fn main() {
     let http = http::client();
     init_paths();
@@ -113,6 +101,7 @@ fn main() {
 
     app.run(move |cx| {
         cx.set_global(*RELEASE_CHANNEL);
+        cx.set_global(listener.clone());
 
         let mut store = SettingsStore::default();
         store
@@ -729,189 +718,6 @@ async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()>
 #[cfg(not(debug_assertions))]
 fn watch_file_types(_fs: Arc<dyn Fs>, _cx: &mut AppContext) {}
 
-fn connect_to_cli(
-    server_name: &str,
-) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
-    let handshake_tx = cli::ipc::IpcSender::<IpcHandshake>::connect(server_name.to_string())
-        .context("error connecting to cli")?;
-    let (request_tx, request_rx) = ipc::channel::<CliRequest>()?;
-    let (response_tx, response_rx) = ipc::channel::<CliResponse>()?;
-
-    handshake_tx
-        .send(IpcHandshake {
-            requests: request_tx,
-            responses: response_rx,
-        })
-        .context("error sending ipc handshake")?;
-
-    let (mut async_request_tx, async_request_rx) =
-        futures::channel::mpsc::channel::<CliRequest>(16);
-    thread::spawn(move || {
-        while let Ok(cli_request) = request_rx.recv() {
-            if smol::block_on(async_request_tx.send(cli_request)).is_err() {
-                break;
-            }
-        }
-        Ok::<_, anyhow::Error>(())
-    });
-
-    Ok((async_request_rx, response_tx))
-}
-
-async fn handle_cli_connection(
-    (mut requests, responses): (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
-    app_state: Arc<AppState>,
-    mut cx: AsyncAppContext,
-) {
-    if let Some(request) = requests.next().await {
-        match request {
-            CliRequest::Open { paths, wait } => {
-                let mut caret_positions = HashMap::new();
-
-                let paths = if paths.is_empty() {
-                    workspace::last_opened_workspace_paths()
-                        .await
-                        .map(|location| location.paths().to_vec())
-                        .unwrap_or_default()
-                } else {
-                    paths
-                        .into_iter()
-                        .filter_map(|path_with_position_string| {
-                            let path_with_position = PathLikeWithPosition::parse_str(
-                                &path_with_position_string,
-                                |path_str| {
-                                    Ok::<_, std::convert::Infallible>(
-                                        Path::new(path_str).to_path_buf(),
-                                    )
-                                },
-                            )
-                            .expect("Infallible");
-                            let path = path_with_position.path_like;
-                            if let Some(row) = path_with_position.row {
-                                if path.is_file() {
-                                    let row = row.saturating_sub(1);
-                                    let col =
-                                        path_with_position.column.unwrap_or(0).saturating_sub(1);
-                                    caret_positions.insert(path.clone(), Point::new(row, col));
-                                }
-                            }
-                            Some(path)
-                        })
-                        .collect()
-                };
-
-                let mut errored = false;
-                match cx
-                    .update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
-                    .await
-                {
-                    Ok((workspace, items)) => {
-                        let mut item_release_futures = Vec::new();
-
-                        for (item, path) in items.into_iter().zip(&paths) {
-                            match item {
-                                Some(Ok(item)) => {
-                                    if let Some(point) = caret_positions.remove(path) {
-                                        if let Some(active_editor) = item.downcast::<Editor>() {
-                                            active_editor
-                                                .downgrade()
-                                                .update(&mut cx, |editor, cx| {
-                                                    let snapshot =
-                                                        editor.snapshot(cx).display_snapshot;
-                                                    let point = snapshot
-                                                        .buffer_snapshot
-                                                        .clip_point(point, Bias::Left);
-                                                    editor.change_selections(
-                                                        Some(Autoscroll::center()),
-                                                        cx,
-                                                        |s| s.select_ranges([point..point]),
-                                                    );
-                                                })
-                                                .log_err();
-                                        }
-                                    }
-
-                                    let released = oneshot::channel();
-                                    cx.update(|cx| {
-                                        item.on_release(
-                                            cx,
-                                            Box::new(move |_| {
-                                                let _ = released.0.send(());
-                                            }),
-                                        )
-                                        .detach();
-                                    });
-                                    item_release_futures.push(released.1);
-                                }
-                                Some(Err(err)) => {
-                                    responses
-                                        .send(CliResponse::Stderr {
-                                            message: format!("error opening {:?}: {}", path, err),
-                                        })
-                                        .log_err();
-                                    errored = true;
-                                }
-                                None => {}
-                            }
-                        }
-
-                        if wait {
-                            let background = cx.background();
-                            let wait = async move {
-                                if paths.is_empty() {
-                                    let (done_tx, done_rx) = oneshot::channel();
-                                    if let Some(workspace) = workspace.upgrade(&cx) {
-                                        let _subscription = cx.update(|cx| {
-                                            cx.observe_release(&workspace, move |_, _| {
-                                                let _ = done_tx.send(());
-                                            })
-                                        });
-                                        drop(workspace);
-                                        let _ = done_rx.await;
-                                    }
-                                } else {
-                                    let _ =
-                                        futures::future::try_join_all(item_release_futures).await;
-                                };
-                            }
-                            .fuse();
-                            futures::pin_mut!(wait);
-
-                            loop {
-                                // Repeatedly check if CLI is still open to avoid wasting resources
-                                // waiting for files or workspaces to close.
-                                let mut timer = background.timer(Duration::from_secs(1)).fuse();
-                                futures::select_biased! {
-                                    _ = wait => break,
-                                    _ = timer => {
-                                        if responses.send(CliResponse::Ping).is_err() {
-                                            break;
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                    }
-                    Err(error) => {
-                        errored = true;
-                        responses
-                            .send(CliResponse::Stderr {
-                                message: format!("error opening {:?}: {}", paths, error),
-                            })
-                            .log_err();
-                    }
-                }
-
-                responses
-                    .send(CliResponse::Exit {
-                        status: i32::from(errored),
-                    })
-                    .log_err();
-            }
-        }
-    }
-}
-
 pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
     &[
         ("Go to file", &file_finder::Toggle),

crates/zed/src/open_listener.rs 🔗

@@ -1,15 +1,26 @@
-use anyhow::anyhow;
+use anyhow::{anyhow, Context, Result};
+use cli::{ipc, IpcHandshake};
 use cli::{ipc::IpcSender, CliRequest, CliResponse};
-use futures::channel::mpsc;
+use editor::scroll::autoscroll::Autoscroll;
+use editor::Editor;
 use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
+use futures::channel::{mpsc, oneshot};
+use futures::{FutureExt, SinkExt, StreamExt};
+use gpui::AsyncAppContext;
+use language::{Bias, Point};
+use std::collections::HashMap;
 use std::ffi::OsStr;
 use std::os::unix::prelude::OsStrExt;
+use std::path::Path;
 use std::sync::atomic::Ordering;
+use std::sync::Arc;
+use std::thread;
+use std::time::Duration;
 use std::{path::PathBuf, sync::atomic::AtomicBool};
 use util::channel::parse_zed_link;
+use util::paths::PathLikeWithPosition;
 use util::ResultExt;
-
-use crate::connect_to_cli;
+use workspace::AppState;
 
 pub enum OpenRequest {
     Paths {
@@ -96,3 +107,186 @@ impl OpenListener {
         Some(OpenRequest::Paths { paths })
     }
 }
+
+fn connect_to_cli(
+    server_name: &str,
+) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
+    let handshake_tx = cli::ipc::IpcSender::<IpcHandshake>::connect(server_name.to_string())
+        .context("error connecting to cli")?;
+    let (request_tx, request_rx) = ipc::channel::<CliRequest>()?;
+    let (response_tx, response_rx) = ipc::channel::<CliResponse>()?;
+
+    handshake_tx
+        .send(IpcHandshake {
+            requests: request_tx,
+            responses: response_rx,
+        })
+        .context("error sending ipc handshake")?;
+
+    let (mut async_request_tx, async_request_rx) =
+        futures::channel::mpsc::channel::<CliRequest>(16);
+    thread::spawn(move || {
+        while let Ok(cli_request) = request_rx.recv() {
+            if smol::block_on(async_request_tx.send(cli_request)).is_err() {
+                break;
+            }
+        }
+        Ok::<_, anyhow::Error>(())
+    });
+
+    Ok((async_request_rx, response_tx))
+}
+
+pub async fn handle_cli_connection(
+    (mut requests, responses): (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
+    app_state: Arc<AppState>,
+    mut cx: AsyncAppContext,
+) {
+    if let Some(request) = requests.next().await {
+        match request {
+            CliRequest::Open { paths, wait } => {
+                let mut caret_positions = HashMap::new();
+
+                let paths = if paths.is_empty() {
+                    workspace::last_opened_workspace_paths()
+                        .await
+                        .map(|location| location.paths().to_vec())
+                        .unwrap_or_default()
+                } else {
+                    paths
+                        .into_iter()
+                        .filter_map(|path_with_position_string| {
+                            let path_with_position = PathLikeWithPosition::parse_str(
+                                &path_with_position_string,
+                                |path_str| {
+                                    Ok::<_, std::convert::Infallible>(
+                                        Path::new(path_str).to_path_buf(),
+                                    )
+                                },
+                            )
+                            .expect("Infallible");
+                            let path = path_with_position.path_like;
+                            if let Some(row) = path_with_position.row {
+                                if path.is_file() {
+                                    let row = row.saturating_sub(1);
+                                    let col =
+                                        path_with_position.column.unwrap_or(0).saturating_sub(1);
+                                    caret_positions.insert(path.clone(), Point::new(row, col));
+                                }
+                            }
+                            Some(path)
+                        })
+                        .collect()
+                };
+
+                let mut errored = false;
+                match cx
+                    .update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
+                    .await
+                {
+                    Ok((workspace, items)) => {
+                        let mut item_release_futures = Vec::new();
+
+                        for (item, path) in items.into_iter().zip(&paths) {
+                            match item {
+                                Some(Ok(item)) => {
+                                    if let Some(point) = caret_positions.remove(path) {
+                                        if let Some(active_editor) = item.downcast::<Editor>() {
+                                            active_editor
+                                                .downgrade()
+                                                .update(&mut cx, |editor, cx| {
+                                                    let snapshot =
+                                                        editor.snapshot(cx).display_snapshot;
+                                                    let point = snapshot
+                                                        .buffer_snapshot
+                                                        .clip_point(point, Bias::Left);
+                                                    editor.change_selections(
+                                                        Some(Autoscroll::center()),
+                                                        cx,
+                                                        |s| s.select_ranges([point..point]),
+                                                    );
+                                                })
+                                                .log_err();
+                                        }
+                                    }
+
+                                    let released = oneshot::channel();
+                                    cx.update(|cx| {
+                                        item.on_release(
+                                            cx,
+                                            Box::new(move |_| {
+                                                let _ = released.0.send(());
+                                            }),
+                                        )
+                                        .detach();
+                                    });
+                                    item_release_futures.push(released.1);
+                                }
+                                Some(Err(err)) => {
+                                    responses
+                                        .send(CliResponse::Stderr {
+                                            message: format!("error opening {:?}: {}", path, err),
+                                        })
+                                        .log_err();
+                                    errored = true;
+                                }
+                                None => {}
+                            }
+                        }
+
+                        if wait {
+                            let background = cx.background();
+                            let wait = async move {
+                                if paths.is_empty() {
+                                    let (done_tx, done_rx) = oneshot::channel();
+                                    if let Some(workspace) = workspace.upgrade(&cx) {
+                                        let _subscription = cx.update(|cx| {
+                                            cx.observe_release(&workspace, move |_, _| {
+                                                let _ = done_tx.send(());
+                                            })
+                                        });
+                                        drop(workspace);
+                                        let _ = done_rx.await;
+                                    }
+                                } else {
+                                    let _ =
+                                        futures::future::try_join_all(item_release_futures).await;
+                                };
+                            }
+                            .fuse();
+                            futures::pin_mut!(wait);
+
+                            loop {
+                                // Repeatedly check if CLI is still open to avoid wasting resources
+                                // waiting for files or workspaces to close.
+                                let mut timer = background.timer(Duration::from_secs(1)).fuse();
+                                futures::select_biased! {
+                                    _ = wait => break,
+                                    _ = timer => {
+                                        if responses.send(CliResponse::Ping).is_err() {
+                                            break;
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    Err(error) => {
+                        errored = true;
+                        responses
+                            .send(CliResponse::Stderr {
+                                message: format!("error opening {:?}: {}", paths, error),
+                            })
+                            .log_err();
+                    }
+                }
+
+                responses
+                    .send(CliResponse::Exit {
+                        status: i32::from(errored),
+                    })
+                    .log_err();
+            }
+        }
+    }
+}

crates/zed/src/zed.rs 🔗

@@ -2,6 +2,7 @@ pub mod assets;
 pub mod languages;
 pub mod menus;
 pub mod only_instance;
+pub mod open_listener;
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
 
@@ -28,6 +29,7 @@ use gpui::{
     AppContext, AsyncAppContext, Task, ViewContext, WeakViewHandle,
 };
 pub use lsp;
+use open_listener::OpenListener;
 pub use project;
 use project_panel::ProjectPanel;
 use quick_action_bar::QuickActionBar;
@@ -87,6 +89,10 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
         },
     );
     cx.add_global_action(quit);
+    cx.add_global_action(move |action: &OpenZedURL, cx| {
+        cx.global::<Arc<OpenListener>>()
+            .open_urls(vec![action.url.clone()])
+    });
     cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
     cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
         theme::adjust_font_size(cx, |size| *size += 1.0)