From e9f4facd39a80626ecfa8e930e7244e824027bc5 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Nov 2024 14:34:58 -0500 Subject: [PATCH] Extract `ExtensionSlashCommand` to `assistant_slash_command` crate (#20617) This PR extracts the `ExtensionSlashCommand` implementation to the `assistant_slash_command` crate. The slash command related methods have been added to the `Extension` trait. We also create separate data types for the slash command data within the `extension` crate so that we can talk about them without depending on the `extension_host` or `assistant_slash_command`. Release Notes: - N/A --- Cargo.lock | 3 + crates/assistant_slash_command/Cargo.toml | 3 + .../src/assistant_slash_command.rs | 4 +- .../src/extension_slash_command.rs | 143 ++++++++++++++++++ crates/extension/src/extension.rs | 15 ++ crates/extension/src/slash_command.rs | 43 ++++++ crates/extension_host/src/extension_host.rs | 10 +- crates/extension_host/src/wasm_host.rs | 52 ++++++- .../src/wasm_host/wit/since_v0_2_0.rs | 40 +++++ .../src/extension_registration_hooks.rs | 18 +-- .../src/extension_slash_command.rs | 138 ----------------- crates/extensions_ui/src/extensions_ui.rs | 1 - 12 files changed, 309 insertions(+), 161 deletions(-) create mode 100644 crates/assistant_slash_command/src/extension_slash_command.rs create mode 100644 crates/extension/src/slash_command.rs delete mode 100644 crates/extensions_ui/src/extension_slash_command.rs diff --git a/Cargo.lock b/Cargo.lock index b99b8933746e21dae6b2c51ee621767b0ea21d75..a601e16b0f642ec9ad76a4e3d63829b85a561f54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -459,8 +459,10 @@ name = "assistant_slash_command" version = "0.1.0" dependencies = [ "anyhow", + "async-trait", "collections", "derive_more", + "extension", "futures 0.3.30", "gpui", "language", @@ -469,6 +471,7 @@ dependencies = [ "pretty_assertions", "serde", "serde_json", + "ui", "workspace", ] diff --git a/crates/assistant_slash_command/Cargo.toml b/crates/assistant_slash_command/Cargo.toml index 9f862a3d26d10618e2b0def189275cdf3a7bd6fd..24d3478ea1e5677c233e3a39290efae0cad62c19 100644 --- a/crates/assistant_slash_command/Cargo.toml +++ b/crates/assistant_slash_command/Cargo.toml @@ -13,8 +13,10 @@ path = "src/assistant_slash_command.rs" [dependencies] anyhow.workspace = true +async-trait.workspace = true collections.workspace = true derive_more.workspace = true +extension.workspace = true futures.workspace = true gpui.workspace = true language.workspace = true @@ -22,6 +24,7 @@ language_model.workspace = true parking_lot.workspace = true serde.workspace = true serde_json.workspace = true +ui.workspace = true workspace.workspace = true [dev-dependencies] diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index fedf33a233eabdc4c22869f743f358d07a503a6f..3fb2dc66b24e05005685de36bc7c2fb024b005be 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -1,5 +1,8 @@ +mod extension_slash_command; mod slash_command_registry; +pub use crate::extension_slash_command::*; +pub use crate::slash_command_registry::*; use anyhow::Result; use futures::stream::{self, BoxStream}; use futures::StreamExt; @@ -7,7 +10,6 @@ use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, Wind use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt}; pub use language_model::Role; use serde::{Deserialize, Serialize}; -pub use slash_command_registry::*; use std::{ ops::Range, sync::{atomic::AtomicBool, Arc}, diff --git a/crates/assistant_slash_command/src/extension_slash_command.rs b/crates/assistant_slash_command/src/extension_slash_command.rs new file mode 100644 index 0000000000000000000000000000000000000000..bfb268806651f5e2274228087582d457fe404585 --- /dev/null +++ b/crates/assistant_slash_command/src/extension_slash_command.rs @@ -0,0 +1,143 @@ +use std::path::PathBuf; +use std::sync::{atomic::AtomicBool, Arc}; + +use anyhow::Result; +use async_trait::async_trait; +use extension::{Extension, WorktreeDelegate}; +use gpui::{Task, WeakView, WindowContext}; +use language::{BufferSnapshot, LspAdapterDelegate}; +use ui::prelude::*; +use workspace::Workspace; + +use crate::{ + ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, + SlashCommandResult, +}; + +/// An adapter that allows an [`LspAdapterDelegate`] to be used as a [`WorktreeDelegate`]. +struct WorktreeDelegateAdapter(Arc); + +#[async_trait] +impl WorktreeDelegate for WorktreeDelegateAdapter { + fn id(&self) -> u64 { + self.0.worktree_id().to_proto() + } + + fn root_path(&self) -> String { + self.0.worktree_root_path().to_string_lossy().to_string() + } + + async fn read_text_file(&self, path: PathBuf) -> Result { + self.0.read_text_file(path).await + } + + async fn which(&self, binary_name: String) -> Option { + self.0 + .which(binary_name.as_ref()) + .await + .map(|path| path.to_string_lossy().to_string()) + } + + async fn shell_env(&self) -> Vec<(String, String)> { + self.0.shell_env().await.into_iter().collect() + } +} + +pub struct ExtensionSlashCommand { + extension: Arc, + command: extension::SlashCommand, +} + +impl ExtensionSlashCommand { + pub fn new(extension: Arc, command: extension::SlashCommand) -> Self { + Self { extension, command } + } +} + +impl SlashCommand for ExtensionSlashCommand { + fn name(&self) -> String { + self.command.name.clone() + } + + fn description(&self) -> String { + self.command.description.clone() + } + + fn menu_text(&self) -> String { + self.command.tooltip_text.clone() + } + + fn requires_argument(&self) -> bool { + self.command.requires_argument + } + + fn complete_argument( + self: Arc, + arguments: &[String], + _cancel: Arc, + _workspace: Option>, + cx: &mut WindowContext, + ) -> Task>> { + let command = self.command.clone(); + let arguments = arguments.to_owned(); + cx.background_executor().spawn(async move { + let completions = self + .extension + .complete_slash_command_argument(command, arguments) + .await?; + + anyhow::Ok( + completions + .into_iter() + .map(|completion| ArgumentCompletion { + label: completion.label.into(), + new_text: completion.new_text, + replace_previous_arguments: false, + after_completion: completion.run_command.into(), + }) + .collect(), + ) + }) + } + + fn run( + self: Arc, + arguments: &[String], + _context_slash_command_output_sections: &[SlashCommandOutputSection], + _context_buffer: BufferSnapshot, + _workspace: WeakView, + delegate: Option>, + cx: &mut WindowContext, + ) -> Task { + let command = self.command.clone(); + let arguments = arguments.to_owned(); + let output = cx.background_executor().spawn(async move { + let delegate = + delegate.map(|delegate| Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _); + let output = self + .extension + .run_slash_command(command, arguments, delegate) + .await?; + + anyhow::Ok(output) + }); + cx.foreground_executor().spawn(async move { + let output = output.await?; + Ok(SlashCommandOutput { + text: output.text, + sections: output + .sections + .into_iter() + .map(|section| SlashCommandOutputSection { + range: section.range, + icon: IconName::Code, + label: section.label.into(), + metadata: None, + }) + .collect(), + run_commands_in_text: false, + } + .to_event_stream()) + }) + } +} diff --git a/crates/extension/src/extension.rs b/crates/extension/src/extension.rs index 19267e16ac70825233694cd02b92f5e080c244ec..0405c0ad2f945b743159612a64f381664eade13a 100644 --- a/crates/extension/src/extension.rs +++ b/crates/extension/src/extension.rs @@ -1,5 +1,6 @@ pub mod extension_builder; mod extension_manifest; +mod slash_command; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -10,6 +11,7 @@ use gpui::Task; use semantic_version::SemanticVersion; pub use crate::extension_manifest::*; +pub use crate::slash_command::*; #[async_trait] pub trait WorktreeDelegate: Send + Sync + 'static { @@ -32,6 +34,19 @@ pub trait Extension: Send + Sync + 'static { /// Returns the path to this extension's working directory. fn work_dir(&self) -> Arc; + async fn complete_slash_command_argument( + &self, + command: SlashCommand, + arguments: Vec, + ) -> Result>; + + async fn run_slash_command( + &self, + command: SlashCommand, + arguments: Vec, + resource: Option>, + ) -> Result; + async fn suggest_docs_packages(&self, provider: Arc) -> Result>; async fn index_docs( diff --git a/crates/extension/src/slash_command.rs b/crates/extension/src/slash_command.rs new file mode 100644 index 0000000000000000000000000000000000000000..0b937984a5e523c8cd9a65c34b11f1b0951a1a2e --- /dev/null +++ b/crates/extension/src/slash_command.rs @@ -0,0 +1,43 @@ +use std::ops::Range; + +/// A slash command for use in the Assistant. +#[derive(Debug, Clone)] +pub struct SlashCommand { + /// The name of the slash command. + pub name: String, + /// The description of the slash command. + pub description: String, + /// The tooltip text to display for the run button. + pub tooltip_text: String, + /// Whether this slash command requires an argument. + pub requires_argument: bool, +} + +/// The output of a slash command. +#[derive(Debug, Clone)] +pub struct SlashCommandOutput { + /// The text produced by the slash command. + pub text: String, + /// The list of sections to show in the slash command placeholder. + pub sections: Vec, +} + +/// A section in the slash command output. +#[derive(Debug, Clone)] +pub struct SlashCommandOutputSection { + /// The range this section occupies. + pub range: Range, + /// The label to display in the placeholder for this section. + pub label: String, +} + +/// A completion for a slash command argument. +#[derive(Debug, Clone)] +pub struct SlashCommandArgumentCompletion { + /// The label to display for this completion. + pub label: String, + /// The new text that should be inserted into the command when this completion is accepted. + pub new_text: String, + /// Whether the command should be run when accepting this completion. + pub run_command: bool, +} diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index faab381bce402ef8358649cd0b2968af4cc3e483..1aed15a05cb4ba518ad30ffb809f2f8c4544408e 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -132,9 +132,8 @@ pub trait ExtensionRegistrationHooks: Send + Sync + 'static { fn register_slash_command( &self, - _slash_command: wit::SlashCommand, - _extension: WasmExtension, - _host: Arc, + _extension: Arc, + _command: extension::SlashCommand, ) { } @@ -1250,7 +1249,8 @@ impl ExtensionStore { for (slash_command_name, slash_command) in &manifest.slash_commands { this.registration_hooks.register_slash_command( - crate::wit::SlashCommand { + extension.clone(), + extension::SlashCommand { name: slash_command_name.to_string(), description: slash_command.description.to_string(), // We don't currently expose this as a configurable option, as it currently drives @@ -1259,8 +1259,6 @@ impl ExtensionStore { tooltip_text: String::new(), requires_argument: slash_command.requires_argument, }, - wasm_extension.clone(), - this.wasm_host.clone(), ); } diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index 95ff5f15877d794cbf6eb38b4eabdc5fe9ff3a08..bd66a31a271abd467b5570da0cd47946789718c4 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -3,7 +3,10 @@ pub mod wit; use crate::{ExtensionManifest, ExtensionRegistrationHooks}; use anyhow::{anyhow, bail, Context as _, Result}; use async_trait::async_trait; -use extension::KeyValueStoreDelegate; +use extension::{ + KeyValueStoreDelegate, SlashCommand, SlashCommandArgumentCompletion, SlashCommandOutput, + WorktreeDelegate, +}; use fs::{normalize_path, Fs}; use futures::future::LocalBoxFuture; use futures::{ @@ -29,7 +32,7 @@ use wasmtime::{ }; use wasmtime_wasi::{self as wasi, WasiView}; use wit::Extension; -pub use wit::{ExtensionProject, SlashCommand}; +pub use wit::ExtensionProject; pub struct WasmHost { engine: Engine, @@ -62,6 +65,51 @@ impl extension::Extension for WasmExtension { self.work_dir.clone() } + async fn complete_slash_command_argument( + &self, + command: SlashCommand, + arguments: Vec, + ) -> Result> { + self.call(|extension, store| { + async move { + let completions = extension + .call_complete_slash_command_argument(store, &command.into(), &arguments) + .await? + .map_err(|err| anyhow!("{err}"))?; + + Ok(completions.into_iter().map(Into::into).collect()) + } + .boxed() + }) + .await + } + + async fn run_slash_command( + &self, + command: SlashCommand, + arguments: Vec, + delegate: Option>, + ) -> Result { + self.call(|extension, store| { + async move { + let resource = if let Some(delegate) = delegate { + Some(store.data_mut().table().push(delegate)?) + } else { + None + }; + + let output = extension + .call_run_slash_command(store, &command.into(), &arguments, resource) + .await? + .map_err(|err| anyhow!("{err}"))?; + + Ok(output.into()) + } + .boxed() + }) + .await + } + async fn suggest_docs_packages(&self, provider: Arc) -> Result> { self.call(|extension, store| { async move { diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs index 09df9aa8b69105518e64772e3f730cf3d076b52a..7e88197ab601779b805bc7855084b70fa6a2d969 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs @@ -1,3 +1,4 @@ +use crate::wasm_host::wit::since_v0_2_0::slash_command::SlashCommandOutputSection; use crate::wasm_host::{wit::ToWasmtimeResult, WasmState}; use ::http_client::{AsyncBody, HttpRequestExt}; use ::settings::{Settings, WorktreeId}; @@ -54,6 +55,45 @@ pub fn linker() -> &'static Linker { LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker)) } +impl From for SlashCommand { + fn from(value: extension::SlashCommand) -> Self { + Self { + name: value.name, + description: value.description, + tooltip_text: value.tooltip_text, + requires_argument: value.requires_argument, + } + } +} + +impl From for extension::SlashCommandOutput { + fn from(value: SlashCommandOutput) -> Self { + Self { + text: value.text, + sections: value.sections.into_iter().map(Into::into).collect(), + } + } +} + +impl From for extension::SlashCommandOutputSection { + fn from(value: SlashCommandOutputSection) -> Self { + Self { + range: value.range.start as usize..value.range.end as usize, + label: value.label, + } + } +} + +impl From for extension::SlashCommandArgumentCompletion { + fn from(value: SlashCommandArgumentCompletion) -> Self { + Self { + label: value.label, + new_text: value.new_text, + run_command: value.run_command, + } + } +} + #[async_trait] impl HostKeyValueStore for WasmState { async fn insert( diff --git a/crates/extensions_ui/src/extension_registration_hooks.rs b/crates/extensions_ui/src/extension_registration_hooks.rs index 5453bd8e038e63edf552d51f03e7f1b57fcd4dda..bab8d90c56a10b41e69ab3e157c8527e6ac7651a 100644 --- a/crates/extensions_ui/src/extension_registration_hooks.rs +++ b/crates/extensions_ui/src/extension_registration_hooks.rs @@ -1,7 +1,7 @@ use std::{path::PathBuf, sync::Arc}; use anyhow::Result; -use assistant_slash_command::SlashCommandRegistry; +use assistant_slash_command::{ExtensionSlashCommand, SlashCommandRegistry}; use context_servers::ContextServerFactoryRegistry; use extension::Extension; use extension_host::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host}; @@ -14,7 +14,6 @@ use theme::{ThemeRegistry, ThemeSettings}; use ui::SharedString; use crate::extension_context_server::ExtensionContextServer; -use crate::extension_slash_command::ExtensionSlashCommand; pub struct ConcreteExtensionRegistrationHooks { slash_command_registry: Arc, @@ -61,18 +60,11 @@ impl extension_host::ExtensionRegistrationHooks for ConcreteExtensionRegistratio fn register_slash_command( &self, - command: wasm_host::SlashCommand, - extension: wasm_host::WasmExtension, - host: Arc, + extension: Arc, + command: extension::SlashCommand, ) { - self.slash_command_registry.register_command( - ExtensionSlashCommand { - command, - extension, - host, - }, - false, - ) + self.slash_command_registry + .register_command(ExtensionSlashCommand::new(extension, command), false) } fn register_context_server( diff --git a/crates/extensions_ui/src/extension_slash_command.rs b/crates/extensions_ui/src/extension_slash_command.rs deleted file mode 100644 index 92c6b148c8637a51484512f7f5df44602cb0dad4..0000000000000000000000000000000000000000 --- a/crates/extensions_ui/src/extension_slash_command.rs +++ /dev/null @@ -1,138 +0,0 @@ -use std::sync::{atomic::AtomicBool, Arc}; - -use anyhow::{anyhow, Result}; -use assistant_slash_command::{ - ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, - SlashCommandResult, -}; -use extension_host::extension_lsp_adapter::WorktreeDelegateAdapter; -use futures::FutureExt as _; -use gpui::{Task, WeakView, WindowContext}; -use language::{BufferSnapshot, LspAdapterDelegate}; -use ui::prelude::*; -use wasmtime_wasi::WasiView; -use workspace::Workspace; - -use extension_host::wasm_host::{WasmExtension, WasmHost}; - -pub struct ExtensionSlashCommand { - pub(crate) extension: WasmExtension, - #[allow(unused)] - pub(crate) host: Arc, - pub(crate) command: extension_host::wasm_host::SlashCommand, -} - -impl SlashCommand for ExtensionSlashCommand { - fn name(&self) -> String { - self.command.name.clone() - } - - fn description(&self) -> String { - self.command.description.clone() - } - - fn menu_text(&self) -> String { - self.command.tooltip_text.clone() - } - - fn requires_argument(&self) -> bool { - self.command.requires_argument - } - - fn complete_argument( - self: Arc, - arguments: &[String], - _cancel: Arc, - _workspace: Option>, - cx: &mut WindowContext, - ) -> Task>> { - let arguments = arguments.to_owned(); - cx.background_executor().spawn(async move { - self.extension - .call({ - let this = self.clone(); - move |extension, store| { - async move { - let completions = extension - .call_complete_slash_command_argument( - store, - &this.command, - &arguments, - ) - .await? - .map_err(|e| anyhow!("{}", e))?; - - anyhow::Ok( - completions - .into_iter() - .map(|completion| ArgumentCompletion { - label: completion.label.into(), - new_text: completion.new_text, - replace_previous_arguments: false, - after_completion: completion.run_command.into(), - }) - .collect(), - ) - } - .boxed() - } - }) - .await - }) - } - - fn run( - self: Arc, - arguments: &[String], - _context_slash_command_output_sections: &[SlashCommandOutputSection], - _context_buffer: BufferSnapshot, - _workspace: WeakView, - delegate: Option>, - cx: &mut WindowContext, - ) -> Task { - let arguments = arguments.to_owned(); - let output = cx.background_executor().spawn(async move { - self.extension - .call({ - let this = self.clone(); - move |extension, store| { - async move { - let resource = if let Some(delegate) = delegate { - let delegate = - Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _; - Some(store.data_mut().table().push(delegate)?) - } else { - None - }; - let output = extension - .call_run_slash_command(store, &this.command, &arguments, resource) - .await? - .map_err(|e| anyhow!("{}", e))?; - - anyhow::Ok(output) - } - .boxed() - } - }) - .await - }); - cx.foreground_executor().spawn(async move { - let output = output.await?; - Ok(SlashCommandOutput { - text: output.text, - sections: output - .sections - .into_iter() - .map(|section| SlashCommandOutputSection { - range: section.range.into(), - icon: IconName::Code, - label: section.label.into(), - metadata: None, - }) - .collect(), - run_commands_in_text: false, - } - .to_event_stream()) - }) - } -} diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 6e57885633db9f4949ececc7b38a180d74cadda1..cd0fe94fa33f82c921833cc88f7db67d09edeb93 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -1,7 +1,6 @@ mod components; mod extension_context_server; mod extension_registration_hooks; -mod extension_slash_command; mod extension_suggest; mod extension_version_selector;