Add language_server_workspace_configuration to extension API (#10212)

This PR adds the ability for extensions to implement
`language_server_workspace_configuration` to provide workspace
configuration to the language server.

We've used the Dart extension as a motivating example for this, pulling
it out into an extension in the process.

Release Notes:

- Removed built-in support for Dart, in favor of making it available as
an extension. The Dart extension will be suggested for download when you
open a `.dart` file.

---------

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
Marshall Bowers 2024-04-05 17:04:07 -04:00 committed by GitHub
parent 4aaf3459c4
commit c851e6edba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 586 additions and 187 deletions

View file

@ -12,6 +12,7 @@ use language::{
};
use lsp::LanguageServerBinary;
use serde::Serialize;
use serde_json::Value;
use std::ops::Range;
use std::{
any::Any,
@ -181,6 +182,42 @@ impl LspAdapter for ExtensionLspAdapter {
})
}
async fn workspace_configuration(
self: Arc<Self>,
delegate: &Arc<dyn LspAdapterDelegate>,
_cx: &mut AsyncAppContext,
) -> Result<Value> {
let delegate = delegate.clone();
let json_options: Option<String> = self
.extension
.call({
let this = self.clone();
|extension, store| {
async move {
let resource = store.data_mut().table().push(delegate)?;
let options = extension
.call_language_server_workspace_configuration(
store,
&this.language_server_id,
resource,
)
.await?
.map_err(|e| anyhow!("{}", e))?;
anyhow::Ok(options)
}
.boxed()
}
})
.await?;
Ok(if let Some(json_options) = json_options {
serde_json::from_str(&json_options).with_context(|| {
format!("failed to parse initialization_options from extension: {json_options}")
})?
} else {
serde_json::json!({})
})
}
async fn labels_for_completions(
self: Arc<Self>,
completions: &[lsp::CompletionItem],

View file

@ -237,6 +237,7 @@ impl ExtensionStore {
node_runtime,
language_registry.clone(),
work_dir,
cx,
),
wasm_extensions: Vec::new(),
fs,

View file

@ -3,6 +3,7 @@ pub(crate) mod wit;
use crate::ExtensionManifest;
use anyhow::{anyhow, bail, Context as _, Result};
use fs::{normalize_path, Fs};
use futures::future::LocalBoxFuture;
use futures::{
channel::{
mpsc::{self, UnboundedSender},
@ -11,7 +12,7 @@ use futures::{
future::BoxFuture,
Future, FutureExt, StreamExt as _,
};
use gpui::BackgroundExecutor;
use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task};
use language::LanguageRegistry;
use node_runtime::NodeRuntime;
use semantic_version::SemanticVersion;
@ -34,6 +35,8 @@ pub(crate) struct WasmHost {
pub(crate) language_registry: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
pub(crate) work_dir: PathBuf,
_main_thread_message_task: Task<()>,
main_thread_message_tx: mpsc::UnboundedSender<MainThreadCall>,
}
#[derive(Clone)]
@ -51,6 +54,9 @@ pub(crate) struct WasmState {
pub(crate) host: Arc<WasmHost>,
}
type MainThreadCall =
Box<dyn Send + for<'a> FnOnce(&'a mut AsyncAppContext) -> LocalBoxFuture<'a, ()>>;
type ExtensionCall = Box<
dyn Send + for<'a> FnOnce(&'a mut Extension, &'a mut Store<WasmState>) -> BoxFuture<'a, ()>,
>;
@ -75,7 +81,14 @@ impl WasmHost {
node_runtime: Arc<dyn NodeRuntime>,
language_registry: Arc<LanguageRegistry>,
work_dir: PathBuf,
cx: &mut AppContext,
) -> Arc<Self> {
let (tx, mut rx) = mpsc::unbounded::<MainThreadCall>();
let task = cx.spawn(|mut cx| async move {
while let Some(message) = rx.next().await {
message(&mut cx).await;
}
});
Arc::new(Self {
engine: wasm_engine(),
fs,
@ -83,6 +96,8 @@ impl WasmHost {
http_client,
node_runtime,
language_registry,
_main_thread_message_task: task,
main_thread_message_tx: tx,
})
}
@ -238,6 +253,26 @@ impl WasmExtension {
}
impl WasmState {
fn on_main_thread<T, Fn>(&self, f: Fn) -> impl 'static + Future<Output = T>
where
T: 'static + Send,
Fn: 'static + Send + for<'a> FnOnce(&'a mut AsyncAppContext) -> LocalBoxFuture<'a, T>,
{
let (return_tx, return_rx) = oneshot::channel();
self.host
.main_thread_message_tx
.clone()
.unbounded_send(Box::new(move |cx| {
async {
let result = f(cx).await;
return_tx.send(result).ok();
}
.boxed_local()
}))
.expect("main thread message channel should not be closed yet");
async move { return_rx.await.expect("main thread message channel") }
}
fn work_dir(&self) -> PathBuf {
self.host.work_dir.join(self.manifest.id.as_ref())
}

View file

@ -148,6 +148,25 @@ impl Extension {
}
}
pub async fn call_language_server_workspace_configuration(
&self,
store: &mut Store<WasmState>,
language_server_id: &LanguageServerName,
resource: Resource<Arc<dyn LspAdapterDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V006(ext) => {
ext.call_language_server_workspace_configuration(
store,
&language_server_id.0,
resource,
)
.await
}
Extension::V004(_) | Extension::V001(_) => Ok(Ok(None)),
}
}
pub async fn call_labels_for_completions(
&self,
store: &mut Store<WasmState>,

View file

@ -1,16 +1,18 @@
use crate::wasm_host::wit::ToWasmtimeResult;
use crate::wasm_host::WasmState;
use anyhow::{anyhow, Result};
use ::settings::Settings;
use anyhow::{anyhow, bail, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use futures::io::BufReader;
use futures::{io::BufReader, FutureExt as _};
use language::language_settings::AllLanguageSettings;
use language::{LanguageServerBinaryStatus, LspAdapterDelegate};
use project::project_settings::ProjectSettings;
use semantic_version::SemanticVersion;
use std::path::Path;
use std::{
env,
path::PathBuf,
path::{Path, PathBuf},
sync::{Arc, OnceLock},
};
use util::maybe;
@ -27,6 +29,10 @@ wasmtime::component::bindgen!({
},
});
mod settings {
include!("../../../../extension_api/wit/since_v0.0.6/settings.rs");
}
pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
pub fn linker() -> &'static Linker<WasmState> {
@ -36,6 +42,22 @@ pub fn linker() -> &'static Linker<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())
}
async fn root_path(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<String> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.worktree_root_path().to_string_lossy().to_string())
}
async fn read_text_file(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
@ -78,6 +100,58 @@ impl self::zed::extension::lsp::Host for WasmState {}
#[async_trait]
impl ExtensionImports for WasmState {
async fn get_settings(
&mut self,
location: Option<self::SettingsLocation>,
category: String,
key: Option<String>,
) -> wasmtime::Result<Result<String, String>> {
self.on_main_thread(|cx| {
async move {
let location = location
.as_ref()
.map(|location| ::settings::SettingsLocation {
worktree_id: location.worktree_id as usize,
path: Path::new(&location.path),
});
cx.update(|cx| match category.as_str() {
"language" => {
let settings =
AllLanguageSettings::get(location, cx).language(key.as_deref());
Ok(serde_json::to_string(&settings::LanguageSettings {
tab_size: settings.tab_size,
})?)
}
"lsp" => {
let settings = key
.and_then(|key| {
ProjectSettings::get_global(cx)
.lsp
.get(&Arc::<str>::from(key))
})
.cloned()
.unwrap_or_default();
Ok(serde_json::to_string(&settings::LspSettings {
binary: settings.binary.map(|binary| settings::BinarySettings {
path: binary.path,
arguments: binary.arguments,
}),
settings: settings.settings,
initialization_options: settings.initialization_options,
})?)
}
_ => {
bail!("Unknown settings category: {}", category);
}
})
}
.boxed_local()
})
.await?
.to_wasmtime_result()
}
async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
self.host
.node_runtime