typescript.rs

  1use super::node_runtime::NodeRuntime;
  2use anyhow::{anyhow, Context, Result};
  3use async_trait::async_trait;
  4use client::http::HttpClient;
  5use futures::StreamExt;
  6use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
  7use serde_json::json;
  8use smol::fs;
  9use std::{
 10    any::Any,
 11    ffi::OsString,
 12    path::{Path, PathBuf},
 13    sync::Arc,
 14};
 15use util::ResultExt;
 16
 17fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
 18    vec![
 19        server_path.into(),
 20        "--stdio".into(),
 21        "--tsserver-path".into(),
 22        "node_modules/typescript/lib".into(),
 23    ]
 24}
 25
 26pub struct TypeScriptLspAdapter {
 27    node: Arc<NodeRuntime>,
 28}
 29
 30impl TypeScriptLspAdapter {
 31    const OLD_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js";
 32    const NEW_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs";
 33
 34    pub fn new(node: Arc<NodeRuntime>) -> Self {
 35        TypeScriptLspAdapter { node }
 36    }
 37}
 38
 39struct Versions {
 40    typescript_version: String,
 41    server_version: String,
 42}
 43
 44#[async_trait]
 45impl LspAdapter for TypeScriptLspAdapter {
 46    async fn name(&self) -> LanguageServerName {
 47        LanguageServerName("typescript-language-server".into())
 48    }
 49
 50    async fn fetch_latest_server_version(
 51        &self,
 52        _: Arc<dyn HttpClient>,
 53    ) -> Result<Box<dyn 'static + Send + Any>> {
 54        Ok(Box::new(Versions {
 55            typescript_version: self.node.npm_package_latest_version("typescript").await?,
 56            server_version: self
 57                .node
 58                .npm_package_latest_version("typescript-language-server")
 59                .await?,
 60        }) as Box<_>)
 61    }
 62
 63    async fn fetch_server_binary(
 64        &self,
 65        versions: Box<dyn 'static + Send + Any>,
 66        _: Arc<dyn HttpClient>,
 67        container_dir: PathBuf,
 68    ) -> Result<LanguageServerBinary> {
 69        let versions = versions.downcast::<Versions>().unwrap();
 70        let version_dir = container_dir.join(&format!(
 71            "typescript-{}:server-{}",
 72            versions.typescript_version, versions.server_version
 73        ));
 74        fs::create_dir_all(&version_dir)
 75            .await
 76            .context("failed to create version directory")?;
 77        let server_path = version_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                    &version_dir,
 90                )
 91                .await?;
 92
 93            if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
 94                while let Some(entry) = entries.next().await {
 95                    if let Some(entry) = entry.log_err() {
 96                        let entry_path = entry.path();
 97                        if entry_path.as_path() != version_dir {
 98                            fs::remove_dir_all(&entry_path).await.log_err();
 99                        }
100                    }
101                }
102            }
103        }
104
105        Ok(LanguageServerBinary {
106            path: self.node.binary_path().await?,
107            arguments: server_binary_arguments(&server_path),
108        })
109    }
110
111    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
112        (|| async move {
113            let mut last_version_dir = None;
114            let mut entries = fs::read_dir(&container_dir).await?;
115            while let Some(entry) = entries.next().await {
116                let entry = entry?;
117                if entry.file_type().await?.is_dir() {
118                    last_version_dir = Some(entry.path());
119                }
120            }
121            let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
122            let old_server_path = last_version_dir.join(Self::OLD_SERVER_PATH);
123            let new_server_path = last_version_dir.join(Self::NEW_SERVER_PATH);
124            if new_server_path.exists() {
125                Ok(LanguageServerBinary {
126                    path: self.node.binary_path().await?,
127                    arguments: server_binary_arguments(&new_server_path),
128                })
129            } else if old_server_path.exists() {
130                Ok(LanguageServerBinary {
131                    path: self.node.binary_path().await?,
132                    arguments: server_binary_arguments(&old_server_path),
133                })
134            } else {
135                Err(anyhow!(
136                    "missing executable in directory {:?}",
137                    last_version_dir
138                ))
139            }
140        })()
141        .await
142        .log_err()
143    }
144
145    async fn label_for_completion(
146        &self,
147        item: &lsp::CompletionItem,
148        language: &Arc<language::Language>,
149    ) -> Option<language::CodeLabel> {
150        use lsp::CompletionItemKind as Kind;
151        let len = item.label.len();
152        let grammar = language.grammar()?;
153        let highlight_id = match item.kind? {
154            Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"),
155            Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
156            Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
157            Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
158            Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
159            _ => None,
160        }?;
161
162        let text = match &item.detail {
163            Some(detail) => format!("{} {}", item.label, detail),
164            None => item.label.clone(),
165        };
166
167        Some(language::CodeLabel {
168            text,
169            runs: vec![(0..len, highlight_id)],
170            filter_range: 0..len,
171        })
172    }
173
174    async fn initialization_options(&self) -> Option<serde_json::Value> {
175        Some(json!({
176            "provideFormatter": true
177        }))
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use gpui::TestAppContext;
184    use unindent::Unindent;
185
186    #[gpui::test]
187    async fn test_outline(cx: &mut TestAppContext) {
188        let language = crate::languages::language(
189            "typescript",
190            tree_sitter_typescript::language_typescript(),
191            None,
192        )
193        .await;
194
195        let text = r#"
196            function a() {
197              // local variables are omitted
198              let a1 = 1;
199              // all functions are included
200              async function a2() {}
201            }
202            // top-level variables are included
203            let b: C
204            function getB() {}
205            // exported variables are included
206            export const d = e;
207        "#
208        .unindent();
209
210        let buffer =
211            cx.add_model(|cx| language::Buffer::new(0, text, cx).with_language(language, cx));
212        let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None).unwrap());
213        assert_eq!(
214            outline
215                .items
216                .iter()
217                .map(|item| (item.text.as_str(), item.depth))
218                .collect::<Vec<_>>(),
219            &[
220                ("function a ( )", 0),
221                ("async function a2 ( )", 1),
222                ("let b", 0),
223                ("function getB ( )", 0),
224                ("const d", 0),
225            ]
226        );
227    }
228}