typescript.rs

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