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