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