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