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<String>,
        worktree: &zed::Worktree,
    ) -> Result<Option<String>, String> {
        match command.name.as_str() {
            "gleam-project" => Ok(Some("Yayyy".to_string())),
            command => Err(format!("unknown slash command: \"{command}\"")),
        }
    }
}
```

Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2024-05-24 15:44:32 -04:00 committed by GitHub
parent 055a13a9b6
commit 82f5f36422
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 310 additions and 14 deletions

4
Cargo.lock generated
View file

@ -436,6 +436,7 @@ dependencies = [
"derive_more", "derive_more",
"futures 0.3.28", "futures 0.3.28",
"gpui", "gpui",
"language",
"parking_lot", "parking_lot",
] ]
@ -3784,6 +3785,7 @@ name = "extension"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assistant_slash_command",
"async-compression", "async-compression",
"async-tar", "async-tar",
"async-trait", "async-trait",
@ -13249,7 +13251,7 @@ dependencies = [
name = "zed_gleam" name = "zed_gleam"
version = "0.1.3" version = "0.1.3"
dependencies = [ dependencies = [
"zed_extension_api 0.0.6", "zed_extension_api 0.0.7",
] ]
[[package]] [[package]]

View file

@ -237,6 +237,7 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
cx.set_global(Assistant::default()); cx.set_global(Assistant::default());
AssistantSettings::register(cx); AssistantSettings::register(cx);
completion_provider::init(client, cx); completion_provider::init(client, cx);
assistant_slash_command::init(cx);
assistant_panel::init(cx); assistant_panel::init(cx);
CommandPaletteFilter::update_global(cx, |filter, _cx| { CommandPaletteFilter::update_global(cx, |filter, _cx| {

View file

@ -43,13 +43,14 @@ use gpui::{
UniformListScrollHandle, View, ViewContext, VisualContext, WeakModel, WeakView, WhiteSpace, UniformListScrollHandle, View, ViewContext, VisualContext, WeakModel, WeakView, WhiteSpace,
WindowContext, WindowContext,
}; };
use language::LspAdapterDelegate;
use language::{ use language::{
language_settings::SoftWrap, AutoindentMode, Buffer, BufferSnapshot, LanguageRegistry, language_settings::SoftWrap, AutoindentMode, Buffer, BufferSnapshot, LanguageRegistry,
OffsetRangeExt as _, Point, ToOffset as _, ToPoint as _, OffsetRangeExt as _, Point, ToOffset as _, ToPoint as _,
}; };
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
use parking_lot::Mutex; use parking_lot::Mutex;
use project::{Project, ProjectTransaction}; use project::{Project, ProjectLspAdapterDelegate, ProjectTransaction};
use search::{buffer_search::DivRegistrar, BufferSearchBar}; use search::{buffer_search::DivRegistrar, BufferSearchBar};
use settings::Settings; use settings::Settings;
use std::{ use std::{
@ -205,8 +206,7 @@ impl AssistantPanel {
}) })
.detach(); .detach();
let slash_command_registry = SlashCommandRegistry::new(); let slash_command_registry = SlashCommandRegistry::global(cx);
let window = cx.window_handle().downcast::<Workspace>(); let window = cx.window_handle().downcast::<Workspace>();
slash_command_registry.register_command(file_command::FileSlashCommand::new( slash_command_registry.register_command(file_command::FileSlashCommand::new(
@ -1129,6 +1129,13 @@ impl AssistantPanel {
let slash_commands = self.slash_commands.clone(); let slash_commands = self.slash_commands.clone();
let languages = self.languages.clone(); let languages = self.languages.clone();
let telemetry = self.telemetry.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 { cx.spawn(|this, mut cx| async move {
let saved_conversation = SavedConversation::load(&path, fs.as_ref()).await?; let saved_conversation = SavedConversation::load(&path, fs.as_ref()).await?;
let model = this.update(&mut cx, |this, _| this.model.clone())?; let model = this.update(&mut cx, |this, _| this.model.clone())?;
@ -1139,6 +1146,7 @@ impl AssistantPanel {
languages, languages,
slash_commands, slash_commands,
Some(telemetry), Some(telemetry),
lsp_adapter_delegate,
&mut cx, &mut cx,
) )
.await?; .await?;
@ -1484,6 +1492,7 @@ pub struct Conversation {
telemetry: Option<Arc<Telemetry>>, telemetry: Option<Arc<Telemetry>>,
slash_command_registry: Arc<SlashCommandRegistry>, slash_command_registry: Arc<SlashCommandRegistry>,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
} }
impl EventEmitter<ConversationEvent> for Conversation {} impl EventEmitter<ConversationEvent> for Conversation {}
@ -1494,6 +1503,7 @@ impl Conversation {
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
slash_command_registry: Arc<SlashCommandRegistry>, slash_command_registry: Arc<SlashCommandRegistry>,
telemetry: Option<Arc<Telemetry>>, telemetry: Option<Arc<Telemetry>>,
lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Self { ) -> Self {
let buffer = cx.new_model(|cx| { let buffer = cx.new_model(|cx| {
@ -1526,6 +1536,7 @@ impl Conversation {
telemetry, telemetry,
slash_command_registry, slash_command_registry,
language_registry, language_registry,
lsp_adapter_delegate,
}; };
let message = MessageAnchor { let message = MessageAnchor {
@ -1569,6 +1580,7 @@ impl Conversation {
} }
} }
#[allow(clippy::too_many_arguments)]
async fn deserialize( async fn deserialize(
saved_conversation: SavedConversation, saved_conversation: SavedConversation,
model: LanguageModel, model: LanguageModel,
@ -1576,6 +1588,7 @@ impl Conversation {
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
slash_command_registry: Arc<SlashCommandRegistry>, slash_command_registry: Arc<SlashCommandRegistry>,
telemetry: Option<Arc<Telemetry>>, telemetry: Option<Arc<Telemetry>>,
lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut AsyncAppContext, cx: &mut AsyncAppContext,
) -> Result<Model<Self>> { ) -> Result<Model<Self>> {
let id = match saved_conversation.id { let id = match saved_conversation.id {
@ -1635,6 +1648,7 @@ impl Conversation {
telemetry, telemetry,
language_registry, language_registry,
slash_command_registry, slash_command_registry,
lsp_adapter_delegate,
}; };
this.set_language(cx); this.set_language(cx);
this.reparse_edit_suggestions(cx); this.reparse_edit_suggestions(cx);
@ -1850,7 +1864,13 @@ impl Conversation {
buffer.anchor_after(offset)..buffer.anchor_before(line_end_offset); buffer.anchor_after(offset)..buffer.anchor_before(line_end_offset);
let argument = call.argument.map(|range| &line[range]); 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 { new_calls.push(SlashCommandCall {
name, name,
@ -2728,12 +2748,16 @@ impl ConversationEditor {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Self { ) -> Self {
let telemetry = workspace.read(cx).client().telemetry().clone(); 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| { let conversation = cx.new_model(|cx| {
Conversation::new( Conversation::new(
model, model,
language_registry, language_registry,
slash_command_registry, slash_command_registry,
Some(telemetry), Some(telemetry),
Some(lsp_adapter_delegate),
cx, cx,
) )
}); });
@ -3907,6 +3931,20 @@ fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
} }
} }
fn make_lsp_adapter_delegate(
project: &Model<Project>,
cx: &mut AppContext,
) -> Arc<dyn LspAdapterDelegate> {
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)] #[cfg(test)]
mod tests { mod tests {
use std::{cell::RefCell, path::Path, rc::Rc}; use std::{cell::RefCell, path::Path, rc::Rc};
@ -3935,6 +3973,7 @@ mod tests {
registry, registry,
Default::default(), Default::default(),
None, None,
None,
cx, cx,
) )
}); });
@ -4074,6 +4113,7 @@ mod tests {
registry, registry,
Default::default(), Default::default(),
None, None,
None,
cx, cx,
) )
}); });
@ -4180,6 +4220,7 @@ mod tests {
registry, registry,
Default::default(), Default::default(),
None, None,
None,
cx, cx,
) )
}); });
@ -4292,6 +4333,15 @@ mod tests {
prompt_library.clone(), 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 registry = Arc::new(LanguageRegistry::test(cx.executor()));
let conversation = cx.new_model(|cx| { let conversation = cx.new_model(|cx| {
Conversation::new( Conversation::new(
@ -4299,6 +4349,7 @@ mod tests {
registry.clone(), registry.clone(),
slash_command_registry, slash_command_registry,
None, None,
Some(lsp_adapter_delegate),
cx, cx,
) )
}); });
@ -4599,6 +4650,7 @@ mod tests {
registry.clone(), registry.clone(),
Default::default(), Default::default(),
None, None,
None,
cx, cx,
) )
}); });
@ -4642,6 +4694,7 @@ mod tests {
registry.clone(), registry.clone(),
Default::default(), Default::default(),
None, None,
None,
&mut cx.to_async(), &mut cx.to_async(),
) )
.await .await

View file

@ -1,3 +1,4 @@
use std::sync::Arc;
use std::{borrow::Cow, cell::Cell, rc::Rc}; use std::{borrow::Cow, cell::Cell, rc::Rc};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
@ -5,6 +6,7 @@ use collections::HashMap;
use editor::Editor; use editor::Editor;
use futures::channel::oneshot; use futures::channel::oneshot;
use gpui::{AppContext, Entity, Subscription, Task, WindowHandle}; use gpui::{AppContext, Entity, Subscription, Task, WindowHandle};
use language::LspAdapterDelegate;
use workspace::{Event as WorkspaceEvent, Workspace}; use workspace::{Event as WorkspaceEvent, Workspace};
use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation}; use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
@ -41,7 +43,12 @@ impl SlashCommand for CurrentFileSlashCommand {
false false
} }
fn run(&self, _argument: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation { fn run(
self: Arc<Self>,
_argument: Option<&str>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut AppContext,
) -> SlashCommandInvocation {
let (invalidate_tx, invalidate_rx) = oneshot::channel(); let (invalidate_tx, invalidate_rx) = oneshot::channel();
let invalidate_tx = Rc::new(Cell::new(Some(invalidate_tx))); let invalidate_tx = Rc::new(Cell::new(Some(invalidate_tx)));
let mut subscriptions: Vec<Subscription> = Vec::new(); let mut subscriptions: Vec<Subscription> = Vec::new();

View file

@ -3,6 +3,7 @@ use anyhow::Result;
use futures::channel::oneshot; use futures::channel::oneshot;
use fuzzy::PathMatch; use fuzzy::PathMatch;
use gpui::{AppContext, Model, Task}; use gpui::{AppContext, Model, Task};
use language::LspAdapterDelegate;
use project::{PathMatchCandidateSet, Project}; use project::{PathMatchCandidateSet, Project};
use std::{ use std::{
path::Path, path::Path,
@ -96,7 +97,12 @@ impl SlashCommand for FileSlashCommand {
}) })
} }
fn run(&self, argument: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation { fn run(
self: Arc<Self>,
argument: Option<&str>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut AppContext,
) -> SlashCommandInvocation {
let project = self.project.read(cx); let project = self.project.read(cx);
let Some(argument) = argument else { let Some(argument) = argument else {
return SlashCommandInvocation { return SlashCommandInvocation {

View file

@ -4,6 +4,7 @@ use anyhow::{anyhow, Context, Result};
use futures::channel::oneshot; use futures::channel::oneshot;
use fuzzy::StringMatchCandidate; use fuzzy::StringMatchCandidate;
use gpui::{AppContext, Task}; use gpui::{AppContext, Task};
use language::LspAdapterDelegate;
use std::sync::{atomic::AtomicBool, Arc}; use std::sync::{atomic::AtomicBool, Arc};
pub(crate) struct PromptSlashCommand { 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<Self>,
title: Option<&str>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut AppContext,
) -> SlashCommandInvocation {
let Some(title) = title else { let Some(title) = title else {
return SlashCommandInvocation { return SlashCommandInvocation {
output: Task::ready(Err(anyhow!("missing prompt name"))), output: Task::ready(Err(anyhow!("missing prompt name"))),

View file

@ -17,4 +17,5 @@ collections.workspace = true
derive_more.workspace = true derive_more.workspace = true
futures.workspace = true futures.workspace = true
gpui.workspace = true gpui.workspace = true
language.workspace = true
parking_lot.workspace = true parking_lot.workspace = true

View file

@ -6,6 +6,7 @@ use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use futures::channel::oneshot; use futures::channel::oneshot;
use gpui::{AppContext, Task}; use gpui::{AppContext, Task};
use language::LspAdapterDelegate;
pub use slash_command_registry::*; pub use slash_command_registry::*;
@ -23,7 +24,17 @@ pub trait SlashCommand: 'static + Send + Sync {
cx: &mut AppContext, cx: &mut AppContext,
) -> Task<Result<Vec<String>>>; ) -> Task<Result<Vec<String>>>;
fn requires_argument(&self) -> bool; fn requires_argument(&self) -> bool;
fn run(&self, argument: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation; fn run(
self: Arc<Self>,
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<dyn LspAdapterDelegate>,
cx: &mut AppContext,
) -> SlashCommandInvocation;
} }
pub struct SlashCommandInvocation { pub struct SlashCommandInvocation {

View file

@ -14,6 +14,7 @@ doctest = false
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
assistant_slash_command.workspace = true
async-compression.workspace = true async-compression.workspace = true
async-tar.workspace = true async-tar.workspace = true
async-trait.workspace = true async-trait.workspace = true

View file

@ -74,6 +74,8 @@ pub struct ExtensionManifest {
pub grammars: BTreeMap<Arc<str>, GrammarManifestEntry>, pub grammars: BTreeMap<Arc<str>, GrammarManifestEntry>,
#[serde(default)] #[serde(default)]
pub language_servers: BTreeMap<LanguageServerName, LanguageServerManifestEntry>, pub language_servers: BTreeMap<LanguageServerName, LanguageServerManifestEntry>,
#[serde(default)]
pub slash_commands: BTreeMap<Arc<str>, SlashCommandManifestEntry>,
} }
#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)] #[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 { impl ExtensionManifest {
pub async fn load(fs: Arc<dyn Fs>, extension_dir: &Path) -> Result<Self> { pub async fn load(fs: Arc<dyn Fs>, extension_dir: &Path) -> Result<Self> {
let extension_name = extension_dir let extension_name = extension_dir
@ -190,5 +198,6 @@ fn manifest_from_old_manifest(
.map(|grammar_name| (grammar_name, Default::default())) .map(|grammar_name| (grammar_name, Default::default()))
.collect(), .collect(),
language_servers: Default::default(), language_servers: Default::default(),
slash_commands: BTreeMap::default(),
} }
} }

View file

@ -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<WasmHost>,
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<AtomicBool>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Ok(Vec::new()))
}
fn run(
self: Arc<Self>,
argument: Option<&str>,
delegate: Arc<dyn LspAdapterDelegate>,
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(),
}
}
}

View file

@ -2,14 +2,17 @@ pub mod extension_builder;
mod extension_lsp_adapter; mod extension_lsp_adapter;
mod extension_manifest; mod extension_manifest;
mod extension_settings; mod extension_settings;
mod extension_slash_command;
mod wasm_host; mod wasm_host;
#[cfg(test)] #[cfg(test)]
mod extension_store_test; mod extension_store_test;
use crate::extension_manifest::SchemaVersion; use crate::extension_manifest::SchemaVersion;
use crate::extension_slash_command::ExtensionSlashCommand;
use crate::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host::wit}; use crate::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host::wit};
use anyhow::{anyhow, bail, Context as _, Result}; use anyhow::{anyhow, bail, Context as _, Result};
use assistant_slash_command::SlashCommandRegistry;
use async_compression::futures::bufread::GzipDecoder; use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive; use async_tar::Archive;
use client::{telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse}; use client::{telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse};
@ -107,6 +110,7 @@ pub struct ExtensionStore {
index_path: PathBuf, index_path: PathBuf,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
theme_registry: Arc<ThemeRegistry>, theme_registry: Arc<ThemeRegistry>,
slash_command_registry: Arc<SlashCommandRegistry>,
modified_extensions: HashSet<Arc<str>>, modified_extensions: HashSet<Arc<str>>,
wasm_host: Arc<WasmHost>, wasm_host: Arc<WasmHost>,
wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>, wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
@ -183,6 +187,7 @@ pub fn init(
node_runtime, node_runtime,
language_registry, language_registry,
theme_registry, theme_registry,
SlashCommandRegistry::global(cx),
cx, cx,
) )
}); });
@ -215,6 +220,7 @@ impl ExtensionStore {
node_runtime: Arc<dyn NodeRuntime>, node_runtime: Arc<dyn NodeRuntime>,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
theme_registry: Arc<ThemeRegistry>, theme_registry: Arc<ThemeRegistry>,
slash_command_registry: Arc<SlashCommandRegistry>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Self { ) -> Self {
let work_dir = extensions_dir.join("work"); let work_dir = extensions_dir.join("work");
@ -245,6 +251,7 @@ impl ExtensionStore {
telemetry, telemetry,
language_registry, language_registry,
theme_registry, theme_registry,
slash_command_registry,
reload_tx, reload_tx,
tasks: Vec::new(), 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); this.wasm_extensions.extend(wasm_extensions);
ThemeSettings::reload_current_theme(cx) ThemeSettings::reload_current_theme(cx)

View file

@ -5,6 +5,7 @@ use crate::{
ExtensionIndexThemeEntry, ExtensionManifest, ExtensionStore, GrammarManifestEntry, ExtensionIndexThemeEntry, ExtensionManifest, ExtensionStore, GrammarManifestEntry,
RELOAD_DEBOUNCE_DURATION, RELOAD_DEBOUNCE_DURATION,
}; };
use assistant_slash_command::SlashCommandRegistry;
use async_compression::futures::bufread::GzipEncoder; use async_compression::futures::bufread::GzipEncoder;
use collections::BTreeMap; use collections::BTreeMap;
use fs::{FakeFs, Fs, RealFs}; use fs::{FakeFs, Fs, RealFs};
@ -156,6 +157,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
.into_iter() .into_iter()
.collect(), .collect(),
language_servers: BTreeMap::default(), language_servers: BTreeMap::default(),
slash_commands: BTreeMap::default(),
}), }),
dev: false, dev: false,
}, },
@ -179,6 +181,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
languages: Default::default(), languages: Default::default(),
grammars: BTreeMap::default(), grammars: BTreeMap::default(),
language_servers: BTreeMap::default(), language_servers: BTreeMap::default(),
slash_commands: BTreeMap::default(),
}), }),
dev: false, dev: false,
}, },
@ -250,6 +253,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
let language_registry = Arc::new(LanguageRegistry::test(cx.executor())); let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(()))); let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
let slash_command_registry = SlashCommandRegistry::new();
let node_runtime = FakeNodeRuntime::new(); let node_runtime = FakeNodeRuntime::new();
let store = cx.new_model(|cx| { let store = cx.new_model(|cx| {
@ -262,6 +266,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
node_runtime.clone(), node_runtime.clone(),
language_registry.clone(), language_registry.clone(),
theme_registry.clone(), theme_registry.clone(),
slash_command_registry.clone(),
cx, cx,
) )
}); });
@ -333,6 +338,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
languages: Default::default(), languages: Default::default(),
grammars: BTreeMap::default(), grammars: BTreeMap::default(),
language_servers: BTreeMap::default(), language_servers: BTreeMap::default(),
slash_commands: BTreeMap::default(),
}), }),
dev: false, dev: false,
}, },
@ -382,6 +388,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
node_runtime.clone(), node_runtime.clone(),
language_registry.clone(), language_registry.clone(),
theme_registry.clone(), theme_registry.clone(),
slash_command_registry,
cx, 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 language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(()))); let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
let slash_command_registry = SlashCommandRegistry::new();
let node_runtime = FakeNodeRuntime::new(); let node_runtime = FakeNodeRuntime::new();
let mut status_updates = language_registry.language_server_binary_statuses(); 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, node_runtime,
language_registry.clone(), language_registry.clone(),
theme_registry.clone(), theme_registry.clone(),
slash_command_registry,
cx, cx,
) )
}); });

View file

@ -19,7 +19,7 @@ use wasmtime::{
pub use latest::CodeLabelSpanLiteral; pub use latest::CodeLabelSpanLiteral;
pub use latest::{ pub use latest::{
zed::extension::lsp::{Completion, CompletionKind, InsertTextFormat, Symbol, SymbolKind}, zed::extension::lsp::{Completion, CompletionKind, InsertTextFormat, Symbol, SymbolKind},
CodeLabel, CodeLabelSpan, Command, Range, CodeLabel, CodeLabelSpan, Command, Range, SlashCommand,
}; };
pub use since_v0_0_4::LanguageServerConfig; pub use since_v0_0_4::LanguageServerConfig;
@ -255,6 +255,22 @@ impl Extension {
Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())), Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())),
} }
} }
pub async fn call_run_slash_command(
&self,
store: &mut Store<WasmState>,
command: &SlashCommand,
argument: Option<&str>,
resource: Resource<Arc<dyn LspAdapterDelegate>>,
) -> Result<Result<Option<String>, 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<T> { trait ToWasmtimeResult<T> {

View file

@ -222,6 +222,9 @@ impl platform::Host for WasmState {
} }
} }
#[async_trait]
impl slash_command::Host for WasmState {}
#[async_trait] #[async_trait]
impl ExtensionImports for WasmState { impl ExtensionImports for WasmState {
async fn get_settings( async fn get_settings(

View file

@ -24,6 +24,7 @@ pub use wit::{
npm_package_latest_version, npm_package_latest_version,
}, },
zed::extension::platform::{current_platform, Architecture, Os}, zed::extension::platform::{current_platform, Architecture, Os},
zed::extension::slash_command::SlashCommand,
CodeLabel, CodeLabelSpan, CodeLabelSpanLiteral, Command, DownloadedFileType, EnvVars, CodeLabel, CodeLabelSpan, CodeLabelSpanLiteral, Command, DownloadedFileType, EnvVars,
LanguageServerInstallationStatus, Range, Worktree, LanguageServerInstallationStatus, Range, Worktree,
}; };
@ -104,6 +105,16 @@ pub trait Extension: Send + Sync {
) -> Option<CodeLabel> { ) -> Option<CodeLabel> {
None None
} }
/// Runs the given slash command.
fn run_slash_command(
&self,
_command: SlashCommand,
_argument: Option<String>,
_worktree: &Worktree,
) -> Result<Option<String>, String> {
Ok(None)
}
} }
/// Registers the provided type as a Zed extension. /// Registers the provided type as a Zed extension.
@ -139,6 +150,8 @@ static mut EXTENSION: Option<Box<dyn Extension>> = None;
pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "/version_bytes")); pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "/version_bytes"));
mod wit { mod wit {
#![allow(clippy::too_many_arguments)]
wit_bindgen::generate!({ wit_bindgen::generate!({
skip: ["init-extension"], skip: ["init-extension"],
path: "./wit/since_v0.0.7", path: "./wit/since_v0.0.7",
@ -209,6 +222,14 @@ impl wit::Guest for Component {
} }
Ok(labels) Ok(labels)
} }
fn run_slash_command(
command: SlashCommand,
argument: Option<String>,
worktree: &Worktree,
) -> Result<Option<String>, String> {
extension().run_slash_command(command, argument, worktree)
}
} }
/// The ID of a language server. /// The ID of a language server.

View file

@ -6,6 +6,7 @@ world extension {
import nodejs; import nodejs;
use lsp.{completion, symbol}; use lsp.{completion, symbol};
use slash-command.{slash-command};
/// Initializes the extension. /// Initializes the extension.
export init-extension: func(); export init-extension: func();
@ -127,4 +128,7 @@ world extension {
export labels-for-completions: func(language-server-id: string, completions: list<completion>) -> result<list<option<code-label>>, string>; export labels-for-completions: func(language-server-id: string, completions: list<completion>) -> result<list<option<code-label>>, string>;
export labels-for-symbols: func(language-server-id: string, symbols: list<symbol>) -> result<list<option<code-label>>, string>; export labels-for-symbols: func(language-server-id: string, symbols: list<symbol>) -> result<list<option<code-label>>, string>;
/// Runs the provided slash command.
export run-slash-command: func(command: slash-command, argument: option<string>, worktree: borrow<worktree>) -> result<option<string>, string>;
} }

View file

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

View file

@ -11175,7 +11175,7 @@ impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
} }
} }
struct ProjectLspAdapterDelegate { pub struct ProjectLspAdapterDelegate {
project: WeakModel<Project>, project: WeakModel<Project>,
worktree: worktree::Snapshot, worktree: worktree::Snapshot,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
@ -11185,7 +11185,11 @@ struct ProjectLspAdapterDelegate {
} }
impl ProjectLspAdapterDelegate { impl ProjectLspAdapterDelegate {
fn new(project: &Project, worktree: &Model<Worktree>, cx: &ModelContext<Project>) -> Arc<Self> { pub fn new(
project: &Project,
worktree: &Model<Worktree>,
cx: &ModelContext<Project>,
) -> Arc<Self> {
Arc::new(Self { Arc::new(Self {
project: cx.weak_model(), project: cx.weak_model(),
worktree: worktree.read(cx).snapshot(), worktree: worktree.read(cx).snapshot(),

View file

@ -13,4 +13,4 @@ path = "src/gleam.rs"
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [dependencies]
zed_extension_api = "0.0.6" zed_extension_api = { path = "../../crates/extension_api" }

View file

@ -13,3 +13,7 @@ language = "Gleam"
[grammars.gleam] [grammars.gleam]
repository = "https://github.com/gleam-lang/tree-sitter-gleam" repository = "https://github.com/gleam-lang/tree-sitter-gleam"
commit = "8432ffe32ccd360534837256747beb5b1c82fca1" commit = "8432ffe32ccd360534837256747beb5b1c82fca1"
[slash_commands.gleam-project]
description = "Returns information about the current Gleam project."
requires_argument = false

View file

@ -1,6 +1,6 @@
use std::fs; use std::fs;
use zed::lsp::CompletionKind; use zed::lsp::CompletionKind;
use zed::{CodeLabel, CodeLabelSpan, LanguageServerId}; use zed::{CodeLabel, CodeLabelSpan, LanguageServerId, SlashCommand};
use zed_extension_api::{self as zed, Result}; use zed_extension_api::{self as zed, Result};
struct GleamExtension { struct GleamExtension {
@ -142,6 +142,28 @@ impl zed::Extension for GleamExtension {
code, code,
}) })
} }
fn run_slash_command(
&self,
command: SlashCommand,
_argument: Option<String>,
worktree: &zed::Worktree,
) -> Result<Option<String>, 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); zed::register_extension!(GleamExtension);