From c851e6edba17cfa0a8088a048a2c8faa1596788a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 5 Apr 2024 17:04:07 -0400 Subject: [PATCH] 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 Co-authored-by: Max Brunsfeld --- Cargo.lock | 21 ++- Cargo.toml | 2 +- crates/extension/src/extension_lsp_adapter.rs | 37 ++++++ crates/extension/src/extension_store.rs | 1 + crates/extension/src/wasm_host.rs | 37 +++++- crates/extension/src/wasm_host/wit.rs | 19 +++ .../src/wasm_host/wit/since_v0_0_6.rs | 82 +++++++++++- crates/extension_api/Cargo.toml | 2 + crates/extension_api/src/extension_api.rs | 29 ++++- crates/extension_api/src/settings.rs | 30 +++++ .../wit/since_v0.0.6/extension.wit | 14 ++ .../wit/since_v0.0.6/settings.rs | 20 +++ crates/extensions_ui/src/extension_suggest.rs | 1 + crates/language/src/language.rs | 14 +- crates/languages/Cargo.toml | 1 - crates/languages/src/dart.rs | 69 ---------- crates/languages/src/elixir.rs | 26 ++-- crates/languages/src/elm.rs | 24 ++-- crates/languages/src/json.rs | 16 ++- crates/languages/src/lib.rs | 3 - crates/languages/src/tailwind.rs | 12 +- crates/languages/src/typescript.rs | 26 ++-- crates/languages/src/yaml.rs | 23 +++- crates/project/src/project.rs | 68 ++++++---- crates/project/src/project_settings.rs | 2 +- extensions/dart/Cargo.toml | 16 +++ extensions/dart/LICENSE-APACHE | 1 + extensions/dart/extension.toml | 15 +++ .../dart/languages}/dart/brackets.scm | 0 .../dart/languages}/dart/config.toml | 0 .../dart/languages}/dart/highlights.scm | 0 .../dart/languages}/dart/indents.scm | 0 .../dart/languages}/dart/outline.scm | 0 extensions/dart/src/dart.rs | 123 ++++++++++++++++++ extensions/svelte/Cargo.toml | 3 +- extensions/svelte/src/svelte.rs | 36 +++-- 36 files changed, 586 insertions(+), 187 deletions(-) create mode 100644 crates/extension_api/src/settings.rs create mode 100644 crates/extension_api/wit/since_v0.0.6/settings.rs delete mode 100644 crates/languages/src/dart.rs create mode 100644 extensions/dart/Cargo.toml create mode 120000 extensions/dart/LICENSE-APACHE create mode 100644 extensions/dart/extension.toml rename {crates/languages/src => extensions/dart/languages}/dart/brackets.scm (100%) rename {crates/languages/src => extensions/dart/languages}/dart/config.toml (100%) rename {crates/languages/src => extensions/dart/languages}/dart/highlights.scm (100%) rename {crates/languages/src => extensions/dart/languages}/dart/indents.scm (100%) rename {crates/languages/src => extensions/dart/languages}/dart/outline.scm (100%) create mode 100644 extensions/dart/src/dart.rs diff --git a/Cargo.lock b/Cargo.lock index 4ef776a3a8..ae41755be8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5338,7 +5338,6 @@ dependencies = [ "tree-sitter-c", "tree-sitter-cpp", "tree-sitter-css", - "tree-sitter-dart", "tree-sitter-elixir", "tree-sitter-elm", "tree-sitter-embedded-template", @@ -10217,15 +10216,6 @@ dependencies = [ "tree-sitter", ] -[[package]] -name = "tree-sitter-dart" -version = "0.0.1" -source = "git+https://github.com/agent3bood/tree-sitter-dart?rev=48934e3bf757a9b78f17bdfaa3e2b4284656fdc7#48934e3bf757a9b78f17bdfaa3e2b4284656fdc7" -dependencies = [ - "cc", - "tree-sitter", -] - [[package]] name = "tree-sitter-elixir" version = "0.1.0" @@ -12490,6 +12480,13 @@ dependencies = [ "zed_extension_api 0.0.4", ] +[[package]] +name = "zed_dart" +version = "0.0.1" +dependencies = [ + "zed_extension_api 0.0.6", +] + [[package]] name = "zed_emmet" version = "0.0.1" @@ -12526,6 +12523,8 @@ dependencies = [ name = "zed_extension_api" version = "0.0.6" dependencies = [ + "serde", + "serde_json", "wit-bindgen", ] @@ -12575,7 +12574,7 @@ dependencies = [ name = "zed_svelte" version = "0.0.1" dependencies = [ - "zed_extension_api 0.0.4", + "zed_extension_api 0.0.6", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0955d9bdc9..92a20f4b0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,6 +102,7 @@ members = [ "extensions/astro", "extensions/clojure", "extensions/csharp", + "extensions/dart", "extensions/emmet", "extensions/erlang", "extensions/gleam", @@ -308,7 +309,6 @@ tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", re tree-sitter-c = "0.20.1" tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "f44509141e7e483323d2ec178f2d2e6c0fc041c1" } tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" } -tree-sitter-dart = { git = "https://github.com/agent3bood/tree-sitter-dart", rev = "48934e3bf757a9b78f17bdfaa3e2b4284656fdc7" } tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "a2861e88a730287a60c11ea9299c033c7d076e30" } tree-sitter-elm = { git = "https://github.com/elm-tooling/tree-sitter-elm", rev = "692c50c0b961364c40299e73c1306aecb5d20f40" } tree-sitter-embedded-template = "0.20.0" diff --git a/crates/extension/src/extension_lsp_adapter.rs b/crates/extension/src/extension_lsp_adapter.rs index 1e535f14c1..b64a15a1cd 100644 --- a/crates/extension/src/extension_lsp_adapter.rs +++ b/crates/extension/src/extension_lsp_adapter.rs @@ -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, + delegate: &Arc, + _cx: &mut AsyncAppContext, + ) -> Result { + let delegate = delegate.clone(); + let json_options: Option = 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, completions: &[lsp::CompletionItem], diff --git a/crates/extension/src/extension_store.rs b/crates/extension/src/extension_store.rs index 2634fc1e68..9a309e2231 100644 --- a/crates/extension/src/extension_store.rs +++ b/crates/extension/src/extension_store.rs @@ -237,6 +237,7 @@ impl ExtensionStore { node_runtime, language_registry.clone(), work_dir, + cx, ), wasm_extensions: Vec::new(), fs, diff --git a/crates/extension/src/wasm_host.rs b/crates/extension/src/wasm_host.rs index 6bfe6537f1..e4c97d2213 100644 --- a/crates/extension/src/wasm_host.rs +++ b/crates/extension/src/wasm_host.rs @@ -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, fs: Arc, pub(crate) work_dir: PathBuf, + _main_thread_message_task: Task<()>, + main_thread_message_tx: mpsc::UnboundedSender, } #[derive(Clone)] @@ -51,6 +54,9 @@ pub(crate) struct WasmState { pub(crate) host: Arc, } +type MainThreadCall = + Box FnOnce(&'a mut AsyncAppContext) -> LocalBoxFuture<'a, ()>>; + type ExtensionCall = Box< dyn Send + for<'a> FnOnce(&'a mut Extension, &'a mut Store) -> BoxFuture<'a, ()>, >; @@ -75,7 +81,14 @@ impl WasmHost { node_runtime: Arc, language_registry: Arc, work_dir: PathBuf, + cx: &mut AppContext, ) -> Arc { + let (tx, mut rx) = mpsc::unbounded::(); + 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(&self, f: Fn) -> impl 'static + Future + 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()) } diff --git a/crates/extension/src/wasm_host/wit.rs b/crates/extension/src/wasm_host/wit.rs index 8a15e0060b..5704b91810 100644 --- a/crates/extension/src/wasm_host/wit.rs +++ b/crates/extension/src/wasm_host/wit.rs @@ -148,6 +148,25 @@ impl Extension { } } + pub async fn call_language_server_workspace_configuration( + &self, + store: &mut Store, + language_server_id: &LanguageServerName, + resource: Resource>, + ) -> Result, 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, diff --git a/crates/extension/src/wasm_host/wit/since_v0_0_6.rs b/crates/extension/src/wasm_host/wit/since_v0_0_6.rs index a46de96afa..27c08e2d44 100644 --- a/crates/extension/src/wasm_host/wit/since_v0_0_6.rs +++ b/crates/extension/src/wasm_host/wit/since_v0_0_6.rs @@ -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; pub fn linker() -> &'static Linker { @@ -36,6 +42,22 @@ pub fn linker() -> &'static Linker { #[async_trait] impl HostWorktree for WasmState { + async fn id( + &mut self, + delegate: Resource>, + ) -> wasmtime::Result { + let delegate = self.table.get(&delegate)?; + Ok(delegate.worktree_id()) + } + + async fn root_path( + &mut self, + delegate: Resource>, + ) -> wasmtime::Result { + 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>, @@ -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, + category: String, + key: Option, + ) -> wasmtime::Result> { + 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::::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> { self.host .node_runtime diff --git a/crates/extension_api/Cargo.toml b/crates/extension_api/Cargo.toml index 2bd89ed93b..1fcb8d0453 100644 --- a/crates/extension_api/Cargo.toml +++ b/crates/extension_api/Cargo.toml @@ -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] diff --git a/crates/extension_api/src/extension_api.rs b/crates/extension_api/src/extension_api.rs index 92cb3f31fb..a58c64e138 100644 --- a/crates/extension_api/src/extension_api.rs +++ b/crates/extension_api/src/extension_api.rs @@ -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> { + ) -> Result> { + 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> { Ok(None) } @@ -142,7 +155,19 @@ impl wit::Guest for Component { worktree: &Worktree, ) -> Result, 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, 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( diff --git a/crates/extension_api/src/settings.rs b/crates/extension_api/src/settings.rs new file mode 100644 index 0000000000..f7829263b5 --- /dev/null +++ b/crates/extension_api/src/settings.rs @@ -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 { + 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 { + 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) + } +} diff --git a/crates/extension_api/wit/since_v0.0.6/extension.wit b/crates/extension_api/wit/since_v0.0.6/extension.wit index ba11fb558a..bbae5b9477 100644 --- a/crates/extension_api/wit/since_v0.0.6/extension.wit +++ b/crates/extension_api/wit/since_v0.0.6/extension.wit @@ -79,6 +79,13 @@ world extension { /// Returns operating system and architecture for the current platform. import current-platform: func() -> tuple; + record settings-location { + worktree-id: u64, + path: string, + } + + import get-settings: func(path: option, category: string, key: option) -> result; + /// Returns the path to the Node binary used by Zed. import node-binary-path: func() -> result; @@ -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; /// 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) -> result, string>; + /// Returns the workspace configuration options to pass to the language server. + export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow) -> result, string>; + record code-label { /// The source code to parse with Tree-sitter. code: string, diff --git a/crates/extension_api/wit/since_v0.0.6/settings.rs b/crates/extension_api/wit/since_v0.0.6/settings.rs new file mode 100644 index 0000000000..5294fac48e --- /dev/null +++ b/crates/extension_api/wit/since_v0.0.6/settings.rs @@ -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, + pub initialization_options: Option, + pub settings: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct BinarySettings { + pub path: Option, + pub arguments: Option>, +} diff --git a/crates/extensions_ui/src/extension_suggest.rs b/crates/extensions_ui/src/extension_suggest.rs index cfdef37106..99dee33dd8 100644 --- a/crates/extensions_ui/src/extension_suggest.rs +++ b/crates/extensions_ui/src/extension_suggest.rs @@ -22,6 +22,7 @@ fn suggested_extensions() -> &'static HashMap<&'static str, Arc> { ("clojure", "cljs"), ("clojure", "edn"), ("csharp", "cs"), + ("dart", "dart"), ("dockerfile", "Dockerfile"), ("elisp", "el"), ("erlang", "erl"), diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 7beecaf884..52272253b2 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -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; + 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; @@ -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, + _: &Arc, + _cx: &mut AsyncAppContext, + ) -> Result { + Ok(serde_json::json!({})) } /// Returns a list of code actions supported by a given LspAdapter diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index f338d9bf02..e8322a1a87 100644 --- a/crates/languages/Cargo.toml +++ b/crates/languages/Cargo.toml @@ -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 diff --git a/crates/languages/src/dart.rs b/crates/languages/src/dart.rs deleted file mode 100644 index 12ac699ee0..0000000000 --- a/crates/languages/src/dart.rs +++ /dev/null @@ -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> { - Ok(Box::new(())) - } - - async fn fetch_server_binary( - &self, - _: Box, - _: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Result { - Err(anyhow!("dart must me installed from dart.dev/get-dart")) - } - - async fn cached_server_binary( - &self, - _: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Option { - 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 { - 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 - }) - } -} diff --git a/crates/languages/src/elixir.rs b/crates/languages/src/elixir.rs index 6114a2fa1b..26f7f3e47e 100644 --- a/crates/languages/src/elixir.rs +++ b/crates/languages/src/elixir.rs @@ -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, + _: &Arc, + cx: &mut AsyncAppContext, + ) -> Result { + 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 - }) + })) } } diff --git a/crates/languages/src/elm.rs b/crates/languages/src/elm.rs index e293554c98..924f43c5df 100644 --- a/crates/languages/src/elm.rs +++ b/crates/languages/src/elm.rs @@ -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, + _: &Arc, + cx: &mut AsyncAppContext, + ) -> Result { // 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, - } + }) } } diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 25e2224434..ee8371b3d5 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -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, + _: &Arc, + cx: &mut AsyncAppContext, + ) -> Result { + cx.update(|cx| { + self.workspace_config + .get_or_init(|| Self::get_workspace_config(self.languages.language_names(), cx)) + .clone() + }) } fn language_ids(&self) -> HashMap { diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 35be182f31..fe482a7089 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -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(), diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index 9734fe3d9d..0c17e042a0 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -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, + _: &Arc, + _cx: &mut AsyncAppContext, + ) -> Result { + Ok(json!({ "tailwindCSS": { "emmetCompletions": true, } - }) + })) } fn language_ids(&self) -> HashMap { diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index ad4543e4d2..dfb9bc690e 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -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, + delegate: &Arc, + cx: &mut AsyncAppContext, + ) -> Result { + 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 { diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index 13f2c829c2..c42907889e 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -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 { 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, + _: &Arc, + cx: &mut AsyncAppContext, + ) -> Result { + 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 } - }) + })) } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index aabca2d9ec..d8db31f76f 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -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, + )), + } }) - .collect() + .collect::>() })?; - 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::( @@ -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, - worktree_path: &Path, + delegate: Arc, override_initialization_options: Option, pending_server: PendingLanguageServer, adapter: Arc, @@ -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, override_options: Option, pending_server: PendingLanguageServer, - worktree_path: &Path, + delegate: Arc, adapter: Arc, server_id: LanguageServerId, cx: &mut AsyncAppContext, ) -> Result> { - 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::({ - 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 { self.load_shell_env().await; self.shell_env.lock().as_ref().cloned().unwrap_or_default() diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 9e0c595ee6..a7a9eafa0e 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -47,7 +47,7 @@ pub struct BinarySettings { pub arguments: Option>, } -#[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, diff --git a/extensions/dart/Cargo.toml b/extensions/dart/Cargo.toml new file mode 100644 index 0000000000..f866c08181 --- /dev/null +++ b/extensions/dart/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "zed_dart" +version = "0.0.1" +edition = "2021" +publish = false +license = "Apache-2.0" + +[lints] +workspace = true + +[lib] +path = "src/dart.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = { path = "../../crates/extension_api" } diff --git a/extensions/dart/LICENSE-APACHE b/extensions/dart/LICENSE-APACHE new file mode 120000 index 0000000000..1cd601d0a3 --- /dev/null +++ b/extensions/dart/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/dart/extension.toml b/extensions/dart/extension.toml new file mode 100644 index 0000000000..66b073ae2f --- /dev/null +++ b/extensions/dart/extension.toml @@ -0,0 +1,15 @@ +id = "dart" +name = "Dart" +description = "Dart support." +version = "0.0.1" +schema_version = 1 +authors = ["Abdullah Alsigar ", "Flo "] +repository = "https://github.com/zed-industries/zed" + +[language_servers.dart] +name = "Dart LSP" +language = "Dart" + +[grammars.dart] +repository = "https://github.com/agent3bood/tree-sitter-dart" +commit = "48934e3bf757a9b78f17bdfaa3e2b4284656fdc7" diff --git a/crates/languages/src/dart/brackets.scm b/extensions/dart/languages/dart/brackets.scm similarity index 100% rename from crates/languages/src/dart/brackets.scm rename to extensions/dart/languages/dart/brackets.scm diff --git a/crates/languages/src/dart/config.toml b/extensions/dart/languages/dart/config.toml similarity index 100% rename from crates/languages/src/dart/config.toml rename to extensions/dart/languages/dart/config.toml diff --git a/crates/languages/src/dart/highlights.scm b/extensions/dart/languages/dart/highlights.scm similarity index 100% rename from crates/languages/src/dart/highlights.scm rename to extensions/dart/languages/dart/highlights.scm diff --git a/crates/languages/src/dart/indents.scm b/extensions/dart/languages/dart/indents.scm similarity index 100% rename from crates/languages/src/dart/indents.scm rename to extensions/dart/languages/dart/indents.scm diff --git a/crates/languages/src/dart/outline.scm b/extensions/dart/languages/dart/outline.scm similarity index 100% rename from crates/languages/src/dart/outline.scm rename to extensions/dart/languages/dart/outline.scm diff --git a/extensions/dart/src/dart.rs b/extensions/dart/src/dart.rs new file mode 100644 index 0000000000..e541846256 --- /dev/null +++ b/extensions/dart/src/dart.rs @@ -0,0 +1,123 @@ +use zed::lsp::CompletionKind; +use zed::settings::LspSettings; +use zed::{CodeLabel, CodeLabelSpan}; +use zed_extension_api::{self as zed, serde_json, Result}; + +struct DartExtension; + +impl zed::Extension for DartExtension { + fn new() -> Self { + Self + } + + fn language_server_command( + &mut self, + _language_server_id: &zed::LanguageServerId, + worktree: &zed::Worktree, + ) -> Result { + let path = worktree + .which("dart") + .ok_or_else(|| "dart must be installed from dart.dev/get-dart".to_string())?; + + Ok(zed::Command { + command: path, + args: vec!["language-server".to_string(), "--protocol=lsp".to_string()], + env: Default::default(), + }) + } + + fn language_server_workspace_configuration( + &mut self, + _language_server_id: &zed::LanguageServerId, + worktree: &zed::Worktree, + ) -> Result> { + let settings = LspSettings::for_worktree("dart", worktree) + .ok() + .and_then(|lsp_settings| lsp_settings.settings.clone()) + .unwrap_or_default(); + + Ok(Some(serde_json::json!({ + "dart": settings + }))) + } + + fn label_for_completion( + &self, + _language_server_id: &zed::LanguageServerId, + completion: zed::lsp::Completion, + ) -> Option { + let arrow = " → "; + + match completion.kind? { + CompletionKind::Class => Some(CodeLabel { + filter_range: (0..completion.label.len()).into(), + spans: vec![CodeLabelSpan::literal( + completion.label, + Some("type".into()), + )], + code: String::new(), + }), + CompletionKind::Function | CompletionKind::Constructor | CompletionKind::Method => { + let mut parts = completion.detail.as_ref()?.split(arrow); + let (name, _) = completion.label.split_once('(')?; + let parameter_list = parts.next()?; + let return_type = parts.next()?; + let fn_name = " a"; + let fat_arrow = " => "; + let call_expr = "();"; + + let code = + format!("{return_type}{fn_name}{parameter_list}{fat_arrow}{name}{call_expr}"); + + let parameter_list_start = return_type.len() + fn_name.len(); + + Some(CodeLabel { + spans: vec![ + CodeLabelSpan::code_range( + code.len() - call_expr.len() - name.len()..code.len() - call_expr.len(), + ), + CodeLabelSpan::code_range( + parameter_list_start..parameter_list_start + parameter_list.len(), + ), + CodeLabelSpan::literal(arrow, None), + CodeLabelSpan::code_range(0..return_type.len()), + ], + filter_range: (0..name.len()).into(), + code, + }) + } + CompletionKind::Property => { + let class_start = "class A {"; + let get = " get "; + let property_end = " => a; }"; + let ty = completion.detail?; + let name = completion.label; + + let code = format!("{class_start}{ty}{get}{name}{property_end}"); + let name_start = class_start.len() + ty.len() + get.len(); + + Some(CodeLabel { + spans: vec![ + CodeLabelSpan::code_range(name_start..name_start + name.len()), + CodeLabelSpan::literal(arrow, None), + CodeLabelSpan::code_range(class_start.len()..class_start.len() + ty.len()), + ], + filter_range: (0..name.len()).into(), + code, + }) + } + CompletionKind::Variable => { + let name = completion.label; + + Some(CodeLabel { + filter_range: (0..name.len()).into(), + spans: vec![CodeLabelSpan::literal(name, Some("variable".into()))], + code: String::new(), + }) + } + _ => None, + } + } +} + +zed::register_extension!(DartExtension); diff --git a/extensions/svelte/Cargo.toml b/extensions/svelte/Cargo.toml index b3be545ac5..a3bc5d2c7c 100644 --- a/extensions/svelte/Cargo.toml +++ b/extensions/svelte/Cargo.toml @@ -13,4 +13,5 @@ path = "src/svelte.rs" crate-type = ["cdylib"] [dependencies] -zed_extension_api = "0.0.4" +zed_extension_api = { path = "../../crates/extension_api" } +# zed_extension_api = "0.0.4" diff --git a/extensions/svelte/src/svelte.rs b/extensions/svelte/src/svelte.rs index 76c6096e83..38e05c2b2c 100644 --- a/extensions/svelte/src/svelte.rs +++ b/extensions/svelte/src/svelte.rs @@ -1,5 +1,5 @@ use std::{env, fs}; -use zed_extension_api::{self as zed, Result}; +use zed_extension_api::{self as zed, serde_json, Result}; struct SvelteExtension { did_find_server: bool, @@ -13,14 +13,14 @@ impl SvelteExtension { fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file()) } - fn server_script_path(&mut self, config: zed::LanguageServerConfig) -> Result { + fn server_script_path(&mut self, id: &zed::LanguageServerId) -> Result { let server_exists = self.server_exists(); if self.did_find_server && server_exists { return Ok(SERVER_PATH.to_string()); } zed::set_language_server_installation_status( - &config.name, + id, &zed::LanguageServerInstallationStatus::CheckingForUpdate, ); let version = zed::npm_package_latest_version(PACKAGE_NAME)?; @@ -29,7 +29,7 @@ impl SvelteExtension { || zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version) { zed::set_language_server_installation_status( - &config.name, + id, &zed::LanguageServerInstallationStatus::Downloading, ); let result = zed::npm_install_package(PACKAGE_NAME, &version); @@ -63,10 +63,10 @@ impl zed::Extension for SvelteExtension { fn language_server_command( &mut self, - config: zed::LanguageServerConfig, + id: &zed::LanguageServerId, _: &zed::Worktree, ) -> Result { - let server_path = self.server_script_path(config)?; + let server_path = self.server_script_path(id)?; Ok(zed::Command { command: zed::node_binary_path()?, args: vec![ @@ -83,10 +83,10 @@ impl zed::Extension for SvelteExtension { fn language_server_initialization_options( &mut self, - _: zed::LanguageServerConfig, + _: &zed::LanguageServerId, _: &zed::Worktree, - ) -> Result> { - let config = r#"{ + ) -> Result> { + let config = serde_json::json!({ "inlayHints": { "parameterNames": { "enabled": "all", @@ -109,17 +109,15 @@ impl zed::Extension for SvelteExtension { "enabled": true } } - }"#; + }); - Ok(Some(format!( - r#"{{ - "provideFormatter": true, - "configuration": {{ - "typescript": {config}, - "javascript": {config} - }} - }}"# - ))) + Ok(Some(serde_json::json!({ + "provideFormatter": true, + "configuration": { + "typescript": config, + "javascript": config + } + }))) } }