typescript.rs

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