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