Skip to repository content431 lines · 14.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:43:48.510Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
vtsls.rs
1use anyhow::Result;
2use async_trait::async_trait;
3use collections::HashMap;
4use gpui::AsyncApp;
5use language::{
6 LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, PromptResponseContext, Toolchain,
7};
8use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName, Uri};
9use node_runtime::{NodeRuntime, VersionStrategy};
10use project::{Fs, lsp_store::language_server_settings};
11use regex::Regex;
12use semver::Version;
13use serde_json::Value;
14use serde_json::json;
15use settings::update_settings_file;
16use std::{
17 ffi::OsString,
18 future::Future,
19 path::{Path, PathBuf},
20 sync::{Arc, LazyLock},
21};
22use util::{ResultExt, maybe, merge_json_value_into};
23
24const ACTION_ALWAYS: &str = "Always";
25const ACTION_NEVER: &str = "Never";
26const UPDATE_IMPORTS_MESSAGE_PATTERN: &str = "Update imports for";
27const VTSLS_SERVER_NAME: &str = "vtsls";
28
29fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
30 vec![server_path.into(), "--stdio".into()]
31}
32
33pub struct VtslsLspAdapter {
34 node: NodeRuntime,
35 fs: Arc<dyn Fs>,
36}
37
38impl VtslsLspAdapter {
39 const PACKAGE_NAME: &'static str = "@vtsls/language-server";
40 const SERVER_PATH: &'static str = "node_modules/@vtsls/language-server/bin/vtsls.js";
41
42 const TYPESCRIPT_TSDK_PATH: &'static str = "node_modules/typescript/lib";
43 const TYPESCRIPT_YARN_TSDK_PATH: &'static str = ".yarn/sdks/typescript/lib";
44
45 pub fn new(node: NodeRuntime, fs: Arc<dyn Fs>) -> Self {
46 VtslsLspAdapter { node, fs }
47 }
48
49 async fn tsdk_path(&self, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {
50 let yarn_sdk = adapter
51 .worktree_root_path()
52 .join(Self::TYPESCRIPT_YARN_TSDK_PATH);
53
54 let tsdk_path = if self.fs.is_dir(&yarn_sdk).await {
55 Self::TYPESCRIPT_YARN_TSDK_PATH
56 } else {
57 Self::TYPESCRIPT_TSDK_PATH
58 };
59
60 // vtsls doesn't support TypeScript 7+, which no longer ships `tsserver.js`.
61 if self
62 .fs
63 .is_file(
64 &adapter
65 .worktree_root_path()
66 .join(tsdk_path)
67 .join("tsserver.js"),
68 )
69 .await
70 {
71 Some(tsdk_path)
72 } else {
73 None
74 }
75 }
76
77 pub fn enhance_diagnostic_message(message: &str) -> Option<String> {
78 static SINGLE_WORD_REGEX: LazyLock<Regex> =
79 LazyLock::new(|| Regex::new(r"'([^\s']*)'").expect("Failed to create REGEX"));
80
81 static MULTI_WORD_REGEX: LazyLock<Regex> =
82 LazyLock::new(|| Regex::new(r"'([^']+\s+[^']*)'").expect("Failed to create REGEX"));
83
84 let first = SINGLE_WORD_REGEX.replace_all(message, "`$1`").to_string();
85 let second = MULTI_WORD_REGEX
86 .replace_all(&first, "\n```typescript\n$1\n```\n")
87 .to_string();
88 Some(second)
89 }
90}
91
92const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("vtsls");
93
94impl LspInstaller for VtslsLspAdapter {
95 type BinaryVersion = Version;
96
97 async fn fetch_latest_server_version(
98 &self,
99 _: &Arc<dyn LspAdapterDelegate>,
100 _: bool,
101 _: &mut AsyncApp,
102 ) -> Result<Self::BinaryVersion> {
103 self.node
104 .npm_package_latest_version(Self::PACKAGE_NAME)
105 .await
106 }
107
108 async fn check_if_user_installed(
109 &self,
110 delegate: &Arc<dyn LspAdapterDelegate>,
111 _: Option<Toolchain>,
112 _: &AsyncApp,
113 ) -> Option<LanguageServerBinary> {
114 let env = delegate.shell_env().await;
115 let path = delegate.which(SERVER_NAME.as_ref()).await?;
116 Some(LanguageServerBinary {
117 path: path.clone(),
118 arguments: typescript_server_binary_arguments(&path),
119 env: Some(env),
120 })
121 }
122
123 fn fetch_server_binary(
124 &self,
125 _latest_version: Self::BinaryVersion,
126 container_dir: PathBuf,
127 _: &Arc<dyn LspAdapterDelegate>,
128 ) -> impl Send + Future<Output = Result<LanguageServerBinary>> + use<> {
129 let node = self.node.clone();
130
131 async move {
132 let server_path = container_dir.join(Self::SERVER_PATH);
133
134 node.npm_install_latest_packages(&container_dir, &[Self::PACKAGE_NAME])
135 .await?;
136
137 Ok(LanguageServerBinary {
138 path: node.binary_path().await?,
139 env: None,
140 arguments: typescript_server_binary_arguments(&server_path),
141 })
142 }
143 }
144
145 fn check_if_version_installed(
146 &self,
147 version: &Self::BinaryVersion,
148 container_dir: &PathBuf,
149 _: &Arc<dyn LspAdapterDelegate>,
150 ) -> impl Send + Future<Output = Option<LanguageServerBinary>> + use<> {
151 let node = self.node.clone();
152 let server_version = version.clone();
153 let container_dir = container_dir.clone();
154
155 async move {
156 let server_path = container_dir.join(Self::SERVER_PATH);
157
158 if node
159 .should_install_npm_package(
160 Self::PACKAGE_NAME,
161 &server_path,
162 &container_dir,
163 VersionStrategy::Latest(&server_version),
164 )
165 .await
166 {
167 return None;
168 }
169
170 Some(LanguageServerBinary {
171 path: node.binary_path().await.ok()?,
172 env: None,
173 arguments: typescript_server_binary_arguments(&server_path),
174 })
175 }
176 }
177
178 async fn cached_server_binary(
179 &self,
180 container_dir: PathBuf,
181 _: &dyn LspAdapterDelegate,
182 ) -> Option<LanguageServerBinary> {
183 get_cached_ts_server_binary(container_dir, &self.node).await
184 }
185}
186
187#[async_trait(?Send)]
188impl LspAdapter for VtslsLspAdapter {
189 fn name(&self) -> LanguageServerName {
190 SERVER_NAME
191 }
192
193 fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
194 Some(vec![
195 CodeActionKind::QUICKFIX,
196 CodeActionKind::REFACTOR,
197 CodeActionKind::REFACTOR_EXTRACT,
198 CodeActionKind::SOURCE,
199 ])
200 }
201
202 async fn label_for_completion(
203 &self,
204 item: &lsp::CompletionItem,
205 language: &Arc<language::Language>,
206 ) -> Option<language::CodeLabel> {
207 use lsp::CompletionItemKind as Kind;
208 let label_len = item.label.len();
209 let grammar = language.grammar()?;
210 let highlight_id = match item.kind? {
211 Kind::CLASS | Kind::INTERFACE | Kind::ENUM => grammar.highlight_id_for_name("type"),
212 Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
213 Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
214 Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
215 Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
216 Kind::VARIABLE => grammar.highlight_id_for_name("variable"),
217 _ => None,
218 }?;
219
220 let text = if let Some(description) = item
221 .label_details
222 .as_ref()
223 .and_then(|label_details| label_details.description.as_ref())
224 {
225 format!("{} {}", item.label, description)
226 } else if let Some(detail) = &item.detail {
227 format!("{} {}", item.label, detail)
228 } else {
229 item.label.clone()
230 };
231 Some(language::CodeLabel::filtered(
232 text,
233 label_len,
234 item.filter_text.as_deref(),
235 vec![(0..label_len, highlight_id)],
236 ))
237 }
238
239 async fn workspace_configuration(
240 self: Arc<Self>,
241 delegate: &Arc<dyn LspAdapterDelegate>,
242 _: Option<Toolchain>,
243 _: Option<Uri>,
244 cx: &mut AsyncApp,
245 ) -> Result<Value> {
246 let tsdk_path = self.tsdk_path(delegate).await;
247 let config = serde_json::json!({
248 "tsdk": tsdk_path,
249 "suggest": {
250 "completeFunctionCalls": true
251 },
252 "inlayHints": {
253 "parameterNames": {
254 "enabled": "all",
255 "suppressWhenArgumentMatchesName": false
256 },
257 "parameterTypes": {
258 "enabled": true
259 },
260 "variableTypes": {
261 "enabled": true,
262 "suppressWhenTypeMatchesName": false
263 },
264 "propertyDeclarationTypes": {
265 "enabled": true
266 },
267 "functionLikeReturnTypes": {
268 "enabled": true
269 },
270 "enumMemberValues": {
271 "enabled": true
272 }
273 },
274 "implementationsCodeLens": {
275 "enabled": true,
276 "showOnAllClassMethods": true,
277 "showOnInterfaceMethods": true
278 },
279 "referencesCodeLens": {
280 "enabled": true,
281 "showOnAllFunctions": true
282 },
283 "tsserver": {
284 "maxTsServerMemory": 8192
285 },
286 });
287
288 let mut default_workspace_configuration = serde_json::json!({
289 "typescript": config,
290 "javascript": config,
291 "vtsls": {
292 "experimental": {
293 "completion": {
294 "enableServerSideFuzzyMatch": true,
295 "entriesLimit": 5000,
296 }
297 },
298 "autoUseWorkspaceTsdk": true
299 }
300 });
301
302 let override_options = cx.update(|cx| {
303 language_server_settings(delegate.as_ref(), &SERVER_NAME, cx)
304 .and_then(|s| s.settings.clone())
305 });
306
307 if let Some(override_options) = override_options {
308 merge_json_value_into(override_options, &mut default_workspace_configuration)
309 }
310
311 Ok(default_workspace_configuration)
312 }
313
314 fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {
315 VtslsLspAdapter::enhance_diagnostic_message(message)
316 }
317
318 fn language_ids(&self) -> HashMap<LanguageName, String> {
319 HashMap::from_iter([
320 (LanguageName::new_static("TypeScript"), "typescript".into()),
321 (LanguageName::new_static("JavaScript"), "javascript".into()),
322 (LanguageName::new_static("TSX"), "typescriptreact".into()),
323 ])
324 }
325
326 fn process_prompt_response(&self, context: &PromptResponseContext, cx: &mut AsyncApp) {
327 let selected_title = context.selected_action.title.as_str();
328 let is_preference_response =
329 selected_title == ACTION_ALWAYS || selected_title == ACTION_NEVER;
330 if !is_preference_response {
331 return;
332 }
333
334 if context.message.contains(UPDATE_IMPORTS_MESSAGE_PATTERN) {
335 let setting_value = match selected_title {
336 ACTION_ALWAYS => "always",
337 ACTION_NEVER => "never",
338 _ => return,
339 };
340
341 let settings = json!({
342 "typescript": {
343 "updateImportsOnFileMove": {
344 "enabled": setting_value
345 }
346 },
347 "javascript": {
348 "updateImportsOnFileMove": {
349 "enabled": setting_value
350 }
351 }
352 });
353
354 let _ = cx.update(|cx| {
355 update_settings_file(self.fs.clone(), cx, move |content, _| {
356 let lsp_settings = content
357 .project
358 .lsp
359 .0
360 .entry(VTSLS_SERVER_NAME.into())
361 .or_default();
362
363 if let Some(existing) = &mut lsp_settings.settings {
364 merge_json_value_into(settings, existing);
365 } else {
366 lsp_settings.settings = Some(settings);
367 }
368 });
369 });
370 }
371 }
372}
373
374async fn get_cached_ts_server_binary(
375 container_dir: PathBuf,
376 node: &NodeRuntime,
377) -> Option<LanguageServerBinary> {
378 maybe!(async {
379 let server_path = container_dir.join(VtslsLspAdapter::SERVER_PATH);
380 anyhow::ensure!(
381 server_path.exists(),
382 "missing executable in directory {container_dir:?}"
383 );
384 Ok(LanguageServerBinary {
385 path: node.binary_path().await?,
386 env: None,
387 arguments: typescript_server_binary_arguments(&server_path),
388 })
389 })
390 .await
391 .log_err()
392}
393
394#[cfg(test)]
395mod tests {
396 use crate::vtsls::VtslsLspAdapter;
397
398 #[test]
399 fn test_diagnostic_message_to_markdown() {
400 // Leaves simple messages unchanged
401 let message = "The expected type comes from the return type of this signature.";
402
403 let expected = "The expected type comes from the return type of this signature.";
404
405 assert_eq!(
406 VtslsLspAdapter::enhance_diagnostic_message(message).expect("Should be some"),
407 expected
408 );
409
410 // Parses both multi-word and single-word correctly
411 let message = "Property 'baz' is missing in type '{ foo: string; bar: string; }' but required in type 'User'.";
412
413 let expected = "Property `baz` is missing in type \n```typescript\n{ foo: string; bar: string; }\n```\n but required in type `User`.";
414
415 assert_eq!(
416 VtslsLspAdapter::enhance_diagnostic_message(message).expect("Should be some"),
417 expected
418 );
419
420 // Parses multi-and-single word in any order, and ignores existing newlines
421 let message = "Type '() => { foo: string; bar: string; }' is not assignable to type 'GetUserFunction'.\n Property 'baz' is missing in type '{ foo: string; bar: string; }' but required in type 'User'.";
422
423 let expected = "Type \n```typescript\n() => { foo: string; bar: string; }\n```\n is not assignable to type `GetUserFunction`.\n Property `baz` is missing in type \n```typescript\n{ foo: string; bar: string; }\n```\n but required in type `User`.";
424
425 assert_eq!(
426 VtslsLspAdapter::enhance_diagnostic_message(message).expect("Should be some"),
427 expected
428 );
429 }
430}
431