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