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
This commit is contained in:
Marshall Bowers 2024-11-13 14:34:58 -05:00 committed by GitHub
parent b913cf2e02
commit 254ce74036
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 309 additions and 161 deletions

3
Cargo.lock generated
View file

@ -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",
]

View file

@ -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]

View file

@ -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},

View file

@ -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())
})
}
}

View file

@ -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(

View file

@ -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,
}

View file

@ -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(),
);
}

View file

@ -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 {

View file

@ -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(

View file

@ -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(

View file

@ -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())
})
}
}

View file

@ -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;