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 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    pub async fn new_markdown_with_rust(cx: &mut gpui::TestAppContext) -> Self {
317        let context = Self::new(
318            Arc::into_inner(markdown_lang()).unwrap(),
319            Default::default(),
320            cx,
321        )
322        .await;
323
324        let language_registry = context.workspace.read_with(cx, |workspace, cx| {
325            workspace.project().read(cx).languages().clone()
326        });
327        language_registry.add(rust_lang());
328
329        context
330    }
331
332    /// Constructs lsp range using a marked string with '[', ']' range delimiters
333    #[track_caller]
334    pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
335        let ranges = self.ranges(marked_text);
336        self.to_lsp_range(ranges[0].clone())
337    }
338
339    #[expect(clippy::wrong_self_convention, reason = "This is test code")]
340    pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
341        let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));
342        let start_point = range.start.to_point(&snapshot.buffer_snapshot());
343        let end_point = range.end.to_point(&snapshot.buffer_snapshot());
344
345        self.editor(|editor, _, cx| {
346            let buffer = editor.buffer().read(cx);
347            let start = point_to_lsp(
348                buffer
349                    .point_to_buffer_offset(start_point, cx)
350                    .unwrap()
351                    .1
352                    .to_point_utf16(&buffer.read(cx)),
353            );
354            let end = point_to_lsp(
355                buffer
356                    .point_to_buffer_offset(end_point, cx)
357                    .unwrap()
358                    .1
359                    .to_point_utf16(&buffer.read(cx)),
360            );
361
362            lsp::Range { start, end }
363        })
364    }
365
366    #[expect(clippy::wrong_self_convention, reason = "This is test code")]
367    pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
368        let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));
369        let point = offset.to_point(&snapshot.buffer_snapshot());
370
371        self.editor(|editor, _, cx| {
372            let buffer = editor.buffer().read(cx);
373            point_to_lsp(
374                buffer
375                    .point_to_buffer_offset(point, cx)
376                    .unwrap()
377                    .1
378                    .to_point_utf16(&buffer.read(cx)),
379            )
380        })
381    }
382
383    pub fn update_workspace<F, T>(&mut self, update: F) -> T
384    where
385        F: FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
386    {
387        self.workspace.update_in(&mut self.cx.cx, update)
388    }
389
390    pub fn set_request_handler<T, F, Fut>(
391        &self,
392        mut handler: F,
393    ) -> futures::channel::mpsc::UnboundedReceiver<()>
394    where
395        T: 'static + request::Request,
396        T::Params: 'static + Send,
397        F: 'static + Send + FnMut(lsp::Uri, T::Params, gpui::AsyncApp) -> Fut,
398        Fut: 'static + Future<Output = Result<T::Result>>,
399    {
400        let url = self.buffer_lsp_url.clone();
401        self.lsp.set_request_handler::<T, _, _>(move |params, cx| {
402            let url = url.clone();
403            handler(url, params, cx)
404        })
405    }
406
407    pub fn notify<T: notification::Notification>(&self, params: T::Params) {
408        self.lsp.notify::<T>(params);
409    }
410
411    #[cfg(target_os = "windows")]
412    fn root_path() -> &'static Path {
413        Path::new("C:\\root")
414    }
415
416    #[cfg(not(target_os = "windows"))]
417    fn root_path() -> &'static Path {
418        Path::new("/root")
419    }
420}
421
422impl Deref for EditorLspTestContext {
423    type Target = EditorTestContext;
424
425    fn deref(&self) -> &Self::Target {
426        &self.cx
427    }
428}
429
430impl DerefMut for EditorLspTestContext {
431    fn deref_mut(&mut self) -> &mut Self::Target {
432        &mut self.cx
433    }
434}