typescript.rs

  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}