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 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>(&params);
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}