Skip to repository content528 lines · 18.6 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:40:05.633Z 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
editor_lsp_test_context.rs
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