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 for (path, script) in &self.scripts {
225 task_templates.0.push(TaskTemplate {
226 label: format!("package.json > {script}",),
227 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
228 args: vec!["run".to_owned(), script.to_owned()],
229 tags: vec!["package-script".into()],
230 cwd: Some(
231 path.parent()
232 .unwrap_or(Path::new(""))
233 .to_string_lossy()
234 .to_string(),
235 ),
236 ..TaskTemplate::default()
237 });
238 }
239 }
240}
241
242impl TypeScriptContextProvider {
243 pub fn new() -> Self {
244 Self {
245 last_package_json: PackageJsonContents::default(),
246 }
247 }
248
249 fn combined_package_json_data(
250 &self,
251 fs: Arc<dyn Fs>,
252 worktree_root: &Path,
253 file_relative_path: &Path,
254 cx: &App,
255 ) -> Task<anyhow::Result<PackageJsonData>> {
256 let new_json_data = file_relative_path
257 .ancestors()
258 .map(|path| worktree_root.join(path))
259 .map(|parent_path| {
260 self.package_json_data(&parent_path, self.last_package_json.clone(), fs.clone(), cx)
261 })
262 .collect::<Vec<_>>();
263
264 cx.background_spawn(async move {
265 let mut package_json_data = PackageJsonData::default();
266 for new_data in join_all(new_json_data).await.into_iter().flatten() {
267 package_json_data.merge(new_data);
268 }
269 Ok(package_json_data)
270 })
271 }
272
273 fn package_json_data(
274 &self,
275 directory_path: &Path,
276 existing_package_json: PackageJsonContents,
277 fs: Arc<dyn Fs>,
278 cx: &App,
279 ) -> Task<anyhow::Result<PackageJsonData>> {
280 let package_json_path = directory_path.join("package.json");
281 let metadata_check_fs = fs.clone();
282 cx.background_spawn(async move {
283 let metadata = metadata_check_fs
284 .metadata(&package_json_path)
285 .await
286 .with_context(|| format!("getting metadata for {package_json_path:?}"))?
287 .with_context(|| format!("missing FS metadata for {package_json_path:?}"))?;
288 let mtime = DateTime::<Local>::from(metadata.mtime.timestamp_for_user());
289 let existing_data = {
290 let contents = existing_package_json.0.read().await;
291 contents
292 .get(&package_json_path)
293 .filter(|package_json| package_json.mtime == mtime)
294 .map(|package_json| package_json.data.clone())
295 };
296 match existing_data {
297 Some(existing_data) => Ok(existing_data),
298 None => {
299 let package_json_string =
300 fs.load(&package_json_path).await.with_context(|| {
301 format!("loading package.json from {package_json_path:?}")
302 })?;
303 let package_json: HashMap<String, serde_json_lenient::Value> =
304 serde_json_lenient::from_str(&package_json_string).with_context(|| {
305 format!("parsing package.json from {package_json_path:?}")
306 })?;
307 let new_data =
308 PackageJsonData::new(package_json_path.as_path().into(), package_json);
309 {
310 let mut contents = existing_package_json.0.write().await;
311 contents.insert(
312 package_json_path,
313 PackageJson {
314 mtime,
315 data: new_data.clone(),
316 },
317 );
318 }
319 Ok(new_data)
320 }
321 }
322 })
323 }
324}
325
326async fn detect_package_manager(
327 worktree_root: PathBuf,
328 fs: Arc<dyn Fs>,
329 package_json_data: Option<PackageJsonData>,
330) -> &'static str {
331 if let Some(package_json_data) = package_json_data {
332 if let Some(package_manager) = package_json_data.package_manager {
333 return package_manager;
334 }
335 }
336 if fs.is_file(&worktree_root.join("pnpm-lock.yaml")).await {
337 return "pnpm";
338 }
339 if fs.is_file(&worktree_root.join("yarn.lock")).await {
340 return "yarn";
341 }
342 "npm"
343}
344
345impl ContextProvider for TypeScriptContextProvider {
346 fn associated_tasks(
347 &self,
348 fs: Arc<dyn Fs>,
349 file: Option<Arc<dyn File>>,
350 cx: &App,
351 ) -> Task<Option<TaskTemplates>> {
352 let Some(file) = project::File::from_dyn(file.as_ref()).cloned() else {
353 return Task::ready(None);
354 };
355 let Some(worktree_root) = file.worktree.read(cx).root_dir() else {
356 return Task::ready(None);
357 };
358 let file_relative_path = file.path().clone();
359 let package_json_data =
360 self.combined_package_json_data(fs.clone(), &worktree_root, &file_relative_path, cx);
361
362 cx.background_spawn(async move {
363 let mut task_templates = TaskTemplates(Vec::new());
364 task_templates.0.push(TaskTemplate {
365 label: format!(
366 "execute selection {}",
367 VariableName::SelectedText.template_value()
368 ),
369 command: "node".to_owned(),
370 args: vec![
371 "-e".to_owned(),
372 format!("\"{}\"", VariableName::SelectedText.template_value()),
373 ],
374 ..TaskTemplate::default()
375 });
376
377 match package_json_data.await {
378 Ok(package_json) => {
379 package_json.fill_task_templates(&mut task_templates);
380 }
381 Err(e) => {
382 log::error!(
383 "Failed to read package.json for worktree {file_relative_path:?}: {e:#}"
384 );
385 }
386 }
387
388 Some(task_templates)
389 })
390 }
391
392 fn build_context(
393 &self,
394 current_vars: &task::TaskVariables,
395 location: ContextLocation<'_>,
396 _project_env: Option<HashMap<String, String>>,
397 _toolchains: Arc<dyn LanguageToolchainStore>,
398 cx: &mut App,
399 ) -> Task<Result<task::TaskVariables>> {
400 let mut vars = task::TaskVariables::default();
401
402 if let Some(symbol) = current_vars.get(&VariableName::Symbol) {
403 vars.insert(
404 TYPESCRIPT_JEST_TEST_NAME_VARIABLE,
405 replace_test_name_parameters(symbol),
406 );
407 vars.insert(
408 TYPESCRIPT_VITEST_TEST_NAME_VARIABLE,
409 replace_test_name_parameters(symbol),
410 );
411 }
412 let file_path = location
413 .file_location
414 .buffer
415 .read(cx)
416 .file()
417 .map(|file| file.path());
418
419 let args = location.worktree_root.zip(location.fs).zip(file_path).map(
420 |((worktree_root, fs), file_path)| {
421 (
422 self.combined_package_json_data(fs.clone(), &worktree_root, file_path, cx),
423 worktree_root,
424 fs,
425 )
426 },
427 );
428 cx.background_spawn(async move {
429 if let Some((task, worktree_root, fs)) = args {
430 let package_json_data = task.await.log_err();
431 vars.insert(
432 TYPESCRIPT_RUNNER_VARIABLE,
433 detect_package_manager(worktree_root, fs, package_json_data.clone())
434 .await
435 .to_owned(),
436 );
437
438 if let Some(package_json_data) = package_json_data {
439 if let Some(path) = package_json_data.jest_package_path {
440 vars.insert(
441 TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE,
442 path.parent()
443 .unwrap_or(Path::new(""))
444 .to_string_lossy()
445 .to_string(),
446 );
447 }
448
449 if let Some(path) = package_json_data.mocha_package_path {
450 vars.insert(
451 TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE,
452 path.parent()
453 .unwrap_or(Path::new(""))
454 .to_string_lossy()
455 .to_string(),
456 );
457 }
458
459 if let Some(path) = package_json_data.vitest_package_path {
460 vars.insert(
461 TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE,
462 path.parent()
463 .unwrap_or(Path::new(""))
464 .to_string_lossy()
465 .to_string(),
466 );
467 }
468
469 if let Some(path) = package_json_data.jasmine_package_path {
470 vars.insert(
471 TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE,
472 path.parent()
473 .unwrap_or(Path::new(""))
474 .to_string_lossy()
475 .to_string(),
476 );
477 }
478 }
479 }
480 Ok(vars)
481 })
482 }
483}
484
485fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
486 vec![server_path.into(), "--stdio".into()]
487}
488
489fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
490 vec![
491 "--max-old-space-size=8192".into(),
492 server_path.into(),
493 "--stdio".into(),
494 ]
495}
496
497fn replace_test_name_parameters(test_name: &str) -> String {
498 let pattern = regex::Regex::new(r"(%|\$)[0-9a-zA-Z]+").unwrap();
499
500 pattern.replace_all(test_name, "(.+?)").to_string()
501}
502
503pub struct TypeScriptLspAdapter {
504 node: NodeRuntime,
505}
506
507impl TypeScriptLspAdapter {
508 const OLD_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js";
509 const NEW_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs";
510 const SERVER_NAME: LanguageServerName =
511 LanguageServerName::new_static("typescript-language-server");
512 const PACKAGE_NAME: &str = "typescript";
513 pub fn new(node: NodeRuntime) -> Self {
514 TypeScriptLspAdapter { node }
515 }
516 async fn tsdk_path(fs: &dyn Fs, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {
517 let is_yarn = adapter
518 .read_text_file(PathBuf::from(".yarn/sdks/typescript/lib/typescript.js"))
519 .await
520 .is_ok();
521
522 let tsdk_path = if is_yarn {
523 ".yarn/sdks/typescript/lib"
524 } else {
525 "node_modules/typescript/lib"
526 };
527
528 if fs
529 .is_dir(&adapter.worktree_root_path().join(tsdk_path))
530 .await
531 {
532 Some(tsdk_path)
533 } else {
534 None
535 }
536 }
537}
538
539struct TypeScriptVersions {
540 typescript_version: String,
541 server_version: String,
542}
543
544#[async_trait(?Send)]
545impl LspAdapter for TypeScriptLspAdapter {
546 fn name(&self) -> LanguageServerName {
547 Self::SERVER_NAME.clone()
548 }
549
550 async fn fetch_latest_server_version(
551 &self,
552 _: &dyn LspAdapterDelegate,
553 ) -> Result<Box<dyn 'static + Send + Any>> {
554 Ok(Box::new(TypeScriptVersions {
555 typescript_version: self.node.npm_package_latest_version("typescript").await?,
556 server_version: self
557 .node
558 .npm_package_latest_version("typescript-language-server")
559 .await?,
560 }) as Box<_>)
561 }
562
563 async fn check_if_version_installed(
564 &self,
565 version: &(dyn 'static + Send + Any),
566 container_dir: &PathBuf,
567 _: &dyn LspAdapterDelegate,
568 ) -> Option<LanguageServerBinary> {
569 let version = version.downcast_ref::<TypeScriptVersions>().unwrap();
570 let server_path = container_dir.join(Self::NEW_SERVER_PATH);
571
572 let should_install_language_server = self
573 .node
574 .should_install_npm_package(
575 Self::PACKAGE_NAME,
576 &server_path,
577 &container_dir,
578 version.typescript_version.as_str(),
579 )
580 .await;
581
582 if should_install_language_server {
583 None
584 } else {
585 Some(LanguageServerBinary {
586 path: self.node.binary_path().await.ok()?,
587 env: None,
588 arguments: typescript_server_binary_arguments(&server_path),
589 })
590 }
591 }
592
593 async fn fetch_server_binary(
594 &self,
595 latest_version: Box<dyn 'static + Send + Any>,
596 container_dir: PathBuf,
597 _: &dyn LspAdapterDelegate,
598 ) -> Result<LanguageServerBinary> {
599 let latest_version = latest_version.downcast::<TypeScriptVersions>().unwrap();
600 let server_path = container_dir.join(Self::NEW_SERVER_PATH);
601
602 self.node
603 .npm_install_packages(
604 &container_dir,
605 &[
606 (
607 Self::PACKAGE_NAME,
608 latest_version.typescript_version.as_str(),
609 ),
610 (
611 "typescript-language-server",
612 latest_version.server_version.as_str(),
613 ),
614 ],
615 )
616 .await?;
617
618 Ok(LanguageServerBinary {
619 path: self.node.binary_path().await?,
620 env: None,
621 arguments: typescript_server_binary_arguments(&server_path),
622 })
623 }
624
625 async fn cached_server_binary(
626 &self,
627 container_dir: PathBuf,
628 _: &dyn LspAdapterDelegate,
629 ) -> Option<LanguageServerBinary> {
630 get_cached_ts_server_binary(container_dir, &self.node).await
631 }
632
633 fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
634 Some(vec![
635 CodeActionKind::QUICKFIX,
636 CodeActionKind::REFACTOR,
637 CodeActionKind::REFACTOR_EXTRACT,
638 CodeActionKind::SOURCE,
639 ])
640 }
641
642 async fn label_for_completion(
643 &self,
644 item: &lsp::CompletionItem,
645 language: &Arc<language::Language>,
646 ) -> Option<language::CodeLabel> {
647 use lsp::CompletionItemKind as Kind;
648 let len = item.label.len();
649 let grammar = language.grammar()?;
650 let highlight_id = match item.kind? {
651 Kind::CLASS | Kind::INTERFACE | Kind::ENUM => grammar.highlight_id_for_name("type"),
652 Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
653 Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
654 Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
655 Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
656 Kind::VARIABLE => grammar.highlight_id_for_name("variable"),
657 _ => None,
658 }?;
659
660 let text = if let Some(description) = item
661 .label_details
662 .as_ref()
663 .and_then(|label_details| label_details.description.as_ref())
664 {
665 format!("{} {}", item.label, description)
666 } else if let Some(detail) = &item.detail {
667 format!("{} {}", item.label, detail)
668 } else {
669 item.label.clone()
670 };
671 let filter_range = item
672 .filter_text
673 .as_deref()
674 .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
675 .unwrap_or(0..len);
676 Some(language::CodeLabel {
677 text,
678 runs: vec![(0..len, highlight_id)],
679 filter_range,
680 })
681 }
682
683 async fn initialization_options(
684 self: Arc<Self>,
685 fs: &dyn Fs,
686 adapter: &Arc<dyn LspAdapterDelegate>,
687 ) -> Result<Option<serde_json::Value>> {
688 let tsdk_path = Self::tsdk_path(fs, adapter).await;
689 Ok(Some(json!({
690 "provideFormatter": true,
691 "hostInfo": "zed",
692 "tsserver": {
693 "path": tsdk_path,
694 },
695 "preferences": {
696 "includeInlayParameterNameHints": "all",
697 "includeInlayParameterNameHintsWhenArgumentMatchesName": true,
698 "includeInlayFunctionParameterTypeHints": true,
699 "includeInlayVariableTypeHints": true,
700 "includeInlayVariableTypeHintsWhenTypeMatchesName": true,
701 "includeInlayPropertyDeclarationTypeHints": true,
702 "includeInlayFunctionLikeReturnTypeHints": true,
703 "includeInlayEnumMemberValueHints": true,
704 }
705 })))
706 }
707
708 async fn workspace_configuration(
709 self: Arc<Self>,
710 _: &dyn Fs,
711 delegate: &Arc<dyn LspAdapterDelegate>,
712 _: Arc<dyn LanguageToolchainStore>,
713 cx: &mut AsyncApp,
714 ) -> Result<Value> {
715 let override_options = cx.update(|cx| {
716 language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
717 .and_then(|s| s.settings.clone())
718 })?;
719 if let Some(options) = override_options {
720 return Ok(options);
721 }
722 Ok(json!({
723 "completions": {
724 "completeFunctionCalls": true
725 }
726 }))
727 }
728
729 fn language_ids(&self) -> HashMap<String, String> {
730 HashMap::from_iter([
731 ("TypeScript".into(), "typescript".into()),
732 ("JavaScript".into(), "javascript".into()),
733 ("TSX".into(), "typescriptreact".into()),
734 ])
735 }
736}
737
738async fn get_cached_ts_server_binary(
739 container_dir: PathBuf,
740 node: &NodeRuntime,
741) -> Option<LanguageServerBinary> {
742 maybe!(async {
743 let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH);
744 let new_server_path = container_dir.join(TypeScriptLspAdapter::NEW_SERVER_PATH);
745 if new_server_path.exists() {
746 Ok(LanguageServerBinary {
747 path: node.binary_path().await?,
748 env: None,
749 arguments: typescript_server_binary_arguments(&new_server_path),
750 })
751 } else if old_server_path.exists() {
752 Ok(LanguageServerBinary {
753 path: node.binary_path().await?,
754 env: None,
755 arguments: typescript_server_binary_arguments(&old_server_path),
756 })
757 } else {
758 anyhow::bail!("missing executable in directory {container_dir:?}")
759 }
760 })
761 .await
762 .log_err()
763}
764
765pub struct EsLintLspAdapter {
766 node: NodeRuntime,
767}
768
769impl EsLintLspAdapter {
770 const CURRENT_VERSION: &'static str = "2.4.4";
771 const CURRENT_VERSION_TAG_NAME: &'static str = "release/2.4.4";
772
773 #[cfg(not(windows))]
774 const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
775 #[cfg(windows)]
776 const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
777
778 const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
779 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("eslint");
780
781 const FLAT_CONFIG_FILE_NAMES: &'static [&'static str] = &[
782 "eslint.config.js",
783 "eslint.config.mjs",
784 "eslint.config.cjs",
785 "eslint.config.ts",
786 "eslint.config.cts",
787 "eslint.config.mts",
788 ];
789
790 pub fn new(node: NodeRuntime) -> Self {
791 EsLintLspAdapter { node }
792 }
793
794 fn build_destination_path(container_dir: &Path) -> PathBuf {
795 container_dir.join(format!("vscode-eslint-{}", Self::CURRENT_VERSION))
796 }
797}
798
799#[async_trait(?Send)]
800impl LspAdapter for EsLintLspAdapter {
801 fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
802 Some(vec![
803 CodeActionKind::QUICKFIX,
804 CodeActionKind::new("source.fixAll.eslint"),
805 ])
806 }
807
808 async fn workspace_configuration(
809 self: Arc<Self>,
810 _: &dyn Fs,
811 delegate: &Arc<dyn LspAdapterDelegate>,
812 _: Arc<dyn LanguageToolchainStore>,
813 cx: &mut AsyncApp,
814 ) -> Result<Value> {
815 let workspace_root = delegate.worktree_root_path();
816 let use_flat_config = Self::FLAT_CONFIG_FILE_NAMES
817 .iter()
818 .any(|file| workspace_root.join(file).is_file());
819
820 let mut default_workspace_configuration = json!({
821 "validate": "on",
822 "rulesCustomizations": [],
823 "run": "onType",
824 "nodePath": null,
825 "workingDirectory": {
826 "mode": "auto"
827 },
828 "workspaceFolder": {
829 "uri": workspace_root,
830 "name": workspace_root.file_name()
831 .unwrap_or(workspace_root.as_os_str())
832 .to_string_lossy(),
833 },
834 "problems": {},
835 "codeActionOnSave": {
836 // We enable this, but without also configuring code_actions_on_format
837 // in the Zed configuration, it doesn't have an effect.
838 "enable": true,
839 },
840 "codeAction": {
841 "disableRuleComment": {
842 "enable": true,
843 "location": "separateLine",
844 },
845 "showDocumentation": {
846 "enable": true
847 }
848 },
849 "experimental": {
850 "useFlatConfig": use_flat_config,
851 },
852 });
853
854 let override_options = cx.update(|cx| {
855 language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
856 .and_then(|s| s.settings.clone())
857 })?;
858
859 if let Some(override_options) = override_options {
860 merge_json_value_into(override_options, &mut default_workspace_configuration);
861 }
862
863 Ok(json!({
864 "": default_workspace_configuration
865 }))
866 }
867
868 fn name(&self) -> LanguageServerName {
869 Self::SERVER_NAME.clone()
870 }
871
872 async fn fetch_latest_server_version(
873 &self,
874 _delegate: &dyn LspAdapterDelegate,
875 ) -> Result<Box<dyn 'static + Send + Any>> {
876 let url = build_asset_url(
877 "zed-industries/vscode-eslint",
878 Self::CURRENT_VERSION_TAG_NAME,
879 Self::GITHUB_ASSET_KIND,
880 )?;
881
882 Ok(Box::new(GitHubLspBinaryVersion {
883 name: Self::CURRENT_VERSION.into(),
884 url,
885 }))
886 }
887
888 async fn fetch_server_binary(
889 &self,
890 version: Box<dyn 'static + Send + Any>,
891 container_dir: PathBuf,
892 delegate: &dyn LspAdapterDelegate,
893 ) -> Result<LanguageServerBinary> {
894 let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
895 let destination_path = Self::build_destination_path(&container_dir);
896 let server_path = destination_path.join(Self::SERVER_PATH);
897
898 if fs::metadata(&server_path).await.is_err() {
899 remove_matching(&container_dir, |entry| entry != destination_path).await;
900
901 let mut response = delegate
902 .http_client()
903 .get(&version.url, Default::default(), true)
904 .await
905 .context("downloading release")?;
906 match Self::GITHUB_ASSET_KIND {
907 AssetKind::TarGz => {
908 let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
909 let archive = Archive::new(decompressed_bytes);
910 archive.unpack(&destination_path).await.with_context(|| {
911 format!("extracting {} to {:?}", version.url, destination_path)
912 })?;
913 }
914 AssetKind::Gz => {
915 let mut decompressed_bytes =
916 GzipDecoder::new(BufReader::new(response.body_mut()));
917 let mut file =
918 fs::File::create(&destination_path).await.with_context(|| {
919 format!(
920 "creating a file {:?} for a download from {}",
921 destination_path, version.url,
922 )
923 })?;
924 futures::io::copy(&mut decompressed_bytes, &mut file)
925 .await
926 .with_context(|| {
927 format!("extracting {} to {:?}", version.url, destination_path)
928 })?;
929 }
930 AssetKind::Zip => {
931 extract_zip(&destination_path, response.body_mut())
932 .await
933 .with_context(|| {
934 format!("unzipping {} to {:?}", version.url, destination_path)
935 })?;
936 }
937 }
938
939 let mut dir = fs::read_dir(&destination_path).await?;
940 let first = dir.next().await.context("missing first file")??;
941 let repo_root = destination_path.join("vscode-eslint");
942 fs::rename(first.path(), &repo_root).await?;
943
944 #[cfg(target_os = "windows")]
945 {
946 handle_symlink(
947 repo_root.join("$shared"),
948 repo_root.join("client").join("src").join("shared"),
949 )
950 .await?;
951 handle_symlink(
952 repo_root.join("$shared"),
953 repo_root.join("server").join("src").join("shared"),
954 )
955 .await?;
956 }
957
958 self.node
959 .run_npm_subcommand(&repo_root, "install", &[])
960 .await?;
961
962 self.node
963 .run_npm_subcommand(&repo_root, "run-script", &["compile"])
964 .await?;
965 }
966
967 Ok(LanguageServerBinary {
968 path: self.node.binary_path().await?,
969 env: None,
970 arguments: eslint_server_binary_arguments(&server_path),
971 })
972 }
973
974 async fn cached_server_binary(
975 &self,
976 container_dir: PathBuf,
977 _: &dyn LspAdapterDelegate,
978 ) -> Option<LanguageServerBinary> {
979 let server_path =
980 Self::build_destination_path(&container_dir).join(EsLintLspAdapter::SERVER_PATH);
981 Some(LanguageServerBinary {
982 path: self.node.binary_path().await.ok()?,
983 env: None,
984 arguments: eslint_server_binary_arguments(&server_path),
985 })
986 }
987}
988
989#[cfg(target_os = "windows")]
990async fn handle_symlink(src_dir: PathBuf, dest_dir: PathBuf) -> Result<()> {
991 anyhow::ensure!(
992 fs::metadata(&src_dir).await.is_ok(),
993 "Directory {src_dir:?} is not present"
994 );
995 if fs::metadata(&dest_dir).await.is_ok() {
996 fs::remove_file(&dest_dir).await?;
997 }
998 fs::create_dir_all(&dest_dir).await?;
999 let mut entries = fs::read_dir(&src_dir).await?;
1000 while let Some(entry) = entries.try_next().await? {
1001 let entry_path = entry.path();
1002 let entry_name = entry.file_name();
1003 let dest_path = dest_dir.join(&entry_name);
1004 fs::copy(&entry_path, &dest_path).await?;
1005 }
1006 Ok(())
1007}
1008
1009#[cfg(test)]
1010mod tests {
1011 use std::path::Path;
1012
1013 use gpui::{AppContext as _, BackgroundExecutor, TestAppContext};
1014 use language::language_settings;
1015 use project::{FakeFs, Project};
1016 use serde_json::json;
1017 use unindent::Unindent;
1018 use util::path;
1019
1020 use crate::typescript::{PackageJsonData, TypeScriptContextProvider};
1021
1022 #[gpui::test]
1023 async fn test_outline(cx: &mut TestAppContext) {
1024 let language = crate::language(
1025 "typescript",
1026 tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
1027 );
1028
1029 let text = r#"
1030 function a() {
1031 // local variables are omitted
1032 let a1 = 1;
1033 // all functions are included
1034 async function a2() {}
1035 }
1036 // top-level variables are included
1037 let b: C
1038 function getB() {}
1039 // exported variables are included
1040 export const d = e;
1041 "#
1042 .unindent();
1043
1044 let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
1045 let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None).unwrap());
1046 assert_eq!(
1047 outline
1048 .items
1049 .iter()
1050 .map(|item| (item.text.as_str(), item.depth))
1051 .collect::<Vec<_>>(),
1052 &[
1053 ("function a()", 0),
1054 ("async function a2()", 1),
1055 ("let b", 0),
1056 ("function getB()", 0),
1057 ("const d", 0),
1058 ]
1059 );
1060 }
1061
1062 #[gpui::test]
1063 async fn test_package_json_discovery(executor: BackgroundExecutor, cx: &mut TestAppContext) {
1064 cx.update(|cx| {
1065 settings::init(cx);
1066 Project::init_settings(cx);
1067 language_settings::init(cx);
1068 });
1069
1070 let package_json_1 = json!({
1071 "dependencies": {
1072 "mocha": "1.0.0",
1073 "vitest": "1.0.0"
1074 },
1075 "scripts": {
1076 "test": ""
1077 }
1078 })
1079 .to_string();
1080
1081 let package_json_2 = json!({
1082 "devDependencies": {
1083 "vitest": "2.0.0"
1084 },
1085 "scripts": {
1086 "test": ""
1087 }
1088 })
1089 .to_string();
1090
1091 let fs = FakeFs::new(executor);
1092 fs.insert_tree(
1093 path!("/root"),
1094 json!({
1095 "package.json": package_json_1,
1096 "sub": {
1097 "package.json": package_json_2,
1098 "file.js": "",
1099 }
1100 }),
1101 )
1102 .await;
1103
1104 let provider = TypeScriptContextProvider::new();
1105 let package_json_data = cx
1106 .update(|cx| {
1107 provider.combined_package_json_data(
1108 fs.clone(),
1109 path!("/root").as_ref(),
1110 "sub/file1.js".as_ref(),
1111 cx,
1112 )
1113 })
1114 .await
1115 .unwrap();
1116 pretty_assertions::assert_eq!(
1117 package_json_data,
1118 PackageJsonData {
1119 jest_package_path: None,
1120 mocha_package_path: Some(Path::new(path!("/root/package.json")).into()),
1121 vitest_package_path: Some(Path::new(path!("/root/sub/package.json")).into()),
1122 jasmine_package_path: None,
1123 scripts: [
1124 (
1125 Path::new(path!("/root/package.json")).into(),
1126 "test".to_owned()
1127 ),
1128 (
1129 Path::new(path!("/root/sub/package.json")).into(),
1130 "test".to_owned()
1131 )
1132 ]
1133 .into_iter()
1134 .collect(),
1135 package_manager: None,
1136 }
1137 );
1138 }
1139}