Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:40:05.633Z 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

editor_lsp_test_context.rs

528 lines · 18.6 KB · rust
1use std::{
2    borrow::Cow,
3    ops::{Deref, DerefMut, Range},
4    path::Path,
5    sync::Arc,
6};
7
8use anyhow::Result;
9use language::{markdown_lang, rust_lang};
10use multi_buffer::MultiBufferOffset;
11use serde_json::json;
12
13use crate::{Editor, ToPoint};
14use collections::HashSet;
15use futures::Future;
16use futures::stream::StreamExt;
17use gpui::{Context, Entity, Focusable as _, VisualTestContext, Window};
18use indoc::indoc;
19use language::{
20    BlockCommentConfig, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageQueries,
21    point_to_lsp,
22};
23use lsp::{notification, request};
24use project::Project;
25use workspace::{AppState, MultiWorkspace, Workspace, WorkspaceHandle};
26
27use super::editor_test_context::{AssertionContextManager, EditorTestContext};
28
29pub struct EditorLspTestContext {
30    pub cx: EditorTestContext,
31    pub lsp: lsp::FakeLanguageServer,
32    pub workspace: Entity<Workspace>,
33    pub buffer_lsp_url: lsp::Uri,
34}
35
36#[cfg(test)]
37pub(crate) fn git_commit_lang() -> Arc<Language> {
38    Arc::new(Language::new(
39        LanguageConfig {
40            name: "Git Commit".into(),
41            line_comments: vec!["#".into()],
42            ..Default::default()
43        },
44        None,
45    ))
46}
47
48impl EditorLspTestContext {
49    pub async fn new(
50        language: Language,
51        capabilities: lsp::ServerCapabilities,
52        cx: &mut gpui::TestAppContext,
53    ) -> EditorLspTestContext {
54        let app_state = cx.update(AppState::test);
55
56        cx.update(|cx| {
57            assets::Assets.load_test_fonts(cx);
58            crate::init(cx);
59            workspace::init(app_state.clone(), cx);
60        });
61
62        let file_name = format!(
63            "file.{}",
64            language
65                .path_suffixes()
66                .first()
67                .expect("language must have a path suffix for EditorLspTestContext")
68        );
69
70        let project = Project::test(app_state.fs.clone(), [], cx).await;
71
72        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
73        let mut fake_servers = language_registry.register_fake_lsp(
74            language.name(),
75            FakeLspAdapter {
76                capabilities,
77                ..Default::default()
78            },
79        );
80        language_registry.add(Arc::new(language));
81
82        let root = Self::root_path();
83
84        app_state
85            .fs
86            .as_fake()
87            .insert_tree(
88                root,
89                json!({
90                    ".git": {},
91                    "dir": {
92                        file_name.clone(): ""
93                    }
94                }),
95            )
96            .await;
97
98        let window =
99            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
100
101        let workspace = window.root(cx).unwrap();
102
103        let mut cx = VisualTestContext::from_window(*window.deref(), cx);
104        project
105            .update(&mut cx, |project, cx| {
106                project.find_or_create_worktree(root, true, cx)
107            })
108            .await
109            .unwrap();
110        cx.read(|cx| {
111            workspace
112                .read(cx)
113                .workspace()
114                .read(cx)
115                .worktree_scans_complete(cx)
116        })
117        .await;
118        let file = cx.read(|cx| workspace.read(cx).workspace().file_project_paths(cx)[0].clone());
119        let item = workspace
120            .update_in(&mut cx, |workspace, window, cx| {
121                workspace.workspace().update(cx, |workspace, cx| {
122                    workspace.open_path(file, None, true, window, cx)
123                })
124            })
125            .await
126            .expect("Could not open test file");
127        let editor = cx.update(|_, cx| {
128            item.act_as::<Editor>(cx)
129                .expect("Opened test file wasn't an editor")
130        });
131        editor.update_in(&mut cx, |editor, window, cx| {
132            let nav_history = workspace
133                .read(cx)
134                .workspace()
135                .read(cx)
136                .active_pane()
137                .read(cx)
138                .nav_history_for_item(&cx.entity());
139            editor.set_nav_history(Some(nav_history));
140            window.focus(&editor.focus_handle(cx), cx)
141        });
142
143        let lsp = fake_servers.next().await.unwrap();
144
145        // Ensure the language server is fully registered with the buffer
146        cx.executor().run_until_parked();
147
148        let workspace = cx.read(|cx| workspace.read(cx).workspace().clone());
149
150        Self {
151            cx: EditorTestContext {
152                cx,
153                window: window.into(),
154                editor,
155                assertion_cx: AssertionContextManager::new(),
156            },
157            lsp,
158            workspace,
159            buffer_lsp_url: lsp::Uri::from_file_path(root.join("dir").join(file_name)).unwrap(),
160        }
161    }
162
163    pub async fn new_rust(
164        capabilities: lsp::ServerCapabilities,
165        cx: &mut gpui::TestAppContext,
166    ) -> EditorLspTestContext {
167        Self::new(Arc::into_inner(rust_lang()).unwrap(), capabilities, cx).await
168    }
169
170    pub async fn new_typescript(
171        capabilities: lsp::ServerCapabilities,
172        cx: &mut gpui::TestAppContext,
173    ) -> EditorLspTestContext {
174        let mut word_characters: HashSet<char> = Default::default();
175        word_characters.insert('$');
176        word_characters.insert('#');
177        let language = Language::new(
178            LanguageConfig {
179                name: "Typescript".into(),
180                matcher: (LanguageMatcher {
181                    path_suffixes: vec!["ts".to_string()],
182                    ..Default::default()
183                }).into(),
184                brackets: language::BracketPairConfig {
185                    pairs: vec![language::BracketPair {
186                        start: "{".to_string(),
187                        end: "}".to_string(),
188                        close: true,
189                        surround: true,
190                        newline: true,
191                    }],
192                    disabled_scopes_by_bracket_ix: Default::default(),
193                },
194                word_characters,
195                ..Default::default()
196            },
197            Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
198        )
199        .with_queries(LanguageQueries {
200            brackets: Some(Cow::from(indoc! {r#"
201                ("(" @open ")" @close)
202                ("[" @open "]" @close)
203                ("{" @open "}" @close)
204                ("<" @open ">" @close)
205                ("'" @open "'" @close)
206                ("`" @open "`" @close)
207                ("\"" @open "\"" @close)"#})),
208            indents: Some(Cow::from(indoc! {r#"
209                [
210                    (call_expression)
211                    (assignment_expression)
212                    (member_expression)
213                    (lexical_declaration)
214                    (variable_declaration)
215                    (assignment_expression)
216                    (if_statement)
217                    (for_statement)
218                ] @indent
219
220                (_ "[" "]" @end) @indent
221                (_ "<" ">" @end) @indent
222                (_ "{" "}" @end) @indent
223                (_ "(" ")" @end) @indent
224                "#})),
225            text_objects: Some(Cow::from(indoc! {r#"
226                (function_declaration
227                    body: (_
228                        "{"
229                        (_)* @function.inside
230                        "}")) @function.around
231
232                (method_definition
233                    body: (_
234                        "{"
235                        (_)* @function.inside
236                        "}")) @function.around
237
238                ; Arrow function in variable declaration - capture the full declaration
239                ([
240                    (lexical_declaration
241                        (variable_declarator
242                            value: (arrow_function
243                                body: (statement_block
244                                    "{"
245                                    (_)* @function.inside
246                                    "}"))))
247                    (variable_declaration
248                        (variable_declarator
249                            value: (arrow_function
250                                body: (statement_block
251                                    "{"
252                                    (_)* @function.inside
253                                    "}"))))
254                ]) @function.around
255
256                ([
257                    (lexical_declaration
258                        (variable_declarator
259                            value: (arrow_function)))
260                    (variable_declaration
261                        (variable_declarator
262                            value: (arrow_function)))
263                ]) @function.around
264
265                ; Catch-all for arrow functions in other contexts (callbacks, etc.)
266                ((arrow_function) @function.around (#not-has-parent? @function.around variable_declarator))
267                "#})),
268            ..Default::default()
269        })
270        .expect("Could not parse queries");
271
272        Self::new(language, capabilities, cx).await
273    }
274
275    pub async fn new_tsx(
276        capabilities: lsp::ServerCapabilities,
277        cx: &mut gpui::TestAppContext,
278    ) -> EditorLspTestContext {
279        let mut word_characters: HashSet<char> = Default::default();
280        word_characters.insert('$');
281        word_characters.insert('#');
282        let language = Language::new(
283            LanguageConfig {
284                name: "TSX".into(),
285                matcher: (LanguageMatcher {
286                    path_suffixes: vec!["tsx".to_string()],
287                    ..Default::default()
288                }).into(),
289                brackets: language::BracketPairConfig {
290                    pairs: vec![language::BracketPair {
291                        start: "{".to_string(),
292                        end: "}".to_string(),
293                        close: true,
294                        surround: true,
295                        newline: true,
296                    }],
297                    disabled_scopes_by_bracket_ix: Default::default(),
298                },
299                word_characters,
300                ..Default::default()
301            },
302            Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
303        )
304        .with_queries(LanguageQueries {
305            brackets: Some(Cow::from(indoc! {r#"
306                ("(" @open ")" @close)
307                ("[" @open "]" @close)
308                ("{" @open "}" @close)
309                ("<" @open ">" @close)
310                ("<" @open "/>" @close)
311                ("</" @open ">" @close)
312                ("\"" @open "\"" @close)
313                ("'" @open "'" @close)
314                ("`" @open "`" @close)
315                ((jsx_element (jsx_opening_element) @open (jsx_closing_element) @close) (#set! newline.only))"#})),
316            indents: Some(Cow::from(indoc! {r#"
317                [
318                    (call_expression)
319                    (assignment_expression)
320                    (member_expression)
321                    (lexical_declaration)
322                    (variable_declaration)
323                    (assignment_expression)
324                    (if_statement)
325                    (for_statement)
326                ] @indent
327
328                (_ "[" "]" @end) @indent
329                (_ "<" ">" @end) @indent
330                (_ "{" "}" @end) @indent
331                (_ "(" ")" @end) @indent
332
333                (jsx_opening_element ">" @end) @indent
334
335                (jsx_element
336                  (jsx_opening_element) @start
337                  (jsx_closing_element)? @end) @indent
338                "#})),
339            text_objects: Some(Cow::from(indoc! {r#"
340                (function_declaration
341                    body: (_
342                        "{"
343                        (_)* @function.inside
344                        "}")) @function.around
345
346                (method_definition
347                    body: (_
348                        "{"
349                        (_)* @function.inside
350                        "}")) @function.around
351
352                ; Arrow function in variable declaration - capture the full declaration
353                ([
354                    (lexical_declaration
355                        (variable_declarator
356                            value: (arrow_function
357                                body: (statement_block
358                                    "{"
359                                    (_)* @function.inside
360                                    "}"))))
361                    (variable_declaration
362                        (variable_declarator
363                            value: (arrow_function
364                                body: (statement_block
365                                    "{"
366                                    (_)* @function.inside
367                                    "}"))))
368                ]) @function.around
369
370                ([
371                    (lexical_declaration
372                        (variable_declarator
373                            value: (arrow_function)))
374                    (variable_declaration
375                        (variable_declarator
376                            value: (arrow_function)))
377                ]) @function.around
378
379                ; Catch-all for arrow functions in other contexts (callbacks, etc.)
380                ((arrow_function) @function.around (#not-has-parent? @function.around variable_declarator))
381                "#})),
382            ..Default::default()
383        })
384        .expect("Could not parse queries");
385
386        Self::new(language, capabilities, cx).await
387    }
388
389    pub async fn new_html(cx: &mut gpui::TestAppContext) -> Self {
390        let language = Language::new(
391            LanguageConfig {
392                name: "HTML".into(),
393                matcher: (LanguageMatcher {
394                    path_suffixes: vec!["html".into()],
395                    ..Default::default()
396                })
397                .into(),
398                block_comment: Some(BlockCommentConfig {
399                    start: "<!--".into(),
400                    prefix: "".into(),
401                    end: "-->".into(),
402                    tab_size: 0,
403                }),
404                completion_query_characters: ['-'].into_iter().collect(),
405                ..Default::default()
406            },
407            Some(tree_sitter_html::LANGUAGE.into()),
408        )
409        .with_queries(LanguageQueries {
410            brackets: Some(Cow::from(indoc! {r#"
411                ("<" @open "/>" @close)
412                ("</" @open ">" @close)
413                ("<" @open ">" @close)
414                ("\"" @open "\"" @close)"#})),
415            ..Default::default()
416        })
417        .expect("Could not parse queries");
418        Self::new(language, Default::default(), cx).await
419    }
420
421    pub async fn new_markdown_with_rust(cx: &mut gpui::TestAppContext) -> Self {
422        let context = Self::new(
423            Arc::into_inner(markdown_lang()).unwrap(),
424            Default::default(),
425            cx,
426        )
427        .await;
428
429        let language_registry = context.workspace.read_with(cx, |workspace, cx| {
430            workspace.project().read(cx).languages().clone()
431        });
432        language_registry.add(rust_lang());
433
434        context
435    }
436
437    /// Constructs lsp range using a marked string with '[', ']' range delimiters
438    #[track_caller]
439    pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
440        let ranges = self.ranges(marked_text);
441        self.to_lsp_range(MultiBufferOffset(ranges[0].start)..MultiBufferOffset(ranges[0].end))
442    }
443
444    #[expect(clippy::wrong_self_convention, reason = "This is test code")]
445    pub fn to_lsp_range(&mut self, range: Range<MultiBufferOffset>) -> lsp::Range {
446        use language::ToPointUtf16;
447        let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));
448        let start_point = range.start.to_point(&snapshot.buffer_snapshot());
449        let end_point = range.end.to_point(&snapshot.buffer_snapshot());
450
451        self.editor(|editor, _, cx| {
452            let buffer = editor.buffer().read(cx);
453            let (start_buffer, start_offset) =
454                buffer.point_to_buffer_offset(start_point, cx).unwrap();
455            let start = point_to_lsp(start_offset.to_point_utf16(&start_buffer.read(cx)));
456            let (end_buffer, end_offset) = buffer.point_to_buffer_offset(end_point, cx).unwrap();
457            let end = point_to_lsp(end_offset.to_point_utf16(&end_buffer.read(cx)));
458            lsp::Range { start, end }
459        })
460    }
461
462    #[expect(clippy::wrong_self_convention, reason = "This is test code")]
463    pub fn to_lsp(&mut self, offset: MultiBufferOffset) -> lsp::Position {
464        use language::ToPointUtf16;
465
466        let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));
467        let point = offset.to_point(&snapshot.buffer_snapshot());
468
469        self.editor(|editor, _, cx| {
470            let buffer = editor.buffer().read(cx);
471            let (buffer, offset) = buffer.point_to_buffer_offset(point, cx).unwrap();
472            point_to_lsp(offset.to_point_utf16(&buffer.read(cx)))
473        })
474    }
475
476    pub fn update_workspace<F, T>(&mut self, update: F) -> T
477    where
478        F: FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
479    {
480        self.workspace.update_in(&mut self.cx.cx, update)
481    }
482
483    pub fn set_request_handler<T, F, Fut>(
484        &self,
485        mut handler: F,
486    ) -> futures::channel::mpsc::UnboundedReceiver<()>
487    where
488        T: 'static + request::Request,
489        T::Params: 'static + Send,
490        F: 'static + Send + FnMut(lsp::Uri, T::Params, gpui::AsyncApp) -> Fut,
491        Fut: 'static + Future<Output = Result<T::Result>>,
492    {
493        let url = self.buffer_lsp_url.clone();
494        self.lsp.set_request_handler::<T, _, _>(move |params, cx| {
495            let url = url.clone();
496            handler(url, params, cx)
497        })
498    }
499
500    pub fn notify<T: notification::Notification>(&self, params: T::Params) {
501        self.lsp.notify::<T>(params);
502    }
503
504    #[cfg(target_os = "windows")]
505    pub fn root_path() -> &'static Path {
506        Path::new("C:\\root")
507    }
508
509    #[cfg(not(target_os = "windows"))]
510    pub fn root_path() -> &'static Path {
511        Path::new("/root")
512    }
513}
514
515impl Deref for EditorLspTestContext {
516    type Target = EditorTestContext;
517
518    fn deref(&self) -> &Self::Target {
519        &self.cx
520    }
521}
522
523impl DerefMut for EditorLspTestContext {
524    fn deref_mut(&mut self) -> &mut Self::Target {
525        &mut self.cx
526    }
527}
528
Served at tenant.openagents/omega Member data and write actions are omitted.