Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:43:48.510Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

vtsls.rs

431 lines · 14.4 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.