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