Detailed changes
@@ -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",
]
@@ -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]
@@ -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},
@@ -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<dyn LspAdapterDelegate>);
+
+#[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<String> {
+ self.0.read_text_file(path).await
+ }
+
+ async fn which(&self, binary_name: String) -> Option<String> {
+ 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<dyn Extension>,
+ command: extension::SlashCommand,
+}
+
+impl ExtensionSlashCommand {
+ pub fn new(extension: Arc<dyn Extension>, 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<Self>,
+ arguments: &[String],
+ _cancel: Arc<AtomicBool>,
+ _workspace: Option<WeakView<Workspace>>,
+ cx: &mut WindowContext,
+ ) -> Task<Result<Vec<ArgumentCompletion>>> {
+ 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<Self>,
+ arguments: &[String],
+ _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
+ _context_buffer: BufferSnapshot,
+ _workspace: WeakView<Workspace>,
+ delegate: Option<Arc<dyn LspAdapterDelegate>>,
+ cx: &mut WindowContext,
+ ) -> Task<SlashCommandResult> {
+ 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())
+ })
+ }
+}
@@ -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<Path>;
+ async fn complete_slash_command_argument(
+ &self,
+ command: SlashCommand,
+ arguments: Vec<String>,
+ ) -> Result<Vec<SlashCommandArgumentCompletion>>;
+
+ async fn run_slash_command(
+ &self,
+ command: SlashCommand,
+ arguments: Vec<String>,
+ resource: Option<Arc<dyn WorktreeDelegate>>,
+ ) -> Result<SlashCommandOutput>;
+
async fn suggest_docs_packages(&self, provider: Arc<str>) -> Result<Vec<String>>;
async fn index_docs(
@@ -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<SlashCommandOutputSection>,
+}
+
+/// A section in the slash command output.
+#[derive(Debug, Clone)]
+pub struct SlashCommandOutputSection {
+ /// The range this section occupies.
+ pub range: Range<usize>,
+ /// 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,
+}
@@ -132,9 +132,8 @@ pub trait ExtensionRegistrationHooks: Send + Sync + 'static {
fn register_slash_command(
&self,
- _slash_command: wit::SlashCommand,
- _extension: WasmExtension,
- _host: Arc<WasmHost>,
+ _extension: Arc<dyn Extension>,
+ _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(),
);
}
@@ -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<String>,
+ ) -> Result<Vec<SlashCommandArgumentCompletion>> {
+ 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<String>,
+ delegate: Option<Arc<dyn WorktreeDelegate>>,
+ ) -> Result<SlashCommandOutput> {
+ 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<str>) -> Result<Vec<String>> {
self.call(|extension, store| {
async move {
@@ -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<WasmState> {
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
}
+impl From<extension::SlashCommand> 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<SlashCommandOutput> for extension::SlashCommandOutput {
+ fn from(value: SlashCommandOutput) -> Self {
+ Self {
+ text: value.text,
+ sections: value.sections.into_iter().map(Into::into).collect(),
+ }
+ }
+}
+
+impl From<SlashCommandOutputSection> 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<SlashCommandArgumentCompletion> 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(
@@ -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<SlashCommandRegistry>,
@@ -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<wasm_host::WasmHost>,
+ extension: Arc<dyn Extension>,
+ 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(
@@ -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<WasmHost>,
- 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<Self>,
- arguments: &[String],
- _cancel: Arc<AtomicBool>,
- _workspace: Option<WeakView<Workspace>>,
- cx: &mut WindowContext,
- ) -> Task<Result<Vec<ArgumentCompletion>>> {
- 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<Self>,
- arguments: &[String],
- _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
- _context_buffer: BufferSnapshot,
- _workspace: WeakView<Workspace>,
- delegate: Option<Arc<dyn LspAdapterDelegate>>,
- cx: &mut WindowContext,
- ) -> Task<SlashCommandResult> {
- 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())
- })
- }
-}
@@ -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;