editor_lsp_test_context.rs

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