1use anyhow::{Context as _, Result, anyhow};
2use async_compression::futures::bufread::GzipDecoder;
3use async_tar::Archive;
4use async_trait::async_trait;
5use collections::HashMap;
6use gpui::AsyncApp;
7use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};
8use language::{LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
9use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
10use node_runtime::NodeRuntime;
11use project::ContextProviderWithTasks;
12use project::{Fs, lsp_store::language_server_settings};
13use serde_json::{Value, json};
14use smol::{fs, io::BufReader, stream::StreamExt};
15use std::{
16 any::Any,
17 ffi::OsString,
18 path::{Path, PathBuf},
19 sync::Arc,
20};
21use task::{TaskTemplate, TaskTemplates, VariableName};
22use util::{ResultExt, fs::remove_matching, maybe};
23
24pub(super) fn typescript_task_context() -> ContextProviderWithTasks {
25 ContextProviderWithTasks::new(TaskTemplates(vec![
26 TaskTemplate {
27 label: "jest file test".to_owned(),
28 command: "npx jest".to_owned(),
29 args: vec![VariableName::File.template_value()],
30 ..TaskTemplate::default()
31 },
32 TaskTemplate {
33 label: "jest test $ZED_SYMBOL".to_owned(),
34 command: "npx jest".to_owned(),
35 args: vec![
36 "--testNamePattern".into(),
37 format!("\"{}\"", VariableName::Symbol.template_value()),
38 VariableName::File.template_value(),
39 ],
40 tags: vec!["ts-test".into(), "js-test".into(), "tsx-test".into()],
41 ..TaskTemplate::default()
42 },
43 TaskTemplate {
44 label: "execute selection $ZED_SELECTED_TEXT".to_owned(),
45 command: "node".to_owned(),
46 args: vec![
47 "-e".into(),
48 format!("\"{}\"", VariableName::SelectedText.template_value()),
49 ],
50 ..TaskTemplate::default()
51 },
52 ]))
53}
54
55fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
56 vec![server_path.into(), "--stdio".into()]
57}
58
59fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
60 vec![
61 "--max-old-space-size=8192".into(),
62 server_path.into(),
63 "--stdio".into(),
64 ]
65}
66
67pub struct TypeScriptLspAdapter {
68 node: NodeRuntime,
69}
70
71impl TypeScriptLspAdapter {
72 const OLD_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js";
73 const NEW_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs";
74 const SERVER_NAME: LanguageServerName =
75 LanguageServerName::new_static("typescript-language-server");
76 const PACKAGE_NAME: &str = "typescript";
77 pub fn new(node: NodeRuntime) -> Self {
78 TypeScriptLspAdapter { node }
79 }
80 async fn tsdk_path(fs: &dyn Fs, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {
81 let is_yarn = adapter
82 .read_text_file(PathBuf::from(".yarn/sdks/typescript/lib/typescript.js"))
83 .await
84 .is_ok();
85
86 let tsdk_path = if is_yarn {
87 ".yarn/sdks/typescript/lib"
88 } else {
89 "node_modules/typescript/lib"
90 };
91
92 if fs
93 .is_dir(&adapter.worktree_root_path().join(tsdk_path))
94 .await
95 {
96 Some(tsdk_path)
97 } else {
98 None
99 }
100 }
101}
102
103struct TypeScriptVersions {
104 typescript_version: String,
105 server_version: String,
106}
107
108#[async_trait(?Send)]
109impl LspAdapter for TypeScriptLspAdapter {
110 fn name(&self) -> LanguageServerName {
111 Self::SERVER_NAME.clone()
112 }
113
114 async fn fetch_latest_server_version(
115 &self,
116 _: &dyn LspAdapterDelegate,
117 ) -> Result<Box<dyn 'static + Send + Any>> {
118 Ok(Box::new(TypeScriptVersions {
119 typescript_version: self.node.npm_package_latest_version("typescript").await?,
120 server_version: self
121 .node
122 .npm_package_latest_version("typescript-language-server")
123 .await?,
124 }) as Box<_>)
125 }
126
127 async fn check_if_version_installed(
128 &self,
129 version: &(dyn 'static + Send + Any),
130 container_dir: &PathBuf,
131 _: &dyn LspAdapterDelegate,
132 ) -> Option<LanguageServerBinary> {
133 let version = version.downcast_ref::<TypeScriptVersions>().unwrap();
134 let server_path = container_dir.join(Self::NEW_SERVER_PATH);
135
136 let should_install_language_server = self
137 .node
138 .should_install_npm_package(
139 Self::PACKAGE_NAME,
140 &server_path,
141 &container_dir,
142 version.typescript_version.as_str(),
143 )
144 .await;
145
146 if should_install_language_server {
147 None
148 } else {
149 Some(LanguageServerBinary {
150 path: self.node.binary_path().await.ok()?,
151 env: None,
152 arguments: typescript_server_binary_arguments(&server_path),
153 })
154 }
155 }
156
157 async fn fetch_server_binary(
158 &self,
159 latest_version: Box<dyn 'static + Send + Any>,
160 container_dir: PathBuf,
161 _: &dyn LspAdapterDelegate,
162 ) -> Result<LanguageServerBinary> {
163 let latest_version = latest_version.downcast::<TypeScriptVersions>().unwrap();
164 let server_path = container_dir.join(Self::NEW_SERVER_PATH);
165
166 self.node
167 .npm_install_packages(
168 &container_dir,
169 &[
170 (
171 Self::PACKAGE_NAME,
172 latest_version.typescript_version.as_str(),
173 ),
174 (
175 "typescript-language-server",
176 latest_version.server_version.as_str(),
177 ),
178 ],
179 )
180 .await?;
181
182 Ok(LanguageServerBinary {
183 path: self.node.binary_path().await?,
184 env: None,
185 arguments: typescript_server_binary_arguments(&server_path),
186 })
187 }
188
189 async fn cached_server_binary(
190 &self,
191 container_dir: PathBuf,
192 _: &dyn LspAdapterDelegate,
193 ) -> Option<LanguageServerBinary> {
194 get_cached_ts_server_binary(container_dir, &self.node).await
195 }
196
197 fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
198 Some(vec![
199 CodeActionKind::QUICKFIX,
200 CodeActionKind::REFACTOR,
201 CodeActionKind::REFACTOR_EXTRACT,
202 CodeActionKind::SOURCE,
203 ])
204 }
205
206 async fn label_for_completion(
207 &self,
208 item: &lsp::CompletionItem,
209 language: &Arc<language::Language>,
210 ) -> Option<language::CodeLabel> {
211 use lsp::CompletionItemKind as Kind;
212 let len = item.label.len();
213 let grammar = language.grammar()?;
214 let highlight_id = match item.kind? {
215 Kind::CLASS | Kind::INTERFACE | Kind::ENUM => grammar.highlight_id_for_name("type"),
216 Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
217 Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
218 Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
219 Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
220 Kind::VARIABLE => grammar.highlight_id_for_name("variable"),
221 _ => None,
222 }?;
223
224 let text = if let Some(description) = item
225 .label_details
226 .as_ref()
227 .and_then(|label_details| label_details.description.as_ref())
228 {
229 format!("{} {}", item.label, description)
230 } else if let Some(detail) = &item.detail {
231 format!("{} {}", item.label, detail)
232 } else {
233 item.label.clone()
234 };
235
236 Some(language::CodeLabel {
237 text,
238 runs: vec![(0..len, highlight_id)],
239 filter_range: 0..len,
240 })
241 }
242
243 async fn initialization_options(
244 self: Arc<Self>,
245 fs: &dyn Fs,
246 adapter: &Arc<dyn LspAdapterDelegate>,
247 ) -> Result<Option<serde_json::Value>> {
248 let tsdk_path = Self::tsdk_path(fs, adapter).await;
249 Ok(Some(json!({
250 "provideFormatter": true,
251 "hostInfo": "zed",
252 "tsserver": {
253 "path": tsdk_path,
254 },
255 "preferences": {
256 "includeInlayParameterNameHints": "all",
257 "includeInlayParameterNameHintsWhenArgumentMatchesName": true,
258 "includeInlayFunctionParameterTypeHints": true,
259 "includeInlayVariableTypeHints": true,
260 "includeInlayVariableTypeHintsWhenTypeMatchesName": true,
261 "includeInlayPropertyDeclarationTypeHints": true,
262 "includeInlayFunctionLikeReturnTypeHints": true,
263 "includeInlayEnumMemberValueHints": true,
264 }
265 })))
266 }
267
268 async fn workspace_configuration(
269 self: Arc<Self>,
270 _: &dyn Fs,
271 delegate: &Arc<dyn LspAdapterDelegate>,
272 _: Arc<dyn LanguageToolchainStore>,
273 cx: &mut AsyncApp,
274 ) -> Result<Value> {
275 let override_options = cx.update(|cx| {
276 language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
277 .and_then(|s| s.settings.clone())
278 })?;
279 if let Some(options) = override_options {
280 return Ok(options);
281 }
282 Ok(json!({
283 "completions": {
284 "completeFunctionCalls": true
285 }
286 }))
287 }
288
289 fn language_ids(&self) -> HashMap<String, String> {
290 HashMap::from_iter([
291 ("TypeScript".into(), "typescript".into()),
292 ("JavaScript".into(), "javascript".into()),
293 ("TSX".into(), "typescriptreact".into()),
294 ])
295 }
296}
297
298async fn get_cached_ts_server_binary(
299 container_dir: PathBuf,
300 node: &NodeRuntime,
301) -> Option<LanguageServerBinary> {
302 maybe!(async {
303 let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH);
304 let new_server_path = container_dir.join(TypeScriptLspAdapter::NEW_SERVER_PATH);
305 if new_server_path.exists() {
306 Ok(LanguageServerBinary {
307 path: node.binary_path().await?,
308 env: None,
309 arguments: typescript_server_binary_arguments(&new_server_path),
310 })
311 } else if old_server_path.exists() {
312 Ok(LanguageServerBinary {
313 path: node.binary_path().await?,
314 env: None,
315 arguments: typescript_server_binary_arguments(&old_server_path),
316 })
317 } else {
318 Err(anyhow!(
319 "missing executable in directory {:?}",
320 container_dir
321 ))
322 }
323 })
324 .await
325 .log_err()
326}
327
328pub struct EsLintLspAdapter {
329 node: NodeRuntime,
330}
331
332impl EsLintLspAdapter {
333 const CURRENT_VERSION: &'static str = "2.4.4";
334 const CURRENT_VERSION_TAG_NAME: &'static str = "release/2.4.4";
335
336 #[cfg(not(windows))]
337 const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
338 #[cfg(windows)]
339 const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
340
341 const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
342 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("eslint");
343
344 const FLAT_CONFIG_FILE_NAMES: &'static [&'static str] = &[
345 "eslint.config.js",
346 "eslint.config.mjs",
347 "eslint.config.cjs",
348 "eslint.config.ts",
349 "eslint.config.cts",
350 "eslint.config.mts",
351 ];
352
353 pub fn new(node: NodeRuntime) -> Self {
354 EsLintLspAdapter { node }
355 }
356
357 fn build_destination_path(container_dir: &Path) -> PathBuf {
358 container_dir.join(format!("vscode-eslint-{}", Self::CURRENT_VERSION))
359 }
360}
361
362#[async_trait(?Send)]
363impl LspAdapter for EsLintLspAdapter {
364 fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
365 Some(vec![
366 CodeActionKind::QUICKFIX,
367 CodeActionKind::new("source.fixAll.eslint"),
368 ])
369 }
370
371 async fn workspace_configuration(
372 self: Arc<Self>,
373 _: &dyn Fs,
374 delegate: &Arc<dyn LspAdapterDelegate>,
375 _: Arc<dyn LanguageToolchainStore>,
376 cx: &mut AsyncApp,
377 ) -> Result<Value> {
378 let workspace_root = delegate.worktree_root_path();
379
380 let eslint_user_settings = cx.update(|cx| {
381 language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
382 .and_then(|s| s.settings.clone())
383 .unwrap_or_default()
384 })?;
385
386 let mut code_action_on_save = json!({
387 // We enable this, but without also configuring `code_actions_on_format`
388 // in the Zed configuration, it doesn't have an effect.
389 "enable": true,
390 });
391
392 if let Some(code_action_settings) = eslint_user_settings
393 .get("codeActionOnSave")
394 .and_then(|settings| settings.as_object())
395 {
396 if let Some(enable) = code_action_settings.get("enable") {
397 code_action_on_save["enable"] = enable.clone();
398 }
399 if let Some(mode) = code_action_settings.get("mode") {
400 code_action_on_save["mode"] = mode.clone();
401 }
402 if let Some(rules) = code_action_settings.get("rules") {
403 code_action_on_save["rules"] = rules.clone();
404 }
405 }
406
407 let working_directory = eslint_user_settings
408 .get("workingDirectory")
409 .cloned()
410 .unwrap_or_else(|| json!({"mode": "auto"}));
411
412 let problems = eslint_user_settings
413 .get("problems")
414 .cloned()
415 .unwrap_or_else(|| json!({}));
416
417 let rules_customizations = eslint_user_settings
418 .get("rulesCustomizations")
419 .cloned()
420 .unwrap_or_else(|| json!([]));
421
422 let node_path = eslint_user_settings.get("nodePath").unwrap_or(&Value::Null);
423 let use_flat_config = Self::FLAT_CONFIG_FILE_NAMES
424 .iter()
425 .any(|file| workspace_root.join(file).is_file());
426
427 Ok(json!({
428 "": {
429 "validate": "on",
430 "rulesCustomizations": rules_customizations,
431 "run": "onType",
432 "nodePath": node_path,
433 "workingDirectory": working_directory,
434 "workspaceFolder": {
435 "uri": workspace_root,
436 "name": workspace_root.file_name()
437 .unwrap_or(workspace_root.as_os_str()),
438 },
439 "problems": problems,
440 "codeActionOnSave": code_action_on_save,
441 "codeAction": {
442 "disableRuleComment": {
443 "enable": true,
444 "location": "separateLine",
445 },
446 "showDocumentation": {
447 "enable": true
448 }
449 },
450 "experimental": {
451 "useFlatConfig": use_flat_config,
452 },
453 }
454 }))
455 }
456
457 fn name(&self) -> LanguageServerName {
458 Self::SERVER_NAME.clone()
459 }
460
461 async fn fetch_latest_server_version(
462 &self,
463 _delegate: &dyn LspAdapterDelegate,
464 ) -> Result<Box<dyn 'static + Send + Any>> {
465 let url = build_asset_url(
466 "zed-industries/vscode-eslint",
467 Self::CURRENT_VERSION_TAG_NAME,
468 Self::GITHUB_ASSET_KIND,
469 )?;
470
471 Ok(Box::new(GitHubLspBinaryVersion {
472 name: Self::CURRENT_VERSION.into(),
473 url,
474 }))
475 }
476
477 async fn fetch_server_binary(
478 &self,
479 version: Box<dyn 'static + Send + Any>,
480 container_dir: PathBuf,
481 delegate: &dyn LspAdapterDelegate,
482 ) -> Result<LanguageServerBinary> {
483 let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
484 let destination_path = Self::build_destination_path(&container_dir);
485 let server_path = destination_path.join(Self::SERVER_PATH);
486
487 if fs::metadata(&server_path).await.is_err() {
488 remove_matching(&container_dir, |entry| entry != destination_path).await;
489
490 let mut response = delegate
491 .http_client()
492 .get(&version.url, Default::default(), true)
493 .await
494 .map_err(|err| anyhow!("error downloading release: {}", err))?;
495 match Self::GITHUB_ASSET_KIND {
496 AssetKind::TarGz => {
497 let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
498 let archive = Archive::new(decompressed_bytes);
499 archive.unpack(&destination_path).await.with_context(|| {
500 format!("extracting {} to {:?}", version.url, destination_path)
501 })?;
502 }
503 AssetKind::Gz => {
504 let mut decompressed_bytes =
505 GzipDecoder::new(BufReader::new(response.body_mut()));
506 let mut file =
507 fs::File::create(&destination_path).await.with_context(|| {
508 format!(
509 "creating a file {:?} for a download from {}",
510 destination_path, version.url,
511 )
512 })?;
513 futures::io::copy(&mut decompressed_bytes, &mut file)
514 .await
515 .with_context(|| {
516 format!("extracting {} to {:?}", version.url, destination_path)
517 })?;
518 }
519 AssetKind::Zip => {
520 node_runtime::extract_zip(
521 &destination_path,
522 BufReader::new(response.body_mut()),
523 )
524 .await
525 .with_context(|| {
526 format!("unzipping {} to {:?}", version.url, destination_path)
527 })?;
528 }
529 }
530
531 let mut dir = fs::read_dir(&destination_path).await?;
532 let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
533 let repo_root = destination_path.join("vscode-eslint");
534 fs::rename(first.path(), &repo_root).await?;
535
536 #[cfg(target_os = "windows")]
537 {
538 handle_symlink(
539 repo_root.join("$shared"),
540 repo_root.join("client").join("src").join("shared"),
541 )
542 .await?;
543 handle_symlink(
544 repo_root.join("$shared"),
545 repo_root.join("server").join("src").join("shared"),
546 )
547 .await?;
548 }
549
550 self.node
551 .run_npm_subcommand(&repo_root, "install", &[])
552 .await?;
553
554 self.node
555 .run_npm_subcommand(&repo_root, "run-script", &["compile"])
556 .await?;
557 }
558
559 Ok(LanguageServerBinary {
560 path: self.node.binary_path().await?,
561 env: None,
562 arguments: eslint_server_binary_arguments(&server_path),
563 })
564 }
565
566 async fn cached_server_binary(
567 &self,
568 container_dir: PathBuf,
569 _: &dyn LspAdapterDelegate,
570 ) -> Option<LanguageServerBinary> {
571 let server_path =
572 Self::build_destination_path(&container_dir).join(EsLintLspAdapter::SERVER_PATH);
573 Some(LanguageServerBinary {
574 path: self.node.binary_path().await.ok()?,
575 env: None,
576 arguments: eslint_server_binary_arguments(&server_path),
577 })
578 }
579}
580
581#[cfg(target_os = "windows")]
582async fn handle_symlink(src_dir: PathBuf, dest_dir: PathBuf) -> Result<()> {
583 if fs::metadata(&src_dir).await.is_err() {
584 return Err(anyhow!("Directory {} not present.", src_dir.display()));
585 }
586 if fs::metadata(&dest_dir).await.is_ok() {
587 fs::remove_file(&dest_dir).await?;
588 }
589 fs::create_dir_all(&dest_dir).await?;
590 let mut entries = fs::read_dir(&src_dir).await?;
591 while let Some(entry) = entries.try_next().await? {
592 let entry_path = entry.path();
593 let entry_name = entry.file_name();
594 let dest_path = dest_dir.join(&entry_name);
595 fs::copy(&entry_path, &dest_path).await?;
596 }
597 Ok(())
598}
599
600#[cfg(test)]
601mod tests {
602 use gpui::{AppContext as _, TestAppContext};
603 use unindent::Unindent;
604
605 #[gpui::test]
606 async fn test_outline(cx: &mut TestAppContext) {
607 let language = crate::language(
608 "typescript",
609 tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
610 );
611
612 let text = r#"
613 function a() {
614 // local variables are omitted
615 let a1 = 1;
616 // all functions are included
617 async function a2() {}
618 }
619 // top-level variables are included
620 let b: C
621 function getB() {}
622 // exported variables are included
623 export const d = e;
624 "#
625 .unindent();
626
627 let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
628 let outline = buffer.update(cx, |buffer, _| buffer.snapshot().outline(None).unwrap());
629 assert_eq!(
630 outline
631 .items
632 .iter()
633 .map(|item| (item.text.as_str(), item.depth))
634 .collect::<Vec<_>>(),
635 &[
636 ("function a()", 0),
637 ("async function a2()", 1),
638 ("let b", 0),
639 ("function getB()", 0),
640 ("const d", 0),
641 ]
642 );
643 }
644}