Add `language_server_workspace_configuration` to extension API (#10212)

Marshall Bowers , Max , and Max Brunsfeld created

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>

Change summary

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 ++
crates/extension/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 +++
crates/extension_api/wit/since_v0.0.6/extension.wit |  14 +
crates/extension_api/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                      |  28 ++-
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 +
extensions/dart/languages/dart/brackets.scm         |   0 
extensions/dart/languages/dart/config.toml          |   0 
extensions/dart/languages/dart/highlights.scm       |   0 
extensions/dart/languages/dart/indents.scm          |   0 
extensions/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, 587 insertions(+), 188 deletions(-)

Detailed changes

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

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"

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

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

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

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

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]

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

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<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)
+    }
+}

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

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<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>>,
+}

crates/extensions_ui/src/extension_suggest.rs 🔗

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

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

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

crates/languages/src/dart.rs 🔗

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

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();
-
-        serde_json::json!({
+    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()
+        })?;
+
+        Ok(serde_json::json!({
             "elixirLS": settings
-        })
+        }))
     }
 }
 

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<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,
-        }
+        })
     }
 }
 

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

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

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<Self>,
+        _: &Arc<dyn LspAdapterDelegate>,
+        _cx: &mut AsyncAppContext,
+    ) -> Result<Value> {
+        Ok(json!({
             "tailwindCSS": {
                 "emmetCompletions": true,
             }
-        })
+        }))
     }
 
     fn language_ids(&self) -> HashMap<String, String> {

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

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<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
             }
-        })
+        }))
     }
 }
 

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

crates/project/src/project_settings.rs 🔗

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

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

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 <abdullah.alsigar@gmail.com>", "Flo <flo80@users.noreply.github.com>"]
+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"

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<zed::Command> {
+        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<Option<serde_json::Value>> {
+        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<CodeLabel> {
+        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);

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"

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<String> {
+    fn server_script_path(&mut self, id: &zed::LanguageServerId) -> Result<String> {
         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<zed::Command> {
-        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<Option<String>> {
-        let config = r#"{
+    ) -> Result<Option<serde_json::Value>> {
+        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
+            }
+        })))
     }
 }