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, Task};
7use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};
8use itertools::Itertools as _;
9use language::{
10 ContextLocation, ContextProvider, File, LanguageName, LanguageToolchainStore, LspAdapter,
11 LspAdapterDelegate, LspInstaller, Toolchain,
12};
13use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
14use node_runtime::{NodeRuntime, VersionStrategy};
15use project::{Fs, lsp_store::language_server_settings};
16use serde_json::{Value, json};
17use smol::{fs, lock::RwLock, stream::StreamExt};
18use std::{
19 borrow::Cow,
20 ffi::OsString,
21 path::{Path, PathBuf},
22 sync::{Arc, LazyLock},
23};
24use task::{TaskTemplate, TaskTemplates, VariableName};
25use util::merge_json_value_into;
26use util::{ResultExt, fs::remove_matching, maybe};
27
28use crate::{PackageJson, PackageJsonData, github_download::download_server_binary};
29
30pub(crate) struct TypeScriptContextProvider {
31 fs: Arc<dyn Fs>,
32 last_package_json: PackageJsonContents,
33}
34
35const TYPESCRIPT_RUNNER_VARIABLE: VariableName =
36 VariableName::Custom(Cow::Borrowed("TYPESCRIPT_RUNNER"));
37
38const TYPESCRIPT_JEST_TEST_NAME_VARIABLE: VariableName =
39 VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST_TEST_NAME"));
40
41const TYPESCRIPT_VITEST_TEST_NAME_VARIABLE: VariableName =
42 VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST_TEST_NAME"));
43
44const TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE: VariableName =
45 VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST_PACKAGE_PATH"));
46
47const TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE: VariableName =
48 VariableName::Custom(Cow::Borrowed("TYPESCRIPT_MOCHA_PACKAGE_PATH"));
49
50const TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE: VariableName =
51 VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST_PACKAGE_PATH"));
52
53const TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE: VariableName =
54 VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JASMINE_PACKAGE_PATH"));
55
56#[derive(Clone, Debug, Default)]
57struct PackageJsonContents(Arc<RwLock<HashMap<PathBuf, PackageJson>>>);
58
59impl PackageJsonData {
60 fn fill_task_templates(&self, task_templates: &mut TaskTemplates) {
61 if self.jest_package_path.is_some() {
62 task_templates.0.push(TaskTemplate {
63 label: "jest file test".to_owned(),
64 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
65 args: vec![
66 "exec".to_owned(),
67 "--".to_owned(),
68 "jest".to_owned(),
69 "--runInBand".to_owned(),
70 VariableName::File.template_value(),
71 ],
72 cwd: Some(TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE.template_value()),
73 ..TaskTemplate::default()
74 });
75 task_templates.0.push(TaskTemplate {
76 label: format!("jest test {}", VariableName::Symbol.template_value()),
77 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
78 args: vec![
79 "exec".to_owned(),
80 "--".to_owned(),
81 "jest".to_owned(),
82 "--runInBand".to_owned(),
83 "--testNamePattern".to_owned(),
84 format!(
85 "\"{}\"",
86 TYPESCRIPT_JEST_TEST_NAME_VARIABLE.template_value()
87 ),
88 VariableName::File.template_value(),
89 ],
90 tags: vec![
91 "ts-test".to_owned(),
92 "js-test".to_owned(),
93 "tsx-test".to_owned(),
94 ],
95 cwd: Some(TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE.template_value()),
96 ..TaskTemplate::default()
97 });
98 }
99
100 if self.vitest_package_path.is_some() {
101 task_templates.0.push(TaskTemplate {
102 label: format!("{} file test", "vitest".to_owned()),
103 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
104 args: vec![
105 "exec".to_owned(),
106 "--".to_owned(),
107 "vitest".to_owned(),
108 "run".to_owned(),
109 "--poolOptions.forks.minForks=0".to_owned(),
110 "--poolOptions.forks.maxForks=1".to_owned(),
111 VariableName::File.template_value(),
112 ],
113 cwd: Some(TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE.template_value()),
114 ..TaskTemplate::default()
115 });
116 task_templates.0.push(TaskTemplate {
117 label: format!(
118 "{} test {}",
119 "vitest".to_owned(),
120 VariableName::Symbol.template_value(),
121 ),
122 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
123 args: vec![
124 "exec".to_owned(),
125 "--".to_owned(),
126 "vitest".to_owned(),
127 "run".to_owned(),
128 "--poolOptions.forks.minForks=0".to_owned(),
129 "--poolOptions.forks.maxForks=1".to_owned(),
130 "--testNamePattern".to_owned(),
131 format!(
132 "\"{}\"",
133 TYPESCRIPT_VITEST_TEST_NAME_VARIABLE.template_value()
134 ),
135 VariableName::File.template_value(),
136 ],
137 tags: vec![
138 "ts-test".to_owned(),
139 "js-test".to_owned(),
140 "tsx-test".to_owned(),
141 ],
142 cwd: Some(TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE.template_value()),
143 ..TaskTemplate::default()
144 });
145 }
146
147 if self.mocha_package_path.is_some() {
148 task_templates.0.push(TaskTemplate {
149 label: format!("{} file test", "mocha".to_owned()),
150 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
151 args: vec![
152 "exec".to_owned(),
153 "--".to_owned(),
154 "mocha".to_owned(),
155 VariableName::File.template_value(),
156 ],
157 cwd: Some(TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE.template_value()),
158 ..TaskTemplate::default()
159 });
160 task_templates.0.push(TaskTemplate {
161 label: format!(
162 "{} test {}",
163 "mocha".to_owned(),
164 VariableName::Symbol.template_value(),
165 ),
166 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
167 args: vec![
168 "exec".to_owned(),
169 "--".to_owned(),
170 "mocha".to_owned(),
171 "--grep".to_owned(),
172 format!("\"{}\"", VariableName::Symbol.template_value()),
173 VariableName::File.template_value(),
174 ],
175 tags: vec![
176 "ts-test".to_owned(),
177 "js-test".to_owned(),
178 "tsx-test".to_owned(),
179 ],
180 cwd: Some(TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE.template_value()),
181 ..TaskTemplate::default()
182 });
183 }
184
185 if self.jasmine_package_path.is_some() {
186 task_templates.0.push(TaskTemplate {
187 label: format!("{} file test", "jasmine".to_owned()),
188 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
189 args: vec![
190 "exec".to_owned(),
191 "--".to_owned(),
192 "jasmine".to_owned(),
193 VariableName::File.template_value(),
194 ],
195 cwd: Some(TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE.template_value()),
196 ..TaskTemplate::default()
197 });
198 task_templates.0.push(TaskTemplate {
199 label: format!(
200 "{} test {}",
201 "jasmine".to_owned(),
202 VariableName::Symbol.template_value(),
203 ),
204 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
205 args: vec![
206 "exec".to_owned(),
207 "--".to_owned(),
208 "jasmine".to_owned(),
209 format!("--filter={}", VariableName::Symbol.template_value()),
210 VariableName::File.template_value(),
211 ],
212 tags: vec![
213 "ts-test".to_owned(),
214 "js-test".to_owned(),
215 "tsx-test".to_owned(),
216 ],
217 cwd: Some(TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE.template_value()),
218 ..TaskTemplate::default()
219 });
220 }
221
222 let script_name_counts: HashMap<_, usize> =
223 self.scripts
224 .iter()
225 .fold(HashMap::default(), |mut acc, (_, script)| {
226 *acc.entry(script).or_default() += 1;
227 acc
228 });
229 for (path, script) in &self.scripts {
230 let label = if script_name_counts.get(script).copied().unwrap_or_default() > 1
231 && let Some(parent) = path.parent().and_then(|parent| parent.file_name())
232 {
233 let parent = parent.to_string_lossy();
234 format!("{parent}/package.json > {script}")
235 } else {
236 format!("package.json > {script}")
237 };
238 task_templates.0.push(TaskTemplate {
239 label,
240 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
241 args: vec!["run".to_owned(), script.to_owned()],
242 tags: vec!["package-script".into()],
243 cwd: Some(
244 path.parent()
245 .unwrap_or(Path::new("/"))
246 .to_string_lossy()
247 .to_string(),
248 ),
249 ..TaskTemplate::default()
250 });
251 }
252 }
253}
254
255impl TypeScriptContextProvider {
256 pub fn new(fs: Arc<dyn Fs>) -> Self {
257 Self {
258 fs,
259 last_package_json: PackageJsonContents::default(),
260 }
261 }
262
263 fn combined_package_json_data(
264 &self,
265 fs: Arc<dyn Fs>,
266 worktree_root: &Path,
267 file_relative_path: &Path,
268 cx: &App,
269 ) -> Task<anyhow::Result<PackageJsonData>> {
270 let new_json_data = file_relative_path
271 .ancestors()
272 .map(|path| worktree_root.join(path))
273 .map(|parent_path| {
274 self.package_json_data(&parent_path, self.last_package_json.clone(), fs.clone(), cx)
275 })
276 .collect::<Vec<_>>();
277
278 cx.background_spawn(async move {
279 let mut package_json_data = PackageJsonData::default();
280 for new_data in join_all(new_json_data).await.into_iter().flatten() {
281 package_json_data.merge(new_data);
282 }
283 Ok(package_json_data)
284 })
285 }
286
287 fn package_json_data(
288 &self,
289 directory_path: &Path,
290 existing_package_json: PackageJsonContents,
291 fs: Arc<dyn Fs>,
292 cx: &App,
293 ) -> Task<anyhow::Result<PackageJsonData>> {
294 let package_json_path = directory_path.join("package.json");
295 let metadata_check_fs = fs.clone();
296 cx.background_spawn(async move {
297 let metadata = metadata_check_fs
298 .metadata(&package_json_path)
299 .await
300 .with_context(|| format!("getting metadata for {package_json_path:?}"))?
301 .with_context(|| format!("missing FS metadata for {package_json_path:?}"))?;
302 let mtime = DateTime::<Local>::from(metadata.mtime.timestamp_for_user());
303 let existing_data = {
304 let contents = existing_package_json.0.read().await;
305 contents
306 .get(&package_json_path)
307 .filter(|package_json| package_json.mtime == mtime)
308 .map(|package_json| package_json.data.clone())
309 };
310 match existing_data {
311 Some(existing_data) => Ok(existing_data),
312 None => {
313 let package_json_string =
314 fs.load(&package_json_path).await.with_context(|| {
315 format!("loading package.json from {package_json_path:?}")
316 })?;
317 let package_json: HashMap<String, serde_json_lenient::Value> =
318 serde_json_lenient::from_str(&package_json_string).with_context(|| {
319 format!("parsing package.json from {package_json_path:?}")
320 })?;
321 let new_data =
322 PackageJsonData::new(package_json_path.as_path().into(), package_json);
323 {
324 let mut contents = existing_package_json.0.write().await;
325 contents.insert(
326 package_json_path,
327 PackageJson {
328 mtime,
329 data: new_data.clone(),
330 },
331 );
332 }
333 Ok(new_data)
334 }
335 }
336 })
337 }
338}
339
340async fn detect_package_manager(
341 worktree_root: PathBuf,
342 fs: Arc<dyn Fs>,
343 package_json_data: Option<PackageJsonData>,
344) -> &'static str {
345 if let Some(package_json_data) = package_json_data
346 && let Some(package_manager) = package_json_data.package_manager
347 {
348 return package_manager;
349 }
350 if fs.is_file(&worktree_root.join("pnpm-lock.yaml")).await {
351 return "pnpm";
352 }
353 if fs.is_file(&worktree_root.join("yarn.lock")).await {
354 return "yarn";
355 }
356 "npm"
357}
358
359impl ContextProvider for TypeScriptContextProvider {
360 fn associated_tasks(
361 &self,
362 file: Option<Arc<dyn File>>,
363 cx: &App,
364 ) -> Task<Option<TaskTemplates>> {
365 let Some(file) = project::File::from_dyn(file.as_ref()).cloned() else {
366 return Task::ready(None);
367 };
368 let Some(worktree_root) = file.worktree.read(cx).root_dir() else {
369 return Task::ready(None);
370 };
371 let file_relative_path = file.path().clone();
372 let package_json_data = self.combined_package_json_data(
373 self.fs.clone(),
374 &worktree_root,
375 &file_relative_path,
376 cx,
377 );
378
379 cx.background_spawn(async move {
380 let mut task_templates = TaskTemplates(Vec::new());
381 task_templates.0.push(TaskTemplate {
382 label: format!(
383 "execute selection {}",
384 VariableName::SelectedText.template_value()
385 ),
386 command: "node".to_owned(),
387 args: vec![
388 "-e".to_owned(),
389 format!("\"{}\"", VariableName::SelectedText.template_value()),
390 ],
391 ..TaskTemplate::default()
392 });
393
394 match package_json_data.await {
395 Ok(package_json) => {
396 package_json.fill_task_templates(&mut task_templates);
397 }
398 Err(e) => {
399 log::error!(
400 "Failed to read package.json for worktree {file_relative_path:?}: {e:#}"
401 );
402 }
403 }
404
405 Some(task_templates)
406 })
407 }
408
409 fn build_context(
410 &self,
411 current_vars: &task::TaskVariables,
412 location: ContextLocation<'_>,
413 _project_env: Option<HashMap<String, String>>,
414 _toolchains: Arc<dyn LanguageToolchainStore>,
415 cx: &mut App,
416 ) -> Task<Result<task::TaskVariables>> {
417 let mut vars = task::TaskVariables::default();
418
419 if let Some(symbol) = current_vars.get(&VariableName::Symbol) {
420 vars.insert(
421 TYPESCRIPT_JEST_TEST_NAME_VARIABLE,
422 replace_test_name_parameters(symbol),
423 );
424 vars.insert(
425 TYPESCRIPT_VITEST_TEST_NAME_VARIABLE,
426 replace_test_name_parameters(symbol),
427 );
428 }
429 let file_path = location
430 .file_location
431 .buffer
432 .read(cx)
433 .file()
434 .map(|file| file.path());
435
436 let args = location.worktree_root.zip(location.fs).zip(file_path).map(
437 |((worktree_root, fs), file_path)| {
438 (
439 self.combined_package_json_data(fs.clone(), &worktree_root, file_path, cx),
440 worktree_root,
441 fs,
442 )
443 },
444 );
445 cx.background_spawn(async move {
446 if let Some((task, worktree_root, fs)) = args {
447 let package_json_data = task.await.log_err();
448 vars.insert(
449 TYPESCRIPT_RUNNER_VARIABLE,
450 detect_package_manager(worktree_root, fs, package_json_data.clone())
451 .await
452 .to_owned(),
453 );
454
455 if let Some(package_json_data) = package_json_data {
456 if let Some(path) = package_json_data.jest_package_path {
457 vars.insert(
458 TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE,
459 path.parent()
460 .unwrap_or(Path::new(""))
461 .to_string_lossy()
462 .to_string(),
463 );
464 }
465
466 if let Some(path) = package_json_data.mocha_package_path {
467 vars.insert(
468 TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE,
469 path.parent()
470 .unwrap_or(Path::new(""))
471 .to_string_lossy()
472 .to_string(),
473 );
474 }
475
476 if let Some(path) = package_json_data.vitest_package_path {
477 vars.insert(
478 TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE,
479 path.parent()
480 .unwrap_or(Path::new(""))
481 .to_string_lossy()
482 .to_string(),
483 );
484 }
485
486 if let Some(path) = package_json_data.jasmine_package_path {
487 vars.insert(
488 TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE,
489 path.parent()
490 .unwrap_or(Path::new(""))
491 .to_string_lossy()
492 .to_string(),
493 );
494 }
495 }
496 }
497 Ok(vars)
498 })
499 }
500}
501
502fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
503 vec![server_path.into(), "--stdio".into()]
504}
505
506fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
507 vec![
508 "--max-old-space-size=8192".into(),
509 server_path.into(),
510 "--stdio".into(),
511 ]
512}
513
514fn replace_test_name_parameters(test_name: &str) -> String {
515 static PATTERN: LazyLock<regex::Regex> =
516 LazyLock::new(|| regex::Regex::new(r"(\$([A-Za-z0-9_\.]+|[\#])|%[psdifjo#\$%])").unwrap());
517 PATTERN.split(test_name).map(regex::escape).join("(.+?)")
518}
519
520pub struct TypeScriptLspAdapter {
521 fs: Arc<dyn Fs>,
522 node: NodeRuntime,
523}
524
525impl TypeScriptLspAdapter {
526 const OLD_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js";
527 const NEW_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs";
528 const SERVER_NAME: LanguageServerName =
529 LanguageServerName::new_static("typescript-language-server");
530 const PACKAGE_NAME: &str = "typescript";
531 pub fn new(node: NodeRuntime, fs: Arc<dyn Fs>) -> Self {
532 TypeScriptLspAdapter { fs, node }
533 }
534 async fn tsdk_path(&self, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {
535 let is_yarn = adapter
536 .read_text_file(PathBuf::from(".yarn/sdks/typescript/lib/typescript.js"))
537 .await
538 .is_ok();
539
540 let tsdk_path = if is_yarn {
541 ".yarn/sdks/typescript/lib"
542 } else {
543 "node_modules/typescript/lib"
544 };
545
546 if self
547 .fs
548 .is_dir(&adapter.worktree_root_path().join(tsdk_path))
549 .await
550 {
551 Some(tsdk_path)
552 } else {
553 None
554 }
555 }
556}
557
558pub struct TypeScriptVersions {
559 typescript_version: String,
560 server_version: String,
561}
562
563impl LspInstaller for TypeScriptLspAdapter {
564 type BinaryVersion = TypeScriptVersions;
565
566 async fn fetch_latest_server_version(
567 &self,
568 _: &dyn LspAdapterDelegate,
569 _: bool,
570 _: &mut AsyncApp,
571 ) -> Result<TypeScriptVersions> {
572 Ok(TypeScriptVersions {
573 typescript_version: self.node.npm_package_latest_version("typescript").await?,
574 server_version: self
575 .node
576 .npm_package_latest_version("typescript-language-server")
577 .await?,
578 })
579 }
580
581 async fn check_if_version_installed(
582 &self,
583 version: &TypeScriptVersions,
584 container_dir: &PathBuf,
585 _: &dyn LspAdapterDelegate,
586 ) -> Option<LanguageServerBinary> {
587 let server_path = container_dir.join(Self::NEW_SERVER_PATH);
588
589 let should_install_language_server = self
590 .node
591 .should_install_npm_package(
592 Self::PACKAGE_NAME,
593 &server_path,
594 container_dir,
595 VersionStrategy::Latest(version.typescript_version.as_str()),
596 )
597 .await;
598
599 if should_install_language_server {
600 None
601 } else {
602 Some(LanguageServerBinary {
603 path: self.node.binary_path().await.ok()?,
604 env: None,
605 arguments: typescript_server_binary_arguments(&server_path),
606 })
607 }
608 }
609
610 async fn fetch_server_binary(
611 &self,
612 latest_version: TypeScriptVersions,
613 container_dir: PathBuf,
614 _: &dyn LspAdapterDelegate,
615 ) -> Result<LanguageServerBinary> {
616 let server_path = container_dir.join(Self::NEW_SERVER_PATH);
617
618 self.node
619 .npm_install_packages(
620 &container_dir,
621 &[
622 (
623 Self::PACKAGE_NAME,
624 latest_version.typescript_version.as_str(),
625 ),
626 (
627 "typescript-language-server",
628 latest_version.server_version.as_str(),
629 ),
630 ],
631 )
632 .await?;
633
634 Ok(LanguageServerBinary {
635 path: self.node.binary_path().await?,
636 env: None,
637 arguments: typescript_server_binary_arguments(&server_path),
638 })
639 }
640
641 async fn cached_server_binary(
642 &self,
643 container_dir: PathBuf,
644 _: &dyn LspAdapterDelegate,
645 ) -> Option<LanguageServerBinary> {
646 get_cached_ts_server_binary(container_dir, &self.node).await
647 }
648}
649
650#[async_trait(?Send)]
651impl LspAdapter for TypeScriptLspAdapter {
652 fn name(&self) -> LanguageServerName {
653 Self::SERVER_NAME
654 }
655
656 fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
657 Some(vec![
658 CodeActionKind::QUICKFIX,
659 CodeActionKind::REFACTOR,
660 CodeActionKind::REFACTOR_EXTRACT,
661 CodeActionKind::SOURCE,
662 ])
663 }
664
665 async fn label_for_completion(
666 &self,
667 item: &lsp::CompletionItem,
668 language: &Arc<language::Language>,
669 ) -> Option<language::CodeLabel> {
670 use lsp::CompletionItemKind as Kind;
671 let len = item.label.len();
672 let grammar = language.grammar()?;
673 let highlight_id = match item.kind? {
674 Kind::CLASS | Kind::INTERFACE | Kind::ENUM => grammar.highlight_id_for_name("type"),
675 Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
676 Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
677 Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
678 Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
679 Kind::VARIABLE => grammar.highlight_id_for_name("variable"),
680 _ => None,
681 }?;
682
683 let text = if let Some(description) = item
684 .label_details
685 .as_ref()
686 .and_then(|label_details| label_details.description.as_ref())
687 {
688 format!("{} {}", item.label, description)
689 } else if let Some(detail) = &item.detail {
690 format!("{} {}", item.label, detail)
691 } else {
692 item.label.clone()
693 };
694 let filter_range = item
695 .filter_text
696 .as_deref()
697 .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
698 .unwrap_or(0..len);
699 Some(language::CodeLabel {
700 text,
701 runs: vec![(0..len, highlight_id)],
702 filter_range,
703 })
704 }
705
706 async fn initialization_options(
707 self: Arc<Self>,
708 adapter: &Arc<dyn LspAdapterDelegate>,
709 ) -> Result<Option<serde_json::Value>> {
710 let tsdk_path = self.tsdk_path(adapter).await;
711 Ok(Some(json!({
712 "provideFormatter": true,
713 "hostInfo": "zed",
714 "tsserver": {
715 "path": tsdk_path,
716 },
717 "preferences": {
718 "includeInlayParameterNameHints": "all",
719 "includeInlayParameterNameHintsWhenArgumentMatchesName": true,
720 "includeInlayFunctionParameterTypeHints": true,
721 "includeInlayVariableTypeHints": true,
722 "includeInlayVariableTypeHintsWhenTypeMatchesName": true,
723 "includeInlayPropertyDeclarationTypeHints": true,
724 "includeInlayFunctionLikeReturnTypeHints": true,
725 "includeInlayEnumMemberValueHints": true,
726 }
727 })))
728 }
729
730 async fn workspace_configuration(
731 self: Arc<Self>,
732
733 delegate: &Arc<dyn LspAdapterDelegate>,
734 _: Option<Toolchain>,
735 cx: &mut AsyncApp,
736 ) -> Result<Value> {
737 let override_options = cx.update(|cx| {
738 language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
739 .and_then(|s| s.settings.clone())
740 })?;
741 if let Some(options) = override_options {
742 return Ok(options);
743 }
744 Ok(json!({
745 "completions": {
746 "completeFunctionCalls": true
747 }
748 }))
749 }
750
751 fn language_ids(&self) -> HashMap<LanguageName, String> {
752 HashMap::from_iter([
753 (LanguageName::new("TypeScript"), "typescript".into()),
754 (LanguageName::new("JavaScript"), "javascript".into()),
755 (LanguageName::new("TSX"), "typescriptreact".into()),
756 ])
757 }
758}
759
760async fn get_cached_ts_server_binary(
761 container_dir: PathBuf,
762 node: &NodeRuntime,
763) -> Option<LanguageServerBinary> {
764 maybe!(async {
765 let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH);
766 let new_server_path = container_dir.join(TypeScriptLspAdapter::NEW_SERVER_PATH);
767 if new_server_path.exists() {
768 Ok(LanguageServerBinary {
769 path: node.binary_path().await?,
770 env: None,
771 arguments: typescript_server_binary_arguments(&new_server_path),
772 })
773 } else if old_server_path.exists() {
774 Ok(LanguageServerBinary {
775 path: node.binary_path().await?,
776 env: None,
777 arguments: typescript_server_binary_arguments(&old_server_path),
778 })
779 } else {
780 anyhow::bail!("missing executable in directory {container_dir:?}")
781 }
782 })
783 .await
784 .log_err()
785}
786
787pub struct EsLintLspAdapter {
788 node: NodeRuntime,
789}
790
791impl EsLintLspAdapter {
792 const CURRENT_VERSION: &'static str = "2.4.4";
793 const CURRENT_VERSION_TAG_NAME: &'static str = "release/2.4.4";
794
795 #[cfg(not(windows))]
796 const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
797 #[cfg(windows)]
798 const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
799
800 const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
801 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("eslint");
802
803 const FLAT_CONFIG_FILE_NAMES: &'static [&'static str] = &[
804 "eslint.config.js",
805 "eslint.config.mjs",
806 "eslint.config.cjs",
807 "eslint.config.ts",
808 "eslint.config.cts",
809 "eslint.config.mts",
810 ];
811
812 pub fn new(node: NodeRuntime) -> Self {
813 EsLintLspAdapter { node }
814 }
815
816 fn build_destination_path(container_dir: &Path) -> PathBuf {
817 container_dir.join(format!("vscode-eslint-{}", Self::CURRENT_VERSION))
818 }
819}
820
821impl LspInstaller for EsLintLspAdapter {
822 type BinaryVersion = GitHubLspBinaryVersion;
823
824 async fn fetch_latest_server_version(
825 &self,
826 _delegate: &dyn LspAdapterDelegate,
827 _: bool,
828 _: &mut AsyncApp,
829 ) -> Result<GitHubLspBinaryVersion> {
830 let url = build_asset_url(
831 "zed-industries/vscode-eslint",
832 Self::CURRENT_VERSION_TAG_NAME,
833 Self::GITHUB_ASSET_KIND,
834 )?;
835
836 Ok(GitHubLspBinaryVersion {
837 name: Self::CURRENT_VERSION.into(),
838 digest: None,
839 url,
840 })
841 }
842
843 async fn fetch_server_binary(
844 &self,
845 version: GitHubLspBinaryVersion,
846 container_dir: PathBuf,
847 delegate: &dyn LspAdapterDelegate,
848 ) -> Result<LanguageServerBinary> {
849 let destination_path = Self::build_destination_path(&container_dir);
850 let server_path = destination_path.join(Self::SERVER_PATH);
851
852 if fs::metadata(&server_path).await.is_err() {
853 remove_matching(&container_dir, |_| true).await;
854
855 download_server_binary(
856 delegate,
857 &version.url,
858 None,
859 &destination_path,
860 Self::GITHUB_ASSET_KIND,
861 )
862 .await?;
863
864 let mut dir = fs::read_dir(&destination_path).await?;
865 let first = dir.next().await.context("missing first file")??;
866 let repo_root = destination_path.join("vscode-eslint");
867 fs::rename(first.path(), &repo_root).await?;
868
869 #[cfg(target_os = "windows")]
870 {
871 handle_symlink(
872 repo_root.join("$shared"),
873 repo_root.join("client").join("src").join("shared"),
874 )
875 .await?;
876 handle_symlink(
877 repo_root.join("$shared"),
878 repo_root.join("server").join("src").join("shared"),
879 )
880 .await?;
881 }
882
883 self.node
884 .run_npm_subcommand(&repo_root, "install", &[])
885 .await?;
886
887 self.node
888 .run_npm_subcommand(&repo_root, "run-script", &["compile"])
889 .await?;
890 }
891
892 Ok(LanguageServerBinary {
893 path: self.node.binary_path().await?,
894 env: None,
895 arguments: eslint_server_binary_arguments(&server_path),
896 })
897 }
898
899 async fn cached_server_binary(
900 &self,
901 container_dir: PathBuf,
902 _: &dyn LspAdapterDelegate,
903 ) -> Option<LanguageServerBinary> {
904 let server_path =
905 Self::build_destination_path(&container_dir).join(EsLintLspAdapter::SERVER_PATH);
906 Some(LanguageServerBinary {
907 path: self.node.binary_path().await.ok()?,
908 env: None,
909 arguments: eslint_server_binary_arguments(&server_path),
910 })
911 }
912}
913
914#[async_trait(?Send)]
915impl LspAdapter for EsLintLspAdapter {
916 fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
917 Some(vec![
918 CodeActionKind::QUICKFIX,
919 CodeActionKind::new("source.fixAll.eslint"),
920 ])
921 }
922
923 async fn workspace_configuration(
924 self: Arc<Self>,
925 delegate: &Arc<dyn LspAdapterDelegate>,
926 _: Option<Toolchain>,
927 cx: &mut AsyncApp,
928 ) -> Result<Value> {
929 let workspace_root = delegate.worktree_root_path();
930 let use_flat_config = Self::FLAT_CONFIG_FILE_NAMES
931 .iter()
932 .any(|file| workspace_root.join(file).is_file());
933
934 let mut default_workspace_configuration = json!({
935 "validate": "on",
936 "rulesCustomizations": [],
937 "run": "onType",
938 "nodePath": null,
939 "workingDirectory": {
940 "mode": "auto"
941 },
942 "workspaceFolder": {
943 "uri": workspace_root,
944 "name": workspace_root.file_name()
945 .unwrap_or(workspace_root.as_os_str())
946 .to_string_lossy(),
947 },
948 "problems": {},
949 "codeActionOnSave": {
950 // We enable this, but without also configuring code_actions_on_format
951 // in the Zed configuration, it doesn't have an effect.
952 "enable": true,
953 },
954 "codeAction": {
955 "disableRuleComment": {
956 "enable": true,
957 "location": "separateLine",
958 },
959 "showDocumentation": {
960 "enable": true
961 }
962 },
963 "experimental": {
964 "useFlatConfig": use_flat_config,
965 }
966 });
967
968 let override_options = cx.update(|cx| {
969 language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
970 .and_then(|s| s.settings.clone())
971 })?;
972
973 if let Some(override_options) = override_options {
974 merge_json_value_into(override_options, &mut default_workspace_configuration);
975 }
976
977 Ok(json!({
978 "": default_workspace_configuration
979 }))
980 }
981
982 fn name(&self) -> LanguageServerName {
983 Self::SERVER_NAME
984 }
985}
986
987#[cfg(target_os = "windows")]
988async fn handle_symlink(src_dir: PathBuf, dest_dir: PathBuf) -> Result<()> {
989 anyhow::ensure!(
990 fs::metadata(&src_dir).await.is_ok(),
991 "Directory {src_dir:?} is not present"
992 );
993 if fs::metadata(&dest_dir).await.is_ok() {
994 fs::remove_file(&dest_dir).await?;
995 }
996 fs::create_dir_all(&dest_dir).await?;
997 let mut entries = fs::read_dir(&src_dir).await?;
998 while let Some(entry) = entries.try_next().await? {
999 let entry_path = entry.path();
1000 let entry_name = entry.file_name();
1001 let dest_path = dest_dir.join(&entry_name);
1002 fs::copy(&entry_path, &dest_path).await?;
1003 }
1004 Ok(())
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009 use std::path::Path;
1010
1011 use gpui::{AppContext as _, BackgroundExecutor, TestAppContext};
1012 use language::language_settings;
1013 use project::{FakeFs, Project};
1014 use serde_json::json;
1015 use task::TaskTemplates;
1016 use unindent::Unindent;
1017 use util::path;
1018
1019 use crate::typescript::{
1020 PackageJsonData, TypeScriptContextProvider, replace_test_name_parameters,
1021 };
1022
1023 #[gpui::test]
1024 async fn test_outline(cx: &mut TestAppContext) {
1025 let language = crate::language(
1026 "typescript",
1027 tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
1028 );
1029
1030 let text = r#"
1031 function a() {
1032 // local variables are omitted
1033 let a1 = 1;
1034 // all functions are included
1035 async function a2() {}
1036 }
1037 // top-level variables are included
1038 let b: C
1039 function getB() {}
1040 // exported variables are included
1041 export const d = e;
1042 "#
1043 .unindent();
1044
1045 let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
1046 let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
1047 assert_eq!(
1048 outline
1049 .items
1050 .iter()
1051 .map(|item| (item.text.as_str(), item.depth))
1052 .collect::<Vec<_>>(),
1053 &[
1054 ("function a()", 0),
1055 ("async function a2()", 1),
1056 ("let b", 0),
1057 ("function getB()", 0),
1058 ("const d", 0),
1059 ]
1060 );
1061 }
1062
1063 #[gpui::test]
1064 async fn test_generator_function_outline(cx: &mut TestAppContext) {
1065 let language = crate::language("javascript", tree_sitter_typescript::LANGUAGE_TSX.into());
1066
1067 let text = r#"
1068 function normalFunction() {
1069 console.log("normal");
1070 }
1071
1072 function* simpleGenerator() {
1073 yield 1;
1074 yield 2;
1075 }
1076
1077 async function* asyncGenerator() {
1078 yield await Promise.resolve(1);
1079 }
1080
1081 function* generatorWithParams(start, end) {
1082 for (let i = start; i <= end; i++) {
1083 yield i;
1084 }
1085 }
1086
1087 class TestClass {
1088 *methodGenerator() {
1089 yield "method";
1090 }
1091
1092 async *asyncMethodGenerator() {
1093 yield "async method";
1094 }
1095 }
1096 "#
1097 .unindent();
1098
1099 let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
1100 let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
1101 assert_eq!(
1102 outline
1103 .items
1104 .iter()
1105 .map(|item| (item.text.as_str(), item.depth))
1106 .collect::<Vec<_>>(),
1107 &[
1108 ("function normalFunction()", 0),
1109 ("function* simpleGenerator()", 0),
1110 ("async function* asyncGenerator()", 0),
1111 ("function* generatorWithParams( )", 0),
1112 ("class TestClass", 0),
1113 ("*methodGenerator()", 1),
1114 ("async *asyncMethodGenerator()", 1),
1115 ]
1116 );
1117 }
1118
1119 #[gpui::test]
1120 async fn test_package_json_discovery(executor: BackgroundExecutor, cx: &mut TestAppContext) {
1121 cx.update(|cx| {
1122 settings::init(cx);
1123 Project::init_settings(cx);
1124 language_settings::init(cx);
1125 });
1126
1127 let package_json_1 = json!({
1128 "dependencies": {
1129 "mocha": "1.0.0",
1130 "vitest": "1.0.0"
1131 },
1132 "scripts": {
1133 "test": ""
1134 }
1135 })
1136 .to_string();
1137
1138 let package_json_2 = json!({
1139 "devDependencies": {
1140 "vitest": "2.0.0"
1141 },
1142 "scripts": {
1143 "test": ""
1144 }
1145 })
1146 .to_string();
1147
1148 let fs = FakeFs::new(executor);
1149 fs.insert_tree(
1150 path!("/root"),
1151 json!({
1152 "package.json": package_json_1,
1153 "sub": {
1154 "package.json": package_json_2,
1155 "file.js": "",
1156 }
1157 }),
1158 )
1159 .await;
1160
1161 let provider = TypeScriptContextProvider::new(fs.clone());
1162 let package_json_data = cx
1163 .update(|cx| {
1164 provider.combined_package_json_data(
1165 fs.clone(),
1166 path!("/root").as_ref(),
1167 "sub/file1.js".as_ref(),
1168 cx,
1169 )
1170 })
1171 .await
1172 .unwrap();
1173 pretty_assertions::assert_eq!(
1174 package_json_data,
1175 PackageJsonData {
1176 jest_package_path: None,
1177 mocha_package_path: Some(Path::new(path!("/root/package.json")).into()),
1178 vitest_package_path: Some(Path::new(path!("/root/sub/package.json")).into()),
1179 jasmine_package_path: None,
1180 scripts: [
1181 (
1182 Path::new(path!("/root/package.json")).into(),
1183 "test".to_owned()
1184 ),
1185 (
1186 Path::new(path!("/root/sub/package.json")).into(),
1187 "test".to_owned()
1188 )
1189 ]
1190 .into_iter()
1191 .collect(),
1192 package_manager: None,
1193 }
1194 );
1195
1196 let mut task_templates = TaskTemplates::default();
1197 package_json_data.fill_task_templates(&mut task_templates);
1198 let task_templates = task_templates
1199 .0
1200 .into_iter()
1201 .map(|template| (template.label, template.cwd))
1202 .collect::<Vec<_>>();
1203 pretty_assertions::assert_eq!(
1204 task_templates,
1205 [
1206 (
1207 "vitest file test".into(),
1208 Some("$ZED_CUSTOM_TYPESCRIPT_VITEST_PACKAGE_PATH".into()),
1209 ),
1210 (
1211 "vitest test $ZED_SYMBOL".into(),
1212 Some("$ZED_CUSTOM_TYPESCRIPT_VITEST_PACKAGE_PATH".into()),
1213 ),
1214 (
1215 "mocha file test".into(),
1216 Some("$ZED_CUSTOM_TYPESCRIPT_MOCHA_PACKAGE_PATH".into()),
1217 ),
1218 (
1219 "mocha test $ZED_SYMBOL".into(),
1220 Some("$ZED_CUSTOM_TYPESCRIPT_MOCHA_PACKAGE_PATH".into()),
1221 ),
1222 (
1223 "root/package.json > test".into(),
1224 Some(path!("/root").into())
1225 ),
1226 (
1227 "sub/package.json > test".into(),
1228 Some(path!("/root/sub").into())
1229 ),
1230 ]
1231 );
1232 }
1233 #[test]
1234 fn test_escaping_name() {
1235 let cases = [
1236 ("plain test name", "plain test name"),
1237 ("test name with $param_name", "test name with (.+?)"),
1238 ("test name with $nested.param.name", "test name with (.+?)"),
1239 ("test name with $#", "test name with (.+?)"),
1240 ("test name with $##", "test name with (.+?)\\#"),
1241 ("test name with %p", "test name with (.+?)"),
1242 ("test name with %s", "test name with (.+?)"),
1243 ("test name with %d", "test name with (.+?)"),
1244 ("test name with %i", "test name with (.+?)"),
1245 ("test name with %f", "test name with (.+?)"),
1246 ("test name with %j", "test name with (.+?)"),
1247 ("test name with %o", "test name with (.+?)"),
1248 ("test name with %#", "test name with (.+?)"),
1249 ("test name with %$", "test name with (.+?)"),
1250 ("test name with %%", "test name with (.+?)"),
1251 ("test name with %q", "test name with %q"),
1252 (
1253 "test name with regex chars .*+?^${}()|[]\\",
1254 "test name with regex chars \\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\",
1255 ),
1256 (
1257 "test name with multiple $params and %pretty and %b and (.+?)",
1258 "test name with multiple (.+?) and (.+?)retty and %b and \\(\\.\\+\\?\\)",
1259 ),
1260 ];
1261
1262 for (input, expected) in cases {
1263 assert_eq!(replace_test_name_parameters(input), expected);
1264 }
1265 }
1266}