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