Introduce `/symbols` command in assistant panel (#14360)

Max Brunsfeld , Antonio , Antonio Scandurra , and Nathan created

Release Notes:

- Added `/symbols` command in assistant panel.

---------

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Nathan <nathan@zed.dev>

Change summary

crates/assistant/src/assistant.rs                     |  5 
crates/assistant/src/slash_command.rs                 |  1 
crates/assistant/src/slash_command/now_command.rs     |  2 
crates/assistant/src/slash_command/symbols_command.rs | 89 +++++++++++++
crates/assistant/src/slash_command/term_command.rs    |  2 
crates/language/src/outline.rs                        |  2 
6 files changed, 96 insertions(+), 5 deletions(-)

Detailed changes

crates/assistant/src/assistant.rs 🔗

@@ -30,8 +30,8 @@ use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsStore};
 use slash_command::{
     active_command, default_command, diagnostics_command, docs_command, fetch_command,
-    file_command, now_command, project_command, prompt_command, search_command, tabs_command,
-    term_command,
+    file_command, now_command, project_command, prompt_command, search_command, symbols_command,
+    tabs_command, term_command,
 };
 use std::{
     fmt::{self, Display},
@@ -367,6 +367,7 @@ fn register_slash_commands(cx: &mut AppContext) {
     let slash_command_registry = SlashCommandRegistry::global(cx);
     slash_command_registry.register_command(file_command::FileSlashCommand, true);
     slash_command_registry.register_command(active_command::ActiveSlashCommand, true);
+    slash_command_registry.register_command(symbols_command::OutlineSlashCommand, true);
     slash_command_registry.register_command(tabs_command::TabsSlashCommand, true);
     slash_command_registry.register_command(project_command::ProjectSlashCommand, true);
     slash_command_registry.register_command(search_command::SearchSlashCommand, true);

crates/assistant/src/slash_command.rs 🔗

@@ -27,6 +27,7 @@ pub mod now_command;
 pub mod project_command;
 pub mod prompt_command;
 pub mod search_command;
+pub mod symbols_command;
 pub mod tabs_command;
 pub mod term_command;
 

crates/assistant/src/slash_command/now_command.rs 🔗

@@ -23,7 +23,7 @@ impl SlashCommand for NowSlashCommand {
     }
 
     fn menu_text(&self) -> String {
-        "Insert current date and time".into()
+        "Insert Current Date and Time".into()
     }
 
     fn requires_argument(&self) -> bool {

crates/assistant/src/slash_command/symbols_command.rs 🔗

@@ -0,0 +1,89 @@
+use super::{SlashCommand, SlashCommandOutput};
+use anyhow::{anyhow, Context as _, Result};
+use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
+use editor::Editor;
+use gpui::{AppContext, Task, WeakView};
+use language::LspAdapterDelegate;
+use std::sync::Arc;
+use std::{path::Path, sync::atomic::AtomicBool};
+use ui::{IconName, WindowContext};
+use workspace::Workspace;
+
+pub(crate) struct OutlineSlashCommand;
+
+impl SlashCommand for OutlineSlashCommand {
+    fn name(&self) -> String {
+        "symbols".into()
+    }
+
+    fn description(&self) -> String {
+        "insert symbols for active tab".into()
+    }
+
+    fn menu_text(&self) -> String {
+        "Insert Symbols for Active Tab".into()
+    }
+
+    fn complete_argument(
+        self: Arc<Self>,
+        _query: String,
+        _cancel: Arc<AtomicBool>,
+        _workspace: Option<WeakView<Workspace>>,
+        _cx: &mut AppContext,
+    ) -> Task<Result<Vec<ArgumentCompletion>>> {
+        Task::ready(Err(anyhow!("this command does not require argument")))
+    }
+
+    fn requires_argument(&self) -> bool {
+        false
+    }
+
+    fn run(
+        self: Arc<Self>,
+        _argument: Option<&str>,
+        workspace: WeakView<Workspace>,
+        _delegate: Arc<dyn LspAdapterDelegate>,
+        cx: &mut WindowContext,
+    ) -> Task<Result<SlashCommandOutput>> {
+        let output = workspace.update(cx, |workspace, cx| {
+            let Some(active_item) = workspace.active_item(cx) else {
+                return Task::ready(Err(anyhow!("no active tab")));
+            };
+            let Some(buffer) = active_item
+                .downcast::<Editor>()
+                .and_then(|editor| editor.read(cx).buffer().read(cx).as_singleton())
+            else {
+                return Task::ready(Err(anyhow!("active tab is not an editor")));
+            };
+
+            let snapshot = buffer.read(cx).snapshot();
+            let path = snapshot.resolve_file_path(cx, true);
+
+            cx.background_executor().spawn(async move {
+                let outline = snapshot
+                    .outline(None)
+                    .context("no symbols for active tab")?;
+
+                let path = path.as_deref().unwrap_or(Path::new("untitled"));
+                let mut outline_text = format!("Symbols for {}:\n", path.display());
+                for item in &outline.path_candidates {
+                    outline_text.push_str("- ");
+                    outline_text.push_str(&item.string);
+                    outline_text.push('\n');
+                }
+
+                Ok(SlashCommandOutput {
+                    sections: vec![SlashCommandOutputSection {
+                        range: 0..outline_text.len(),
+                        icon: IconName::ListTree,
+                        label: path.to_string_lossy().to_string().into(),
+                    }],
+                    text: outline_text,
+                    run_commands_in_text: false,
+                })
+            })
+        });
+
+        output.unwrap_or_else(|error| Task::ready(Err(error)))
+    }
+}

crates/language/src/outline.rs 🔗

@@ -12,7 +12,7 @@ use theme::{color_alpha, ActiveTheme, ThemeSettings};
 pub struct Outline<T> {
     pub items: Vec<OutlineItem<T>>,
     candidates: Vec<StringMatchCandidate>,
-    path_candidates: Vec<StringMatchCandidate>,
+    pub path_candidates: Vec<StringMatchCandidate>,
     path_candidate_prefixes: Vec<usize>,
 }