Decouple extension `Worktree` resource from `LspAdapterDelegate` (#20611)

Marshall Bowers created

This PR decouples the extension `Worktree` resource from the
`LspAdapterDelegate`.

We now have a `WorktreeDelegate` trait that corresponds to the methods
on the resource.

We then create a `WorktreeDelegateAdapter` that can wrap an
`LspAdapterDelegate` and implement the `WorktreeDelegate` trait.

Release Notes:

- N/A

Change summary

crates/extension/src/extension.rs                       | 11 ++
crates/extension_host/src/extension_lsp_adapter.rs      | 33 ++++++++
crates/extension_host/src/wasm_host/wit.rs              | 11 +-
crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs | 11 +-
crates/extension_host/src/wasm_host/wit/since_v0_0_4.rs | 10 +-
crates/extension_host/src/wasm_host/wit/since_v0_0_6.rs | 17 +--
crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs | 42 +++-------
crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs | 31 ++-----
crates/extensions_ui/src/extension_slash_command.rs     |  3 
9 files changed, 93 insertions(+), 76 deletions(-)

Detailed changes

crates/extension/src/extension.rs 🔗

@@ -1,7 +1,7 @@
 pub mod extension_builder;
 mod extension_manifest;
 
-use std::path::Path;
+use std::path::{Path, PathBuf};
 use std::sync::Arc;
 
 use anyhow::{anyhow, bail, Context as _, Result};
@@ -11,6 +11,15 @@ use semantic_version::SemanticVersion;
 
 pub use crate::extension_manifest::*;
 
+#[async_trait]
+pub trait WorktreeDelegate: Send + Sync + 'static {
+    fn id(&self) -> u64;
+    fn root_path(&self) -> String;
+    async fn read_text_file(&self, path: PathBuf) -> Result<String>;
+    async fn which(&self, binary_name: String) -> Option<String>;
+    async fn shell_env(&self) -> Vec<(String, String)>;
+}
+
 pub trait KeyValueStoreDelegate: Send + Sync + 'static {
     fn insert(&self, key: String, docs: String) -> Task<Result<()>>;
 }

crates/extension_host/src/extension_lsp_adapter.rs 🔗

