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}