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: &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    use std::sync::Arc;
148
149    use gpui::MutableAppContext;
150    use unindent::Unindent;
151
152    #[gpui::test]
153    fn test_outline(cx: &mut MutableAppContext) {
154        let language = crate::languages::language(
155            "typescript",
156            tree_sitter_typescript::language_typescript(),
157            None,
158        );
159
160        let text = r#"
161            function a() {
162              // local variables are omitted
163              let a1 = 1;
164              // all functions are included
165              async function a2() {}
166            }
167            // top-level variables are included
168            let b: C
169            function getB() {}
170            // exported variables are included
171            export const d = e;
172        "#
173        .unindent();
174
175        let buffer = cx.add_model(|cx| {
176            language::Buffer::new(0, text, cx).with_language(Arc::new(language), cx)
177        });
178        let outline = buffer.read(cx).snapshot().outline(None).unwrap();
179        assert_eq!(
180            outline
181                .items
182                .iter()
183                .map(|item| (item.text.as_str(), item.depth))
184                .collect::<Vec<_>>(),
185            &[
186                ("function a ( )", 0),
187                ("async function a2 ( )", 1),
188                ("let b", 0),
189                ("function getB ( )", 0),
190                ("const d", 0),
191            ]
192        );
193    }
194}