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