1use anyhow::{Context as _, Result};
2use async_trait::async_trait;
3use gpui::AsyncApp;
4use http_client::{
5 github::{AssetKind, GitHubLspBinaryVersion, build_asset_url},
6 github_download::download_server_binary,
7};
8use language::{LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
9use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName, Uri};
10use node_runtime::{NodeRuntime, read_package_installed_version};
11use project::Fs;
12use project::lsp_store::language_server_settings_for;
13use semver::Version;
14use serde::{Deserialize, Serialize};
15use serde_json::{Value, json};
16use settings::SettingsLocation;
17use smol::{fs, stream::StreamExt};
18use std::{
19 ffi::OsString,
20 path::{Path, PathBuf},
21 sync::Arc,
22};
23use util::merge_json_value_into;
24use util::{fs::remove_matching, rel_path::RelPath};
25
26fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
27 vec![
28 "--max-old-space-size=8192".into(),
29 server_path.into(),
30 "--stdio".into(),
31 ]
32}
33
34pub struct EsLintLspAdapter {
35 node: NodeRuntime,
36 fs: Arc<dyn Fs>,
37}
38
39impl EsLintLspAdapter {
40 const CURRENT_VERSION: &'static str = "3.0.24";
41 const CURRENT_VERSION_TAG_NAME: &'static str = "release/3.0.24";
42
43 #[cfg(not(windows))]
44 const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
45 #[cfg(windows)]
46 const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
47
48 const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
49 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("eslint");
50
51 const FLAT_CONFIG_FILE_NAMES_V8_21: &'static [&'static str] = &["eslint.config.js"];
52 const FLAT_CONFIG_FILE_NAMES_V8_57: &'static [&'static str] =
53 &["eslint.config.js", "eslint.config.mjs", "eslint.config.cjs"];
54 const FLAT_CONFIG_FILE_NAMES_V10: &'static [&'static str] = &[
55 "eslint.config.js",
56 "eslint.config.mjs",
57 "eslint.config.cjs",
58 "eslint.config.ts",
59 "eslint.config.cts",
60 "eslint.config.mts",
61 ];
62 const LEGACY_CONFIG_FILE_NAMES: &'static [&'static str] = &[
63 ".eslintrc",
64 ".eslintrc.js",
65 ".eslintrc.cjs",
66 ".eslintrc.yaml",
67 ".eslintrc.yml",
68 ".eslintrc.json",
69 ];
70
71 pub fn new(node: NodeRuntime, fs: Arc<dyn Fs>) -> Self {
72 EsLintLspAdapter { node, fs }
73 }
74
75 fn build_destination_path(container_dir: &Path) -> PathBuf {
76 container_dir.join(format!("vscode-eslint-{}", Self::CURRENT_VERSION))
77 }
78}
79
80impl LspInstaller for EsLintLspAdapter {
81 type BinaryVersion = GitHubLspBinaryVersion;
82
83 async fn fetch_latest_server_version(
84 &self,
85 _delegate: &dyn LspAdapterDelegate,
86 _: bool,
87 _: &mut AsyncApp,
88 ) -> Result<GitHubLspBinaryVersion> {
89 let url = build_asset_url(
90 "microsoft/vscode-eslint",
91 Self::CURRENT_VERSION_TAG_NAME,
92 Self::GITHUB_ASSET_KIND,
93 )?;
94
95 Ok(GitHubLspBinaryVersion {
96 name: Self::CURRENT_VERSION.into(),
97 digest: None,
98 url,
99 })
100 }
101
102 async fn fetch_server_binary(
103 &self,
104 version: GitHubLspBinaryVersion,
105 container_dir: PathBuf,
106 delegate: &dyn LspAdapterDelegate,
107 ) -> Result<LanguageServerBinary> {
108 let destination_path = Self::build_destination_path(&container_dir);
109 let server_path = destination_path.join(Self::SERVER_PATH);
110
111 if fs::metadata(&server_path).await.is_err() {
112 remove_matching(&container_dir, |_| true).await;
113
114 download_server_binary(
115 &*delegate.http_client(),
116 &version.url,
117 None,
118 &destination_path,
119 Self::GITHUB_ASSET_KIND,
120 )
121 .await?;
122
123 let mut dir = fs::read_dir(&destination_path).await?;
124 let first = dir.next().await.context("missing first file")??;
125 let repo_root = destination_path.join("vscode-eslint");
126 fs::rename(first.path(), &repo_root).await?;
127
128 #[cfg(target_os = "windows")]
129 {
130 handle_symlink(
131 repo_root.join("$shared"),
132 repo_root.join("client").join("src").join("shared"),
133 )
134 .await?;
135 handle_symlink(
136 repo_root.join("$shared"),
137 repo_root.join("server").join("src").join("shared"),
138 )
139 .await?;
140 }
141
142 self.node
143 .run_npm_subcommand(Some(&repo_root), "install", &[])
144 .await?;
145
146 self.node
147 .run_npm_subcommand(Some(&repo_root), "run-script", &["compile"])
148 .await?;
149 }
150
151 Ok(LanguageServerBinary {
152 path: self.node.binary_path().await?,
153 env: None,
154 arguments: eslint_server_binary_arguments(&server_path),
155 })
156 }
157
158 async fn cached_server_binary(
159 &self,
160 container_dir: PathBuf,
161 _: &dyn LspAdapterDelegate,
162 ) -> Option<LanguageServerBinary> {
163 let server_path =
164 Self::build_destination_path(&container_dir).join(EsLintLspAdapter::SERVER_PATH);
165 fs::metadata(&server_path).await.ok()?;
166 Some(LanguageServerBinary {
167 path: self.node.binary_path().await.ok()?,
168 env: None,
169 arguments: eslint_server_binary_arguments(&server_path),
170 })
171 }
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175enum EslintConfigKind {
176 Flat,
177 Legacy,
178}
179
180#[derive(Debug, Default, Clone, PartialEq, Eq)]
181struct EslintSettingsOverrides {
182 use_flat_config: Option<bool>,
183 experimental_use_flat_config: Option<bool>,
184}
185
186impl EslintSettingsOverrides {
187 fn apply_to(self, workspace_configuration: &mut Value) {
188 if let Some(use_flat_config) = self.use_flat_config
189 && let Some(workspace_configuration) = workspace_configuration.as_object_mut()
190 {
191 workspace_configuration.insert("useFlatConfig".to_string(), json!(use_flat_config));
192 }
193
194 if let Some(experimental_use_flat_config) = self.experimental_use_flat_config
195 && let Some(workspace_configuration) = workspace_configuration.as_object_mut()
196 {
197 let experimental = workspace_configuration
198 .entry("experimental")
199 .or_insert_with(|| json!({}));
200 if let Some(experimental) = experimental.as_object_mut() {
201 experimental.insert(
202 "useFlatConfig".to_string(),
203 json!(experimental_use_flat_config),
204 );
205 }
206 }
207 }
208}
209
210#[async_trait(?Send)]
211impl LspAdapter for EsLintLspAdapter {
212 fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
213 Some(vec![
214 CodeActionKind::QUICKFIX,
215 CodeActionKind::new("source.fixAll.eslint"),
216 ])
217 }
218
219 async fn workspace_configuration(
220 self: Arc<Self>,
221 delegate: &Arc<dyn LspAdapterDelegate>,
222 _: Option<Toolchain>,
223 requested_uri: Option<Uri>,
224 cx: &mut AsyncApp,
225 ) -> Result<Value> {
226 let worktree_root = delegate.worktree_root_path();
227 let requested_file_path = requested_uri
228 .as_ref()
229 .filter(|uri| uri.scheme() == "file")
230 .and_then(|uri| uri.to_file_path().ok())
231 .filter(|path| path.starts_with(worktree_root));
232 let eslint_version = find_eslint_version(
233 delegate.as_ref(),
234 worktree_root,
235 requested_file_path.as_deref(),
236 )
237 .await?;
238 let config_kind = find_eslint_config_kind(
239 worktree_root,
240 requested_file_path.as_deref(),
241 eslint_version.as_ref(),
242 self.fs.as_ref(),
243 )
244 .await;
245 let eslint_settings_overrides =
246 eslint_settings_overrides_for(eslint_version.as_ref(), config_kind);
247
248 let mut default_workspace_configuration = json!({
249 "validate": "on",
250 "rulesCustomizations": [],
251 "run": "onType",
252 "nodePath": null,
253 "workingDirectory": {
254 "mode": "auto"
255 },
256 "workspaceFolder": {
257 "uri": worktree_root,
258 "name": worktree_root.file_name()
259 .unwrap_or(worktree_root.as_os_str())
260 .to_string_lossy(),
261 },
262 "problems": {},
263 "codeActionOnSave": {
264 // We enable this, but without also configuring code_actions_on_format
265 // in the Zed configuration, it doesn't have an effect.
266 "enable": true,
267 },
268 "codeAction": {
269 "disableRuleComment": {
270 "enable": true,
271 "location": "separateLine",
272 },
273 "showDocumentation": {
274 "enable": true
275 }
276 }
277 });
278 eslint_settings_overrides.apply_to(&mut default_workspace_configuration);
279
280 let file_path = requested_file_path
281 .as_ref()
282 .and_then(|abs_path| abs_path.strip_prefix(worktree_root).ok())
283 .and_then(|p| RelPath::unix(&p).ok().map(ToOwned::to_owned))
284 .unwrap_or_else(|| RelPath::empty().to_owned());
285 let override_options = cx.update(|cx| {
286 language_server_settings_for(
287 SettingsLocation {
288 worktree_id: delegate.worktree_id(),
289 path: &file_path,
290 },
291 &Self::SERVER_NAME,
292 cx,
293 )
294 .and_then(|s| s.settings.clone())
295 });
296
297 if let Some(override_options) = override_options {
298 let working_directories = override_options.get("workingDirectories").and_then(|wd| {
299 serde_json::from_value::<WorkingDirectories>(wd.clone())
300 .ok()
301 .and_then(|wd| wd.0)
302 });
303
304 merge_json_value_into(override_options, &mut default_workspace_configuration);
305
306 let working_directory = working_directories
307 .zip(requested_uri)
308 .and_then(|(wd, uri)| {
309 determine_working_directory(uri, wd, worktree_root.to_owned())
310 });
311
312 if let Some(working_directory) = working_directory
313 && let Some(wd) = default_workspace_configuration.get_mut("workingDirectory")
314 {
315 *wd = serde_json::to_value(working_directory)?;
316 }
317 }
318
319 Ok(json!({
320 "": default_workspace_configuration
321 }))
322 }
323
324 fn name(&self) -> LanguageServerName {
325 Self::SERVER_NAME
326 }
327}
328
329fn ancestor_directories<'a>(
330 worktree_root: &'a Path,
331 requested_file: Option<&'a Path>,
332) -> impl Iterator<Item = &'a Path> + 'a {
333 let start = requested_file
334 .filter(|file| file.starts_with(worktree_root))
335 .and_then(Path::parent)
336 .unwrap_or(worktree_root);
337
338 start
339 .ancestors()
340 .take_while(move |dir| dir.starts_with(worktree_root))
341}
342
343fn flat_config_file_names(version: Option<&Version>) -> &'static [&'static str] {
344 match version {
345 Some(version) if version.major >= 10 => EsLintLspAdapter::FLAT_CONFIG_FILE_NAMES_V10,
346 Some(version) if version.major == 9 => EsLintLspAdapter::FLAT_CONFIG_FILE_NAMES_V8_57,
347 Some(version) if version.major == 8 && version.minor >= 57 => {
348 EsLintLspAdapter::FLAT_CONFIG_FILE_NAMES_V8_57
349 }
350 Some(version) if version.major == 8 && version.minor >= 21 => {
351 EsLintLspAdapter::FLAT_CONFIG_FILE_NAMES_V8_21
352 }
353 _ => &[],
354 }
355}
356
357async fn find_eslint_config_kind(
358 worktree_root: &Path,
359 requested_file: Option<&Path>,
360 version: Option<&Version>,
361 fs: &dyn Fs,
362) -> Option<EslintConfigKind> {
363 let flat_config_file_names = flat_config_file_names(version);
364
365 for directory in ancestor_directories(worktree_root, requested_file) {
366 for file_name in flat_config_file_names {
367 if fs.is_file(&directory.join(file_name)).await {
368 return Some(EslintConfigKind::Flat);
369 }
370 }
371
372 for file_name in EsLintLspAdapter::LEGACY_CONFIG_FILE_NAMES {
373 if fs.is_file(&directory.join(file_name)).await {
374 return Some(EslintConfigKind::Legacy);
375 }
376 }
377 }
378
379 None
380}
381
382fn eslint_settings_overrides_for(
383 version: Option<&Version>,
384 config_kind: Option<EslintConfigKind>,
385) -> EslintSettingsOverrides {
386 // vscode-eslint 3.x already discovers config files and chooses a working
387 // directory from the active file on its own. Zed only overrides settings
388 // for the two cases where leaving everything unset is known to be wrong:
389 //
390 // - ESLint 8.21-8.56 flat config still needs experimental.useFlatConfig.
391 // - ESLint 9.x legacy config needs useFlatConfig = false.
392 //
393 // All other cases should defer to the server's own defaults and discovery.
394 let Some(version) = version else {
395 return EslintSettingsOverrides::default();
396 };
397
398 match config_kind {
399 Some(EslintConfigKind::Flat) if version.major == 8 && (21..57).contains(&version.minor) => {
400 EslintSettingsOverrides {
401 use_flat_config: None,
402 experimental_use_flat_config: Some(true),
403 }
404 }
405 Some(EslintConfigKind::Legacy) if version.major == 9 => EslintSettingsOverrides {
406 use_flat_config: Some(false),
407 experimental_use_flat_config: None,
408 },
409 _ => EslintSettingsOverrides::default(),
410 }
411}
412
413async fn find_eslint_version(
414 delegate: &dyn LspAdapterDelegate,
415 worktree_root: &Path,
416 requested_file: Option<&Path>,
417) -> Result<Option<Version>> {
418 for directory in ancestor_directories(worktree_root, requested_file) {
419 if let Some(version) =
420 read_package_installed_version(directory.join("node_modules"), "eslint").await?
421 {
422 return Ok(Some(version));
423 }
424 }
425
426 Ok(delegate
427 .npm_package_installed_version("eslint")
428 .await?
429 .map(|(_, version)| version))
430}
431
432/// On Windows, converts Unix-style separators (/) to Windows-style (\).
433/// On Unix, returns the path unchanged
434fn normalize_path_separators(path: &str) -> String {
435 #[cfg(windows)]
436 {
437 path.replace('/', "\\")
438 }
439 #[cfg(not(windows))]
440 {
441 path.to_string()
442 }
443}
444
445fn determine_working_directory(
446 uri: Uri,
447 working_directories: Vec<WorkingDirectory>,
448 workspace_folder_path: PathBuf,
449) -> Option<ResultWorkingDirectory> {
450 let mut working_directory = None;
451
452 for item in working_directories {
453 let mut directory: Option<String> = None;
454 let mut pattern: Option<String> = None;
455 let mut no_cwd = false;
456 match item {
457 WorkingDirectory::String(contents) => {
458 directory = Some(normalize_path_separators(&contents));
459 }
460 WorkingDirectory::LegacyDirectoryItem(legacy_directory_item) => {
461 directory = Some(normalize_path_separators(&legacy_directory_item.directory));
462 no_cwd = !legacy_directory_item.change_process_cwd;
463 }
464 WorkingDirectory::DirectoryItem(directory_item) => {
465 directory = Some(normalize_path_separators(&directory_item.directory));
466 if let Some(not_cwd) = directory_item.not_cwd {
467 no_cwd = not_cwd;
468 }
469 }
470 WorkingDirectory::PatternItem(pattern_item) => {
471 pattern = Some(normalize_path_separators(&pattern_item.pattern));
472 if let Some(not_cwd) = pattern_item.not_cwd {
473 no_cwd = not_cwd;
474 }
475 }
476 WorkingDirectory::ModeItem(mode_item) => {
477 working_directory = Some(ResultWorkingDirectory::ModeItem(mode_item));
478 continue;
479 }
480 }
481
482 let mut item_value: Option<String> = None;
483 if directory.is_some() || pattern.is_some() {
484 let file_path: Option<PathBuf> = (uri.scheme() == "file")
485 .then(|| uri.to_file_path().ok())
486 .flatten();
487 if let Some(file_path) = file_path {
488 if let Some(mut directory) = directory {
489 if Path::new(&directory).is_relative() {
490 directory = workspace_folder_path
491 .join(directory)
492 .to_string_lossy()
493 .to_string();
494 }
495 if !directory.ends_with(std::path::MAIN_SEPARATOR) {
496 directory.push(std::path::MAIN_SEPARATOR);
497 }
498 if file_path.starts_with(&directory) {
499 item_value = Some(directory);
500 }
501 } else if let Some(mut pattern) = pattern
502 && !pattern.is_empty()
503 {
504 if Path::new(&pattern).is_relative() {
505 pattern = workspace_folder_path
506 .join(pattern)
507 .to_string_lossy()
508 .to_string();
509 }
510 if !pattern.ends_with(std::path::MAIN_SEPARATOR) {
511 pattern.push(std::path::MAIN_SEPARATOR);
512 }
513 if let Some(matched) = match_glob_pattern(&pattern, &file_path) {
514 item_value = Some(matched);
515 }
516 }
517 }
518 }
519 if let Some(item_value) = item_value {
520 if working_directory
521 .as_ref()
522 .is_none_or(|wd| matches!(wd, ResultWorkingDirectory::ModeItem(_)))
523 {
524 working_directory = Some(ResultWorkingDirectory::DirectoryItem(DirectoryItem {
525 directory: item_value,
526 not_cwd: Some(no_cwd),
527 }));
528 } else if let Some(ResultWorkingDirectory::DirectoryItem(item)) = &mut working_directory
529 && item.directory.len() < item_value.len()
530 {
531 item.directory = item_value;
532 item.not_cwd = Some(no_cwd);
533 }
534 }
535 }
536
537 working_directory
538}
539
540fn match_glob_pattern(pattern: &str, file_path: &Path) -> Option<String> {
541 use globset::GlobBuilder;
542
543 let glob = GlobBuilder::new(pattern)
544 .literal_separator(true)
545 .build()
546 .ok()?
547 .compile_matcher();
548
549 let mut current = file_path.to_path_buf();
550 let mut matched: Option<String> = None;
551
552 while let Some(parent) = current.parent() {
553 let mut prefix = parent.to_string_lossy().to_string();
554 if !prefix.ends_with(std::path::MAIN_SEPARATOR) {
555 prefix.push(std::path::MAIN_SEPARATOR);
556 }
557 if glob.is_match(&prefix) {
558 matched = Some(prefix);
559 }
560 current = parent.to_path_buf();
561 }
562
563 matched
564}
565
566#[cfg(target_os = "windows")]
567async fn handle_symlink(src_dir: PathBuf, dest_dir: PathBuf) -> Result<()> {
568 anyhow::ensure!(
569 fs::metadata(&src_dir).await.is_ok(),
570 "Directory {src_dir:?} is not present"
571 );
572 if fs::metadata(&dest_dir).await.is_ok() {
573 fs::remove_file(&dest_dir).await?;
574 }
575 fs::create_dir_all(&dest_dir).await?;
576 let mut entries = fs::read_dir(&src_dir).await?;
577 while let Some(entry) = entries.try_next().await? {
578 let entry_path = entry.path();
579 let entry_name = entry.file_name();
580 let dest_path = dest_dir.join(&entry_name);
581 fs::copy(&entry_path, &dest_path).await?;
582 }
583 Ok(())
584}
585
586#[derive(Serialize, Deserialize, Debug)]
587struct LegacyDirectoryItem {
588 directory: String,
589 #[serde(rename = "changeProcessCWD")]
590 change_process_cwd: bool,
591}
592
593#[derive(Serialize, Deserialize, Debug)]
594struct DirectoryItem {
595 directory: String,
596 #[serde(rename = "!cwd")]
597 not_cwd: Option<bool>,
598}
599
600#[derive(Serialize, Deserialize, Debug)]
601struct PatternItem {
602 pattern: String,
603 #[serde(rename = "!cwd")]
604 not_cwd: Option<bool>,
605}
606
607#[derive(Serialize, Deserialize, Debug)]
608struct ModeItem {
609 mode: ModeEnum,
610}
611
612#[derive(Serialize, Deserialize, Debug)]
613#[serde(rename_all = "lowercase")]
614enum ModeEnum {
615 Auto,
616 Location,
617}
618
619#[derive(Serialize, Deserialize, Debug)]
620#[serde(untagged)]
621enum WorkingDirectory {
622 String(String),
623 LegacyDirectoryItem(LegacyDirectoryItem),
624 DirectoryItem(DirectoryItem),
625 PatternItem(PatternItem),
626 ModeItem(ModeItem),
627}
628#[derive(Serialize, Deserialize)]
629struct WorkingDirectories(Option<Vec<WorkingDirectory>>);
630
631#[derive(Serialize, Deserialize, Debug)]
632#[serde(untagged)]
633enum ResultWorkingDirectory {
634 ModeItem(ModeItem),
635 DirectoryItem(DirectoryItem),
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641
642 mod glob_patterns {
643 use super::*;
644
645 #[test]
646 fn test_match_glob_pattern() {
647 let pattern = unix_path_to_platform("/test/*/");
648 let file_path = PathBuf::from(unix_path_to_platform("/test/foo/bar/file.txt"));
649 let matched = match_glob_pattern(&pattern, &file_path);
650 assert_eq!(matched, Some(unix_path_to_platform("/test/foo/")));
651 }
652
653 #[test]
654 fn test_match_glob_pattern_globstar() {
655 let pattern = unix_path_to_platform("/workspace/**/src/");
656 let file_path = PathBuf::from(unix_path_to_platform(
657 "/workspace/packages/core/src/index.ts",
658 ));
659 let matched = match_glob_pattern(&pattern, &file_path);
660 assert_eq!(
661 matched,
662 Some(unix_path_to_platform("/workspace/packages/core/src/"))
663 );
664 }
665
666 #[test]
667 fn test_match_glob_pattern_no_match() {
668 let pattern = unix_path_to_platform("/other/*/");
669 let file_path = PathBuf::from(unix_path_to_platform("/test/foo/bar/file.txt"));
670 let matched = match_glob_pattern(&pattern, &file_path);
671 assert_eq!(matched, None);
672 }
673 }
674
675 mod unix_style_paths {
676 use super::*;
677
678 #[test]
679 fn test_working_directory_string() {
680 let uri = make_uri("/workspace/packages/foo/src/file.ts");
681 let working_directories = vec![WorkingDirectory::String("packages/foo".to_string())];
682 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
683
684 let result = determine_working_directory(uri, working_directories, workspace_folder);
685 assert_directory_result(
686 result,
687 &unix_path_to_platform("/workspace/packages/foo/"),
688 false,
689 );
690 }
691
692 #[test]
693 fn test_working_directory_absolute_path() {
694 let uri = make_uri("/workspace/packages/foo/src/file.ts");
695 let working_directories = vec![WorkingDirectory::String(unix_path_to_platform(
696 "/workspace/packages/foo",
697 ))];
698 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
699
700 let result = determine_working_directory(uri, working_directories, workspace_folder);
701 assert_directory_result(
702 result,
703 &unix_path_to_platform("/workspace/packages/foo/"),
704 false,
705 );
706 }
707
708 #[test]
709 fn test_working_directory_directory_item() {
710 let uri = make_uri("/workspace/packages/foo/src/file.ts");
711 let working_directories = vec![WorkingDirectory::DirectoryItem(DirectoryItem {
712 directory: "packages/foo".to_string(),
713 not_cwd: Some(true),
714 })];
715 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
716
717 let result = determine_working_directory(uri, working_directories, workspace_folder);
718 assert_directory_result(
719 result,
720 &unix_path_to_platform("/workspace/packages/foo/"),
721 true,
722 );
723 }
724
725 #[test]
726 fn test_working_directory_legacy_item() {
727 let uri = make_uri("/workspace/packages/foo/src/file.ts");
728 let working_directories =
729 vec![WorkingDirectory::LegacyDirectoryItem(LegacyDirectoryItem {
730 directory: "packages/foo".to_string(),
731 change_process_cwd: false,
732 })];
733 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
734
735 let result = determine_working_directory(uri, working_directories, workspace_folder);
736 assert_directory_result(
737 result,
738 &unix_path_to_platform("/workspace/packages/foo/"),
739 true,
740 );
741 }
742
743 #[test]
744 fn test_working_directory_pattern_item() {
745 let uri = make_uri("/workspace/packages/foo/src/file.ts");
746 let working_directories = vec![WorkingDirectory::PatternItem(PatternItem {
747 pattern: "packages/*/".to_string(),
748 not_cwd: Some(false),
749 })];
750 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
751
752 let result = determine_working_directory(uri, working_directories, workspace_folder);
753 assert_directory_result(
754 result,
755 &unix_path_to_platform("/workspace/packages/foo/"),
756 false,
757 );
758 }
759
760 #[test]
761 fn test_working_directory_multiple_patterns() {
762 let uri = make_uri("/workspace/apps/web/src/file.ts");
763 let working_directories = vec![
764 WorkingDirectory::PatternItem(PatternItem {
765 pattern: "packages/*/".to_string(),
766 not_cwd: None,
767 }),
768 WorkingDirectory::PatternItem(PatternItem {
769 pattern: "apps/*/".to_string(),
770 not_cwd: None,
771 }),
772 ];
773 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
774
775 let result = determine_working_directory(uri, working_directories, workspace_folder);
776 assert_directory_result(
777 result,
778 &unix_path_to_platform("/workspace/apps/web/"),
779 false,
780 );
781 }
782 }
783
784 mod eslint_settings {
785 use super::*;
786 use ::fs::FakeFs;
787 use gpui::TestAppContext;
788
789 #[test]
790 fn test_ancestor_directories_for_package_local_file() {
791 let worktree_root = PathBuf::from(unix_path_to_platform("/workspace"));
792 let requested_file = PathBuf::from(unix_path_to_platform(
793 "/workspace/packages/web/src/index.js",
794 ));
795
796 let directories: Vec<&Path> =
797 ancestor_directories(&worktree_root, Some(&requested_file)).collect();
798
799 assert_eq!(
800 directories,
801 vec![
802 Path::new(&unix_path_to_platform("/workspace/packages/web/src")),
803 Path::new(&unix_path_to_platform("/workspace/packages/web")),
804 Path::new(&unix_path_to_platform("/workspace/packages")),
805 Path::new(&unix_path_to_platform("/workspace")),
806 ]
807 );
808 }
809
810 #[test]
811 fn test_eslint_8_flat_root_repo_uses_experimental_flag() {
812 let version = Version::parse("8.56.0").expect("valid ESLint version");
813 let settings =
814 eslint_settings_overrides_for(Some(&version), Some(EslintConfigKind::Flat));
815
816 assert_eq!(
817 settings,
818 EslintSettingsOverrides {
819 use_flat_config: None,
820 experimental_use_flat_config: Some(true),
821 }
822 );
823 }
824
825 #[test]
826 fn test_eslint_8_57_flat_repo_uses_no_override() {
827 let version = Version::parse("8.57.0").expect("valid ESLint version");
828 let settings =
829 eslint_settings_overrides_for(Some(&version), Some(EslintConfigKind::Flat));
830
831 assert_eq!(settings, EslintSettingsOverrides::default());
832 }
833
834 #[test]
835 fn test_eslint_9_legacy_repo_uses_use_flat_config_false() {
836 let version = Version::parse("9.0.0").expect("valid ESLint version");
837 let settings =
838 eslint_settings_overrides_for(Some(&version), Some(EslintConfigKind::Legacy));
839
840 assert_eq!(
841 settings,
842 EslintSettingsOverrides {
843 use_flat_config: Some(false),
844 experimental_use_flat_config: None,
845 }
846 );
847 }
848
849 #[test]
850 fn test_eslint_10_repo_uses_no_override() {
851 let version = Version::parse("10.0.0").expect("valid ESLint version");
852 let settings =
853 eslint_settings_overrides_for(Some(&version), Some(EslintConfigKind::Flat));
854
855 assert_eq!(settings, EslintSettingsOverrides::default());
856 }
857
858 #[gpui::test]
859 async fn test_eslint_8_56_does_not_treat_cjs_as_flat_config(cx: &mut TestAppContext) {
860 let fs = FakeFs::new(cx.executor());
861 fs.insert_tree(
862 unix_path_to_platform("/workspace"),
863 json!({ "eslint.config.cjs": "" }),
864 )
865 .await;
866 let worktree_root = PathBuf::from(unix_path_to_platform("/workspace"));
867 let requested_file = PathBuf::from(unix_path_to_platform("/workspace/src/index.js"));
868 let version = Version::parse("8.56.0").expect("valid ESLint version");
869
870 let config_kind = find_eslint_config_kind(
871 &worktree_root,
872 Some(&requested_file),
873 Some(&version),
874 fs.as_ref(),
875 )
876 .await;
877
878 assert_eq!(config_kind, None);
879 }
880
881 #[gpui::test]
882 async fn test_eslint_8_57_treats_cjs_as_flat_config(cx: &mut TestAppContext) {
883 let fs = FakeFs::new(cx.executor());
884 fs.insert_tree(
885 unix_path_to_platform("/workspace"),
886 json!({ "eslint.config.cjs": "" }),
887 )
888 .await;
889 let worktree_root = PathBuf::from(unix_path_to_platform("/workspace"));
890 let requested_file = PathBuf::from(unix_path_to_platform("/workspace/src/index.js"));
891 let version = Version::parse("8.57.0").expect("valid ESLint version");
892
893 let config_kind = find_eslint_config_kind(
894 &worktree_root,
895 Some(&requested_file),
896 Some(&version),
897 fs.as_ref(),
898 )
899 .await;
900
901 assert_eq!(config_kind, Some(EslintConfigKind::Flat));
902 }
903
904 #[gpui::test]
905 async fn test_eslint_10_treats_typescript_config_as_flat_config(cx: &mut TestAppContext) {
906 let fs = FakeFs::new(cx.executor());
907 fs.insert_tree(
908 unix_path_to_platform("/workspace"),
909 json!({ "eslint.config.ts": "" }),
910 )
911 .await;
912 let worktree_root = PathBuf::from(unix_path_to_platform("/workspace"));
913 let requested_file = PathBuf::from(unix_path_to_platform("/workspace/src/index.js"));
914 let version = Version::parse("10.0.0").expect("valid ESLint version");
915
916 let config_kind = find_eslint_config_kind(
917 &worktree_root,
918 Some(&requested_file),
919 Some(&version),
920 fs.as_ref(),
921 )
922 .await;
923
924 assert_eq!(config_kind, Some(EslintConfigKind::Flat));
925 }
926
927 #[gpui::test]
928 async fn test_package_local_flat_config_is_preferred_for_monorepo_file(
929 cx: &mut TestAppContext,
930 ) {
931 let fs = FakeFs::new(cx.executor());
932 fs.insert_tree(
933 unix_path_to_platform("/workspace"),
934 json!({
935 "eslint.config.js": "",
936 "packages": {
937 "web": {
938 "eslint.config.js": ""
939 }
940 }
941 }),
942 )
943 .await;
944 let worktree_root = PathBuf::from(unix_path_to_platform("/workspace"));
945 let requested_file = PathBuf::from(unix_path_to_platform(
946 "/workspace/packages/web/src/index.js",
947 ));
948 let version = Version::parse("8.56.0").expect("valid ESLint version");
949
950 let config_kind = find_eslint_config_kind(
951 &worktree_root,
952 Some(&requested_file),
953 Some(&version),
954 fs.as_ref(),
955 )
956 .await;
957
958 assert_eq!(config_kind, Some(EslintConfigKind::Flat));
959 }
960
961 #[gpui::test]
962 async fn test_package_local_legacy_config_is_detected_for_eslint_9(
963 cx: &mut TestAppContext,
964 ) {
965 let fs = FakeFs::new(cx.executor());
966 fs.insert_tree(
967 unix_path_to_platform("/workspace"),
968 json!({
969 "packages": {
970 "web": {
971 ".eslintrc.cjs": ""
972 }
973 }
974 }),
975 )
976 .await;
977 let worktree_root = PathBuf::from(unix_path_to_platform("/workspace"));
978 let requested_file = PathBuf::from(unix_path_to_platform(
979 "/workspace/packages/web/src/index.js",
980 ));
981 let version = Version::parse("9.0.0").expect("valid ESLint version");
982
983 let config_kind = find_eslint_config_kind(
984 &worktree_root,
985 Some(&requested_file),
986 Some(&version),
987 fs.as_ref(),
988 )
989 .await;
990
991 assert_eq!(config_kind, Some(EslintConfigKind::Legacy));
992 }
993 }
994
995 #[cfg(windows)]
996 mod windows_style_paths {
997 use super::*;
998
999 #[test]
1000 fn test_working_directory_string() {
1001 let uri = make_uri("/workspace/packages/foo/src/file.ts");
1002 let working_directories = vec![WorkingDirectory::String("packages\\foo".to_string())];
1003 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
1004
1005 let result = determine_working_directory(uri, working_directories, workspace_folder);
1006 assert_directory_result(
1007 result,
1008 &unix_path_to_platform("/workspace/packages/foo/"),
1009 false,
1010 );
1011 }
1012
1013 #[test]
1014 fn test_working_directory_absolute_path() {
1015 let uri = make_uri("/workspace/packages/foo/src/file.ts");
1016 let working_directories = vec![WorkingDirectory::String(
1017 unix_path_to_platform("/workspace/packages/foo").replace('/', "\\"),
1018 )];
1019 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
1020
1021 let result = determine_working_directory(uri, working_directories, workspace_folder);
1022 assert_directory_result(
1023 result,
1024 &unix_path_to_platform("/workspace/packages/foo/"),
1025 false,
1026 );
1027 }
1028
1029 #[test]
1030 fn test_working_directory_directory_item() {
1031 let uri = make_uri("/workspace/packages/foo/src/file.ts");
1032 let working_directories = vec![WorkingDirectory::DirectoryItem(DirectoryItem {
1033 directory: "packages\\foo".to_string(),
1034 not_cwd: Some(true),
1035 })];
1036 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
1037
1038 let result = determine_working_directory(uri, working_directories, workspace_folder);
1039 assert_directory_result(
1040 result,
1041 &unix_path_to_platform("/workspace/packages/foo/"),
1042 true,
1043 );
1044 }
1045
1046 #[test]
1047 fn test_working_directory_legacy_item() {
1048 let uri = make_uri("/workspace/packages/foo/src/file.ts");
1049 let working_directories =
1050 vec![WorkingDirectory::LegacyDirectoryItem(LegacyDirectoryItem {
1051 directory: "packages\\foo".to_string(),
1052 change_process_cwd: false,
1053 })];
1054 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
1055
1056 let result = determine_working_directory(uri, working_directories, workspace_folder);
1057 assert_directory_result(
1058 result,
1059 &unix_path_to_platform("/workspace/packages/foo/"),
1060 true,
1061 );
1062 }
1063
1064 #[test]
1065 fn test_working_directory_pattern_item() {
1066 let uri = make_uri("/workspace/packages/foo/src/file.ts");
1067 let working_directories = vec![WorkingDirectory::PatternItem(PatternItem {
1068 pattern: "packages\\*\\".to_string(),
1069 not_cwd: Some(false),
1070 })];
1071 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
1072
1073 let result = determine_working_directory(uri, working_directories, workspace_folder);
1074 assert_directory_result(
1075 result,
1076 &unix_path_to_platform("/workspace/packages/foo/"),
1077 false,
1078 );
1079 }
1080
1081 #[test]
1082 fn test_working_directory_multiple_patterns() {
1083 let uri = make_uri("/workspace/apps/web/src/file.ts");
1084 let working_directories = vec![
1085 WorkingDirectory::PatternItem(PatternItem {
1086 pattern: "packages\\*\\".to_string(),
1087 not_cwd: None,
1088 }),
1089 WorkingDirectory::PatternItem(PatternItem {
1090 pattern: "apps\\*\\".to_string(),
1091 not_cwd: None,
1092 }),
1093 ];
1094 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
1095
1096 let result = determine_working_directory(uri, working_directories, workspace_folder);
1097 assert_directory_result(
1098 result,
1099 &unix_path_to_platform("/workspace/apps/web/"),
1100 false,
1101 );
1102 }
1103 }
1104
1105 /// Converts a Unix-style path to a platform-specific path.
1106 /// On Windows, converts "/workspace/foo/bar" to "C:\workspace\foo\bar"
1107 /// On Unix, returns the path unchanged.
1108 fn unix_path_to_platform(path: &str) -> String {
1109 #[cfg(windows)]
1110 {
1111 if path.starts_with('/') {
1112 format!("C:{}", path.replace('/', "\\"))
1113 } else {
1114 path.replace('/', "\\")
1115 }
1116 }
1117 #[cfg(not(windows))]
1118 {
1119 path.to_string()
1120 }
1121 }
1122
1123 fn make_uri(path: &str) -> Uri {
1124 let platform_path = unix_path_to_platform(path);
1125 Uri::from_file_path(&platform_path).unwrap()
1126 }
1127
1128 fn assert_directory_result(
1129 result: Option<ResultWorkingDirectory>,
1130 expected_directory: &str,
1131 expected_not_cwd: bool,
1132 ) {
1133 match result {
1134 Some(ResultWorkingDirectory::DirectoryItem(item)) => {
1135 assert_eq!(item.directory, expected_directory);
1136 assert_eq!(item.not_cwd, Some(expected_not_cwd));
1137 }
1138 other => panic!("Expected DirectoryItem, got {:?}", other),
1139 }
1140 }
1141}