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
|
@ -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"))),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue