test.rs

  1use std::{
  2    ops::{Deref, DerefMut, Range},
  3    sync::Arc,
  4};
  5
  6use futures::StreamExt;
  7use indoc::indoc;
  8
  9use collections::BTreeMap;
 10use gpui::{json, keymap::Keystroke, AppContext, ModelHandle, ViewContext, ViewHandle};
 11use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, Selection};
 12use project::Project;
 13use settings::Settings;
 14use util::{
 15    assert_set_eq, set_eq,
 16    test::{marked_text, marked_text_ranges, marked_text_ranges_by, SetEqError},
 17};
 18use workspace::{pane, AppState, Workspace, WorkspaceHandle};
 19
 20use crate::{
 21    display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
 22    multi_buffer::ToPointUtf16,
 23    AnchorRangeExt, Autoscroll, DisplayPoint, Editor, EditorMode, MultiBuffer, ToPoint,
 24};
 25
 26#[cfg(test)]
 27#[ctor::ctor]
 28fn init_logger() {
 29    if std::env::var("RUST_LOG").is_ok() {
 30        env_logger::init();
 31    }
 32}
 33
 34// Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one.
 35pub fn marked_display_snapshot(
 36    text: &str,
 37    cx: &mut gpui::MutableAppContext,
 38) -> (DisplaySnapshot, Vec<DisplayPoint>) {
 39    let (unmarked_text, markers) = marked_text(text);
 40
 41    let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
 42    let font_id = cx
 43        .font_cache()
 44        .select_font(family_id, &Default::default())
 45        .unwrap();
 46    let font_size = 14.0;
 47
 48    let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
 49    let display_map =
 50        cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
 51    let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
 52    let markers = markers
 53        .into_iter()
 54        .map(|offset| offset.to_display_point(&snapshot))
 55        .collect();
 56
 57    (snapshot, markers)
 58}
 59
 60pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext<Editor>) {
 61    let (umarked_text, text_ranges) = marked_text_ranges(marked_text);
 62    assert_eq!(editor.text(cx), umarked_text);
 63    editor.change_selections(None, cx, |s| s.select_ranges(text_ranges));
 64}
 65
 66pub fn assert_text_with_selections(
 67    editor: &mut Editor,
 68    marked_text: &str,
 69    cx: &mut ViewContext<Editor>,
 70) {
 71    let (unmarked_text, text_ranges) = marked_text_ranges(marked_text);
 72
 73    assert_eq!(editor.text(cx), unmarked_text);
 74    assert_eq!(editor.selections.ranges(cx), text_ranges);
 75}
 76
 77pub(crate) fn build_editor(
 78    buffer: ModelHandle<MultiBuffer>,
 79    cx: &mut ViewContext<Editor>,
 80) -> Editor {
 81    Editor::new(EditorMode::Full, buffer, None, None, cx)
 82}
 83
 84pub struct EditorTestContext<'a> {
 85    pub cx: &'a mut gpui::TestAppContext,
 86    pub window_id: usize,
 87    pub editor: ViewHandle<Editor>,
 88}
 89
 90impl<'a> EditorTestContext<'a> {
 91    pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
 92        let (window_id, editor) = cx.update(|cx| {
 93            cx.set_global(Settings::test(cx));
 94            crate::init(cx);
 95
 96            let (window_id, editor) = cx.add_window(Default::default(), |cx| {
 97                build_editor(MultiBuffer::build_simple("", cx), cx)
 98            });
 99
100            editor.update(cx, |_, cx| cx.focus_self());
101
102            (window_id, editor)
103        });
104
105        Self {
106            cx,
107            window_id,
108            editor,
109        }
110    }
111
112    pub fn editor<F, T>(&mut self, read: F) -> T
113    where
114        F: FnOnce(&Editor, &AppContext) -> T,
115    {
116        self.editor.read_with(self.cx, read)
117    }
118
119    pub fn update_editor<F, T>(&mut self, update: F) -> T
120    where
121        F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
122    {
123        self.editor.update(self.cx, update)
124    }
125
126    pub fn buffer_text(&mut self) -> String {
127        self.editor.read_with(self.cx, |editor, cx| {
128            editor.buffer.read(cx).snapshot(cx).text()
129        })
130    }
131
132    pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
133        let keystroke = Keystroke::parse(keystroke_text).unwrap();
134        let input = if keystroke.modified() {
135            None
136        } else {
137            Some(keystroke.key.clone())
138        };
139        self.cx
140            .dispatch_keystroke(self.window_id, keystroke, input, false);
141    }
142
143    pub fn simulate_keystrokes<const COUNT: usize>(&mut self, keystroke_texts: [&str; COUNT]) {
144        for keystroke_text in keystroke_texts.into_iter() {
145            self.simulate_keystroke(keystroke_text);
146        }
147    }
148
149    pub fn display_point(&mut self, cursor_location: &str) -> DisplayPoint {
150        let (_, locations) = marked_text(cursor_location);
151        let snapshot = self
152            .editor
153            .update(self.cx, |editor, cx| editor.snapshot(cx));
154        locations[0].to_display_point(&snapshot.display_snapshot)
155    }
156
157    // Sets the editor state via a marked string.
158    // `|` characters represent empty selections
159    // `[` to `}` represents a non empty selection with the head at `}`
160    // `{` to `]` represents a non empty selection with the head at `{`
161    pub fn set_state(&mut self, text: &str) {
162        self.editor.update(self.cx, |editor, cx| {
163            let (unmarked_text, mut selection_ranges) = marked_text_ranges_by(
164                &text,
165                vec!['|'.into(), ('[', '}').into(), ('{', ']').into()],
166            );
167            editor.set_text(unmarked_text, cx);
168
169            let mut selections: Vec<Range<usize>> =
170                selection_ranges.remove(&'|'.into()).unwrap_or_default();
171            selections.extend(
172                selection_ranges
173                    .remove(&('{', ']').into())
174                    .unwrap_or_default()
175                    .into_iter()
176                    .map(|range| range.end..range.start),
177            );
178            selections.extend(
179                selection_ranges
180                    .remove(&('[', '}').into())
181                    .unwrap_or_default(),
182            );
183
184            editor.change_selections(Some(Autoscroll::Fit), cx, |s| s.select_ranges(selections));
185        })
186    }
187
188    // Asserts the editor state via a marked string.
189    // `|` characters represent empty selections
190    // `[` to `}` represents a non empty selection with the head at `}`
191    // `{` to `]` represents a non empty selection with the head at `{`
192    pub fn assert_editor_state(&mut self, text: &str) {
193        let (unmarked_text, mut selection_ranges) = marked_text_ranges_by(
194            &text,
195            vec!['|'.into(), ('[', '}').into(), ('{', ']').into()],
196        );
197        let buffer_text = self.buffer_text();
198        assert_eq!(
199            buffer_text, unmarked_text,
200            "Unmarked text doesn't match buffer text"
201        );
202
203        let expected_empty_selections = selection_ranges.remove(&'|'.into()).unwrap_or_default();
204        let expected_reverse_selections = selection_ranges
205            .remove(&('{', ']').into())
206            .unwrap_or_default();
207        let expected_forward_selections = selection_ranges
208            .remove(&('[', '}').into())
209            .unwrap_or_default();
210
211        self.assert_selections(
212            expected_empty_selections,
213            expected_reverse_selections,
214            expected_forward_selections,
215            Some(text.to_string()),
216        )
217    }
218
219    pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
220        let (unmarked, mut ranges) = marked_text_ranges_by(marked_text, vec![('[', ']').into()]);
221        assert_eq!(unmarked, self.buffer_text());
222
223        let asserted_ranges = ranges.remove(&('[', ']').into()).unwrap();
224        let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
225        let actual_ranges: Vec<Range<usize>> = snapshot
226            .display_snapshot
227            .highlight_ranges::<Tag>()
228            .map(|ranges| ranges.as_ref().clone().1)
229            .unwrap_or_default()
230            .into_iter()
231            .map(|range| range.to_offset(&snapshot.buffer_snapshot))
232            .collect();
233
234        assert_set_eq!(asserted_ranges, actual_ranges);
235    }
236
237    pub fn assert_editor_selections(&mut self, expected_selections: Vec<Selection<usize>>) {
238        let mut empty_selections = Vec::new();
239        let mut reverse_selections = Vec::new();
240        let mut forward_selections = Vec::new();
241
242        for selection in expected_selections {
243            let range = selection.range();
244            if selection.is_empty() {
245                empty_selections.push(range);
246            } else if selection.reversed {
247                reverse_selections.push(range);
248            } else {
249                forward_selections.push(range)
250            }
251        }
252
253        self.assert_selections(
254            empty_selections,
255            reverse_selections,
256            forward_selections,
257            None,
258        )
259    }
260
261    fn assert_selections(
262        &mut self,
263        expected_empty_selections: Vec<Range<usize>>,
264        expected_reverse_selections: Vec<Range<usize>>,
265        expected_forward_selections: Vec<Range<usize>>,
266        asserted_text: Option<String>,
267    ) {
268        let (empty_selections, reverse_selections, forward_selections) =
269            self.editor.read_with(self.cx, |editor, cx| {
270                let mut empty_selections = Vec::new();
271                let mut reverse_selections = Vec::new();
272                let mut forward_selections = Vec::new();
273
274                for selection in editor.selections.all::<usize>(cx) {
275                    let range = selection.range();
276                    if selection.is_empty() {
277                        empty_selections.push(range);
278                    } else if selection.reversed {
279                        reverse_selections.push(range);
280                    } else {
281                        forward_selections.push(range)
282                    }
283                }
284
285                (empty_selections, reverse_selections, forward_selections)
286            });
287
288        let asserted_selections = asserted_text.unwrap_or_else(|| {
289            self.insert_markers(
290                &expected_empty_selections,
291                &expected_reverse_selections,
292                &expected_forward_selections,
293            )
294        });
295        let actual_selections =
296            self.insert_markers(&empty_selections, &reverse_selections, &forward_selections);
297
298        let unmarked_text = self.buffer_text();
299        let all_eq: Result<(), SetEqError<String>> =
300            set_eq!(expected_empty_selections, empty_selections)
301                .map_err(|err| {
302                    err.map(|missing| {
303                        let mut error_text = unmarked_text.clone();
304                        error_text.insert(missing.start, '|');
305                        error_text
306                    })
307                })
308                .and_then(|_| {
309                    set_eq!(expected_reverse_selections, reverse_selections).map_err(|err| {
310                        err.map(|missing| {
311                            let mut error_text = unmarked_text.clone();
312                            error_text.insert(missing.start, '{');
313                            error_text.insert(missing.end, ']');
314                            error_text
315                        })
316                    })
317                })
318                .and_then(|_| {
319                    set_eq!(expected_forward_selections, forward_selections).map_err(|err| {
320                        err.map(|missing| {
321                            let mut error_text = unmarked_text.clone();
322                            error_text.insert(missing.start, '[');
323                            error_text.insert(missing.end, '}');
324                            error_text
325                        })
326                    })
327                });
328
329        match all_eq {
330            Err(SetEqError::LeftMissing(location_text)) => {
331                panic!(
332                    indoc! {"
333                        Editor has extra selection
334                        Extra Selection Location:
335                        {}
336                        Asserted selections:
337                        {}
338                        Actual selections:
339                        {}"},
340                    location_text, asserted_selections, actual_selections,
341                );
342            }
343            Err(SetEqError::RightMissing(location_text)) => {
344                panic!(
345                    indoc! {"
346                        Editor is missing empty selection
347                        Missing Selection Location:
348                        {}
349                        Asserted selections:
350                        {}
351                        Actual selections:
352                        {}"},
353                    location_text, asserted_selections, actual_selections,
354                );
355            }
356            _ => {}
357        }
358    }
359
360    fn insert_markers(
361        &mut self,
362        empty_selections: &Vec<Range<usize>>,
363        reverse_selections: &Vec<Range<usize>>,
364        forward_selections: &Vec<Range<usize>>,
365    ) -> String {
366        let mut editor_text_with_selections = self.buffer_text();
367        let mut selection_marks = BTreeMap::new();
368        for range in empty_selections {
369            selection_marks.insert(&range.start, '|');
370        }
371        for range in reverse_selections {
372            selection_marks.insert(&range.start, '{');
373            selection_marks.insert(&range.end, ']');
374        }
375        for range in forward_selections {
376            selection_marks.insert(&range.start, '[');
377            selection_marks.insert(&range.end, '}');
378        }
379        for (offset, mark) in selection_marks.into_iter().rev() {
380            editor_text_with_selections.insert(*offset, mark);
381        }
382
383        editor_text_with_selections
384    }
385
386    pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
387        self.cx.update(|cx| {
388            let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
389            let expected_content = expected_content.map(|content| content.to_owned());
390            assert_eq!(actual_content, expected_content);
391        })
392    }
393}
394
395impl<'a> Deref for EditorTestContext<'a> {
396    type Target = gpui::TestAppContext;
397
398    fn deref(&self) -> &Self::Target {
399        self.cx
400    }
401}
402
403impl<'a> DerefMut for EditorTestContext<'a> {
404    fn deref_mut(&mut self) -> &mut Self::Target {
405        &mut self.cx
406    }
407}
408
409pub struct EditorLspTestContext<'a> {
410    pub cx: EditorTestContext<'a>,
411    pub lsp: lsp::FakeLanguageServer,
412    pub workspace: ViewHandle<Workspace>,
413}
414
415impl<'a> EditorLspTestContext<'a> {
416    pub async fn new(
417        mut language: Language,
418        capabilities: lsp::ServerCapabilities,
419        cx: &'a mut gpui::TestAppContext,
420    ) -> EditorLspTestContext<'a> {
421        use json::json;
422
423        cx.update(|cx| {
424            crate::init(cx);
425            pane::init(cx);
426        });
427
428        let params = cx.update(AppState::test);
429
430        let file_name = format!(
431            "file.{}",
432            language
433                .path_suffixes()
434                .first()
435                .unwrap_or(&"txt".to_string())
436        );
437
438        let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
439            capabilities,
440            ..Default::default()
441        });
442
443        let project = Project::test(params.fs.clone(), [], cx).await;
444        project.update(cx, |project, _| project.languages().add(Arc::new(language)));
445
446        params
447            .fs
448            .as_fake()
449            .insert_tree("/root", json!({ "dir": { file_name: "" }}))
450            .await;
451
452        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
453        project
454            .update(cx, |project, cx| {
455                project.find_or_create_local_worktree("/root", true, cx)
456            })
457            .await
458            .unwrap();
459        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
460            .await;
461
462        let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
463        let item = workspace
464            .update(cx, |workspace, cx| workspace.open_path(file, true, cx))
465            .await
466            .expect("Could not open test file");
467
468        let editor = cx.update(|cx| {
469            item.act_as::<Editor>(cx)
470                .expect("Opened test file wasn't an editor")
471        });
472        editor.update(cx, |_, cx| cx.focus_self());
473
474        let lsp = fake_servers.next().await.unwrap();
475
476        Self {
477            cx: EditorTestContext {
478                cx,
479                window_id,
480                editor,
481            },
482            lsp,
483            workspace,
484        }
485    }
486
487    pub async fn new_rust(
488        capabilities: lsp::ServerCapabilities,
489        cx: &'a mut gpui::TestAppContext,
490    ) -> EditorLspTestContext<'a> {
491        let language = Language::new(
492            LanguageConfig {
493                name: "Rust".into(),
494                path_suffixes: vec!["rs".to_string()],
495                ..Default::default()
496            },
497            Some(tree_sitter_rust::language()),
498        );
499
500        Self::new(language, capabilities, cx).await
501    }
502
503    // Constructs lsp range using a marked string with '[', ']' range delimiters
504    pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
505        let (unmarked, mut ranges) = marked_text_ranges_by(marked_text, vec![('[', ']').into()]);
506        assert_eq!(unmarked, self.cx.buffer_text());
507        let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
508
509        let offset_range = ranges.remove(&('[', ']').into()).unwrap()[0].clone();
510        let start_point = offset_range.start.to_point(&snapshot.buffer_snapshot);
511        let end_point = offset_range.end.to_point(&snapshot.buffer_snapshot);
512        self.editor(|editor, cx| {
513            let buffer = editor.buffer().read(cx);
514            let start = point_to_lsp(
515                buffer
516                    .point_to_buffer_offset(start_point, cx)
517                    .unwrap()
518                    .1
519                    .to_point_utf16(&buffer.read(cx)),
520            );
521            let end = point_to_lsp(
522                buffer
523                    .point_to_buffer_offset(end_point, cx)
524                    .unwrap()
525                    .1
526                    .to_point_utf16(&buffer.read(cx)),
527            );
528
529            lsp::Range { start, end }
530        })
531    }
532
533    pub fn update_workspace<F, T>(&mut self, update: F) -> T
534    where
535        F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
536    {
537        self.workspace.update(self.cx.cx, update)
538    }
539}
540
541impl<'a> Deref for EditorLspTestContext<'a> {
542    type Target = EditorTestContext<'a>;
543
544    fn deref(&self) -> &Self::Target {
545        &self.cx
546    }
547}
548
549impl<'a> DerefMut for EditorLspTestContext<'a> {
550    fn deref_mut(&mut self) -> &mut Self::Target {
551        &mut self.cx
552    }
553}