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