1use anyhow::{anyhow, Result};
2use async_trait::async_trait;
3use collections::HashMap;
4use gpui::AsyncAppContext;
5use language::{LanguageServerName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
6use lsp::{CodeActionKind, LanguageServerBinary};
7use node_runtime::NodeRuntime;
8use project::lsp_store::language_server_settings;
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 _: &AsyncAppContext,
75 ) -> Option<LanguageServerBinary> {
76 let env = delegate.shell_env().await;
77 let path = delegate.which(SERVER_NAME.as_ref()).await?;
78 Some(LanguageServerBinary {
79 path: path.clone(),
80 arguments: typescript_server_binary_arguments(&path),
81 env: Some(env),
82 })
83 }
84
85 async fn fetch_server_binary(
86 &self,
87 latest_version: Box<dyn 'static + Send + Any>,
88 container_dir: PathBuf,
89 _: &dyn LspAdapterDelegate,
90 ) -> Result<LanguageServerBinary> {
91 let latest_version = latest_version.downcast::<TypeScriptVersions>().unwrap();
92 let server_path = container_dir.join(Self::SERVER_PATH);
93 let package_name = "typescript";
94
95 let should_install_language_server = self
96 .node
97 .should_install_npm_package(
98 package_name,
99 &server_path,
100 &container_dir,
101 latest_version.typescript_version.as_str(),
102 )
103 .await;
104
105 if should_install_language_server {
106 self.node
107 .npm_install_packages(
108 &container_dir,
109 &[
110 (package_name, latest_version.typescript_version.as_str()),
111 (
112 "@vtsls/language-server",
113 latest_version.server_version.as_str(),
114 ),
115 ],
116 )
117 .await?;
118 }
119
120 Ok(LanguageServerBinary {
121 path: self.node.binary_path().await?,
122 env: None,
123 arguments: typescript_server_binary_arguments(&server_path),
124 })
125 }
126
127 async fn cached_server_binary(
128 &self,
129 container_dir: PathBuf,
130 _: &dyn LspAdapterDelegate,
131 ) -> Option<LanguageServerBinary> {
132 get_cached_ts_server_binary(container_dir, &self.node).await
133 }
134
135 fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
136 Some(vec![
137 CodeActionKind::QUICKFIX,
138 CodeActionKind::REFACTOR,
139 CodeActionKind::REFACTOR_EXTRACT,
140 CodeActionKind::SOURCE,
141 ])
142 }
143
144 async fn label_for_completion(
145 &self,
146 item: &lsp::CompletionItem,
147 language: &Arc<language::Language>,
148 ) -> Option<language::CodeLabel> {
149 use lsp::CompletionItemKind as Kind;
150 let len = item.label.len();
151 let grammar = language.grammar()?;
152 let highlight_id = match item.kind? {
153 Kind::CLASS | Kind::INTERFACE | Kind::ENUM => grammar.highlight_id_for_name("type"),
154 Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
155 Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
156 Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
157 Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
158 Kind::VARIABLE => grammar.highlight_id_for_name("variable"),
159 _ => None,
160 }?;
161
162 let one_line = |s: &str| s.replace(" ", "").replace('\n', " ");
163
164 let text = if let Some(description) = item
165 .label_details
166 .as_ref()
167 .and_then(|label_details| label_details.description.as_ref())
168 {
169 format!("{} {}", item.label, one_line(description))
170 } else if let Some(detail) = &item.detail {
171 format!("{} {}", item.label, one_line(detail))
172 } else {
173 item.label.clone()
174 };
175
176 Some(language::CodeLabel {
177 text,
178 runs: vec![(0..len, highlight_id)],
179 filter_range: 0..len,
180 })
181 }
182
183 async fn workspace_configuration(
184 self: Arc<Self>,
185 delegate: &Arc<dyn LspAdapterDelegate>,
186 _: Arc<dyn LanguageToolchainStore>,
187 cx: &mut AsyncAppContext,
188 ) -> Result<Value> {
189 let tsdk_path = Self::tsdk_path(delegate).await;
190 let config = serde_json::json!({
191 "tsdk": tsdk_path,
192 "suggest": {
193 "completeFunctionCalls": true
194 },
195 "inlayHints": {
196 "parameterNames": {
197 "enabled": "all",
198 "suppressWhenArgumentMatchesName": false
199 },
200 "parameterTypes": {
201 "enabled": true
202 },
203 "variableTypes": {
204 "enabled": true,
205 "suppressWhenTypeMatchesName": false
206 },
207 "propertyDeclarationTypes": {
208 "enabled": true
209 },
210 "functionLikeReturnTypes": {
211 "enabled": true
212 },
213 "enumMemberValues": {
214 "enabled": true
215 }
216 },
217 "tsserver": {
218 "maxTsServerMemory": 8092
219 },
220 });
221
222 let mut default_workspace_configuration = serde_json::json!({
223 "typescript": config,
224 "javascript": config,
225 "vtsls": {
226 "experimental": {
227 "completion": {
228 "enableServerSideFuzzyMatch": true,
229 "entriesLimit": 5000,
230 }
231 },
232 "autoUseWorkspaceTsdk": true
233 }
234 });
235
236 let override_options = cx.update(|cx| {
237 language_server_settings(delegate.as_ref(), &SERVER_NAME, cx)
238 .and_then(|s| s.settings.clone())
239 })?;
240
241 if let Some(override_options) = override_options {
242 merge_json_value_into(override_options, &mut default_workspace_configuration)
243 }
244
245 Ok(default_workspace_configuration)
246 }
247
248 fn language_ids(&self) -> HashMap<String, String> {
249 HashMap::from_iter([
250 ("TypeScript".into(), "typescript".into()),
251 ("JavaScript".into(), "javascript".into()),
252 ("TSX".into(), "typescriptreact".into()),
253 ])
254 }
255}
256
257async fn get_cached_ts_server_binary(
258 container_dir: PathBuf,
259 node: &NodeRuntime,
260) -> Option<LanguageServerBinary> {
261 maybe!(async {
262 let server_path = container_dir.join(VtslsLspAdapter::SERVER_PATH);
263 if server_path.exists() {
264 Ok(LanguageServerBinary {
265 path: node.binary_path().await?,
266 env: None,
267 arguments: typescript_server_binary_arguments(&server_path),
268 })
269 } else {
270 Err(anyhow!(
271 "missing executable in directory {:?}",
272 container_dir
273 ))
274 }
275 })
276 .await
277 .log_err()
278}