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}