Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:36:33.623Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

eslint.rs

1148 lines · 40.5 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.