1use anyhow::{anyhow, Result};
2use async_compression::futures::bufread::GzipDecoder;
3use async_tar::Archive;
4use async_trait::async_trait;
5use collections::HashMap;
6use gpui::AppContext;
7use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
8use lsp::{CodeActionKind, LanguageServerBinary};
9use node_runtime::NodeRuntime;
10use serde_json::{json, Value};
11use smol::{fs, io::BufReader, stream::StreamExt};
12use std::{
13 any::Any,
14 ffi::OsString,
15 path::{Path, PathBuf},
16 sync::Arc,
17};
18use util::{
19 async_maybe,
20 fs::remove_matching,
21 github::{latest_github_release, GitHubLspBinaryVersion},
22 ResultExt,
23};
24
25fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
26 vec![server_path.into(), "--stdio".into()]
27}
28
29fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
30 vec![server_path.into(), "--stdio".into()]
31}
32
33pub struct TypeScriptLspAdapter {
34 node: Arc<dyn NodeRuntime>,
35}
36
37impl TypeScriptLspAdapter {
38 const OLD_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js";
39 const NEW_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs";
40
41 pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
42 TypeScriptLspAdapter { node }
43 }
44}
45
46struct TypeScriptVersions {
47 typescript_version: String,
48 server_version: String,
49}
50
51#[async_trait]
52impl LspAdapter for TypeScriptLspAdapter {
53 fn name(&self) -> LanguageServerName {
54 LanguageServerName("typescript-language-server".into())
55 }
56
57 fn short_name(&self) -> &'static str {
58 "tsserver"
59 }
60
61 async fn fetch_latest_server_version(
62 &self,
63 _: &dyn LspAdapterDelegate,
64 ) -> Result<Box<dyn 'static + Send + Any>> {
65 Ok(Box::new(TypeScriptVersions {
66 typescript_version: self.node.npm_package_latest_version("typescript").await?,
67 server_version: self
68 .node
69 .npm_package_latest_version("typescript-language-server")
70 .await?,
71 }) as Box<_>)
72 }
73
74 async fn fetch_server_binary(
75 &self,
76 version: Box<dyn 'static + Send + Any>,
77 container_dir: PathBuf,
78 _: &dyn LspAdapterDelegate,
79 ) -> Result<LanguageServerBinary> {
80 let version = version.downcast::<TypeScriptVersions>().unwrap();
81 let server_path = container_dir.join(Self::NEW_SERVER_PATH);
82
83 if fs::metadata(&server_path).await.is_err() {
84 self.node
85 .npm_install_packages(
86 &container_dir,
87 &[
88 ("typescript", version.typescript_version.as_str()),
89 (
90 "typescript-language-server",
91 version.server_version.as_str(),
92 ),
93 ],
94 )
95 .await?;
96 }
97
98 Ok(LanguageServerBinary {
99 path: self.node.binary_path().await?,
100 env: None,
101 arguments: typescript_server_binary_arguments(&server_path),
102 })
103 }
104
105 async fn cached_server_binary(
106 &self,
107 container_dir: PathBuf,
108 _: &dyn LspAdapterDelegate,
109 ) -> Option<LanguageServerBinary> {
110 get_cached_ts_server_binary(container_dir, &*self.node).await
111 }
112
113 async fn installation_test_binary(
114 &self,
115 container_dir: PathBuf,
116 ) -> Option<LanguageServerBinary> {
117 get_cached_ts_server_binary(container_dir, &*self.node).await
118 }
119
120 fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
121 Some(vec![
122 CodeActionKind::QUICKFIX,
123 CodeActionKind::REFACTOR,
124 CodeActionKind::REFACTOR_EXTRACT,
125 CodeActionKind::SOURCE,
126 ])
127 }
128
129 async fn label_for_completion(
130 &self,
131 item: &lsp::CompletionItem,
132 language: &Arc<language::Language>,
133 ) -> Option<language::CodeLabel> {
134 use lsp::CompletionItemKind as Kind;
135 let len = item.label.len();
136 let grammar = language.grammar()?;
137 let highlight_id = match item.kind? {
138 Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"),
139 Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
140 Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
141 Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
142 Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
143 _ => None,
144 }?;
145
146 let text = match &item.detail {
147 Some(detail) => format!("{} {}", item.label, detail),
148 None => item.label.clone(),
149 };
150
151 Some(language::CodeLabel {
152 text,
153 runs: vec![(0..len, highlight_id)],
154 filter_range: 0..len,
155 })
156 }
157
158 fn initialization_options(&self) -> Option<serde_json::Value> {
159 Some(json!({
160 "provideFormatter": true,
161 "tsserver": {
162 "path": "node_modules/typescript/lib",
163 },
164 "preferences": {
165 "includeInlayParameterNameHints": "all",
166 "includeInlayParameterNameHintsWhenArgumentMatchesName": true,
167 "includeInlayFunctionParameterTypeHints": true,
168 "includeInlayVariableTypeHints": true,
169 "includeInlayVariableTypeHintsWhenTypeMatchesName": true,
170 "includeInlayPropertyDeclarationTypeHints": true,
171 "includeInlayFunctionLikeReturnTypeHints": true,
172 "includeInlayEnumMemberValueHints": true,
173 }
174 }))
175 }
176
177 fn language_ids(&self) -> HashMap<String, String> {
178 HashMap::from_iter([
179 ("TypeScript".into(), "typescript".into()),
180 ("JavaScript".into(), "javascript".into()),
181 ("TSX".into(), "typescriptreact".into()),
182 ])
183 }
184}
185
186async fn get_cached_ts_server_binary(
187 container_dir: PathBuf,
188 node: &dyn NodeRuntime,
189) -> Option<LanguageServerBinary> {
190 async_maybe!({
191 let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH);
192 let new_server_path = container_dir.join(TypeScriptLspAdapter::NEW_SERVER_PATH);
193 if new_server_path.exists() {
194 Ok(LanguageServerBinary {
195 path: node.binary_path().await?,
196 env: None,
197 arguments: typescript_server_binary_arguments(&new_server_path),
198 })
199 } else if old_server_path.exists() {
200 Ok(LanguageServerBinary {
201 path: node.binary_path().await?,
202 env: None,
203 arguments: typescript_server_binary_arguments(&old_server_path),
204 })
205 } else {
206 Err(anyhow!(
207 "missing executable in directory {:?}",
208 container_dir
209 ))
210 }
211 })
212 .await
213 .log_err()
214}
215
216pub struct EsLintLspAdapter {
217 node: Arc<dyn NodeRuntime>,
218}
219
220impl EsLintLspAdapter {
221 const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
222
223 pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
224 EsLintLspAdapter { node }
225 }
226}
227
228#[async_trait]
229impl LspAdapter for EsLintLspAdapter {
230 fn workspace_configuration(&self, workspace_root: &Path, _: &mut AppContext) -> Value {
231 json!({
232 "": {
233 "validate": "on",
234 "rulesCustomizations": [],
235 "run": "onType",
236 "nodePath": null,
237 "workingDirectory": {"mode": "auto"},
238 "workspaceFolder": {
239 "uri": workspace_root,
240 "name": workspace_root.file_name()
241 .unwrap_or_else(|| workspace_root.as_os_str()),
242 },
243 "problems": {},
244 "experimental": {
245 "useFlatConfig": workspace_root.join("eslint.config.js").is_file(),
246 },
247 }
248 })
249 }
250
251 fn name(&self) -> LanguageServerName {
252 LanguageServerName("eslint".into())
253 }
254
255 fn short_name(&self) -> &'static str {
256 "eslint"
257 }
258
259 async fn fetch_latest_server_version(
260 &self,
261 delegate: &dyn LspAdapterDelegate,
262 ) -> Result<Box<dyn 'static + Send + Any>> {
263 // At the time of writing the latest vscode-eslint release was released in 2020 and requires
264 // special custom LSP protocol extensions be handled to fully initialize. Download the latest
265 // prerelease instead to sidestep this issue
266 let release = latest_github_release(
267 "microsoft/vscode-eslint",
268 false,
269 true,
270 delegate.http_client(),
271 )
272 .await?;
273 Ok(Box::new(GitHubLspBinaryVersion {
274 name: release.tag_name,
275 url: release.tarball_url,
276 }))
277 }
278
279 async fn fetch_server_binary(
280 &self,
281 version: Box<dyn 'static + Send + Any>,
282 container_dir: PathBuf,
283 delegate: &dyn LspAdapterDelegate,
284 ) -> Result<LanguageServerBinary> {
285 let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
286 let destination_path = container_dir.join(format!("vscode-eslint-{}", version.name));
287 let server_path = destination_path.join(Self::SERVER_PATH);
288
289 if fs::metadata(&server_path).await.is_err() {
290 remove_matching(&container_dir, |entry| entry != destination_path).await;
291
292 let mut response = delegate
293 .http_client()
294 .get(&version.url, Default::default(), true)
295 .await
296 .map_err(|err| anyhow!("error downloading release: {}", err))?;
297 let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
298 let archive = Archive::new(decompressed_bytes);
299 archive.unpack(&destination_path).await?;
300
301 let mut dir = fs::read_dir(&destination_path).await?;
302 let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
303 let repo_root = destination_path.join("vscode-eslint");
304 fs::rename(first.path(), &repo_root).await?;
305
306 self.node
307 .run_npm_subcommand(Some(&repo_root), "install", &[])
308 .await?;
309
310 self.node
311 .run_npm_subcommand(Some(&repo_root), "run-script", &["compile"])
312 .await?;
313 }
314
315 Ok(LanguageServerBinary {
316 path: self.node.binary_path().await?,
317 env: None,
318 arguments: eslint_server_binary_arguments(&server_path),
319 })
320 }
321
322 async fn cached_server_binary(
323 &self,
324 container_dir: PathBuf,
325 _: &dyn LspAdapterDelegate,
326 ) -> Option<LanguageServerBinary> {
327 get_cached_eslint_server_binary(container_dir, &*self.node).await
328 }
329
330 async fn installation_test_binary(
331 &self,
332 container_dir: PathBuf,
333 ) -> Option<LanguageServerBinary> {
334 get_cached_eslint_server_binary(container_dir, &*self.node).await
335 }
336
337 async fn label_for_completion(
338 &self,
339 _item: &lsp::CompletionItem,
340 _language: &Arc<language::Language>,
341 ) -> Option<language::CodeLabel> {
342 None
343 }
344
345 fn initialization_options(&self) -> Option<serde_json::Value> {
346 None
347 }
348}
349
350async fn get_cached_eslint_server_binary(
351 container_dir: PathBuf,
352 node: &dyn NodeRuntime,
353) -> Option<LanguageServerBinary> {
354 async_maybe!({
355 // This is unfortunate but we don't know what the version is to build a path directly
356 let mut dir = fs::read_dir(&container_dir).await?;
357 let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
358 if !first.file_type().await?.is_dir() {
359 return Err(anyhow!("First entry is not a directory"));
360 }
361 let server_path = first.path().join(EsLintLspAdapter::SERVER_PATH);
362
363 Ok(LanguageServerBinary {
364 path: node.binary_path().await?,
365 env: None,
366 arguments: eslint_server_binary_arguments(&server_path),
367 })
368 })
369 .await
370 .log_err()
371}
372
373#[cfg(test)]
374mod tests {
375 use gpui::{Context, TestAppContext};
376 use text::BufferId;
377 use unindent::Unindent;
378
379 #[gpui::test]
380 async fn test_outline(cx: &mut TestAppContext) {
381 let language = crate::language(
382 "typescript",
383 tree_sitter_typescript::language_typescript(),
384 None,
385 )
386 .await;
387
388 let text = r#"
389 function a() {
390 // local variables are omitted
391 let a1 = 1;
392 // all functions are included
393 async function a2() {}
394 }
395 // top-level variables are included
396 let b: C
397 function getB() {}
398 // exported variables are included
399 export const d = e;
400 "#
401 .unindent();
402
403 let buffer = cx.new_model(|cx| {
404 language::Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text)
405 .with_language(language, cx)
406 });
407 let outline = buffer.update(cx, |buffer, _| buffer.snapshot().outline(None).unwrap());
408 assert_eq!(
409 outline
410 .items
411 .iter()
412 .map(|item| (item.text.as_str(), item.depth))
413 .collect::<Vec<_>>(),
414 &[
415 ("function a()", 0),
416 ("async function a2()", 1),
417 ("let b", 0),
418 ("function getB()", 0),
419 ("const d", 0),
420 ]
421 );
422 }
423}