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