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