1use anyhow::{anyhow, Result};
2use async_trait::async_trait;
3use collections::HashMap;
4use gpui::AsyncAppContext;
5use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
6use lsp::{CodeActionKind, LanguageServerBinary};
7use node_runtime::NodeRuntime;
8use project::project_settings::{BinarySettings, ProjectSettings};
9use serde_json::{json, Value};
10use settings::{Settings, SettingsLocation};
11use std::{
12 any::Any,
13 ffi::OsString,
14 path::{Path, PathBuf},
15 sync::Arc,
16};
17use util::{maybe, merge_json_value_into, ResultExt};
18
19fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
20 vec![server_path.into(), "--stdio".into()]
21}
22
23pub struct VtslsLspAdapter {
24 node: Arc<dyn NodeRuntime>,
25}
26
27impl VtslsLspAdapter {
28 const SERVER_PATH: &'static str = "node_modules/@vtsls/language-server/bin/vtsls.js";
29
30 pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
31 VtslsLspAdapter { node }
32 }
33 async fn tsdk_path(adapter: &Arc<dyn LspAdapterDelegate>) -> &'static str {
34 let is_yarn = adapter
35 .read_text_file(PathBuf::from(".yarn/sdks/typescript/lib/typescript.js"))
36 .await
37 .is_ok();
38
39 if is_yarn {
40 ".yarn/sdks/typescript/lib"
41 } else {
42 "node_modules/typescript/lib"
43 }
44 }
45}
46
47struct TypeScriptVersions {
48 typescript_version: String,
49 server_version: String,
50}
51
52const SERVER_NAME: &str = "vtsls";
53#[async_trait(?Send)]
54impl LspAdapter for VtslsLspAdapter {
55 fn name(&self) -> LanguageServerName {
56 LanguageServerName(SERVER_NAME.into())
57 }
58
59 async fn fetch_latest_server_version(
60 &self,
61 _: &dyn LspAdapterDelegate,
62 ) -> Result<Box<dyn 'static + Send + Any>> {
63 Ok(Box::new(TypeScriptVersions {
64 typescript_version: self.node.npm_package_latest_version("typescript").await?,
65 server_version: self
66 .node
67 .npm_package_latest_version("@vtsls/language-server")
68 .await?,
69 }) as Box<_>)
70 }
71
72 async fn check_if_user_installed(
73 &self,
74 delegate: &dyn LspAdapterDelegate,
75 cx: &AsyncAppContext,
76 ) -> Option<LanguageServerBinary> {
77 let configured_binary = cx.update(|cx| {
78 ProjectSettings::get_global(cx)
79 .lsp
80 .get(SERVER_NAME)
81 .and_then(|s| s.binary.clone())
82 });
83
84 match configured_binary {
85 Ok(Some(BinarySettings {
86 path: Some(path),
87 arguments,
88 ..
89 })) => Some(LanguageServerBinary {
90 path: path.into(),
91 arguments: arguments
92 .unwrap_or_default()
93 .iter()
94 .map(|arg| arg.into())
95 .collect(),
96 env: None,
97 }),
98 Ok(Some(BinarySettings {
99 path_lookup: Some(false),
100 ..
101 })) => None,
102 _ => {
103 let env = delegate.shell_env().await;
104 let path = delegate.which(SERVER_NAME.as_ref()).await?;
105 Some(LanguageServerBinary {
106 path: path.clone(),
107 arguments: typescript_server_binary_arguments(&path),
108 env: Some(env),
109 })
110 }
111 }
112 }
113
114 async fn fetch_server_binary(
115 &self,
116 latest_version: Box<dyn 'static + Send + Any>,
117 container_dir: PathBuf,
118 _: &dyn LspAdapterDelegate,
119 ) -> Result<LanguageServerBinary> {
120 let latest_version = latest_version.downcast::<TypeScriptVersions>().unwrap();
121 let server_path = container_dir.join(Self::SERVER_PATH);
122 let package_name = "typescript";
123
124 let should_install_language_server = self
125 .node
126 .should_install_npm_package(
127 package_name,
128 &server_path,
129 &container_dir,
130 latest_version.typescript_version.as_str(),
131 )
132 .await;
133
134 if should_install_language_server {
135 self.node
136 .npm_install_packages(
137 &container_dir,
138 &[
139 (package_name, latest_version.typescript_version.as_str()),
140 (
141 "@vtsls/language-server",
142 latest_version.server_version.as_str(),
143 ),
144 ],
145 )
146 .await?;
147 }
148
149 Ok(LanguageServerBinary {
150 path: self.node.binary_path().await?,
151 env: None,
152 arguments: typescript_server_binary_arguments(&server_path),
153 })
154 }
155
156 async fn cached_server_binary(
157 &self,
158 container_dir: PathBuf,
159 _: &dyn LspAdapterDelegate,
160 ) -> Option<LanguageServerBinary> {
161 get_cached_ts_server_binary(container_dir, &*self.node).await
162 }
163
164 async fn installation_test_binary(
165 &self,
166 container_dir: PathBuf,
167 ) -> Option<LanguageServerBinary> {
168 get_cached_ts_server_binary(container_dir, &*self.node).await
169 }
170
171 fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
172 Some(vec![
173 CodeActionKind::QUICKFIX,
174 CodeActionKind::REFACTOR,
175 CodeActionKind::REFACTOR_EXTRACT,
176 CodeActionKind::SOURCE,
177 ])
178 }
179
180 async fn label_for_completion(
181 &self,
182 item: &lsp::CompletionItem,
183 language: &Arc<language::Language>,
184 ) -> Option<language::CodeLabel> {
185 use lsp::CompletionItemKind as Kind;
186 let len = item.label.len();
187 let grammar = language.grammar()?;
188 let highlight_id = match item.kind? {
189 Kind::CLASS | Kind::INTERFACE | Kind::ENUM => grammar.highlight_id_for_name("type"),
190 Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
191 Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
192 Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
193 Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
194 Kind::VARIABLE => grammar.highlight_id_for_name("variable"),
195 _ => None,
196 }?;
197
198 let one_line = |s: &str| s.replace(" ", "").replace('\n', " ");
199
200 let text = if let Some(description) = item
201 .label_details
202 .as_ref()
203 .and_then(|label_details| label_details.description.as_ref())
204 {
205 format!("{} {}", item.label, one_line(description))
206 } else if let Some(detail) = &item.detail {
207 format!("{} {}", item.label, one_line(detail))
208 } else {
209 item.label.clone()
210 };
211
212 Some(language::CodeLabel {
213 text,
214 runs: vec![(0..len, highlight_id)],
215 filter_range: 0..len,
216 })
217 }
218
219 async fn initialization_options(
220 self: Arc<Self>,
221 adapter: &Arc<dyn LspAdapterDelegate>,
222 ) -> Result<Option<serde_json::Value>> {
223 let tsdk_path = Self::tsdk_path(adapter).await;
224 let config = serde_json::json!({
225 "tsdk": tsdk_path,
226 "suggest": {
227 "completeFunctionCalls": true
228 },
229 "tsserver": {
230 "maxTsServerMemory": 8092
231 },
232 "inlayHints": {
233 "parameterNames": {
234 "enabled": "all",
235 "suppressWhenArgumentMatchesName": false
236 },
237 "parameterTypes": {
238 "enabled": true
239 },
240 "variableTypes": {
241 "enabled": true,
242 "suppressWhenTypeMatchesName": false
243 },
244 "propertyDeclarationTypes": {
245 "enabled": true
246 },
247 "functionLikeReturnTypes": {
248 "enabled": true
249 },
250 "enumMemberValues": {
251 "enabled": true
252 }
253 }
254 });
255
256 Ok(Some(json!({
257 "typescript": config,
258 "javascript": config,
259 "vtsls": {
260 "experimental": {
261 "completion": {
262 "enableServerSideFuzzyMatch": true,
263 "entriesLimit": 5000,
264 }
265 },
266 "autoUseWorkspaceTsdk": true
267 }
268 })))
269 }
270
271 async fn workspace_configuration(
272 self: Arc<Self>,
273 adapter: &Arc<dyn LspAdapterDelegate>,
274 cx: &mut AsyncAppContext,
275 ) -> Result<Value> {
276 let override_options = cx.update(|cx| {
277 ProjectSettings::get(
278 Some(SettingsLocation {
279 worktree_id: adapter.worktree_id(),
280 path: adapter.worktree_root_path(),
281 }),
282 cx,
283 )
284 .lsp
285 .get(SERVER_NAME)
286 .and_then(|s| s.initialization_options.clone())
287 })?;
288 if let Some(options) = override_options {
289 return Ok(options);
290 }
291 let mut initialization_options = self
292 .initialization_options(adapter)
293 .await
294 .map(|o| o.unwrap())?;
295
296 if let Some(override_options) = override_options {
297 merge_json_value_into(override_options, &mut initialization_options)
298 }
299 Ok(initialization_options)
300 }
301
302 fn language_ids(&self) -> HashMap<String, String> {
303 HashMap::from_iter([
304 ("TypeScript".into(), "typescript".into()),
305 ("JavaScript".into(), "javascript".into()),
306 ("TSX".into(), "typescriptreact".into()),
307 ])
308 }
309}
310
311async fn get_cached_ts_server_binary(
312 container_dir: PathBuf,
313 node: &dyn NodeRuntime,
314) -> Option<LanguageServerBinary> {
315 maybe!(async {
316 let server_path = container_dir.join(VtslsLspAdapter::SERVER_PATH);
317 if server_path.exists() {
318 Ok(LanguageServerBinary {
319 path: node.binary_path().await?,
320 env: None,
321 arguments: typescript_server_binary_arguments(&server_path),
322 })
323 } else {
324 Err(anyhow!(
325 "missing executable in directory {:?}",
326 container_dir
327 ))
328 }
329 })
330 .await
331 .log_err()
332}