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