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