Add zed://extension/{id} links (#34492)

Conrad Irwin created

Release Notes:

- Add zed://extension/{id} links to open the extensions UI with a
specific extension

Change summary

crates/agent_ui/src/agent_configuration.rs       |  1 
crates/agent_ui/src/agent_panel.rs               |  1 
crates/debugger_ui/src/debugger_panel.rs         |  1 
crates/extensions_ui/src/extensions_ui.rs        | 54 ++++++++++++++---
crates/theme_selector/src/icon_theme_selector.rs |  1 
crates/theme_selector/src/theme_selector.rs      |  1 
crates/zed/src/main.rs                           | 17 +++++
crates/zed/src/zed/open_listener.rs              |  3 +
crates/zed_actions/src/lib.rs                    |  3 +
9 files changed, 72 insertions(+), 10 deletions(-)

Detailed changes

crates/agent_ui/src/agent_panel.rs πŸ”—

@@ -1921,6 +1921,7 @@ impl AgentPanel {
                                 category_filter: Some(
                                     zed_actions::ExtensionCategoryFilter::ContextServers,
                                 ),
+                                id: None,
                             }),
                         )
                         .action("Add Custom Server…", Box::new(AddContextServer))

crates/extensions_ui/src/extensions_ui.rs πŸ”—

@@ -6,6 +6,7 @@ use std::sync::OnceLock;
 use std::time::Duration;
 use std::{ops::Range, sync::Arc};
 
+use anyhow::Context as _;
 use client::{ExtensionMetadata, ExtensionProvides};
 use collections::{BTreeMap, BTreeSet};
 use editor::{Editor, EditorElement, EditorStyle};
@@ -80,16 +81,24 @@ pub fn init(cx: &mut App) {
                         .find_map(|item| item.downcast::<ExtensionsPage>());
 
                     if let Some(existing) = existing {
-                        if provides_filter.is_some() {
-                            existing.update(cx, |extensions_page, cx| {
+                        existing.update(cx, |extensions_page, cx| {
+                            if provides_filter.is_some() {
                                 extensions_page.change_provides_filter(provides_filter, cx);
-                            });
-                        }
+                            }
+                            if let Some(id) = action.id.as_ref() {
+                                extensions_page.focus_extension(id, window, cx);
+                            }
+                        });
 
                         workspace.activate_item(&existing, true, true, window, cx);
                     } else {
-                        let extensions_page =
-                            ExtensionsPage::new(workspace, provides_filter, window, cx);
+                        let extensions_page = ExtensionsPage::new(
+                            workspace,
+                            provides_filter,
+                            action.id.as_deref(),
+                            window,
+                            cx,
+                        );
                         workspace.add_item_to_active_pane(
                             Box::new(extensions_page),
                             None,
@@ -287,6 +296,7 @@ impl ExtensionsPage {
     pub fn new(
         workspace: &Workspace,
         provides_filter: Option<ExtensionProvides>,
+        focus_extension_id: Option<&str>,
         window: &mut Window,
         cx: &mut Context<Workspace>,
     ) -> Entity<Self> {
@@ -317,6 +327,9 @@ impl ExtensionsPage {
             let query_editor = cx.new(|cx| {
                 let mut input = Editor::single_line(window, cx);
                 input.set_placeholder_text("Search extensions...", cx);
+                if let Some(id) = focus_extension_id {
+                    input.set_text(format!("id:{id}"), window, cx);
+                }
                 input
             });
             cx.subscribe(&query_editor, Self::on_query_change).detach();
@@ -340,7 +353,7 @@ impl ExtensionsPage {
                 scrollbar_state: ScrollbarState::new(scroll_handle),
             };
             this.fetch_extensions(
-                None,
+                this.search_query(cx),
                 Some(BTreeSet::from_iter(this.provides_filter)),
                 None,
                 cx,
@@ -464,9 +477,23 @@ impl ExtensionsPage {
             .cloned()
             .collect::<Vec<_>>();
 
-        let remote_extensions = extension_store.update(cx, |store, cx| {
-            store.fetch_extensions(search.as_deref(), provides_filter.as_ref(), cx)
-        });
+        let remote_extensions =
+            if let Some(id) = search.as_ref().and_then(|s| s.strip_prefix("id:")) {
+                let versions =
+                    extension_store.update(cx, |store, cx| store.fetch_extension_versions(id, cx));
+                cx.foreground_executor().spawn(async move {
+                    let versions = versions.await?;
+                    let latest = versions
+                        .into_iter()
+                        .max_by_key(|v| v.published_at)
+                        .context("no extension found")?;
+                    Ok(vec![latest])
+                })
+            } else {
+                extension_store.update(cx, |store, cx| {
+                    store.fetch_extensions(search.as_deref(), provides_filter.as_ref(), cx)
+                })
+            };
 
         cx.spawn(async move |this, cx| {
             let dev_extensions = if let Some(search) = search {
@@ -1165,6 +1192,13 @@ impl ExtensionsPage {
         self.refresh_feature_upsells(cx);
     }
 
+    pub fn focus_extension(&mut self, id: &str, window: &mut Window, cx: &mut Context<Self>) {
+        self.query_editor.update(cx, |editor, cx| {
+            editor.set_text(format!("id:{id}"), window, cx)
+        });
+        self.refresh_search(cx);
+    }
+
     pub fn change_provides_filter(
         &mut self,
         provides_filter: Option<ExtensionProvides>,

crates/zed/src/main.rs πŸ”—

@@ -746,6 +746,23 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
         return;
     }
 
+    if let Some(extension) = request.extension_id {
+        cx.spawn(async move |cx| {
+            let workspace = workspace::get_any_active_workspace(app_state, cx.clone()).await?;
+            workspace.update(cx, |_, window, cx| {
+                window.dispatch_action(
+                    Box::new(zed_actions::Extensions {
+                        category_filter: None,
+                        id: Some(extension),
+                    }),
+                    cx,
+                );
+            })
+        })
+        .detach_and_log_err(cx);
+        return;
+    }
+
     if let Some(connection_options) = request.ssh_connection {
         cx.spawn(async move |mut cx| {
             let paths: Vec<PathBuf> = request.open_paths.into_iter().map(PathBuf::from).collect();

crates/zed/src/zed/open_listener.rs πŸ”—

@@ -37,6 +37,7 @@ pub struct OpenRequest {
     pub join_channel: Option<u64>,
     pub ssh_connection: Option<SshConnectionOptions>,
     pub dock_menu_action: Option<usize>,
+    pub extension_id: Option<String>,
 }
 
 impl OpenRequest {
@@ -54,6 +55,8 @@ impl OpenRequest {
             } else if let Some(file) = url.strip_prefix("zed://ssh") {
                 let ssh_url = "ssh:/".to_string() + file;
                 this.parse_ssh_file_path(&ssh_url, cx)?
+            } else if let Some(file) = url.strip_prefix("zed://extension/") {
+                this.extension_id = Some(file.to_string())
             } else if url.starts_with("ssh://") {
                 this.parse_ssh_file_path(&url, cx)?
             } else if let Some(request_path) = parse_zed_link(&url, cx) {

crates/zed_actions/src/lib.rs πŸ”—

@@ -76,6 +76,9 @@ pub struct Extensions {
     /// Filters the extensions page down to extensions that are in the specified category.
     #[serde(default)]
     pub category_filter: Option<ExtensionCategoryFilter>,
+    /// Focuses just the extension with the specified ID.
+    #[serde(default)]
+    pub id: Option<String>,
 }
 
 /// Decreases the font size in the editor buffer.