Skip to repository content1887 lines · 65.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:28:00.673Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
typescript.rs
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