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

View file

@ -15,6 +15,8 @@ workspace = true
path = "src/extension_api.rs"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
wit-bindgen = "0.22"
[package.metadata.component]

View file

@ -1,9 +1,13 @@
//! The Zed Rust Extension API allows you write extensions for [Zed](https://zed.dev/) in Rust.
pub mod settings;
use core::fmt;
use wit::*;
pub use serde_json;
// WIT re-exports.
//
// We explicitly enumerate the symbols we want to re-export, as there are some
@ -62,7 +66,16 @@ pub trait Extension: Send + Sync {
&mut self,
_language_server_id: &LanguageServerId,
_worktree: &Worktree,
) -> Result<Option<String>> {
) -> Result<Option<serde_json::Value>> {
Ok(None)
}
/// Returns the workspace configuration options to pass to the language server.
fn language_server_workspace_configuration(
&mut self,
_language_server_id: &LanguageServerId,
_worktree: &Worktree,
) -> Result<Option<serde_json::Value>> {
Ok(None)
}
@ -142,7 +155,19 @@ impl wit::Guest for Component {
worktree: &Worktree,
) -> Result<Option<String>, String> {
let language_server_id = LanguageServerId(language_server_id);
extension().language_server_initialization_options(&language_server_id, worktree)
Ok(extension()
.language_server_initialization_options(&language_server_id, worktree)?
.and_then(|value| serde_json::to_string(&value).ok()))
}
fn language_server_workspace_configuration(
language_server_id: String,
worktree: &Worktree,
) -> Result<Option<String>, String> {
let language_server_id = LanguageServerId(language_server_id);
Ok(extension()
.language_server_workspace_configuration(&language_server_id, worktree)?
.and_then(|value| serde_json::to_string(&value).ok()))
}
fn labels_for_completions(

View file

@ -0,0 +1,30 @@
#[path = "../wit/since_v0.0.6/settings.rs"]
pub mod types;
use crate::{wit, Result, SettingsLocation, Worktree};
use serde_json;
pub use types::*;
impl LanguageSettings {
pub fn for_worktree(language: Option<&str>, worktree: &Worktree) -> Result<Self> {
let location = SettingsLocation {
worktree_id: worktree.id(),
path: worktree.root_path(),
};
let settings_json = wit::get_settings(Some(&location), "language", language)?;
let settings: Self = serde_json::from_str(&settings_json).map_err(|err| err.to_string())?;
Ok(settings)
}
}
impl LspSettings {
pub fn for_worktree(language_server_name: &str, worktree: &Worktree) -> Result<Self> {
let location = SettingsLocation {
worktree_id: worktree.id(),
path: worktree.root_path(),
};
let settings_json = wit::get_settings(Some(&location), "lsp", Some(language_server_name))?;
let settings: Self = serde_json::from_str(&settings_json).map_err(|err| err.to_string())?;
Ok(settings)
}
}

View file

@ -79,6 +79,13 @@ world extension {
/// Returns operating system and architecture for the current platform.
import current-platform: func() -> tuple<os, architecture>;
record settings-location {
worktree-id: u64,
path: string,
}
import get-settings: func(path: option<settings-location>, category: string, key: option<string>) -> result<string, string>;
/// Returns the path to the Node binary used by Zed.
import node-binary-path: func() -> result<string, string>;
@ -121,6 +128,10 @@ world extension {
/// A Zed worktree.
resource worktree {
/// Returns the ID of the worktree.
id: func() -> u64;
/// Returns the root path of the worktree.
root-path: func() -> string;
/// Returns the textual contents of the specified file in the worktree.
read-text-file: func(path: string) -> result<string, string>;
/// Returns the path to the given binary name, if one is present on the `$PATH`.
@ -137,6 +148,9 @@ world extension {
/// The initialization options are represented as a JSON string.
export language-server-initialization-options: func(language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
/// Returns the workspace configuration options to pass to the language server.
export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
record code-label {
/// The source code to parse with Tree-sitter.
code: string,

View file

@ -0,0 +1,20 @@
use serde::{Deserialize, Serialize};
use std::num::NonZeroU32;
#[derive(Debug, Serialize, Deserialize)]
pub struct LanguageSettings {
pub tab_size: NonZeroU32,
}
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct LspSettings {
pub binary: Option<BinarySettings>,
pub initialization_options: Option<serde_json::Value>,
pub settings: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BinarySettings {
pub path: Option<String>,
pub arguments: Option<Vec<String>>,
}

View file

@ -22,6 +22,7 @@ fn suggested_extensions() -> &'static HashMap<&'static str, Arc<str>> {
("clojure", "cljs"),
("clojure", "edn"),
("csharp", "cs"),
("dart", "dart"),
("dockerfile", "Dockerfile"),
("elisp", "el"),
("erlang", "erl"),

View file

@ -197,10 +197,6 @@ impl CachedLspAdapter {
self.adapter.code_action_kinds()
}
pub fn workspace_configuration(&self, workspace_root: &Path, cx: &mut AppContext) -> Value {
self.adapter.workspace_configuration(workspace_root, cx)
}
pub fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
self.adapter.process_diagnostics(params)
}
@ -243,6 +239,8 @@ impl CachedLspAdapter {
pub trait LspAdapterDelegate: Send + Sync {
fn show_notification(&self, message: &str, cx: &mut AppContext);
fn http_client(&self) -> Arc<dyn HttpClient>;
fn worktree_id(&self) -> u64;
fn worktree_root_path(&self) -> &Path;
fn update_status(&self, language: LanguageServerName, status: LanguageServerBinaryStatus);
async fn which(&self, command: &OsStr) -> Option<PathBuf>;
@ -445,8 +443,12 @@ pub trait LspAdapter: 'static + Send + Sync {
Ok(None)
}
fn workspace_configuration(&self, _workspace_root: &Path, _cx: &mut AppContext) -> Value {
serde_json::json!({})
async fn workspace_configuration(
self: Arc<Self>,
_: &Arc<dyn LspAdapterDelegate>,
_cx: &mut AsyncAppContext,
) -> Result<Value> {
Ok(serde_json::json!({}))
}
/// Returns a list of code actions supported by a given LspAdapter

View file

@ -40,7 +40,6 @@ tree-sitter-bash.workspace = true
tree-sitter-c.workspace = true
tree-sitter-cpp.workspace = true
tree-sitter-css.workspace = true
tree-sitter-dart.workspace = true
tree-sitter-elixir.workspace = true
tree-sitter-elm.workspace = true
tree-sitter-embedded-template.workspace = true

View file

@ -1,69 +0,0 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use gpui::AppContext;
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use project::project_settings::ProjectSettings;
use serde_json::Value;
use settings::Settings;
use std::{
any::Any,
path::{Path, PathBuf},
};
pub struct DartLanguageServer;
#[async_trait(?Send)]
impl LspAdapter for DartLanguageServer {
fn name(&self) -> LanguageServerName {
LanguageServerName("dart".into())
}
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
Ok(Box::new(()))
}
async fn fetch_server_binary(
&self,
_: Box<dyn 'static + Send + Any>,
_: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
Err(anyhow!("dart must me installed from dart.dev/get-dart"))
}
async fn cached_server_binary(
&self,
_: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
Some(LanguageServerBinary {
path: "dart".into(),
env: None,
arguments: vec!["language-server".into(), "--protocol=lsp".into()],
})
}
fn can_be_reinstalled(&self) -> bool {
false
}
async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
None
}
fn workspace_configuration(&self, _workspace_root: &Path, cx: &mut AppContext) -> Value {
let settings = ProjectSettings::get_global(cx)
.lsp
.get("dart")
.and_then(|s| s.settings.clone())
.unwrap_or_default();
serde_json::json!({
"dart": settings
})
}
}

View file

@ -1,6 +0,0 @@
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
("<" @open ">" @close)
("\"" @open "\"" @close)
("'" @open "'" @close)

View file

@ -1,13 +0,0 @@
name = "Dart"
grammar = "dart"
path_suffixes = ["dart"]
line_comments = ["// "]
autoclose_before = ";:.,=}])>"
brackets = [
{ start = "{", end = "}", close = true, newline = true },
{ start = "[", end = "]", close = true, newline = true },
{ start = "(", end = ")", close = true, newline = true },
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
{ start = "'", end = "'", close = true, newline = false, not_in = ["string"] },
{ start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
]

View file

@ -1,209 +0,0 @@
(dotted_identifier_list) @string
; Methods
; --------------------
(function_type
name: (identifier) @function)
(super) @function
; Annotations
; --------------------
(annotation
name: (identifier) @attribute)
; Operators and Tokens
; --------------------
(template_substitution
"$" @punctuation.special
"{" @punctuation.special
"}" @punctuation.special
) @none
(template_substitution
"$" @punctuation.special
(identifier_dollar_escaped) @variable
) @none
(escape_sequence) @string.escape
[
"@"
"=>"
".."
"??"
"=="
"?"
":"
"&&"
"%"
"<"
">"
"="
">="
"<="
"||"
(increment_operator)
(is_operator)
(prefix_operator)
(equality_operator)
(additive_operator)
] @operator
[
"("
")"
"["
"]"
"{"
"}"
"<"
">"
] @punctuation.bracket
; Delimiters
; --------------------
[
";"
"."
","
] @punctuation.delimiter
; Types
; --------------------
(class_definition
name: (identifier) @type)
(constructor_signature
name: (identifier) @type)
(scoped_identifier
scope: (identifier) @type)
(function_signature
name: (identifier) @function)
(getter_signature
(identifier) @function)
(setter_signature
name: (identifier) @function)
(enum_declaration
name: (identifier) @type)
(enum_constant
name: (identifier) @type)
(type_identifier) @type
(void_type) @type
((scoped_identifier
scope: (identifier) @type
name: (identifier) @type)
(#match? @type "^[a-zA-Z]"))
(type_identifier) @type
; Variables
; --------------------
; var keyword
(inferred_type) @keyword
(const_builtin) @constant.builtin
(final_builtin) @constant.builtin
((identifier) @type
(#match? @type "^_?[A-Z]"))
("Function" @type)
; properties
; TODO: add method/call_expression to grammar and
; distinguish method call from variable access
(unconditional_assignable_selector
(identifier) @property)
; assignments
(assignment_expression
left: (assignable_expression) @variable)
(this) @variable.builtin
; Literals
; --------------------
[
(hex_integer_literal)
(decimal_integer_literal)
(decimal_floating_point_literal)
; TODO: inaccessible nodes
; (octal_integer_literal)
; (hex_floating_point_literal)
] @number
(symbol_literal) @symbol
(string_literal) @string
(true) @boolean
(false) @boolean
(null_literal) @constant.builtin
(documentation_comment) @comment
(comment) @comment
; Keywords
; --------------------
["import" "library" "export"] @keyword.include
; Reserved words (cannot be used as identifiers)
; TODO: "rethrow" @keyword
[
; "assert"
(case_builtin)
"extension"
"on"
"class"
"enum"
"extends"
"in"
"is"
"new"
"return"
"super"
"with"
] @keyword
; Built in identifiers:
; alone these are marked as keywords
[
"abstract"
"as"
"async"
"async*"
"yield"
"sync*"
"await"
"covariant"
"deferred"
"dynamic"
"external"
"factory"
"get"
"implements"
"interface"
"library"
"operator"
"mixin"
"part"
"set"
"show"
"static"
"typedef"
] @keyword
; when used as an identifier:
((identifier) @variable.builtin
(#vim-match? @variable.builtin "^(abstract|as|covariant|deferred|dynamic|export|external|factory|Function|get|implements|import|interface|library|operator|mixin|part|set|static|typedef)$"))
["if" "else" "switch" "default"] @keyword
[
"try"
"throw"
"catch"
"finally"
(break_statement)
] @keyword
["do" "while" "continue" "for"] @keyword

View file

@ -1,7 +0,0 @@
[
(if_statement)
(for_statement)
] @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent

View file

@ -1,18 +0,0 @@
(class_definition
"class" @context
name: (_) @name) @item
(function_signature
name: (_) @name) @item
(getter_signature
"get" @context
name: (_) @name) @item
(setter_signature
"set" @context
name: (_) @name) @item
(enum_declaration
"enum" @context
name: (_) @name) @item

View file

@ -1,7 +1,7 @@
use anyhow::{anyhow, bail, Context, Result};
use async_trait::async_trait;
use futures::StreamExt;
use gpui::{AppContext, AsyncAppContext, Task};
use gpui::{AsyncAppContext, Task};
pub use language::*;
use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind};
use project::project_settings::ProjectSettings;
@ -14,7 +14,7 @@ use std::{
any::Any,
env::consts,
ops::Deref,
path::{Path, PathBuf},
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
@ -278,16 +278,22 @@ impl LspAdapter for ElixirLspAdapter {
})
}
fn workspace_configuration(&self, _workspace_root: &Path, cx: &mut AppContext) -> Value {
let settings = ProjectSettings::get_global(cx)
.lsp
.get("elixir-ls")
.and_then(|s| s.settings.clone())
.unwrap_or_default();
async fn workspace_configuration(
self: Arc<Self>,
_: &Arc<dyn LspAdapterDelegate>,
cx: &mut AsyncAppContext,
) -> Result<Value> {
let settings = cx.update(|cx| {
ProjectSettings::get_global(cx)
.lsp
.get("elixir-ls")
.and_then(|s| s.settings.clone())
.unwrap_or_default()
})?;
serde_json::json!({
Ok(serde_json::json!({
"elixirLS": settings
})
}))
}
}

View file

@ -1,7 +1,7 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use futures::StreamExt;
use gpui::AppContext;
use gpui::AsyncAppContext;
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
@ -94,16 +94,22 @@ impl LspAdapter for ElmLspAdapter {
get_cached_server_binary(container_dir, &*self.node).await
}
fn workspace_configuration(&self, _workspace_root: &Path, cx: &mut AppContext) -> Value {
async fn workspace_configuration(
self: Arc<Self>,
_: &Arc<dyn LspAdapterDelegate>,
cx: &mut AsyncAppContext,
) -> Result<Value> {
// elm-language-server expects workspace didChangeConfiguration notification
// params to be the same as lsp initialization_options
let override_options = ProjectSettings::get_global(cx)
.lsp
.get(SERVER_NAME)
.and_then(|s| s.initialization_options.clone())
.unwrap_or_default();
let override_options = cx.update(|cx| {
ProjectSettings::get_global(cx)
.lsp
.get(SERVER_NAME)
.and_then(|s| s.initialization_options.clone())
.unwrap_or_default()
})?;
match override_options.clone().as_object_mut() {
Ok(match override_options.clone().as_object_mut() {
Some(op) => {
// elm-language-server requests workspace configuration
// for the `elmLS` section, so we have to nest
@ -112,7 +118,7 @@ impl LspAdapter for ElmLspAdapter {
serde_json::to_value(op).unwrap_or_default()
}
None => override_options,
}
})
}
}

View file

@ -3,7 +3,7 @@ use async_trait::async_trait;
use collections::HashMap;
use feature_flags::FeatureFlagAppExt;
use futures::StreamExt;
use gpui::AppContext;
use gpui::{AppContext, AsyncAppContext};
use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
@ -152,10 +152,16 @@ impl LspAdapter for JsonLspAdapter {
})))
}
fn workspace_configuration(&self, _workspace_root: &Path, cx: &mut AppContext) -> Value {
self.workspace_config
.get_or_init(|| Self::get_workspace_config(self.languages.language_names(), cx))
.clone()
async fn workspace_configuration(
self: Arc<Self>,
_: &Arc<dyn LspAdapterDelegate>,
cx: &mut AsyncAppContext,
) -> Result<Value> {
cx.update(|cx| {
self.workspace_config
.get_or_init(|| Self::get_workspace_config(self.languages.language_names(), cx))
.clone()
})
}
fn language_ids(&self) -> HashMap<String, String> {

View file

@ -13,7 +13,6 @@ use self::{deno::DenoSettings, elixir::ElixirSettings};
mod c;
mod css;
mod dart;
mod deno;
mod elixir;
mod elm;
@ -92,7 +91,6 @@ pub fn init(
("typescript", tree_sitter_typescript::language_typescript()),
("vue", tree_sitter_vue::language()),
("yaml", tree_sitter_yaml::language()),
("dart", tree_sitter_dart::language()),
]);
macro_rules! language {
@ -312,7 +310,6 @@ pub fn init(
vec![Arc::new(terraform::TerraformLspAdapter)]
);
language!("hcl", vec![]);
language!("dart", vec![Arc::new(dart::DartLanguageServer {})]);
languages.register_secondary_lsp_adapter(
"Astro".into(),

View file

@ -2,7 +2,7 @@ use anyhow::{anyhow, Result};
use async_trait::async_trait;
use collections::HashMap;
use futures::StreamExt;
use gpui::AppContext;
use gpui::AsyncAppContext;
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
@ -107,12 +107,16 @@ impl LspAdapter for TailwindLspAdapter {
})))
}
fn workspace_configuration(&self, _workspace_root: &Path, _: &mut AppContext) -> Value {
json!({
async fn workspace_configuration(
self: Arc<Self>,
_: &Arc<dyn LspAdapterDelegate>,
_cx: &mut AsyncAppContext,
) -> Result<Value> {
Ok(json!({
"tailwindCSS": {
"emmetCompletions": true,
}
})
}))
}
fn language_ids(&self) -> HashMap<String, String> {

View file

@ -3,7 +3,7 @@ use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use collections::HashMap;
use gpui::AppContext;
use gpui::AsyncAppContext;
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::{CodeActionKind, LanguageServerBinary};
use node_runtime::NodeRuntime;
@ -245,12 +245,20 @@ impl EsLintLspAdapter {
#[async_trait(?Send)]
impl LspAdapter for EsLintLspAdapter {
fn workspace_configuration(&self, workspace_root: &Path, cx: &mut AppContext) -> Value {
let eslint_user_settings = ProjectSettings::get_global(cx)
.lsp
.get(Self::SERVER_NAME)
.and_then(|s| s.settings.clone())
.unwrap_or_default();
async fn workspace_configuration(
self: Arc<Self>,
delegate: &Arc<dyn LspAdapterDelegate>,
cx: &mut AsyncAppContext,
) -> Result<Value> {
let workspace_root = delegate.worktree_root_path();
let eslint_user_settings = cx.update(|cx| {
ProjectSettings::get_global(cx)
.lsp
.get(Self::SERVER_NAME)
.and_then(|s| s.settings.clone())
.unwrap_or_default()
})?;
let mut code_action_on_save = json!({
// We enable this, but without also configuring `code_actions_on_format`
@ -283,7 +291,7 @@ impl LspAdapter for EsLintLspAdapter {
.iter()
.any(|file| workspace_root.join(file).is_file());
json!({
Ok(json!({
"": {
"validate": "on",
"rulesCustomizations": [],
@ -301,7 +309,7 @@ impl LspAdapter for EsLintLspAdapter {
"useFlatConfig": use_flat_config,
},
}
})
}))
}
fn name(&self) -> LanguageServerName {

View file

@ -1,7 +1,7 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use futures::StreamExt;
use gpui::AppContext;
use gpui::AsyncAppContext;
use language::{
language_settings::all_language_settings, LanguageServerName, LspAdapter, LspAdapterDelegate,
};
@ -92,17 +92,26 @@ impl LspAdapter for YamlLspAdapter {
) -> Option<LanguageServerBinary> {
get_cached_server_binary(container_dir, &*self.node).await
}
fn workspace_configuration(&self, _workspace_root: &Path, cx: &mut AppContext) -> Value {
serde_json::json!({
async fn workspace_configuration(
self: Arc<Self>,
_: &Arc<dyn LspAdapterDelegate>,
cx: &mut AsyncAppContext,
) -> Result<Value> {
let tab_size = cx.update(|cx| {
all_language_settings(None, cx)
.language(Some("YAML"))
.tab_size
})?;
Ok(serde_json::json!({
"yaml": {
"keyOrdering": false
},
"[yaml]": {
"editor.tabSize": all_language_settings(None, cx)
.language(Some("YAML"))
.tab_size,
"editor.tabSize": tab_size
}
})
}))
}
}

View file

@ -2857,21 +2857,29 @@ impl Project {
cx.spawn(move |this, mut cx| async move {
while let Some(()) = settings_changed_rx.next().await {
let servers: Vec<_> = this.update(&mut cx, |this, _| {
this.language_servers
.values()
.filter_map(|state| match state {
LanguageServerState::Starting(_) => None,
LanguageServerState::Running {
adapter, server, ..
} => Some((adapter.clone(), server.clone())),
let servers = this.update(&mut cx, |this, cx| {
this.language_server_ids
.iter()
.filter_map(|((worktree_id, _), server_id)| {
let worktree = this.worktree_for_id(*worktree_id, cx)?;
let state = this.language_servers.get(server_id)?;
let delegate = ProjectLspAdapterDelegate::new(this, &worktree, cx);
match state {
LanguageServerState::Starting(_) => None,
LanguageServerState::Running {
adapter, server, ..
} => Some((
adapter.adapter.clone(),
server.clone(),
delegate as Arc<dyn LspAdapterDelegate>,
)),
}
})
.collect()
.collect::<Vec<_>>()
})?;
for (adapter, server) in servers {
let settings =
cx.update(|cx| adapter.workspace_configuration(server.root_path(), cx))?;
for (adapter, server, delegate) in servers {
let settings = adapter.workspace_configuration(&delegate, &mut cx).await?;
server
.notify::<lsp::notification::DidChangeConfiguration>(
@ -2985,12 +2993,13 @@ impl Project {
}
let stderr_capture = Arc::new(Mutex::new(Some(String::new())));
let lsp_adapter_delegate = ProjectLspAdapterDelegate::new(self, worktree_handle, cx);
let pending_server = match self.languages.create_pending_language_server(
stderr_capture.clone(),
language.clone(),
adapter.clone(),
Arc::clone(&worktree_path),
ProjectLspAdapterDelegate::new(self, worktree_handle, cx),
lsp_adapter_delegate.clone(),
cx,
) {
Some(pending_server) => pending_server,
@ -3018,7 +3027,7 @@ impl Project {
cx.spawn(move |this, mut cx| async move {
let result = Self::setup_and_insert_language_server(
this.clone(),
&worktree_path,
lsp_adapter_delegate,
override_options,
pending_server,
adapter.clone(),
@ -3142,7 +3151,7 @@ impl Project {
#[allow(clippy::too_many_arguments)]
async fn setup_and_insert_language_server(
this: WeakModel<Self>,
worktree_path: &Path,
delegate: Arc<dyn LspAdapterDelegate>,
override_initialization_options: Option<serde_json::Value>,
pending_server: PendingLanguageServer,
adapter: Arc<CachedLspAdapter>,
@ -3155,7 +3164,7 @@ impl Project {
this.clone(),
override_initialization_options,
pending_server,
worktree_path,
delegate,
adapter.clone(),
server_id,
cx,
@ -3185,13 +3194,16 @@ impl Project {
this: WeakModel<Self>,
override_options: Option<serde_json::Value>,
pending_server: PendingLanguageServer,
worktree_path: &Path,
delegate: Arc<dyn LspAdapterDelegate>,
adapter: Arc<CachedLspAdapter>,
server_id: LanguageServerId,
cx: &mut AsyncAppContext,
) -> Result<Arc<LanguageServer>> {
let workspace_config =
cx.update(|cx| adapter.workspace_configuration(worktree_path, cx))?;
let workspace_config = adapter
.adapter
.clone()
.workspace_configuration(&delegate, cx)
.await?;
let (language_server, mut initialization_options) = pending_server.task.await?;
let name = language_server.name();
@ -3220,14 +3232,14 @@ impl Project {
language_server
.on_request::<lsp::request::WorkspaceConfiguration, _, _>({
let adapter = adapter.clone();
let worktree_path = worktree_path.to_path_buf();
move |params, cx| {
let adapter = adapter.adapter.clone();
let delegate = delegate.clone();
move |params, mut cx| {
let adapter = adapter.clone();
let worktree_path = worktree_path.clone();
let delegate = delegate.clone();
async move {
let workspace_config =
cx.update(|cx| adapter.workspace_configuration(&worktree_path, cx))?;
adapter.workspace_configuration(&delegate, &mut cx).await?;
Ok(params
.items
.into_iter()
@ -10315,6 +10327,14 @@ impl LspAdapterDelegate for ProjectLspAdapterDelegate {
self.http_client.clone()
}
fn worktree_id(&self) -> u64 {
self.worktree.id().to_proto()
}
fn worktree_root_path(&self) -> &Path {
self.worktree.abs_path().as_ref()
}
async fn shell_env(&self) -> HashMap<String, String> {
self.load_shell_env().await;
self.shell_env.lock().as_ref().cloned().unwrap_or_default()

View file

@ -47,7 +47,7 @@ pub struct BinarySettings {
pub arguments: Option<Vec<String>>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct LspSettings {
pub binary: Option<BinarySettings>,