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