language: Allow support for ESLint working directories (#43677)

Smit Barmase and Piotr Osiewicz created

Closes #9648 #9755

Release Notes:

- Added way to configure ESLint's working directories in settings. For
example:
`{"lsp":{"eslint":{"settings":{"workingDirectories":["./client","./server"]}}}}`

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>

Change summary

Cargo.lock                                             |   1 
crates/language/src/language.rs                        |   5 
crates/language_extension/src/extension_lsp_adapter.rs |   3 
crates/languages/Cargo.toml                            |   1 
crates/languages/src/css.rs                            |   3 
crates/languages/src/eslint.rs                         | 772 ++++++++++++
crates/languages/src/json.rs                           |   3 
crates/languages/src/lib.rs                            |   3 
crates/languages/src/python.rs                         |   6 
crates/languages/src/tailwind.rs                       |   3 
crates/languages/src/typescript.rs                     | 240 ---
crates/languages/src/vtsls.rs                          |   3 
crates/languages/src/yaml.rs                           |   4 
crates/project/src/lsp_store.rs                        |  50 
14 files changed, 837 insertions(+), 260 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -9015,6 +9015,7 @@ dependencies = [
  "chrono",
  "collections",
  "futures 0.3.31",
+ "globset",
  "gpui",
  "http_client",
  "itertools 0.14.0",

crates/language/src/language.rs 🔗

@@ -36,7 +36,9 @@ use http_client::HttpClient;
 pub use language_registry::{
     LanguageName, LanguageServerStatusUpdate, LoadedLanguage, ServerHealth,
 };
-use lsp::{CodeActionKind, InitializeParams, LanguageServerBinary, LanguageServerBinaryOptions};
+use lsp::{
+    CodeActionKind, InitializeParams, LanguageServerBinary, LanguageServerBinaryOptions, Uri,
+};
 pub use manifest::{ManifestDelegate, ManifestName, ManifestProvider, ManifestQuery};
 use parking_lot::Mutex;
 use regex::Regex;
@@ -406,6 +408,7 @@ pub trait LspAdapter: 'static + Send + Sync + DynLspInstaller {
         self: Arc<Self>,
         _: &Arc<dyn LspAdapterDelegate>,
         _: Option<Toolchain>,
+        _: Option<Uri>,
         _cx: &mut AsyncApp,
     ) -> Result<Value> {
         Ok(serde_json::json!({}))

crates/language_extension/src/extension_lsp_adapter.rs 🔗

@@ -14,7 +14,7 @@ use language::{
 };
 use lsp::{
     CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName,
-    LanguageServerSelector,
+    LanguageServerSelector, Uri,
 };
 use serde::Serialize;
 use serde_json::Value;
@@ -282,6 +282,7 @@ impl LspAdapter for ExtensionLspAdapter {
         self: Arc<Self>,
         delegate: &Arc<dyn LspAdapterDelegate>,
         _: Option<Toolchain>,
+        _: Option<Uri>,
         _cx: &mut AsyncApp,
     ) -> Result<Value> {
         let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;

crates/languages/Cargo.toml 🔗

@@ -42,6 +42,7 @@ async-trait.workspace = true
 chrono.workspace = true
 collections.workspace = true
 futures.workspace = true
+globset.workspace = true
 gpui.workspace = true
 http_client.workspace = true
 json_schema_store.workspace = true

crates/languages/src/css.rs 🔗

@@ -2,7 +2,7 @@ use anyhow::Result;
 use async_trait::async_trait;
 use gpui::AsyncApp;
 use language::{LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
-use lsp::{LanguageServerBinary, LanguageServerName};
+use lsp::{LanguageServerBinary, LanguageServerName, Uri};
 use node_runtime::{NodeRuntime, VersionStrategy};
 use project::lsp_store::language_server_settings;
 use serde_json::json;
@@ -142,6 +142,7 @@ impl LspAdapter for CssLspAdapter {
         self: Arc<Self>,
         delegate: &Arc<dyn LspAdapterDelegate>,
         _: Option<Toolchain>,
+        _: Option<Uri>,
         cx: &mut AsyncApp,
     ) -> Result<serde_json::Value> {
         let mut default_config = json!({

crates/languages/src/eslint.rs 🔗

@@ -0,0 +1,772 @@
+use anyhow::{Context as _, Result};
+use async_trait::async_trait;
+use gpui::AsyncApp;
+use http_client::{
+    github::{AssetKind, GitHubLspBinaryVersion, build_asset_url},
+    github_download::download_server_binary,
+};
+use language::{LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
+use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName, Uri};
+use node_runtime::NodeRuntime;
+use project::lsp_store::language_server_settings_for;
+use serde::{Deserialize, Serialize};
+use serde_json::{Value, json};
+use settings::SettingsLocation;
+use smol::{fs, stream::StreamExt};
+use std::{
+    ffi::OsString,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::merge_json_value_into;
+use util::{fs::remove_matching, rel_path::RelPath};
+
+fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![
+        "--max-old-space-size=8192".into(),
+        server_path.into(),
+        "--stdio".into(),
+    ]
+}
+
+pub struct EsLintLspAdapter {
+    node: NodeRuntime,
+}
+
+impl EsLintLspAdapter {
+    const CURRENT_VERSION: &'static str = "2.4.4";
+    const CURRENT_VERSION_TAG_NAME: &'static str = "release/2.4.4";
+
+    #[cfg(not(windows))]
+    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
+    #[cfg(windows)]
+    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
+
+    const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
+    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("eslint");
+
+    const FLAT_CONFIG_FILE_NAMES: &'static [&'static str] = &[
+        "eslint.config.js",
+        "eslint.config.mjs",
+        "eslint.config.cjs",
+        "eslint.config.ts",
+        "eslint.config.cts",
+        "eslint.config.mts",
+    ];
+
+    pub fn new(node: NodeRuntime) -> Self {
+        EsLintLspAdapter { node }
+    }
+
+    fn build_destination_path(container_dir: &Path) -> PathBuf {
+        container_dir.join(format!("vscode-eslint-{}", Self::CURRENT_VERSION))
+    }
+}
+
+impl LspInstaller for EsLintLspAdapter {
+    type BinaryVersion = GitHubLspBinaryVersion;
+
+    async fn fetch_latest_server_version(
+        &self,
+        _delegate: &dyn LspAdapterDelegate,
+        _: bool,
+        _: &mut AsyncApp,
+    ) -> Result<GitHubLspBinaryVersion> {
+        let url = build_asset_url(
+            "zed-industries/vscode-eslint",
+            Self::CURRENT_VERSION_TAG_NAME,
+            Self::GITHUB_ASSET_KIND,
+        )?;
+
+        Ok(GitHubLspBinaryVersion {
+            name: Self::CURRENT_VERSION.into(),
+            digest: None,
+            url,
+        })
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: GitHubLspBinaryVersion,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let destination_path = Self::build_destination_path(&container_dir);
+        let server_path = destination_path.join(Self::SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            remove_matching(&container_dir, |_| true).await;
+
+            download_server_binary(
+                &*delegate.http_client(),
+                &version.url,
+                None,
+                &destination_path,
+                Self::GITHUB_ASSET_KIND,
+            )
+            .await?;
+
+            let mut dir = fs::read_dir(&destination_path).await?;
+            let first = dir.next().await.context("missing first file")??;
+            let repo_root = destination_path.join("vscode-eslint");
+            fs::rename(first.path(), &repo_root).await?;
+
+            #[cfg(target_os = "windows")]
+            {
+                handle_symlink(
+                    repo_root.join("$shared"),
+                    repo_root.join("client").join("src").join("shared"),
+                )
+                .await?;
+                handle_symlink(
+                    repo_root.join("$shared"),
+                    repo_root.join("server").join("src").join("shared"),
+                )
+                .await?;
+            }
+
+            self.node
+                .run_npm_subcommand(&repo_root, "install", &[])
+                .await?;
+
+            self.node
+                .run_npm_subcommand(&repo_root, "run-script", &["compile"])
+                .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            env: None,
+            arguments: eslint_server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        let server_path =
+            Self::build_destination_path(&container_dir).join(EsLintLspAdapter::SERVER_PATH);
+        Some(LanguageServerBinary {
+            path: self.node.binary_path().await.ok()?,
+            env: None,
+            arguments: eslint_server_binary_arguments(&server_path),
+        })
+    }
+}
+
+#[async_trait(?Send)]
+impl LspAdapter for EsLintLspAdapter {
+    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
+        Some(vec![
+            CodeActionKind::QUICKFIX,
+            CodeActionKind::new("source.fixAll.eslint"),
+        ])
+    }
+
+    async fn workspace_configuration(
+        self: Arc<Self>,
+        delegate: &Arc<dyn LspAdapterDelegate>,
+        _: Option<Toolchain>,
+        requested_uri: Option<Uri>,
+        cx: &mut AsyncApp,
+    ) -> Result<Value> {
+        let worktree_root = delegate.worktree_root_path();
+        let use_flat_config = Self::FLAT_CONFIG_FILE_NAMES
+            .iter()
+            .any(|file| worktree_root.join(file).is_file());
+
+        let mut default_workspace_configuration = json!({
+            "validate": "on",
+            "rulesCustomizations": [],
+            "run": "onType",
+            "nodePath": null,
+            "workingDirectory": {
+                "mode": "auto"
+            },
+            "workspaceFolder": {
+                "uri": worktree_root,
+                "name": worktree_root.file_name()
+                    .unwrap_or(worktree_root.as_os_str())
+                    .to_string_lossy(),
+            },
+            "problems": {},
+            "codeActionOnSave": {
+                // We enable this, but without also configuring code_actions_on_format
+                // in the Zed configuration, it doesn't have an effect.
+                "enable": true,
+            },
+            "codeAction": {
+                "disableRuleComment": {
+                    "enable": true,
+                    "location": "separateLine",
+                },
+                "showDocumentation": {
+                    "enable": true
+                }
+            },
+            "experimental": {
+                "useFlatConfig": use_flat_config,
+            }
+        });
+
+        let file_path = requested_uri
+            .as_ref()
+            .and_then(|uri| {
+                (uri.scheme() == "file")
+                    .then(|| uri.to_file_path().ok())
+                    .flatten()
+            })
+            .and_then(|abs_path| {
+                abs_path
+                    .strip_prefix(&worktree_root)
+                    .ok()
+                    .map(ToOwned::to_owned)
+            });
+        let file_path = file_path
+            .and_then(|p| RelPath::unix(&p).ok().map(ToOwned::to_owned))
+            .unwrap_or_else(|| RelPath::empty().to_owned());
+        let override_options = cx.update(|cx| {
+            language_server_settings_for(
+                SettingsLocation {
+                    worktree_id: delegate.worktree_id(),
+                    path: &file_path,
+                },
+                &Self::SERVER_NAME,
+                cx,
+            )
+            .and_then(|s| s.settings.clone())
+        })?;
+
+        if let Some(override_options) = override_options {
+            let working_directories = override_options.get("workingDirectories").and_then(|wd| {
+                serde_json::from_value::<WorkingDirectories>(wd.clone())
+                    .ok()
+                    .and_then(|wd| wd.0)
+            });
+
+            merge_json_value_into(override_options, &mut default_workspace_configuration);
+
+            let working_directory = working_directories
+                .zip(requested_uri)
+                .and_then(|(wd, uri)| {
+                    determine_working_directory(uri, wd, worktree_root.to_owned())
+                });
+
+            if let Some(working_directory) = working_directory
+                && let Some(wd) = default_workspace_configuration.get_mut("workingDirectory")
+            {
+                *wd = serde_json::to_value(working_directory)?;
+            }
+        }
+
+        Ok(json!({
+            "": default_workspace_configuration
+        }))
+    }
+
+    fn name(&self) -> LanguageServerName {
+        Self::SERVER_NAME
+    }
+}
+
+/// On Windows, converts Unix-style separators (/) to Windows-style (\).
+/// On Unix, returns the path unchanged
+fn normalize_path_separators(path: &str) -> String {
+    #[cfg(windows)]
+    {
+        path.replace('/', "\\")
+    }
+    #[cfg(not(windows))]
+    {
+        path.to_string()
+    }
+}
+
+fn determine_working_directory(
+    uri: Uri,
+    working_directories: Vec<WorkingDirectory>,
+    workspace_folder_path: PathBuf,
+) -> Option<ResultWorkingDirectory> {
+    let mut working_directory = None;
+
+    for item in working_directories {
+        let mut directory: Option<String> = None;
+        let mut pattern: Option<String> = None;
+        let mut no_cwd = false;
+        match item {
+            WorkingDirectory::String(contents) => {
+                directory = Some(normalize_path_separators(&contents));
+            }
+            WorkingDirectory::LegacyDirectoryItem(legacy_directory_item) => {
+                directory = Some(normalize_path_separators(&legacy_directory_item.directory));
+                no_cwd = !legacy_directory_item.change_process_cwd;
+            }
+            WorkingDirectory::DirectoryItem(directory_item) => {
+                directory = Some(normalize_path_separators(&directory_item.directory));
+                if let Some(not_cwd) = directory_item.not_cwd {
+                    no_cwd = not_cwd;
+                }
+            }
+            WorkingDirectory::PatternItem(pattern_item) => {
+                pattern = Some(normalize_path_separators(&pattern_item.pattern));
+                if let Some(not_cwd) = pattern_item.not_cwd {
+                    no_cwd = not_cwd;
+                }
+            }
+            WorkingDirectory::ModeItem(mode_item) => {
+                working_directory = Some(ResultWorkingDirectory::ModeItem(mode_item));
+                continue;
+            }
+        }
+
+        let mut item_value: Option<String> = None;
+        if directory.is_some() || pattern.is_some() {
+            let file_path: Option<PathBuf> = (uri.scheme() == "file")
+                .then(|| uri.to_file_path().ok())
+                .flatten();
+            if let Some(file_path) = file_path {
+                if let Some(mut directory) = directory {
+                    if Path::new(&directory).is_relative() {
+                        directory = workspace_folder_path
+                            .join(directory)
+                            .to_string_lossy()
+                            .to_string();
+                    }
+                    if !directory.ends_with(std::path::MAIN_SEPARATOR) {
+                        directory.push(std::path::MAIN_SEPARATOR);
+                    }
+                    if file_path.starts_with(&directory) {
+                        item_value = Some(directory);
+                    }
+                } else if let Some(mut pattern) = pattern
+                    && !pattern.is_empty()
+                {
+                    if Path::new(&pattern).is_relative() {
+                        pattern = workspace_folder_path
+                            .join(pattern)
+                            .to_string_lossy()
+                            .to_string();
+                    }
+                    if !pattern.ends_with(std::path::MAIN_SEPARATOR) {
+                        pattern.push(std::path::MAIN_SEPARATOR);
+                    }
+                    if let Some(matched) = match_glob_pattern(&pattern, &file_path) {
+                        item_value = Some(matched);
+                    }
+                }
+            }
+        }
+        if let Some(item_value) = item_value {
+            if working_directory
+                .as_ref()
+                .is_none_or(|wd| matches!(wd, ResultWorkingDirectory::ModeItem(_)))
+            {
+                working_directory = Some(ResultWorkingDirectory::DirectoryItem(DirectoryItem {
+                    directory: item_value,
+                    not_cwd: Some(no_cwd),
+                }));
+            } else if let Some(ResultWorkingDirectory::DirectoryItem(item)) = &mut working_directory
+                && item.directory.len() < item_value.len()
+            {
+                item.directory = item_value;
+                item.not_cwd = Some(no_cwd);
+            }
+        }
+    }
+
+    working_directory
+}
+
+fn match_glob_pattern(pattern: &str, file_path: &Path) -> Option<String> {
+    use globset::GlobBuilder;
+
+    let glob = GlobBuilder::new(pattern)
+        .literal_separator(true)
+        .build()
+        .ok()?
+        .compile_matcher();
+
+    let mut current = file_path.to_path_buf();
+    let mut matched: Option<String> = None;
+
+    while let Some(parent) = current.parent() {
+        let mut prefix = parent.to_string_lossy().to_string();
+        if !prefix.ends_with(std::path::MAIN_SEPARATOR) {
+            prefix.push(std::path::MAIN_SEPARATOR);
+        }
+        if glob.is_match(&prefix) {
+            matched = Some(prefix);
+        }
+        current = parent.to_path_buf();
+    }
+
+    matched
+}
+
+#[cfg(target_os = "windows")]
+async fn handle_symlink(src_dir: PathBuf, dest_dir: PathBuf) -> Result<()> {
+    anyhow::ensure!(
+        fs::metadata(&src_dir).await.is_ok(),
+        "Directory {src_dir:?} is not present"
+    );
+    if fs::metadata(&dest_dir).await.is_ok() {
+        fs::remove_file(&dest_dir).await?;
+    }
+    fs::create_dir_all(&dest_dir).await?;
+    let mut entries = fs::read_dir(&src_dir).await?;
+    while let Some(entry) = entries.try_next().await? {
+        let entry_path = entry.path();
+        let entry_name = entry.file_name();
+        let dest_path = dest_dir.join(&entry_name);
+        fs::copy(&entry_path, &dest_path).await?;
+    }
+    Ok(())
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+struct LegacyDirectoryItem {
+    directory: String,
+    #[serde(rename = "changeProcessCWD")]
+    change_process_cwd: bool,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+struct DirectoryItem {
+    directory: String,
+    #[serde(rename = "!cwd")]
+    not_cwd: Option<bool>,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+struct PatternItem {
+    pattern: String,
+    #[serde(rename = "!cwd")]
+    not_cwd: Option<bool>,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+struct ModeItem {
+    mode: ModeEnum,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(rename_all = "lowercase")]
+enum ModeEnum {
+    Auto,
+    Location,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(untagged)]
+enum WorkingDirectory {
+    String(String),
+    LegacyDirectoryItem(LegacyDirectoryItem),
+    DirectoryItem(DirectoryItem),
+    PatternItem(PatternItem),
+    ModeItem(ModeItem),
+}
+#[derive(Serialize, Deserialize)]
+struct WorkingDirectories(Option<Vec<WorkingDirectory>>);
+
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(untagged)]
+enum ResultWorkingDirectory {
+    ModeItem(ModeItem),
+    DirectoryItem(DirectoryItem),
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    mod glob_patterns {
+        use super::*;
+
+        #[test]
+        fn test_match_glob_pattern() {
+            let pattern = unix_path_to_platform("/test/*/");
+            let file_path = PathBuf::from(unix_path_to_platform("/test/foo/bar/file.txt"));
+            let matched = match_glob_pattern(&pattern, &file_path);
+            assert_eq!(matched, Some(unix_path_to_platform("/test/foo/")));
+        }
+
+        #[test]
+        fn test_match_glob_pattern_globstar() {
+            let pattern = unix_path_to_platform("/workspace/**/src/");
+            let file_path = PathBuf::from(unix_path_to_platform(
+                "/workspace/packages/core/src/index.ts",
+            ));
+            let matched = match_glob_pattern(&pattern, &file_path);
+            assert_eq!(
+                matched,
+                Some(unix_path_to_platform("/workspace/packages/core/src/"))
+            );
+        }
+
+        #[test]
+        fn test_match_glob_pattern_no_match() {
+            let pattern = unix_path_to_platform("/other/*/");
+            let file_path = PathBuf::from(unix_path_to_platform("/test/foo/bar/file.txt"));
+            let matched = match_glob_pattern(&pattern, &file_path);
+            assert_eq!(matched, None);
+        }
+    }
+
+    mod unix_style_paths {
+        use super::*;
+
+        #[test]
+        fn test_working_directory_string() {
+            let uri = make_uri("/workspace/packages/foo/src/file.ts");
+            let working_directories = vec![WorkingDirectory::String("packages/foo".to_string())];
+            let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+            let result = determine_working_directory(uri, working_directories, workspace_folder);
+            assert_directory_result(
+                result,
+                &unix_path_to_platform("/workspace/packages/foo/"),
+                false,
+            );
+        }
+
+        #[test]
+        fn test_working_directory_absolute_path() {
+            let uri = make_uri("/workspace/packages/foo/src/file.ts");
+            let working_directories = vec![WorkingDirectory::String(unix_path_to_platform(
+                "/workspace/packages/foo",
+            ))];
+            let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+            let result = determine_working_directory(uri, working_directories, workspace_folder);
+            assert_directory_result(
+                result,
+                &unix_path_to_platform("/workspace/packages/foo/"),
+                false,
+            );
+        }
+
+        #[test]
+        fn test_working_directory_directory_item() {
+            let uri = make_uri("/workspace/packages/foo/src/file.ts");
+            let working_directories = vec![WorkingDirectory::DirectoryItem(DirectoryItem {
+                directory: "packages/foo".to_string(),
+                not_cwd: Some(true),
+            })];
+            let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+            let result = determine_working_directory(uri, working_directories, workspace_folder);
+            assert_directory_result(
+                result,
+                &unix_path_to_platform("/workspace/packages/foo/"),
+                true,
+            );
+        }
+
+        #[test]
+        fn test_working_directory_legacy_item() {
+            let uri = make_uri("/workspace/packages/foo/src/file.ts");
+            let working_directories =
+                vec![WorkingDirectory::LegacyDirectoryItem(LegacyDirectoryItem {
+                    directory: "packages/foo".to_string(),
+                    change_process_cwd: false,
+                })];
+            let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+            let result = determine_working_directory(uri, working_directories, workspace_folder);
+            assert_directory_result(
+                result,
+                &unix_path_to_platform("/workspace/packages/foo/"),
+                true,
+            );
+        }
+
+        #[test]
+        fn test_working_directory_pattern_item() {
+            let uri = make_uri("/workspace/packages/foo/src/file.ts");
+            let working_directories = vec![WorkingDirectory::PatternItem(PatternItem {
+                pattern: "packages/*/".to_string(),
+                not_cwd: Some(false),
+            })];
+            let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+            let result = determine_working_directory(uri, working_directories, workspace_folder);
+            assert_directory_result(
+                result,
+                &unix_path_to_platform("/workspace/packages/foo/"),
+                false,
+            );
+        }
+
+        #[test]
+        fn test_working_directory_multiple_patterns() {
+            let uri = make_uri("/workspace/apps/web/src/file.ts");
+            let working_directories = vec![
+                WorkingDirectory::PatternItem(PatternItem {
+                    pattern: "packages/*/".to_string(),
+                    not_cwd: None,
+                }),
+                WorkingDirectory::PatternItem(PatternItem {
+                    pattern: "apps/*/".to_string(),
+                    not_cwd: None,
+                }),
+            ];
+            let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+            let result = determine_working_directory(uri, working_directories, workspace_folder);
+            assert_directory_result(
+                result,
+                &unix_path_to_platform("/workspace/apps/web/"),
+                false,
+            );
+        }
+    }
+
+    #[cfg(windows)]
+    mod windows_style_paths {
+        use super::*;
+
+        #[test]
+        fn test_working_directory_string() {
+            let uri = make_uri("/workspace/packages/foo/src/file.ts");
+            let working_directories = vec![WorkingDirectory::String("packages\\foo".to_string())];
+            let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+            let result = determine_working_directory(uri, working_directories, workspace_folder);
+            assert_directory_result(
+                result,
+                &unix_path_to_platform("/workspace/packages/foo/"),
+                false,
+            );
+        }
+
+        #[test]
+        fn test_working_directory_absolute_path() {
+            let uri = make_uri("/workspace/packages/foo/src/file.ts");
+            let working_directories = vec![WorkingDirectory::String(
+                unix_path_to_platform("/workspace/packages/foo").replace('/', "\\"),
+            )];
+            let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+            let result = determine_working_directory(uri, working_directories, workspace_folder);
+            assert_directory_result(
+                result,
+                &unix_path_to_platform("/workspace/packages/foo/"),
+                false,
+            );
+        }
+
+        #[test]
+        fn test_working_directory_directory_item() {
+            let uri = make_uri("/workspace/packages/foo/src/file.ts");
+            let working_directories = vec![WorkingDirectory::DirectoryItem(DirectoryItem {
+                directory: "packages\\foo".to_string(),
+                not_cwd: Some(true),
+            })];
+            let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+            let result = determine_working_directory(uri, working_directories, workspace_folder);
+            assert_directory_result(
+                result,
+                &unix_path_to_platform("/workspace/packages/foo/"),
+                true,
+            );
+        }
+
+        #[test]
+        fn test_working_directory_legacy_item() {
+            let uri = make_uri("/workspace/packages/foo/src/file.ts");
+            let working_directories =
+                vec![WorkingDirectory::LegacyDirectoryItem(LegacyDirectoryItem {
+                    directory: "packages\\foo".to_string(),
+                    change_process_cwd: false,
+                })];
+            let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+            let result = determine_working_directory(uri, working_directories, workspace_folder);
+            assert_directory_result(
+                result,
+                &unix_path_to_platform("/workspace/packages/foo/"),
+                true,
+            );
+        }
+
+        #[test]
+        fn test_working_directory_pattern_item() {
+            let uri = make_uri("/workspace/packages/foo/src/file.ts");
+            let working_directories = vec![WorkingDirectory::PatternItem(PatternItem {
+                pattern: "packages\\*\\".to_string(),
+                not_cwd: Some(false),
+            })];
+            let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+            let result = determine_working_directory(uri, working_directories, workspace_folder);
+            assert_directory_result(
+                result,
+                &unix_path_to_platform("/workspace/packages/foo/"),
+                false,
+            );
+        }
+
+        #[test]
+        fn test_working_directory_multiple_patterns() {
+            let uri = make_uri("/workspace/apps/web/src/file.ts");
+            let working_directories = vec![
+                WorkingDirectory::PatternItem(PatternItem {
+                    pattern: "packages\\*\\".to_string(),
+                    not_cwd: None,
+                }),
+                WorkingDirectory::PatternItem(PatternItem {
+                    pattern: "apps\\*\\".to_string(),
+                    not_cwd: None,
+                }),
+            ];
+            let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+            let result = determine_working_directory(uri, working_directories, workspace_folder);
+            assert_directory_result(
+                result,
+                &unix_path_to_platform("/workspace/apps/web/"),
+                false,
+            );
+        }
+    }
+
+    /// Converts a Unix-style path to a platform-specific path.
+    /// On Windows, converts "/workspace/foo/bar" to "C:\workspace\foo\bar"
+    /// On Unix, returns the path unchanged.
+    fn unix_path_to_platform(path: &str) -> String {
+        #[cfg(windows)]
+        {
+            if path.starts_with('/') {
+                format!("C:{}", path.replace('/', "\\"))
+            } else {
+                path.replace('/', "\\")
+            }
+        }
+        #[cfg(not(windows))]
+        {
+            path.to_string()
+        }
+    }
+
+    fn make_uri(path: &str) -> Uri {
+        let platform_path = unix_path_to_platform(path);
+        Uri::from_file_path(&platform_path).unwrap()
+    }
+
+    fn assert_directory_result(
+        result: Option<ResultWorkingDirectory>,
+        expected_directory: &str,
+        expected_not_cwd: bool,
+    ) {
+        match result {
+            Some(ResultWorkingDirectory::DirectoryItem(item)) => {
+                assert_eq!(item.directory, expected_directory);
+                assert_eq!(item.not_cwd, Some(expected_not_cwd));
+            }
+            other => panic!("Expected DirectoryItem, got {:?}", other),
+        }
+    }
+}

crates/languages/src/json.rs 🔗

@@ -10,7 +10,7 @@ use language::{
     ContextProvider, LanguageName, LocalFile as _, LspAdapter, LspAdapterDelegate, LspInstaller,
     Toolchain,
 };
-use lsp::{LanguageServerBinary, LanguageServerName};
+use lsp::{LanguageServerBinary, LanguageServerName, Uri};
 use node_runtime::{NodeRuntime, VersionStrategy};
 use project::lsp_store::language_server_settings;
 use serde_json::{Value, json};
@@ -251,6 +251,7 @@ impl LspAdapter for JsonLspAdapter {
         self: Arc<Self>,
         delegate: &Arc<dyn LspAdapterDelegate>,
         _: Option<Toolchain>,
+        _: Option<Uri>,
         cx: &mut AsyncApp,
     ) -> Result<Value> {
         let mut config = cx.update(|cx| {

crates/languages/src/lib.rs 🔗

@@ -21,6 +21,7 @@ mod bash;
 mod c;
 mod cpp;
 mod css;
+mod eslint;
 mod go;
 mod json;
 mod package_json;
@@ -84,7 +85,7 @@ pub fn init(languages: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, node: NodeRuntime
 
     let c_lsp_adapter = Arc::new(c::CLspAdapter);
     let css_lsp_adapter = Arc::new(css::CssLspAdapter::new(node.clone()));
-    let eslint_adapter = Arc::new(typescript::EsLintLspAdapter::new(node.clone()));
+    let eslint_adapter = Arc::new(eslint::EsLintLspAdapter::new(node.clone()));
     let go_context_provider = Arc::new(go::GoContextProvider);
     let go_lsp_adapter = Arc::new(go::GoLspAdapter);
     let json_context_provider = Arc::new(JsonTaskProvider);

crates/languages/src/python.rs 🔗

@@ -10,8 +10,8 @@ use language::{ContextLocation, LanguageToolchainStore, LspInstaller};
 use language::{ContextProvider, LspAdapter, LspAdapterDelegate};
 use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery};
 use language::{Toolchain, ToolchainList, ToolchainLister, ToolchainMetadata};
-use lsp::LanguageServerBinary;
 use lsp::LanguageServerName;
+use lsp::{LanguageServerBinary, Uri};
 use node_runtime::{NodeRuntime, VersionStrategy};
 use pet_core::Configuration;
 use pet_core::os_environment::Environment;
@@ -206,6 +206,7 @@ impl LspAdapter for TyLspAdapter {
         self: Arc<Self>,
         delegate: &Arc<dyn LspAdapterDelegate>,
         toolchain: Option<Toolchain>,
+        _: Option<Uri>,
         cx: &mut AsyncApp,
     ) -> Result<Value> {
         let mut ret = cx
@@ -517,6 +518,7 @@ impl LspAdapter for PyrightLspAdapter {
         self: Arc<Self>,
         adapter: &Arc<dyn LspAdapterDelegate>,
         toolchain: Option<Toolchain>,
+        _: Option<Uri>,
         cx: &mut AsyncApp,
     ) -> Result<Value> {
         cx.update(move |cx| {
@@ -1593,6 +1595,7 @@ impl LspAdapter for PyLspAdapter {
         self: Arc<Self>,
         adapter: &Arc<dyn LspAdapterDelegate>,
         toolchain: Option<Toolchain>,
+        _: Option<Uri>,
         cx: &mut AsyncApp,
     ) -> Result<Value> {
         cx.update(move |cx| {
@@ -1884,6 +1887,7 @@ impl LspAdapter for BasedPyrightLspAdapter {
         self: Arc<Self>,
         adapter: &Arc<dyn LspAdapterDelegate>,
         toolchain: Option<Toolchain>,
+        _: Option<Uri>,
         cx: &mut AsyncApp,
     ) -> Result<Value> {
         cx.update(move |cx| {

crates/languages/src/tailwind.rs 🔗

@@ -3,7 +3,7 @@ use async_trait::async_trait;
 use collections::HashMap;
 use gpui::AsyncApp;
 use language::{LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
-use lsp::{LanguageServerBinary, LanguageServerName};
+use lsp::{LanguageServerBinary, LanguageServerName, Uri};
 use node_runtime::{NodeRuntime, VersionStrategy};
 use project::lsp_store::language_server_settings;
 use serde_json::{Value, json};
@@ -154,6 +154,7 @@ impl LspAdapter for TailwindLspAdapter {
         self: Arc<Self>,
         delegate: &Arc<dyn LspAdapterDelegate>,
         _: Option<Toolchain>,
+        _: Option<Uri>,
         cx: &mut AsyncApp,
     ) -> Result<Value> {
         let mut tailwind_user_settings = cx.update(|cx| {

crates/languages/src/typescript.rs 🔗

@@ -4,18 +4,16 @@ use chrono::{DateTime, Local};
 use collections::HashMap;
 use futures::future::join_all;
 use gpui::{App, AppContext, AsyncApp, Task};
-use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};
-use http_client::github_download::download_server_binary;
 use itertools::Itertools as _;
 use language::{
     ContextLocation, ContextProvider, File, LanguageName, LanguageToolchainStore, LspAdapter,
     LspAdapterDelegate, LspInstaller, Toolchain,
 };
-use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
+use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName, Uri};
 use node_runtime::{NodeRuntime, VersionStrategy};
 use project::{Fs, lsp_store::language_server_settings};
 use serde_json::{Value, json};
-use smol::{fs, lock::RwLock, stream::StreamExt};
+use smol::lock::RwLock;
 use std::{
     borrow::Cow,
     ffi::OsString,
@@ -23,8 +21,8 @@ use std::{
     sync::{Arc, LazyLock},
 };
 use task::{TaskTemplate, TaskTemplates, VariableName};
-use util::{ResultExt, fs::remove_matching, maybe};
-use util::{merge_json_value_into, rel_path::RelPath};
+use util::rel_path::RelPath;
+use util::{ResultExt, maybe};
 
 use crate::{PackageJson, PackageJsonData};
 
@@ -589,14 +587,6 @@ fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
     vec![server_path.into(), "--stdio".into()]
 }
 
-fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
-    vec![
-        "--max-old-space-size=8192".into(),
-        server_path.into(),
-        "--stdio".into(),
-    ]
-}
-
 fn replace_test_name_parameters(test_name: &str) -> String {
     static PATTERN: LazyLock<regex::Regex> =
         LazyLock::new(|| regex::Regex::new(r"(\$([A-Za-z0-9_\.]+|[\#])|%[psdifjo#\$%])").unwrap());
@@ -811,9 +801,9 @@ impl LspAdapter for TypeScriptLspAdapter {
 
     async fn workspace_configuration(
         self: Arc<Self>,
-
         delegate: &Arc<dyn LspAdapterDelegate>,
         _: Option<Toolchain>,
+        _: Option<Uri>,
         cx: &mut AsyncApp,
     ) -> Result<Value> {
         let override_options = cx.update(|cx| {
@@ -866,226 +856,6 @@ async fn get_cached_ts_server_binary(
     .log_err()
 }
 
-pub struct EsLintLspAdapter {
-    node: NodeRuntime,
-}
-
-impl EsLintLspAdapter {
-    const CURRENT_VERSION: &'static str = "2.4.4";
-    const CURRENT_VERSION_TAG_NAME: &'static str = "release/2.4.4";
-
-    #[cfg(not(windows))]
-    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
-    #[cfg(windows)]
-    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
-
-    const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
-    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("eslint");
-
-    const FLAT_CONFIG_FILE_NAMES: &'static [&'static str] = &[
-        "eslint.config.js",
-        "eslint.config.mjs",
-        "eslint.config.cjs",
-        "eslint.config.ts",
-        "eslint.config.cts",
-        "eslint.config.mts",
-    ];
-
-    pub fn new(node: NodeRuntime) -> Self {
-        EsLintLspAdapter { node }
-    }
-
-    fn build_destination_path(container_dir: &Path) -> PathBuf {
-        container_dir.join(format!("vscode-eslint-{}", Self::CURRENT_VERSION))
-    }
-}
-
-impl LspInstaller for EsLintLspAdapter {
-    type BinaryVersion = GitHubLspBinaryVersion;
-
-    async fn fetch_latest_server_version(
-        &self,
-        _delegate: &dyn LspAdapterDelegate,
-        _: bool,
-        _: &mut AsyncApp,
-    ) -> Result<GitHubLspBinaryVersion> {
-        let url = build_asset_url(
-            "zed-industries/vscode-eslint",
-            Self::CURRENT_VERSION_TAG_NAME,
-            Self::GITHUB_ASSET_KIND,
-        )?;
-
-        Ok(GitHubLspBinaryVersion {
-            name: Self::CURRENT_VERSION.into(),
-            digest: None,
-            url,
-        })
-    }
-
-    async fn fetch_server_binary(
-        &self,
-        version: GitHubLspBinaryVersion,
-        container_dir: PathBuf,
-        delegate: &dyn LspAdapterDelegate,
-    ) -> Result<LanguageServerBinary> {
-        let destination_path = Self::build_destination_path(&container_dir);
-        let server_path = destination_path.join(Self::SERVER_PATH);
-
-        if fs::metadata(&server_path).await.is_err() {
-            remove_matching(&container_dir, |_| true).await;
-
-            download_server_binary(
-                &*delegate.http_client(),
-                &version.url,
-                None,
-                &destination_path,
-                Self::GITHUB_ASSET_KIND,
-            )
-            .await?;
-
-            let mut dir = fs::read_dir(&destination_path).await?;
-            let first = dir.next().await.context("missing first file")??;
-            let repo_root = destination_path.join("vscode-eslint");
-            fs::rename(first.path(), &repo_root).await?;
-
-            #[cfg(target_os = "windows")]
-            {
-                handle_symlink(
-                    repo_root.join("$shared"),
-                    repo_root.join("client").join("src").join("shared"),
-                )
-                .await?;
-                handle_symlink(
-                    repo_root.join("$shared"),
-                    repo_root.join("server").join("src").join("shared"),
-                )
-                .await?;
-            }
-
-            self.node
-                .run_npm_subcommand(&repo_root, "install", &[])
-                .await?;
-
-            self.node
-                .run_npm_subcommand(&repo_root, "run-script", &["compile"])
-                .await?;
-        }
-
-        Ok(LanguageServerBinary {
-            path: self.node.binary_path().await?,
-            env: None,
-            arguments: eslint_server_binary_arguments(&server_path),
-        })
-    }
-
-    async fn cached_server_binary(
-        &self,
-        container_dir: PathBuf,
-        _: &dyn LspAdapterDelegate,
-    ) -> Option<LanguageServerBinary> {
-        let server_path =
-            Self::build_destination_path(&container_dir).join(EsLintLspAdapter::SERVER_PATH);
-        Some(LanguageServerBinary {
-            path: self.node.binary_path().await.ok()?,
-            env: None,
-            arguments: eslint_server_binary_arguments(&server_path),
-        })
-    }
-}
-
-#[async_trait(?Send)]
-impl LspAdapter for EsLintLspAdapter {
-    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
-        Some(vec![
-            CodeActionKind::QUICKFIX,
-            CodeActionKind::new("source.fixAll.eslint"),
-        ])
-    }
-
-    async fn workspace_configuration(
-        self: Arc<Self>,
-        delegate: &Arc<dyn LspAdapterDelegate>,
-        _: Option<Toolchain>,
-        cx: &mut AsyncApp,
-    ) -> Result<Value> {
-        let workspace_root = delegate.worktree_root_path();
-        let use_flat_config = Self::FLAT_CONFIG_FILE_NAMES
-            .iter()
-            .any(|file| workspace_root.join(file).is_file());
-
-        let mut default_workspace_configuration = json!({
-            "validate": "on",
-            "rulesCustomizations": [],
-            "run": "onType",
-            "nodePath": null,
-            "workingDirectory": {
-                "mode": "auto"
-            },
-            "workspaceFolder": {
-                "uri": workspace_root,
-                "name": workspace_root.file_name()
-                    .unwrap_or(workspace_root.as_os_str())
-                    .to_string_lossy(),
-            },
-            "problems": {},
-            "codeActionOnSave": {
-                // We enable this, but without also configuring code_actions_on_format
-                // in the Zed configuration, it doesn't have an effect.
-                "enable": true,
-            },
-            "codeAction": {
-                "disableRuleComment": {
-                    "enable": true,
-                    "location": "separateLine",
-                },
-                "showDocumentation": {
-                    "enable": true
-                }
-            },
-            "experimental": {
-                "useFlatConfig": use_flat_config,
-            }
-        });
-
-        let override_options = cx.update(|cx| {
-            language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
-                .and_then(|s| s.settings.clone())
-        })?;
-
-        if let Some(override_options) = override_options {
-            merge_json_value_into(override_options, &mut default_workspace_configuration);
-        }
-
-        Ok(json!({
-            "": default_workspace_configuration
-        }))
-    }
-
-    fn name(&self) -> LanguageServerName {
-        Self::SERVER_NAME
-    }
-}
-
-#[cfg(target_os = "windows")]
-async fn handle_symlink(src_dir: PathBuf, dest_dir: PathBuf) -> Result<()> {
-    anyhow::ensure!(
-        fs::metadata(&src_dir).await.is_ok(),
-        "Directory {src_dir:?} is not present"
-    );
-    if fs::metadata(&dest_dir).await.is_ok() {
-        fs::remove_file(&dest_dir).await?;
-    }
-    fs::create_dir_all(&dest_dir).await?;
-    let mut entries = fs::read_dir(&src_dir).await?;
-    while let Some(entry) = entries.try_next().await? {
-        let entry_path = entry.path();
-        let entry_name = entry.file_name();
-        let dest_path = dest_dir.join(&entry_name);
-        fs::copy(&entry_path, &dest_path).await?;
-    }
-    Ok(())
-}
-
 #[cfg(test)]
 mod tests {
     use std::path::Path;

crates/languages/src/vtsls.rs 🔗

@@ -3,7 +3,7 @@ use async_trait::async_trait;
 use collections::HashMap;
 use gpui::AsyncApp;
 use language::{LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
-use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
+use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName, Uri};
 use node_runtime::{NodeRuntime, VersionStrategy};
 use project::{Fs, lsp_store::language_server_settings};
 use regex::Regex;
@@ -228,6 +228,7 @@ impl LspAdapter for VtslsLspAdapter {
         self: Arc<Self>,
         delegate: &Arc<dyn LspAdapterDelegate>,
         _: Option<Toolchain>,
+        _: Option<Uri>,
         cx: &mut AsyncApp,
     ) -> Result<Value> {
         let tsdk_path = self.tsdk_path(delegate).await;

crates/languages/src/yaml.rs 🔗

@@ -4,7 +4,7 @@ use gpui::AsyncApp;
 use language::{
     LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain, language_settings::AllLanguageSettings,
 };
-use lsp::{LanguageServerBinary, LanguageServerName};
+use lsp::{LanguageServerBinary, LanguageServerName, Uri};
 use node_runtime::{NodeRuntime, VersionStrategy};
 use project::lsp_store::language_server_settings;
 use serde_json::Value;
@@ -132,9 +132,9 @@ impl LspAdapter for YamlLspAdapter {
 
     async fn workspace_configuration(
         self: Arc<Self>,
-
         delegate: &Arc<dyn LspAdapterDelegate>,
         _: Option<Toolchain>,
+        _: Option<Uri>,
         cx: &mut AsyncApp,
     ) -> Result<Value> {
         let location = SettingsLocation {

crates/project/src/lsp_store.rs 🔗

@@ -436,6 +436,7 @@ impl LocalLspStore {
                         adapter.adapter.clone(),
                         &delegate,
                         toolchain,
+                        None,
                         cx,
                     )
                     .await?;
@@ -720,25 +721,42 @@ impl LocalLspStore {
                                 )
                             })?
                             .context("Expected the LSP store to be in a local mode")?;
-                        let workspace_config = Self::workspace_configuration_for_adapter(
-                            adapter.clone(),
-                            &delegate,
-                            toolchain_for_id,
-                            &mut cx,
-                        )
-                        .await?;
+
+                        let mut scope_uri_to_workspace_config = BTreeMap::new();
+                        for item in &params.items {
+                            let scope_uri = item.scope_uri.clone();
+                            let std::collections::btree_map::Entry::Vacant(new_scope_uri) =
+                                scope_uri_to_workspace_config.entry(scope_uri.clone())
+                            else {
+                                // We've already queried workspace configuration of this URI.
+                                continue;
+                            };
+                            let workspace_config = Self::workspace_configuration_for_adapter(
+                                adapter.clone(),
+                                &delegate,
+                                toolchain_for_id.clone(),
+                                scope_uri,
+                                &mut cx,
+                            )
+                            .await?;
+                            new_scope_uri.insert(workspace_config);
+                        }
 
                         Ok(params
                             .items
                             .into_iter()
-                            .map(|item| {
+                            .filter_map(|item| {
+                                let workspace_config =
+                                    scope_uri_to_workspace_config.get(&item.scope_uri)?;
                                 if let Some(section) = &item.section {
-                                    workspace_config
-                                        .get(section)
-                                        .cloned()
-                                        .unwrap_or(serde_json::Value::Null)
+                                    Some(
+                                        workspace_config
+                                            .get(section)
+                                            .cloned()
+                                            .unwrap_or(serde_json::Value::Null),
+                                    )
                                 } else {
-                                    workspace_config.clone()
+                                    Some(workspace_config.clone())
                                 }
                             })
                             .collect())
@@ -3527,11 +3545,12 @@ impl LocalLspStore {
         adapter: Arc<dyn LspAdapter>,
         delegate: &Arc<dyn LspAdapterDelegate>,
         toolchain: Option<Toolchain>,
+        requested_uri: Option<Uri>,
         cx: &mut AsyncApp,
     ) -> Result<serde_json::Value> {
         let mut workspace_config = adapter
             .clone()
-            .workspace_configuration(delegate, toolchain, cx)
+            .workspace_configuration(delegate, toolchain, requested_uri, cx)
             .await?;
 
         for other_adapter in delegate.registered_lsp_adapters() {
@@ -7963,6 +7982,7 @@ impl LspStore {
                                                 adapter.adapter.clone(),
                                                 &delegate,
                                                 toolchain,
+                                                None,
                                                 cx,
                                             )
                                             .await
@@ -13619,7 +13639,7 @@ pub fn language_server_settings<'a>(
     )
 }
 
-pub(crate) fn language_server_settings_for<'a>(
+pub fn language_server_settings_for<'a>(
     location: SettingsLocation<'a>,
     language: &LanguageServerName,
     cx: &'a App,