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