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