From 82f5f364224f06d5802dd3bd204d9e75f9cebf69 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 24 May 2024 15:44:32 -0400 Subject: [PATCH] Allow defining slash commands in extensions (#12255) This PR adds initial support for defining slash commands for the Assistant from extensions. Slash commands are defined in an extension's `extension.toml`: ```toml [slash_commands.gleam-project] description = "Returns information about the current Gleam project." requires_argument = false ``` and then executed via the `run_slash_command` method on the `Extension` trait: ```rs impl Extension for GleamExtension { // ... fn run_slash_command( &self, command: SlashCommand, _argument: Option, worktree: &zed::Worktree, ) -> Result, String> { match command.name.as_str() { "gleam-project" => Ok(Some("Yayyy".to_string())), command => Err(format!("unknown slash command: \"{command}\"")), } } } ``` Release Notes: - N/A --- Cargo.lock | 4 +- crates/assistant/src/assistant.rs | 1 + crates/assistant/src/assistant_panel.rs | 61 ++++++++++++- .../src/slash_command/current_file_command.rs | 9 +- .../src/slash_command/file_command.rs | 8 +- .../src/slash_command/prompt_command.rs | 8 +- crates/assistant_slash_command/Cargo.toml | 1 + .../src/assistant_slash_command.rs | 13 ++- crates/extension/Cargo.toml | 1 + crates/extension/src/extension_manifest.rs | 9 ++ .../extension/src/extension_slash_command.rs | 85 +++++++++++++++++++ crates/extension/src/extension_store.rs | 20 +++++ crates/extension/src/extension_store_test.rs | 9 ++ crates/extension/src/wasm_host/wit.rs | 18 +++- .../src/wasm_host/wit/since_v0_0_7.rs | 3 + crates/extension_api/src/extension_api.rs | 21 +++++ .../wit/since_v0.0.7/extension.wit | 4 + .../wit/since_v0.0.7/slash-command.wit | 11 +++ crates/project/src/project.rs | 8 +- extensions/gleam/Cargo.toml | 2 +- extensions/gleam/extension.toml | 4 + extensions/gleam/src/gleam.rs | 24 +++++- 22 files changed, 310 insertions(+), 14 deletions(-) create mode 100644 crates/extension/src/extension_slash_command.rs create mode 100644 crates/extension_api/wit/since_v0.0.7/slash-command.wit diff --git a/Cargo.lock b/Cargo.lock index 4d4ffe0251..9844fb5b1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,6 +436,7 @@ dependencies = [ "derive_more", "futures 0.3.28", "gpui", + "language", "parking_lot", ] @@ -3784,6 +3785,7 @@ name = "extension" version = "0.1.0" dependencies = [ "anyhow", + "assistant_slash_command", "async-compression", "async-tar", "async-trait", @@ -13249,7 +13251,7 @@ dependencies = [ name = "zed_gleam" version = "0.1.3" dependencies = [ - "zed_extension_api 0.0.6", + "zed_extension_api 0.0.7", ] [[package]] diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 3ef2d09770..da1be612c5 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -237,6 +237,7 @@ pub fn init(client: Arc, cx: &mut AppContext) { cx.set_global(Assistant::default()); AssistantSettings::register(cx); completion_provider::init(client, cx); + assistant_slash_command::init(cx); assistant_panel::init(cx); CommandPaletteFilter::update_global(cx, |filter, _cx| { diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index fa8054d5f4..41fc176f28 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -43,13 +43,14 @@ use gpui::{ UniformListScrollHandle, View, ViewContext, VisualContext, WeakModel, WeakView, WhiteSpace, WindowContext, }; +use language::LspAdapterDelegate; use language::{ language_settings::SoftWrap, AutoindentMode, Buffer, BufferSnapshot, LanguageRegistry, OffsetRangeExt as _, Point, ToOffset as _, ToPoint as _, }; use multi_buffer::MultiBufferRow; use parking_lot::Mutex; -use project::{Project, ProjectTransaction}; +use project::{Project, ProjectLspAdapterDelegate, ProjectTransaction}; use search::{buffer_search::DivRegistrar, BufferSearchBar}; use settings::Settings; use std::{ @@ -205,8 +206,7 @@ impl AssistantPanel { }) .detach(); - let slash_command_registry = SlashCommandRegistry::new(); - + let slash_command_registry = SlashCommandRegistry::global(cx); let window = cx.window_handle().downcast::(); slash_command_registry.register_command(file_command::FileSlashCommand::new( @@ -1129,6 +1129,13 @@ impl AssistantPanel { let slash_commands = self.slash_commands.clone(); let languages = self.languages.clone(); let telemetry = self.telemetry.clone(); + + let lsp_adapter_delegate = workspace + .update(cx, |workspace, cx| { + make_lsp_adapter_delegate(workspace.project(), cx) + }) + .log_err(); + cx.spawn(|this, mut cx| async move { let saved_conversation = SavedConversation::load(&path, fs.as_ref()).await?; let model = this.update(&mut cx, |this, _| this.model.clone())?; @@ -1139,6 +1146,7 @@ impl AssistantPanel { languages, slash_commands, Some(telemetry), + lsp_adapter_delegate, &mut cx, ) .await?; @@ -1484,6 +1492,7 @@ pub struct Conversation { telemetry: Option>, slash_command_registry: Arc, language_registry: Arc, + lsp_adapter_delegate: Option>, } impl EventEmitter for Conversation {} @@ -1494,6 +1503,7 @@ impl Conversation { language_registry: Arc, slash_command_registry: Arc, telemetry: Option>, + lsp_adapter_delegate: Option>, cx: &mut ModelContext, ) -> Self { let buffer = cx.new_model(|cx| { @@ -1526,6 +1536,7 @@ impl Conversation { telemetry, slash_command_registry, language_registry, + lsp_adapter_delegate, }; let message = MessageAnchor { @@ -1569,6 +1580,7 @@ impl Conversation { } } + #[allow(clippy::too_many_arguments)] async fn deserialize( saved_conversation: SavedConversation, model: LanguageModel, @@ -1576,6 +1588,7 @@ impl Conversation { language_registry: Arc, slash_command_registry: Arc, telemetry: Option>, + lsp_adapter_delegate: Option>, cx: &mut AsyncAppContext, ) -> Result> { let id = match saved_conversation.id { @@ -1635,6 +1648,7 @@ impl Conversation { telemetry, language_registry, slash_command_registry, + lsp_adapter_delegate, }; this.set_language(cx); this.reparse_edit_suggestions(cx); @@ -1850,7 +1864,13 @@ impl Conversation { buffer.anchor_after(offset)..buffer.anchor_before(line_end_offset); let argument = call.argument.map(|range| &line[range]); - let invocation = command.run(argument, cx); + let invocation = command.run( + argument, + this.lsp_adapter_delegate + .clone() + .expect("no LspAdapterDelegate present when invoking command"), + cx, + ); new_calls.push(SlashCommandCall { name, @@ -2728,12 +2748,16 @@ impl ConversationEditor { cx: &mut ViewContext, ) -> Self { let telemetry = workspace.read(cx).client().telemetry().clone(); + let project = workspace.read(cx).project().clone(); + let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx); + let conversation = cx.new_model(|cx| { Conversation::new( model, language_registry, slash_command_registry, Some(telemetry), + Some(lsp_adapter_delegate), cx, ) }); @@ -3907,6 +3931,20 @@ fn merge_ranges(ranges: &mut Vec>, buffer: &MultiBufferSnapshot) { } } +fn make_lsp_adapter_delegate( + project: &Model, + cx: &mut AppContext, +) -> Arc { + project.update(cx, |project, cx| { + // TODO: Find the right worktree. + let worktree = project + .worktrees() + .next() + .expect("expected at least one worktree"); + ProjectLspAdapterDelegate::new(project, &worktree, cx) + }) +} + #[cfg(test)] mod tests { use std::{cell::RefCell, path::Path, rc::Rc}; @@ -3935,6 +3973,7 @@ mod tests { registry, Default::default(), None, + None, cx, ) }); @@ -4074,6 +4113,7 @@ mod tests { registry, Default::default(), None, + None, cx, ) }); @@ -4180,6 +4220,7 @@ mod tests { registry, Default::default(), None, + None, cx, ) }); @@ -4292,6 +4333,15 @@ mod tests { prompt_library.clone(), )); + let lsp_adapter_delegate = project.update(cx, |project, cx| { + // TODO: Find the right worktree. + let worktree = project + .worktrees() + .next() + .expect("expected at least one worktree"); + ProjectLspAdapterDelegate::new(project, &worktree, cx) + }); + let registry = Arc::new(LanguageRegistry::test(cx.executor())); let conversation = cx.new_model(|cx| { Conversation::new( @@ -4299,6 +4349,7 @@ mod tests { registry.clone(), slash_command_registry, None, + Some(lsp_adapter_delegate), cx, ) }); @@ -4599,6 +4650,7 @@ mod tests { registry.clone(), Default::default(), None, + None, cx, ) }); @@ -4642,6 +4694,7 @@ mod tests { registry.clone(), Default::default(), None, + None, &mut cx.to_async(), ) .await diff --git a/crates/assistant/src/slash_command/current_file_command.rs b/crates/assistant/src/slash_command/current_file_command.rs index 752e599e92..5f55253557 100644 --- a/crates/assistant/src/slash_command/current_file_command.rs +++ b/crates/assistant/src/slash_command/current_file_command.rs @@ -1,3 +1,4 @@ +use std::sync::Arc; use std::{borrow::Cow, cell::Cell, rc::Rc}; use anyhow::{anyhow, Result}; @@ -5,6 +6,7 @@ use collections::HashMap; use editor::Editor; use futures::channel::oneshot; use gpui::{AppContext, Entity, Subscription, Task, WindowHandle}; +use language::LspAdapterDelegate; use workspace::{Event as WorkspaceEvent, Workspace}; use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation}; @@ -41,7 +43,12 @@ impl SlashCommand for CurrentFileSlashCommand { false } - fn run(&self, _argument: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation { + fn run( + self: Arc, + _argument: Option<&str>, + _delegate: Arc, + cx: &mut AppContext, + ) -> SlashCommandInvocation { let (invalidate_tx, invalidate_rx) = oneshot::channel(); let invalidate_tx = Rc::new(Cell::new(Some(invalidate_tx))); let mut subscriptions: Vec = Vec::new(); diff --git a/crates/assistant/src/slash_command/file_command.rs b/crates/assistant/src/slash_command/file_command.rs index c8d1aac6e3..e9b9b4060b 100644 --- a/crates/assistant/src/slash_command/file_command.rs +++ b/crates/assistant/src/slash_command/file_command.rs @@ -3,6 +3,7 @@ use anyhow::Result; use futures::channel::oneshot; use fuzzy::PathMatch; use gpui::{AppContext, Model, Task}; +use language::LspAdapterDelegate; use project::{PathMatchCandidateSet, Project}; use std::{ path::Path, @@ -96,7 +97,12 @@ impl SlashCommand for FileSlashCommand { }) } - fn run(&self, argument: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation { + fn run( + self: Arc, + argument: Option<&str>, + _delegate: Arc, + cx: &mut AppContext, + ) -> SlashCommandInvocation { let project = self.project.read(cx); let Some(argument) = argument else { return SlashCommandInvocation { diff --git a/crates/assistant/src/slash_command/prompt_command.rs b/crates/assistant/src/slash_command/prompt_command.rs index 27c123f35c..f3aef558d2 100644 --- a/crates/assistant/src/slash_command/prompt_command.rs +++ b/crates/assistant/src/slash_command/prompt_command.rs @@ -4,6 +4,7 @@ use anyhow::{anyhow, Context, Result}; use futures::channel::oneshot; use fuzzy::StringMatchCandidate; use gpui::{AppContext, Task}; +use language::LspAdapterDelegate; use std::sync::{atomic::AtomicBool, Arc}; pub(crate) struct PromptSlashCommand { @@ -65,7 +66,12 @@ impl SlashCommand for PromptSlashCommand { }) } - fn run(&self, title: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation { + fn run( + self: Arc, + title: Option<&str>, + _delegate: Arc, + cx: &mut AppContext, + ) -> SlashCommandInvocation { let Some(title) = title else { return SlashCommandInvocation { output: Task::ready(Err(anyhow!("missing prompt name"))), diff --git a/crates/assistant_slash_command/Cargo.toml b/crates/assistant_slash_command/Cargo.toml index 467f4fad2d..a30e9ba0a0 100644 --- a/crates/assistant_slash_command/Cargo.toml +++ b/crates/assistant_slash_command/Cargo.toml @@ -17,4 +17,5 @@ collections.workspace = true derive_more.workspace = true futures.workspace = true gpui.workspace = true +language.workspace = true parking_lot.workspace = true diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index fea430c582..dcdf446673 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use anyhow::Result; use futures::channel::oneshot; use gpui::{AppContext, Task}; +use language::LspAdapterDelegate; pub use slash_command_registry::*; @@ -23,7 +24,17 @@ pub trait SlashCommand: 'static + Send + Sync { cx: &mut AppContext, ) -> Task>>; fn requires_argument(&self) -> bool; - fn run(&self, argument: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation; + fn run( + self: Arc, + argument: Option<&str>, + // TODO: We're just using the `LspAdapterDelegate` here because that is + // what the extension API is already expecting. + // + // It may be that `LspAdapterDelegate` needs a more general name, or + // perhaps another kind of delegate is needed here. + delegate: Arc, + cx: &mut AppContext, + ) -> SlashCommandInvocation; } pub struct SlashCommandInvocation { diff --git a/crates/extension/Cargo.toml b/crates/extension/Cargo.toml index b517c14110..24ce269ea0 100644 --- a/crates/extension/Cargo.toml +++ b/crates/extension/Cargo.toml @@ -14,6 +14,7 @@ doctest = false [dependencies] anyhow.workspace = true +assistant_slash_command.workspace = true async-compression.workspace = true async-tar.workspace = true async-trait.workspace = true diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index bd315f0b5f..28cc796538 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/crates/extension/src/extension_manifest.rs @@ -74,6 +74,8 @@ pub struct ExtensionManifest { pub grammars: BTreeMap, GrammarManifestEntry>, #[serde(default)] pub language_servers: BTreeMap, + #[serde(default)] + pub slash_commands: BTreeMap, SlashCommandManifestEntry>, } #[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)] @@ -128,6 +130,12 @@ impl LanguageServerManifestEntry { } } +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub struct SlashCommandManifestEntry { + pub description: String, + pub requires_argument: bool, +} + impl ExtensionManifest { pub async fn load(fs: Arc, extension_dir: &Path) -> Result { let extension_name = extension_dir @@ -190,5 +198,6 @@ fn manifest_from_old_manifest( .map(|grammar_name| (grammar_name, Default::default())) .collect(), language_servers: Default::default(), + slash_commands: BTreeMap::default(), } } diff --git a/crates/extension/src/extension_slash_command.rs b/crates/extension/src/extension_slash_command.rs new file mode 100644 index 0000000000..8f98c414d0 --- /dev/null +++ b/crates/extension/src/extension_slash_command.rs @@ -0,0 +1,85 @@ +use std::sync::atomic::AtomicBool; +use std::sync::Arc; + +use anyhow::{anyhow, Result}; +use assistant_slash_command::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation}; +use futures::channel::oneshot; +use futures::FutureExt; +use gpui::{AppContext, Task}; +use language::LspAdapterDelegate; +use wasmtime_wasi::WasiView; + +use crate::wasm_host::{WasmExtension, WasmHost}; + +pub struct ExtensionSlashCommand { + pub(crate) extension: WasmExtension, + #[allow(unused)] + pub(crate) host: Arc, + pub(crate) command: crate::wit::SlashCommand, +} + +impl SlashCommand for ExtensionSlashCommand { + fn name(&self) -> String { + self.command.name.clone() + } + + fn description(&self) -> String { + self.command.description.clone() + } + + fn requires_argument(&self) -> bool { + self.command.requires_argument + } + + fn complete_argument( + &self, + _query: String, + _cancel: Arc, + _cx: &mut AppContext, + ) -> Task>> { + Task::ready(Ok(Vec::new())) + } + + fn run( + self: Arc, + argument: Option<&str>, + delegate: Arc, + cx: &mut AppContext, + ) -> SlashCommandInvocation { + let argument = argument.map(|arg| arg.to_string()); + + let output = cx.background_executor().spawn(async move { + let output = self + .extension + .call({ + let this = self.clone(); + move |extension, store| { + async move { + let resource = store.data_mut().table().push(delegate)?; + let output = extension + .call_run_slash_command( + store, + &this.command, + argument.as_deref(), + resource, + ) + .await? + .map_err(|e| anyhow!("{}", e))?; + + anyhow::Ok(output) + } + .boxed() + } + }) + .await?; + + output.ok_or_else(|| anyhow!("no output from command: {}", self.command.name)) + }); + + SlashCommandInvocation { + output, + invalidated: oneshot::channel().1, + cleanup: SlashCommandCleanup::default(), + } + } +} diff --git a/crates/extension/src/extension_store.rs b/crates/extension/src/extension_store.rs index d0f6d0e1fc..4c18af6781 100644 --- a/crates/extension/src/extension_store.rs +++ b/crates/extension/src/extension_store.rs @@ -2,14 +2,17 @@ pub mod extension_builder; mod extension_lsp_adapter; mod extension_manifest; mod extension_settings; +mod extension_slash_command; mod wasm_host; #[cfg(test)] mod extension_store_test; use crate::extension_manifest::SchemaVersion; +use crate::extension_slash_command::ExtensionSlashCommand; use crate::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host::wit}; use anyhow::{anyhow, bail, Context as _, Result}; +use assistant_slash_command::SlashCommandRegistry; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use client::{telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse}; @@ -107,6 +110,7 @@ pub struct ExtensionStore { index_path: PathBuf, language_registry: Arc, theme_registry: Arc, + slash_command_registry: Arc, modified_extensions: HashSet>, wasm_host: Arc, wasm_extensions: Vec<(Arc, WasmExtension)>, @@ -183,6 +187,7 @@ pub fn init( node_runtime, language_registry, theme_registry, + SlashCommandRegistry::global(cx), cx, ) }); @@ -215,6 +220,7 @@ impl ExtensionStore { node_runtime: Arc, language_registry: Arc, theme_registry: Arc, + slash_command_registry: Arc, cx: &mut ModelContext, ) -> Self { let work_dir = extensions_dir.join("work"); @@ -245,6 +251,7 @@ impl ExtensionStore { telemetry, language_registry, theme_registry, + slash_command_registry, reload_tx, tasks: Vec::new(), }; @@ -1169,6 +1176,19 @@ impl ExtensionStore { ); } } + + for (slash_command_name, slash_command) in &manifest.slash_commands { + this.slash_command_registry + .register_command(ExtensionSlashCommand { + command: crate::wit::SlashCommand { + name: slash_command_name.to_string(), + description: slash_command.description.to_string(), + requires_argument: slash_command.requires_argument, + }, + extension: wasm_extension.clone(), + host: this.wasm_host.clone(), + }); + } } this.wasm_extensions.extend(wasm_extensions); ThemeSettings::reload_current_theme(cx) diff --git a/crates/extension/src/extension_store_test.rs b/crates/extension/src/extension_store_test.rs index af00fc1859..ebbfb14cdd 100644 --- a/crates/extension/src/extension_store_test.rs +++ b/crates/extension/src/extension_store_test.rs @@ -5,6 +5,7 @@ use crate::{ ExtensionIndexThemeEntry, ExtensionManifest, ExtensionStore, GrammarManifestEntry, RELOAD_DEBOUNCE_DURATION, }; +use assistant_slash_command::SlashCommandRegistry; use async_compression::futures::bufread::GzipEncoder; use collections::BTreeMap; use fs::{FakeFs, Fs, RealFs}; @@ -156,6 +157,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { .into_iter() .collect(), language_servers: BTreeMap::default(), + slash_commands: BTreeMap::default(), }), dev: false, }, @@ -179,6 +181,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { languages: Default::default(), grammars: BTreeMap::default(), language_servers: BTreeMap::default(), + slash_commands: BTreeMap::default(), }), dev: false, }, @@ -250,6 +253,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { let language_registry = Arc::new(LanguageRegistry::test(cx.executor())); let theme_registry = Arc::new(ThemeRegistry::new(Box::new(()))); + let slash_command_registry = SlashCommandRegistry::new(); let node_runtime = FakeNodeRuntime::new(); let store = cx.new_model(|cx| { @@ -262,6 +266,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { node_runtime.clone(), language_registry.clone(), theme_registry.clone(), + slash_command_registry.clone(), cx, ) }); @@ -333,6 +338,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { languages: Default::default(), grammars: BTreeMap::default(), language_servers: BTreeMap::default(), + slash_commands: BTreeMap::default(), }), dev: false, }, @@ -382,6 +388,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { node_runtime.clone(), language_registry.clone(), theme_registry.clone(), + slash_command_registry, cx, ) }); @@ -460,6 +467,7 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) { let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let theme_registry = Arc::new(ThemeRegistry::new(Box::new(()))); + let slash_command_registry = SlashCommandRegistry::new(); let node_runtime = FakeNodeRuntime::new(); let mut status_updates = language_registry.language_server_binary_statuses(); @@ -541,6 +549,7 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) { node_runtime, language_registry.clone(), theme_registry.clone(), + slash_command_registry, cx, ) }); diff --git a/crates/extension/src/wasm_host/wit.rs b/crates/extension/src/wasm_host/wit.rs index 57d53dbfae..57a0e140a3 100644 --- a/crates/extension/src/wasm_host/wit.rs +++ b/crates/extension/src/wasm_host/wit.rs @@ -19,7 +19,7 @@ use wasmtime::{ pub use latest::CodeLabelSpanLiteral; pub use latest::{ zed::extension::lsp::{Completion, CompletionKind, InsertTextFormat, Symbol, SymbolKind}, - CodeLabel, CodeLabelSpan, Command, Range, + CodeLabel, CodeLabelSpan, Command, Range, SlashCommand, }; pub use since_v0_0_4::LanguageServerConfig; @@ -255,6 +255,22 @@ impl Extension { Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())), } } + + pub async fn call_run_slash_command( + &self, + store: &mut Store, + command: &SlashCommand, + argument: Option<&str>, + resource: Resource>, + ) -> Result, String>> { + match self { + Extension::V007(ext) => { + ext.call_run_slash_command(store, command, argument, resource) + .await + } + Extension::V001(_) | Extension::V004(_) | Extension::V006(_) => Ok(Ok(None)), + } + } } trait ToWasmtimeResult { diff --git a/crates/extension/src/wasm_host/wit/since_v0_0_7.rs b/crates/extension/src/wasm_host/wit/since_v0_0_7.rs index b631113d4b..35874b6cd6 100644 --- a/crates/extension/src/wasm_host/wit/since_v0_0_7.rs +++ b/crates/extension/src/wasm_host/wit/since_v0_0_7.rs @@ -222,6 +222,9 @@ impl platform::Host for WasmState { } } +#[async_trait] +impl slash_command::Host for WasmState {} + #[async_trait] impl ExtensionImports for WasmState { async fn get_settings( diff --git a/crates/extension_api/src/extension_api.rs b/crates/extension_api/src/extension_api.rs index 617a9337e8..71b9b2ebea 100644 --- a/crates/extension_api/src/extension_api.rs +++ b/crates/extension_api/src/extension_api.rs @@ -24,6 +24,7 @@ pub use wit::{ npm_package_latest_version, }, zed::extension::platform::{current_platform, Architecture, Os}, + zed::extension::slash_command::SlashCommand, CodeLabel, CodeLabelSpan, CodeLabelSpanLiteral, Command, DownloadedFileType, EnvVars, LanguageServerInstallationStatus, Range, Worktree, }; @@ -104,6 +105,16 @@ pub trait Extension: Send + Sync { ) -> Option { None } + + /// Runs the given slash command. + fn run_slash_command( + &self, + _command: SlashCommand, + _argument: Option, + _worktree: &Worktree, + ) -> Result, String> { + Ok(None) + } } /// Registers the provided type as a Zed extension. @@ -139,6 +150,8 @@ static mut EXTENSION: Option> = None; pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "/version_bytes")); mod wit { + #![allow(clippy::too_many_arguments)] + wit_bindgen::generate!({ skip: ["init-extension"], path: "./wit/since_v0.0.7", @@ -209,6 +222,14 @@ impl wit::Guest for Component { } Ok(labels) } + + fn run_slash_command( + command: SlashCommand, + argument: Option, + worktree: &Worktree, + ) -> Result, String> { + extension().run_slash_command(command, argument, worktree) + } } /// The ID of a language server. diff --git a/crates/extension_api/wit/since_v0.0.7/extension.wit b/crates/extension_api/wit/since_v0.0.7/extension.wit index 2f42cc0365..6758e5f691 100644 --- a/crates/extension_api/wit/since_v0.0.7/extension.wit +++ b/crates/extension_api/wit/since_v0.0.7/extension.wit @@ -6,6 +6,7 @@ world extension { import nodejs; use lsp.{completion, symbol}; + use slash-command.{slash-command}; /// Initializes the extension. export init-extension: func(); @@ -127,4 +128,7 @@ world extension { export labels-for-completions: func(language-server-id: string, completions: list) -> result>, string>; export labels-for-symbols: func(language-server-id: string, symbols: list) -> result>, string>; + + /// Runs the provided slash command. + export run-slash-command: func(command: slash-command, argument: option, worktree: borrow) -> result, string>; } diff --git a/crates/extension_api/wit/since_v0.0.7/slash-command.wit b/crates/extension_api/wit/since_v0.0.7/slash-command.wit new file mode 100644 index 0000000000..4dedfde07a --- /dev/null +++ b/crates/extension_api/wit/since_v0.0.7/slash-command.wit @@ -0,0 +1,11 @@ +interface slash-command { + /// A slash command for use in the Assistant. + record slash-command { + /// The name of the slash command. + name: string, + /// The description of the slash command. + description: string, + /// Whether this slash command requires an argument. + requires-argument: bool, + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1465b8b46e..34c8dec394 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -11175,7 +11175,7 @@ impl> From<(WorktreeId, P)> for ProjectPath { } } -struct ProjectLspAdapterDelegate { +pub struct ProjectLspAdapterDelegate { project: WeakModel, worktree: worktree::Snapshot, fs: Arc, @@ -11185,7 +11185,11 @@ struct ProjectLspAdapterDelegate { } impl ProjectLspAdapterDelegate { - fn new(project: &Project, worktree: &Model, cx: &ModelContext) -> Arc { + pub fn new( + project: &Project, + worktree: &Model, + cx: &ModelContext, + ) -> Arc { Arc::new(Self { project: cx.weak_model(), worktree: worktree.read(cx).snapshot(), diff --git a/extensions/gleam/Cargo.toml b/extensions/gleam/Cargo.toml index f967ac7030..04e0c40497 100644 --- a/extensions/gleam/Cargo.toml +++ b/extensions/gleam/Cargo.toml @@ -13,4 +13,4 @@ path = "src/gleam.rs" crate-type = ["cdylib"] [dependencies] -zed_extension_api = "0.0.6" +zed_extension_api = { path = "../../crates/extension_api" } diff --git a/extensions/gleam/extension.toml b/extensions/gleam/extension.toml index b12bb28f0b..642db17a74 100644 --- a/extensions/gleam/extension.toml +++ b/extensions/gleam/extension.toml @@ -13,3 +13,7 @@ language = "Gleam" [grammars.gleam] repository = "https://github.com/gleam-lang/tree-sitter-gleam" commit = "8432ffe32ccd360534837256747beb5b1c82fca1" + +[slash_commands.gleam-project] +description = "Returns information about the current Gleam project." +requires_argument = false diff --git a/extensions/gleam/src/gleam.rs b/extensions/gleam/src/gleam.rs index 8deef729d4..d75ac8bc25 100644 --- a/extensions/gleam/src/gleam.rs +++ b/extensions/gleam/src/gleam.rs @@ -1,6 +1,6 @@ use std::fs; use zed::lsp::CompletionKind; -use zed::{CodeLabel, CodeLabelSpan, LanguageServerId}; +use zed::{CodeLabel, CodeLabelSpan, LanguageServerId, SlashCommand}; use zed_extension_api::{self as zed, Result}; struct GleamExtension { @@ -142,6 +142,28 @@ impl zed::Extension for GleamExtension { code, }) } + + fn run_slash_command( + &self, + command: SlashCommand, + _argument: Option, + worktree: &zed::Worktree, + ) -> Result, String> { + match command.name.as_str() { + "gleam-project" => { + let mut message = String::new(); + message.push_str("You are in a Gleam project.\n"); + + if let Some(gleam_toml) = worktree.read_text_file("gleam.toml").ok() { + message.push_str("The `gleam.toml` is as follows:\n"); + message.push_str(&gleam_toml); + } + + Ok(Some(message)) + } + command => Err(format!("unknown slash command: \"{command}\"")), + } + } } zed::register_extension!(GleamExtension);