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

View file

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

View file

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

View file

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

View file

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

View file

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