@@ -7,8 +7,10 @@ use http_client::{
};
use language::{LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName, Uri};
-use node_runtime::NodeRuntime;
+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;
@@ -31,11 +33,12 @@ fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
pub struct EsLintLspAdapter {
node: NodeRuntime,
+ fs: Arc<dyn Fs>,
}
impl EsLintLspAdapter {
- const CURRENT_VERSION: &'static str = "2.4.4";
- const CURRENT_VERSION_TAG_NAME: &'static str = "release/2.4.4";
+ 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;
@@ -45,7 +48,10 @@ impl EsLintLspAdapter {
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] = &[
+ 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",
@@ -53,9 +59,17 @@ impl EsLintLspAdapter {
"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) -> Self {
- EsLintLspAdapter { node }
+ pub fn new(node: NodeRuntime, fs: Arc<dyn Fs>) -> Self {
+ EsLintLspAdapter { node, fs }
}
fn build_destination_path(container_dir: &Path) -> PathBuf {
@@ -73,7 +87,7 @@ impl LspInstaller for EsLintLspAdapter {
_: &mut AsyncApp,
) -> Result<GitHubLspBinaryVersion> {
let url = build_asset_url(
- "zed-industries/vscode-eslint",
+ "microsoft/vscode-eslint",
Self::CURRENT_VERSION_TAG_NAME,
Self::GITHUB_ASSET_KIND,
)?;
@@ -157,6 +171,42 @@ impl LspInstaller for EsLintLspAdapter {
}
}
+#[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>> {
@@ -174,9 +224,26 @@ impl LspAdapter for EsLintLspAdapter {
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 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",
@@ -206,26 +273,13 @@ impl LspAdapter for EsLintLspAdapter {
"showDocumentation": {
"enable": true
}
- },
- "experimental": {
- "useFlatConfig": use_flat_config,
}
});
+ eslint_settings_overrides.apply_to(&mut default_workspace_configuration);
- let file_path = requested_uri
+ let file_path = requested_file_path
.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(|abs_path| abs_path.strip_prefix(worktree_root).ok())
.and_then(|p| RelPath::unix(&p).ok().map(ToOwned::to_owned))
.unwrap_or_else(|| RelPath::empty().to_owned());
let override_options = cx.update(|cx| {
@@ -272,6 +326,109 @@ impl LspAdapter for EsLintLspAdapter {
}
}
+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 {
@@ -624,6 +781,217 @@ mod tests {
}
}
+ mod eslint_settings {
+ use super::*;
+ use ::fs::FakeFs;
+ use gpui::TestAppContext;
+
+ #[test]
+ fn test_ancestor_directories_for_package_local_file() {
+ let worktree_root = PathBuf::from(unix_path_to_platform("/workspace"));
+ let requested_file = PathBuf::from(unix_path_to_platform(
+ "/workspace/packages/web/src/index.js",
+ ));
+
+ let directories: Vec<&Path> =
+ ancestor_directories(&worktree_root, Some(&requested_file)).collect();
+
+ assert_eq!(
+ directories,
+ vec![
+ Path::new(&unix_path_to_platform("/workspace/packages/web/src")),
+ Path::new(&unix_path_to_platform("/workspace/packages/web")),
+ Path::new(&unix_path_to_platform("/workspace/packages")),
+ Path::new(&unix_path_to_platform("/workspace")),
+ ]
+ );
+ }
+
+ #[test]
+ fn test_eslint_8_flat_root_repo_uses_experimental_flag() {
+ let version = Version::parse("8.56.0").expect("valid ESLint version");
+ let settings =
+ eslint_settings_overrides_for(Some(&version), Some(EslintConfigKind::Flat));
+
+ assert_eq!(
+ settings,
+ EslintSettingsOverrides {
+ use_flat_config: None,
+ experimental_use_flat_config: Some(true),
+ }
+ );
+ }
+
+ #[test]
+ fn test_eslint_8_57_flat_repo_uses_no_override() {
+ let version = Version::parse("8.57.0").expect("valid ESLint version");
+ let settings =
+ eslint_settings_overrides_for(Some(&version), Some(EslintConfigKind::Flat));
+
+ assert_eq!(settings, EslintSettingsOverrides::default());
+ }
+
+ #[test]
+ fn test_eslint_9_legacy_repo_uses_use_flat_config_false() {
+ let version = Version::parse("9.0.0").expect("valid ESLint version");
+ let settings =
+ eslint_settings_overrides_for(Some(&version), Some(EslintConfigKind::Legacy));
+
+ assert_eq!(
+ settings,
+ EslintSettingsOverrides {
+ use_flat_config: Some(false),
+ experimental_use_flat_config: None,
+ }
+ );
+ }
+
+ #[test]
+ fn test_eslint_10_repo_uses_no_override() {
+ let version = Version::parse("10.0.0").expect("valid ESLint version");
+ let settings =
+ eslint_settings_overrides_for(Some(&version), Some(EslintConfigKind::Flat));
+
+ assert_eq!(settings, EslintSettingsOverrides::default());
+ }
+
+ #[gpui::test]
+ async fn test_eslint_8_56_does_not_treat_cjs_as_flat_config(cx: &mut TestAppContext) {
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ unix_path_to_platform("/workspace"),
+ json!({ "eslint.config.cjs": "" }),
+ )
+ .await;
+ let worktree_root = PathBuf::from(unix_path_to_platform("/workspace"));
+ let requested_file = PathBuf::from(unix_path_to_platform("/workspace/src/index.js"));
+ let version = Version::parse("8.56.0").expect("valid ESLint version");
+
+ let config_kind = find_eslint_config_kind(
+ &worktree_root,
+ Some(&requested_file),
+ Some(&version),
+ fs.as_ref(),
+ )
+ .await;
+
+ assert_eq!(config_kind, None);
+ }
+
+ #[gpui::test]
+ async fn test_eslint_8_57_treats_cjs_as_flat_config(cx: &mut TestAppContext) {
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ unix_path_to_platform("/workspace"),
+ json!({ "eslint.config.cjs": "" }),
+ )
+ .await;
+ let worktree_root = PathBuf::from(unix_path_to_platform("/workspace"));
+ let requested_file = PathBuf::from(unix_path_to_platform("/workspace/src/index.js"));
+ let version = Version::parse("8.57.0").expect("valid ESLint version");
+
+ let config_kind = find_eslint_config_kind(
+ &worktree_root,
+ Some(&requested_file),
+ Some(&version),
+ fs.as_ref(),
+ )
+ .await;
+
+ assert_eq!(config_kind, Some(EslintConfigKind::Flat));
+ }
+
+ #[gpui::test]
+ async fn test_eslint_10_treats_typescript_config_as_flat_config(cx: &mut TestAppContext) {
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ unix_path_to_platform("/workspace"),
+ json!({ "eslint.config.ts": "" }),
+ )
+ .await;
+ let worktree_root = PathBuf::from(unix_path_to_platform("/workspace"));
+ let requested_file = PathBuf::from(unix_path_to_platform("/workspace/src/index.js"));
+ let version = Version::parse("10.0.0").expect("valid ESLint version");
+
+ let config_kind = find_eslint_config_kind(
+ &worktree_root,
+ Some(&requested_file),
+ Some(&version),
+ fs.as_ref(),
+ )
+ .await;
+
+ assert_eq!(config_kind, Some(EslintConfigKind::Flat));
+ }
+
+ #[gpui::test]
+ async fn test_package_local_flat_config_is_preferred_for_monorepo_file(
+ cx: &mut TestAppContext,
+ ) {
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ unix_path_to_platform("/workspace"),
+ json!({
+ "eslint.config.js": "",
+ "packages": {
+ "web": {
+ "eslint.config.js": ""
+ }
+ }
+ }),
+ )
+ .await;
+ let worktree_root = PathBuf::from(unix_path_to_platform("/workspace"));
+ let requested_file = PathBuf::from(unix_path_to_platform(
+ "/workspace/packages/web/src/index.js",
+ ));
+ let version = Version::parse("8.56.0").expect("valid ESLint version");
+
+ let config_kind = find_eslint_config_kind(
+ &worktree_root,
+ Some(&requested_file),
+ Some(&version),
+ fs.as_ref(),
+ )
+ .await;
+
+ assert_eq!(config_kind, Some(EslintConfigKind::Flat));
+ }
+
+ #[gpui::test]
+ async fn test_package_local_legacy_config_is_detected_for_eslint_9(
+ cx: &mut TestAppContext,
+ ) {
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ unix_path_to_platform("/workspace"),
+ json!({
+ "packages": {
+ "web": {
+ ".eslintrc.cjs": ""
+ }
+ }
+ }),
+ )
+ .await;
+ let worktree_root = PathBuf::from(unix_path_to_platform("/workspace"));
+ let requested_file = PathBuf::from(unix_path_to_platform(
+ "/workspace/packages/web/src/index.js",
+ ));
+ let version = Version::parse("9.0.0").expect("valid ESLint version");
+
+ let config_kind = find_eslint_config_kind(
+ &worktree_root,
+ Some(&requested_file),
+ Some(&version),
+ fs.as_ref(),
+ )
+ .await;
+
+ assert_eq!(config_kind, Some(EslintConfigKind::Legacy));
+ }
+ }
+
#[cfg(windows)]
mod windows_style_paths {
use super::*;