Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:32:56.729Z 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

typescript.rs

1887 lines · 65.9 KB · rust
1use anyhow::{Context as _, Result};
2use async_trait::async_trait;
3use chrono::{DateTime, Local};
4use collections::HashMap;
5use futures::future::join_all;
6use gpui::{App, AppContext, AsyncApp, Entity, Task};
7use itertools::Itertools as _;
8use language::{
9    Buffer, ContextLocation, ContextProvider, File, LanguageName, LanguageToolchainStore,
10    LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain,
11};
12use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName, Uri};
13use node_runtime::{NodeRuntime, VersionStrategy};
14use project::{Fs, lsp_store::language_server_settings};
15use semver::{Version, VersionReq};
16use serde_json::{Value, json};
17use smol::lock::RwLock;
18use std::{
19    borrow::Cow,
20    ffi::OsString,
21    future::Future,
22    path::{Path, PathBuf},
23    sync::{Arc, LazyLock},
24};
25use task::{TaskTemplate, TaskTemplates, VariableName};
26use util::rel_path::RelPath;
27use util::{ResultExt, maybe};
28
29use crate::{PackageJson, PackageJsonData};
30
31pub(crate) struct TypeScriptContextProvider {
32    fs: Arc<dyn Fs>,
33    last_package_json: PackageJsonContents,
34}
35
36const TYPESCRIPT_RUNNER_VARIABLE: VariableName =
37    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_RUNNER"));
38
39const TYPESCRIPT_JEST_TEST_NAME_VARIABLE: VariableName =
40    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST_TEST_NAME"));
41
42const TYPESCRIPT_VITEST_TEST_NAME_VARIABLE: VariableName =
43    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST_TEST_NAME"));
44
45const TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE: VariableName =
46    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST_PACKAGE_PATH"));
47
48const TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE: VariableName =
49    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_MOCHA_PACKAGE_PATH"));
50
51const TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE: VariableName =
52    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST_PACKAGE_PATH"));
53
54const TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE: VariableName =
55    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JASMINE_PACKAGE_PATH"));
56
57const TYPESCRIPT_BUN_PACKAGE_PATH_VARIABLE: VariableName =
58    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_BUN_PACKAGE_PATH"));
59
60const TYPESCRIPT_BUN_TEST_NAME_VARIABLE: VariableName =
61    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_BUN_TEST_NAME"));
62
63const TYPESCRIPT_NODE_PACKAGE_PATH_VARIABLE: VariableName =
64    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_NODE_PACKAGE_PATH"));
65
66#[derive(Clone, Debug, Default)]
67struct PackageJsonContents(Arc<RwLock<HashMap<PathBuf, PackageJson>>>);
68
69impl PackageJsonData {
70    fn fill_task_templates(&self, task_templates: &mut TaskTemplates) {
71        if self.jest_package_path.is_some() {
72            task_templates.0.push(TaskTemplate {
73                label: "jest file test".to_owned(),
74                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
75                args: vec![
76                    "exec".to_owned(),
77                    "--".to_owned(),
78                    "jest".to_owned(),
79                    "--runInBand".to_owned(),
80                    VariableName::File.template_value(),
81                ],
82                cwd: Some(TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE.template_value()),
83                ..TaskTemplate::default()
84            });
85            task_templates.0.push(TaskTemplate {
86                label: format!("jest test {}", VariableName::Symbol.template_value()),
87                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
88                args: vec![
89                    "exec".to_owned(),
90                    "--".to_owned(),
91                    "jest".to_owned(),
92                    "--runInBand".to_owned(),
93                    "--testNamePattern".to_owned(),
94                    format!(
95                        "\"{}\"",
96                        TYPESCRIPT_JEST_TEST_NAME_VARIABLE.template_value()
97                    ),
98                    VariableName::File.template_value(),
99                ],
100                tags: vec![
101                    "ts-test".to_owned(),
102                    "js-test".to_owned(),
103                    "tsx-test".to_owned(),
104                ],
105                cwd: Some(TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE.template_value()),
106                ..TaskTemplate::default()
107            });
108        }
109
110        if self.vitest_package_path.is_some() {
111            task_templates.0.push(TaskTemplate {
112                label: format!("{} file test", "vitest".to_owned()),
113                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
114                args: vec![
115                    "exec".to_owned(),
116                    "--".to_owned(),
117                    "vitest".to_owned(),
118                    "run".to_owned(),
119                    "--no-file-parallelism".to_owned(),
120                    VariableName::File.template_value(),
121                ],
122                cwd: Some(TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE.template_value()),
123                ..TaskTemplate::default()
124            });
125            task_templates.0.push(TaskTemplate {
126                label: format!(
127                    "{} test {}",
128                    "vitest".to_owned(),
129                    VariableName::Symbol.template_value(),
130                ),
131                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
132                args: vec![
133                    "exec".to_owned(),
134                    "--".to_owned(),
135                    "vitest".to_owned(),
136                    "run".to_owned(),
137                    "--no-file-parallelism".to_owned(),
138                    "--testNamePattern".to_owned(),
139                    format!(
140                        "\"{}\"",
141                        TYPESCRIPT_VITEST_TEST_NAME_VARIABLE.template_value()
142                    ),
143                    VariableName::File.template_value(),
144                ],
145                tags: vec![
146                    "ts-test".to_owned(),
147                    "js-test".to_owned(),
148                    "tsx-test".to_owned(),
149                ],
150                cwd: Some(TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE.template_value()),
151                ..TaskTemplate::default()
152            });
153        }
154
155        if self.mocha_package_path.is_some() {
156            task_templates.0.push(TaskTemplate {
157                label: format!("{} file test", "mocha".to_owned()),
158                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
159                args: vec![
160                    "exec".to_owned(),
161                    "--".to_owned(),
162                    "mocha".to_owned(),
163                    VariableName::File.template_value(),
164                ],
165                cwd: Some(TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE.template_value()),
166                ..TaskTemplate::default()
167            });
168            task_templates.0.push(TaskTemplate {
169                label: format!(
170                    "{} test {}",
171                    "mocha".to_owned(),
172                    VariableName::Symbol.template_value(),
173                ),
174                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
175                args: vec![
176                    "exec".to_owned(),
177                    "--".to_owned(),
178                    "mocha".to_owned(),
179                    "--grep".to_owned(),
180                    format!("\"{}\"", VariableName::Symbol.template_value()),
181                    VariableName::File.template_value(),
182                ],
183                tags: vec![
184                    "ts-test".to_owned(),
185                    "js-test".to_owned(),
186                    "tsx-test".to_owned(),
187                ],
188                cwd: Some(TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE.template_value()),
189                ..TaskTemplate::default()
190            });
191        }
192
193        if self.jasmine_package_path.is_some() {
194            task_templates.0.push(TaskTemplate {
195                label: format!("{} file test", "jasmine".to_owned()),
196                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
197                args: vec![
198                    "exec".to_owned(),
199                    "--".to_owned(),
200                    "jasmine".to_owned(),
201                    VariableName::File.template_value(),
202                ],
203                cwd: Some(TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE.template_value()),
204                ..TaskTemplate::default()
205            });
206            task_templates.0.push(TaskTemplate {
207                label: format!(
208                    "{} test {}",
209                    "jasmine".to_owned(),
210                    VariableName::Symbol.template_value(),
211                ),
212                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
213                args: vec![
214                    "exec".to_owned(),
215                    "--".to_owned(),
216                    "jasmine".to_owned(),
217                    format!("--filter={}", VariableName::Symbol.template_value()),
218                    VariableName::File.template_value(),
219                ],
220                tags: vec![
221                    "ts-test".to_owned(),
222                    "js-test".to_owned(),
223                    "tsx-test".to_owned(),
224                ],
225                cwd: Some(TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE.template_value()),
226                ..TaskTemplate::default()
227            });
228        }
229
230        if self.bun_package_path.is_some() {
231            task_templates.0.push(TaskTemplate {
232                label: format!("{} file test", "bun test".to_owned()),
233                command: "bun".to_owned(),
234                args: vec!["test".to_owned(), VariableName::File.template_value()],
235                cwd: Some(TYPESCRIPT_BUN_PACKAGE_PATH_VARIABLE.template_value()),
236                ..TaskTemplate::default()
237            });
238            task_templates.0.push(TaskTemplate {
239                label: format!("bun test {}", VariableName::Symbol.template_value(),),
240                command: "bun".to_owned(),
241                args: vec![
242                    "test".to_owned(),
243                    "--test-name-pattern".to_owned(),
244                    format!("\"{}\"", TYPESCRIPT_BUN_TEST_NAME_VARIABLE.template_value()),
245                    VariableName::File.template_value(),
246                ],
247                tags: vec![
248                    "ts-test".to_owned(),
249                    "js-test".to_owned(),
250                    "tsx-test".to_owned(),
251                ],
252                cwd: Some(TYPESCRIPT_BUN_PACKAGE_PATH_VARIABLE.template_value()),
253                ..TaskTemplate::default()
254            });
255        }
256
257        if self.node_package_path.is_some() {
258            task_templates.0.push(TaskTemplate {
259                label: format!("{} file test", "node test".to_owned()),
260                command: "node".to_owned(),
261                args: vec!["--test".to_owned(), VariableName::File.template_value()],
262                tags: vec![
263                    "ts-test".to_owned(),
264                    "js-test".to_owned(),
265                    "tsx-test".to_owned(),
266                ],
267                cwd: Some(TYPESCRIPT_NODE_PACKAGE_PATH_VARIABLE.template_value()),
268                ..TaskTemplate::default()
269            });
270            task_templates.0.push(TaskTemplate {
271                label: format!("node test {}", VariableName::Symbol.template_value()),
272                command: "node".to_owned(),
273                args: vec![
274                    "--test".to_owned(),
275                    "--test-name-pattern".to_owned(),
276                    format!("\"{}\"", VariableName::Symbol.template_value()),
277                    VariableName::File.template_value(),
278                ],
279                tags: vec![
280                    "ts-test".to_owned(),
281                    "js-test".to_owned(),
282                    "tsx-test".to_owned(),
283                ],
284                cwd: Some(TYPESCRIPT_NODE_PACKAGE_PATH_VARIABLE.template_value()),
285                ..TaskTemplate::default()
286            });
287        }
288
289        let script_name_counts: HashMap<_, usize> =
290            self.scripts
291                .iter()
292                .fold(HashMap::default(), |mut acc, (_, script)| {
293                    *acc.entry(script).or_default() += 1;
294                    acc
295                });
296        for (path, script) in &self.scripts {
297            let label = if script_name_counts.get(script).copied().unwrap_or_default() > 1
298                && let Some(parent) = path.parent().and_then(|parent| parent.file_name())
299            {
300                let parent = parent.to_string_lossy();
301                format!("{parent}/package.json > {script}")
302            } else {
303                format!("package.json > {script}")
304            };
305            task_templates.0.push(TaskTemplate {
306                label,
307                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
308                args: vec!["run".to_owned(), script.to_owned()],
309                tags: vec!["package-script".into()],
310                cwd: Some(
311                    path.parent()
312                        .unwrap_or(Path::new("/"))
313                        .to_string_lossy()
314                        .to_string(),
315                ),
316                ..TaskTemplate::default()
317            });
318        }
319    }
320}
321
322impl TypeScriptContextProvider {
323    pub fn new(fs: Arc<dyn Fs>) -> Self {
324        Self {
325            fs,
326            last_package_json: PackageJsonContents::default(),
327        }
328    }
329
330    fn combined_package_json_data(
331        &self,
332        fs: Arc<dyn Fs>,
333        worktree_root: &Path,
334        file_relative_path: &RelPath,
335        cx: &App,
336    ) -> Task<anyhow::Result<PackageJsonData>> {
337        let new_json_data = file_relative_path
338            .ancestors()
339            .map(|path| worktree_root.join(path.as_std_path()))
340            .map(|parent_path| {
341                self.package_json_data(&parent_path, self.last_package_json.clone(), fs.clone(), cx)
342            })
343            .collect::<Vec<_>>();
344
345        cx.background_spawn(async move {
346            let mut package_json_data = PackageJsonData::default();
347            for new_data in join_all(new_json_data).await.into_iter().flatten() {
348                package_json_data.merge(new_data);
349            }
350            Ok(package_json_data)
351        })
352    }
353
354    fn package_json_data(
355        &self,
356        directory_path: &Path,
357        existing_package_json: PackageJsonContents,
358        fs: Arc<dyn Fs>,
359        cx: &App,
360    ) -> Task<anyhow::Result<PackageJsonData>> {
361        let package_json_path = directory_path.join("package.json");
362        let metadata_check_fs = fs.clone();
363        cx.background_spawn(async move {
364            let metadata = metadata_check_fs
365                .metadata(&package_json_path)
366                .await
367                .with_context(|| format!("getting metadata for {package_json_path:?}"))?
368                .with_context(|| format!("missing FS metadata for {package_json_path:?}"))?;
369            let mtime = DateTime::<Local>::from(metadata.mtime.timestamp_for_user());
370            let existing_data = {
371                let contents = existing_package_json.0.read().await;
372                contents
373                    .get(&package_json_path)
374                    .filter(|package_json| package_json.mtime == mtime)
375                    .map(|package_json| package_json.data.clone())
376            };
377            match existing_data {
378                Some(existing_data) => Ok(existing_data),
379                None => {
380                    let package_json_string =
381                        fs.load(&package_json_path).await.with_context(|| {
382                            format!("loading package.json from {package_json_path:?}")
383                        })?;
384                    let package_json: HashMap<String, serde_json_lenient::Value> =
385                        serde_json_lenient::from_str(&package_json_string).with_context(|| {
386                            format!("parsing package.json from {package_json_path:?}")
387                        })?;
388                    let new_data =
389                        PackageJsonData::new(package_json_path.as_path().into(), package_json);
390                    {
391                        let mut contents = existing_package_json.0.write().await;
392                        contents.insert(
393                            package_json_path,
394                            PackageJson {
395                                mtime,
396                                data: new_data.clone(),
397                            },
398                        );
399                    }
400                    Ok(new_data)
401                }
402            }
403        })
404    }
405}
406
407async fn detect_package_manager(
408    worktree_root: PathBuf,
409    fs: Arc<dyn Fs>,
410    package_json_data: Option<PackageJsonData>,
411) -> &'static str {
412    if let Some(package_json_data) = package_json_data
413        && let Some(package_manager) = package_json_data.package_manager
414    {
415        return package_manager;
416    }
417    if fs.is_file(&worktree_root.join("pnpm-lock.yaml")).await {
418        return "pnpm";
419    }
420    if fs.is_file(&worktree_root.join("yarn.lock")).await {
421        return "yarn";
422    }
423    "npm"
424}
425
426impl ContextProvider for TypeScriptContextProvider {
427    fn associated_tasks(
428        &self,
429        buffer: Option<Entity<Buffer>>,
430        cx: &App,
431    ) -> Task<Option<TaskTemplates>> {
432        let file = buffer.and_then(|buffer| buffer.read(cx).file());
433        let Some(file) = project::File::from_dyn(file).cloned() else {
434            return Task::ready(None);
435        };
436        let Some(worktree_root) = file.worktree.read(cx).root_dir() else {
437            return Task::ready(None);
438        };
439        let file_relative_path = file.path().clone();
440        let package_json_data = self.combined_package_json_data(
441            self.fs.clone(),
442            &worktree_root,
443            &file_relative_path,
444            cx,
445        );
446
447        cx.background_spawn(async move {
448            let mut task_templates = TaskTemplates(Vec::new());
449            task_templates.0.push(TaskTemplate {
450                label: format!(
451                    "execute selection {}",
452                    VariableName::SelectedText.template_value()
453                ),
454                command: "node".to_owned(),
455                args: vec![
456                    "-e".to_owned(),
457                    format!("\"{}\"", VariableName::SelectedText.template_value()),
458                ],
459                ..TaskTemplate::default()
460            });
461
462            match package_json_data.await {
463                Ok(package_json) => {
464                    package_json.fill_task_templates(&mut task_templates);
465                }
466                Err(e) => {
467                    log::error!(
468                        "Failed to read package.json for worktree {file_relative_path:?}: {e:#}"
469                    );
470                }
471            }
472
473            Some(task_templates)
474        })
475    }
476
477    fn build_context(
478        &self,
479        current_vars: &task::TaskVariables,
480        location: ContextLocation<'_>,
481        _project_env: Option<HashMap<String, String>>,
482        _toolchains: Arc<dyn LanguageToolchainStore>,
483        cx: &mut App,
484    ) -> Task<Result<task::TaskVariables>> {
485        let mut vars = task::TaskVariables::default();
486
487        if let Some(symbol) = current_vars.get(&VariableName::Symbol) {
488            vars.insert(
489                TYPESCRIPT_JEST_TEST_NAME_VARIABLE,
490                replace_test_name_parameters(symbol),
491            );
492            vars.insert(
493                TYPESCRIPT_VITEST_TEST_NAME_VARIABLE,
494                replace_test_name_parameters(symbol),
495            );
496            vars.insert(
497                TYPESCRIPT_BUN_TEST_NAME_VARIABLE,
498                replace_test_name_parameters(symbol),
499            );
500        }
501        let file_path = location
502            .file_location
503            .buffer
504            .read(cx)
505            .file()
506            .map(|file| file.path());
507
508        let args = location.worktree_root.zip(location.fs).zip(file_path).map(
509            |((worktree_root, fs), file_path)| {
510                (
511                    self.combined_package_json_data(fs.clone(), &worktree_root, file_path, cx),
512                    worktree_root,
513                    fs,
514                )
515            },
516        );
517        cx.background_spawn(async move {
518            if let Some((task, worktree_root, fs)) = args {
519                let package_json_data = task.await.log_err();
520                vars.insert(
521                    TYPESCRIPT_RUNNER_VARIABLE,
522                    detect_package_manager(worktree_root, fs, package_json_data.clone())
523                        .await
524                        .to_owned(),
525                );
526
527                if let Some(package_json_data) = package_json_data {
528                    if let Some(path) = package_json_data.jest_package_path {
529                        vars.insert(
530                            TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE,
531                            path.parent()
532                                .unwrap_or(Path::new(""))
533                                .to_string_lossy()
534                                .to_string(),
535                        );
536                    }
537
538                    if let Some(path) = package_json_data.mocha_package_path {
539                        vars.insert(
540                            TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE,
541                            path.parent()
542                                .unwrap_or(Path::new(""))
543                                .to_string_lossy()
544                                .to_string(),
545                        );
546                    }
547
548                    if let Some(path) = package_json_data.vitest_package_path {
549                        vars.insert(
550                            TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE,
551                            path.parent()
552                                .unwrap_or(Path::new(""))
553                                .to_string_lossy()
554                                .to_string(),
555                        );
556                    }
557
558                    if let Some(path) = package_json_data.jasmine_package_path {
559                        vars.insert(
560                            TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE,
561                            path.parent()
562                                .unwrap_or(Path::new(""))
563                                .to_string_lossy()
564                                .to_string(),
565                        );
566                    }
567
568                    if let Some(path) = package_json_data.bun_package_path {
569                        vars.insert(
570                            TYPESCRIPT_BUN_PACKAGE_PATH_VARIABLE,
571                            path.parent()
572                                .unwrap_or(Path::new(""))
573                                .to_string_lossy()
574                                .to_string(),
575                        );
576                    }
577
578                    if let Some(path) = package_json_data.node_package_path {
579                        vars.insert(
580                            TYPESCRIPT_NODE_PACKAGE_PATH_VARIABLE,
581                            path.parent()
582                                .unwrap_or(Path::new(""))
583                                .to_string_lossy()
584                                .to_string(),
585                        );
586                    }
587                }
588            }
589            Ok(vars)
590        })
591    }
592}
593
594fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
595    vec![server_path.into(), "--stdio".into()]
596}
597
598fn replace_test_name_parameters(test_name: &str) -> String {
599    static PATTERN: LazyLock<regex::Regex> =
600        LazyLock::new(|| regex::Regex::new(r"(\$([A-Za-z0-9_\.]+|[\#])|%[psdifjo#\$%])").unwrap());
601    PATTERN.split(test_name).map(regex::escape).join("(.+?)")
602}
603
604static TYPESCRIPT_VERSION_REQ: LazyLock<VersionReq> =
605    LazyLock::new(|| VersionReq::parse("^6").expect("Failed to parse TypeScript version req"));
606
607pub struct TypeScriptLspAdapter {
608    fs: Arc<dyn Fs>,
609    node: NodeRuntime,
610}
611
612impl TypeScriptLspAdapter {
613    const OLD_SERVER_PATH: &str = "node_modules/typescript-language-server/lib/cli.js";
614    const NEW_SERVER_PATH: &str = "node_modules/typescript-language-server/lib/cli.mjs";
615
616    const PACKAGE_NAME: &str = "typescript";
617    const SERVER_PACKAGE_NAME: &str = "typescript-language-server";
618
619    const SERVER_NAME: LanguageServerName =
620        LanguageServerName::new_static(Self::SERVER_PACKAGE_NAME);
621
622    pub fn new(node: NodeRuntime, fs: Arc<dyn Fs>) -> Self {
623        TypeScriptLspAdapter { fs, node }
624    }
625
626    async fn tsdk_path(&self, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {
627        let is_yarn = adapter
628            .read_text_file(
629                RelPath::from_unix_str(".yarn/sdks/typescript/lib/typescript.js").unwrap(),
630            )
631            .await
632            .is_ok();
633
634        let tsdk_path = if is_yarn {
635            ".yarn/sdks/typescript/lib"
636        } else {
637            "node_modules/typescript/lib"
638        };
639
640        // typescript-language-server doesn't support TypeScript 7+, which no longer
641        // ships `tsserver.js`.
642        if self
643            .fs
644            .is_file(
645                &adapter
646                    .worktree_root_path()
647                    .join(tsdk_path)
648                    .join("tsserver.js"),
649            )
650            .await
651        {
652            Some(tsdk_path)
653        } else {
654            None
655        }
656    }
657}
658
659pub struct TypeScriptVersions {
660    typescript_version: Version,
661    server_version: Version,
662}
663
664impl LspInstaller for TypeScriptLspAdapter {
665    type BinaryVersion = TypeScriptVersions;
666
667    async fn fetch_latest_server_version(
668        &self,
669        _: &Arc<dyn LspAdapterDelegate>,
670        _: bool,
671        _: &mut AsyncApp,
672    ) -> Result<Self::BinaryVersion> {
673        Ok(TypeScriptVersions {
674            typescript_version: self
675                .node
676                .npm_package_latest_version_with_requirement(
677                    Self::PACKAGE_NAME,
678                    Some(&TYPESCRIPT_VERSION_REQ),
679                )
680                .await?,
681            server_version: self
682                .node
683                .npm_package_latest_version(Self::SERVER_PACKAGE_NAME)
684                .await?,
685        })
686    }
687
688    fn check_if_version_installed(
689        &self,
690        version: &Self::BinaryVersion,
691        container_dir: &PathBuf,
692        _: &Arc<dyn LspAdapterDelegate>,
693    ) -> impl Send + Future<Output = Option<LanguageServerBinary>> + use<> {
694        let node = self.node.clone();
695        let typescript_version = version.typescript_version.clone();
696        let server_version = version.server_version.clone();
697        let container_dir = container_dir.clone();
698
699        async move {
700            let server_path = container_dir.join(Self::NEW_SERVER_PATH);
701
702            // Pin rather than Latest so an unusable TypeScript 7.x install gets downgraded.
703            if node
704                .should_install_npm_package(
705                    Self::PACKAGE_NAME,
706                    &server_path,
707                    &container_dir,
708                    VersionStrategy::Pin(&typescript_version),
709                )
710                .await
711            {
712                return None;
713            }
714
715            if node
716                .should_install_npm_package(
717                    Self::SERVER_PACKAGE_NAME,
718                    &server_path,
719                    &container_dir,
720                    VersionStrategy::Latest(&server_version),
721                )
722                .await
723            {
724                return None;
725            }
726
727            Some(LanguageServerBinary {
728                path: node.binary_path().await.ok()?,
729                env: None,
730                arguments: typescript_server_binary_arguments(&server_path),
731            })
732        }
733    }
734
735    fn fetch_server_binary(
736        &self,
737        latest_version: Self::BinaryVersion,
738        container_dir: PathBuf,
739        _: &Arc<dyn LspAdapterDelegate>,
740    ) -> impl Send + Future<Output = Result<LanguageServerBinary>> + use<> {
741        let node = self.node.clone();
742
743        async move {
744            let server_path = container_dir.join(Self::NEW_SERVER_PATH);
745            let typescript_version = latest_version.typescript_version.to_string();
746
747            node.npm_install_packages(
748                &container_dir,
749                &[
750                    (Self::PACKAGE_NAME, typescript_version.as_str()),
751                    (Self::SERVER_PACKAGE_NAME, "latest"),
752                ],
753            )
754            .await?;
755
756            Ok(LanguageServerBinary {
757                path: node.binary_path().await?,
758                env: None,
759                arguments: typescript_server_binary_arguments(&server_path),
760            })
761        }
762    }
763
764    async fn cached_server_binary(
765        &self,
766        container_dir: PathBuf,
767        _: &dyn LspAdapterDelegate,
768    ) -> Option<LanguageServerBinary> {
769        get_cached_ts_server_binary(container_dir, &self.node).await
770    }
771}
772
773#[async_trait(?Send)]
774impl LspAdapter for TypeScriptLspAdapter {
775    fn name(&self) -> LanguageServerName {
776        Self::SERVER_NAME
777    }
778
779    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
780        Some(vec![
781            CodeActionKind::QUICKFIX,
782            CodeActionKind::REFACTOR,
783            CodeActionKind::REFACTOR_EXTRACT,
784            CodeActionKind::SOURCE,
785        ])
786    }
787
788    async fn label_for_completion(
789        &self,
790        item: &lsp::CompletionItem,
791        language: &Arc<language::Language>,
792    ) -> Option<language::CodeLabel> {
793        use lsp::CompletionItemKind as Kind;
794        let label_len = item.label.len();
795        let grammar = language.grammar()?;
796        let highlight_id = match item.kind? {
797            Kind::CLASS | Kind::INTERFACE | Kind::ENUM => grammar.highlight_id_for_name("type"),
798            Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
799            Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
800            Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
801            Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
802            Kind::VARIABLE => grammar.highlight_id_for_name("variable"),
803            _ => None,
804        }?;
805
806        let text = if let Some(description) = item
807            .label_details
808            .as_ref()
809            .and_then(|label_details| label_details.description.as_ref())
810        {
811            format!("{} {}", item.label, description)
812        } else if let Some(detail) = &item.detail {
813            format!("{} {}", item.label, detail)
814        } else {
815            item.label.clone()
816        };
817        Some(language::CodeLabel::filtered(
818            text,
819            label_len,
820            item.filter_text.as_deref(),
821            vec![(0..label_len, highlight_id)],
822        ))
823    }
824
825    async fn initialization_options(
826        self: Arc<Self>,
827        adapter: &Arc<dyn LspAdapterDelegate>,
828        _: &mut AsyncApp,
829    ) -> Result<Option<serde_json::Value>> {
830        let tsdk_path = self.tsdk_path(adapter).await;
831        Ok(Some(json!({
832            "provideFormatter": true,
833            "hostInfo": "zed",
834            "tsserver": {
835                "path": tsdk_path,
836            },
837            "preferences": {
838                "includeInlayParameterNameHints": "all",
839                "includeInlayParameterNameHintsWhenArgumentMatchesName": true,
840                "includeInlayFunctionParameterTypeHints": true,
841                "includeInlayVariableTypeHints": true,
842                "includeInlayVariableTypeHintsWhenTypeMatchesName": true,
843                "includeInlayPropertyDeclarationTypeHints": true,
844                "includeInlayFunctionLikeReturnTypeHints": true,
845                "includeInlayEnumMemberValueHints": true,
846            }
847        })))
848    }
849
850    async fn workspace_configuration(
851        self: Arc<Self>,
852        delegate: &Arc<dyn LspAdapterDelegate>,
853        _: Option<Toolchain>,
854        _: Option<Uri>,
855        cx: &mut AsyncApp,
856    ) -> Result<Value> {
857        let override_options = cx.update(|cx| {
858            language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
859                .and_then(|s| s.settings.clone())
860        });
861        if let Some(options) = override_options {
862            return Ok(options);
863        }
864        Ok(json!({
865            "completions": {
866              "completeFunctionCalls": true
867            }
868        }))
869    }
870
871    fn language_ids(&self) -> HashMap<LanguageName, String> {
872        HashMap::from_iter([
873            (LanguageName::new_static("TypeScript"), "typescript".into()),
874            (LanguageName::new_static("JavaScript"), "javascript".into()),
875            (LanguageName::new_static("TSX"), "typescriptreact".into()),
876        ])
877    }
878}
879
880async fn get_cached_ts_server_binary(
881    container_dir: PathBuf,
882    node: &NodeRuntime,
883) -> Option<LanguageServerBinary> {
884    maybe!(async {
885        let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH);
886        let new_server_path = container_dir.join(TypeScriptLspAdapter::NEW_SERVER_PATH);
887        if new_server_path.exists() {
888            Ok(LanguageServerBinary {
889                path: node.binary_path().await?,
890                env: None,
891                arguments: typescript_server_binary_arguments(&new_server_path),
892            })
893        } else if old_server_path.exists() {
894            Ok(LanguageServerBinary {
895                path: node.binary_path().await?,
896                env: None,
897                arguments: typescript_server_binary_arguments(&old_server_path),
898            })
899        } else {
900            anyhow::bail!("missing executable in directory {container_dir:?}")
901        }
902    })
903    .await
904    .log_err()
905}
906
907#[cfg(test)]
908mod tests {
909    use std::path::Path;
910
911    use gpui::{AppContext as _, BackgroundExecutor, TestAppContext};
912    use project::FakeFs;
913    use serde_json::json;
914    use task::TaskTemplates;
915    use unindent::Unindent;
916    use util::{path, rel_path::rel_path};
917
918    use crate::typescript::{
919        PackageJsonData, TypeScriptContextProvider, replace_test_name_parameters,
920    };
921
922    #[gpui::test]
923    async fn test_outline(cx: &mut TestAppContext) {
924        for language in [
925            crate::language(
926                "typescript",
927                tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
928            ),
929            crate::language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
930        ] {
931            let text = r#"
932            function a() {
933              // local variables are included
934              let a1 = 1;
935              // all functions are included
936              async function a2() {}
937            }
938            // top-level variables are included
939            let b: C
940            function getB() {}
941            // exported variables are included
942            export const d = e;
943        "#
944            .unindent();
945
946            let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
947            let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
948            assert_eq!(
949                outline
950                    .items
951                    .iter()
952                    .map(|item| (item.text.as_str(), item.depth))
953                    .collect::<Vec<_>>(),
954                &[
955                    ("function a()", 0),
956                    ("let a1", 1),
957                    ("async function a2()", 1),
958                    ("let b", 0),
959                    ("function getB()", 0),
960                    ("const d", 0),
961                ]
962            );
963        }
964    }
965
966    #[gpui::test]
967    async fn test_outline_with_destructuring(cx: &mut TestAppContext) {
968        for language in [
969            crate::language(
970                "typescript",
971                tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
972            ),
973            crate::language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
974        ] {
975            let text = r#"
976            // Top-level destructuring
977            const { a1, a2 } = a;
978            const [b1, b2] = b;
979
980            // Defaults and rest
981            const [c1 = 1, , c2, ...rest1] = c;
982            const { d1, d2: e1, f1 = 2, g1: h1 = 3, ...rest2 } = d;
983
984            function processData() {
985              // Nested object destructuring
986              const { c1, c2 } = c;
987              // Nested array destructuring
988              const [d1, d2, d3] = d;
989              // Destructuring with renaming
990              const { f1: g1 } = f;
991              // With defaults
992              const [x = 10, y] = xy;
993            }
994
995            class DataHandler {
996              method() {
997                // Destructuring in class method
998                const { a1, a2 } = a;
999                const [b1, ...b2] = b;
1000              }
1001            }
1002        "#
1003            .unindent();
1004
1005            let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
1006            let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
1007            assert_eq!(
1008                outline
1009                    .items
1010                    .iter()
1011                    .map(|item| (item.text.as_str(), item.depth))
1012                    .collect::<Vec<_>>(),
1013                &[
1014                    ("const a1", 0),
1015                    ("const a2", 0),
1016                    ("const b1", 0),
1017                    ("const b2", 0),
1018                    ("const c1", 0),
1019                    ("const c2", 0),
1020                    ("const rest1", 0),
1021                    ("const d1", 0),
1022                    ("const e1", 0),
1023                    ("const h1", 0),
1024                    ("const rest2", 0),
1025                    ("function processData()", 0),
1026                    ("const c1", 1),
1027                    ("const c2", 1),
1028                    ("const d1", 1),
1029                    ("const d2", 1),
1030                    ("const d3", 1),
1031                    ("const g1", 1),
1032                    ("const x", 1),
1033                    ("const y", 1),
1034                    ("class DataHandler", 0),
1035                    ("method()", 1),
1036                    ("const a1", 2),
1037                    ("const a2", 2),
1038                    ("const b1", 2),
1039                    ("const b2", 2),
1040                ]
1041            );
1042        }
1043    }
1044
1045    #[gpui::test]
1046    async fn test_outline_with_object_properties(cx: &mut TestAppContext) {
1047        for language in [
1048            crate::language(
1049                "typescript",
1050                tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
1051            ),
1052            crate::language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
1053        ] {
1054            let text = r#"
1055            // Object with function properties
1056            const o = { m() {}, async n() {}, g: function* () {}, h: () => {}, k: function () {} };
1057
1058            // Object with primitive properties
1059            const p = { p1: 1, p2: "hello", p3: true };
1060
1061            // Nested objects
1062            const q = {
1063                r: {
1064                    // won't be included due to one-level depth limit
1065                    s: 1
1066                },
1067                t: 2
1068            };
1069
1070            function getData() {
1071                const local = { x: 1, y: 2 };
1072                return local;
1073            }
1074        "#
1075            .unindent();
1076
1077            let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
1078            cx.run_until_parked();
1079            let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
1080            assert_eq!(
1081                outline
1082                    .items
1083                    .iter()
1084                    .map(|item| (item.text.as_str(), item.depth))
1085                    .collect::<Vec<_>>(),
1086                &[
1087                    ("const o", 0),
1088                    ("m()", 1),
1089                    ("async n()", 1),
1090                    ("g", 1),
1091                    ("h", 1),
1092                    ("k", 1),
1093                    ("const p", 0),
1094                    ("p1", 1),
1095                    ("p2", 1),
1096                    ("p3", 1),
1097                    ("const q", 0),
1098                    ("r", 1),
1099                    ("s", 2),
1100                    ("t", 1),
1101                    ("function getData()", 0),
1102                    ("const local", 1),
1103                    ("x", 2),
1104                    ("y", 2),
1105                ]
1106            );
1107        }
1108    }
1109
1110    #[gpui::test]
1111    async fn test_outline_with_nested_object_methods(cx: &mut TestAppContext) {
1112        for language in [
1113            crate::language(
1114                "typescript",
1115                tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
1116            ),
1117            crate::language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
1118            crate::language("javascript", tree_sitter_typescript::LANGUAGE_TSX.into()),
1119        ] {
1120            let text = r#"
1121            // Reproduction from https://github.com/zed-industries/zed/issues/48711
1122            const a = {
1123              p01: '01',
1124              fn01: () => {},
1125              fn02() {},
1126              deep: {
1127                subFn01: () => {},
1128                subFn02() {},
1129                subP03: '03',
1130                deep2: {
1131                  subFn01: () => {},
1132                  subFn02() {},
1133                  subP03: '03',
1134                },
1135              },
1136            };
1137
1138            // Edge case: async methods in nested objects
1139            const b = {
1140              async topAsync() {},
1141              nested: { async nestedAsync() {} },
1142            };
1143
1144            // Edge case: object literal in function argument
1145            foo({ bar() {}, inner: { baz() {} } });
1146        "#
1147            .unindent();
1148
1149            let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
1150            cx.run_until_parked();
1151            let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
1152
1153            let items: Vec<_> = outline
1154                .items
1155                .iter()
1156                .map(|item| (item.text.as_str(), item.depth))
1157                .collect();
1158
1159            assert_eq!(
1160                items,
1161                &[
1162                    ("const a", 0),
1163                    ("p01", 1),
1164                    ("fn01", 1),
1165                    ("fn02()", 1),
1166                    ("deep", 1),
1167                    ("subFn01", 2),
1168                    ("subFn02()", 2),
1169                    ("subP03", 2),
1170                    ("deep2", 2),
1171                    ("subFn01", 3),
1172                    ("subFn02()", 3),
1173                    ("subP03", 3),
1174                    ("const b", 0),
1175                    ("async topAsync()", 1),
1176                    ("nested", 1),
1177                    ("async nestedAsync()", 2),
1178                    ("bar()", 0),
1179                    ("inner", 0),
1180                    ("baz()", 1),
1181                ]
1182            );
1183        }
1184    }
1185
1186    #[gpui::test]
1187    async fn test_outline_with_complex_nested_objects(cx: &mut TestAppContext) {
1188        for language in [
1189            crate::language(
1190                "typescript",
1191                tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
1192            ),
1193            crate::language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
1194            crate::language("javascript", tree_sitter_typescript::LANGUAGE_TSX.into()),
1195        ] {
1196            let text = r#"
1197            const config = {
1198              init() {},
1199              destroy() {},
1200              api: {
1201                baseUrl: "x",
1202                fetchData() {},
1203                async submitForm() {},
1204                errorHandler() {},
1205              },
1206              features: {
1207                auth: {
1208                  login() {},
1209                  logout() {},
1210                  refreshToken() {},
1211                },
1212                cache: {
1213                  get() {},
1214                  set() {},
1215                  invalidate() {},
1216                },
1217              },
1218              watch: {
1219                value() {},
1220              },
1221              computed: {
1222                fullName() {},
1223                displayValue() {},
1224              },
1225            };
1226
1227            registerPlugin({
1228              name: "my-plugin",
1229              setup() {},
1230              teardown() {},
1231              hooks: {
1232                beforeMount() {},
1233                mounted() {},
1234                beforeUnmount() {},
1235              },
1236            });
1237
1238            export const store = {
1239              state: {},
1240              mutations: {
1241                setUser() {},
1242                clearUser() {},
1243              },
1244              actions: {
1245                async fetchUser() {},
1246                logout() {},
1247              },
1248              getters: {
1249                currentUser() {},
1250                isAuthenticated() {},
1251              },
1252            };
1253
1254            function registerPlugin(_plugin: unknown) {}
1255        "#
1256            .unindent();
1257
1258            let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
1259            cx.run_until_parked();
1260            let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
1261
1262            let items: Vec<_> = outline
1263                .items
1264                .iter()
1265                .map(|item| (item.text.as_str(), item.depth))
1266                .collect();
1267
1268            assert_eq!(
1269                items,
1270                &[
1271                    ("const config", 0),
1272                    ("init()", 1),
1273                    ("destroy()", 1),
1274                    ("api", 1),
1275                    ("baseUrl", 2),
1276                    ("fetchData()", 2),
1277                    ("async submitForm()", 2),
1278                    ("errorHandler()", 2),
1279                    ("features", 1),
1280                    ("auth", 2),
1281                    ("login()", 3),
1282                    ("logout()", 3),
1283                    ("refreshToken()", 3),
1284                    ("cache", 2),
1285                    ("get()", 3),
1286                    ("set()", 3),
1287                    ("invalidate()", 3),
1288                    ("watch", 1),
1289                    ("value()", 2),
1290                    ("computed", 1),
1291                    ("fullName()", 2),
1292                    ("displayValue()", 2),
1293                    ("name", 0),
1294                    ("setup()", 0),
1295                    ("teardown()", 0),
1296                    ("hooks", 0),
1297                    ("beforeMount()", 1),
1298                    ("mounted()", 1),
1299                    ("beforeUnmount()", 1),
1300                    ("const store", 0),
1301                    ("state", 1),
1302                    ("mutations", 1),
1303                    ("setUser()", 2),
1304                    ("clearUser()", 2),
1305                    ("actions", 1),
1306                    ("async fetchUser()", 2),
1307                    ("logout()", 2),
1308                    ("getters", 1),
1309                    ("currentUser()", 2),
1310                    ("isAuthenticated()", 2),
1311                    ("function registerPlugin( )", 0),
1312                ]
1313            );
1314        }
1315    }
1316
1317    #[gpui::test]
1318    async fn test_outline_with_computed_property_names(cx: &mut TestAppContext) {
1319        for language in [
1320            crate::language(
1321                "typescript",
1322                tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
1323            ),
1324            crate::language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
1325        ] {
1326            let text = r#"
1327            // Symbols as object keys
1328            const sym = Symbol("test");
1329            const obj1 = {
1330                [sym]: 1,
1331                [Symbol("inline")]: 2,
1332                normalKey: 3
1333            };
1334
1335            // Enums as object keys
1336            enum Color { Red, Blue, Green }
1337
1338            const obj2 = {
1339                [Color.Red]: "red value",
1340                [Color.Blue]: "blue value",
1341                regularProp: "normal"
1342            };
1343
1344            // Mixed computed properties
1345            const key = "dynamic";
1346            const obj3 = {
1347                [key]: 1,
1348                ["string" + "concat"]: 2,
1349                [1 + 1]: 3,
1350                static: 4
1351            };
1352
1353            // Nested objects with computed properties
1354            const obj4 = {
1355                [sym]: {
1356                    nested: 1
1357                },
1358                regular: {
1359                    [key]: 2
1360                }
1361            };
1362        "#
1363            .unindent();
1364
1365            let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
1366            let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
1367            assert_eq!(
1368                outline
1369                    .items
1370                    .iter()
1371                    .map(|item| (item.text.as_str(), item.depth))
1372                    .collect::<Vec<_>>(),
1373                &[
1374                    ("const sym", 0),
1375                    ("const obj1", 0),
1376                    ("[sym]", 1),
1377                    ("[Symbol(\"inline\")]", 1),
1378                    ("normalKey", 1),
1379                    ("enum Color", 0),
1380                    ("const obj2", 0),
1381                    ("[Color.Red]", 1),
1382                    ("[Color.Blue]", 1),
1383                    ("regularProp", 1),
1384                    ("const key", 0),
1385                    ("const obj3", 0),
1386                    ("[key]", 1),
1387                    ("[\"string\" + \"concat\"]", 1),
1388                    ("[1 + 1]", 1),
1389                    ("static", 1),
1390                    ("const obj4", 0),
1391                    ("[sym]", 1),
1392                    ("nested", 2),
1393                    ("regular", 1),
1394                    ("[key]", 2),
1395                ]
1396            );
1397        }
1398    }
1399
1400    #[gpui::test]
1401    async fn test_generator_function_outline(cx: &mut TestAppContext) {
1402        let language = crate::language("javascript", tree_sitter_typescript::LANGUAGE_TSX.into());
1403
1404        let text = r#"
1405            function normalFunction() {
1406                console.log("normal");
1407            }
1408
1409            function* simpleGenerator() {
1410                yield 1;
1411                yield 2;
1412            }
1413
1414            async function* asyncGenerator() {
1415                yield await Promise.resolve(1);
1416            }
1417
1418            function* generatorWithParams(start, end) {
1419                for (let i = start; i <= end; i++) {
1420                    yield i;
1421                }
1422            }
1423
1424            class TestClass {
1425                *methodGenerator() {
1426                    yield "method";
1427                }
1428
1429                async *asyncMethodGenerator() {
1430                    yield "async method";
1431                }
1432            }
1433        "#
1434        .unindent();
1435
1436        let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
1437        let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
1438        assert_eq!(
1439            outline
1440                .items
1441                .iter()
1442                .map(|item| (item.text.as_str(), item.depth))
1443                .collect::<Vec<_>>(),
1444            &[
1445                ("function normalFunction()", 0),
1446                ("function* simpleGenerator()", 0),
1447                ("async function* asyncGenerator()", 0),
1448                ("function* generatorWithParams( )", 0),
1449                ("class TestClass", 0),
1450                ("*methodGenerator()", 1),
1451                ("async *asyncMethodGenerator()", 1),
1452            ]
1453        );
1454    }
1455
1456    #[gpui::test]
1457    async fn test_conditional_test_wrappers(cx: &mut TestAppContext) {
1458        for language in [
1459            crate::language(
1460                "typescript",
1461                tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
1462            ),
1463            crate::language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
1464            crate::language("javascript", tree_sitter_typescript::LANGUAGE_TSX.into()),
1465        ] {
1466            let text = r#"
1467                it.runIf(true)("runIf test", () => {
1468                    true;
1469                });
1470
1471                it.skipIf(false)("skipIf test", () => {
1472                    true;
1473                });
1474
1475                test.runIf(true)("runIf test 2", () => {
1476                    true;
1477                });
1478
1479                test.skipIf(false)("skipIf test 2", () => {
1480                    true;
1481                });
1482
1483                describe.runIf(true)("runIf describe", () => {
1484                    it("inner test", () => {
1485                        true;
1486                    });
1487                });
1488
1489                describe.skipIf(false)("skipIf describe", () => {
1490                    it("inner test 2", () => {
1491                        true;
1492                    });
1493                });
1494
1495                it.todoIf(false)("todoIf test", () => {
1496                    true;
1497                });
1498
1499                it.if(true)("if test", () => {
1500                    true;
1501                });
1502
1503                test.todoIf(false)("todoIf test 2", () => {
1504                    true;
1505                });
1506
1507                test.if(true)("if test 2", () => {
1508                    true;
1509                });
1510
1511                describe.todoIf(false)("todoIf describe", () => {
1512                    it("inner todoIf", () => {
1513                        true;
1514                    });
1515                });
1516
1517                describe.if(true)("if describe", () => {
1518                    it("inner if", () => {
1519                        true;
1520                    });
1521                });
1522
1523                test.failing("failing test", () => {
1524                    true;
1525                });
1526
1527                it.failing("failing it", () => {
1528                    true;
1529                });
1530
1531                it.each([1, 2, 3])("each test", () => {
1532                    true;
1533                });
1534
1535                describe.each([1, 2])("each describe", () => {
1536                    it("inner each", () => {
1537                        true;
1538                    });
1539                });
1540
1541                it.skip("skip test", () => {
1542                    true;
1543                });
1544
1545                it.only("only test", () => {
1546                    true;
1547                });
1548
1549                it.todo("todo test");
1550            "#
1551            .unindent();
1552
1553            let text_len = text.len();
1554            let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
1555            cx.executor().run_until_parked();
1556
1557            let outline = buffer.update(cx, |buffer, _cx| buffer.snapshot().outline(None));
1558            let outline_names = outline
1559                .items
1560                .iter()
1561                .map(|item| item.text.as_str())
1562                .collect::<Vec<_>>();
1563            assert_eq!(
1564                outline_names,
1565                [
1566                    "runIf test",
1567                    "skipIf test",
1568                    "runIf test 2",
1569                    "skipIf test 2",
1570                    "runIf describe",
1571                    "it inner test",
1572                    "skipIf describe",
1573                    "it inner test 2",
1574                    "todoIf test",
1575                    "if test",
1576                    "todoIf test 2",
1577                    "if test 2",
1578                    "todoIf describe",
1579                    "it inner todoIf",
1580                    "if describe",
1581                    "it inner if",
1582                    "test.failing failing test",
1583                    "it.failing failing it",
1584                    "each test",
1585                    "each describe",
1586                    "it inner each",
1587                    "it.skip skip test",
1588                    "it.only only test",
1589                    "it.todo todo test",
1590                ]
1591            );
1592
1593            let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
1594            let runnable_names = snapshot
1595                .runnable_ranges(0..text_len)
1596                .map(|runnable| {
1597                    snapshot
1598                        .text_for_range(runnable.run_range)
1599                        .collect::<String>()
1600                })
1601                .collect::<Vec<_>>();
1602            assert_eq!(
1603                runnable_names,
1604                [
1605                    "runIf test",
1606                    "skipIf test",
1607                    "runIf test 2",
1608                    "skipIf test 2",
1609                    "runIf describe",
1610                    "inner test",
1611                    "skipIf describe",
1612                    "inner test 2",
1613                    "todoIf test",
1614                    "if test",
1615                    "todoIf test 2",
1616                    "if test 2",
1617                    "todoIf describe",
1618                    "inner todoIf",
1619                    "if describe",
1620                    "inner if",
1621                    "failing test",
1622                    "failing it",
1623                    "each test",
1624                    "each describe",
1625                    "inner each",
1626                    "skip test",
1627                    "only test",
1628                    "todo test",
1629                ]
1630            );
1631        }
1632    }
1633
1634    #[gpui::test]
1635    async fn test_package_json_discovery(executor: BackgroundExecutor, cx: &mut TestAppContext) {
1636        cx.update(|cx| {
1637            settings::init(cx);
1638        });
1639
1640        let package_json_1 = json!({
1641            "dependencies": {
1642                "mocha": "1.0.0",
1643                "vitest": "1.0.0"
1644            },
1645            "scripts": {
1646                "test": ""
1647            }
1648        })
1649        .to_string();
1650
1651        let package_json_2 = json!({
1652            "devDependencies": {
1653                "vitest": "2.0.0"
1654            },
1655            "scripts": {
1656                "test": ""
1657            }
1658        })
1659        .to_string();
1660
1661        let fs = FakeFs::new(executor);
1662        fs.insert_tree(
1663            path!("/root"),
1664            json!({
1665                "package.json": package_json_1,
1666                "sub": {
1667                    "package.json": package_json_2,
1668                    "file.js": "",
1669                }
1670            }),
1671        )
1672        .await;
1673
1674        let provider = TypeScriptContextProvider::new(fs.clone());
1675        let package_json_data = cx
1676            .update(|cx| {
1677                provider.combined_package_json_data(
1678                    fs.clone(),
1679                    path!("/root").as_ref(),
1680                    rel_path("sub/file1.js"),
1681                    cx,
1682                )
1683            })
1684            .await
1685            .unwrap();
1686        pretty_assertions::assert_eq!(
1687            package_json_data,
1688            PackageJsonData {
1689                jest_package_path: None,
1690                mocha_package_path: Some(Path::new(path!("/root/package.json")).into()),
1691                vitest_package_path: Some(Path::new(path!("/root/sub/package.json")).into()),
1692                jasmine_package_path: None,
1693                bun_package_path: None,
1694                node_package_path: None,
1695                scripts: [
1696                    (
1697                        Path::new(path!("/root/package.json")).into(),
1698                        "test".to_owned()
1699                    ),
1700                    (
1701                        Path::new(path!("/root/sub/package.json")).into(),
1702                        "test".to_owned()
1703                    )
1704                ]
1705                .into_iter()
1706                .collect(),
1707                package_manager: None,
1708            }
1709        );
1710
1711        let mut task_templates = TaskTemplates::default();
1712        package_json_data.fill_task_templates(&mut task_templates);
1713        let task_templates = task_templates
1714            .0
1715            .into_iter()
1716            .map(|template| (template.label, template.cwd))
1717            .collect::<Vec<_>>();
1718        pretty_assertions::assert_eq!(
1719            task_templates,
1720            [
1721                (
1722                    "vitest file test".into(),
1723                    Some("$ZED_CUSTOM_TYPESCRIPT_VITEST_PACKAGE_PATH".into()),
1724                ),
1725                (
1726                    "vitest test $ZED_SYMBOL".into(),
1727                    Some("$ZED_CUSTOM_TYPESCRIPT_VITEST_PACKAGE_PATH".into()),
1728                ),
1729                (
1730                    "mocha file test".into(),
1731                    Some("$ZED_CUSTOM_TYPESCRIPT_MOCHA_PACKAGE_PATH".into()),
1732                ),
1733                (
1734                    "mocha test $ZED_SYMBOL".into(),
1735                    Some("$ZED_CUSTOM_TYPESCRIPT_MOCHA_PACKAGE_PATH".into()),
1736                ),
1737                (
1738                    "root/package.json > test".into(),
1739                    Some(path!("/root").into())
1740                ),
1741                (
1742                    "sub/package.json > test".into(),
1743                    Some(path!("/root/sub").into())
1744                ),
1745            ]
1746        );
1747    }
1748
1749    #[test]
1750    fn test_escaping_name() {
1751        let cases = [
1752            ("plain test name", "plain test name"),
1753            ("test name with $param_name", "test name with (.+?)"),
1754            ("test name with $nested.param.name", "test name with (.+?)"),
1755            ("test name with $#", "test name with (.+?)"),
1756            ("test name with $##", "test name with (.+?)\\#"),
1757            ("test name with %p", "test name with (.+?)"),
1758            ("test name with %s", "test name with (.+?)"),
1759            ("test name with %d", "test name with (.+?)"),
1760            ("test name with %i", "test name with (.+?)"),
1761            ("test name with %f", "test name with (.+?)"),
1762            ("test name with %j", "test name with (.+?)"),
1763            ("test name with %o", "test name with (.+?)"),
1764            ("test name with %#", "test name with (.+?)"),
1765            ("test name with %$", "test name with (.+?)"),
1766            ("test name with %%", "test name with (.+?)"),
1767            ("test name with %q", "test name with %q"),
1768            (
1769                "test name with regex chars .*+?^${}()|[]\\",
1770                "test name with regex chars \\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\",
1771            ),
1772            (
1773                "test name with multiple $params and %pretty and %b and (.+?)",
1774                "test name with multiple (.+?) and (.+?)retty and %b and \\(\\.\\+\\?\\)",
1775            ),
1776        ];
1777
1778        for (input, expected) in cases {
1779            assert_eq!(replace_test_name_parameters(input), expected);
1780        }
1781    }
1782
1783    // The order of test runner tasks is based on inferred user preference:
1784    // 1. Dedicated test runners (e.g., Jest, Vitest, Mocha, Jasmine) are prioritized.
1785    // 2. Bun's built-in test runner (`bun test`) comes next.
1786    // 3. Node.js's built-in test runner (`node --test`) is last.
1787    // This hierarchy assumes that if a dedicated test framework is installed, it is the
1788    // preferred testing mechanism. Between runtime-specific options, `bun test` is
1789    // typically preferred over `node --test` when @types/bun is present.
1790    #[gpui::test]
1791    async fn test_task_ordering_with_multiple_test_runners(
1792        executor: BackgroundExecutor,
1793        cx: &mut TestAppContext,
1794    ) {
1795        cx.update(|cx| {
1796            settings::init(cx);
1797        });
1798
1799        // Test case with all test runners present
1800        let package_json_all_runners = json!({
1801            "devDependencies": {
1802                "@types/bun": "1.0.0",
1803                "@types/node": "^20.0.0",
1804                "jest": "29.0.0",
1805                "mocha": "10.0.0",
1806                "vitest": "1.0.0",
1807                "jasmine": "5.0.0",
1808            },
1809            "scripts": {
1810                "test": "jest"
1811            }
1812        })
1813        .to_string();
1814
1815        let fs = FakeFs::new(executor);
1816        fs.insert_tree(
1817            path!("/root"),
1818            json!({
1819                "package.json": package_json_all_runners,
1820                "file.js": "",
1821            }),
1822        )
1823        .await;
1824
1825        let provider = TypeScriptContextProvider::new(fs.clone());
1826
1827        let package_json_data = cx
1828            .update(|cx| {
1829                provider.combined_package_json_data(
1830                    fs.clone(),
1831                    path!("/root").as_ref(),
1832                    rel_path("file.js"),
1833                    cx,
1834                )
1835            })
1836            .await
1837            .unwrap();
1838
1839        assert!(package_json_data.jest_package_path.is_some());
1840        assert!(package_json_data.mocha_package_path.is_some());
1841        assert!(package_json_data.vitest_package_path.is_some());
1842        assert!(package_json_data.jasmine_package_path.is_some());
1843        assert!(package_json_data.bun_package_path.is_some());
1844        assert!(package_json_data.node_package_path.is_some());
1845
1846        let mut task_templates = TaskTemplates::default();
1847        package_json_data.fill_task_templates(&mut task_templates);
1848
1849        let test_tasks: Vec<_> = task_templates
1850            .0
1851            .iter()
1852            .filter(|template| {
1853                template.tags.contains(&"ts-test".to_owned())
1854                    || template.tags.contains(&"js-test".to_owned())
1855            })
1856            .map(|template| &template.label)
1857            .collect();
1858
1859        let node_test_index = test_tasks
1860            .iter()
1861            .position(|label| label.contains("node test"));
1862        let jest_test_index = test_tasks.iter().position(|label| label.contains("jest"));
1863        let bun_test_index = test_tasks
1864            .iter()
1865            .position(|label| label.contains("bun test"));
1866
1867        assert!(
1868            node_test_index.is_some(),
1869            "Node test tasks should be present"
1870        );
1871        assert!(
1872            jest_test_index.is_some(),
1873            "Jest test tasks should be present"
1874        );
1875        assert!(bun_test_index.is_some(), "Bun test tasks should be present");
1876
1877        assert!(
1878            jest_test_index.unwrap() < bun_test_index.unwrap(),
1879            "Jest should come before Bun"
1880        );
1881        assert!(
1882            bun_test_index.unwrap() < node_test_index.unwrap(),
1883            "Bun should come before Node"
1884        );
1885    }
1886}
1887
Served at tenant.openagents/omega Member data and write actions are omitted.