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