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