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

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

View file

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

View file

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

View file

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

View file

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

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_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)

View file

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

View file

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

View file

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

View file

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

View file

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

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>,
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(),

View file

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

View file

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

View file

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