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:
parent
055a13a9b6
commit
82f5f36422
22 changed files with 310 additions and 14 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -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]]
|
||||
|
|
|
@ -237,6 +237,7 @@ pub fn init(client: Arc<Client>, 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| {
|
||||
|
|
|
@ -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::<Workspace>();
|
||||
|
||||
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<Arc<Telemetry>>,
|
||||
slash_command_registry: Arc<SlashCommandRegistry>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
}
|
||||
|
||||
impl EventEmitter<ConversationEvent> for Conversation {}
|
||||
|
@ -1494,6 +1503,7 @@ impl Conversation {
|
|||
language_registry: Arc<LanguageRegistry>,
|
||||
slash_command_registry: Arc<SlashCommandRegistry>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> 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<LanguageRegistry>,
|
||||
slash_command_registry: Arc<SlashCommandRegistry>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Model<Self>> {
|
||||
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>,
|
||||
) -> 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<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)]
|
||||
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
|
||||
|
|
|
@ -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<Self>,
|
||||
_argument: Option<&str>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
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<Subscription> = Vec::new();
|
||||
|
|
|
@ -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<Self>,
|
||||
argument: Option<&str>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut AppContext,
|
||||
) -> SlashCommandInvocation {
|
||||
let project = self.project.read(cx);
|
||||
let Some(argument) = argument else {
|
||||
return SlashCommandInvocation {
|
||||
|
|
|
@ -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<Self>,
|
||||
title: Option<&str>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut AppContext,
|
||||
) -> SlashCommandInvocation {
|
||||
let Some(title) = title else {
|
||||
return SlashCommandInvocation {
|
||||
output: Task::ready(Err(anyhow!("missing prompt name"))),
|
||||
|
|
|
@ -17,4 +17,5 @@ collections.workspace = true
|
|||
derive_more.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
parking_lot.workspace = true
|
||||
|
|
|
@ -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<Result<Vec<String>>>;
|
||||
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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -74,6 +74,8 @@ pub struct ExtensionManifest {
|
|||
pub grammars: BTreeMap<Arc<str>, GrammarManifestEntry>,
|
||||
#[serde(default)]
|
||||
pub language_servers: BTreeMap<LanguageServerName, LanguageServerManifestEntry>,
|
||||
#[serde(default)]
|
||||
pub slash_commands: BTreeMap<Arc<str>, 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<dyn Fs>, extension_dir: &Path) -> Result<Self> {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
|
85
crates/extension/src/extension_slash_command.rs
Normal file
85
crates/extension/src/extension_slash_command.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<LanguageRegistry>,
|
||||
theme_registry: Arc<ThemeRegistry>,
|
||||
slash_command_registry: Arc<SlashCommandRegistry>,
|
||||
modified_extensions: HashSet<Arc<str>>,
|
||||
wasm_host: Arc<WasmHost>,
|
||||
wasm_extensions: Vec<(Arc<ExtensionManifest>, 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<dyn NodeRuntime>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
theme_registry: Arc<ThemeRegistry>,
|
||||
slash_command_registry: Arc<SlashCommandRegistry>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> 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)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
});
|
||||
|
|
|
@ -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<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> {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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<CodeLabel> {
|
||||
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.
|
||||
|
@ -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"));
|
||||
|
||||
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<String>,
|
||||
worktree: &Worktree,
|
||||
) -> Result<Option<String>, String> {
|
||||
extension().run_slash_command(command, argument, worktree)
|
||||
}
|
||||
}
|
||||
|
||||
/// The ID of a language server.
|
||||
|
|
|
@ -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<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>;
|
||||
|
||||
/// Runs the provided slash command.
|
||||
export run-slash-command: func(command: slash-command, argument: option<string>, worktree: borrow<worktree>) -> result<option<string>, string>;
|
||||
}
|
||||
|
|
11
crates/extension_api/wit/since_v0.0.7/slash-command.wit
Normal file
11
crates/extension_api/wit/since_v0.0.7/slash-command.wit
Normal 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,
|
||||
}
|
||||
}
|
|
@ -11175,7 +11175,7 @@ impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
|
|||
}
|
||||
}
|
||||
|
||||
struct ProjectLspAdapterDelegate {
|
||||
pub struct ProjectLspAdapterDelegate {
|
||||
project: WeakModel<Project>,
|
||||
worktree: worktree::Snapshot,
|
||||
fs: Arc<dyn Fs>,
|
||||
|
@ -11185,7 +11185,11 @@ struct 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 {
|
||||
project: cx.weak_model(),
|
||||
worktree: worktree.read(cx).snapshot(),
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue