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, read_package_installed_version};use project::Fs;use project::lsp_store::language_server_settings_for;use semver::Version;use serde::{Deserialize, Serialize};use serde_json::{Value, json};use settings::SettingsLocation;use smol::{fs, stream::StreamExt};use std::{ ffi::OsString, future::Future, 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, fs: Arc<dyn Fs>,} impl EsLintLspAdapter { const CURRENT_VERSION: &'static str = "3.0.24"; const CURRENT_VERSION_TAG_NAME: &'static str = "release/3.0.24"; #[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_V8_21: &'static [&'static str] = &["eslint.config.js"]; const FLAT_CONFIG_FILE_NAMES_V8_57: &'static [&'static str] = &["eslint.config.js", "eslint.config.mjs", "eslint.config.cjs"]; const FLAT_CONFIG_FILE_NAMES_V10: &'static [&'static str] = &[ "eslint.config.js", "eslint.config.mjs", "eslint.config.cjs", "eslint.config.ts", "eslint.config.cts", "eslint.config.mts", ]; const LEGACY_CONFIG_FILE_NAMES: &'static [&'static str] = &[ ".eslintrc", ".eslintrc.js", ".eslintrc.cjs", ".eslintrc.yaml", ".eslintrc.yml", ".eslintrc.json", ]; pub fn new(node: NodeRuntime, fs: Arc<dyn Fs>) -> Self { EsLintLspAdapter { node, fs } } 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: &Arc<dyn LspAdapterDelegate>, _: bool, _: &mut AsyncApp, ) -> Result<GitHubLspBinaryVersion> { let url = build_asset_url( "microsoft/vscode-eslint", Self::CURRENT_VERSION_TAG_NAME, Self::GITHUB_ASSET_KIND, )?; Ok(GitHubLspBinaryVersion { name: Self::CURRENT_VERSION.into(), digest: None, url, }) } fn fetch_server_binary( &self, version: GitHubLspBinaryVersion, container_dir: PathBuf, delegate: &Arc<dyn LspAdapterDelegate>, ) -> impl Send + Future<Output = Result<LanguageServerBinary>> + use<> { let delegate = delegate.clone(); let node = self.node.clone(); async move { 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?; } node.run_npm_subcommand(Some(&repo_root), "install", &[]) .await?; node.run_npm_subcommand(Some(&repo_root), "run-script", &["compile"]) .await?; } Ok(LanguageServerBinary { path: 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); fs::metadata(&server_path).await.ok()?; Some(LanguageServerBinary { path: self.node.binary_path().await.ok()?, env: None, arguments: eslint_server_binary_arguments(&server_path), }) }} #[derive(Debug, Clone, Copy, PartialEq, Eq)]enum EslintConfigKind { Flat, Legacy,} #[derive(Debug, Default, Clone, PartialEq, Eq)]struct EslintSettingsOverrides { use_flat_config: Option<bool>, experimental_use_flat_config: Option<bool>,} impl EslintSettingsOverrides { fn apply_to(self, workspace_configuration: &mut Value) { if let Some(use_flat_config) = self.use_flat_config && let Some(workspace_configuration) = workspace_configuration.as_object_mut() { workspace_configuration.insert("useFlatConfig".to_string(), json!(use_flat_config)); } if let Some(experimental_use_flat_config) = self.experimental_use_flat_config && let Some(workspace_configuration) = workspace_configuration.as_object_mut() { let experimental = workspace_configuration .entry("experimental") .or_insert_with(|| json!({})); if let Some(experimental) = experimental.as_object_mut() { experimental.insert( "useFlatConfig".to_string(), json!(experimental_use_flat_config), ); } } }} #[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 requested_file_path = requested_uri .as_ref() .filter(|uri| uri.scheme() == "file") .and_then(|uri| uri.to_file_path().ok()) .filter(|path| path.starts_with(worktree_root)); let eslint_version = find_eslint_version( delegate.as_ref(), worktree_root, requested_file_path.as_deref(), ) .await?; let config_kind = find_eslint_config_kind( worktree_root, requested_file_path.as_deref(), eslint_version.as_ref(), self.fs.as_ref(), ) .await; let eslint_settings_overrides = eslint_settings_overrides_for(eslint_version.as_ref(), config_kind); let mut default_workspace_configuration = json!({ "validate": "on", "rulesCustomizations": [], "run": "onType", "nodePath": null, "workingDirectory": { "mode": "auto" }, "workspaceFolder": { "uri": Uri::from_file_path(worktree_root) .map(|uri| uri.as_str().to_owned()) .unwrap_or_default(), "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 } } }); eslint_settings_overrides.apply_to(&mut default_workspace_configuration); let file_path = requested_file_path .as_ref() .and_then(|abs_path| abs_path.strip_prefix(worktree_root).ok()) .and_then(|p| RelPath::from_unix_str(&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 }} fn ancestor_directories<'a>( worktree_root: &'a Path, requested_file: Option<&'a Path>,) -> impl Iterator<Item = &'a Path> + 'a { let start = requested_file .filter(|file| file.starts_with(worktree_root)) .and_then(Path::parent) .unwrap_or(worktree_root); start .ancestors() .take_while(move |dir| dir.starts_with(worktree_root))} fn flat_config_file_names(version: Option<&Version>) -> &'static [&'static str] { match version { Some(version) if version.major >= 10 => EsLintLspAdapter::FLAT_CONFIG_FILE_NAMES_V10, Some(version) if version.major == 9 => EsLintLspAdapter::FLAT_CONFIG_FILE_NAMES_V8_57, Some(version) if version.major == 8 && version.minor >= 57 => { EsLintLspAdapter::FLAT_CONFIG_FILE_NAMES_V8_57 } Some(version) if version.major == 8 && version.minor >= 21 => { EsLintLspAdapter::FLAT_CONFIG_FILE_NAMES_V8_21 } _ => &[], }} async fn find_eslint_config_kind( worktree_root: &Path, requested_file: Option<&Path>, version: Option<&Version>, fs: &dyn Fs,) -> Option<EslintConfigKind> { let flat_config_file_names = flat_config_file_names(version); for directory in ancestor_directories(worktree_root, requested_file) { for file_name in flat_config_file_names { if fs.is_file(&directory.join(file_name)).await { return Some(EslintConfigKind::Flat); } } for file_name in EsLintLspAdapter::LEGACY_CONFIG_FILE_NAMES { if fs.is_file(&directory.join(file_name)).await { return Some(EslintConfigKind::Legacy); } } } None} fn eslint_settings_overrides_for( version: Option<&Version>, config_kind: Option<EslintConfigKind>,) -> EslintSettingsOverrides { // vscode-eslint 3.x already discovers config files and chooses a working // directory from the active file on its own. Zed only overrides settings // for the two cases where leaving everything unset is known to be wrong: // // - ESLint 8.21-8.56 flat config still needs experimental.useFlatConfig. // - ESLint 9.x legacy config needs useFlatConfig = false. // // All other cases should defer to the server's own defaults and discovery. let Some(version) = version else { return EslintSettingsOverrides::default(); }; match config_kind { Some(EslintConfigKind::Flat) if version.major == 8 && (21..57).contains(&version.minor) => { EslintSettingsOverrides { use_flat_config: None, experimental_use_flat_config: Some(true), } } Some(EslintConfigKind::Legacy) if version.major == 9 => EslintSettingsOverrides { use_flat_config: Some(false), experimental_use_flat_config: None, }, _ => EslintSettingsOverrides::default(), }} async fn find_eslint_version( delegate: &dyn LspAdapterDelegate, worktree_root: &Path, requested_file: Option<&Path>,) -> Result<Option<Version>> { for directory in ancestor_directories(worktree_root, requested_file) { if let Some(version) = read_package_installed_version(directory.join("node_modules"), "eslint").await? { return Ok(Some(version)); } } Ok(delegate .npm_package_installed_version("eslint") .await? .map(|(_, version)| version))} /// On Windows, converts Unix-style separators (/) to Windows-style (\)./// On Unix, returns the path unchangedfn 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(); }Lines 1–500 of 1147. Load more
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, read_package_installed_version}; use project::Fs; use project::lsp_store::language_server_settings_for; use semver::Version; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use settings::SettingsLocation; use smol::{fs, stream::StreamExt}; use std::{ ffi::OsString, future::Future, 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, fs: Arc<dyn Fs>, } impl EsLintLspAdapter { const CURRENT_VERSION: &'static str = "3.0.24"; const CURRENT_VERSION_TAG_NAME: &'static str = "release/3.0.24"; #[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_V8_21: &'static [&'static str] = &["eslint.config.js"]; const FLAT_CONFIG_FILE_NAMES_V8_57: &'static [&'static str] = &["eslint.config.js", "eslint.config.mjs", "eslint.config.cjs"]; const FLAT_CONFIG_FILE_NAMES_V10: &'static [&'static str] = &[ "eslint.config.js", "eslint.config.mjs", "eslint.config.cjs", "eslint.config.ts", "eslint.config.cts", "eslint.config.mts", ]; const LEGACY_CONFIG_FILE_NAMES: &'static [&'static str] = &[ ".eslintrc", ".eslintrc.js", ".eslintrc.cjs", ".eslintrc.yaml", ".eslintrc.yml", ".eslintrc.json", ]; pub fn new(node: NodeRuntime, fs: Arc<dyn Fs>) -> Self { EsLintLspAdapter { node, fs } } 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: &Arc<dyn LspAdapterDelegate>, _: bool, _: &mut AsyncApp, ) -> Result<GitHubLspBinaryVersion> { let url = build_asset_url( "microsoft/vscode-eslint", Self::CURRENT_VERSION_TAG_NAME, Self::GITHUB_ASSET_KIND, )?; Ok(GitHubLspBinaryVersion { name: Self::CURRENT_VERSION.into(), digest: None, url, }) } fn fetch_server_binary( &self, version: GitHubLspBinaryVersion, container_dir: PathBuf, delegate: &Arc<dyn LspAdapterDelegate>, ) -> impl Send + Future<Output = Result<LanguageServerBinary>> + use<> { let delegate = delegate.clone(); let node = self.node.clone(); async move { 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?; } node.run_npm_subcommand(Some(&repo_root), "install", &[]) .await?; node.run_npm_subcommand(Some(&repo_root), "run-script", &["compile"]) .await?; } Ok(LanguageServerBinary { path: 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); fs::metadata(&server_path).await.ok()?; Some(LanguageServerBinary { path: self.node.binary_path().await.ok()?, env: None, arguments: eslint_server_binary_arguments(&server_path), }) } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum EslintConfigKind { Flat, Legacy, } #[derive(Debug, Default, Clone, PartialEq, Eq)] struct EslintSettingsOverrides { use_flat_config: Option<bool>, experimental_use_flat_config: Option<bool>, } impl EslintSettingsOverrides { fn apply_to(self, workspace_configuration: &mut Value) { if let Some(use_flat_config) = self.use_flat_config && let Some(workspace_configuration) = workspace_configuration.as_object_mut() { workspace_configuration.insert("useFlatConfig".to_string(), json!(use_flat_config)); } if let Some(experimental_use_flat_config) = self.experimental_use_flat_config && let Some(workspace_configuration) = workspace_configuration.as_object_mut() { let experimental = workspace_configuration .entry("experimental") .or_insert_with(|| json!({})); if let Some(experimental) = experimental.as_object_mut() { experimental.insert( "useFlatConfig".to_string(), json!(experimental_use_flat_config), ); } } } } #[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 requested_file_path = requested_uri .as_ref() .filter(|uri| uri.scheme() == "file") .and_then(|uri| uri.to_file_path().ok()) .filter(|path| path.starts_with(worktree_root)); let eslint_version = find_eslint_version( delegate.as_ref(), worktree_root, requested_file_path.as_deref(), ) .await?; let config_kind = find_eslint_config_kind( worktree_root, requested_file_path.as_deref(), eslint_version.as_ref(), self.fs.as_ref(), ) .await; let eslint_settings_overrides = eslint_settings_overrides_for(eslint_version.as_ref(), config_kind); let mut default_workspace_configuration = json!({ "validate": "on", "rulesCustomizations": [], "run": "onType", "nodePath": null, "workingDirectory": { "mode": "auto" }, "workspaceFolder": { "uri": Uri::from_file_path(worktree_root) .map(|uri| uri.as_str().to_owned()) .unwrap_or_default(), "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 } } }); eslint_settings_overrides.apply_to(&mut default_workspace_configuration); let file_path = requested_file_path .as_ref() .and_then(|abs_path| abs_path.strip_prefix(worktree_root).ok()) .and_then(|p| RelPath::from_unix_str(&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 } } fn ancestor_directories<'a>( worktree_root: &'a Path, requested_file: Option<&'a Path>, ) -> impl Iterator<Item = &'a Path> + 'a { let start = requested_file .filter(|file| file.starts_with(worktree_root)) .and_then(Path::parent) .unwrap_or(worktree_root); start .ancestors() .take_while(move |dir| dir.starts_with(worktree_root)) } fn flat_config_file_names(version: Option<&Version>) -> &'static [&'static str] { match version { Some(version) if version.major >= 10 => EsLintLspAdapter::FLAT_CONFIG_FILE_NAMES_V10, Some(version) if version.major == 9 => EsLintLspAdapter::FLAT_CONFIG_FILE_NAMES_V8_57, Some(version) if version.major == 8 && version.minor >= 57 => { EsLintLspAdapter::FLAT_CONFIG_FILE_NAMES_V8_57 } Some(version) if version.major == 8 && version.minor >= 21 => { EsLintLspAdapter::FLAT_CONFIG_FILE_NAMES_V8_21 } _ => &[], } } async fn find_eslint_config_kind( worktree_root: &Path, requested_file: Option<&Path>, version: Option<&Version>, fs: &dyn Fs, ) -> Option<EslintConfigKind> { let flat_config_file_names = flat_config_file_names(version); for directory in ancestor_directories(worktree_root, requested_file) { for file_name in flat_config_file_names { if fs.is_file(&directory.join(file_name)).await { return Some(EslintConfigKind::Flat); } } for file_name in EsLintLspAdapter::LEGACY_CONFIG_FILE_NAMES { if fs.is_file(&directory.join(file_name)).await { return Some(EslintConfigKind::Legacy); } } } None } fn eslint_settings_overrides_for( version: Option<&Version>, config_kind: Option<EslintConfigKind>, ) -> EslintSettingsOverrides { // vscode-eslint 3.x already discovers config files and chooses a working // directory from the active file on its own. Zed only overrides settings // for the two cases where leaving everything unset is known to be wrong: // // - ESLint 8.21-8.56 flat config still needs experimental.useFlatConfig. // - ESLint 9.x legacy config needs useFlatConfig = false. // // All other cases should defer to the server's own defaults and discovery. let Some(version) = version else { return EslintSettingsOverrides::default(); }; match config_kind { Some(EslintConfigKind::Flat) if version.major == 8 && (21..57).contains(&version.minor) => { EslintSettingsOverrides { use_flat_config: None, experimental_use_flat_config: Some(true), } } Some(EslintConfigKind::Legacy) if version.major == 9 => EslintSettingsOverrides { use_flat_config: Some(false), experimental_use_flat_config: None, }, _ => EslintSettingsOverrides::default(), } } async fn find_eslint_version( delegate: &dyn LspAdapterDelegate, worktree_root: &Path, requested_file: Option<&Path>, ) -> Result<Option<Version>> { for directory in ancestor_directories(worktree_root, requested_file) { if let Some(version) = read_package_installed_version(directory.join("node_modules"), "eslint").await? { return Ok(Some(version)); } } Ok(delegate .npm_package_installed_version("eslint") .await? .map(|(_, version)| version)) } /// 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(); }