1use anyhow::{anyhow, Result};
2use async_trait::async_trait;
3use futures::{future::BoxFuture, FutureExt};
4use gpui::AppContext;
5use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
6use lsp::CodeActionKind;
7use node_runtime::NodeRuntime;
8use serde_json::{json, Value};
9use smol::fs;
10use std::{
11 any::Any,
12 ffi::OsString,
13 future,
14 path::{Path, PathBuf},
15 sync::Arc,
16};
17use util::http::HttpClient;
18use util::ResultExt;
19
20fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
21 vec![
22 server_path.into(),
23 "--stdio".into(),
24 "--tsserver-path".into(),
25 "node_modules/typescript/lib".into(),
26 ]
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<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<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 async fn name(&self) -> LanguageServerName {
54 LanguageServerName("typescript-language-server".into())
55 }
56
57 async fn fetch_latest_server_version(
58 &self,
59 _: Arc<dyn HttpClient>,
60 ) -> Result<Box<dyn 'static + Send + Any>> {
61 Ok(Box::new(TypeScriptVersions {
62 typescript_version: self.node.npm_package_latest_version("typescript").await?,
63 server_version: self
64 .node
65 .npm_package_latest_version("typescript-language-server")
66 .await?,
67 }) as Box<_>)
68 }
69
70 async fn fetch_server_binary(
71 &self,
72 versions: Box<dyn 'static + Send + Any>,
73 _: Arc<dyn HttpClient>,
74 container_dir: PathBuf,
75 ) -> Result<LanguageServerBinary> {
76 let versions = versions.downcast::<TypeScriptVersions>().unwrap();
77 let server_path = container_dir.join(Self::NEW_SERVER_PATH);
78
79 if fs::metadata(&server_path).await.is_err() {
80 self.node
81 .npm_install_packages(
82 [
83 ("typescript", versions.typescript_version.as_str()),
84 (
85 "typescript-language-server",
86 versions.server_version.as_str(),
87 ),
88 ],
89 &container_dir,
90 )
91 .await?;
92 }
93
94 Ok(LanguageServerBinary {
95 path: self.node.binary_path().await?,
96 arguments: typescript_server_binary_arguments(&server_path),
97 })
98 }
99
100 async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
101 (|| async move {
102 let old_server_path = container_dir.join(Self::OLD_SERVER_PATH);
103 let new_server_path = container_dir.join(Self::NEW_SERVER_PATH);
104 if new_server_path.exists() {
105 Ok(LanguageServerBinary {
106 path: self.node.binary_path().await?,
107 arguments: typescript_server_binary_arguments(&new_server_path),
108 })
109 } else if old_server_path.exists() {
110 Ok(LanguageServerBinary {
111 path: self.node.binary_path().await?,
112 arguments: typescript_server_binary_arguments(&old_server_path),
113 })
114 } else {
115 Err(anyhow!(
116 "missing executable in directory {:?}",
117 container_dir
118 ))
119 }
120 })()
121 .await
122 .log_err()
123 }
124
125 fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
126 Some(vec![
127 CodeActionKind::QUICKFIX,
128 CodeActionKind::REFACTOR,
129 CodeActionKind::REFACTOR_EXTRACT,
130 CodeActionKind::SOURCE,
131 ])
132 }
133
134 async fn label_for_completion(
135 &self,
136 item: &lsp::CompletionItem,
137 language: &Arc<language::Language>,
138 ) -> Option<language::CodeLabel> {
139 use lsp::CompletionItemKind as Kind;
140 let len = item.label.len();
141 let grammar = language.grammar()?;
142 let highlight_id = match item.kind? {
143 Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"),
144 Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
145 Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
146 Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
147 Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
148 _ => None,
149 }?;
150
151 let text = match &item.detail {
152 Some(detail) => format!("{} {}", item.label, detail),
153 None => item.label.clone(),
154 };
155
156 Some(language::CodeLabel {
157 text,
158 runs: vec![(0..len, highlight_id)],
159 filter_range: 0..len,
160 })
161 }
162
163 async fn initialization_options(&self) -> Option<serde_json::Value> {
164 Some(json!({
165 "provideFormatter": true
166 }))
167 }
168}
169
170pub struct EsLintLspAdapter {
171 node: Arc<NodeRuntime>,
172}
173
174impl EsLintLspAdapter {
175 const SERVER_PATH: &'static str =
176 "node_modules/vscode-langservers-extracted/lib/eslint-language-server/eslintServer.js";
177
178 #[allow(unused)]
179 pub fn new(node: Arc<NodeRuntime>) -> Self {
180 EsLintLspAdapter { node }
181 }
182}
183
184#[async_trait]
185impl LspAdapter for EsLintLspAdapter {
186 fn workspace_configuration(&self, _: &mut AppContext) -> Option<BoxFuture<'static, Value>> {
187 Some(
188 future::ready(json!({
189 "": {
190 "validate": "on",
191 "packageManager": "npm",
192 "useESLintClass": false,
193 "experimental": {
194 "useFlatConfig": false
195 },
196 "codeActionOnSave": {
197 "mode": "all"
198 },
199 "format": false,
200 "quiet": false,
201 "onIgnoredFiles": "off",
202 "options": {},
203 "rulesCustomizations": [],
204 "run": "onType",
205 "problems": {
206 "shortenToSingleLine": false
207 },
208 "nodePath": null,
209 "workspaceFolder": {
210 "name": "testing_ts",
211 "uri": "file:///Users/julia/Stuff/testing_ts"
212 },
213 "codeAction": {
214 "disableRuleComment": {
215 "enable": true,
216 "location": "separateLine",
217 "commentStyle": "line"
218 },
219 "showDocumentation": {
220 "enable": true
221 }
222 }
223 }
224 }))
225 .boxed(),
226 )
227 }
228
229 async fn name(&self) -> LanguageServerName {
230 LanguageServerName("eslint".into())
231 }
232
233 async fn fetch_latest_server_version(
234 &self,
235 _: Arc<dyn HttpClient>,
236 ) -> Result<Box<dyn 'static + Send + Any>> {
237 Ok(Box::new(
238 self.node
239 .npm_package_latest_version("vscode-langservers-extracted")
240 .await?,
241 ))
242 }
243
244 async fn fetch_server_binary(
245 &self,
246 versions: Box<dyn 'static + Send + Any>,
247 _: Arc<dyn HttpClient>,
248 container_dir: PathBuf,
249 ) -> Result<LanguageServerBinary> {
250 let version = versions.downcast::<String>().unwrap();
251 let server_path = container_dir.join(Self::SERVER_PATH);
252
253 if fs::metadata(&server_path).await.is_err() {
254 self.node
255 .npm_install_packages(
256 [("vscode-langservers-extracted", version.as_str())],
257 &container_dir,
258 )
259 .await?;
260 }
261
262 Ok(LanguageServerBinary {
263 path: self.node.binary_path().await?,
264 arguments: eslint_server_binary_arguments(&server_path),
265 })
266 }
267
268 async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
269 (|| async move {
270 let server_path = container_dir.join(Self::SERVER_PATH);
271 if server_path.exists() {
272 Ok(LanguageServerBinary {
273 path: self.node.binary_path().await?,
274 arguments: eslint_server_binary_arguments(&server_path),
275 })
276 } else {
277 Err(anyhow!(
278 "missing executable in directory {:?}",
279 container_dir
280 ))
281 }
282 })()
283 .await
284 .log_err()
285 }
286
287 async fn label_for_completion(
288 &self,
289 _item: &lsp::CompletionItem,
290 _language: &Arc<language::Language>,
291 ) -> Option<language::CodeLabel> {
292 None
293 }
294
295 async fn initialization_options(&self) -> Option<serde_json::Value> {
296 None
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use gpui::TestAppContext;
303 use unindent::Unindent;
304
305 #[gpui::test]
306 async fn test_outline(cx: &mut TestAppContext) {
307 let language = crate::languages::language(
308 "typescript",
309 tree_sitter_typescript::language_typescript(),
310 None,
311 )
312 .await;
313
314 let text = r#"
315 function a() {
316 // local variables are omitted
317 let a1 = 1;
318 // all functions are included
319 async function a2() {}
320 }
321 // top-level variables are included
322 let b: C
323 function getB() {}
324 // exported variables are included
325 export const d = e;
326 "#
327 .unindent();
328
329 let buffer =
330 cx.add_model(|cx| language::Buffer::new(0, text, cx).with_language(language, cx));
331 let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None).unwrap());
332 assert_eq!(
333 outline
334 .items
335 .iter()
336 .map(|item| (item.text.as_str(), item.depth))
337 .collect::<Vec<_>>(),
338 &[
339 ("function a ( )", 0),
340 ("async function a2 ( )", 1),
341 ("let b", 0),
342 ("function getB ( )", 0),
343 ("const d", 0),
344 ]
345 );
346 }
347}