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