@@ -5,6 +5,7 @@ use crate::wasm_host::{
 use anyhow::{anyhow, Context, Result};
 use async_trait::async_trait;
 use collections::HashMap;
+use extension::WorktreeDelegate;
 use futures::{Future, FutureExt};
 use gpui::AsyncAppContext;
 use language::{
@@ -18,6 +19,35 @@ use std::{any::Any, path::PathBuf, pin::Pin, sync::Arc};
 use util::{maybe, ResultExt};
 use wasmtime_wasi::WasiView as _;
 
+/// An adapter that allows an [`LspAdapterDelegate`] to be used as a [`WorktreeDelegate`].
+pub struct WorktreeDelegateAdapter(pub Arc<dyn LspAdapterDelegate>);
+
+#[async_trait]
+impl WorktreeDelegate for WorktreeDelegateAdapter {
+    fn id(&self) -> u64 {
+        self.0.worktree_id().to_proto()
+    }
+
+    fn root_path(&self) -> String {
+        self.0.worktree_root_path().to_string_lossy().to_string()
+    }
+
+    async fn read_text_file(&self, path: PathBuf) -> Result<String> {
+        self.0.read_text_file(path).await
+    }
+
+    async fn which(&self, binary_name: String) -> Option<String> {
+        self.0
+            .which(binary_name.as_ref())
+            .await
+            .map(|path| path.to_string_lossy().to_string())
+    }
+
+    async fn shell_env(&self) -> Vec<(String, String)> {
+        self.0.shell_env().await.into_iter().collect()
+    }
+}
+
 pub struct ExtensionLspAdapter {
     pub(crate) extension: WasmExtension,
     pub(crate) language_server_id: LanguageServerName,
@@ -45,6 +75,7 @@ impl LspAdapter for ExtensionLspAdapter {
                     let this = self.clone();
                     |extension, store| {
                         async move {
+                            let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
                             let resource = store.data_mut().table().push(delegate)?;
                             let command = extension
                                 .call_language_server_command(
@@ -166,6 +197,7 @@ impl LspAdapter for ExtensionLspAdapter {
                 let this = self.clone();
                 |extension, store| {
                     async move {
+                        let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
                         let resource = store.data_mut().table().push(delegate)?;
                         let options = extension
                             .call_language_server_initialization_options(
@@ -204,6 +236,7 @@ impl LspAdapter for ExtensionLspAdapter {
                 let this = self.clone();
                 |extension, store| {
                     async move {
+                        let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
                         let resource = store.data_mut().table().push(delegate)?;
                         let options = extension
                             .call_language_server_workspace_configuration(

crates/extension_host/src/wasm_host/wit.rs 🔗

@@ -3,14 +3,13 @@ mod since_v0_0_4;
 mod since_v0_0_6;
 mod since_v0_1_0;
 mod since_v0_2_0;
-use extension::KeyValueStoreDelegate;
+use extension::{KeyValueStoreDelegate, WorktreeDelegate};
 use lsp::LanguageServerName;
 use release_channel::ReleaseChannel;
 use since_v0_2_0 as latest;
 
 use super::{wasm_engine, WasmState};
 use anyhow::{anyhow, Context, Result};
-use language::LspAdapterDelegate;
 use semantic_version::SemanticVersion;
 use std::{ops::RangeInclusive, sync::Arc};
 use wasmtime::{
@@ -152,7 +151,7 @@ impl Extension {
         store: &mut Store<WasmState>,
         language_server_id: &LanguageServerName,
         config: &LanguageServerConfig,
-        resource: Resource<Arc<dyn LspAdapterDelegate>>,
+        resource: Resource<Arc<dyn WorktreeDelegate>>,
     ) -> Result<Result<Command, String>> {
         match self {
             Extension::V020(ext) => {
@@ -183,7 +182,7 @@ impl Extension {
         store: &mut Store<WasmState>,
         language_server_id: &LanguageServerName,
         config: &LanguageServerConfig,
-        resource: Resource<Arc<dyn LspAdapterDelegate>>,
+        resource: Resource<Arc<dyn WorktreeDelegate>>,
     ) -> Result<Result<Option<String>, String>> {
         match self {
             Extension::V020(ext) => {
@@ -229,7 +228,7 @@ impl Extension {
         &self,
         store: &mut Store<WasmState>,
         language_server_id: &LanguageServerName,
-        resource: Resource<Arc<dyn LspAdapterDelegate>>,
+        resource: Resource<Arc<dyn WorktreeDelegate>>,
     ) -> Result<Result<Option<String>, String>> {
         match self {
             Extension::V020(ext) => {
@@ -366,7 +365,7 @@ impl Extension {
         store: &mut Store<WasmState>,
         command: &SlashCommand,
         arguments: &[String],
-        resource: Option<Resource<Arc<dyn LspAdapterDelegate>>>,
+        resource: Option<Resource<Arc<dyn WorktreeDelegate>>>,
     ) -> Result<Result<SlashCommandOutput, String>> {
         match self {
             Extension::V020(ext) => {

crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs 🔗

@@ -3,7 +3,8 @@ use crate::wasm_host::wit::since_v0_0_4;
 use crate::wasm_host::WasmState;
 use anyhow::Result;
 use async_trait::async_trait;
-use language::{LanguageServerBinaryStatus, LspAdapterDelegate};
+use extension::WorktreeDelegate;
+use language::LanguageServerBinaryStatus;
 use semantic_version::SemanticVersion;
 use std::sync::{Arc, OnceLock};
 use wasmtime::component::{Linker, Resource};
@@ -21,7 +22,7 @@ wasmtime::component::bindgen!({
     },
 });
 
-pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
+pub type ExtensionWorktree = Arc<dyn WorktreeDelegate>;
 
 pub fn linker() -> &'static Linker<WasmState> {
     static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
@@ -62,7 +63,7 @@ impl From<Command> for latest::Command {
 impl HostWorktree for WasmState {
     async fn read_text_file(
         &mut self,
-        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
+        delegate: Resource<Arc<dyn WorktreeDelegate>>,
         path: String,
     ) -> wasmtime::Result<Result<String, String>> {
         latest::HostWorktree::read_text_file(self, delegate, path).await
@@ -70,14 +71,14 @@ impl HostWorktree for WasmState {
 
     async fn shell_env(
         &mut self,
-        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
+        delegate: Resource<Arc<dyn WorktreeDelegate>>,
     ) -> wasmtime::Result<EnvVars> {
         latest::HostWorktree::shell_env(self, delegate).await
     }
 
     async fn which(
         &mut self,
-        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
+        delegate: Resource<Arc<dyn WorktreeDelegate>>,
         binary_name: String,
     ) -> wasmtime::Result<Option<String>> {
         latest::HostWorktree::which(self, delegate, binary_name).await

crates/extension_host/src/wasm_host/wit/since_v0_0_4.rs 🔗

@@ -2,7 +2,7 @@ use super::latest;
 use crate::wasm_host::WasmState;
 use anyhow::Result;
 use async_trait::async_trait;
-use language::LspAdapterDelegate;
+use extension::WorktreeDelegate;
 use semantic_version::SemanticVersion;
 use std::sync::{Arc, OnceLock};
 use wasmtime::component::{Linker, Resource};
@@ -20,7 +20,7 @@ wasmtime::component::bindgen!({
     },
 });
 
-pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
+pub type ExtensionWorktree = Arc<dyn WorktreeDelegate>;
 
 pub fn linker() -> &'static Linker<WasmState> {
     static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
@@ -71,7 +71,7 @@ impl From<Command> for latest::Command {
 impl HostWorktree for WasmState {
     async fn read_text_file(
         &mut self,
-        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
+        delegate: Resource<Arc<dyn WorktreeDelegate>>,
         path: String,
     ) -> wasmtime::Result<Result<String, String>> {
         latest::HostWorktree::read_text_file(self, delegate, path).await
@@ -79,14 +79,14 @@ impl HostWorktree for WasmState {
 
     async fn shell_env(
         &mut self,
-        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
+        delegate: Resource<Arc<dyn WorktreeDelegate>>,
     ) -> wasmtime::Result<EnvVars> {
         latest::HostWorktree::shell_env(self, delegate).await
     }
 
     async fn which(
         &mut self,
-        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
+        delegate: Resource<Arc<dyn WorktreeDelegate>>,
         binary_name: String,
     ) -> wasmtime::Result<Option<String>> {
         latest::HostWorktree::which(self, delegate, binary_name).await

crates/extension_host/src/wasm_host/wit/since_v0_0_6.rs 🔗

@@ -2,7 +2,7 @@ use super::{latest, since_v0_1_0};
 use crate::wasm_host::WasmState;
 use anyhow::Result;
 use async_trait::async_trait;
-use language::LspAdapterDelegate;
+use extension::WorktreeDelegate;
 use semantic_version::SemanticVersion;
 use std::sync::{Arc, OnceLock};
 use wasmtime::component::{Linker, Resource};
@@ -26,7 +26,7 @@ mod settings {
     include!(concat!(env!("OUT_DIR"), "/since_v0.0.6/settings.rs"));
 }
 
-pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
+pub type ExtensionWorktree = Arc<dyn WorktreeDelegate>;
 
 pub fn linker() -> &'static Linker<WasmState> {
     static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
@@ -113,23 +113,20 @@ impl From<CodeLabel> for latest::CodeLabel {
 
 #[async_trait]
 impl HostWorktree for WasmState {
-    async fn id(
-        &mut self,
-        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
-    ) -> wasmtime::Result<u64> {
+    async fn id(&mut self, delegate: Resource<Arc<dyn WorktreeDelegate>>) -> wasmtime::Result<u64> {
         latest::HostWorktree::id(self, delegate).await
     }
 
     async fn root_path(
         &mut self,
-        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
+        delegate: Resource<Arc<dyn WorktreeDelegate>>,
     ) -> wasmtime::Result<String> {
         latest::HostWorktree::root_path(self, delegate).await
     }
 
     async fn read_text_file(
         &mut self,
-        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
+        delegate: Resource<Arc<dyn WorktreeDelegate>>,
         path: String,
     ) -> wasmtime::Result<Result<String, String>> {
         latest::HostWorktree::read_text_file(self, delegate, path).await
@@ -137,14 +134,14 @@ impl HostWorktree for WasmState {
 
     async fn shell_env(
         &mut self,
-        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
+        delegate: Resource<Arc<dyn WorktreeDelegate>>,
     ) -> wasmtime::Result<EnvVars> {
         latest::HostWorktree::shell_env(self, delegate).await
     }
 
     async fn which(
         &mut self,
-        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
+        delegate: Resource<Arc<dyn WorktreeDelegate>>,
         binary_name: String,
     ) -> wasmtime::Result<Option<String>> {
         latest::HostWorktree::which(self, delegate, binary_name).await

crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs 🔗

@@ -5,13 +5,11 @@ use anyhow::{anyhow, bail, Context, Result};
 use async_compression::futures::bufread::GzipDecoder;
 use async_tar::Archive;
 use async_trait::async_trait;
-use extension::KeyValueStoreDelegate;
+use extension::{KeyValueStoreDelegate, WorktreeDelegate};
 use futures::{io::BufReader, FutureExt as _};
 use futures::{lock::Mutex, AsyncReadExt};
 use language::LanguageName;
-use language::{
-    language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate,
-};
+use language::{language_settings::AllLanguageSettings, LanguageServerBinaryStatus};
 use project::project_settings::ProjectSettings;
 use semantic_version::SemanticVersion;
 use std::{
@@ -47,7 +45,7 @@ mod settings {
     include!(concat!(env!("OUT_DIR"), "/since_v0.1.0/settings.rs"));
 }
 
-pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
+pub type ExtensionWorktree = Arc<dyn WorktreeDelegate>;
 pub type ExtensionKeyValueStore = Arc<dyn KeyValueStoreDelegate>;
 pub type ExtensionHttpResponseStream = Arc<Mutex<::http_client::Response<AsyncBody>>>;
 
@@ -251,52 +249,38 @@ impl HostKeyValueStore for WasmState {
 
 #[async_trait]
 impl HostWorktree for WasmState {
-    async fn id(
-        &mut self,
-        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
-    ) -> wasmtime::Result<u64> {
-        let delegate = self.table.get(&delegate)?;
-        Ok(delegate.worktree_id().to_proto())
+    async fn id(&mut self, delegate: Resource<Arc<dyn WorktreeDelegate>>) -> wasmtime::Result<u64> {
+        latest::HostWorktree::id(self, delegate).await
     }
 
     async fn root_path(
         &mut self,
-        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
+        delegate: Resource<Arc<dyn WorktreeDelegate>>,
     ) -> wasmtime::Result<String> {
-        let delegate = self.table.get(&delegate)?;
-        Ok(delegate.worktree_root_path().to_string_lossy().to_string())
+        latest::HostWorktree::root_path(self, delegate).await
     }
 
     async fn read_text_file(
         &mut self,
-        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
+        delegate: Resource<Arc<dyn WorktreeDelegate>>,
         path: String,
     ) -> wasmtime::Result<Result<String, String>> {
-        let delegate = self.table.get(&delegate)?;
-        Ok(delegate
-            .read_text_file(path.into())
-            .await
-            .map_err(|error| error.to_string()))
+        latest::HostWorktree::read_text_file(self, delegate, path).await
     }
 
     async fn shell_env(
         &mut self,
-        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
+        delegate: Resource<Arc<dyn WorktreeDelegate>>,
     ) -> wasmtime::Result<EnvVars> {
-        let delegate = self.table.get(&delegate)?;
-        Ok(delegate.shell_env().await.into_iter().collect())
+        latest::HostWorktree::shell_env(self, delegate).await
     }
 
     async fn which(
         &mut self,
-        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
+        delegate: Resource<Arc<dyn WorktreeDelegate>>,
         binary_name: String,
     ) -> wasmtime::Result<Option<String>> {
-        let delegate = self.table.get(&delegate)?;
-        Ok(delegate
-            .which(binary_name.as_ref())
-            .await
-            .map(|path| path.to_string_lossy().to_string()))
+        latest::HostWorktree::which(self, delegate, binary_name).await
     }
 
     fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {

crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs 🔗

@@ -6,13 +6,10 @@ use async_compression::futures::bufread::GzipDecoder;
 use async_tar::Archive;
 use async_trait::async_trait;
 use context_servers::manager::ContextServerSettings;
-use extension::KeyValueStoreDelegate;
+use extension::{KeyValueStoreDelegate, WorktreeDelegate};
 use futures::{io::BufReader, FutureExt as _};
 use futures::{lock::Mutex, AsyncReadExt};
-use language::{
-    language_settings::AllLanguageSettings, LanguageName, LanguageServerBinaryStatus,
-    LspAdapterDelegate,
-};
+use language::{language_settings::AllLanguageSettings, LanguageName, LanguageServerBinaryStatus};
 use project::project_settings::ProjectSettings;
 use semantic_version::SemanticVersion;
 use std::{
@@ -44,7 +41,7 @@ mod settings {
     include!(concat!(env!("OUT_DIR"), "/since_v0.2.0/settings.rs"));
 }
 
-pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
+pub type ExtensionWorktree = Arc<dyn WorktreeDelegate>;
 pub type ExtensionKeyValueStore = Arc<dyn KeyValueStoreDelegate>;
 pub type ExtensionHttpResponseStream = Arc<Mutex<::http_client::Response<AsyncBody>>>;
 
@@ -93,25 +90,22 @@ impl HostProject for WasmState {
 
 #[async_trait]
 impl HostWorktree for WasmState {
-    async fn id(
-        &mut self,
-        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
-    ) -> wasmtime::Result<u64> {
+    async fn id(&mut self, delegate: Resource<Arc<dyn WorktreeDelegate>>) -> wasmtime::Result<u64> {
         let delegate = self.table.get(&delegate)?;
-        Ok(delegate.worktree_id().to_proto())
+        Ok(delegate.id())
     }
 
     async fn root_path(
         &mut self,
-        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
+        delegate: Resource<Arc<dyn WorktreeDelegate>>,
     ) -> wasmtime::Result<String> {
         let delegate = self.table.get(&delegate)?;
-        Ok(delegate.worktree_root_path().to_string_lossy().to_string())
+        Ok(delegate.root_path())
     }
 
     async fn read_text_file(
         &mut self,
-        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
+        delegate: Resource<Arc<dyn WorktreeDelegate>>,
         path: String,
     ) -> wasmtime::Result<Result<String, String>> {
         let delegate = self.table.get(&delegate)?;
@@ -123,7 +117,7 @@ impl HostWorktree for WasmState {
 
     async fn shell_env(
         &mut self,
-        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
+        delegate: Resource<Arc<dyn WorktreeDelegate>>,
     ) -> wasmtime::Result<EnvVars> {
         let delegate = self.table.get(&delegate)?;
         Ok(delegate.shell_env().await.into_iter().collect())
@@ -131,14 +125,11 @@ impl HostWorktree for WasmState {
 
     async fn which(
         &mut self,
-        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
+        delegate: Resource<Arc<dyn WorktreeDelegate>>,
         binary_name: String,
     ) -> wasmtime::Result<Option<String>> {
         let delegate = self.table.get(&delegate)?;
-        Ok(delegate
-            .which(binary_name.as_ref())
-            .await
-            .map(|path| path.to_string_lossy().to_string()))
+        Ok(delegate.which(binary_name).await)
     }
 
     fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {

crates/extensions_ui/src/extension_slash_command.rs 🔗

@@ -5,6 +5,7 @@ use assistant_slash_command::{
     ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
     SlashCommandResult,
 };
+use extension_host::extension_lsp_adapter::WorktreeDelegateAdapter;
 use futures::FutureExt as _;
 use gpui::{Task, WeakView, WindowContext};
 use language::{BufferSnapshot, LspAdapterDelegate};
@@ -97,6 +98,8 @@ impl SlashCommand for ExtensionSlashCommand {
                     move |extension, store| {
                         async move {
                             let resource = if let Some(delegate) = delegate {
+                                let delegate =
+                                    Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
                                 Some(store.data_mut().table().push(delegate)?)
                             } else {
                                 None