1use super::*;
2use crate::{
3 scroll::scroll_amount::ScrollAmount,
4 test::{
5 assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
6 editor_test_context::EditorTestContext, select_ranges,
7 },
8 JoinLines,
9};
10use futures::StreamExt;
11use gpui::{div, TestAppContext, VisualTestContext, WindowOptions};
12use indoc::indoc;
13use language::{
14 language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
15 BracketPairConfig,
16 Capability::ReadWrite,
17 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override, Point,
18};
19use parking_lot::Mutex;
20use project::project_settings::{LspSettings, ProjectSettings};
21use project::FakeFs;
22use serde_json::{self, json};
23use std::sync::atomic;
24use std::sync::atomic::AtomicUsize;
25use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
26use unindent::Unindent;
27use util::{
28 assert_set_eq,
29 test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
30};
31use workspace::{
32 item::{FollowEvent, FollowableItem, Item, ItemHandle},
33 NavigationEntry, ViewId,
34};
35
36#[gpui::test]
37fn test_edit_events(cx: &mut TestAppContext) {
38 init_test(cx, |_| {});
39
40 let buffer = cx.new_model(|cx| {
41 let mut buffer =
42 language::Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), "123456");
43 buffer.set_group_interval(Duration::from_secs(1));
44 buffer
45 });
46
47 let events = Rc::new(RefCell::new(Vec::new()));
48 let editor1 = cx.add_window({
49 let events = events.clone();
50 |cx| {
51 let view = cx.view().clone();
52 cx.subscribe(&view, move |_, _, event: &EditorEvent, _| {
53 if matches!(event, EditorEvent::Edited | EditorEvent::BufferEdited) {
54 events.borrow_mut().push(("editor1", event.clone()));
55 }
56 })
57 .detach();
58 Editor::for_buffer(buffer.clone(), None, cx)
59 }
60 });
61
62 let editor2 = cx.add_window({
63 let events = events.clone();
64 |cx| {
65 cx.subscribe(&cx.view().clone(), move |_, _, event: &EditorEvent, _| {
66 if matches!(event, EditorEvent::Edited | EditorEvent::BufferEdited) {
67 events.borrow_mut().push(("editor2", event.clone()));
68 }
69 })
70 .detach();
71 Editor::for_buffer(buffer.clone(), None, cx)
72 }
73 });
74
75 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
76
77 // Mutating editor 1 will emit an `Edited` event only for that editor.
78 _ = editor1.update(cx, |editor, cx| editor.insert("X", cx));
79 assert_eq!(
80 mem::take(&mut *events.borrow_mut()),
81 [
82 ("editor1", EditorEvent::Edited),
83 ("editor1", EditorEvent::BufferEdited),
84 ("editor2", EditorEvent::BufferEdited),
85 ]
86 );
87
88 // Mutating editor 2 will emit an `Edited` event only for that editor.
89 _ = editor2.update(cx, |editor, cx| editor.delete(&Delete, cx));
90 assert_eq!(
91 mem::take(&mut *events.borrow_mut()),
92 [
93 ("editor2", EditorEvent::Edited),
94 ("editor1", EditorEvent::BufferEdited),
95 ("editor2", EditorEvent::BufferEdited),
96 ]
97 );
98
99 // Undoing on editor 1 will emit an `Edited` event only for that editor.
100 _ = editor1.update(cx, |editor, cx| editor.undo(&Undo, cx));
101 assert_eq!(
102 mem::take(&mut *events.borrow_mut()),
103 [
104 ("editor1", EditorEvent::Edited),
105 ("editor1", EditorEvent::BufferEdited),
106 ("editor2", EditorEvent::BufferEdited),
107 ]
108 );
109
110 // Redoing on editor 1 will emit an `Edited` event only for that editor.
111 _ = editor1.update(cx, |editor, cx| editor.redo(&Redo, cx));
112 assert_eq!(
113 mem::take(&mut *events.borrow_mut()),
114 [
115 ("editor1", EditorEvent::Edited),
116 ("editor1", EditorEvent::BufferEdited),
117 ("editor2", EditorEvent::BufferEdited),
118 ]
119 );
120
121 // Undoing on editor 2 will emit an `Edited` event only for that editor.
122 _ = editor2.update(cx, |editor, cx| editor.undo(&Undo, cx));
123 assert_eq!(
124 mem::take(&mut *events.borrow_mut()),
125 [
126 ("editor2", EditorEvent::Edited),
127 ("editor1", EditorEvent::BufferEdited),
128 ("editor2", EditorEvent::BufferEdited),
129 ]
130 );
131
132 // Redoing on editor 2 will emit an `Edited` event only for that editor.
133 _ = editor2.update(cx, |editor, cx| editor.redo(&Redo, cx));
134 assert_eq!(
135 mem::take(&mut *events.borrow_mut()),
136 [
137 ("editor2", EditorEvent::Edited),
138 ("editor1", EditorEvent::BufferEdited),
139 ("editor2", EditorEvent::BufferEdited),
140 ]
141 );
142
143 // No event is emitted when the mutation is a no-op.
144 _ = editor2.update(cx, |editor, cx| {
145 editor.change_selections(None, cx, |s| s.select_ranges([0..0]));
146
147 editor.backspace(&Backspace, cx);
148 });
149 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
150}
151
152#[gpui::test]
153fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
154 init_test(cx, |_| {});
155
156 let mut now = Instant::now();
157 let buffer = cx.new_model(|cx| {
158 language::Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), "123456")
159 });
160 let group_interval = buffer.update(cx, |buffer, _| buffer.transaction_group_interval());
161 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
162 let editor = cx.add_window(|cx| build_editor(buffer.clone(), cx));
163
164 _ = editor.update(cx, |editor, cx| {
165 editor.start_transaction_at(now, cx);
166 editor.change_selections(None, cx, |s| s.select_ranges([2..4]));
167
168 editor.insert("cd", cx);
169 editor.end_transaction_at(now, cx);
170 assert_eq!(editor.text(cx), "12cd56");
171 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
172
173 editor.start_transaction_at(now, cx);
174 editor.change_selections(None, cx, |s| s.select_ranges([4..5]));
175 editor.insert("e", cx);
176 editor.end_transaction_at(now, cx);
177 assert_eq!(editor.text(cx), "12cde6");
178 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
179
180 now += group_interval + Duration::from_millis(1);
181 editor.change_selections(None, cx, |s| s.select_ranges([2..2]));
182
183 // Simulate an edit in another editor
184 _ = buffer.update(cx, |buffer, cx| {
185 buffer.start_transaction_at(now, cx);
186 buffer.edit([(0..1, "a")], None, cx);
187 buffer.edit([(1..1, "b")], None, cx);
188 buffer.end_transaction_at(now, cx);
189 });
190
191 assert_eq!(editor.text(cx), "ab2cde6");
192 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
193
194 // Last transaction happened past the group interval in a different editor.
195 // Undo it individually and don't restore selections.
196 editor.undo(&Undo, cx);
197 assert_eq!(editor.text(cx), "12cde6");
198 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
199
200 // First two transactions happened within the group interval in this editor.
201 // Undo them together and restore selections.
202 editor.undo(&Undo, cx);
203 editor.undo(&Undo, cx); // Undo stack is empty here, so this is a no-op.
204 assert_eq!(editor.text(cx), "123456");
205 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
206
207 // Redo the first two transactions together.
208 editor.redo(&Redo, cx);
209 assert_eq!(editor.text(cx), "12cde6");
210 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
211
212 // Redo the last transaction on its own.
213 editor.redo(&Redo, cx);
214 assert_eq!(editor.text(cx), "ab2cde6");
215 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
216
217 // Test empty transactions.
218 editor.start_transaction_at(now, cx);
219 editor.end_transaction_at(now, cx);
220 editor.undo(&Undo, cx);
221 assert_eq!(editor.text(cx), "12cde6");
222 });
223}
224
225#[gpui::test]
226fn test_ime_composition(cx: &mut TestAppContext) {
227 init_test(cx, |_| {});
228
229 let buffer = cx.new_model(|cx| {
230 let mut buffer =
231 language::Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), "abcde");
232 // Ensure automatic grouping doesn't occur.
233 buffer.set_group_interval(Duration::ZERO);
234 buffer
235 });
236
237 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
238 cx.add_window(|cx| {
239 let mut editor = build_editor(buffer.clone(), cx);
240
241 // Start a new IME composition.
242 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx);
243 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, cx);
244 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, cx);
245 assert_eq!(editor.text(cx), "äbcde");
246 assert_eq!(
247 editor.marked_text_ranges(cx),
248 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
249 );
250
251 // Finalize IME composition.
252 editor.replace_text_in_range(None, "ā", cx);
253 assert_eq!(editor.text(cx), "ābcde");
254 assert_eq!(editor.marked_text_ranges(cx), None);
255
256 // IME composition edits are grouped and are undone/redone at once.
257 editor.undo(&Default::default(), cx);
258 assert_eq!(editor.text(cx), "abcde");
259 assert_eq!(editor.marked_text_ranges(cx), None);
260 editor.redo(&Default::default(), cx);
261 assert_eq!(editor.text(cx), "ābcde");
262 assert_eq!(editor.marked_text_ranges(cx), None);
263
264 // Start a new IME composition.
265 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx);
266 assert_eq!(
267 editor.marked_text_ranges(cx),
268 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
269 );
270
271 // Undoing during an IME composition cancels it.
272 editor.undo(&Default::default(), cx);
273 assert_eq!(editor.text(cx), "ābcde");
274 assert_eq!(editor.marked_text_ranges(cx), None);
275
276 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
277 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, cx);
278 assert_eq!(editor.text(cx), "ābcdè");
279 assert_eq!(
280 editor.marked_text_ranges(cx),
281 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
282 );
283
284 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
285 editor.replace_text_in_range(Some(4..999), "ę", cx);
286 assert_eq!(editor.text(cx), "ābcdę");
287 assert_eq!(editor.marked_text_ranges(cx), None);
288
289 // Start a new IME composition with multiple cursors.
290 editor.change_selections(None, cx, |s| {
291 s.select_ranges([
292 OffsetUtf16(1)..OffsetUtf16(1),
293 OffsetUtf16(3)..OffsetUtf16(3),
294 OffsetUtf16(5)..OffsetUtf16(5),
295 ])
296 });
297 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, cx);
298 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
299 assert_eq!(
300 editor.marked_text_ranges(cx),
301 Some(vec![
302 OffsetUtf16(0)..OffsetUtf16(3),
303 OffsetUtf16(4)..OffsetUtf16(7),
304 OffsetUtf16(8)..OffsetUtf16(11)
305 ])
306 );
307
308 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
309 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, cx);
310 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
311 assert_eq!(
312 editor.marked_text_ranges(cx),
313 Some(vec![
314 OffsetUtf16(1)..OffsetUtf16(2),
315 OffsetUtf16(5)..OffsetUtf16(6),
316 OffsetUtf16(9)..OffsetUtf16(10)
317 ])
318 );
319
320 // Finalize IME composition with multiple cursors.
321 editor.replace_text_in_range(Some(9..10), "2", cx);
322 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
323 assert_eq!(editor.marked_text_ranges(cx), None);
324
325 editor
326 });
327}
328
329#[gpui::test]
330fn test_selection_with_mouse(cx: &mut TestAppContext) {
331 init_test(cx, |_| {});
332
333 let editor = cx.add_window(|cx| {
334 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
335 build_editor(buffer, cx)
336 });
337
338 _ = editor.update(cx, |view, cx| {
339 view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx);
340 });
341 assert_eq!(
342 editor
343 .update(cx, |view, cx| view.selections.display_ranges(cx))
344 .unwrap(),
345 [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)]
346 );
347
348 _ = editor.update(cx, |view, cx| {
349 view.update_selection(
350 DisplayPoint::new(3, 3),
351 0,
352 gpui::Point::<f32>::default(),
353 cx,
354 );
355 });
356
357 assert_eq!(
358 editor
359 .update(cx, |view, cx| view.selections.display_ranges(cx))
360 .unwrap(),
361 [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
362 );
363
364 _ = editor.update(cx, |view, cx| {
365 view.update_selection(
366 DisplayPoint::new(1, 1),
367 0,
368 gpui::Point::<f32>::default(),
369 cx,
370 );
371 });
372
373 assert_eq!(
374 editor
375 .update(cx, |view, cx| view.selections.display_ranges(cx))
376 .unwrap(),
377 [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)]
378 );
379
380 _ = editor.update(cx, |view, cx| {
381 view.end_selection(cx);
382 view.update_selection(
383 DisplayPoint::new(3, 3),
384 0,
385 gpui::Point::<f32>::default(),
386 cx,
387 );
388 });
389
390 assert_eq!(
391 editor
392 .update(cx, |view, cx| view.selections.display_ranges(cx))
393 .unwrap(),
394 [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)]
395 );
396
397 _ = editor.update(cx, |view, cx| {
398 view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx);
399 view.update_selection(
400 DisplayPoint::new(0, 0),
401 0,
402 gpui::Point::<f32>::default(),
403 cx,
404 );
405 });
406
407 assert_eq!(
408 editor
409 .update(cx, |view, cx| view.selections.display_ranges(cx))
410 .unwrap(),
411 [
412 DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1),
413 DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)
414 ]
415 );
416
417 _ = editor.update(cx, |view, cx| {
418 view.end_selection(cx);
419 });
420
421 assert_eq!(
422 editor
423 .update(cx, |view, cx| view.selections.display_ranges(cx))
424 .unwrap(),
425 [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)]
426 );
427}
428
429#[gpui::test]
430fn test_canceling_pending_selection(cx: &mut TestAppContext) {
431 init_test(cx, |_| {});
432
433 let view = cx.add_window(|cx| {
434 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
435 build_editor(buffer, cx)
436 });
437
438 _ = view.update(cx, |view, cx| {
439 view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx);
440 assert_eq!(
441 view.selections.display_ranges(cx),
442 [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)]
443 );
444 });
445
446 _ = view.update(cx, |view, cx| {
447 view.update_selection(
448 DisplayPoint::new(3, 3),
449 0,
450 gpui::Point::<f32>::default(),
451 cx,
452 );
453 assert_eq!(
454 view.selections.display_ranges(cx),
455 [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
456 );
457 });
458
459 _ = view.update(cx, |view, cx| {
460 view.cancel(&Cancel, cx);
461 view.update_selection(
462 DisplayPoint::new(1, 1),
463 0,
464 gpui::Point::<f32>::default(),
465 cx,
466 );
467 assert_eq!(
468 view.selections.display_ranges(cx),
469 [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
470 );
471 });
472}
473
474#[gpui::test]
475fn test_clone(cx: &mut TestAppContext) {
476 init_test(cx, |_| {});
477
478 let (text, selection_ranges) = marked_text_ranges(
479 indoc! {"
480 one
481 two
482 threeˇ
483 four
484 fiveˇ
485 "},
486 true,
487 );
488
489 let editor = cx.add_window(|cx| {
490 let buffer = MultiBuffer::build_simple(&text, cx);
491 build_editor(buffer, cx)
492 });
493
494 _ = editor.update(cx, |editor, cx| {
495 editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone()));
496 editor.fold_ranges(
497 [
498 Point::new(1, 0)..Point::new(2, 0),
499 Point::new(3, 0)..Point::new(4, 0),
500 ],
501 true,
502 cx,
503 );
504 });
505
506 let cloned_editor = editor
507 .update(cx, |editor, cx| {
508 cx.open_window(Default::default(), |cx| cx.new_view(|cx| editor.clone(cx)))
509 })
510 .unwrap();
511
512 let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)).unwrap();
513 let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx)).unwrap();
514
515 assert_eq!(
516 cloned_editor
517 .update(cx, |e, cx| e.display_text(cx))
518 .unwrap(),
519 editor.update(cx, |e, cx| e.display_text(cx)).unwrap()
520 );
521 assert_eq!(
522 cloned_snapshot
523 .folds_in_range(0..text.len())
524 .collect::<Vec<_>>(),
525 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
526 );
527 assert_set_eq!(
528 cloned_editor
529 .update(cx, |editor, cx| editor.selections.ranges::<Point>(cx))
530 .unwrap(),
531 editor
532 .update(cx, |editor, cx| editor.selections.ranges(cx))
533 .unwrap()
534 );
535 assert_set_eq!(
536 cloned_editor
537 .update(cx, |e, cx| e.selections.display_ranges(cx))
538 .unwrap(),
539 editor
540 .update(cx, |e, cx| e.selections.display_ranges(cx))
541 .unwrap()
542 );
543}
544
545#[gpui::test]
546async fn test_navigation_history(cx: &mut TestAppContext) {
547 init_test(cx, |_| {});
548
549 use workspace::item::Item;
550
551 let fs = FakeFs::new(cx.executor());
552 let project = Project::test(fs, [], cx).await;
553 let workspace = cx.add_window(|cx| Workspace::test_new(project, cx));
554 let pane = workspace
555 .update(cx, |workspace, _| workspace.active_pane().clone())
556 .unwrap();
557
558 _ = workspace.update(cx, |_v, cx| {
559 cx.new_view(|cx| {
560 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
561 let mut editor = build_editor(buffer.clone(), cx);
562 let handle = cx.view();
563 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
564
565 fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option<NavigationEntry> {
566 editor.nav_history.as_mut().unwrap().pop_backward(cx)
567 }
568
569 // Move the cursor a small distance.
570 // Nothing is added to the navigation history.
571 editor.change_selections(None, cx, |s| {
572 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
573 });
574 editor.change_selections(None, cx, |s| {
575 s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
576 });
577 assert!(pop_history(&mut editor, cx).is_none());
578
579 // Move the cursor a large distance.
580 // The history can jump back to the previous position.
581 editor.change_selections(None, cx, |s| {
582 s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)])
583 });
584 let nav_entry = pop_history(&mut editor, cx).unwrap();
585 editor.navigate(nav_entry.data.unwrap(), cx);
586 assert_eq!(nav_entry.item.id(), cx.entity_id());
587 assert_eq!(
588 editor.selections.display_ranges(cx),
589 &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]
590 );
591 assert!(pop_history(&mut editor, cx).is_none());
592
593 // Move the cursor a small distance via the mouse.
594 // Nothing is added to the navigation history.
595 editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx);
596 editor.end_selection(cx);
597 assert_eq!(
598 editor.selections.display_ranges(cx),
599 &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
600 );
601 assert!(pop_history(&mut editor, cx).is_none());
602
603 // Move the cursor a large distance via the mouse.
604 // The history can jump back to the previous position.
605 editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx);
606 editor.end_selection(cx);
607 assert_eq!(
608 editor.selections.display_ranges(cx),
609 &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]
610 );
611 let nav_entry = pop_history(&mut editor, cx).unwrap();
612 editor.navigate(nav_entry.data.unwrap(), cx);
613 assert_eq!(nav_entry.item.id(), cx.entity_id());
614 assert_eq!(
615 editor.selections.display_ranges(cx),
616 &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
617 );
618 assert!(pop_history(&mut editor, cx).is_none());
619
620 // Set scroll position to check later
621 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), cx);
622 let original_scroll_position = editor.scroll_manager.anchor();
623
624 // Jump to the end of the document and adjust scroll
625 editor.move_to_end(&MoveToEnd, cx);
626 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), cx);
627 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
628
629 let nav_entry = pop_history(&mut editor, cx).unwrap();
630 editor.navigate(nav_entry.data.unwrap(), cx);
631 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
632
633 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
634 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
635 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
636 let invalid_point = Point::new(9999, 0);
637 editor.navigate(
638 Box::new(NavigationData {
639 cursor_anchor: invalid_anchor,
640 cursor_position: invalid_point,
641 scroll_anchor: ScrollAnchor {
642 anchor: invalid_anchor,
643 offset: Default::default(),
644 },
645 scroll_top_row: invalid_point.row,
646 }),
647 cx,
648 );
649 assert_eq!(
650 editor.selections.display_ranges(cx),
651 &[editor.max_point(cx)..editor.max_point(cx)]
652 );
653 assert_eq!(
654 editor.scroll_position(cx),
655 gpui::Point::new(0., editor.max_point(cx).row() as f32)
656 );
657
658 editor
659 })
660 });
661}
662
663#[gpui::test]
664fn test_cancel(cx: &mut TestAppContext) {
665 init_test(cx, |_| {});
666
667 let view = cx.add_window(|cx| {
668 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
669 build_editor(buffer, cx)
670 });
671
672 _ = view.update(cx, |view, cx| {
673 view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx);
674 view.update_selection(
675 DisplayPoint::new(1, 1),
676 0,
677 gpui::Point::<f32>::default(),
678 cx,
679 );
680 view.end_selection(cx);
681
682 view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx);
683 view.update_selection(
684 DisplayPoint::new(0, 3),
685 0,
686 gpui::Point::<f32>::default(),
687 cx,
688 );
689 view.end_selection(cx);
690 assert_eq!(
691 view.selections.display_ranges(cx),
692 [
693 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3),
694 DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1),
695 ]
696 );
697 });
698
699 _ = view.update(cx, |view, cx| {
700 view.cancel(&Cancel, cx);
701 assert_eq!(
702 view.selections.display_ranges(cx),
703 [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)]
704 );
705 });
706
707 _ = view.update(cx, |view, cx| {
708 view.cancel(&Cancel, cx);
709 assert_eq!(
710 view.selections.display_ranges(cx),
711 [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)]
712 );
713 });
714}
715
716#[gpui::test]
717fn test_fold_action(cx: &mut TestAppContext) {
718 init_test(cx, |_| {});
719
720 let view = cx.add_window(|cx| {
721 let buffer = MultiBuffer::build_simple(
722 &"
723 impl Foo {
724 // Hello!
725
726 fn a() {
727 1
728 }
729
730 fn b() {
731 2
732 }
733
734 fn c() {
735 3
736 }
737 }
738 "
739 .unindent(),
740 cx,
741 );
742 build_editor(buffer.clone(), cx)
743 });
744
745 _ = view.update(cx, |view, cx| {
746 view.change_selections(None, cx, |s| {
747 s.select_display_ranges([DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)]);
748 });
749 view.fold(&Fold, cx);
750 assert_eq!(
751 view.display_text(cx),
752 "
753 impl Foo {
754 // Hello!
755
756 fn a() {
757 1
758 }
759
760 fn b() {⋯
761 }
762
763 fn c() {⋯
764 }
765 }
766 "
767 .unindent(),
768 );
769
770 view.fold(&Fold, cx);
771 assert_eq!(
772 view.display_text(cx),
773 "
774 impl Foo {⋯
775 }
776 "
777 .unindent(),
778 );
779
780 view.unfold_lines(&UnfoldLines, cx);
781 assert_eq!(
782 view.display_text(cx),
783 "
784 impl Foo {
785 // Hello!
786
787 fn a() {
788 1
789 }
790
791 fn b() {⋯
792 }
793
794 fn c() {⋯
795 }
796 }
797 "
798 .unindent(),
799 );
800
801 view.unfold_lines(&UnfoldLines, cx);
802 assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text());
803 });
804}
805
806#[gpui::test]
807fn test_move_cursor(cx: &mut TestAppContext) {
808 init_test(cx, |_| {});
809
810 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
811 let view = cx.add_window(|cx| build_editor(buffer.clone(), cx));
812
813 _ = buffer.update(cx, |buffer, cx| {
814 buffer.edit(
815 vec![
816 (Point::new(1, 0)..Point::new(1, 0), "\t"),
817 (Point::new(1, 1)..Point::new(1, 1), "\t"),
818 ],
819 None,
820 cx,
821 );
822 });
823 _ = view.update(cx, |view, cx| {
824 assert_eq!(
825 view.selections.display_ranges(cx),
826 &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
827 );
828
829 view.move_down(&MoveDown, cx);
830 assert_eq!(
831 view.selections.display_ranges(cx),
832 &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]
833 );
834
835 view.move_right(&MoveRight, cx);
836 assert_eq!(
837 view.selections.display_ranges(cx),
838 &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)]
839 );
840
841 view.move_left(&MoveLeft, cx);
842 assert_eq!(
843 view.selections.display_ranges(cx),
844 &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]
845 );
846
847 view.move_up(&MoveUp, cx);
848 assert_eq!(
849 view.selections.display_ranges(cx),
850 &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
851 );
852
853 view.move_to_end(&MoveToEnd, cx);
854 assert_eq!(
855 view.selections.display_ranges(cx),
856 &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)]
857 );
858
859 view.move_to_beginning(&MoveToBeginning, cx);
860 assert_eq!(
861 view.selections.display_ranges(cx),
862 &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
863 );
864
865 view.change_selections(None, cx, |s| {
866 s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)]);
867 });
868 view.select_to_beginning(&SelectToBeginning, cx);
869 assert_eq!(
870 view.selections.display_ranges(cx),
871 &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)]
872 );
873
874 view.select_to_end(&SelectToEnd, cx);
875 assert_eq!(
876 view.selections.display_ranges(cx),
877 &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)]
878 );
879 });
880}
881
882#[gpui::test]
883fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
884 init_test(cx, |_| {});
885
886 let view = cx.add_window(|cx| {
887 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε", cx);
888 build_editor(buffer.clone(), cx)
889 });
890
891 assert_eq!('ⓐ'.len_utf8(), 3);
892 assert_eq!('α'.len_utf8(), 2);
893
894 _ = view.update(cx, |view, cx| {
895 view.fold_ranges(
896 vec![
897 Point::new(0, 6)..Point::new(0, 12),
898 Point::new(1, 2)..Point::new(1, 4),
899 Point::new(2, 4)..Point::new(2, 8),
900 ],
901 true,
902 cx,
903 );
904 assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\nαβ⋯ε");
905
906 view.move_right(&MoveRight, cx);
907 assert_eq!(
908 view.selections.display_ranges(cx),
909 &[empty_range(0, "ⓐ".len())]
910 );
911 view.move_right(&MoveRight, cx);
912 assert_eq!(
913 view.selections.display_ranges(cx),
914 &[empty_range(0, "ⓐⓑ".len())]
915 );
916 view.move_right(&MoveRight, cx);
917 assert_eq!(
918 view.selections.display_ranges(cx),
919 &[empty_range(0, "ⓐⓑ⋯".len())]
920 );
921
922 view.move_down(&MoveDown, cx);
923 assert_eq!(
924 view.selections.display_ranges(cx),
925 &[empty_range(1, "ab⋯e".len())]
926 );
927 view.move_left(&MoveLeft, cx);
928 assert_eq!(
929 view.selections.display_ranges(cx),
930 &[empty_range(1, "ab⋯".len())]
931 );
932 view.move_left(&MoveLeft, cx);
933 assert_eq!(
934 view.selections.display_ranges(cx),
935 &[empty_range(1, "ab".len())]
936 );
937 view.move_left(&MoveLeft, cx);
938 assert_eq!(
939 view.selections.display_ranges(cx),
940 &[empty_range(1, "a".len())]
941 );
942
943 view.move_down(&MoveDown, cx);
944 assert_eq!(
945 view.selections.display_ranges(cx),
946 &[empty_range(2, "α".len())]
947 );
948 view.move_right(&MoveRight, cx);
949 assert_eq!(
950 view.selections.display_ranges(cx),
951 &[empty_range(2, "αβ".len())]
952 );
953 view.move_right(&MoveRight, cx);
954 assert_eq!(
955 view.selections.display_ranges(cx),
956 &[empty_range(2, "αβ⋯".len())]
957 );
958 view.move_right(&MoveRight, cx);
959 assert_eq!(
960 view.selections.display_ranges(cx),
961 &[empty_range(2, "αβ⋯ε".len())]
962 );
963
964 view.move_up(&MoveUp, cx);
965 assert_eq!(
966 view.selections.display_ranges(cx),
967 &[empty_range(1, "ab⋯e".len())]
968 );
969 view.move_down(&MoveDown, cx);
970 assert_eq!(
971 view.selections.display_ranges(cx),
972 &[empty_range(2, "αβ⋯ε".len())]
973 );
974 view.move_up(&MoveUp, cx);
975 assert_eq!(
976 view.selections.display_ranges(cx),
977 &[empty_range(1, "ab⋯e".len())]
978 );
979
980 view.move_up(&MoveUp, cx);
981 assert_eq!(
982 view.selections.display_ranges(cx),
983 &[empty_range(0, "ⓐⓑ".len())]
984 );
985 view.move_left(&MoveLeft, cx);
986 assert_eq!(
987 view.selections.display_ranges(cx),
988 &[empty_range(0, "ⓐ".len())]
989 );
990 view.move_left(&MoveLeft, cx);
991 assert_eq!(
992 view.selections.display_ranges(cx),
993 &[empty_range(0, "".len())]
994 );
995 });
996}
997
998#[gpui::test]
999fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1000 init_test(cx, |_| {});
1001
1002 let view = cx.add_window(|cx| {
1003 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1004 build_editor(buffer.clone(), cx)
1005 });
1006 _ = view.update(cx, |view, cx| {
1007 view.change_selections(None, cx, |s| {
1008 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1009 });
1010 view.move_down(&MoveDown, cx);
1011 assert_eq!(
1012 view.selections.display_ranges(cx),
1013 &[empty_range(1, "abcd".len())]
1014 );
1015
1016 view.move_down(&MoveDown, cx);
1017 assert_eq!(
1018 view.selections.display_ranges(cx),
1019 &[empty_range(2, "αβγ".len())]
1020 );
1021
1022 view.move_down(&MoveDown, cx);
1023 assert_eq!(
1024 view.selections.display_ranges(cx),
1025 &[empty_range(3, "abcd".len())]
1026 );
1027
1028 view.move_down(&MoveDown, cx);
1029 assert_eq!(
1030 view.selections.display_ranges(cx),
1031 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1032 );
1033
1034 view.move_up(&MoveUp, cx);
1035 assert_eq!(
1036 view.selections.display_ranges(cx),
1037 &[empty_range(3, "abcd".len())]
1038 );
1039
1040 view.move_up(&MoveUp, cx);
1041 assert_eq!(
1042 view.selections.display_ranges(cx),
1043 &[empty_range(2, "αβγ".len())]
1044 );
1045 });
1046}
1047
1048#[gpui::test]
1049fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1050 init_test(cx, |_| {});
1051
1052 let view = cx.add_window(|cx| {
1053 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1054 build_editor(buffer, cx)
1055 });
1056 _ = view.update(cx, |view, cx| {
1057 view.change_selections(None, cx, |s| {
1058 s.select_display_ranges([
1059 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
1060 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4),
1061 ]);
1062 });
1063 });
1064
1065 _ = view.update(cx, |view, cx| {
1066 view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx);
1067 assert_eq!(
1068 view.selections.display_ranges(cx),
1069 &[
1070 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
1071 DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
1072 ]
1073 );
1074 });
1075
1076 _ = view.update(cx, |view, cx| {
1077 view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx);
1078 assert_eq!(
1079 view.selections.display_ranges(cx),
1080 &[
1081 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
1082 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
1083 ]
1084 );
1085 });
1086
1087 _ = view.update(cx, |view, cx| {
1088 view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx);
1089 assert_eq!(
1090 view.selections.display_ranges(cx),
1091 &[
1092 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
1093 DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
1094 ]
1095 );
1096 });
1097
1098 _ = view.update(cx, |view, cx| {
1099 view.move_to_end_of_line(&MoveToEndOfLine, cx);
1100 assert_eq!(
1101 view.selections.display_ranges(cx),
1102 &[
1103 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
1104 DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5),
1105 ]
1106 );
1107 });
1108
1109 // Moving to the end of line again is a no-op.
1110 _ = view.update(cx, |view, cx| {
1111 view.move_to_end_of_line(&MoveToEndOfLine, cx);
1112 assert_eq!(
1113 view.selections.display_ranges(cx),
1114 &[
1115 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
1116 DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5),
1117 ]
1118 );
1119 });
1120
1121 _ = view.update(cx, |view, cx| {
1122 view.move_left(&MoveLeft, cx);
1123 view.select_to_beginning_of_line(
1124 &SelectToBeginningOfLine {
1125 stop_at_soft_wraps: true,
1126 },
1127 cx,
1128 );
1129 assert_eq!(
1130 view.selections.display_ranges(cx),
1131 &[
1132 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0),
1133 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2),
1134 ]
1135 );
1136 });
1137
1138 _ = view.update(cx, |view, cx| {
1139 view.select_to_beginning_of_line(
1140 &SelectToBeginningOfLine {
1141 stop_at_soft_wraps: true,
1142 },
1143 cx,
1144 );
1145 assert_eq!(
1146 view.selections.display_ranges(cx),
1147 &[
1148 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0),
1149 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 0),
1150 ]
1151 );
1152 });
1153
1154 _ = view.update(cx, |view, cx| {
1155 view.select_to_beginning_of_line(
1156 &SelectToBeginningOfLine {
1157 stop_at_soft_wraps: true,
1158 },
1159 cx,
1160 );
1161 assert_eq!(
1162 view.selections.display_ranges(cx),
1163 &[
1164 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0),
1165 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2),
1166 ]
1167 );
1168 });
1169
1170 _ = view.update(cx, |view, cx| {
1171 view.select_to_end_of_line(
1172 &SelectToEndOfLine {
1173 stop_at_soft_wraps: true,
1174 },
1175 cx,
1176 );
1177 assert_eq!(
1178 view.selections.display_ranges(cx),
1179 &[
1180 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3),
1181 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 5),
1182 ]
1183 );
1184 });
1185
1186 _ = view.update(cx, |view, cx| {
1187 view.delete_to_end_of_line(&DeleteToEndOfLine, cx);
1188 assert_eq!(view.display_text(cx), "ab\n de");
1189 assert_eq!(
1190 view.selections.display_ranges(cx),
1191 &[
1192 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
1193 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4),
1194 ]
1195 );
1196 });
1197
1198 _ = view.update(cx, |view, cx| {
1199 view.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
1200 assert_eq!(view.display_text(cx), "\n");
1201 assert_eq!(
1202 view.selections.display_ranges(cx),
1203 &[
1204 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
1205 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
1206 ]
1207 );
1208 });
1209}
1210
1211#[gpui::test]
1212fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1213 init_test(cx, |_| {});
1214
1215 let view = cx.add_window(|cx| {
1216 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1217 build_editor(buffer, cx)
1218 });
1219 _ = view.update(cx, |view, cx| {
1220 view.change_selections(None, cx, |s| {
1221 s.select_display_ranges([
1222 DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11),
1223 DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4),
1224 ])
1225 });
1226
1227 view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
1228 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx);
1229
1230 view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
1231 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", view, cx);
1232
1233 view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
1234 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", view, cx);
1235
1236 view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
1237 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx);
1238
1239 view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
1240 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", view, cx);
1241
1242 view.move_to_next_word_end(&MoveToNextWordEnd, cx);
1243 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", view, cx);
1244
1245 view.move_to_next_word_end(&MoveToNextWordEnd, cx);
1246 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx);
1247
1248 view.move_to_next_word_end(&MoveToNextWordEnd, cx);
1249 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx);
1250
1251 view.move_right(&MoveRight, cx);
1252 view.select_to_previous_word_start(&SelectToPreviousWordStart, cx);
1253 assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, cx);
1254
1255 view.select_to_previous_word_start(&SelectToPreviousWordStart, cx);
1256 assert_selection_ranges("use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}", view, cx);
1257
1258 view.select_to_next_word_end(&SelectToNextWordEnd, cx);
1259 assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, cx);
1260 });
1261}
1262
1263#[gpui::test]
1264fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1265 init_test(cx, |_| {});
1266
1267 let view = cx.add_window(|cx| {
1268 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1269 build_editor(buffer, cx)
1270 });
1271
1272 _ = view.update(cx, |view, cx| {
1273 view.set_wrap_width(Some(140.0.into()), cx);
1274 assert_eq!(
1275 view.display_text(cx),
1276 "use one::{\n two::three::\n four::five\n};"
1277 );
1278
1279 view.change_selections(None, cx, |s| {
1280 s.select_display_ranges([DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]);
1281 });
1282
1283 view.move_to_next_word_end(&MoveToNextWordEnd, cx);
1284 assert_eq!(
1285 view.selections.display_ranges(cx),
1286 &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)]
1287 );
1288
1289 view.move_to_next_word_end(&MoveToNextWordEnd, cx);
1290 assert_eq!(
1291 view.selections.display_ranges(cx),
1292 &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)]
1293 );
1294
1295 view.move_to_next_word_end(&MoveToNextWordEnd, cx);
1296 assert_eq!(
1297 view.selections.display_ranges(cx),
1298 &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)]
1299 );
1300
1301 view.move_to_next_word_end(&MoveToNextWordEnd, cx);
1302 assert_eq!(
1303 view.selections.display_ranges(cx),
1304 &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)]
1305 );
1306
1307 view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
1308 assert_eq!(
1309 view.selections.display_ranges(cx),
1310 &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)]
1311 );
1312
1313 view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
1314 assert_eq!(
1315 view.selections.display_ranges(cx),
1316 &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)]
1317 );
1318 });
1319}
1320
1321#[gpui::test]
1322async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1323 init_test(cx, |_| {});
1324 let mut cx = EditorTestContext::new(cx).await;
1325
1326 let line_height = cx.editor(|editor, cx| {
1327 editor
1328 .style()
1329 .unwrap()
1330 .text
1331 .line_height_in_pixels(cx.rem_size())
1332 });
1333 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
1334
1335 cx.set_state(
1336 &r#"ˇone
1337 two
1338
1339 three
1340 fourˇ
1341 five
1342
1343 six"#
1344 .unindent(),
1345 );
1346
1347 cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
1348 cx.assert_editor_state(
1349 &r#"one
1350 two
1351 ˇ
1352 three
1353 four
1354 five
1355 ˇ
1356 six"#
1357 .unindent(),
1358 );
1359
1360 cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
1361 cx.assert_editor_state(
1362 &r#"one
1363 two
1364
1365 three
1366 four
1367 five
1368 ˇ
1369 sixˇ"#
1370 .unindent(),
1371 );
1372
1373 cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
1374 cx.assert_editor_state(
1375 &r#"one
1376 two
1377
1378 three
1379 four
1380 five
1381
1382 sixˇ"#
1383 .unindent(),
1384 );
1385
1386 cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
1387 cx.assert_editor_state(
1388 &r#"one
1389 two
1390
1391 three
1392 four
1393 five
1394 ˇ
1395 six"#
1396 .unindent(),
1397 );
1398
1399 cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
1400 cx.assert_editor_state(
1401 &r#"one
1402 two
1403 ˇ
1404 three
1405 four
1406 five
1407
1408 six"#
1409 .unindent(),
1410 );
1411
1412 cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
1413 cx.assert_editor_state(
1414 &r#"ˇone
1415 two
1416
1417 three
1418 four
1419 five
1420
1421 six"#
1422 .unindent(),
1423 );
1424}
1425
1426#[gpui::test]
1427async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
1428 init_test(cx, |_| {});
1429 let mut cx = EditorTestContext::new(cx).await;
1430 let line_height = cx.editor(|editor, cx| {
1431 editor
1432 .style()
1433 .unwrap()
1434 .text
1435 .line_height_in_pixels(cx.rem_size())
1436 });
1437 let window = cx.window;
1438 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
1439
1440 cx.set_state(
1441 &r#"ˇone
1442 two
1443 three
1444 four
1445 five
1446 six
1447 seven
1448 eight
1449 nine
1450 ten
1451 "#,
1452 );
1453
1454 cx.update_editor(|editor, cx| {
1455 assert_eq!(
1456 editor.snapshot(cx).scroll_position(),
1457 gpui::Point::new(0., 0.)
1458 );
1459 editor.scroll_screen(&ScrollAmount::Page(1.), cx);
1460 assert_eq!(
1461 editor.snapshot(cx).scroll_position(),
1462 gpui::Point::new(0., 3.)
1463 );
1464 editor.scroll_screen(&ScrollAmount::Page(1.), cx);
1465 assert_eq!(
1466 editor.snapshot(cx).scroll_position(),
1467 gpui::Point::new(0., 6.)
1468 );
1469 editor.scroll_screen(&ScrollAmount::Page(-1.), cx);
1470 assert_eq!(
1471 editor.snapshot(cx).scroll_position(),
1472 gpui::Point::new(0., 3.)
1473 );
1474
1475 editor.scroll_screen(&ScrollAmount::Page(-0.5), cx);
1476 assert_eq!(
1477 editor.snapshot(cx).scroll_position(),
1478 gpui::Point::new(0., 1.)
1479 );
1480 editor.scroll_screen(&ScrollAmount::Page(0.5), cx);
1481 assert_eq!(
1482 editor.snapshot(cx).scroll_position(),
1483 gpui::Point::new(0., 3.)
1484 );
1485 });
1486}
1487
1488#[gpui::test]
1489async fn test_autoscroll(cx: &mut gpui::TestAppContext) {
1490 init_test(cx, |_| {});
1491 let mut cx = EditorTestContext::new(cx).await;
1492
1493 let line_height = cx.update_editor(|editor, cx| {
1494 editor.set_vertical_scroll_margin(2, cx);
1495 editor
1496 .style()
1497 .unwrap()
1498 .text
1499 .line_height_in_pixels(cx.rem_size())
1500 });
1501 let window = cx.window;
1502 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
1503
1504 cx.set_state(
1505 &r#"ˇone
1506 two
1507 three
1508 four
1509 five
1510 six
1511 seven
1512 eight
1513 nine
1514 ten
1515 "#,
1516 );
1517 cx.update_editor(|editor, cx| {
1518 assert_eq!(
1519 editor.snapshot(cx).scroll_position(),
1520 gpui::Point::new(0., 0.0)
1521 );
1522 });
1523
1524 // Add a cursor below the visible area. Since both cursors cannot fit
1525 // on screen, the editor autoscrolls to reveal the newest cursor, and
1526 // allows the vertical scroll margin below that cursor.
1527 cx.update_editor(|editor, cx| {
1528 editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
1529 selections.select_ranges([
1530 Point::new(0, 0)..Point::new(0, 0),
1531 Point::new(6, 0)..Point::new(6, 0),
1532 ]);
1533 })
1534 });
1535 cx.update_editor(|editor, cx| {
1536 assert_eq!(
1537 editor.snapshot(cx).scroll_position(),
1538 gpui::Point::new(0., 3.0)
1539 );
1540 });
1541
1542 // Move down. The editor cursor scrolls down to track the newest cursor.
1543 cx.update_editor(|editor, cx| {
1544 editor.move_down(&Default::default(), cx);
1545 });
1546 cx.update_editor(|editor, cx| {
1547 assert_eq!(
1548 editor.snapshot(cx).scroll_position(),
1549 gpui::Point::new(0., 4.0)
1550 );
1551 });
1552
1553 // Add a cursor above the visible area. Since both cursors fit on screen,
1554 // the editor scrolls to show both.
1555 cx.update_editor(|editor, cx| {
1556 editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
1557 selections.select_ranges([
1558 Point::new(1, 0)..Point::new(1, 0),
1559 Point::new(6, 0)..Point::new(6, 0),
1560 ]);
1561 })
1562 });
1563 cx.update_editor(|editor, cx| {
1564 assert_eq!(
1565 editor.snapshot(cx).scroll_position(),
1566 gpui::Point::new(0., 1.0)
1567 );
1568 });
1569}
1570
1571#[gpui::test]
1572async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
1573 init_test(cx, |_| {});
1574 let mut cx = EditorTestContext::new(cx).await;
1575
1576 let line_height = cx.editor(|editor, cx| {
1577 editor
1578 .style()
1579 .unwrap()
1580 .text
1581 .line_height_in_pixels(cx.rem_size())
1582 });
1583 let window = cx.window;
1584 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
1585 cx.set_state(
1586 &r#"
1587 ˇone
1588 two
1589 threeˇ
1590 four
1591 five
1592 six
1593 seven
1594 eight
1595 nine
1596 ten
1597 "#
1598 .unindent(),
1599 );
1600
1601 cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
1602 cx.assert_editor_state(
1603 &r#"
1604 one
1605 two
1606 three
1607 ˇfour
1608 five
1609 sixˇ
1610 seven
1611 eight
1612 nine
1613 ten
1614 "#
1615 .unindent(),
1616 );
1617
1618 cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
1619 cx.assert_editor_state(
1620 &r#"
1621 one
1622 two
1623 three
1624 four
1625 five
1626 six
1627 ˇseven
1628 eight
1629 nineˇ
1630 ten
1631 "#
1632 .unindent(),
1633 );
1634
1635 cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
1636 cx.assert_editor_state(
1637 &r#"
1638 one
1639 two
1640 three
1641 ˇfour
1642 five
1643 sixˇ
1644 seven
1645 eight
1646 nine
1647 ten
1648 "#
1649 .unindent(),
1650 );
1651
1652 cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
1653 cx.assert_editor_state(
1654 &r#"
1655 ˇone
1656 two
1657 threeˇ
1658 four
1659 five
1660 six
1661 seven
1662 eight
1663 nine
1664 ten
1665 "#
1666 .unindent(),
1667 );
1668
1669 // Test select collapsing
1670 cx.update_editor(|editor, cx| {
1671 editor.move_page_down(&MovePageDown::default(), cx);
1672 editor.move_page_down(&MovePageDown::default(), cx);
1673 editor.move_page_down(&MovePageDown::default(), cx);
1674 });
1675 cx.assert_editor_state(
1676 &r#"
1677 one
1678 two
1679 three
1680 four
1681 five
1682 six
1683 seven
1684 eight
1685 nine
1686 ˇten
1687 ˇ"#
1688 .unindent(),
1689 );
1690}
1691
1692#[gpui::test]
1693async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
1694 init_test(cx, |_| {});
1695 let mut cx = EditorTestContext::new(cx).await;
1696 cx.set_state("one «two threeˇ» four");
1697 cx.update_editor(|editor, cx| {
1698 editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
1699 assert_eq!(editor.text(cx), " four");
1700 });
1701}
1702
1703#[gpui::test]
1704fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
1705 init_test(cx, |_| {});
1706
1707 let view = cx.add_window(|cx| {
1708 let buffer = MultiBuffer::build_simple("one two three four", cx);
1709 build_editor(buffer.clone(), cx)
1710 });
1711
1712 _ = view.update(cx, |view, cx| {
1713 view.change_selections(None, cx, |s| {
1714 s.select_display_ranges([
1715 // an empty selection - the preceding word fragment is deleted
1716 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
1717 // characters selected - they are deleted
1718 DisplayPoint::new(0, 9)..DisplayPoint::new(0, 12),
1719 ])
1720 });
1721 view.delete_to_previous_word_start(&DeleteToPreviousWordStart, cx);
1722 assert_eq!(view.buffer.read(cx).read(cx).text(), "e two te four");
1723 });
1724
1725 _ = view.update(cx, |view, cx| {
1726 view.change_selections(None, cx, |s| {
1727 s.select_display_ranges([
1728 // an empty selection - the following word fragment is deleted
1729 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
1730 // characters selected - they are deleted
1731 DisplayPoint::new(0, 9)..DisplayPoint::new(0, 10),
1732 ])
1733 });
1734 view.delete_to_next_word_end(&DeleteToNextWordEnd, cx);
1735 assert_eq!(view.buffer.read(cx).read(cx).text(), "e t te our");
1736 });
1737}
1738
1739#[gpui::test]
1740fn test_newline(cx: &mut TestAppContext) {
1741 init_test(cx, |_| {});
1742
1743 let view = cx.add_window(|cx| {
1744 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
1745 build_editor(buffer.clone(), cx)
1746 });
1747
1748 _ = view.update(cx, |view, cx| {
1749 view.change_selections(None, cx, |s| {
1750 s.select_display_ranges([
1751 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
1752 DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
1753 DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6),
1754 ])
1755 });
1756
1757 view.newline(&Newline, cx);
1758 assert_eq!(view.text(cx), "aa\naa\n \n bb\n bb\n");
1759 });
1760}
1761
1762#[gpui::test]
1763fn test_newline_with_old_selections(cx: &mut TestAppContext) {
1764 init_test(cx, |_| {});
1765
1766 let editor = cx.add_window(|cx| {
1767 let buffer = MultiBuffer::build_simple(
1768 "
1769 a
1770 b(
1771 X
1772 )
1773 c(
1774 X
1775 )
1776 "
1777 .unindent()
1778 .as_str(),
1779 cx,
1780 );
1781 let mut editor = build_editor(buffer.clone(), cx);
1782 editor.change_selections(None, cx, |s| {
1783 s.select_ranges([
1784 Point::new(2, 4)..Point::new(2, 5),
1785 Point::new(5, 4)..Point::new(5, 5),
1786 ])
1787 });
1788 editor
1789 });
1790
1791 _ = editor.update(cx, |editor, cx| {
1792 // Edit the buffer directly, deleting ranges surrounding the editor's selections
1793 editor.buffer.update(cx, |buffer, cx| {
1794 buffer.edit(
1795 [
1796 (Point::new(1, 2)..Point::new(3, 0), ""),
1797 (Point::new(4, 2)..Point::new(6, 0), ""),
1798 ],
1799 None,
1800 cx,
1801 );
1802 assert_eq!(
1803 buffer.read(cx).text(),
1804 "
1805 a
1806 b()
1807 c()
1808 "
1809 .unindent()
1810 );
1811 });
1812 assert_eq!(
1813 editor.selections.ranges(cx),
1814 &[
1815 Point::new(1, 2)..Point::new(1, 2),
1816 Point::new(2, 2)..Point::new(2, 2),
1817 ],
1818 );
1819
1820 editor.newline(&Newline, cx);
1821 assert_eq!(
1822 editor.text(cx),
1823 "
1824 a
1825 b(
1826 )
1827 c(
1828 )
1829 "
1830 .unindent()
1831 );
1832
1833 // The selections are moved after the inserted newlines
1834 assert_eq!(
1835 editor.selections.ranges(cx),
1836 &[
1837 Point::new(2, 0)..Point::new(2, 0),
1838 Point::new(4, 0)..Point::new(4, 0),
1839 ],
1840 );
1841 });
1842}
1843
1844#[gpui::test]
1845async fn test_newline_above(cx: &mut gpui::TestAppContext) {
1846 init_test(cx, |settings| {
1847 settings.defaults.tab_size = NonZeroU32::new(4)
1848 });
1849
1850 let language = Arc::new(
1851 Language::new(
1852 LanguageConfig::default(),
1853 Some(tree_sitter_rust::language()),
1854 )
1855 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
1856 .unwrap(),
1857 );
1858
1859 let mut cx = EditorTestContext::new(cx).await;
1860 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
1861 cx.set_state(indoc! {"
1862 const a: ˇA = (
1863 (ˇ
1864 «const_functionˇ»(ˇ),
1865 so«mˇ»et«hˇ»ing_ˇelse,ˇ
1866 )ˇ
1867 ˇ);ˇ
1868 "});
1869
1870 cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx));
1871 cx.assert_editor_state(indoc! {"
1872 ˇ
1873 const a: A = (
1874 ˇ
1875 (
1876 ˇ
1877 ˇ
1878 const_function(),
1879 ˇ
1880 ˇ
1881 ˇ
1882 ˇ
1883 something_else,
1884 ˇ
1885 )
1886 ˇ
1887 ˇ
1888 );
1889 "});
1890}
1891
1892#[gpui::test]
1893async fn test_newline_below(cx: &mut gpui::TestAppContext) {
1894 init_test(cx, |settings| {
1895 settings.defaults.tab_size = NonZeroU32::new(4)
1896 });
1897
1898 let language = Arc::new(
1899 Language::new(
1900 LanguageConfig::default(),
1901 Some(tree_sitter_rust::language()),
1902 )
1903 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
1904 .unwrap(),
1905 );
1906
1907 let mut cx = EditorTestContext::new(cx).await;
1908 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
1909 cx.set_state(indoc! {"
1910 const a: ˇA = (
1911 (ˇ
1912 «const_functionˇ»(ˇ),
1913 so«mˇ»et«hˇ»ing_ˇelse,ˇ
1914 )ˇ
1915 ˇ);ˇ
1916 "});
1917
1918 cx.update_editor(|e, cx| e.newline_below(&NewlineBelow, cx));
1919 cx.assert_editor_state(indoc! {"
1920 const a: A = (
1921 ˇ
1922 (
1923 ˇ
1924 const_function(),
1925 ˇ
1926 ˇ
1927 something_else,
1928 ˇ
1929 ˇ
1930 ˇ
1931 ˇ
1932 )
1933 ˇ
1934 );
1935 ˇ
1936 ˇ
1937 "});
1938}
1939
1940#[gpui::test]
1941async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
1942 init_test(cx, |settings| {
1943 settings.defaults.tab_size = NonZeroU32::new(4)
1944 });
1945
1946 let language = Arc::new(Language::new(
1947 LanguageConfig {
1948 line_comments: vec!["//".into()],
1949 ..LanguageConfig::default()
1950 },
1951 None,
1952 ));
1953 {
1954 let mut cx = EditorTestContext::new(cx).await;
1955 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
1956 cx.set_state(indoc! {"
1957 // Fooˇ
1958 "});
1959
1960 cx.update_editor(|e, cx| e.newline(&Newline, cx));
1961 cx.assert_editor_state(indoc! {"
1962 // Foo
1963 //ˇ
1964 "});
1965 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
1966 cx.set_state(indoc! {"
1967 ˇ// Foo
1968 "});
1969 cx.update_editor(|e, cx| e.newline(&Newline, cx));
1970 cx.assert_editor_state(indoc! {"
1971
1972 ˇ// Foo
1973 "});
1974 }
1975 // Ensure that comment continuations can be disabled.
1976 update_test_language_settings(cx, |settings| {
1977 settings.defaults.extend_comment_on_newline = Some(false);
1978 });
1979 let mut cx = EditorTestContext::new(cx).await;
1980 cx.set_state(indoc! {"
1981 // Fooˇ
1982 "});
1983 cx.update_editor(|e, cx| e.newline(&Newline, cx));
1984 cx.assert_editor_state(indoc! {"
1985 // Foo
1986 ˇ
1987 "});
1988}
1989
1990#[gpui::test]
1991fn test_insert_with_old_selections(cx: &mut TestAppContext) {
1992 init_test(cx, |_| {});
1993
1994 let editor = cx.add_window(|cx| {
1995 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
1996 let mut editor = build_editor(buffer.clone(), cx);
1997 editor.change_selections(None, cx, |s| s.select_ranges([3..4, 11..12, 19..20]));
1998 editor
1999 });
2000
2001 _ = editor.update(cx, |editor, cx| {
2002 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2003 editor.buffer.update(cx, |buffer, cx| {
2004 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2005 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2006 });
2007 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2008
2009 editor.insert("Z", cx);
2010 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2011
2012 // The selections are moved after the inserted characters
2013 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2014 });
2015}
2016
2017#[gpui::test]
2018async fn test_tab(cx: &mut gpui::TestAppContext) {
2019 init_test(cx, |settings| {
2020 settings.defaults.tab_size = NonZeroU32::new(3)
2021 });
2022
2023 let mut cx = EditorTestContext::new(cx).await;
2024 cx.set_state(indoc! {"
2025 ˇabˇc
2026 ˇ🏀ˇ🏀ˇefg
2027 dˇ
2028 "});
2029 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2030 cx.assert_editor_state(indoc! {"
2031 ˇab ˇc
2032 ˇ🏀 ˇ🏀 ˇefg
2033 d ˇ
2034 "});
2035
2036 cx.set_state(indoc! {"
2037 a
2038 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2039 "});
2040 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2041 cx.assert_editor_state(indoc! {"
2042 a
2043 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2044 "});
2045}
2046
2047#[gpui::test]
2048async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) {
2049 init_test(cx, |_| {});
2050
2051 let mut cx = EditorTestContext::new(cx).await;
2052 let language = Arc::new(
2053 Language::new(
2054 LanguageConfig::default(),
2055 Some(tree_sitter_rust::language()),
2056 )
2057 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2058 .unwrap(),
2059 );
2060 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2061
2062 // cursors that are already at the suggested indent level insert
2063 // a soft tab. cursors that are to the left of the suggested indent
2064 // auto-indent their line.
2065 cx.set_state(indoc! {"
2066 ˇ
2067 const a: B = (
2068 c(
2069 d(
2070 ˇ
2071 )
2072 ˇ
2073 ˇ )
2074 );
2075 "});
2076 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2077 cx.assert_editor_state(indoc! {"
2078 ˇ
2079 const a: B = (
2080 c(
2081 d(
2082 ˇ
2083 )
2084 ˇ
2085 ˇ)
2086 );
2087 "});
2088
2089 // handle auto-indent when there are multiple cursors on the same line
2090 cx.set_state(indoc! {"
2091 const a: B = (
2092 c(
2093 ˇ ˇ
2094 ˇ )
2095 );
2096 "});
2097 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2098 cx.assert_editor_state(indoc! {"
2099 const a: B = (
2100 c(
2101 ˇ
2102 ˇ)
2103 );
2104 "});
2105}
2106
2107#[gpui::test]
2108async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
2109 init_test(cx, |settings| {
2110 settings.defaults.tab_size = NonZeroU32::new(4)
2111 });
2112
2113 let language = Arc::new(
2114 Language::new(
2115 LanguageConfig::default(),
2116 Some(tree_sitter_rust::language()),
2117 )
2118 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2119 .unwrap(),
2120 );
2121
2122 let mut cx = EditorTestContext::new(cx).await;
2123 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2124 cx.set_state(indoc! {"
2125 fn a() {
2126 if b {
2127 \t ˇc
2128 }
2129 }
2130 "});
2131
2132 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2133 cx.assert_editor_state(indoc! {"
2134 fn a() {
2135 if b {
2136 ˇc
2137 }
2138 }
2139 "});
2140}
2141
2142#[gpui::test]
2143async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
2144 init_test(cx, |settings| {
2145 settings.defaults.tab_size = NonZeroU32::new(4);
2146 });
2147
2148 let mut cx = EditorTestContext::new(cx).await;
2149
2150 cx.set_state(indoc! {"
2151 «oneˇ» «twoˇ»
2152 three
2153 four
2154 "});
2155 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2156 cx.assert_editor_state(indoc! {"
2157 «oneˇ» «twoˇ»
2158 three
2159 four
2160 "});
2161
2162 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2163 cx.assert_editor_state(indoc! {"
2164 «oneˇ» «twoˇ»
2165 three
2166 four
2167 "});
2168
2169 // select across line ending
2170 cx.set_state(indoc! {"
2171 one two
2172 t«hree
2173 ˇ» four
2174 "});
2175 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2176 cx.assert_editor_state(indoc! {"
2177 one two
2178 t«hree
2179 ˇ» four
2180 "});
2181
2182 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2183 cx.assert_editor_state(indoc! {"
2184 one two
2185 t«hree
2186 ˇ» four
2187 "});
2188
2189 // Ensure that indenting/outdenting works when the cursor is at column 0.
2190 cx.set_state(indoc! {"
2191 one two
2192 ˇthree
2193 four
2194 "});
2195 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2196 cx.assert_editor_state(indoc! {"
2197 one two
2198 ˇthree
2199 four
2200 "});
2201
2202 cx.set_state(indoc! {"
2203 one two
2204 ˇ three
2205 four
2206 "});
2207 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2208 cx.assert_editor_state(indoc! {"
2209 one two
2210 ˇthree
2211 four
2212 "});
2213}
2214
2215#[gpui::test]
2216async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
2217 init_test(cx, |settings| {
2218 settings.defaults.hard_tabs = Some(true);
2219 });
2220
2221 let mut cx = EditorTestContext::new(cx).await;
2222
2223 // select two ranges on one line
2224 cx.set_state(indoc! {"
2225 «oneˇ» «twoˇ»
2226 three
2227 four
2228 "});
2229 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2230 cx.assert_editor_state(indoc! {"
2231 \t«oneˇ» «twoˇ»
2232 three
2233 four
2234 "});
2235 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2236 cx.assert_editor_state(indoc! {"
2237 \t\t«oneˇ» «twoˇ»
2238 three
2239 four
2240 "});
2241 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2242 cx.assert_editor_state(indoc! {"
2243 \t«oneˇ» «twoˇ»
2244 three
2245 four
2246 "});
2247 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2248 cx.assert_editor_state(indoc! {"
2249 «oneˇ» «twoˇ»
2250 three
2251 four
2252 "});
2253
2254 // select across a line ending
2255 cx.set_state(indoc! {"
2256 one two
2257 t«hree
2258 ˇ»four
2259 "});
2260 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2261 cx.assert_editor_state(indoc! {"
2262 one two
2263 \tt«hree
2264 ˇ»four
2265 "});
2266 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2267 cx.assert_editor_state(indoc! {"
2268 one two
2269 \t\tt«hree
2270 ˇ»four
2271 "});
2272 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2273 cx.assert_editor_state(indoc! {"
2274 one two
2275 \tt«hree
2276 ˇ»four
2277 "});
2278 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2279 cx.assert_editor_state(indoc! {"
2280 one two
2281 t«hree
2282 ˇ»four
2283 "});
2284
2285 // Ensure that indenting/outdenting works when the cursor is at column 0.
2286 cx.set_state(indoc! {"
2287 one two
2288 ˇthree
2289 four
2290 "});
2291 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2292 cx.assert_editor_state(indoc! {"
2293 one two
2294 ˇthree
2295 four
2296 "});
2297 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2298 cx.assert_editor_state(indoc! {"
2299 one two
2300 \tˇthree
2301 four
2302 "});
2303 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2304 cx.assert_editor_state(indoc! {"
2305 one two
2306 ˇthree
2307 four
2308 "});
2309}
2310
2311#[gpui::test]
2312fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
2313 init_test(cx, |settings| {
2314 settings.languages.extend([
2315 (
2316 "TOML".into(),
2317 LanguageSettingsContent {
2318 tab_size: NonZeroU32::new(2),
2319 ..Default::default()
2320 },
2321 ),
2322 (
2323 "Rust".into(),
2324 LanguageSettingsContent {
2325 tab_size: NonZeroU32::new(4),
2326 ..Default::default()
2327 },
2328 ),
2329 ]);
2330 });
2331
2332 let toml_language = Arc::new(Language::new(
2333 LanguageConfig {
2334 name: "TOML".into(),
2335 ..Default::default()
2336 },
2337 None,
2338 ));
2339 let rust_language = Arc::new(Language::new(
2340 LanguageConfig {
2341 name: "Rust".into(),
2342 ..Default::default()
2343 },
2344 None,
2345 ));
2346
2347 let toml_buffer = cx.new_model(|cx| {
2348 Buffer::new(
2349 0,
2350 BufferId::new(cx.entity_id().as_u64()).unwrap(),
2351 "a = 1\nb = 2\n",
2352 )
2353 .with_language(toml_language, cx)
2354 });
2355 let rust_buffer = cx.new_model(|cx| {
2356 Buffer::new(
2357 0,
2358 BufferId::new(cx.entity_id().as_u64()).unwrap(),
2359 "const c: usize = 3;\n",
2360 )
2361 .with_language(rust_language, cx)
2362 });
2363 let multibuffer = cx.new_model(|cx| {
2364 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
2365 multibuffer.push_excerpts(
2366 toml_buffer.clone(),
2367 [ExcerptRange {
2368 context: Point::new(0, 0)..Point::new(2, 0),
2369 primary: None,
2370 }],
2371 cx,
2372 );
2373 multibuffer.push_excerpts(
2374 rust_buffer.clone(),
2375 [ExcerptRange {
2376 context: Point::new(0, 0)..Point::new(1, 0),
2377 primary: None,
2378 }],
2379 cx,
2380 );
2381 multibuffer
2382 });
2383
2384 cx.add_window(|cx| {
2385 let mut editor = build_editor(multibuffer, cx);
2386
2387 assert_eq!(
2388 editor.text(cx),
2389 indoc! {"
2390 a = 1
2391 b = 2
2392
2393 const c: usize = 3;
2394 "}
2395 );
2396
2397 select_ranges(
2398 &mut editor,
2399 indoc! {"
2400 «aˇ» = 1
2401 b = 2
2402
2403 «const c:ˇ» usize = 3;
2404 "},
2405 cx,
2406 );
2407
2408 editor.tab(&Tab, cx);
2409 assert_text_with_selections(
2410 &mut editor,
2411 indoc! {"
2412 «aˇ» = 1
2413 b = 2
2414
2415 «const c:ˇ» usize = 3;
2416 "},
2417 cx,
2418 );
2419 editor.tab_prev(&TabPrev, cx);
2420 assert_text_with_selections(
2421 &mut editor,
2422 indoc! {"
2423 «aˇ» = 1
2424 b = 2
2425
2426 «const c:ˇ» usize = 3;
2427 "},
2428 cx,
2429 );
2430
2431 editor
2432 });
2433}
2434
2435#[gpui::test]
2436async fn test_backspace(cx: &mut gpui::TestAppContext) {
2437 init_test(cx, |_| {});
2438
2439 let mut cx = EditorTestContext::new(cx).await;
2440
2441 // Basic backspace
2442 cx.set_state(indoc! {"
2443 onˇe two three
2444 fou«rˇ» five six
2445 seven «ˇeight nine
2446 »ten
2447 "});
2448 cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
2449 cx.assert_editor_state(indoc! {"
2450 oˇe two three
2451 fouˇ five six
2452 seven ˇten
2453 "});
2454
2455 // Test backspace inside and around indents
2456 cx.set_state(indoc! {"
2457 zero
2458 ˇone
2459 ˇtwo
2460 ˇ ˇ ˇ three
2461 ˇ ˇ four
2462 "});
2463 cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
2464 cx.assert_editor_state(indoc! {"
2465 zero
2466 ˇone
2467 ˇtwo
2468 ˇ threeˇ four
2469 "});
2470
2471 // Test backspace with line_mode set to true
2472 cx.update_editor(|e, _| e.selections.line_mode = true);
2473 cx.set_state(indoc! {"
2474 The ˇquick ˇbrown
2475 fox jumps over
2476 the lazy dog
2477 ˇThe qu«ick bˇ»rown"});
2478 cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
2479 cx.assert_editor_state(indoc! {"
2480 ˇfox jumps over
2481 the lazy dogˇ"});
2482}
2483
2484#[gpui::test]
2485async fn test_delete(cx: &mut gpui::TestAppContext) {
2486 init_test(cx, |_| {});
2487
2488 let mut cx = EditorTestContext::new(cx).await;
2489 cx.set_state(indoc! {"
2490 onˇe two three
2491 fou«rˇ» five six
2492 seven «ˇeight nine
2493 »ten
2494 "});
2495 cx.update_editor(|e, cx| e.delete(&Delete, cx));
2496 cx.assert_editor_state(indoc! {"
2497 onˇ two three
2498 fouˇ five six
2499 seven ˇten
2500 "});
2501
2502 // Test backspace with line_mode set to true
2503 cx.update_editor(|e, _| e.selections.line_mode = true);
2504 cx.set_state(indoc! {"
2505 The ˇquick ˇbrown
2506 fox «ˇjum»ps over
2507 the lazy dog
2508 ˇThe qu«ick bˇ»rown"});
2509 cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
2510 cx.assert_editor_state("ˇthe lazy dogˇ");
2511}
2512
2513#[gpui::test]
2514fn test_delete_line(cx: &mut TestAppContext) {
2515 init_test(cx, |_| {});
2516
2517 let view = cx.add_window(|cx| {
2518 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
2519 build_editor(buffer, cx)
2520 });
2521 _ = view.update(cx, |view, cx| {
2522 view.change_selections(None, cx, |s| {
2523 s.select_display_ranges([
2524 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
2525 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
2526 DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
2527 ])
2528 });
2529 view.delete_line(&DeleteLine, cx);
2530 assert_eq!(view.display_text(cx), "ghi");
2531 assert_eq!(
2532 view.selections.display_ranges(cx),
2533 vec![
2534 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
2535 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)
2536 ]
2537 );
2538 });
2539
2540 let view = cx.add_window(|cx| {
2541 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
2542 build_editor(buffer, cx)
2543 });
2544 _ = view.update(cx, |view, cx| {
2545 view.change_selections(None, cx, |s| {
2546 s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)])
2547 });
2548 view.delete_line(&DeleteLine, cx);
2549 assert_eq!(view.display_text(cx), "ghi\n");
2550 assert_eq!(
2551 view.selections.display_ranges(cx),
2552 vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)]
2553 );
2554 });
2555}
2556
2557#[gpui::test]
2558fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
2559 init_test(cx, |_| {});
2560
2561 cx.add_window(|cx| {
2562 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
2563 let mut editor = build_editor(buffer.clone(), cx);
2564 let buffer = buffer.read(cx).as_singleton().unwrap();
2565
2566 assert_eq!(
2567 editor.selections.ranges::<Point>(cx),
2568 &[Point::new(0, 0)..Point::new(0, 0)]
2569 );
2570
2571 // When on single line, replace newline at end by space
2572 editor.join_lines(&JoinLines, cx);
2573 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
2574 assert_eq!(
2575 editor.selections.ranges::<Point>(cx),
2576 &[Point::new(0, 3)..Point::new(0, 3)]
2577 );
2578
2579 // When multiple lines are selected, remove newlines that are spanned by the selection
2580 editor.change_selections(None, cx, |s| {
2581 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
2582 });
2583 editor.join_lines(&JoinLines, cx);
2584 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
2585 assert_eq!(
2586 editor.selections.ranges::<Point>(cx),
2587 &[Point::new(0, 11)..Point::new(0, 11)]
2588 );
2589
2590 // Undo should be transactional
2591 editor.undo(&Undo, cx);
2592 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
2593 assert_eq!(
2594 editor.selections.ranges::<Point>(cx),
2595 &[Point::new(0, 5)..Point::new(2, 2)]
2596 );
2597
2598 // When joining an empty line don't insert a space
2599 editor.change_selections(None, cx, |s| {
2600 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
2601 });
2602 editor.join_lines(&JoinLines, cx);
2603 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
2604 assert_eq!(
2605 editor.selections.ranges::<Point>(cx),
2606 [Point::new(2, 3)..Point::new(2, 3)]
2607 );
2608
2609 // We can remove trailing newlines
2610 editor.join_lines(&JoinLines, cx);
2611 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
2612 assert_eq!(
2613 editor.selections.ranges::<Point>(cx),
2614 [Point::new(2, 3)..Point::new(2, 3)]
2615 );
2616
2617 // We don't blow up on the last line
2618 editor.join_lines(&JoinLines, cx);
2619 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
2620 assert_eq!(
2621 editor.selections.ranges::<Point>(cx),
2622 [Point::new(2, 3)..Point::new(2, 3)]
2623 );
2624
2625 // reset to test indentation
2626 editor.buffer.update(cx, |buffer, cx| {
2627 buffer.edit(
2628 [
2629 (Point::new(1, 0)..Point::new(1, 2), " "),
2630 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
2631 ],
2632 None,
2633 cx,
2634 )
2635 });
2636
2637 // We remove any leading spaces
2638 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
2639 editor.change_selections(None, cx, |s| {
2640 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
2641 });
2642 editor.join_lines(&JoinLines, cx);
2643 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
2644
2645 // We don't insert a space for a line containing only spaces
2646 editor.join_lines(&JoinLines, cx);
2647 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
2648
2649 // We ignore any leading tabs
2650 editor.join_lines(&JoinLines, cx);
2651 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
2652
2653 editor
2654 });
2655}
2656
2657#[gpui::test]
2658fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
2659 init_test(cx, |_| {});
2660
2661 cx.add_window(|cx| {
2662 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
2663 let mut editor = build_editor(buffer.clone(), cx);
2664 let buffer = buffer.read(cx).as_singleton().unwrap();
2665
2666 editor.change_selections(None, cx, |s| {
2667 s.select_ranges([
2668 Point::new(0, 2)..Point::new(1, 1),
2669 Point::new(1, 2)..Point::new(1, 2),
2670 Point::new(3, 1)..Point::new(3, 2),
2671 ])
2672 });
2673
2674 editor.join_lines(&JoinLines, cx);
2675 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
2676
2677 assert_eq!(
2678 editor.selections.ranges::<Point>(cx),
2679 [
2680 Point::new(0, 7)..Point::new(0, 7),
2681 Point::new(1, 3)..Point::new(1, 3)
2682 ]
2683 );
2684 editor
2685 });
2686}
2687
2688#[gpui::test]
2689async fn test_join_lines_with_git_diff_base(
2690 executor: BackgroundExecutor,
2691 cx: &mut gpui::TestAppContext,
2692) {
2693 init_test(cx, |_| {});
2694
2695 let mut cx = EditorTestContext::new(cx).await;
2696
2697 let diff_base = r#"
2698 Line 0
2699 Line 1
2700 Line 2
2701 Line 3
2702 "#
2703 .unindent();
2704
2705 cx.set_state(
2706 &r#"
2707 ˇLine 0
2708 Line 1
2709 Line 2
2710 Line 3
2711 "#
2712 .unindent(),
2713 );
2714
2715 cx.set_diff_base(Some(&diff_base));
2716 executor.run_until_parked();
2717
2718 // Join lines
2719 cx.update_editor(|editor, cx| {
2720 editor.join_lines(&JoinLines, cx);
2721 });
2722 executor.run_until_parked();
2723
2724 cx.assert_editor_state(
2725 &r#"
2726 Line 0ˇ Line 1
2727 Line 2
2728 Line 3
2729 "#
2730 .unindent(),
2731 );
2732 // Join again
2733 cx.update_editor(|editor, cx| {
2734 editor.join_lines(&JoinLines, cx);
2735 });
2736 executor.run_until_parked();
2737
2738 cx.assert_editor_state(
2739 &r#"
2740 Line 0 Line 1ˇ Line 2
2741 Line 3
2742 "#
2743 .unindent(),
2744 );
2745}
2746
2747#[gpui::test]
2748async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
2749 init_test(cx, |_| {});
2750
2751 let mut cx = EditorTestContext::new(cx).await;
2752
2753 // Test sort_lines_case_insensitive()
2754 cx.set_state(indoc! {"
2755 «z
2756 y
2757 x
2758 Z
2759 Y
2760 Xˇ»
2761 "});
2762 cx.update_editor(|e, cx| e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, cx));
2763 cx.assert_editor_state(indoc! {"
2764 «x
2765 X
2766 y
2767 Y
2768 z
2769 Zˇ»
2770 "});
2771
2772 // Test reverse_lines()
2773 cx.set_state(indoc! {"
2774 «5
2775 4
2776 3
2777 2
2778 1ˇ»
2779 "});
2780 cx.update_editor(|e, cx| e.reverse_lines(&ReverseLines, cx));
2781 cx.assert_editor_state(indoc! {"
2782 «1
2783 2
2784 3
2785 4
2786 5ˇ»
2787 "});
2788
2789 // Skip testing shuffle_line()
2790
2791 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
2792 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
2793
2794 // Don't manipulate when cursor is on single line, but expand the selection
2795 cx.set_state(indoc! {"
2796 ddˇdd
2797 ccc
2798 bb
2799 a
2800 "});
2801 cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
2802 cx.assert_editor_state(indoc! {"
2803 «ddddˇ»
2804 ccc
2805 bb
2806 a
2807 "});
2808
2809 // Basic manipulate case
2810 // Start selection moves to column 0
2811 // End of selection shrinks to fit shorter line
2812 cx.set_state(indoc! {"
2813 dd«d
2814 ccc
2815 bb
2816 aaaaaˇ»
2817 "});
2818 cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
2819 cx.assert_editor_state(indoc! {"
2820 «aaaaa
2821 bb
2822 ccc
2823 dddˇ»
2824 "});
2825
2826 // Manipulate case with newlines
2827 cx.set_state(indoc! {"
2828 dd«d
2829 ccc
2830
2831 bb
2832 aaaaa
2833
2834 ˇ»
2835 "});
2836 cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
2837 cx.assert_editor_state(indoc! {"
2838 «
2839
2840 aaaaa
2841 bb
2842 ccc
2843 dddˇ»
2844
2845 "});
2846
2847 // Adding new line
2848 cx.set_state(indoc! {"
2849 aa«a
2850 bbˇ»b
2851 "});
2852 cx.update_editor(|e, cx| e.manipulate_lines(cx, |lines| lines.push("added_line")));
2853 cx.assert_editor_state(indoc! {"
2854 «aaa
2855 bbb
2856 added_lineˇ»
2857 "});
2858
2859 // Removing line
2860 cx.set_state(indoc! {"
2861 aa«a
2862 bbbˇ»
2863 "});
2864 cx.update_editor(|e, cx| {
2865 e.manipulate_lines(cx, |lines| {
2866 lines.pop();
2867 })
2868 });
2869 cx.assert_editor_state(indoc! {"
2870 «aaaˇ»
2871 "});
2872
2873 // Removing all lines
2874 cx.set_state(indoc! {"
2875 aa«a
2876 bbbˇ»
2877 "});
2878 cx.update_editor(|e, cx| {
2879 e.manipulate_lines(cx, |lines| {
2880 lines.drain(..);
2881 })
2882 });
2883 cx.assert_editor_state(indoc! {"
2884 ˇ
2885 "});
2886}
2887
2888#[gpui::test]
2889async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
2890 init_test(cx, |_| {});
2891
2892 let mut cx = EditorTestContext::new(cx).await;
2893
2894 // Consider continuous selection as single selection
2895 cx.set_state(indoc! {"
2896 Aaa«aa
2897 cˇ»c«c
2898 bb
2899 aaaˇ»aa
2900 "});
2901 cx.update_editor(|e, cx| e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, cx));
2902 cx.assert_editor_state(indoc! {"
2903 «Aaaaa
2904 ccc
2905 bb
2906 aaaaaˇ»
2907 "});
2908
2909 cx.set_state(indoc! {"
2910 Aaa«aa
2911 cˇ»c«c
2912 bb
2913 aaaˇ»aa
2914 "});
2915 cx.update_editor(|e, cx| e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, cx));
2916 cx.assert_editor_state(indoc! {"
2917 «Aaaaa
2918 ccc
2919 bbˇ»
2920 "});
2921
2922 // Consider non continuous selection as distinct dedup operations
2923 cx.set_state(indoc! {"
2924 «aaaaa
2925 bb
2926 aaaaa
2927 aaaaaˇ»
2928
2929 aaa«aaˇ»
2930 "});
2931 cx.update_editor(|e, cx| e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, cx));
2932 cx.assert_editor_state(indoc! {"
2933 «aaaaa
2934 bbˇ»
2935
2936 «aaaaaˇ»
2937 "});
2938}
2939
2940#[gpui::test]
2941async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
2942 init_test(cx, |_| {});
2943
2944 let mut cx = EditorTestContext::new(cx).await;
2945
2946 cx.set_state(indoc! {"
2947 «Aaa
2948 aAa
2949 Aaaˇ»
2950 "});
2951 cx.update_editor(|e, cx| e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, cx));
2952 cx.assert_editor_state(indoc! {"
2953 «Aaa
2954 aAaˇ»
2955 "});
2956
2957 cx.set_state(indoc! {"
2958 «Aaa
2959 aAa
2960 aaAˇ»
2961 "});
2962 cx.update_editor(|e, cx| e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, cx));
2963 cx.assert_editor_state(indoc! {"
2964 «Aaaˇ»
2965 "});
2966}
2967
2968#[gpui::test]
2969async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
2970 init_test(cx, |_| {});
2971
2972 let mut cx = EditorTestContext::new(cx).await;
2973
2974 // Manipulate with multiple selections on a single line
2975 cx.set_state(indoc! {"
2976 dd«dd
2977 cˇ»c«c
2978 bb
2979 aaaˇ»aa
2980 "});
2981 cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
2982 cx.assert_editor_state(indoc! {"
2983 «aaaaa
2984 bb
2985 ccc
2986 ddddˇ»
2987 "});
2988
2989 // Manipulate with multiple disjoin selections
2990 cx.set_state(indoc! {"
2991 5«
2992 4
2993 3
2994 2
2995 1ˇ»
2996
2997 dd«dd
2998 ccc
2999 bb
3000 aaaˇ»aa
3001 "});
3002 cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
3003 cx.assert_editor_state(indoc! {"
3004 «1
3005 2
3006 3
3007 4
3008 5ˇ»
3009
3010 «aaaaa
3011 bb
3012 ccc
3013 ddddˇ»
3014 "});
3015
3016 // Adding lines on each selection
3017 cx.set_state(indoc! {"
3018 2«
3019 1ˇ»
3020
3021 bb«bb
3022 aaaˇ»aa
3023 "});
3024 cx.update_editor(|e, cx| e.manipulate_lines(cx, |lines| lines.push("added line")));
3025 cx.assert_editor_state(indoc! {"
3026 «2
3027 1
3028 added lineˇ»
3029
3030 «bbbb
3031 aaaaa
3032 added lineˇ»
3033 "});
3034
3035 // Removing lines on each selection
3036 cx.set_state(indoc! {"
3037 2«
3038 1ˇ»
3039
3040 bb«bb
3041 aaaˇ»aa
3042 "});
3043 cx.update_editor(|e, cx| {
3044 e.manipulate_lines(cx, |lines| {
3045 lines.pop();
3046 })
3047 });
3048 cx.assert_editor_state(indoc! {"
3049 «2ˇ»
3050
3051 «bbbbˇ»
3052 "});
3053}
3054
3055#[gpui::test]
3056async fn test_manipulate_text(cx: &mut TestAppContext) {
3057 init_test(cx, |_| {});
3058
3059 let mut cx = EditorTestContext::new(cx).await;
3060
3061 // Test convert_to_upper_case()
3062 cx.set_state(indoc! {"
3063 «hello worldˇ»
3064 "});
3065 cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
3066 cx.assert_editor_state(indoc! {"
3067 «HELLO WORLDˇ»
3068 "});
3069
3070 // Test convert_to_lower_case()
3071 cx.set_state(indoc! {"
3072 «HELLO WORLDˇ»
3073 "});
3074 cx.update_editor(|e, cx| e.convert_to_lower_case(&ConvertToLowerCase, cx));
3075 cx.assert_editor_state(indoc! {"
3076 «hello worldˇ»
3077 "});
3078
3079 // Test multiple line, single selection case
3080 // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
3081 cx.set_state(indoc! {"
3082 «The quick brown
3083 fox jumps over
3084 the lazy dogˇ»
3085 "});
3086 cx.update_editor(|e, cx| e.convert_to_title_case(&ConvertToTitleCase, cx));
3087 cx.assert_editor_state(indoc! {"
3088 «The Quick Brown
3089 Fox Jumps Over
3090 The Lazy Dogˇ»
3091 "});
3092
3093 // Test multiple line, single selection case
3094 // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
3095 cx.set_state(indoc! {"
3096 «The quick brown
3097 fox jumps over
3098 the lazy dogˇ»
3099 "});
3100 cx.update_editor(|e, cx| e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, cx));
3101 cx.assert_editor_state(indoc! {"
3102 «TheQuickBrown
3103 FoxJumpsOver
3104 TheLazyDogˇ»
3105 "});
3106
3107 // From here on out, test more complex cases of manipulate_text()
3108
3109 // Test no selection case - should affect words cursors are in
3110 // Cursor at beginning, middle, and end of word
3111 cx.set_state(indoc! {"
3112 ˇhello big beauˇtiful worldˇ
3113 "});
3114 cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
3115 cx.assert_editor_state(indoc! {"
3116 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
3117 "});
3118
3119 // Test multiple selections on a single line and across multiple lines
3120 cx.set_state(indoc! {"
3121 «Theˇ» quick «brown
3122 foxˇ» jumps «overˇ»
3123 the «lazyˇ» dog
3124 "});
3125 cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
3126 cx.assert_editor_state(indoc! {"
3127 «THEˇ» quick «BROWN
3128 FOXˇ» jumps «OVERˇ»
3129 the «LAZYˇ» dog
3130 "});
3131
3132 // Test case where text length grows
3133 cx.set_state(indoc! {"
3134 «tschüߡ»
3135 "});
3136 cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
3137 cx.assert_editor_state(indoc! {"
3138 «TSCHÜSSˇ»
3139 "});
3140
3141 // Test to make sure we don't crash when text shrinks
3142 cx.set_state(indoc! {"
3143 aaa_bbbˇ
3144 "});
3145 cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
3146 cx.assert_editor_state(indoc! {"
3147 «aaaBbbˇ»
3148 "});
3149
3150 // Test to make sure we all aware of the fact that each word can grow and shrink
3151 // Final selections should be aware of this fact
3152 cx.set_state(indoc! {"
3153 aaa_bˇbb bbˇb_ccc ˇccc_ddd
3154 "});
3155 cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
3156 cx.assert_editor_state(indoc! {"
3157 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
3158 "});
3159}
3160
3161#[gpui::test]
3162fn test_duplicate_line(cx: &mut TestAppContext) {
3163 init_test(cx, |_| {});
3164
3165 let view = cx.add_window(|cx| {
3166 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3167 build_editor(buffer, cx)
3168 });
3169 _ = view.update(cx, |view, cx| {
3170 view.change_selections(None, cx, |s| {
3171 s.select_display_ranges([
3172 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
3173 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3174 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
3175 DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
3176 ])
3177 });
3178 view.duplicate_line_down(&DuplicateLineDown, cx);
3179 assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
3180 assert_eq!(
3181 view.selections.display_ranges(cx),
3182 vec![
3183 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
3184 DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
3185 DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
3186 DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0),
3187 ]
3188 );
3189 });
3190
3191 let view = cx.add_window(|cx| {
3192 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3193 build_editor(buffer, cx)
3194 });
3195 _ = view.update(cx, |view, cx| {
3196 view.change_selections(None, cx, |s| {
3197 s.select_display_ranges([
3198 DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1),
3199 DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1),
3200 ])
3201 });
3202 view.duplicate_line_down(&DuplicateLineDown, cx);
3203 assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
3204 assert_eq!(
3205 view.selections.display_ranges(cx),
3206 vec![
3207 DisplayPoint::new(3, 1)..DisplayPoint::new(4, 1),
3208 DisplayPoint::new(4, 2)..DisplayPoint::new(5, 1),
3209 ]
3210 );
3211 });
3212
3213 // With `move_upwards` the selections stay in place, except for
3214 // the lines inserted above them
3215 let view = cx.add_window(|cx| {
3216 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3217 build_editor(buffer, cx)
3218 });
3219 _ = view.update(cx, |view, cx| {
3220 view.change_selections(None, cx, |s| {
3221 s.select_display_ranges([
3222 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
3223 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3224 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
3225 DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
3226 ])
3227 });
3228 view.duplicate_line_up(&DuplicateLineUp, cx);
3229 assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
3230 assert_eq!(
3231 view.selections.display_ranges(cx),
3232 vec![
3233 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
3234 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3235 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0),
3236 DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0),
3237 ]
3238 );
3239 });
3240
3241 let view = cx.add_window(|cx| {
3242 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3243 build_editor(buffer, cx)
3244 });
3245 _ = view.update(cx, |view, cx| {
3246 view.change_selections(None, cx, |s| {
3247 s.select_display_ranges([
3248 DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1),
3249 DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1),
3250 ])
3251 });
3252 view.duplicate_line_up(&DuplicateLineUp, cx);
3253 assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
3254 assert_eq!(
3255 view.selections.display_ranges(cx),
3256 vec![
3257 DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1),
3258 DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1),
3259 ]
3260 );
3261 });
3262}
3263
3264#[gpui::test]
3265fn test_move_line_up_down(cx: &mut TestAppContext) {
3266 init_test(cx, |_| {});
3267
3268 let view = cx.add_window(|cx| {
3269 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
3270 build_editor(buffer, cx)
3271 });
3272 _ = view.update(cx, |view, cx| {
3273 view.fold_ranges(
3274 vec![
3275 Point::new(0, 2)..Point::new(1, 2),
3276 Point::new(2, 3)..Point::new(4, 1),
3277 Point::new(7, 0)..Point::new(8, 4),
3278 ],
3279 true,
3280 cx,
3281 );
3282 view.change_selections(None, cx, |s| {
3283 s.select_display_ranges([
3284 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
3285 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
3286 DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
3287 DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2),
3288 ])
3289 });
3290 assert_eq!(
3291 view.display_text(cx),
3292 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
3293 );
3294
3295 view.move_line_up(&MoveLineUp, cx);
3296 assert_eq!(
3297 view.display_text(cx),
3298 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
3299 );
3300 assert_eq!(
3301 view.selections.display_ranges(cx),
3302 vec![
3303 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
3304 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
3305 DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3),
3306 DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2)
3307 ]
3308 );
3309 });
3310
3311 _ = view.update(cx, |view, cx| {
3312 view.move_line_down(&MoveLineDown, cx);
3313 assert_eq!(
3314 view.display_text(cx),
3315 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
3316 );
3317 assert_eq!(
3318 view.selections.display_ranges(cx),
3319 vec![
3320 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
3321 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
3322 DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
3323 DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2)
3324 ]
3325 );
3326 });
3327
3328 _ = view.update(cx, |view, cx| {
3329 view.move_line_down(&MoveLineDown, cx);
3330 assert_eq!(
3331 view.display_text(cx),
3332 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
3333 );
3334 assert_eq!(
3335 view.selections.display_ranges(cx),
3336 vec![
3337 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
3338 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
3339 DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
3340 DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2)
3341 ]
3342 );
3343 });
3344
3345 _ = view.update(cx, |view, cx| {
3346 view.move_line_up(&MoveLineUp, cx);
3347 assert_eq!(
3348 view.display_text(cx),
3349 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
3350 );
3351 assert_eq!(
3352 view.selections.display_ranges(cx),
3353 vec![
3354 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
3355 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
3356 DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3),
3357 DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2)
3358 ]
3359 );
3360 });
3361}
3362
3363#[gpui::test]
3364fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
3365 init_test(cx, |_| {});
3366
3367 let editor = cx.add_window(|cx| {
3368 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
3369 build_editor(buffer, cx)
3370 });
3371 _ = editor.update(cx, |editor, cx| {
3372 let snapshot = editor.buffer.read(cx).snapshot(cx);
3373 editor.insert_blocks(
3374 [BlockProperties {
3375 style: BlockStyle::Fixed,
3376 position: snapshot.anchor_after(Point::new(2, 0)),
3377 disposition: BlockDisposition::Below,
3378 height: 1,
3379 render: Box::new(|_| div().into_any()),
3380 }],
3381 Some(Autoscroll::fit()),
3382 cx,
3383 );
3384 editor.change_selections(None, cx, |s| {
3385 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
3386 });
3387 editor.move_line_down(&MoveLineDown, cx);
3388 });
3389}
3390
3391#[gpui::test]
3392fn test_transpose(cx: &mut TestAppContext) {
3393 init_test(cx, |_| {});
3394
3395 _ = cx.add_window(|cx| {
3396 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx);
3397 editor.set_style(EditorStyle::default(), cx);
3398 editor.change_selections(None, cx, |s| s.select_ranges([1..1]));
3399 editor.transpose(&Default::default(), cx);
3400 assert_eq!(editor.text(cx), "bac");
3401 assert_eq!(editor.selections.ranges(cx), [2..2]);
3402
3403 editor.transpose(&Default::default(), cx);
3404 assert_eq!(editor.text(cx), "bca");
3405 assert_eq!(editor.selections.ranges(cx), [3..3]);
3406
3407 editor.transpose(&Default::default(), cx);
3408 assert_eq!(editor.text(cx), "bac");
3409 assert_eq!(editor.selections.ranges(cx), [3..3]);
3410
3411 editor
3412 });
3413
3414 _ = cx.add_window(|cx| {
3415 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
3416 editor.set_style(EditorStyle::default(), cx);
3417 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
3418 editor.transpose(&Default::default(), cx);
3419 assert_eq!(editor.text(cx), "acb\nde");
3420 assert_eq!(editor.selections.ranges(cx), [3..3]);
3421
3422 editor.change_selections(None, cx, |s| s.select_ranges([4..4]));
3423 editor.transpose(&Default::default(), cx);
3424 assert_eq!(editor.text(cx), "acbd\ne");
3425 assert_eq!(editor.selections.ranges(cx), [5..5]);
3426
3427 editor.transpose(&Default::default(), cx);
3428 assert_eq!(editor.text(cx), "acbde\n");
3429 assert_eq!(editor.selections.ranges(cx), [6..6]);
3430
3431 editor.transpose(&Default::default(), cx);
3432 assert_eq!(editor.text(cx), "acbd\ne");
3433 assert_eq!(editor.selections.ranges(cx), [6..6]);
3434
3435 editor
3436 });
3437
3438 _ = cx.add_window(|cx| {
3439 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
3440 editor.set_style(EditorStyle::default(), cx);
3441 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
3442 editor.transpose(&Default::default(), cx);
3443 assert_eq!(editor.text(cx), "bacd\ne");
3444 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
3445
3446 editor.transpose(&Default::default(), cx);
3447 assert_eq!(editor.text(cx), "bcade\n");
3448 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
3449
3450 editor.transpose(&Default::default(), cx);
3451 assert_eq!(editor.text(cx), "bcda\ne");
3452 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
3453
3454 editor.transpose(&Default::default(), cx);
3455 assert_eq!(editor.text(cx), "bcade\n");
3456 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
3457
3458 editor.transpose(&Default::default(), cx);
3459 assert_eq!(editor.text(cx), "bcaed\n");
3460 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
3461
3462 editor
3463 });
3464
3465 _ = cx.add_window(|cx| {
3466 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx);
3467 editor.set_style(EditorStyle::default(), cx);
3468 editor.change_selections(None, cx, |s| s.select_ranges([4..4]));
3469 editor.transpose(&Default::default(), cx);
3470 assert_eq!(editor.text(cx), "🏀🍐✋");
3471 assert_eq!(editor.selections.ranges(cx), [8..8]);
3472
3473 editor.transpose(&Default::default(), cx);
3474 assert_eq!(editor.text(cx), "🏀✋🍐");
3475 assert_eq!(editor.selections.ranges(cx), [11..11]);
3476
3477 editor.transpose(&Default::default(), cx);
3478 assert_eq!(editor.text(cx), "🏀🍐✋");
3479 assert_eq!(editor.selections.ranges(cx), [11..11]);
3480
3481 editor
3482 });
3483}
3484
3485#[gpui::test]
3486async fn test_clipboard(cx: &mut gpui::TestAppContext) {
3487 init_test(cx, |_| {});
3488
3489 let mut cx = EditorTestContext::new(cx).await;
3490
3491 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
3492 cx.update_editor(|e, cx| e.cut(&Cut, cx));
3493 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
3494
3495 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
3496 cx.set_state("two ˇfour ˇsix ˇ");
3497 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3498 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
3499
3500 // Paste again but with only two cursors. Since the number of cursors doesn't
3501 // match the number of slices in the clipboard, the entire clipboard text
3502 // is pasted at each cursor.
3503 cx.set_state("ˇtwo one✅ four three six five ˇ");
3504 cx.update_editor(|e, cx| {
3505 e.handle_input("( ", cx);
3506 e.paste(&Paste, cx);
3507 e.handle_input(") ", cx);
3508 });
3509 cx.assert_editor_state(
3510 &([
3511 "( one✅ ",
3512 "three ",
3513 "five ) ˇtwo one✅ four three six five ( one✅ ",
3514 "three ",
3515 "five ) ˇ",
3516 ]
3517 .join("\n")),
3518 );
3519
3520 // Cut with three selections, one of which is full-line.
3521 cx.set_state(indoc! {"
3522 1«2ˇ»3
3523 4ˇ567
3524 «8ˇ»9"});
3525 cx.update_editor(|e, cx| e.cut(&Cut, cx));
3526 cx.assert_editor_state(indoc! {"
3527 1ˇ3
3528 ˇ9"});
3529
3530 // Paste with three selections, noticing how the copied selection that was full-line
3531 // gets inserted before the second cursor.
3532 cx.set_state(indoc! {"
3533 1ˇ3
3534 9ˇ
3535 «oˇ»ne"});
3536 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3537 cx.assert_editor_state(indoc! {"
3538 12ˇ3
3539 4567
3540 9ˇ
3541 8ˇne"});
3542
3543 // Copy with a single cursor only, which writes the whole line into the clipboard.
3544 cx.set_state(indoc! {"
3545 The quick brown
3546 fox juˇmps over
3547 the lazy dog"});
3548 cx.update_editor(|e, cx| e.copy(&Copy, cx));
3549 assert_eq!(
3550 cx.read_from_clipboard().map(|item| item.text().to_owned()),
3551 Some("fox jumps over\n".to_owned())
3552 );
3553
3554 // Paste with three selections, noticing how the copied full-line selection is inserted
3555 // before the empty selections but replaces the selection that is non-empty.
3556 cx.set_state(indoc! {"
3557 Tˇhe quick brown
3558 «foˇ»x jumps over
3559 tˇhe lazy dog"});
3560 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3561 cx.assert_editor_state(indoc! {"
3562 fox jumps over
3563 Tˇhe quick brown
3564 fox jumps over
3565 ˇx jumps over
3566 fox jumps over
3567 tˇhe lazy dog"});
3568}
3569
3570#[gpui::test]
3571async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
3572 init_test(cx, |_| {});
3573
3574 let mut cx = EditorTestContext::new(cx).await;
3575 let language = Arc::new(Language::new(
3576 LanguageConfig::default(),
3577 Some(tree_sitter_rust::language()),
3578 ));
3579 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3580
3581 // Cut an indented block, without the leading whitespace.
3582 cx.set_state(indoc! {"
3583 const a: B = (
3584 c(),
3585 «d(
3586 e,
3587 f
3588 )ˇ»
3589 );
3590 "});
3591 cx.update_editor(|e, cx| e.cut(&Cut, cx));
3592 cx.assert_editor_state(indoc! {"
3593 const a: B = (
3594 c(),
3595 ˇ
3596 );
3597 "});
3598
3599 // Paste it at the same position.
3600 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3601 cx.assert_editor_state(indoc! {"
3602 const a: B = (
3603 c(),
3604 d(
3605 e,
3606 f
3607 )ˇ
3608 );
3609 "});
3610
3611 // Paste it at a line with a lower indent level.
3612 cx.set_state(indoc! {"
3613 ˇ
3614 const a: B = (
3615 c(),
3616 );
3617 "});
3618 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3619 cx.assert_editor_state(indoc! {"
3620 d(
3621 e,
3622 f
3623 )ˇ
3624 const a: B = (
3625 c(),
3626 );
3627 "});
3628
3629 // Cut an indented block, with the leading whitespace.
3630 cx.set_state(indoc! {"
3631 const a: B = (
3632 c(),
3633 « d(
3634 e,
3635 f
3636 )
3637 ˇ»);
3638 "});
3639 cx.update_editor(|e, cx| e.cut(&Cut, cx));
3640 cx.assert_editor_state(indoc! {"
3641 const a: B = (
3642 c(),
3643 ˇ);
3644 "});
3645
3646 // Paste it at the same position.
3647 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3648 cx.assert_editor_state(indoc! {"
3649 const a: B = (
3650 c(),
3651 d(
3652 e,
3653 f
3654 )
3655 ˇ);
3656 "});
3657
3658 // Paste it at a line with a higher indent level.
3659 cx.set_state(indoc! {"
3660 const a: B = (
3661 c(),
3662 d(
3663 e,
3664 fˇ
3665 )
3666 );
3667 "});
3668 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3669 cx.assert_editor_state(indoc! {"
3670 const a: B = (
3671 c(),
3672 d(
3673 e,
3674 f d(
3675 e,
3676 f
3677 )
3678 ˇ
3679 )
3680 );
3681 "});
3682}
3683
3684#[gpui::test]
3685fn test_select_all(cx: &mut TestAppContext) {
3686 init_test(cx, |_| {});
3687
3688 let view = cx.add_window(|cx| {
3689 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
3690 build_editor(buffer, cx)
3691 });
3692 _ = view.update(cx, |view, cx| {
3693 view.select_all(&SelectAll, cx);
3694 assert_eq!(
3695 view.selections.display_ranges(cx),
3696 &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)]
3697 );
3698 });
3699}
3700
3701#[gpui::test]
3702fn test_select_line(cx: &mut TestAppContext) {
3703 init_test(cx, |_| {});
3704
3705 let view = cx.add_window(|cx| {
3706 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
3707 build_editor(buffer, cx)
3708 });
3709 _ = view.update(cx, |view, cx| {
3710 view.change_selections(None, cx, |s| {
3711 s.select_display_ranges([
3712 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
3713 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3714 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
3715 DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2),
3716 ])
3717 });
3718 view.select_line(&SelectLine, cx);
3719 assert_eq!(
3720 view.selections.display_ranges(cx),
3721 vec![
3722 DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0),
3723 DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0),
3724 ]
3725 );
3726 });
3727
3728 _ = view.update(cx, |view, cx| {
3729 view.select_line(&SelectLine, cx);
3730 assert_eq!(
3731 view.selections.display_ranges(cx),
3732 vec![
3733 DisplayPoint::new(0, 0)..DisplayPoint::new(3, 0),
3734 DisplayPoint::new(4, 0)..DisplayPoint::new(5, 5),
3735 ]
3736 );
3737 });
3738
3739 _ = view.update(cx, |view, cx| {
3740 view.select_line(&SelectLine, cx);
3741 assert_eq!(
3742 view.selections.display_ranges(cx),
3743 vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)]
3744 );
3745 });
3746}
3747
3748#[gpui::test]
3749fn test_split_selection_into_lines(cx: &mut TestAppContext) {
3750 init_test(cx, |_| {});
3751
3752 let view = cx.add_window(|cx| {
3753 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
3754 build_editor(buffer, cx)
3755 });
3756 _ = view.update(cx, |view, cx| {
3757 view.fold_ranges(
3758 vec![
3759 Point::new(0, 2)..Point::new(1, 2),
3760 Point::new(2, 3)..Point::new(4, 1),
3761 Point::new(7, 0)..Point::new(8, 4),
3762 ],
3763 true,
3764 cx,
3765 );
3766 view.change_selections(None, cx, |s| {
3767 s.select_display_ranges([
3768 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
3769 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3770 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
3771 DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4),
3772 ])
3773 });
3774 assert_eq!(view.display_text(cx), "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i");
3775 });
3776
3777 _ = view.update(cx, |view, cx| {
3778 view.split_selection_into_lines(&SplitSelectionIntoLines, cx);
3779 assert_eq!(
3780 view.display_text(cx),
3781 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
3782 );
3783 assert_eq!(
3784 view.selections.display_ranges(cx),
3785 [
3786 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
3787 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3788 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0),
3789 DisplayPoint::new(5, 4)..DisplayPoint::new(5, 4)
3790 ]
3791 );
3792 });
3793
3794 _ = view.update(cx, |view, cx| {
3795 view.change_selections(None, cx, |s| {
3796 s.select_display_ranges([DisplayPoint::new(5, 0)..DisplayPoint::new(0, 1)])
3797 });
3798 view.split_selection_into_lines(&SplitSelectionIntoLines, cx);
3799 assert_eq!(
3800 view.display_text(cx),
3801 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
3802 );
3803 assert_eq!(
3804 view.selections.display_ranges(cx),
3805 [
3806 DisplayPoint::new(0, 5)..DisplayPoint::new(0, 5),
3807 DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5),
3808 DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
3809 DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5),
3810 DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5),
3811 DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5),
3812 DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5),
3813 DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0)
3814 ]
3815 );
3816 });
3817}
3818
3819#[gpui::test]
3820async fn test_add_selection_above_below(cx: &mut TestAppContext) {
3821 init_test(cx, |_| {});
3822
3823 let mut cx = EditorTestContext::new(cx).await;
3824
3825 // let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx);
3826 cx.set_state(indoc!(
3827 r#"abc
3828 defˇghi
3829
3830 jk
3831 nlmo
3832 "#
3833 ));
3834
3835 cx.update_editor(|editor, cx| {
3836 editor.add_selection_above(&Default::default(), cx);
3837 });
3838
3839 cx.assert_editor_state(indoc!(
3840 r#"abcˇ
3841 defˇghi
3842
3843 jk
3844 nlmo
3845 "#
3846 ));
3847
3848 cx.update_editor(|editor, cx| {
3849 editor.add_selection_above(&Default::default(), cx);
3850 });
3851
3852 cx.assert_editor_state(indoc!(
3853 r#"abcˇ
3854 defˇghi
3855
3856 jk
3857 nlmo
3858 "#
3859 ));
3860
3861 cx.update_editor(|view, cx| {
3862 view.add_selection_below(&Default::default(), cx);
3863 });
3864
3865 cx.assert_editor_state(indoc!(
3866 r#"abc
3867 defˇghi
3868
3869 jk
3870 nlmo
3871 "#
3872 ));
3873
3874 cx.update_editor(|view, cx| {
3875 view.undo_selection(&Default::default(), cx);
3876 });
3877
3878 cx.assert_editor_state(indoc!(
3879 r#"abcˇ
3880 defˇghi
3881
3882 jk
3883 nlmo
3884 "#
3885 ));
3886
3887 cx.update_editor(|view, cx| {
3888 view.redo_selection(&Default::default(), cx);
3889 });
3890
3891 cx.assert_editor_state(indoc!(
3892 r#"abc
3893 defˇghi
3894
3895 jk
3896 nlmo
3897 "#
3898 ));
3899
3900 cx.update_editor(|view, cx| {
3901 view.add_selection_below(&Default::default(), cx);
3902 });
3903
3904 cx.assert_editor_state(indoc!(
3905 r#"abc
3906 defˇghi
3907
3908 jk
3909 nlmˇo
3910 "#
3911 ));
3912
3913 cx.update_editor(|view, cx| {
3914 view.add_selection_below(&Default::default(), cx);
3915 });
3916
3917 cx.assert_editor_state(indoc!(
3918 r#"abc
3919 defˇghi
3920
3921 jk
3922 nlmˇo
3923 "#
3924 ));
3925
3926 // change selections
3927 cx.set_state(indoc!(
3928 r#"abc
3929 def«ˇg»hi
3930
3931 jk
3932 nlmo
3933 "#
3934 ));
3935
3936 cx.update_editor(|view, cx| {
3937 view.add_selection_below(&Default::default(), cx);
3938 });
3939
3940 cx.assert_editor_state(indoc!(
3941 r#"abc
3942 def«ˇg»hi
3943
3944 jk
3945 nlm«ˇo»
3946 "#
3947 ));
3948
3949 cx.update_editor(|view, cx| {
3950 view.add_selection_below(&Default::default(), cx);
3951 });
3952
3953 cx.assert_editor_state(indoc!(
3954 r#"abc
3955 def«ˇg»hi
3956
3957 jk
3958 nlm«ˇo»
3959 "#
3960 ));
3961
3962 cx.update_editor(|view, cx| {
3963 view.add_selection_above(&Default::default(), cx);
3964 });
3965
3966 cx.assert_editor_state(indoc!(
3967 r#"abc
3968 def«ˇg»hi
3969
3970 jk
3971 nlmo
3972 "#
3973 ));
3974
3975 cx.update_editor(|view, cx| {
3976 view.add_selection_above(&Default::default(), cx);
3977 });
3978
3979 cx.assert_editor_state(indoc!(
3980 r#"abc
3981 def«ˇg»hi
3982
3983 jk
3984 nlmo
3985 "#
3986 ));
3987
3988 // Change selections again
3989 cx.set_state(indoc!(
3990 r#"a«bc
3991 defgˇ»hi
3992
3993 jk
3994 nlmo
3995 "#
3996 ));
3997
3998 cx.update_editor(|view, cx| {
3999 view.add_selection_below(&Default::default(), cx);
4000 });
4001
4002 cx.assert_editor_state(indoc!(
4003 r#"a«bcˇ»
4004 d«efgˇ»hi
4005
4006 j«kˇ»
4007 nlmo
4008 "#
4009 ));
4010
4011 cx.update_editor(|view, cx| {
4012 view.add_selection_below(&Default::default(), cx);
4013 });
4014 cx.assert_editor_state(indoc!(
4015 r#"a«bcˇ»
4016 d«efgˇ»hi
4017
4018 j«kˇ»
4019 n«lmoˇ»
4020 "#
4021 ));
4022 cx.update_editor(|view, cx| {
4023 view.add_selection_above(&Default::default(), cx);
4024 });
4025
4026 cx.assert_editor_state(indoc!(
4027 r#"a«bcˇ»
4028 d«efgˇ»hi
4029
4030 j«kˇ»
4031 nlmo
4032 "#
4033 ));
4034
4035 // Change selections again
4036 cx.set_state(indoc!(
4037 r#"abc
4038 d«ˇefghi
4039
4040 jk
4041 nlm»o
4042 "#
4043 ));
4044
4045 cx.update_editor(|view, cx| {
4046 view.add_selection_above(&Default::default(), cx);
4047 });
4048
4049 cx.assert_editor_state(indoc!(
4050 r#"a«ˇbc»
4051 d«ˇef»ghi
4052
4053 j«ˇk»
4054 n«ˇlm»o
4055 "#
4056 ));
4057
4058 cx.update_editor(|view, cx| {
4059 view.add_selection_below(&Default::default(), cx);
4060 });
4061
4062 cx.assert_editor_state(indoc!(
4063 r#"abc
4064 d«ˇef»ghi
4065
4066 j«ˇk»
4067 n«ˇlm»o
4068 "#
4069 ));
4070}
4071
4072#[gpui::test]
4073async fn test_select_next(cx: &mut gpui::TestAppContext) {
4074 init_test(cx, |_| {});
4075
4076 let mut cx = EditorTestContext::new(cx).await;
4077 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
4078
4079 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
4080 .unwrap();
4081 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
4082
4083 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
4084 .unwrap();
4085 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
4086
4087 cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
4088 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
4089
4090 cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
4091 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
4092
4093 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
4094 .unwrap();
4095 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
4096
4097 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
4098 .unwrap();
4099 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
4100}
4101
4102#[gpui::test]
4103async fn test_select_all_matches(cx: &mut gpui::TestAppContext) {
4104 init_test(cx, |_| {});
4105
4106 let mut cx = EditorTestContext::new(cx).await;
4107 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
4108
4109 cx.update_editor(|e, cx| e.select_all_matches(&SelectAllMatches, cx))
4110 .unwrap();
4111 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
4112}
4113
4114#[gpui::test]
4115async fn test_select_next_with_multiple_carets(cx: &mut gpui::TestAppContext) {
4116 init_test(cx, |_| {});
4117
4118 let mut cx = EditorTestContext::new(cx).await;
4119 cx.set_state(
4120 r#"let foo = 2;
4121lˇet foo = 2;
4122let fooˇ = 2;
4123let foo = 2;
4124let foo = ˇ2;"#,
4125 );
4126
4127 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
4128 .unwrap();
4129 cx.assert_editor_state(
4130 r#"let foo = 2;
4131«letˇ» foo = 2;
4132let «fooˇ» = 2;
4133let foo = 2;
4134let foo = «2ˇ»;"#,
4135 );
4136
4137 // noop for multiple selections with different contents
4138 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
4139 .unwrap();
4140 cx.assert_editor_state(
4141 r#"let foo = 2;
4142«letˇ» foo = 2;
4143let «fooˇ» = 2;
4144let foo = 2;
4145let foo = «2ˇ»;"#,
4146 );
4147}
4148
4149#[gpui::test]
4150async fn test_select_previous_multibuffer(cx: &mut gpui::TestAppContext) {
4151 init_test(cx, |_| {});
4152
4153 let mut cx = EditorTestContext::new_multibuffer(
4154 cx,
4155 [
4156 indoc! {
4157 "aaa\n«bbb\nccc\n»ddd"
4158 },
4159 indoc! {
4160 "aaa\n«bbb\nccc\n»ddd"
4161 },
4162 ],
4163 );
4164
4165 cx.assert_editor_state(indoc! {"
4166 ˇbbb
4167 ccc
4168
4169 bbb
4170 ccc
4171 "});
4172 cx.dispatch_action(SelectPrevious::default());
4173 cx.assert_editor_state(indoc! {"
4174 «bbbˇ»
4175 ccc
4176
4177 bbb
4178 ccc
4179 "});
4180 cx.dispatch_action(SelectPrevious::default());
4181 cx.assert_editor_state(indoc! {"
4182 «bbbˇ»
4183 ccc
4184
4185 «bbbˇ»
4186 ccc
4187 "});
4188}
4189
4190#[gpui::test]
4191async fn test_select_previous_with_single_caret(cx: &mut gpui::TestAppContext) {
4192 init_test(cx, |_| {});
4193
4194 let mut cx = EditorTestContext::new(cx).await;
4195 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
4196
4197 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4198 .unwrap();
4199 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
4200
4201 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4202 .unwrap();
4203 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
4204
4205 cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
4206 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
4207
4208 cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
4209 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
4210
4211 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4212 .unwrap();
4213 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
4214
4215 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4216 .unwrap();
4217 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
4218
4219 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4220 .unwrap();
4221 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
4222}
4223
4224#[gpui::test]
4225async fn test_select_previous_with_multiple_carets(cx: &mut gpui::TestAppContext) {
4226 init_test(cx, |_| {});
4227
4228 let mut cx = EditorTestContext::new(cx).await;
4229 cx.set_state(
4230 r#"let foo = 2;
4231lˇet foo = 2;
4232let fooˇ = 2;
4233let foo = 2;
4234let foo = ˇ2;"#,
4235 );
4236
4237 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4238 .unwrap();
4239 cx.assert_editor_state(
4240 r#"let foo = 2;
4241«letˇ» foo = 2;
4242let «fooˇ» = 2;
4243let foo = 2;
4244let foo = «2ˇ»;"#,
4245 );
4246
4247 // noop for multiple selections with different contents
4248 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4249 .unwrap();
4250 cx.assert_editor_state(
4251 r#"let foo = 2;
4252«letˇ» foo = 2;
4253let «fooˇ» = 2;
4254let foo = 2;
4255let foo = «2ˇ»;"#,
4256 );
4257}
4258
4259#[gpui::test]
4260async fn test_select_previous_with_single_selection(cx: &mut gpui::TestAppContext) {
4261 init_test(cx, |_| {});
4262
4263 let mut cx = EditorTestContext::new(cx).await;
4264 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
4265
4266 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4267 .unwrap();
4268 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
4269
4270 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4271 .unwrap();
4272 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
4273
4274 cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
4275 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
4276
4277 cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
4278 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
4279
4280 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4281 .unwrap();
4282 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
4283
4284 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4285 .unwrap();
4286 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
4287}
4288
4289#[gpui::test]
4290async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
4291 init_test(cx, |_| {});
4292
4293 let language = Arc::new(Language::new(
4294 LanguageConfig::default(),
4295 Some(tree_sitter_rust::language()),
4296 ));
4297
4298 let text = r#"
4299 use mod1::mod2::{mod3, mod4};
4300
4301 fn fn_1(param1: bool, param2: &str) {
4302 let var1 = "text";
4303 }
4304 "#
4305 .unindent();
4306
4307 let buffer = cx.new_model(|cx| {
4308 Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text)
4309 .with_language(language, cx)
4310 });
4311 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
4312 let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
4313
4314 view.condition::<crate::EditorEvent>(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
4315 .await;
4316
4317 _ = view.update(cx, |view, cx| {
4318 view.change_selections(None, cx, |s| {
4319 s.select_display_ranges([
4320 DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
4321 DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
4322 DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
4323 ]);
4324 });
4325 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
4326 });
4327 assert_eq!(
4328 view.update(cx, |view, cx| { view.selections.display_ranges(cx) }),
4329 &[
4330 DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
4331 DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
4332 DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21),
4333 ]
4334 );
4335
4336 _ = view.update(cx, |view, cx| {
4337 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
4338 });
4339 assert_eq!(
4340 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4341 &[
4342 DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
4343 DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
4344 ]
4345 );
4346
4347 _ = view.update(cx, |view, cx| {
4348 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
4349 });
4350 assert_eq!(
4351 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4352 &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
4353 );
4354
4355 // Trying to expand the selected syntax node one more time has no effect.
4356 _ = view.update(cx, |view, cx| {
4357 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
4358 });
4359 assert_eq!(
4360 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4361 &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
4362 );
4363
4364 _ = view.update(cx, |view, cx| {
4365 view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
4366 });
4367 assert_eq!(
4368 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4369 &[
4370 DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
4371 DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
4372 ]
4373 );
4374
4375 _ = view.update(cx, |view, cx| {
4376 view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
4377 });
4378 assert_eq!(
4379 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4380 &[
4381 DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
4382 DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
4383 DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21),
4384 ]
4385 );
4386
4387 _ = view.update(cx, |view, cx| {
4388 view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
4389 });
4390 assert_eq!(
4391 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4392 &[
4393 DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
4394 DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
4395 DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
4396 ]
4397 );
4398
4399 // Trying to shrink the selected syntax node one more time has no effect.
4400 _ = view.update(cx, |view, cx| {
4401 view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
4402 });
4403 assert_eq!(
4404 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4405 &[
4406 DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
4407 DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
4408 DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
4409 ]
4410 );
4411
4412 // Ensure that we keep expanding the selection if the larger selection starts or ends within
4413 // a fold.
4414 _ = view.update(cx, |view, cx| {
4415 view.fold_ranges(
4416 vec![
4417 Point::new(0, 21)..Point::new(0, 24),
4418 Point::new(3, 20)..Point::new(3, 22),
4419 ],
4420 true,
4421 cx,
4422 );
4423 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
4424 });
4425 assert_eq!(
4426 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4427 &[
4428 DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
4429 DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
4430 DisplayPoint::new(3, 4)..DisplayPoint::new(3, 23),
4431 ]
4432 );
4433}
4434
4435#[gpui::test]
4436async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
4437 init_test(cx, |_| {});
4438
4439 let language = Arc::new(
4440 Language::new(
4441 LanguageConfig {
4442 brackets: BracketPairConfig {
4443 pairs: vec![
4444 BracketPair {
4445 start: "{".to_string(),
4446 end: "}".to_string(),
4447 close: false,
4448 newline: true,
4449 },
4450 BracketPair {
4451 start: "(".to_string(),
4452 end: ")".to_string(),
4453 close: false,
4454 newline: true,
4455 },
4456 ],
4457 ..Default::default()
4458 },
4459 ..Default::default()
4460 },
4461 Some(tree_sitter_rust::language()),
4462 )
4463 .with_indents_query(
4464 r#"
4465 (_ "(" ")" @end) @indent
4466 (_ "{" "}" @end) @indent
4467 "#,
4468 )
4469 .unwrap(),
4470 );
4471
4472 let text = "fn a() {}";
4473
4474 let buffer = cx.new_model(|cx| {
4475 Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text)
4476 .with_language(language, cx)
4477 });
4478 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
4479 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
4480 editor
4481 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
4482 .await;
4483
4484 _ = editor.update(cx, |editor, cx| {
4485 editor.change_selections(None, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
4486 editor.newline(&Newline, cx);
4487 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
4488 assert_eq!(
4489 editor.selections.ranges(cx),
4490 &[
4491 Point::new(1, 4)..Point::new(1, 4),
4492 Point::new(3, 4)..Point::new(3, 4),
4493 Point::new(5, 0)..Point::new(5, 0)
4494 ]
4495 );
4496 });
4497}
4498
4499#[gpui::test]
4500async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
4501 init_test(cx, |_| {});
4502
4503 let mut cx = EditorTestContext::new(cx).await;
4504
4505 let language = Arc::new(Language::new(
4506 LanguageConfig {
4507 brackets: BracketPairConfig {
4508 pairs: vec![
4509 BracketPair {
4510 start: "{".to_string(),
4511 end: "}".to_string(),
4512 close: true,
4513 newline: true,
4514 },
4515 BracketPair {
4516 start: "(".to_string(),
4517 end: ")".to_string(),
4518 close: true,
4519 newline: true,
4520 },
4521 BracketPair {
4522 start: "/*".to_string(),
4523 end: " */".to_string(),
4524 close: true,
4525 newline: true,
4526 },
4527 BracketPair {
4528 start: "[".to_string(),
4529 end: "]".to_string(),
4530 close: false,
4531 newline: true,
4532 },
4533 BracketPair {
4534 start: "\"".to_string(),
4535 end: "\"".to_string(),
4536 close: true,
4537 newline: false,
4538 },
4539 ],
4540 ..Default::default()
4541 },
4542 autoclose_before: "})]".to_string(),
4543 ..Default::default()
4544 },
4545 Some(tree_sitter_rust::language()),
4546 ));
4547
4548 cx.language_registry().add(language.clone());
4549 cx.update_buffer(|buffer, cx| {
4550 buffer.set_language(Some(language), cx);
4551 });
4552
4553 cx.set_state(
4554 &r#"
4555 🏀ˇ
4556 εˇ
4557 ❤️ˇ
4558 "#
4559 .unindent(),
4560 );
4561
4562 // autoclose multiple nested brackets at multiple cursors
4563 cx.update_editor(|view, cx| {
4564 view.handle_input("{", cx);
4565 view.handle_input("{", cx);
4566 view.handle_input("{", cx);
4567 });
4568 cx.assert_editor_state(
4569 &"
4570 🏀{{{ˇ}}}
4571 ε{{{ˇ}}}
4572 ❤️{{{ˇ}}}
4573 "
4574 .unindent(),
4575 );
4576
4577 // insert a different closing bracket
4578 cx.update_editor(|view, cx| {
4579 view.handle_input(")", cx);
4580 });
4581 cx.assert_editor_state(
4582 &"
4583 🏀{{{)ˇ}}}
4584 ε{{{)ˇ}}}
4585 ❤️{{{)ˇ}}}
4586 "
4587 .unindent(),
4588 );
4589
4590 // skip over the auto-closed brackets when typing a closing bracket
4591 cx.update_editor(|view, cx| {
4592 view.move_right(&MoveRight, cx);
4593 view.handle_input("}", cx);
4594 view.handle_input("}", cx);
4595 view.handle_input("}", cx);
4596 });
4597 cx.assert_editor_state(
4598 &"
4599 🏀{{{)}}}}ˇ
4600 ε{{{)}}}}ˇ
4601 ❤️{{{)}}}}ˇ
4602 "
4603 .unindent(),
4604 );
4605
4606 // autoclose multi-character pairs
4607 cx.set_state(
4608 &"
4609 ˇ
4610 ˇ
4611 "
4612 .unindent(),
4613 );
4614 cx.update_editor(|view, cx| {
4615 view.handle_input("/", cx);
4616 view.handle_input("*", cx);
4617 });
4618 cx.assert_editor_state(
4619 &"
4620 /*ˇ */
4621 /*ˇ */
4622 "
4623 .unindent(),
4624 );
4625
4626 // one cursor autocloses a multi-character pair, one cursor
4627 // does not autoclose.
4628 cx.set_state(
4629 &"
4630 /ˇ
4631 ˇ
4632 "
4633 .unindent(),
4634 );
4635 cx.update_editor(|view, cx| view.handle_input("*", cx));
4636 cx.assert_editor_state(
4637 &"
4638 /*ˇ */
4639 *ˇ
4640 "
4641 .unindent(),
4642 );
4643
4644 // Don't autoclose if the next character isn't whitespace and isn't
4645 // listed in the language's "autoclose_before" section.
4646 cx.set_state("ˇa b");
4647 cx.update_editor(|view, cx| view.handle_input("{", cx));
4648 cx.assert_editor_state("{ˇa b");
4649
4650 // Don't autoclose if `close` is false for the bracket pair
4651 cx.set_state("ˇ");
4652 cx.update_editor(|view, cx| view.handle_input("[", cx));
4653 cx.assert_editor_state("[ˇ");
4654
4655 // Surround with brackets if text is selected
4656 cx.set_state("«aˇ» b");
4657 cx.update_editor(|view, cx| view.handle_input("{", cx));
4658 cx.assert_editor_state("{«aˇ»} b");
4659
4660 // Autclose pair where the start and end characters are the same
4661 cx.set_state("aˇ");
4662 cx.update_editor(|view, cx| view.handle_input("\"", cx));
4663 cx.assert_editor_state("a\"ˇ\"");
4664 cx.update_editor(|view, cx| view.handle_input("\"", cx));
4665 cx.assert_editor_state("a\"\"ˇ");
4666}
4667
4668#[gpui::test]
4669async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut gpui::TestAppContext) {
4670 init_test(cx, |settings| {
4671 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
4672 });
4673
4674 let mut cx = EditorTestContext::new(cx).await;
4675
4676 let language = Arc::new(Language::new(
4677 LanguageConfig {
4678 brackets: BracketPairConfig {
4679 pairs: vec![
4680 BracketPair {
4681 start: "{".to_string(),
4682 end: "}".to_string(),
4683 close: true,
4684 newline: true,
4685 },
4686 BracketPair {
4687 start: "(".to_string(),
4688 end: ")".to_string(),
4689 close: true,
4690 newline: true,
4691 },
4692 BracketPair {
4693 start: "[".to_string(),
4694 end: "]".to_string(),
4695 close: false,
4696 newline: true,
4697 },
4698 ],
4699 ..Default::default()
4700 },
4701 autoclose_before: "})]".to_string(),
4702 ..Default::default()
4703 },
4704 Some(tree_sitter_rust::language()),
4705 ));
4706
4707 cx.language_registry().add(language.clone());
4708 cx.update_buffer(|buffer, cx| {
4709 buffer.set_language(Some(language), cx);
4710 });
4711
4712 cx.set_state(
4713 &"
4714 ˇ
4715 ˇ
4716 ˇ
4717 "
4718 .unindent(),
4719 );
4720
4721 // ensure only matching closing brackets are skipped over
4722 cx.update_editor(|view, cx| {
4723 view.handle_input("}", cx);
4724 view.move_left(&MoveLeft, cx);
4725 view.handle_input(")", cx);
4726 view.move_left(&MoveLeft, cx);
4727 });
4728 cx.assert_editor_state(
4729 &"
4730 ˇ)}
4731 ˇ)}
4732 ˇ)}
4733 "
4734 .unindent(),
4735 );
4736
4737 // skip-over closing brackets at multiple cursors
4738 cx.update_editor(|view, cx| {
4739 view.handle_input(")", cx);
4740 view.handle_input("}", cx);
4741 });
4742 cx.assert_editor_state(
4743 &"
4744 )}ˇ
4745 )}ˇ
4746 )}ˇ
4747 "
4748 .unindent(),
4749 );
4750
4751 // ignore non-close brackets
4752 cx.update_editor(|view, cx| {
4753 view.handle_input("]", cx);
4754 view.move_left(&MoveLeft, cx);
4755 view.handle_input("]", cx);
4756 });
4757 cx.assert_editor_state(
4758 &"
4759 )}]ˇ]
4760 )}]ˇ]
4761 )}]ˇ]
4762 "
4763 .unindent(),
4764 );
4765}
4766
4767#[gpui::test]
4768async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
4769 init_test(cx, |_| {});
4770
4771 let mut cx = EditorTestContext::new(cx).await;
4772
4773 let html_language = Arc::new(
4774 Language::new(
4775 LanguageConfig {
4776 name: "HTML".into(),
4777 brackets: BracketPairConfig {
4778 pairs: vec![
4779 BracketPair {
4780 start: "<".into(),
4781 end: ">".into(),
4782 close: true,
4783 ..Default::default()
4784 },
4785 BracketPair {
4786 start: "{".into(),
4787 end: "}".into(),
4788 close: true,
4789 ..Default::default()
4790 },
4791 BracketPair {
4792 start: "(".into(),
4793 end: ")".into(),
4794 close: true,
4795 ..Default::default()
4796 },
4797 ],
4798 ..Default::default()
4799 },
4800 autoclose_before: "})]>".into(),
4801 ..Default::default()
4802 },
4803 Some(tree_sitter_html::language()),
4804 )
4805 .with_injection_query(
4806 r#"
4807 (script_element
4808 (raw_text) @content
4809 (#set! "language" "javascript"))
4810 "#,
4811 )
4812 .unwrap(),
4813 );
4814
4815 let javascript_language = Arc::new(Language::new(
4816 LanguageConfig {
4817 name: "JavaScript".into(),
4818 brackets: BracketPairConfig {
4819 pairs: vec![
4820 BracketPair {
4821 start: "/*".into(),
4822 end: " */".into(),
4823 close: true,
4824 ..Default::default()
4825 },
4826 BracketPair {
4827 start: "{".into(),
4828 end: "}".into(),
4829 close: true,
4830 ..Default::default()
4831 },
4832 BracketPair {
4833 start: "(".into(),
4834 end: ")".into(),
4835 close: true,
4836 ..Default::default()
4837 },
4838 ],
4839 ..Default::default()
4840 },
4841 autoclose_before: "})]>".into(),
4842 ..Default::default()
4843 },
4844 Some(tree_sitter_typescript::language_tsx()),
4845 ));
4846
4847 cx.language_registry().add(html_language.clone());
4848 cx.language_registry().add(javascript_language.clone());
4849
4850 cx.update_buffer(|buffer, cx| {
4851 buffer.set_language(Some(html_language), cx);
4852 });
4853
4854 cx.set_state(
4855 &r#"
4856 <body>ˇ
4857 <script>
4858 var x = 1;ˇ
4859 </script>
4860 </body>ˇ
4861 "#
4862 .unindent(),
4863 );
4864
4865 // Precondition: different languages are active at different locations.
4866 cx.update_editor(|editor, cx| {
4867 let snapshot = editor.snapshot(cx);
4868 let cursors = editor.selections.ranges::<usize>(cx);
4869 let languages = cursors
4870 .iter()
4871 .map(|c| snapshot.language_at(c.start).unwrap().name())
4872 .collect::<Vec<_>>();
4873 assert_eq!(
4874 languages,
4875 &["HTML".into(), "JavaScript".into(), "HTML".into()]
4876 );
4877 });
4878
4879 // Angle brackets autoclose in HTML, but not JavaScript.
4880 cx.update_editor(|editor, cx| {
4881 editor.handle_input("<", cx);
4882 editor.handle_input("a", cx);
4883 });
4884 cx.assert_editor_state(
4885 &r#"
4886 <body><aˇ>
4887 <script>
4888 var x = 1;<aˇ
4889 </script>
4890 </body><aˇ>
4891 "#
4892 .unindent(),
4893 );
4894
4895 // Curly braces and parens autoclose in both HTML and JavaScript.
4896 cx.update_editor(|editor, cx| {
4897 editor.handle_input(" b=", cx);
4898 editor.handle_input("{", cx);
4899 editor.handle_input("c", cx);
4900 editor.handle_input("(", cx);
4901 });
4902 cx.assert_editor_state(
4903 &r#"
4904 <body><a b={c(ˇ)}>
4905 <script>
4906 var x = 1;<a b={c(ˇ)}
4907 </script>
4908 </body><a b={c(ˇ)}>
4909 "#
4910 .unindent(),
4911 );
4912
4913 // Brackets that were already autoclosed are skipped.
4914 cx.update_editor(|editor, cx| {
4915 editor.handle_input(")", cx);
4916 editor.handle_input("d", cx);
4917 editor.handle_input("}", cx);
4918 });
4919 cx.assert_editor_state(
4920 &r#"
4921 <body><a b={c()d}ˇ>
4922 <script>
4923 var x = 1;<a b={c()d}ˇ
4924 </script>
4925 </body><a b={c()d}ˇ>
4926 "#
4927 .unindent(),
4928 );
4929 cx.update_editor(|editor, cx| {
4930 editor.handle_input(">", cx);
4931 });
4932 cx.assert_editor_state(
4933 &r#"
4934 <body><a b={c()d}>ˇ
4935 <script>
4936 var x = 1;<a b={c()d}>ˇ
4937 </script>
4938 </body><a b={c()d}>ˇ
4939 "#
4940 .unindent(),
4941 );
4942
4943 // Reset
4944 cx.set_state(
4945 &r#"
4946 <body>ˇ
4947 <script>
4948 var x = 1;ˇ
4949 </script>
4950 </body>ˇ
4951 "#
4952 .unindent(),
4953 );
4954
4955 cx.update_editor(|editor, cx| {
4956 editor.handle_input("<", cx);
4957 });
4958 cx.assert_editor_state(
4959 &r#"
4960 <body><ˇ>
4961 <script>
4962 var x = 1;<ˇ
4963 </script>
4964 </body><ˇ>
4965 "#
4966 .unindent(),
4967 );
4968
4969 // When backspacing, the closing angle brackets are removed.
4970 cx.update_editor(|editor, cx| {
4971 editor.backspace(&Backspace, cx);
4972 });
4973 cx.assert_editor_state(
4974 &r#"
4975 <body>ˇ
4976 <script>
4977 var x = 1;ˇ
4978 </script>
4979 </body>ˇ
4980 "#
4981 .unindent(),
4982 );
4983
4984 // Block comments autoclose in JavaScript, but not HTML.
4985 cx.update_editor(|editor, cx| {
4986 editor.handle_input("/", cx);
4987 editor.handle_input("*", cx);
4988 });
4989 cx.assert_editor_state(
4990 &r#"
4991 <body>/*ˇ
4992 <script>
4993 var x = 1;/*ˇ */
4994 </script>
4995 </body>/*ˇ
4996 "#
4997 .unindent(),
4998 );
4999}
5000
5001#[gpui::test]
5002async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
5003 init_test(cx, |_| {});
5004
5005 let mut cx = EditorTestContext::new(cx).await;
5006
5007 let rust_language = Arc::new(
5008 Language::new(
5009 LanguageConfig {
5010 name: "Rust".into(),
5011 brackets: serde_json::from_value(json!([
5012 { "start": "{", "end": "}", "close": true, "newline": true },
5013 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
5014 ]))
5015 .unwrap(),
5016 autoclose_before: "})]>".into(),
5017 ..Default::default()
5018 },
5019 Some(tree_sitter_rust::language()),
5020 )
5021 .with_override_query("(string_literal) @string")
5022 .unwrap(),
5023 );
5024
5025 cx.language_registry().add(rust_language.clone());
5026 cx.update_buffer(|buffer, cx| {
5027 buffer.set_language(Some(rust_language), cx);
5028 });
5029
5030 cx.set_state(
5031 &r#"
5032 let x = ˇ
5033 "#
5034 .unindent(),
5035 );
5036
5037 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
5038 cx.update_editor(|editor, cx| {
5039 editor.handle_input("\"", cx);
5040 });
5041 cx.assert_editor_state(
5042 &r#"
5043 let x = "ˇ"
5044 "#
5045 .unindent(),
5046 );
5047
5048 // Inserting another quotation mark. The cursor moves across the existing
5049 // automatically-inserted quotation mark.
5050 cx.update_editor(|editor, cx| {
5051 editor.handle_input("\"", cx);
5052 });
5053 cx.assert_editor_state(
5054 &r#"
5055 let x = ""ˇ
5056 "#
5057 .unindent(),
5058 );
5059
5060 // Reset
5061 cx.set_state(
5062 &r#"
5063 let x = ˇ
5064 "#
5065 .unindent(),
5066 );
5067
5068 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
5069 cx.update_editor(|editor, cx| {
5070 editor.handle_input("\"", cx);
5071 editor.handle_input(" ", cx);
5072 editor.move_left(&Default::default(), cx);
5073 editor.handle_input("\\", cx);
5074 editor.handle_input("\"", cx);
5075 });
5076 cx.assert_editor_state(
5077 &r#"
5078 let x = "\"ˇ "
5079 "#
5080 .unindent(),
5081 );
5082
5083 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
5084 // mark. Nothing is inserted.
5085 cx.update_editor(|editor, cx| {
5086 editor.move_right(&Default::default(), cx);
5087 editor.handle_input("\"", cx);
5088 });
5089 cx.assert_editor_state(
5090 &r#"
5091 let x = "\" "ˇ
5092 "#
5093 .unindent(),
5094 );
5095}
5096
5097#[gpui::test]
5098async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
5099 init_test(cx, |_| {});
5100
5101 let language = Arc::new(Language::new(
5102 LanguageConfig {
5103 brackets: BracketPairConfig {
5104 pairs: vec![
5105 BracketPair {
5106 start: "{".to_string(),
5107 end: "}".to_string(),
5108 close: true,
5109 newline: true,
5110 },
5111 BracketPair {
5112 start: "/* ".to_string(),
5113 end: "*/".to_string(),
5114 close: true,
5115 ..Default::default()
5116 },
5117 ],
5118 ..Default::default()
5119 },
5120 ..Default::default()
5121 },
5122 Some(tree_sitter_rust::language()),
5123 ));
5124
5125 let text = r#"
5126 a
5127 b
5128 c
5129 "#
5130 .unindent();
5131
5132 let buffer = cx.new_model(|cx| {
5133 Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text)
5134 .with_language(language, cx)
5135 });
5136 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
5137 let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
5138 view.condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
5139 .await;
5140
5141 _ = view.update(cx, |view, cx| {
5142 view.change_selections(None, cx, |s| {
5143 s.select_display_ranges([
5144 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
5145 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
5146 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1),
5147 ])
5148 });
5149
5150 view.handle_input("{", cx);
5151 view.handle_input("{", cx);
5152 view.handle_input("{", cx);
5153 assert_eq!(
5154 view.text(cx),
5155 "
5156 {{{a}}}
5157 {{{b}}}
5158 {{{c}}}
5159 "
5160 .unindent()
5161 );
5162 assert_eq!(
5163 view.selections.display_ranges(cx),
5164 [
5165 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 4),
5166 DisplayPoint::new(1, 3)..DisplayPoint::new(1, 4),
5167 DisplayPoint::new(2, 3)..DisplayPoint::new(2, 4)
5168 ]
5169 );
5170
5171 view.undo(&Undo, cx);
5172 view.undo(&Undo, cx);
5173 view.undo(&Undo, cx);
5174 assert_eq!(
5175 view.text(cx),
5176 "
5177 a
5178 b
5179 c
5180 "
5181 .unindent()
5182 );
5183 assert_eq!(
5184 view.selections.display_ranges(cx),
5185 [
5186 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
5187 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
5188 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1)
5189 ]
5190 );
5191
5192 // Ensure inserting the first character of a multi-byte bracket pair
5193 // doesn't surround the selections with the bracket.
5194 view.handle_input("/", cx);
5195 assert_eq!(
5196 view.text(cx),
5197 "
5198 /
5199 /
5200 /
5201 "
5202 .unindent()
5203 );
5204 assert_eq!(
5205 view.selections.display_ranges(cx),
5206 [
5207 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
5208 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
5209 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1)
5210 ]
5211 );
5212
5213 view.undo(&Undo, cx);
5214 assert_eq!(
5215 view.text(cx),
5216 "
5217 a
5218 b
5219 c
5220 "
5221 .unindent()
5222 );
5223 assert_eq!(
5224 view.selections.display_ranges(cx),
5225 [
5226 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
5227 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
5228 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1)
5229 ]
5230 );
5231
5232 // Ensure inserting the last character of a multi-byte bracket pair
5233 // doesn't surround the selections with the bracket.
5234 view.handle_input("*", cx);
5235 assert_eq!(
5236 view.text(cx),
5237 "
5238 *
5239 *
5240 *
5241 "
5242 .unindent()
5243 );
5244 assert_eq!(
5245 view.selections.display_ranges(cx),
5246 [
5247 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
5248 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
5249 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1)
5250 ]
5251 );
5252 });
5253}
5254
5255#[gpui::test]
5256async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
5257 init_test(cx, |_| {});
5258
5259 let language = Arc::new(Language::new(
5260 LanguageConfig {
5261 brackets: BracketPairConfig {
5262 pairs: vec![BracketPair {
5263 start: "{".to_string(),
5264 end: "}".to_string(),
5265 close: true,
5266 newline: true,
5267 }],
5268 ..Default::default()
5269 },
5270 autoclose_before: "}".to_string(),
5271 ..Default::default()
5272 },
5273 Some(tree_sitter_rust::language()),
5274 ));
5275
5276 let text = r#"
5277 a
5278 b
5279 c
5280 "#
5281 .unindent();
5282
5283 let buffer = cx.new_model(|cx| {
5284 Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text)
5285 .with_language(language, cx)
5286 });
5287 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
5288 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
5289 editor
5290 .condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
5291 .await;
5292
5293 _ = editor.update(cx, |editor, cx| {
5294 editor.change_selections(None, cx, |s| {
5295 s.select_ranges([
5296 Point::new(0, 1)..Point::new(0, 1),
5297 Point::new(1, 1)..Point::new(1, 1),
5298 Point::new(2, 1)..Point::new(2, 1),
5299 ])
5300 });
5301
5302 editor.handle_input("{", cx);
5303 editor.handle_input("{", cx);
5304 editor.handle_input("_", cx);
5305 assert_eq!(
5306 editor.text(cx),
5307 "
5308 a{{_}}
5309 b{{_}}
5310 c{{_}}
5311 "
5312 .unindent()
5313 );
5314 assert_eq!(
5315 editor.selections.ranges::<Point>(cx),
5316 [
5317 Point::new(0, 4)..Point::new(0, 4),
5318 Point::new(1, 4)..Point::new(1, 4),
5319 Point::new(2, 4)..Point::new(2, 4)
5320 ]
5321 );
5322
5323 editor.backspace(&Default::default(), cx);
5324 editor.backspace(&Default::default(), cx);
5325 assert_eq!(
5326 editor.text(cx),
5327 "
5328 a{}
5329 b{}
5330 c{}
5331 "
5332 .unindent()
5333 );
5334 assert_eq!(
5335 editor.selections.ranges::<Point>(cx),
5336 [
5337 Point::new(0, 2)..Point::new(0, 2),
5338 Point::new(1, 2)..Point::new(1, 2),
5339 Point::new(2, 2)..Point::new(2, 2)
5340 ]
5341 );
5342
5343 editor.delete_to_previous_word_start(&Default::default(), cx);
5344 assert_eq!(
5345 editor.text(cx),
5346 "
5347 a
5348 b
5349 c
5350 "
5351 .unindent()
5352 );
5353 assert_eq!(
5354 editor.selections.ranges::<Point>(cx),
5355 [
5356 Point::new(0, 1)..Point::new(0, 1),
5357 Point::new(1, 1)..Point::new(1, 1),
5358 Point::new(2, 1)..Point::new(2, 1)
5359 ]
5360 );
5361 });
5362}
5363
5364#[gpui::test]
5365async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut gpui::TestAppContext) {
5366 init_test(cx, |settings| {
5367 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
5368 });
5369
5370 let mut cx = EditorTestContext::new(cx).await;
5371
5372 let language = Arc::new(Language::new(
5373 LanguageConfig {
5374 brackets: BracketPairConfig {
5375 pairs: vec![
5376 BracketPair {
5377 start: "{".to_string(),
5378 end: "}".to_string(),
5379 close: true,
5380 newline: true,
5381 },
5382 BracketPair {
5383 start: "(".to_string(),
5384 end: ")".to_string(),
5385 close: true,
5386 newline: true,
5387 },
5388 BracketPair {
5389 start: "[".to_string(),
5390 end: "]".to_string(),
5391 close: false,
5392 newline: true,
5393 },
5394 ],
5395 ..Default::default()
5396 },
5397 autoclose_before: "})]".to_string(),
5398 ..Default::default()
5399 },
5400 Some(tree_sitter_rust::language()),
5401 ));
5402
5403 cx.language_registry().add(language.clone());
5404 cx.update_buffer(|buffer, cx| {
5405 buffer.set_language(Some(language), cx);
5406 });
5407
5408 cx.set_state(
5409 &"
5410 {(ˇ)}
5411 [[ˇ]]
5412 {(ˇ)}
5413 "
5414 .unindent(),
5415 );
5416
5417 cx.update_editor(|view, cx| {
5418 view.backspace(&Default::default(), cx);
5419 view.backspace(&Default::default(), cx);
5420 });
5421
5422 cx.assert_editor_state(
5423 &"
5424 ˇ
5425 ˇ]]
5426 ˇ
5427 "
5428 .unindent(),
5429 );
5430
5431 cx.update_editor(|view, cx| {
5432 view.handle_input("{", cx);
5433 view.handle_input("{", cx);
5434 view.move_right(&MoveRight, cx);
5435 view.move_right(&MoveRight, cx);
5436 view.move_left(&MoveLeft, cx);
5437 view.move_left(&MoveLeft, cx);
5438 view.backspace(&Default::default(), cx);
5439 });
5440
5441 cx.assert_editor_state(
5442 &"
5443 {ˇ}
5444 {ˇ}]]
5445 {ˇ}
5446 "
5447 .unindent(),
5448 );
5449
5450 cx.update_editor(|view, cx| {
5451 view.backspace(&Default::default(), cx);
5452 });
5453
5454 cx.assert_editor_state(
5455 &"
5456 ˇ
5457 ˇ]]
5458 ˇ
5459 "
5460 .unindent(),
5461 );
5462}
5463
5464#[gpui::test]
5465async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) {
5466 init_test(cx, |_| {});
5467
5468 let language = Arc::new(Language::new(
5469 LanguageConfig::default(),
5470 Some(tree_sitter_rust::language()),
5471 ));
5472
5473 let buffer = cx.new_model(|cx| {
5474 Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), "")
5475 .with_language(language, cx)
5476 });
5477 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
5478 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
5479 editor
5480 .condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
5481 .await;
5482
5483 _ = editor.update(cx, |editor, cx| {
5484 editor.set_auto_replace_emoji_shortcode(true);
5485
5486 editor.handle_input("Hello ", cx);
5487 editor.handle_input(":wave", cx);
5488 assert_eq!(editor.text(cx), "Hello :wave".unindent());
5489
5490 editor.handle_input(":", cx);
5491 assert_eq!(editor.text(cx), "Hello 👋".unindent());
5492
5493 editor.handle_input(" :smile", cx);
5494 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
5495
5496 editor.handle_input(":", cx);
5497 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
5498
5499 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
5500 editor.handle_input(":wave", cx);
5501 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
5502
5503 editor.handle_input(":", cx);
5504 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
5505
5506 editor.handle_input(":1", cx);
5507 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
5508
5509 editor.handle_input(":", cx);
5510 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
5511
5512 // Ensure shortcode does not get replaced when it is part of a word
5513 editor.handle_input(" Test:wave", cx);
5514 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
5515
5516 editor.handle_input(":", cx);
5517 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
5518
5519 editor.set_auto_replace_emoji_shortcode(false);
5520
5521 // Ensure shortcode does not get replaced when auto replace is off
5522 editor.handle_input(" :wave", cx);
5523 assert_eq!(
5524 editor.text(cx),
5525 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
5526 );
5527
5528 editor.handle_input(":", cx);
5529 assert_eq!(
5530 editor.text(cx),
5531 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
5532 );
5533 });
5534}
5535
5536#[gpui::test]
5537async fn test_snippets(cx: &mut gpui::TestAppContext) {
5538 init_test(cx, |_| {});
5539
5540 let (text, insertion_ranges) = marked_text_ranges(
5541 indoc! {"
5542 a.ˇ b
5543 a.ˇ b
5544 a.ˇ b
5545 "},
5546 false,
5547 );
5548
5549 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
5550 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
5551
5552 _ = editor.update(cx, |editor, cx| {
5553 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
5554
5555 editor
5556 .insert_snippet(&insertion_ranges, snippet, cx)
5557 .unwrap();
5558
5559 fn assert(editor: &mut Editor, cx: &mut ViewContext<Editor>, marked_text: &str) {
5560 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
5561 assert_eq!(editor.text(cx), expected_text);
5562 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
5563 }
5564
5565 assert(
5566 editor,
5567 cx,
5568 indoc! {"
5569 a.f(«one», two, «three») b
5570 a.f(«one», two, «three») b
5571 a.f(«one», two, «three») b
5572 "},
5573 );
5574
5575 // Can't move earlier than the first tab stop
5576 assert!(!editor.move_to_prev_snippet_tabstop(cx));
5577 assert(
5578 editor,
5579 cx,
5580 indoc! {"
5581 a.f(«one», two, «three») b
5582 a.f(«one», two, «three») b
5583 a.f(«one», two, «three») b
5584 "},
5585 );
5586
5587 assert!(editor.move_to_next_snippet_tabstop(cx));
5588 assert(
5589 editor,
5590 cx,
5591 indoc! {"
5592 a.f(one, «two», three) b
5593 a.f(one, «two», three) b
5594 a.f(one, «two», three) b
5595 "},
5596 );
5597
5598 editor.move_to_prev_snippet_tabstop(cx);
5599 assert(
5600 editor,
5601 cx,
5602 indoc! {"
5603 a.f(«one», two, «three») b
5604 a.f(«one», two, «three») b
5605 a.f(«one», two, «three») b
5606 "},
5607 );
5608
5609 assert!(editor.move_to_next_snippet_tabstop(cx));
5610 assert(
5611 editor,
5612 cx,
5613 indoc! {"
5614 a.f(one, «two», three) b
5615 a.f(one, «two», three) b
5616 a.f(one, «two», three) b
5617 "},
5618 );
5619 assert!(editor.move_to_next_snippet_tabstop(cx));
5620 assert(
5621 editor,
5622 cx,
5623 indoc! {"
5624 a.f(one, two, three)ˇ b
5625 a.f(one, two, three)ˇ b
5626 a.f(one, two, three)ˇ b
5627 "},
5628 );
5629
5630 // As soon as the last tab stop is reached, snippet state is gone
5631 editor.move_to_prev_snippet_tabstop(cx);
5632 assert(
5633 editor,
5634 cx,
5635 indoc! {"
5636 a.f(one, two, three)ˇ b
5637 a.f(one, two, three)ˇ b
5638 a.f(one, two, three)ˇ b
5639 "},
5640 );
5641 });
5642}
5643
5644#[gpui::test]
5645async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
5646 init_test(cx, |_| {});
5647
5648 let fs = FakeFs::new(cx.executor());
5649 fs.insert_file("/file.rs", Default::default()).await;
5650
5651 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
5652
5653 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
5654 language_registry.add(rust_lang());
5655 let mut fake_servers = language_registry.register_fake_lsp_adapter(
5656 "Rust",
5657 FakeLspAdapter {
5658 capabilities: lsp::ServerCapabilities {
5659 document_formatting_provider: Some(lsp::OneOf::Left(true)),
5660 ..Default::default()
5661 },
5662 ..Default::default()
5663 },
5664 );
5665
5666 let buffer = project
5667 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
5668 .await
5669 .unwrap();
5670
5671 cx.executor().start_waiting();
5672 let fake_server = fake_servers.next().await.unwrap();
5673
5674 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
5675 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
5676 editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
5677 assert!(cx.read(|cx| editor.is_dirty(cx)));
5678
5679 let save = editor
5680 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
5681 .unwrap();
5682 fake_server
5683 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
5684 assert_eq!(
5685 params.text_document.uri,
5686 lsp::Url::from_file_path("/file.rs").unwrap()
5687 );
5688 assert_eq!(params.options.tab_size, 4);
5689 Ok(Some(vec![lsp::TextEdit::new(
5690 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
5691 ", ".to_string(),
5692 )]))
5693 })
5694 .next()
5695 .await;
5696 cx.executor().start_waiting();
5697 save.await;
5698
5699 assert_eq!(
5700 editor.update(cx, |editor, cx| editor.text(cx)),
5701 "one, two\nthree\n"
5702 );
5703 assert!(!cx.read(|cx| editor.is_dirty(cx)));
5704
5705 editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
5706 assert!(cx.read(|cx| editor.is_dirty(cx)));
5707
5708 // Ensure we can still save even if formatting hangs.
5709 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
5710 assert_eq!(
5711 params.text_document.uri,
5712 lsp::Url::from_file_path("/file.rs").unwrap()
5713 );
5714 futures::future::pending::<()>().await;
5715 unreachable!()
5716 });
5717 let save = editor
5718 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
5719 .unwrap();
5720 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
5721 cx.executor().start_waiting();
5722 save.await;
5723 assert_eq!(
5724 editor.update(cx, |editor, cx| editor.text(cx)),
5725 "one\ntwo\nthree\n"
5726 );
5727 assert!(!cx.read(|cx| editor.is_dirty(cx)));
5728
5729 // For non-dirty buffer, no formatting request should be sent
5730 let save = editor
5731 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
5732 .unwrap();
5733 let _pending_format_request = fake_server
5734 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
5735 panic!("Should not be invoked on non-dirty buffer");
5736 })
5737 .next();
5738 cx.executor().start_waiting();
5739 save.await;
5740
5741 // Set rust language override and assert overridden tabsize is sent to language server
5742 update_test_language_settings(cx, |settings| {
5743 settings.languages.insert(
5744 "Rust".into(),
5745 LanguageSettingsContent {
5746 tab_size: NonZeroU32::new(8),
5747 ..Default::default()
5748 },
5749 );
5750 });
5751
5752 editor.update(cx, |editor, cx| editor.set_text("somehting_new\n", cx));
5753 assert!(cx.read(|cx| editor.is_dirty(cx)));
5754 let save = editor
5755 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
5756 .unwrap();
5757 fake_server
5758 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
5759 assert_eq!(
5760 params.text_document.uri,
5761 lsp::Url::from_file_path("/file.rs").unwrap()
5762 );
5763 assert_eq!(params.options.tab_size, 8);
5764 Ok(Some(vec![]))
5765 })
5766 .next()
5767 .await;
5768 cx.executor().start_waiting();
5769 save.await;
5770}
5771
5772#[gpui::test]
5773async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) {
5774 init_test(cx, |_| {});
5775
5776 let cols = 4;
5777 let rows = 10;
5778 let sample_text_1 = sample_text(rows, cols, 'a');
5779 assert_eq!(
5780 sample_text_1,
5781 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
5782 );
5783 let sample_text_2 = sample_text(rows, cols, 'l');
5784 assert_eq!(
5785 sample_text_2,
5786 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
5787 );
5788 let sample_text_3 = sample_text(rows, cols, 'v');
5789 assert_eq!(
5790 sample_text_3,
5791 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
5792 );
5793
5794 let fs = FakeFs::new(cx.executor());
5795 fs.insert_tree(
5796 "/a",
5797 json!({
5798 "main.rs": sample_text_1,
5799 "other.rs": sample_text_2,
5800 "lib.rs": sample_text_3,
5801 }),
5802 )
5803 .await;
5804
5805 let project = Project::test(fs, ["/a".as_ref()], cx).await;
5806 let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
5807 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
5808
5809 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
5810 language_registry.add(rust_lang());
5811 let mut fake_servers = language_registry.register_fake_lsp_adapter(
5812 "Rust",
5813 FakeLspAdapter {
5814 capabilities: lsp::ServerCapabilities {
5815 document_formatting_provider: Some(lsp::OneOf::Left(true)),
5816 ..Default::default()
5817 },
5818 ..Default::default()
5819 },
5820 );
5821
5822 let worktree = project.update(cx, |project, _| {
5823 let mut worktrees = project.worktrees().collect::<Vec<_>>();
5824 assert_eq!(worktrees.len(), 1);
5825 worktrees.pop().unwrap()
5826 });
5827 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
5828
5829 let buffer_1 = project
5830 .update(cx, |project, cx| {
5831 project.open_buffer((worktree_id, "main.rs"), cx)
5832 })
5833 .await
5834 .unwrap();
5835 let buffer_2 = project
5836 .update(cx, |project, cx| {
5837 project.open_buffer((worktree_id, "other.rs"), cx)
5838 })
5839 .await
5840 .unwrap();
5841 let buffer_3 = project
5842 .update(cx, |project, cx| {
5843 project.open_buffer((worktree_id, "lib.rs"), cx)
5844 })
5845 .await
5846 .unwrap();
5847
5848 let multi_buffer = cx.new_model(|cx| {
5849 let mut multi_buffer = MultiBuffer::new(0, ReadWrite);
5850 multi_buffer.push_excerpts(
5851 buffer_1.clone(),
5852 [
5853 ExcerptRange {
5854 context: Point::new(0, 0)..Point::new(3, 0),
5855 primary: None,
5856 },
5857 ExcerptRange {
5858 context: Point::new(5, 0)..Point::new(7, 0),
5859 primary: None,
5860 },
5861 ExcerptRange {
5862 context: Point::new(9, 0)..Point::new(10, 4),
5863 primary: None,
5864 },
5865 ],
5866 cx,
5867 );
5868 multi_buffer.push_excerpts(
5869 buffer_2.clone(),
5870 [
5871 ExcerptRange {
5872 context: Point::new(0, 0)..Point::new(3, 0),
5873 primary: None,
5874 },
5875 ExcerptRange {
5876 context: Point::new(5, 0)..Point::new(7, 0),
5877 primary: None,
5878 },
5879 ExcerptRange {
5880 context: Point::new(9, 0)..Point::new(10, 4),
5881 primary: None,
5882 },
5883 ],
5884 cx,
5885 );
5886 multi_buffer.push_excerpts(
5887 buffer_3.clone(),
5888 [
5889 ExcerptRange {
5890 context: Point::new(0, 0)..Point::new(3, 0),
5891 primary: None,
5892 },
5893 ExcerptRange {
5894 context: Point::new(5, 0)..Point::new(7, 0),
5895 primary: None,
5896 },
5897 ExcerptRange {
5898 context: Point::new(9, 0)..Point::new(10, 4),
5899 primary: None,
5900 },
5901 ],
5902 cx,
5903 );
5904 multi_buffer
5905 });
5906 let multi_buffer_editor =
5907 cx.new_view(|cx| Editor::new(EditorMode::Full, multi_buffer, Some(project.clone()), cx));
5908
5909 multi_buffer_editor.update(cx, |editor, cx| {
5910 editor.change_selections(Some(Autoscroll::Next), cx, |s| s.select_ranges(Some(1..2)));
5911 editor.insert("|one|two|three|", cx);
5912 });
5913 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
5914 multi_buffer_editor.update(cx, |editor, cx| {
5915 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
5916 s.select_ranges(Some(60..70))
5917 });
5918 editor.insert("|four|five|six|", cx);
5919 });
5920 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
5921
5922 // First two buffers should be edited, but not the third one.
5923 assert_eq!(
5924 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
5925 "a|one|two|three|aa\nbbbb\ncccc\n\nffff\ngggg\n\njjjj\nllll\nmmmm\nnnnn|four|five|six|\nr\n\nuuuu\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}",
5926 );
5927 buffer_1.update(cx, |buffer, _| {
5928 assert!(buffer.is_dirty());
5929 assert_eq!(
5930 buffer.text(),
5931 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
5932 )
5933 });
5934 buffer_2.update(cx, |buffer, _| {
5935 assert!(buffer.is_dirty());
5936 assert_eq!(
5937 buffer.text(),
5938 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
5939 )
5940 });
5941 buffer_3.update(cx, |buffer, _| {
5942 assert!(!buffer.is_dirty());
5943 assert_eq!(buffer.text(), sample_text_3,)
5944 });
5945
5946 cx.executor().start_waiting();
5947 let save = multi_buffer_editor
5948 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
5949 .unwrap();
5950
5951 let fake_server = fake_servers.next().await.unwrap();
5952 fake_server
5953 .server
5954 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
5955 Ok(Some(vec![lsp::TextEdit::new(
5956 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
5957 format!("[{} formatted]", params.text_document.uri),
5958 )]))
5959 })
5960 .detach();
5961 save.await;
5962
5963 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
5964 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
5965 assert_eq!(
5966 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
5967 "a|o[file:///a/main.rs formatted]bbbb\ncccc\n\nffff\ngggg\n\njjjj\n\nlll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|\nr\n\nuuuu\n\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}",
5968 );
5969 buffer_1.update(cx, |buffer, _| {
5970 assert!(!buffer.is_dirty());
5971 assert_eq!(
5972 buffer.text(),
5973 "a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n",
5974 )
5975 });
5976 buffer_2.update(cx, |buffer, _| {
5977 assert!(!buffer.is_dirty());
5978 assert_eq!(
5979 buffer.text(),
5980 "lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n",
5981 )
5982 });
5983 buffer_3.update(cx, |buffer, _| {
5984 assert!(!buffer.is_dirty());
5985 assert_eq!(buffer.text(), sample_text_3,)
5986 });
5987}
5988
5989#[gpui::test]
5990async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
5991 init_test(cx, |_| {});
5992
5993 let fs = FakeFs::new(cx.executor());
5994 fs.insert_file("/file.rs", Default::default()).await;
5995
5996 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
5997
5998 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
5999 language_registry.add(rust_lang());
6000 let mut fake_servers = language_registry.register_fake_lsp_adapter(
6001 "Rust",
6002 FakeLspAdapter {
6003 capabilities: lsp::ServerCapabilities {
6004 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
6005 ..Default::default()
6006 },
6007 ..Default::default()
6008 },
6009 );
6010
6011 let buffer = project
6012 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
6013 .await
6014 .unwrap();
6015
6016 cx.executor().start_waiting();
6017 let fake_server = fake_servers.next().await.unwrap();
6018
6019 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
6020 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
6021 editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
6022 assert!(cx.read(|cx| editor.is_dirty(cx)));
6023
6024 let save = editor
6025 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
6026 .unwrap();
6027 fake_server
6028 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
6029 assert_eq!(
6030 params.text_document.uri,
6031 lsp::Url::from_file_path("/file.rs").unwrap()
6032 );
6033 assert_eq!(params.options.tab_size, 4);
6034 Ok(Some(vec![lsp::TextEdit::new(
6035 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
6036 ", ".to_string(),
6037 )]))
6038 })
6039 .next()
6040 .await;
6041 cx.executor().start_waiting();
6042 save.await;
6043 assert_eq!(
6044 editor.update(cx, |editor, cx| editor.text(cx)),
6045 "one, two\nthree\n"
6046 );
6047 assert!(!cx.read(|cx| editor.is_dirty(cx)));
6048
6049 editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
6050 assert!(cx.read(|cx| editor.is_dirty(cx)));
6051
6052 // Ensure we can still save even if formatting hangs.
6053 fake_server.handle_request::<lsp::request::RangeFormatting, _, _>(
6054 move |params, _| async move {
6055 assert_eq!(
6056 params.text_document.uri,
6057 lsp::Url::from_file_path("/file.rs").unwrap()
6058 );
6059 futures::future::pending::<()>().await;
6060 unreachable!()
6061 },
6062 );
6063 let save = editor
6064 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
6065 .unwrap();
6066 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
6067 cx.executor().start_waiting();
6068 save.await;
6069 assert_eq!(
6070 editor.update(cx, |editor, cx| editor.text(cx)),
6071 "one\ntwo\nthree\n"
6072 );
6073 assert!(!cx.read(|cx| editor.is_dirty(cx)));
6074
6075 // For non-dirty buffer, no formatting request should be sent
6076 let save = editor
6077 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
6078 .unwrap();
6079 let _pending_format_request = fake_server
6080 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
6081 panic!("Should not be invoked on non-dirty buffer");
6082 })
6083 .next();
6084 cx.executor().start_waiting();
6085 save.await;
6086
6087 // Set Rust language override and assert overridden tabsize is sent to language server
6088 update_test_language_settings(cx, |settings| {
6089 settings.languages.insert(
6090 "Rust".into(),
6091 LanguageSettingsContent {
6092 tab_size: NonZeroU32::new(8),
6093 ..Default::default()
6094 },
6095 );
6096 });
6097
6098 editor.update(cx, |editor, cx| editor.set_text("somehting_new\n", cx));
6099 assert!(cx.read(|cx| editor.is_dirty(cx)));
6100 let save = editor
6101 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
6102 .unwrap();
6103 fake_server
6104 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
6105 assert_eq!(
6106 params.text_document.uri,
6107 lsp::Url::from_file_path("/file.rs").unwrap()
6108 );
6109 assert_eq!(params.options.tab_size, 8);
6110 Ok(Some(vec![]))
6111 })
6112 .next()
6113 .await;
6114 cx.executor().start_waiting();
6115 save.await;
6116}
6117
6118#[gpui::test]
6119async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
6120 init_test(cx, |settings| {
6121 settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer)
6122 });
6123
6124 let fs = FakeFs::new(cx.executor());
6125 fs.insert_file("/file.rs", Default::default()).await;
6126
6127 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
6128
6129 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
6130 language_registry.add(Arc::new(Language::new(
6131 LanguageConfig {
6132 name: "Rust".into(),
6133 matcher: LanguageMatcher {
6134 path_suffixes: vec!["rs".to_string()],
6135 ..Default::default()
6136 },
6137 // Enable Prettier formatting for the same buffer, and ensure
6138 // LSP is called instead of Prettier.
6139 prettier_parser_name: Some("test_parser".to_string()),
6140 ..Default::default()
6141 },
6142 Some(tree_sitter_rust::language()),
6143 )));
6144 let mut fake_servers = language_registry.register_fake_lsp_adapter(
6145 "Rust",
6146 FakeLspAdapter {
6147 capabilities: lsp::ServerCapabilities {
6148 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6149 ..Default::default()
6150 },
6151 ..Default::default()
6152 },
6153 );
6154
6155 let buffer = project
6156 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
6157 .await
6158 .unwrap();
6159
6160 cx.executor().start_waiting();
6161 let fake_server = fake_servers.next().await.unwrap();
6162
6163 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
6164 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
6165 _ = editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
6166
6167 let format = editor
6168 .update(cx, |editor, cx| {
6169 editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
6170 })
6171 .unwrap();
6172 fake_server
6173 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
6174 assert_eq!(
6175 params.text_document.uri,
6176 lsp::Url::from_file_path("/file.rs").unwrap()
6177 );
6178 assert_eq!(params.options.tab_size, 4);
6179 Ok(Some(vec![lsp::TextEdit::new(
6180 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
6181 ", ".to_string(),
6182 )]))
6183 })
6184 .next()
6185 .await;
6186 cx.executor().start_waiting();
6187 format.await;
6188 assert_eq!(
6189 editor.update(cx, |editor, cx| editor.text(cx)),
6190 "one, two\nthree\n"
6191 );
6192
6193 _ = editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
6194 // Ensure we don't lock if formatting hangs.
6195 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
6196 assert_eq!(
6197 params.text_document.uri,
6198 lsp::Url::from_file_path("/file.rs").unwrap()
6199 );
6200 futures::future::pending::<()>().await;
6201 unreachable!()
6202 });
6203 let format = editor
6204 .update(cx, |editor, cx| {
6205 editor.perform_format(project, FormatTrigger::Manual, cx)
6206 })
6207 .unwrap();
6208 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
6209 cx.executor().start_waiting();
6210 format.await;
6211 assert_eq!(
6212 editor.update(cx, |editor, cx| editor.text(cx)),
6213 "one\ntwo\nthree\n"
6214 );
6215}
6216
6217#[gpui::test]
6218async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
6219 init_test(cx, |_| {});
6220
6221 let mut cx = EditorLspTestContext::new_rust(
6222 lsp::ServerCapabilities {
6223 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6224 ..Default::default()
6225 },
6226 cx,
6227 )
6228 .await;
6229
6230 cx.set_state(indoc! {"
6231 one.twoˇ
6232 "});
6233
6234 // The format request takes a long time. When it completes, it inserts
6235 // a newline and an indent before the `.`
6236 cx.lsp
6237 .handle_request::<lsp::request::Formatting, _, _>(move |_, cx| {
6238 let executor = cx.background_executor().clone();
6239 async move {
6240 executor.timer(Duration::from_millis(100)).await;
6241 Ok(Some(vec![lsp::TextEdit {
6242 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
6243 new_text: "\n ".into(),
6244 }]))
6245 }
6246 });
6247
6248 // Submit a format request.
6249 let format_1 = cx
6250 .update_editor(|editor, cx| editor.format(&Format, cx))
6251 .unwrap();
6252 cx.executor().run_until_parked();
6253
6254 // Submit a second format request.
6255 let format_2 = cx
6256 .update_editor(|editor, cx| editor.format(&Format, cx))
6257 .unwrap();
6258 cx.executor().run_until_parked();
6259
6260 // Wait for both format requests to complete
6261 cx.executor().advance_clock(Duration::from_millis(200));
6262 cx.executor().start_waiting();
6263 format_1.await.unwrap();
6264 cx.executor().start_waiting();
6265 format_2.await.unwrap();
6266
6267 // The formatting edits only happens once.
6268 cx.assert_editor_state(indoc! {"
6269 one
6270 .twoˇ
6271 "});
6272}
6273
6274#[gpui::test]
6275async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
6276 init_test(cx, |settings| {
6277 settings.defaults.formatter = Some(language_settings::Formatter::Auto)
6278 });
6279
6280 let mut cx = EditorLspTestContext::new_rust(
6281 lsp::ServerCapabilities {
6282 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6283 ..Default::default()
6284 },
6285 cx,
6286 )
6287 .await;
6288
6289 // Set up a buffer white some trailing whitespace and no trailing newline.
6290 cx.set_state(
6291 &[
6292 "one ", //
6293 "twoˇ", //
6294 "three ", //
6295 "four", //
6296 ]
6297 .join("\n"),
6298 );
6299
6300 // Submit a format request.
6301 let format = cx
6302 .update_editor(|editor, cx| editor.format(&Format, cx))
6303 .unwrap();
6304
6305 // Record which buffer changes have been sent to the language server
6306 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
6307 cx.lsp
6308 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
6309 let buffer_changes = buffer_changes.clone();
6310 move |params, _| {
6311 buffer_changes.lock().extend(
6312 params
6313 .content_changes
6314 .into_iter()
6315 .map(|e| (e.range.unwrap(), e.text)),
6316 );
6317 }
6318 });
6319
6320 // Handle formatting requests to the language server.
6321 cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
6322 let buffer_changes = buffer_changes.clone();
6323 move |_, _| {
6324 // When formatting is requested, trailing whitespace has already been stripped,
6325 // and the trailing newline has already been added.
6326 assert_eq!(
6327 &buffer_changes.lock()[1..],
6328 &[
6329 (
6330 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
6331 "".into()
6332 ),
6333 (
6334 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
6335 "".into()
6336 ),
6337 (
6338 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
6339 "\n".into()
6340 ),
6341 ]
6342 );
6343
6344 // Insert blank lines between each line of the buffer.
6345 async move {
6346 Ok(Some(vec![
6347 lsp::TextEdit {
6348 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
6349 new_text: "\n".into(),
6350 },
6351 lsp::TextEdit {
6352 range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
6353 new_text: "\n".into(),
6354 },
6355 ]))
6356 }
6357 }
6358 });
6359
6360 // After formatting the buffer, the trailing whitespace is stripped,
6361 // a newline is appended, and the edits provided by the language server
6362 // have been applied.
6363 format.await.unwrap();
6364 cx.assert_editor_state(
6365 &[
6366 "one", //
6367 "", //
6368 "twoˇ", //
6369 "", //
6370 "three", //
6371 "four", //
6372 "", //
6373 ]
6374 .join("\n"),
6375 );
6376
6377 // Undoing the formatting undoes the trailing whitespace removal, the
6378 // trailing newline, and the LSP edits.
6379 cx.update_buffer(|buffer, cx| buffer.undo(cx));
6380 cx.assert_editor_state(
6381 &[
6382 "one ", //
6383 "twoˇ", //
6384 "three ", //
6385 "four", //
6386 ]
6387 .join("\n"),
6388 );
6389}
6390
6391#[gpui::test]
6392async fn test_completion(cx: &mut gpui::TestAppContext) {
6393 init_test(cx, |_| {});
6394
6395 let mut cx = EditorLspTestContext::new_rust(
6396 lsp::ServerCapabilities {
6397 completion_provider: Some(lsp::CompletionOptions {
6398 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
6399 resolve_provider: Some(true),
6400 ..Default::default()
6401 }),
6402 ..Default::default()
6403 },
6404 cx,
6405 )
6406 .await;
6407
6408 cx.set_state(indoc! {"
6409 oneˇ
6410 two
6411 three
6412 "});
6413 cx.simulate_keystroke(".");
6414 handle_completion_request(
6415 &mut cx,
6416 indoc! {"
6417 one.|<>
6418 two
6419 three
6420 "},
6421 vec!["first_completion", "second_completion"],
6422 )
6423 .await;
6424 cx.condition(|editor, _| editor.context_menu_visible())
6425 .await;
6426 let apply_additional_edits = cx.update_editor(|editor, cx| {
6427 editor.context_menu_next(&Default::default(), cx);
6428 editor
6429 .confirm_completion(&ConfirmCompletion::default(), cx)
6430 .unwrap()
6431 });
6432 cx.assert_editor_state(indoc! {"
6433 one.second_completionˇ
6434 two
6435 three
6436 "});
6437
6438 handle_resolve_completion_request(
6439 &mut cx,
6440 Some(vec![
6441 (
6442 //This overlaps with the primary completion edit which is
6443 //misbehavior from the LSP spec, test that we filter it out
6444 indoc! {"
6445 one.second_ˇcompletion
6446 two
6447 threeˇ
6448 "},
6449 "overlapping additional edit",
6450 ),
6451 (
6452 indoc! {"
6453 one.second_completion
6454 two
6455 threeˇ
6456 "},
6457 "\nadditional edit",
6458 ),
6459 ]),
6460 )
6461 .await;
6462 apply_additional_edits.await.unwrap();
6463 cx.assert_editor_state(indoc! {"
6464 one.second_completionˇ
6465 two
6466 three
6467 additional edit
6468 "});
6469
6470 cx.set_state(indoc! {"
6471 one.second_completion
6472 twoˇ
6473 threeˇ
6474 additional edit
6475 "});
6476 cx.simulate_keystroke(" ");
6477 assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
6478 cx.simulate_keystroke("s");
6479 assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
6480
6481 cx.assert_editor_state(indoc! {"
6482 one.second_completion
6483 two sˇ
6484 three sˇ
6485 additional edit
6486 "});
6487 handle_completion_request(
6488 &mut cx,
6489 indoc! {"
6490 one.second_completion
6491 two s
6492 three <s|>
6493 additional edit
6494 "},
6495 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
6496 )
6497 .await;
6498 cx.condition(|editor, _| editor.context_menu_visible())
6499 .await;
6500
6501 cx.simulate_keystroke("i");
6502
6503 handle_completion_request(
6504 &mut cx,
6505 indoc! {"
6506 one.second_completion
6507 two si
6508 three <si|>
6509 additional edit
6510 "},
6511 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
6512 )
6513 .await;
6514 cx.condition(|editor, _| editor.context_menu_visible())
6515 .await;
6516
6517 let apply_additional_edits = cx.update_editor(|editor, cx| {
6518 editor
6519 .confirm_completion(&ConfirmCompletion::default(), cx)
6520 .unwrap()
6521 });
6522 cx.assert_editor_state(indoc! {"
6523 one.second_completion
6524 two sixth_completionˇ
6525 three sixth_completionˇ
6526 additional edit
6527 "});
6528
6529 handle_resolve_completion_request(&mut cx, None).await;
6530 apply_additional_edits.await.unwrap();
6531
6532 _ = cx.update(|cx| {
6533 cx.update_global::<SettingsStore, _>(|settings, cx| {
6534 settings.update_user_settings::<EditorSettings>(cx, |settings| {
6535 settings.show_completions_on_input = Some(false);
6536 });
6537 })
6538 });
6539 cx.set_state("editorˇ");
6540 cx.simulate_keystroke(".");
6541 assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
6542 cx.simulate_keystroke("c");
6543 cx.simulate_keystroke("l");
6544 cx.simulate_keystroke("o");
6545 cx.assert_editor_state("editor.cloˇ");
6546 assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
6547 cx.update_editor(|editor, cx| {
6548 editor.show_completions(&ShowCompletions, cx);
6549 });
6550 handle_completion_request(&mut cx, "editor.<clo|>", vec!["close", "clobber"]).await;
6551 cx.condition(|editor, _| editor.context_menu_visible())
6552 .await;
6553 let apply_additional_edits = cx.update_editor(|editor, cx| {
6554 editor
6555 .confirm_completion(&ConfirmCompletion::default(), cx)
6556 .unwrap()
6557 });
6558 cx.assert_editor_state("editor.closeˇ");
6559 handle_resolve_completion_request(&mut cx, None).await;
6560 apply_additional_edits.await.unwrap();
6561}
6562
6563#[gpui::test]
6564async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
6565 init_test(cx, |_| {});
6566 let mut cx = EditorTestContext::new(cx).await;
6567 let language = Arc::new(Language::new(
6568 LanguageConfig {
6569 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
6570 ..Default::default()
6571 },
6572 Some(tree_sitter_rust::language()),
6573 ));
6574 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6575
6576 // If multiple selections intersect a line, the line is only toggled once.
6577 cx.set_state(indoc! {"
6578 fn a() {
6579 «//b();
6580 ˇ»// «c();
6581 //ˇ» d();
6582 }
6583 "});
6584
6585 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
6586
6587 cx.assert_editor_state(indoc! {"
6588 fn a() {
6589 «b();
6590 c();
6591 ˇ» d();
6592 }
6593 "});
6594
6595 // The comment prefix is inserted at the same column for every line in a
6596 // selection.
6597 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
6598
6599 cx.assert_editor_state(indoc! {"
6600 fn a() {
6601 // «b();
6602 // c();
6603 ˇ»// d();
6604 }
6605 "});
6606
6607 // If a selection ends at the beginning of a line, that line is not toggled.
6608 cx.set_selections_state(indoc! {"
6609 fn a() {
6610 // b();
6611 «// c();
6612 ˇ» // d();
6613 }
6614 "});
6615
6616 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
6617
6618 cx.assert_editor_state(indoc! {"
6619 fn a() {
6620 // b();
6621 «c();
6622 ˇ» // d();
6623 }
6624 "});
6625
6626 // If a selection span a single line and is empty, the line is toggled.
6627 cx.set_state(indoc! {"
6628 fn a() {
6629 a();
6630 b();
6631 ˇ
6632 }
6633 "});
6634
6635 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
6636
6637 cx.assert_editor_state(indoc! {"
6638 fn a() {
6639 a();
6640 b();
6641 //•ˇ
6642 }
6643 "});
6644
6645 // If a selection span multiple lines, empty lines are not toggled.
6646 cx.set_state(indoc! {"
6647 fn a() {
6648 «a();
6649
6650 c();ˇ»
6651 }
6652 "});
6653
6654 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
6655
6656 cx.assert_editor_state(indoc! {"
6657 fn a() {
6658 // «a();
6659
6660 // c();ˇ»
6661 }
6662 "});
6663
6664 // If a selection includes multiple comment prefixes, all lines are uncommented.
6665 cx.set_state(indoc! {"
6666 fn a() {
6667 «// a();
6668 /// b();
6669 //! c();ˇ»
6670 }
6671 "});
6672
6673 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
6674
6675 cx.assert_editor_state(indoc! {"
6676 fn a() {
6677 «a();
6678 b();
6679 c();ˇ»
6680 }
6681 "});
6682}
6683
6684#[gpui::test]
6685async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
6686 init_test(cx, |_| {});
6687
6688 let language = Arc::new(Language::new(
6689 LanguageConfig {
6690 line_comments: vec!["// ".into()],
6691 ..Default::default()
6692 },
6693 Some(tree_sitter_rust::language()),
6694 ));
6695
6696 let mut cx = EditorTestContext::new(cx).await;
6697
6698 cx.language_registry().add(language.clone());
6699 cx.update_buffer(|buffer, cx| {
6700 buffer.set_language(Some(language), cx);
6701 });
6702
6703 let toggle_comments = &ToggleComments {
6704 advance_downwards: true,
6705 };
6706
6707 // Single cursor on one line -> advance
6708 // Cursor moves horizontally 3 characters as well on non-blank line
6709 cx.set_state(indoc!(
6710 "fn a() {
6711 ˇdog();
6712 cat();
6713 }"
6714 ));
6715 cx.update_editor(|editor, cx| {
6716 editor.toggle_comments(toggle_comments, cx);
6717 });
6718 cx.assert_editor_state(indoc!(
6719 "fn a() {
6720 // dog();
6721 catˇ();
6722 }"
6723 ));
6724
6725 // Single selection on one line -> don't advance
6726 cx.set_state(indoc!(
6727 "fn a() {
6728 «dog()ˇ»;
6729 cat();
6730 }"
6731 ));
6732 cx.update_editor(|editor, cx| {
6733 editor.toggle_comments(toggle_comments, cx);
6734 });
6735 cx.assert_editor_state(indoc!(
6736 "fn a() {
6737 // «dog()ˇ»;
6738 cat();
6739 }"
6740 ));
6741
6742 // Multiple cursors on one line -> advance
6743 cx.set_state(indoc!(
6744 "fn a() {
6745 ˇdˇog();
6746 cat();
6747 }"
6748 ));
6749 cx.update_editor(|editor, cx| {
6750 editor.toggle_comments(toggle_comments, cx);
6751 });
6752 cx.assert_editor_state(indoc!(
6753 "fn a() {
6754 // dog();
6755 catˇ(ˇ);
6756 }"
6757 ));
6758
6759 // Multiple cursors on one line, with selection -> don't advance
6760 cx.set_state(indoc!(
6761 "fn a() {
6762 ˇdˇog«()ˇ»;
6763 cat();
6764 }"
6765 ));
6766 cx.update_editor(|editor, cx| {
6767 editor.toggle_comments(toggle_comments, cx);
6768 });
6769 cx.assert_editor_state(indoc!(
6770 "fn a() {
6771 // ˇdˇog«()ˇ»;
6772 cat();
6773 }"
6774 ));
6775
6776 // Single cursor on one line -> advance
6777 // Cursor moves to column 0 on blank line
6778 cx.set_state(indoc!(
6779 "fn a() {
6780 ˇdog();
6781
6782 cat();
6783 }"
6784 ));
6785 cx.update_editor(|editor, cx| {
6786 editor.toggle_comments(toggle_comments, cx);
6787 });
6788 cx.assert_editor_state(indoc!(
6789 "fn a() {
6790 // dog();
6791 ˇ
6792 cat();
6793 }"
6794 ));
6795
6796 // Single cursor on one line -> advance
6797 // Cursor starts and ends at column 0
6798 cx.set_state(indoc!(
6799 "fn a() {
6800 ˇ dog();
6801 cat();
6802 }"
6803 ));
6804 cx.update_editor(|editor, cx| {
6805 editor.toggle_comments(toggle_comments, cx);
6806 });
6807 cx.assert_editor_state(indoc!(
6808 "fn a() {
6809 // dog();
6810 ˇ cat();
6811 }"
6812 ));
6813}
6814
6815#[gpui::test]
6816async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
6817 init_test(cx, |_| {});
6818
6819 let mut cx = EditorTestContext::new(cx).await;
6820
6821 let html_language = Arc::new(
6822 Language::new(
6823 LanguageConfig {
6824 name: "HTML".into(),
6825 block_comment: Some(("<!-- ".into(), " -->".into())),
6826 ..Default::default()
6827 },
6828 Some(tree_sitter_html::language()),
6829 )
6830 .with_injection_query(
6831 r#"
6832 (script_element
6833 (raw_text) @content
6834 (#set! "language" "javascript"))
6835 "#,
6836 )
6837 .unwrap(),
6838 );
6839
6840 let javascript_language = Arc::new(Language::new(
6841 LanguageConfig {
6842 name: "JavaScript".into(),
6843 line_comments: vec!["// ".into()],
6844 ..Default::default()
6845 },
6846 Some(tree_sitter_typescript::language_tsx()),
6847 ));
6848
6849 cx.language_registry().add(html_language.clone());
6850 cx.language_registry().add(javascript_language.clone());
6851 cx.update_buffer(|buffer, cx| {
6852 buffer.set_language(Some(html_language), cx);
6853 });
6854
6855 // Toggle comments for empty selections
6856 cx.set_state(
6857 &r#"
6858 <p>A</p>ˇ
6859 <p>B</p>ˇ
6860 <p>C</p>ˇ
6861 "#
6862 .unindent(),
6863 );
6864 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
6865 cx.assert_editor_state(
6866 &r#"
6867 <!-- <p>A</p>ˇ -->
6868 <!-- <p>B</p>ˇ -->
6869 <!-- <p>C</p>ˇ -->
6870 "#
6871 .unindent(),
6872 );
6873 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
6874 cx.assert_editor_state(
6875 &r#"
6876 <p>A</p>ˇ
6877 <p>B</p>ˇ
6878 <p>C</p>ˇ
6879 "#
6880 .unindent(),
6881 );
6882
6883 // Toggle comments for mixture of empty and non-empty selections, where
6884 // multiple selections occupy a given line.
6885 cx.set_state(
6886 &r#"
6887 <p>A«</p>
6888 <p>ˇ»B</p>ˇ
6889 <p>C«</p>
6890 <p>ˇ»D</p>ˇ
6891 "#
6892 .unindent(),
6893 );
6894
6895 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
6896 cx.assert_editor_state(
6897 &r#"
6898 <!-- <p>A«</p>
6899 <p>ˇ»B</p>ˇ -->
6900 <!-- <p>C«</p>
6901 <p>ˇ»D</p>ˇ -->
6902 "#
6903 .unindent(),
6904 );
6905 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
6906 cx.assert_editor_state(
6907 &r#"
6908 <p>A«</p>
6909 <p>ˇ»B</p>ˇ
6910 <p>C«</p>
6911 <p>ˇ»D</p>ˇ
6912 "#
6913 .unindent(),
6914 );
6915
6916 // Toggle comments when different languages are active for different
6917 // selections.
6918 cx.set_state(
6919 &r#"
6920 ˇ<script>
6921 ˇvar x = new Y();
6922 ˇ</script>
6923 "#
6924 .unindent(),
6925 );
6926 cx.executor().run_until_parked();
6927 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
6928 cx.assert_editor_state(
6929 &r#"
6930 <!-- ˇ<script> -->
6931 // ˇvar x = new Y();
6932 <!-- ˇ</script> -->
6933 "#
6934 .unindent(),
6935 );
6936}
6937
6938#[gpui::test]
6939fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
6940 init_test(cx, |_| {});
6941
6942 let buffer = cx.new_model(|cx| {
6943 Buffer::new(
6944 0,
6945 BufferId::new(cx.entity_id().as_u64()).unwrap(),
6946 sample_text(3, 4, 'a'),
6947 )
6948 });
6949 let multibuffer = cx.new_model(|cx| {
6950 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
6951 multibuffer.push_excerpts(
6952 buffer.clone(),
6953 [
6954 ExcerptRange {
6955 context: Point::new(0, 0)..Point::new(0, 4),
6956 primary: None,
6957 },
6958 ExcerptRange {
6959 context: Point::new(1, 0)..Point::new(1, 4),
6960 primary: None,
6961 },
6962 ],
6963 cx,
6964 );
6965 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
6966 multibuffer
6967 });
6968
6969 let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
6970 _ = view.update(cx, |view, cx| {
6971 assert_eq!(view.text(cx), "aaaa\nbbbb");
6972 view.change_selections(None, cx, |s| {
6973 s.select_ranges([
6974 Point::new(0, 0)..Point::new(0, 0),
6975 Point::new(1, 0)..Point::new(1, 0),
6976 ])
6977 });
6978
6979 view.handle_input("X", cx);
6980 assert_eq!(view.text(cx), "Xaaaa\nXbbbb");
6981 assert_eq!(
6982 view.selections.ranges(cx),
6983 [
6984 Point::new(0, 1)..Point::new(0, 1),
6985 Point::new(1, 1)..Point::new(1, 1),
6986 ]
6987 );
6988
6989 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
6990 view.change_selections(None, cx, |s| {
6991 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
6992 });
6993 view.backspace(&Default::default(), cx);
6994 assert_eq!(view.text(cx), "Xa\nbbb");
6995 assert_eq!(
6996 view.selections.ranges(cx),
6997 [Point::new(1, 0)..Point::new(1, 0)]
6998 );
6999
7000 view.change_selections(None, cx, |s| {
7001 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
7002 });
7003 view.backspace(&Default::default(), cx);
7004 assert_eq!(view.text(cx), "X\nbb");
7005 assert_eq!(
7006 view.selections.ranges(cx),
7007 [Point::new(0, 1)..Point::new(0, 1)]
7008 );
7009 });
7010}
7011
7012#[gpui::test]
7013fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
7014 init_test(cx, |_| {});
7015
7016 let markers = vec![('[', ']').into(), ('(', ')').into()];
7017 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
7018 indoc! {"
7019 [aaaa
7020 (bbbb]
7021 cccc)",
7022 },
7023 markers.clone(),
7024 );
7025 let excerpt_ranges = markers.into_iter().map(|marker| {
7026 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
7027 ExcerptRange {
7028 context,
7029 primary: None,
7030 }
7031 });
7032 let buffer = cx.new_model(|cx| {
7033 Buffer::new(
7034 0,
7035 BufferId::new(cx.entity_id().as_u64()).unwrap(),
7036 initial_text,
7037 )
7038 });
7039 let multibuffer = cx.new_model(|cx| {
7040 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
7041 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
7042 multibuffer
7043 });
7044
7045 let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
7046 _ = view.update(cx, |view, cx| {
7047 let (expected_text, selection_ranges) = marked_text_ranges(
7048 indoc! {"
7049 aaaa
7050 bˇbbb
7051 bˇbbˇb
7052 cccc"
7053 },
7054 true,
7055 );
7056 assert_eq!(view.text(cx), expected_text);
7057 view.change_selections(None, cx, |s| s.select_ranges(selection_ranges));
7058
7059 view.handle_input("X", cx);
7060
7061 let (expected_text, expected_selections) = marked_text_ranges(
7062 indoc! {"
7063 aaaa
7064 bXˇbbXb
7065 bXˇbbXˇb
7066 cccc"
7067 },
7068 false,
7069 );
7070 assert_eq!(view.text(cx), expected_text);
7071 assert_eq!(view.selections.ranges(cx), expected_selections);
7072
7073 view.newline(&Newline, cx);
7074 let (expected_text, expected_selections) = marked_text_ranges(
7075 indoc! {"
7076 aaaa
7077 bX
7078 ˇbbX
7079 b
7080 bX
7081 ˇbbX
7082 ˇb
7083 cccc"
7084 },
7085 false,
7086 );
7087 assert_eq!(view.text(cx), expected_text);
7088 assert_eq!(view.selections.ranges(cx), expected_selections);
7089 });
7090}
7091
7092#[gpui::test]
7093fn test_refresh_selections(cx: &mut TestAppContext) {
7094 init_test(cx, |_| {});
7095
7096 let buffer = cx.new_model(|cx| {
7097 Buffer::new(
7098 0,
7099 BufferId::new(cx.entity_id().as_u64()).unwrap(),
7100 sample_text(3, 4, 'a'),
7101 )
7102 });
7103 let mut excerpt1_id = None;
7104 let multibuffer = cx.new_model(|cx| {
7105 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
7106 excerpt1_id = multibuffer
7107 .push_excerpts(
7108 buffer.clone(),
7109 [
7110 ExcerptRange {
7111 context: Point::new(0, 0)..Point::new(1, 4),
7112 primary: None,
7113 },
7114 ExcerptRange {
7115 context: Point::new(1, 0)..Point::new(2, 4),
7116 primary: None,
7117 },
7118 ],
7119 cx,
7120 )
7121 .into_iter()
7122 .next();
7123 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
7124 multibuffer
7125 });
7126
7127 let editor = cx.add_window(|cx| {
7128 let mut editor = build_editor(multibuffer.clone(), cx);
7129 let snapshot = editor.snapshot(cx);
7130 editor.change_selections(None, cx, |s| {
7131 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
7132 });
7133 editor.begin_selection(Point::new(2, 1).to_display_point(&snapshot), true, 1, cx);
7134 assert_eq!(
7135 editor.selections.ranges(cx),
7136 [
7137 Point::new(1, 3)..Point::new(1, 3),
7138 Point::new(2, 1)..Point::new(2, 1),
7139 ]
7140 );
7141 editor
7142 });
7143
7144 // Refreshing selections is a no-op when excerpts haven't changed.
7145 _ = editor.update(cx, |editor, cx| {
7146 editor.change_selections(None, cx, |s| s.refresh());
7147 assert_eq!(
7148 editor.selections.ranges(cx),
7149 [
7150 Point::new(1, 3)..Point::new(1, 3),
7151 Point::new(2, 1)..Point::new(2, 1),
7152 ]
7153 );
7154 });
7155
7156 _ = multibuffer.update(cx, |multibuffer, cx| {
7157 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
7158 });
7159 _ = editor.update(cx, |editor, cx| {
7160 // Removing an excerpt causes the first selection to become degenerate.
7161 assert_eq!(
7162 editor.selections.ranges(cx),
7163 [
7164 Point::new(0, 0)..Point::new(0, 0),
7165 Point::new(0, 1)..Point::new(0, 1)
7166 ]
7167 );
7168
7169 // Refreshing selections will relocate the first selection to the original buffer
7170 // location.
7171 editor.change_selections(None, cx, |s| s.refresh());
7172 assert_eq!(
7173 editor.selections.ranges(cx),
7174 [
7175 Point::new(0, 1)..Point::new(0, 1),
7176 Point::new(0, 3)..Point::new(0, 3)
7177 ]
7178 );
7179 assert!(editor.selections.pending_anchor().is_some());
7180 });
7181}
7182
7183#[gpui::test]
7184fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
7185 init_test(cx, |_| {});
7186
7187 let buffer = cx.new_model(|cx| {
7188 Buffer::new(
7189 0,
7190 BufferId::new(cx.entity_id().as_u64()).unwrap(),
7191 sample_text(3, 4, 'a'),
7192 )
7193 });
7194 let mut excerpt1_id = None;
7195 let multibuffer = cx.new_model(|cx| {
7196 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
7197 excerpt1_id = multibuffer
7198 .push_excerpts(
7199 buffer.clone(),
7200 [
7201 ExcerptRange {
7202 context: Point::new(0, 0)..Point::new(1, 4),
7203 primary: None,
7204 },
7205 ExcerptRange {
7206 context: Point::new(1, 0)..Point::new(2, 4),
7207 primary: None,
7208 },
7209 ],
7210 cx,
7211 )
7212 .into_iter()
7213 .next();
7214 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
7215 multibuffer
7216 });
7217
7218 let editor = cx.add_window(|cx| {
7219 let mut editor = build_editor(multibuffer.clone(), cx);
7220 let snapshot = editor.snapshot(cx);
7221 editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx);
7222 assert_eq!(
7223 editor.selections.ranges(cx),
7224 [Point::new(1, 3)..Point::new(1, 3)]
7225 );
7226 editor
7227 });
7228
7229 _ = multibuffer.update(cx, |multibuffer, cx| {
7230 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
7231 });
7232 _ = editor.update(cx, |editor, cx| {
7233 assert_eq!(
7234 editor.selections.ranges(cx),
7235 [Point::new(0, 0)..Point::new(0, 0)]
7236 );
7237
7238 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
7239 editor.change_selections(None, cx, |s| s.refresh());
7240 assert_eq!(
7241 editor.selections.ranges(cx),
7242 [Point::new(0, 3)..Point::new(0, 3)]
7243 );
7244 assert!(editor.selections.pending_anchor().is_some());
7245 });
7246}
7247
7248#[gpui::test]
7249async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
7250 init_test(cx, |_| {});
7251
7252 let language = Arc::new(
7253 Language::new(
7254 LanguageConfig {
7255 brackets: BracketPairConfig {
7256 pairs: vec![
7257 BracketPair {
7258 start: "{".to_string(),
7259 end: "}".to_string(),
7260 close: true,
7261 newline: true,
7262 },
7263 BracketPair {
7264 start: "/* ".to_string(),
7265 end: " */".to_string(),
7266 close: true,
7267 newline: true,
7268 },
7269 ],
7270 ..Default::default()
7271 },
7272 ..Default::default()
7273 },
7274 Some(tree_sitter_rust::language()),
7275 )
7276 .with_indents_query("")
7277 .unwrap(),
7278 );
7279
7280 let text = concat!(
7281 "{ }\n", //
7282 " x\n", //
7283 " /* */\n", //
7284 "x\n", //
7285 "{{} }\n", //
7286 );
7287
7288 let buffer = cx.new_model(|cx| {
7289 Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text)
7290 .with_language(language, cx)
7291 });
7292 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
7293 let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
7294 view.condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
7295 .await;
7296
7297 _ = view.update(cx, |view, cx| {
7298 view.change_selections(None, cx, |s| {
7299 s.select_display_ranges([
7300 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3),
7301 DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
7302 DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4),
7303 ])
7304 });
7305 view.newline(&Newline, cx);
7306
7307 assert_eq!(
7308 view.buffer().read(cx).read(cx).text(),
7309 concat!(
7310 "{ \n", // Suppress rustfmt
7311 "\n", //
7312 "}\n", //
7313 " x\n", //
7314 " /* \n", //
7315 " \n", //
7316 " */\n", //
7317 "x\n", //
7318 "{{} \n", //
7319 "}\n", //
7320 )
7321 );
7322 });
7323}
7324
7325#[gpui::test]
7326fn test_highlighted_ranges(cx: &mut TestAppContext) {
7327 init_test(cx, |_| {});
7328
7329 let editor = cx.add_window(|cx| {
7330 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
7331 build_editor(buffer.clone(), cx)
7332 });
7333
7334 _ = editor.update(cx, |editor, cx| {
7335 struct Type1;
7336 struct Type2;
7337
7338 let buffer = editor.buffer.read(cx).snapshot(cx);
7339
7340 let anchor_range =
7341 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
7342
7343 editor.highlight_background::<Type1>(
7344 &[
7345 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
7346 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
7347 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
7348 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
7349 ],
7350 |_| Hsla::red(),
7351 cx,
7352 );
7353 editor.highlight_background::<Type2>(
7354 &[
7355 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
7356 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
7357 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
7358 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
7359 ],
7360 |_| Hsla::green(),
7361 cx,
7362 );
7363
7364 let snapshot = editor.snapshot(cx);
7365 let mut highlighted_ranges = editor.background_highlights_in_range(
7366 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
7367 &snapshot,
7368 cx.theme().colors(),
7369 );
7370 // Enforce a consistent ordering based on color without relying on the ordering of the
7371 // highlight's `TypeId` which is non-executor.
7372 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
7373 assert_eq!(
7374 highlighted_ranges,
7375 &[
7376 (
7377 DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4),
7378 Hsla::red(),
7379 ),
7380 (
7381 DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
7382 Hsla::red(),
7383 ),
7384 (
7385 DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5),
7386 Hsla::green(),
7387 ),
7388 (
7389 DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6),
7390 Hsla::green(),
7391 ),
7392 ]
7393 );
7394 assert_eq!(
7395 editor.background_highlights_in_range(
7396 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
7397 &snapshot,
7398 cx.theme().colors(),
7399 ),
7400 &[(
7401 DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
7402 Hsla::red(),
7403 )]
7404 );
7405 });
7406}
7407
7408#[gpui::test]
7409async fn test_following(cx: &mut gpui::TestAppContext) {
7410 init_test(cx, |_| {});
7411
7412 let fs = FakeFs::new(cx.executor());
7413 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
7414
7415 let buffer = project.update(cx, |project, cx| {
7416 let buffer = project
7417 .create_buffer(&sample_text(16, 8, 'a'), None, cx)
7418 .unwrap();
7419 cx.new_model(|cx| MultiBuffer::singleton(buffer, cx))
7420 });
7421 let leader = cx.add_window(|cx| build_editor(buffer.clone(), cx));
7422 let follower = cx.update(|cx| {
7423 cx.open_window(
7424 WindowOptions {
7425 bounds: Some(Bounds::from_corners(
7426 gpui::Point::new(0.into(), 0.into()),
7427 gpui::Point::new(10.into(), 80.into()),
7428 )),
7429 ..Default::default()
7430 },
7431 |cx| cx.new_view(|cx| build_editor(buffer.clone(), cx)),
7432 )
7433 });
7434
7435 let is_still_following = Rc::new(RefCell::new(true));
7436 let follower_edit_event_count = Rc::new(RefCell::new(0));
7437 let pending_update = Rc::new(RefCell::new(None));
7438 _ = follower.update(cx, {
7439 let update = pending_update.clone();
7440 let is_still_following = is_still_following.clone();
7441 let follower_edit_event_count = follower_edit_event_count.clone();
7442 |_, cx| {
7443 cx.subscribe(
7444 &leader.root_view(cx).unwrap(),
7445 move |_, leader, event, cx| {
7446 leader
7447 .read(cx)
7448 .add_event_to_update_proto(event, &mut update.borrow_mut(), cx);
7449 },
7450 )
7451 .detach();
7452
7453 cx.subscribe(
7454 &follower.root_view(cx).unwrap(),
7455 move |_, _, event: &EditorEvent, _cx| {
7456 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
7457 *is_still_following.borrow_mut() = false;
7458 }
7459
7460 if let EditorEvent::BufferEdited = event {
7461 *follower_edit_event_count.borrow_mut() += 1;
7462 }
7463 },
7464 )
7465 .detach();
7466 }
7467 });
7468
7469 // Update the selections only
7470 _ = leader.update(cx, |leader, cx| {
7471 leader.change_selections(None, cx, |s| s.select_ranges([1..1]));
7472 });
7473 follower
7474 .update(cx, |follower, cx| {
7475 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
7476 })
7477 .unwrap()
7478 .await
7479 .unwrap();
7480 _ = follower.update(cx, |follower, cx| {
7481 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
7482 });
7483 assert_eq!(*is_still_following.borrow(), true);
7484 assert_eq!(*follower_edit_event_count.borrow(), 0);
7485
7486 // Update the scroll position only
7487 _ = leader.update(cx, |leader, cx| {
7488 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), cx);
7489 });
7490 follower
7491 .update(cx, |follower, cx| {
7492 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
7493 })
7494 .unwrap()
7495 .await
7496 .unwrap();
7497 assert_eq!(
7498 follower
7499 .update(cx, |follower, cx| follower.scroll_position(cx))
7500 .unwrap(),
7501 gpui::Point::new(1.5, 3.5)
7502 );
7503 assert_eq!(*is_still_following.borrow(), true);
7504 assert_eq!(*follower_edit_event_count.borrow(), 0);
7505
7506 // Update the selections and scroll position. The follower's scroll position is updated
7507 // via autoscroll, not via the leader's exact scroll position.
7508 _ = leader.update(cx, |leader, cx| {
7509 leader.change_selections(None, cx, |s| s.select_ranges([0..0]));
7510 leader.request_autoscroll(Autoscroll::newest(), cx);
7511 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), cx);
7512 });
7513 follower
7514 .update(cx, |follower, cx| {
7515 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
7516 })
7517 .unwrap()
7518 .await
7519 .unwrap();
7520 _ = follower.update(cx, |follower, cx| {
7521 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
7522 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
7523 });
7524 assert_eq!(*is_still_following.borrow(), true);
7525
7526 // Creating a pending selection that precedes another selection
7527 _ = leader.update(cx, |leader, cx| {
7528 leader.change_selections(None, cx, |s| s.select_ranges([1..1]));
7529 leader.begin_selection(DisplayPoint::new(0, 0), true, 1, cx);
7530 });
7531 follower
7532 .update(cx, |follower, cx| {
7533 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
7534 })
7535 .unwrap()
7536 .await
7537 .unwrap();
7538 _ = follower.update(cx, |follower, cx| {
7539 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
7540 });
7541 assert_eq!(*is_still_following.borrow(), true);
7542
7543 // Extend the pending selection so that it surrounds another selection
7544 _ = leader.update(cx, |leader, cx| {
7545 leader.extend_selection(DisplayPoint::new(0, 2), 1, cx);
7546 });
7547 follower
7548 .update(cx, |follower, cx| {
7549 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
7550 })
7551 .unwrap()
7552 .await
7553 .unwrap();
7554 _ = follower.update(cx, |follower, cx| {
7555 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
7556 });
7557
7558 // Scrolling locally breaks the follow
7559 _ = follower.update(cx, |follower, cx| {
7560 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
7561 follower.set_scroll_anchor(
7562 ScrollAnchor {
7563 anchor: top_anchor,
7564 offset: gpui::Point::new(0.0, 0.5),
7565 },
7566 cx,
7567 );
7568 });
7569 assert_eq!(*is_still_following.borrow(), false);
7570}
7571
7572#[gpui::test]
7573async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
7574 init_test(cx, |_| {});
7575
7576 let fs = FakeFs::new(cx.executor());
7577 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
7578 let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
7579 let pane = workspace
7580 .update(cx, |workspace, _| workspace.active_pane().clone())
7581 .unwrap();
7582
7583 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7584
7585 let leader = pane.update(cx, |_, cx| {
7586 let multibuffer = cx.new_model(|_| MultiBuffer::new(0, ReadWrite));
7587 cx.new_view(|cx| build_editor(multibuffer.clone(), cx))
7588 });
7589
7590 // Start following the editor when it has no excerpts.
7591 let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
7592 let follower_1 = cx
7593 .update_window(*workspace.deref(), |_, cx| {
7594 Editor::from_state_proto(
7595 pane.clone(),
7596 workspace.root_view(cx).unwrap(),
7597 ViewId {
7598 creator: Default::default(),
7599 id: 0,
7600 },
7601 &mut state_message,
7602 cx,
7603 )
7604 })
7605 .unwrap()
7606 .unwrap()
7607 .await
7608 .unwrap();
7609
7610 let update_message = Rc::new(RefCell::new(None));
7611 follower_1.update(cx, {
7612 let update = update_message.clone();
7613 |_, cx| {
7614 cx.subscribe(&leader, move |_, leader, event, cx| {
7615 leader
7616 .read(cx)
7617 .add_event_to_update_proto(event, &mut update.borrow_mut(), cx);
7618 })
7619 .detach();
7620 }
7621 });
7622
7623 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
7624 (
7625 project
7626 .create_buffer("abc\ndef\nghi\njkl\n", None, cx)
7627 .unwrap(),
7628 project
7629 .create_buffer("mno\npqr\nstu\nvwx\n", None, cx)
7630 .unwrap(),
7631 )
7632 });
7633
7634 // Insert some excerpts.
7635 _ = leader.update(cx, |leader, cx| {
7636 leader.buffer.update(cx, |multibuffer, cx| {
7637 let excerpt_ids = multibuffer.push_excerpts(
7638 buffer_1.clone(),
7639 [
7640 ExcerptRange {
7641 context: 1..6,
7642 primary: None,
7643 },
7644 ExcerptRange {
7645 context: 12..15,
7646 primary: None,
7647 },
7648 ExcerptRange {
7649 context: 0..3,
7650 primary: None,
7651 },
7652 ],
7653 cx,
7654 );
7655 multibuffer.insert_excerpts_after(
7656 excerpt_ids[0],
7657 buffer_2.clone(),
7658 [
7659 ExcerptRange {
7660 context: 8..12,
7661 primary: None,
7662 },
7663 ExcerptRange {
7664 context: 0..6,
7665 primary: None,
7666 },
7667 ],
7668 cx,
7669 );
7670 });
7671 });
7672
7673 // Apply the update of adding the excerpts.
7674 follower_1
7675 .update(cx, |follower, cx| {
7676 follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
7677 })
7678 .await
7679 .unwrap();
7680 assert_eq!(
7681 follower_1.update(cx, |editor, cx| editor.text(cx)),
7682 leader.update(cx, |editor, cx| editor.text(cx))
7683 );
7684 update_message.borrow_mut().take();
7685
7686 // Start following separately after it already has excerpts.
7687 let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
7688 let follower_2 = cx
7689 .update_window(*workspace.deref(), |_, cx| {
7690 Editor::from_state_proto(
7691 pane.clone(),
7692 workspace.root_view(cx).unwrap().clone(),
7693 ViewId {
7694 creator: Default::default(),
7695 id: 0,
7696 },
7697 &mut state_message,
7698 cx,
7699 )
7700 })
7701 .unwrap()
7702 .unwrap()
7703 .await
7704 .unwrap();
7705 assert_eq!(
7706 follower_2.update(cx, |editor, cx| editor.text(cx)),
7707 leader.update(cx, |editor, cx| editor.text(cx))
7708 );
7709
7710 // Remove some excerpts.
7711 _ = leader.update(cx, |leader, cx| {
7712 leader.buffer.update(cx, |multibuffer, cx| {
7713 let excerpt_ids = multibuffer.excerpt_ids();
7714 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
7715 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
7716 });
7717 });
7718
7719 // Apply the update of removing the excerpts.
7720 follower_1
7721 .update(cx, |follower, cx| {
7722 follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
7723 })
7724 .await
7725 .unwrap();
7726 follower_2
7727 .update(cx, |follower, cx| {
7728 follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
7729 })
7730 .await
7731 .unwrap();
7732 update_message.borrow_mut().take();
7733 assert_eq!(
7734 follower_1.update(cx, |editor, cx| editor.text(cx)),
7735 leader.update(cx, |editor, cx| editor.text(cx))
7736 );
7737}
7738
7739#[gpui::test]
7740async fn go_to_prev_overlapping_diagnostic(
7741 executor: BackgroundExecutor,
7742 cx: &mut gpui::TestAppContext,
7743) {
7744 init_test(cx, |_| {});
7745
7746 let mut cx = EditorTestContext::new(cx).await;
7747 let project = cx.update_editor(|editor, _| editor.project.clone().unwrap());
7748
7749 cx.set_state(indoc! {"
7750 ˇfn func(abc def: i32) -> u32 {
7751 }
7752 "});
7753
7754 _ = cx.update(|cx| {
7755 _ = project.update(cx, |project, cx| {
7756 project
7757 .update_diagnostics(
7758 LanguageServerId(0),
7759 lsp::PublishDiagnosticsParams {
7760 uri: lsp::Url::from_file_path("/root/file").unwrap(),
7761 version: None,
7762 diagnostics: vec![
7763 lsp::Diagnostic {
7764 range: lsp::Range::new(
7765 lsp::Position::new(0, 11),
7766 lsp::Position::new(0, 12),
7767 ),
7768 severity: Some(lsp::DiagnosticSeverity::ERROR),
7769 ..Default::default()
7770 },
7771 lsp::Diagnostic {
7772 range: lsp::Range::new(
7773 lsp::Position::new(0, 12),
7774 lsp::Position::new(0, 15),
7775 ),
7776 severity: Some(lsp::DiagnosticSeverity::ERROR),
7777 ..Default::default()
7778 },
7779 lsp::Diagnostic {
7780 range: lsp::Range::new(
7781 lsp::Position::new(0, 25),
7782 lsp::Position::new(0, 28),
7783 ),
7784 severity: Some(lsp::DiagnosticSeverity::ERROR),
7785 ..Default::default()
7786 },
7787 ],
7788 },
7789 &[],
7790 cx,
7791 )
7792 .unwrap()
7793 });
7794 });
7795
7796 executor.run_until_parked();
7797
7798 cx.update_editor(|editor, cx| {
7799 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
7800 });
7801
7802 cx.assert_editor_state(indoc! {"
7803 fn func(abc def: i32) -> ˇu32 {
7804 }
7805 "});
7806
7807 cx.update_editor(|editor, cx| {
7808 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
7809 });
7810
7811 cx.assert_editor_state(indoc! {"
7812 fn func(abc ˇdef: i32) -> u32 {
7813 }
7814 "});
7815
7816 cx.update_editor(|editor, cx| {
7817 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
7818 });
7819
7820 cx.assert_editor_state(indoc! {"
7821 fn func(abcˇ def: i32) -> u32 {
7822 }
7823 "});
7824
7825 cx.update_editor(|editor, cx| {
7826 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
7827 });
7828
7829 cx.assert_editor_state(indoc! {"
7830 fn func(abc def: i32) -> ˇu32 {
7831 }
7832 "});
7833}
7834
7835#[gpui::test]
7836async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
7837 init_test(cx, |_| {});
7838
7839 let mut cx = EditorTestContext::new(cx).await;
7840
7841 let diff_base = r#"
7842 use some::mod;
7843
7844 const A: u32 = 42;
7845
7846 fn main() {
7847 println!("hello");
7848
7849 println!("world");
7850 }
7851 "#
7852 .unindent();
7853
7854 // Edits are modified, removed, modified, added
7855 cx.set_state(
7856 &r#"
7857 use some::modified;
7858
7859 ˇ
7860 fn main() {
7861 println!("hello there");
7862
7863 println!("around the");
7864 println!("world");
7865 }
7866 "#
7867 .unindent(),
7868 );
7869
7870 cx.set_diff_base(Some(&diff_base));
7871 executor.run_until_parked();
7872
7873 cx.update_editor(|editor, cx| {
7874 //Wrap around the bottom of the buffer
7875 for _ in 0..3 {
7876 editor.go_to_hunk(&GoToHunk, cx);
7877 }
7878 });
7879
7880 cx.assert_editor_state(
7881 &r#"
7882 ˇuse some::modified;
7883
7884
7885 fn main() {
7886 println!("hello there");
7887
7888 println!("around the");
7889 println!("world");
7890 }
7891 "#
7892 .unindent(),
7893 );
7894
7895 cx.update_editor(|editor, cx| {
7896 //Wrap around the top of the buffer
7897 for _ in 0..2 {
7898 editor.go_to_prev_hunk(&GoToPrevHunk, cx);
7899 }
7900 });
7901
7902 cx.assert_editor_state(
7903 &r#"
7904 use some::modified;
7905
7906
7907 fn main() {
7908 ˇ println!("hello there");
7909
7910 println!("around the");
7911 println!("world");
7912 }
7913 "#
7914 .unindent(),
7915 );
7916
7917 cx.update_editor(|editor, cx| {
7918 editor.go_to_prev_hunk(&GoToPrevHunk, cx);
7919 });
7920
7921 cx.assert_editor_state(
7922 &r#"
7923 use some::modified;
7924
7925 ˇ
7926 fn main() {
7927 println!("hello there");
7928
7929 println!("around the");
7930 println!("world");
7931 }
7932 "#
7933 .unindent(),
7934 );
7935
7936 cx.update_editor(|editor, cx| {
7937 for _ in 0..3 {
7938 editor.go_to_prev_hunk(&GoToPrevHunk, cx);
7939 }
7940 });
7941
7942 cx.assert_editor_state(
7943 &r#"
7944 use some::modified;
7945
7946
7947 fn main() {
7948 ˇ println!("hello there");
7949
7950 println!("around the");
7951 println!("world");
7952 }
7953 "#
7954 .unindent(),
7955 );
7956
7957 cx.update_editor(|editor, cx| {
7958 editor.fold(&Fold, cx);
7959
7960 //Make sure that the fold only gets one hunk
7961 for _ in 0..4 {
7962 editor.go_to_hunk(&GoToHunk, cx);
7963 }
7964 });
7965
7966 cx.assert_editor_state(
7967 &r#"
7968 ˇuse some::modified;
7969
7970
7971 fn main() {
7972 println!("hello there");
7973
7974 println!("around the");
7975 println!("world");
7976 }
7977 "#
7978 .unindent(),
7979 );
7980}
7981
7982#[test]
7983fn test_split_words() {
7984 fn split(text: &str) -> Vec<&str> {
7985 split_words(text).collect()
7986 }
7987
7988 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
7989 assert_eq!(split("hello_world"), &["hello_", "world"]);
7990 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
7991 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
7992 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
7993 assert_eq!(split("helloworld"), &["helloworld"]);
7994
7995 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
7996}
7997
7998#[gpui::test]
7999async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
8000 init_test(cx, |_| {});
8001
8002 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
8003 let mut assert = |before, after| {
8004 let _state_context = cx.set_state(before);
8005 cx.update_editor(|editor, cx| {
8006 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, cx)
8007 });
8008 cx.assert_editor_state(after);
8009 };
8010
8011 // Outside bracket jumps to outside of matching bracket
8012 assert("console.logˇ(var);", "console.log(var)ˇ;");
8013 assert("console.log(var)ˇ;", "console.logˇ(var);");
8014
8015 // Inside bracket jumps to inside of matching bracket
8016 assert("console.log(ˇvar);", "console.log(varˇ);");
8017 assert("console.log(varˇ);", "console.log(ˇvar);");
8018
8019 // When outside a bracket and inside, favor jumping to the inside bracket
8020 assert(
8021 "console.log('foo', [1, 2, 3]ˇ);",
8022 "console.log(ˇ'foo', [1, 2, 3]);",
8023 );
8024 assert(
8025 "console.log(ˇ'foo', [1, 2, 3]);",
8026 "console.log('foo', [1, 2, 3]ˇ);",
8027 );
8028
8029 // Bias forward if two options are equally likely
8030 assert(
8031 "let result = curried_fun()ˇ();",
8032 "let result = curried_fun()()ˇ;",
8033 );
8034
8035 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
8036 assert(
8037 indoc! {"
8038 function test() {
8039 console.log('test')ˇ
8040 }"},
8041 indoc! {"
8042 function test() {
8043 console.logˇ('test')
8044 }"},
8045 );
8046}
8047
8048#[gpui::test]
8049async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
8050 init_test(cx, |_| {});
8051
8052 let fs = FakeFs::new(cx.executor());
8053 fs.insert_tree(
8054 "/a",
8055 json!({
8056 "main.rs": "fn main() { let a = 5; }",
8057 "other.rs": "// Test file",
8058 }),
8059 )
8060 .await;
8061 let project = Project::test(fs, ["/a".as_ref()], cx).await;
8062
8063 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8064 language_registry.add(Arc::new(Language::new(
8065 LanguageConfig {
8066 name: "Rust".into(),
8067 matcher: LanguageMatcher {
8068 path_suffixes: vec!["rs".to_string()],
8069 ..Default::default()
8070 },
8071 brackets: BracketPairConfig {
8072 pairs: vec![BracketPair {
8073 start: "{".to_string(),
8074 end: "}".to_string(),
8075 close: true,
8076 newline: true,
8077 }],
8078 disabled_scopes_by_bracket_ix: Vec::new(),
8079 },
8080 ..Default::default()
8081 },
8082 Some(tree_sitter_rust::language()),
8083 )));
8084 let mut fake_servers = language_registry.register_fake_lsp_adapter(
8085 "Rust",
8086 FakeLspAdapter {
8087 capabilities: lsp::ServerCapabilities {
8088 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
8089 first_trigger_character: "{".to_string(),
8090 more_trigger_character: None,
8091 }),
8092 ..Default::default()
8093 },
8094 ..Default::default()
8095 },
8096 );
8097
8098 let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
8099
8100 let cx = &mut VisualTestContext::from_window(*workspace, cx);
8101
8102 let worktree_id = workspace
8103 .update(cx, |workspace, cx| {
8104 workspace.project().update(cx, |project, cx| {
8105 project.worktrees().next().unwrap().read(cx).id()
8106 })
8107 })
8108 .unwrap();
8109
8110 let buffer = project
8111 .update(cx, |project, cx| {
8112 project.open_local_buffer("/a/main.rs", cx)
8113 })
8114 .await
8115 .unwrap();
8116 cx.executor().run_until_parked();
8117 cx.executor().start_waiting();
8118 let fake_server = fake_servers.next().await.unwrap();
8119 let editor_handle = workspace
8120 .update(cx, |workspace, cx| {
8121 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
8122 })
8123 .unwrap()
8124 .await
8125 .unwrap()
8126 .downcast::<Editor>()
8127 .unwrap();
8128
8129 fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
8130 assert_eq!(
8131 params.text_document_position.text_document.uri,
8132 lsp::Url::from_file_path("/a/main.rs").unwrap(),
8133 );
8134 assert_eq!(
8135 params.text_document_position.position,
8136 lsp::Position::new(0, 21),
8137 );
8138
8139 Ok(Some(vec![lsp::TextEdit {
8140 new_text: "]".to_string(),
8141 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
8142 }]))
8143 });
8144
8145 editor_handle.update(cx, |editor, cx| {
8146 editor.focus(cx);
8147 editor.change_selections(None, cx, |s| {
8148 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
8149 });
8150 editor.handle_input("{", cx);
8151 });
8152
8153 cx.executor().run_until_parked();
8154
8155 _ = buffer.update(cx, |buffer, _| {
8156 assert_eq!(
8157 buffer.text(),
8158 "fn main() { let a = {5}; }",
8159 "No extra braces from on type formatting should appear in the buffer"
8160 )
8161 });
8162}
8163
8164#[gpui::test]
8165async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) {
8166 init_test(cx, |_| {});
8167
8168 let fs = FakeFs::new(cx.executor());
8169 fs.insert_tree(
8170 "/a",
8171 json!({
8172 "main.rs": "fn main() { let a = 5; }",
8173 "other.rs": "// Test file",
8174 }),
8175 )
8176 .await;
8177
8178 let project = Project::test(fs, ["/a".as_ref()], cx).await;
8179
8180 let server_restarts = Arc::new(AtomicUsize::new(0));
8181 let closure_restarts = Arc::clone(&server_restarts);
8182 let language_server_name = "test language server";
8183 let language_name: Arc<str> = "Rust".into();
8184
8185 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8186 language_registry.add(Arc::new(Language::new(
8187 LanguageConfig {
8188 name: Arc::clone(&language_name),
8189 matcher: LanguageMatcher {
8190 path_suffixes: vec!["rs".to_string()],
8191 ..Default::default()
8192 },
8193 ..Default::default()
8194 },
8195 Some(tree_sitter_rust::language()),
8196 )));
8197 let mut fake_servers = language_registry.register_fake_lsp_adapter(
8198 "Rust",
8199 FakeLspAdapter {
8200 name: language_server_name,
8201 initialization_options: Some(json!({
8202 "testOptionValue": true
8203 })),
8204 initializer: Some(Box::new(move |fake_server| {
8205 let task_restarts = Arc::clone(&closure_restarts);
8206 fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
8207 task_restarts.fetch_add(1, atomic::Ordering::Release);
8208 futures::future::ready(Ok(()))
8209 });
8210 })),
8211 ..Default::default()
8212 },
8213 );
8214
8215 let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
8216 let _buffer = project
8217 .update(cx, |project, cx| {
8218 project.open_local_buffer("/a/main.rs", cx)
8219 })
8220 .await
8221 .unwrap();
8222 let _fake_server = fake_servers.next().await.unwrap();
8223 update_test_language_settings(cx, |language_settings| {
8224 language_settings.languages.insert(
8225 Arc::clone(&language_name),
8226 LanguageSettingsContent {
8227 tab_size: NonZeroU32::new(8),
8228 ..Default::default()
8229 },
8230 );
8231 });
8232 cx.executor().run_until_parked();
8233 assert_eq!(
8234 server_restarts.load(atomic::Ordering::Acquire),
8235 0,
8236 "Should not restart LSP server on an unrelated change"
8237 );
8238
8239 update_test_project_settings(cx, |project_settings| {
8240 project_settings.lsp.insert(
8241 "Some other server name".into(),
8242 LspSettings {
8243 binary: None,
8244 settings: None,
8245 initialization_options: Some(json!({
8246 "some other init value": false
8247 })),
8248 },
8249 );
8250 });
8251 cx.executor().run_until_parked();
8252 assert_eq!(
8253 server_restarts.load(atomic::Ordering::Acquire),
8254 0,
8255 "Should not restart LSP server on an unrelated LSP settings change"
8256 );
8257
8258 update_test_project_settings(cx, |project_settings| {
8259 project_settings.lsp.insert(
8260 language_server_name.into(),
8261 LspSettings {
8262 binary: None,
8263 settings: None,
8264 initialization_options: Some(json!({
8265 "anotherInitValue": false
8266 })),
8267 },
8268 );
8269 });
8270 cx.executor().run_until_parked();
8271 assert_eq!(
8272 server_restarts.load(atomic::Ordering::Acquire),
8273 1,
8274 "Should restart LSP server on a related LSP settings change"
8275 );
8276
8277 update_test_project_settings(cx, |project_settings| {
8278 project_settings.lsp.insert(
8279 language_server_name.into(),
8280 LspSettings {
8281 binary: None,
8282 settings: None,
8283 initialization_options: Some(json!({
8284 "anotherInitValue": false
8285 })),
8286 },
8287 );
8288 });
8289 cx.executor().run_until_parked();
8290 assert_eq!(
8291 server_restarts.load(atomic::Ordering::Acquire),
8292 1,
8293 "Should not restart LSP server on a related LSP settings change that is the same"
8294 );
8295
8296 update_test_project_settings(cx, |project_settings| {
8297 project_settings.lsp.insert(
8298 language_server_name.into(),
8299 LspSettings {
8300 binary: None,
8301 settings: None,
8302 initialization_options: None,
8303 },
8304 );
8305 });
8306 cx.executor().run_until_parked();
8307 assert_eq!(
8308 server_restarts.load(atomic::Ordering::Acquire),
8309 2,
8310 "Should restart LSP server on another related LSP settings change"
8311 );
8312}
8313
8314#[gpui::test]
8315async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
8316 init_test(cx, |_| {});
8317
8318 let mut cx = EditorLspTestContext::new_rust(
8319 lsp::ServerCapabilities {
8320 completion_provider: Some(lsp::CompletionOptions {
8321 trigger_characters: Some(vec![".".to_string()]),
8322 resolve_provider: Some(true),
8323 ..Default::default()
8324 }),
8325 ..Default::default()
8326 },
8327 cx,
8328 )
8329 .await;
8330
8331 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
8332 cx.simulate_keystroke(".");
8333 let completion_item = lsp::CompletionItem {
8334 label: "some".into(),
8335 kind: Some(lsp::CompletionItemKind::SNIPPET),
8336 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
8337 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
8338 kind: lsp::MarkupKind::Markdown,
8339 value: "```rust\nSome(2)\n```".to_string(),
8340 })),
8341 deprecated: Some(false),
8342 sort_text: Some("fffffff2".to_string()),
8343 filter_text: Some("some".to_string()),
8344 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
8345 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8346 range: lsp::Range {
8347 start: lsp::Position {
8348 line: 0,
8349 character: 22,
8350 },
8351 end: lsp::Position {
8352 line: 0,
8353 character: 22,
8354 },
8355 },
8356 new_text: "Some(2)".to_string(),
8357 })),
8358 additional_text_edits: Some(vec![lsp::TextEdit {
8359 range: lsp::Range {
8360 start: lsp::Position {
8361 line: 0,
8362 character: 20,
8363 },
8364 end: lsp::Position {
8365 line: 0,
8366 character: 22,
8367 },
8368 },
8369 new_text: "".to_string(),
8370 }]),
8371 ..Default::default()
8372 };
8373
8374 let closure_completion_item = completion_item.clone();
8375 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
8376 let task_completion_item = closure_completion_item.clone();
8377 async move {
8378 Ok(Some(lsp::CompletionResponse::Array(vec![
8379 task_completion_item,
8380 ])))
8381 }
8382 });
8383
8384 request.next().await;
8385
8386 cx.condition(|editor, _| editor.context_menu_visible())
8387 .await;
8388 let apply_additional_edits = cx.update_editor(|editor, cx| {
8389 editor
8390 .confirm_completion(&ConfirmCompletion::default(), cx)
8391 .unwrap()
8392 });
8393 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
8394
8395 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
8396 let task_completion_item = completion_item.clone();
8397 async move { Ok(task_completion_item) }
8398 })
8399 .next()
8400 .await
8401 .unwrap();
8402 apply_additional_edits.await.unwrap();
8403 cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
8404}
8405
8406#[gpui::test]
8407async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
8408 init_test(cx, |_| {});
8409
8410 let mut cx = EditorLspTestContext::new(
8411 Language::new(
8412 LanguageConfig {
8413 matcher: LanguageMatcher {
8414 path_suffixes: vec!["jsx".into()],
8415 ..Default::default()
8416 },
8417 overrides: [(
8418 "element".into(),
8419 LanguageConfigOverride {
8420 word_characters: Override::Set(['-'].into_iter().collect()),
8421 ..Default::default()
8422 },
8423 )]
8424 .into_iter()
8425 .collect(),
8426 ..Default::default()
8427 },
8428 Some(tree_sitter_typescript::language_tsx()),
8429 )
8430 .with_override_query("(jsx_self_closing_element) @element")
8431 .unwrap(),
8432 lsp::ServerCapabilities {
8433 completion_provider: Some(lsp::CompletionOptions {
8434 trigger_characters: Some(vec![":".to_string()]),
8435 ..Default::default()
8436 }),
8437 ..Default::default()
8438 },
8439 cx,
8440 )
8441 .await;
8442
8443 cx.lsp
8444 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
8445 Ok(Some(lsp::CompletionResponse::Array(vec![
8446 lsp::CompletionItem {
8447 label: "bg-blue".into(),
8448 ..Default::default()
8449 },
8450 lsp::CompletionItem {
8451 label: "bg-red".into(),
8452 ..Default::default()
8453 },
8454 lsp::CompletionItem {
8455 label: "bg-yellow".into(),
8456 ..Default::default()
8457 },
8458 ])))
8459 });
8460
8461 cx.set_state(r#"<p class="bgˇ" />"#);
8462
8463 // Trigger completion when typing a dash, because the dash is an extra
8464 // word character in the 'element' scope, which contains the cursor.
8465 cx.simulate_keystroke("-");
8466 cx.executor().run_until_parked();
8467 cx.update_editor(|editor, _| {
8468 if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
8469 assert_eq!(
8470 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
8471 &["bg-red", "bg-blue", "bg-yellow"]
8472 );
8473 } else {
8474 panic!("expected completion menu to be open");
8475 }
8476 });
8477
8478 cx.simulate_keystroke("l");
8479 cx.executor().run_until_parked();
8480 cx.update_editor(|editor, _| {
8481 if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
8482 assert_eq!(
8483 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
8484 &["bg-blue", "bg-yellow"]
8485 );
8486 } else {
8487 panic!("expected completion menu to be open");
8488 }
8489 });
8490
8491 // When filtering completions, consider the character after the '-' to
8492 // be the start of a subword.
8493 cx.set_state(r#"<p class="yelˇ" />"#);
8494 cx.simulate_keystroke("l");
8495 cx.executor().run_until_parked();
8496 cx.update_editor(|editor, _| {
8497 if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
8498 assert_eq!(
8499 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
8500 &["bg-yellow"]
8501 );
8502 } else {
8503 panic!("expected completion menu to be open");
8504 }
8505 });
8506}
8507
8508#[gpui::test]
8509async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
8510 init_test(cx, |settings| {
8511 settings.defaults.formatter = Some(language_settings::Formatter::Prettier)
8512 });
8513
8514 let fs = FakeFs::new(cx.executor());
8515 fs.insert_file("/file.rs", Default::default()).await;
8516
8517 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
8518 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8519
8520 language_registry.add(Arc::new(Language::new(
8521 LanguageConfig {
8522 name: "Rust".into(),
8523 matcher: LanguageMatcher {
8524 path_suffixes: vec!["rs".to_string()],
8525 ..Default::default()
8526 },
8527 prettier_parser_name: Some("test_parser".to_string()),
8528 ..Default::default()
8529 },
8530 Some(tree_sitter_rust::language()),
8531 )));
8532
8533 let test_plugin = "test_plugin";
8534 let _ = language_registry.register_fake_lsp_adapter(
8535 "Rust",
8536 FakeLspAdapter {
8537 prettier_plugins: vec![test_plugin],
8538 ..Default::default()
8539 },
8540 );
8541
8542 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
8543 let buffer = project
8544 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
8545 .await
8546 .unwrap();
8547
8548 let buffer_text = "one\ntwo\nthree\n";
8549 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
8550 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
8551 _ = editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx));
8552
8553 editor
8554 .update(cx, |editor, cx| {
8555 editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
8556 })
8557 .unwrap()
8558 .await;
8559 assert_eq!(
8560 editor.update(cx, |editor, cx| editor.text(cx)),
8561 buffer_text.to_string() + prettier_format_suffix,
8562 "Test prettier formatting was not applied to the original buffer text",
8563 );
8564
8565 update_test_language_settings(cx, |settings| {
8566 settings.defaults.formatter = Some(language_settings::Formatter::Auto)
8567 });
8568 let format = editor.update(cx, |editor, cx| {
8569 editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
8570 });
8571 format.await.unwrap();
8572 assert_eq!(
8573 editor.update(cx, |editor, cx| editor.text(cx)),
8574 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
8575 "Autoformatting (via test prettier) was not applied to the original buffer text",
8576 );
8577}
8578
8579#[gpui::test]
8580async fn test_addition_reverts(cx: &mut gpui::TestAppContext) {
8581 init_test(cx, |_| {});
8582 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
8583 let base_text = indoc! {r#"struct Row;
8584struct Row1;
8585struct Row2;
8586
8587struct Row4;
8588struct Row5;
8589struct Row6;
8590
8591struct Row8;
8592struct Row9;
8593struct Row10;"#};
8594
8595 // When addition hunks are not adjacent to carets, no hunk revert is performed
8596 assert_hunk_revert(
8597 indoc! {r#"struct Row;
8598 struct Row1;
8599 struct Row1.1;
8600 struct Row1.2;
8601 struct Row2;ˇ
8602
8603 struct Row4;
8604 struct Row5;
8605 struct Row6;
8606
8607 struct Row8;
8608 ˇstruct Row9;
8609 struct Row9.1;
8610 struct Row9.2;
8611 struct Row9.3;
8612 struct Row10;"#},
8613 vec![DiffHunkStatus::Added, DiffHunkStatus::Added],
8614 indoc! {r#"struct Row;
8615 struct Row1;
8616 struct Row1.1;
8617 struct Row1.2;
8618 struct Row2;ˇ
8619
8620 struct Row4;
8621 struct Row5;
8622 struct Row6;
8623
8624 struct Row8;
8625 ˇstruct Row9;
8626 struct Row9.1;
8627 struct Row9.2;
8628 struct Row9.3;
8629 struct Row10;"#},
8630 base_text,
8631 &mut cx,
8632 );
8633 // Same for selections
8634 assert_hunk_revert(
8635 indoc! {r#"struct Row;
8636 struct Row1;
8637 struct Row2;
8638 struct Row2.1;
8639 struct Row2.2;
8640 «ˇ
8641 struct Row4;
8642 struct» Row5;
8643 «struct Row6;
8644 ˇ»
8645 struct Row9.1;
8646 struct Row9.2;
8647 struct Row9.3;
8648 struct Row8;
8649 struct Row9;
8650 struct Row10;"#},
8651 vec![DiffHunkStatus::Added, DiffHunkStatus::Added],
8652 indoc! {r#"struct Row;
8653 struct Row1;
8654 struct Row2;
8655 struct Row2.1;
8656 struct Row2.2;
8657 «ˇ
8658 struct Row4;
8659 struct» Row5;
8660 «struct Row6;
8661 ˇ»
8662 struct Row9.1;
8663 struct Row9.2;
8664 struct Row9.3;
8665 struct Row8;
8666 struct Row9;
8667 struct Row10;"#},
8668 base_text,
8669 &mut cx,
8670 );
8671
8672 // When carets and selections intersect the addition hunks, those are reverted.
8673 // Adjacent carets got merged.
8674 assert_hunk_revert(
8675 indoc! {r#"struct Row;
8676 ˇ// something on the top
8677 struct Row1;
8678 struct Row2;
8679 struct Roˇw3.1;
8680 struct Row2.2;
8681 struct Row2.3;ˇ
8682
8683 struct Row4;
8684 struct ˇRow5.1;
8685 struct Row5.2;
8686 struct «Rowˇ»5.3;
8687 struct Row5;
8688 struct Row6;
8689 ˇ
8690 struct Row9.1;
8691 struct «Rowˇ»9.2;
8692 struct «ˇRow»9.3;
8693 struct Row8;
8694 struct Row9;
8695 «ˇ// something on bottom»
8696 struct Row10;"#},
8697 vec![
8698 DiffHunkStatus::Added,
8699 DiffHunkStatus::Added,
8700 DiffHunkStatus::Added,
8701 DiffHunkStatus::Added,
8702 DiffHunkStatus::Added,
8703 ],
8704 indoc! {r#"struct Row;
8705 ˇstruct Row1;
8706 struct Row2;
8707 ˇ
8708 struct Row4;
8709 ˇstruct Row5;
8710 struct Row6;
8711 ˇ
8712 ˇstruct Row8;
8713 struct Row9;
8714 ˇstruct Row10;"#},
8715 base_text,
8716 &mut cx,
8717 );
8718}
8719
8720#[gpui::test]
8721async fn test_modification_reverts(cx: &mut gpui::TestAppContext) {
8722 init_test(cx, |_| {});
8723 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
8724 let base_text = indoc! {r#"struct Row;
8725struct Row1;
8726struct Row2;
8727
8728struct Row4;
8729struct Row5;
8730struct Row6;
8731
8732struct Row8;
8733struct Row9;
8734struct Row10;"#};
8735
8736 // Modification hunks behave the same as the addition ones.
8737 assert_hunk_revert(
8738 indoc! {r#"struct Row;
8739 struct Row1;
8740 struct Row33;
8741 ˇ
8742 struct Row4;
8743 struct Row5;
8744 struct Row6;
8745 ˇ
8746 struct Row99;
8747 struct Row9;
8748 struct Row10;"#},
8749 vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified],
8750 indoc! {r#"struct Row;
8751 struct Row1;
8752 struct Row33;
8753 ˇ
8754 struct Row4;
8755 struct Row5;
8756 struct Row6;
8757 ˇ
8758 struct Row99;
8759 struct Row9;
8760 struct Row10;"#},
8761 base_text,
8762 &mut cx,
8763 );
8764 assert_hunk_revert(
8765 indoc! {r#"struct Row;
8766 struct Row1;
8767 struct Row33;
8768 «ˇ
8769 struct Row4;
8770 struct» Row5;
8771 «struct Row6;
8772 ˇ»
8773 struct Row99;
8774 struct Row9;
8775 struct Row10;"#},
8776 vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified],
8777 indoc! {r#"struct Row;
8778 struct Row1;
8779 struct Row33;
8780 «ˇ
8781 struct Row4;
8782 struct» Row5;
8783 «struct Row6;
8784 ˇ»
8785 struct Row99;
8786 struct Row9;
8787 struct Row10;"#},
8788 base_text,
8789 &mut cx,
8790 );
8791
8792 assert_hunk_revert(
8793 indoc! {r#"ˇstruct Row1.1;
8794 struct Row1;
8795 «ˇstr»uct Row22;
8796
8797 struct ˇRow44;
8798 struct Row5;
8799 struct «Rˇ»ow66;ˇ
8800
8801 «struˇ»ct Row88;
8802 struct Row9;
8803 struct Row1011;ˇ"#},
8804 vec![
8805 DiffHunkStatus::Modified,
8806 DiffHunkStatus::Modified,
8807 DiffHunkStatus::Modified,
8808 DiffHunkStatus::Modified,
8809 DiffHunkStatus::Modified,
8810 DiffHunkStatus::Modified,
8811 ],
8812 indoc! {r#"struct Row;
8813 ˇstruct Row1;
8814 struct Row2;
8815 ˇ
8816 struct Row4;
8817 ˇstruct Row5;
8818 struct Row6;
8819 ˇ
8820 struct Row8;
8821 ˇstruct Row9;
8822 struct Row10;ˇ"#},
8823 base_text,
8824 &mut cx,
8825 );
8826}
8827
8828#[gpui::test]
8829async fn test_deletion_reverts(cx: &mut gpui::TestAppContext) {
8830 init_test(cx, |_| {});
8831 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
8832 let base_text = indoc! {r#"struct Row;
8833struct Row1;
8834struct Row2;
8835
8836struct Row4;
8837struct Row5;
8838struct Row6;
8839
8840struct Row8;
8841struct Row9;
8842struct Row10;"#};
8843
8844 // Deletion hunks trigger with carets on ajacent rows, so carets and selections have to stay farther to avoid the revert
8845 assert_hunk_revert(
8846 indoc! {r#"struct Row;
8847 struct Row2;
8848
8849 ˇstruct Row4;
8850 struct Row5;
8851 struct Row6;
8852 ˇ
8853 struct Row8;
8854 struct Row10;"#},
8855 vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
8856 indoc! {r#"struct Row;
8857 struct Row2;
8858
8859 ˇstruct Row4;
8860 struct Row5;
8861 struct Row6;
8862 ˇ
8863 struct Row8;
8864 struct Row10;"#},
8865 base_text,
8866 &mut cx,
8867 );
8868 assert_hunk_revert(
8869 indoc! {r#"struct Row;
8870 struct Row2;
8871
8872 «ˇstruct Row4;
8873 struct» Row5;
8874 «struct Row6;
8875 ˇ»
8876 struct Row8;
8877 struct Row10;"#},
8878 vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
8879 indoc! {r#"struct Row;
8880 struct Row2;
8881
8882 «ˇstruct Row4;
8883 struct» Row5;
8884 «struct Row6;
8885 ˇ»
8886 struct Row8;
8887 struct Row10;"#},
8888 base_text,
8889 &mut cx,
8890 );
8891
8892 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
8893 assert_hunk_revert(
8894 indoc! {r#"struct Row;
8895 ˇstruct Row2;
8896
8897 struct Row4;
8898 struct Row5;
8899 struct Row6;
8900
8901 struct Row8;ˇ
8902 struct Row10;"#},
8903 vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
8904 indoc! {r#"struct Row;
8905 struct Row1;
8906 ˇstruct Row2;
8907
8908 struct Row4;
8909 struct Row5;
8910 struct Row6;
8911
8912 struct Row8;ˇ
8913 struct Row9;
8914 struct Row10;"#},
8915 base_text,
8916 &mut cx,
8917 );
8918 assert_hunk_revert(
8919 indoc! {r#"struct Row;
8920 struct Row2«ˇ;
8921 struct Row4;
8922 struct» Row5;
8923 «struct Row6;
8924
8925 struct Row8;ˇ»
8926 struct Row10;"#},
8927 vec![
8928 DiffHunkStatus::Removed,
8929 DiffHunkStatus::Removed,
8930 DiffHunkStatus::Removed,
8931 ],
8932 indoc! {r#"struct Row;
8933 struct Row1;
8934 struct Row2«ˇ;
8935
8936 struct Row4;
8937 struct» Row5;
8938 «struct Row6;
8939
8940 struct Row8;ˇ»
8941 struct Row9;
8942 struct Row10;"#},
8943 base_text,
8944 &mut cx,
8945 );
8946}
8947
8948#[gpui::test]
8949async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
8950 init_test(cx, |_| {});
8951
8952 let cols = 4;
8953 let rows = 10;
8954 let sample_text_1 = sample_text(rows, cols, 'a');
8955 assert_eq!(
8956 sample_text_1,
8957 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
8958 );
8959 let sample_text_2 = sample_text(rows, cols, 'l');
8960 assert_eq!(
8961 sample_text_2,
8962 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
8963 );
8964 let sample_text_3 = sample_text(rows, cols, 'v');
8965 assert_eq!(
8966 sample_text_3,
8967 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
8968 );
8969
8970 fn diff_every_buffer_row(
8971 buffer: &Model<Buffer>,
8972 sample_text: String,
8973 cols: usize,
8974 cx: &mut gpui::TestAppContext,
8975 ) {
8976 // revert first character in each row, creating one large diff hunk per buffer
8977 let is_first_char = |offset: usize| offset % cols == 0;
8978 buffer.update(cx, |buffer, cx| {
8979 buffer.set_text(
8980 sample_text
8981 .chars()
8982 .enumerate()
8983 .map(|(offset, c)| if is_first_char(offset) { 'X' } else { c })
8984 .collect::<String>(),
8985 cx,
8986 );
8987 buffer.set_diff_base(Some(sample_text), cx);
8988 });
8989 cx.executor().run_until_parked();
8990 }
8991
8992 let buffer_1 = cx.new_model(|cx| {
8993 Buffer::new(
8994 0,
8995 BufferId::new(cx.entity_id().as_u64()).unwrap(),
8996 sample_text_1.clone(),
8997 )
8998 });
8999 diff_every_buffer_row(&buffer_1, sample_text_1.clone(), cols, cx);
9000
9001 let buffer_2 = cx.new_model(|cx| {
9002 Buffer::new(
9003 1,
9004 BufferId::new(cx.entity_id().as_u64() + 1).unwrap(),
9005 sample_text_2.clone(),
9006 )
9007 });
9008 diff_every_buffer_row(&buffer_2, sample_text_2.clone(), cols, cx);
9009
9010 let buffer_3 = cx.new_model(|cx| {
9011 Buffer::new(
9012 2,
9013 BufferId::new(cx.entity_id().as_u64() + 2).unwrap(),
9014 sample_text_3.clone(),
9015 )
9016 });
9017 diff_every_buffer_row(&buffer_3, sample_text_3.clone(), cols, cx);
9018
9019 let multibuffer = cx.new_model(|cx| {
9020 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
9021 multibuffer.push_excerpts(
9022 buffer_1.clone(),
9023 [
9024 ExcerptRange {
9025 context: Point::new(0, 0)..Point::new(3, 0),
9026 primary: None,
9027 },
9028 ExcerptRange {
9029 context: Point::new(5, 0)..Point::new(7, 0),
9030 primary: None,
9031 },
9032 ExcerptRange {
9033 context: Point::new(9, 0)..Point::new(10, 4),
9034 primary: None,
9035 },
9036 ],
9037 cx,
9038 );
9039 multibuffer.push_excerpts(
9040 buffer_2.clone(),
9041 [
9042 ExcerptRange {
9043 context: Point::new(0, 0)..Point::new(3, 0),
9044 primary: None,
9045 },
9046 ExcerptRange {
9047 context: Point::new(5, 0)..Point::new(7, 0),
9048 primary: None,
9049 },
9050 ExcerptRange {
9051 context: Point::new(9, 0)..Point::new(10, 4),
9052 primary: None,
9053 },
9054 ],
9055 cx,
9056 );
9057 multibuffer.push_excerpts(
9058 buffer_3.clone(),
9059 [
9060 ExcerptRange {
9061 context: Point::new(0, 0)..Point::new(3, 0),
9062 primary: None,
9063 },
9064 ExcerptRange {
9065 context: Point::new(5, 0)..Point::new(7, 0),
9066 primary: None,
9067 },
9068 ExcerptRange {
9069 context: Point::new(9, 0)..Point::new(10, 4),
9070 primary: None,
9071 },
9072 ],
9073 cx,
9074 );
9075 multibuffer
9076 });
9077
9078 let (editor, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
9079 editor.update(cx, |editor, cx| {
9080 assert_eq!(editor.text(cx), "XaaaXbbbX\nccXc\ndXdd\n\nhXhh\nXiiiXjjjX\n\nXlllXmmmX\nnnXn\noXoo\n\nsXss\nXtttXuuuX\n\nXvvvXwwwX\nxxXx\nyXyy\n\n}X}}\nX~~~X\u{7f}\u{7f}\u{7f}X\n");
9081 editor.select_all(&SelectAll, cx);
9082 editor.revert_selected_hunks(&RevertSelectedHunks, cx);
9083 });
9084 cx.executor().run_until_parked();
9085 // When all ranges are selected, all buffer hunks are reverted.
9086 editor.update(cx, |editor, cx| {
9087 assert_eq!(editor.text(cx), "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n\n\nllll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu\n\n\nvvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}\n\n");
9088 });
9089 buffer_1.update(cx, |buffer, _| {
9090 assert_eq!(buffer.text(), sample_text_1);
9091 });
9092 buffer_2.update(cx, |buffer, _| {
9093 assert_eq!(buffer.text(), sample_text_2);
9094 });
9095 buffer_3.update(cx, |buffer, _| {
9096 assert_eq!(buffer.text(), sample_text_3);
9097 });
9098
9099 diff_every_buffer_row(&buffer_1, sample_text_1.clone(), cols, cx);
9100 diff_every_buffer_row(&buffer_2, sample_text_2.clone(), cols, cx);
9101 diff_every_buffer_row(&buffer_3, sample_text_3.clone(), cols, cx);
9102 editor.update(cx, |editor, cx| {
9103 editor.change_selections(None, cx, |s| {
9104 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
9105 });
9106 editor.revert_selected_hunks(&RevertSelectedHunks, cx);
9107 });
9108 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
9109 // but not affect buffer_2 and its related excerpts.
9110 editor.update(cx, |editor, cx| {
9111 assert_eq!(
9112 editor.text(cx),
9113 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n\n\nXlllXmmmX\nnnXn\noXoo\nXpppXqqqX\nrrXr\nsXss\nXtttXuuuX\n\n\nXvvvXwwwX\nxxXx\nyXyy\nXzzzX{{{X\n||X|\n}X}}\nX~~~X\u{7f}\u{7f}\u{7f}X\n\n"
9114 );
9115 });
9116 buffer_1.update(cx, |buffer, _| {
9117 assert_eq!(buffer.text(), sample_text_1);
9118 });
9119 buffer_2.update(cx, |buffer, _| {
9120 assert_eq!(
9121 buffer.text(),
9122 "XlllXmmmX\nnnXn\noXoo\nXpppXqqqX\nrrXr\nsXss\nXtttXuuuX"
9123 );
9124 });
9125 buffer_3.update(cx, |buffer, _| {
9126 assert_eq!(
9127 buffer.text(),
9128 "XvvvXwwwX\nxxXx\nyXyy\nXzzzX{{{X\n||X|\n}X}}\nX~~~X\u{7f}\u{7f}\u{7f}X"
9129 );
9130 });
9131}
9132
9133#[gpui::test]
9134async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
9135 init_test(cx, |_| {});
9136
9137 let cols = 4;
9138 let rows = 10;
9139 let sample_text_1 = sample_text(rows, cols, 'a');
9140 assert_eq!(
9141 sample_text_1,
9142 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9143 );
9144 let sample_text_2 = sample_text(rows, cols, 'l');
9145 assert_eq!(
9146 sample_text_2,
9147 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9148 );
9149 let sample_text_3 = sample_text(rows, cols, 'v');
9150 assert_eq!(
9151 sample_text_3,
9152 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9153 );
9154
9155 let buffer_1 = cx.new_model(|cx| {
9156 Buffer::new(
9157 0,
9158 BufferId::new(cx.entity_id().as_u64()).unwrap(),
9159 sample_text_1.clone(),
9160 )
9161 });
9162
9163 let buffer_2 = cx.new_model(|cx| {
9164 Buffer::new(
9165 1,
9166 BufferId::new(cx.entity_id().as_u64() + 1).unwrap(),
9167 sample_text_2.clone(),
9168 )
9169 });
9170
9171 let buffer_3 = cx.new_model(|cx| {
9172 Buffer::new(
9173 2,
9174 BufferId::new(cx.entity_id().as_u64() + 2).unwrap(),
9175 sample_text_3.clone(),
9176 )
9177 });
9178
9179 let multi_buffer = cx.new_model(|cx| {
9180 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
9181 multibuffer.push_excerpts(
9182 buffer_1.clone(),
9183 [
9184 ExcerptRange {
9185 context: Point::new(0, 0)..Point::new(3, 0),
9186 primary: None,
9187 },
9188 ExcerptRange {
9189 context: Point::new(5, 0)..Point::new(7, 0),
9190 primary: None,
9191 },
9192 ExcerptRange {
9193 context: Point::new(9, 0)..Point::new(10, 4),
9194 primary: None,
9195 },
9196 ],
9197 cx,
9198 );
9199 multibuffer.push_excerpts(
9200 buffer_2.clone(),
9201 [
9202 ExcerptRange {
9203 context: Point::new(0, 0)..Point::new(3, 0),
9204 primary: None,
9205 },
9206 ExcerptRange {
9207 context: Point::new(5, 0)..Point::new(7, 0),
9208 primary: None,
9209 },
9210 ExcerptRange {
9211 context: Point::new(9, 0)..Point::new(10, 4),
9212 primary: None,
9213 },
9214 ],
9215 cx,
9216 );
9217 multibuffer.push_excerpts(
9218 buffer_3.clone(),
9219 [
9220 ExcerptRange {
9221 context: Point::new(0, 0)..Point::new(3, 0),
9222 primary: None,
9223 },
9224 ExcerptRange {
9225 context: Point::new(5, 0)..Point::new(7, 0),
9226 primary: None,
9227 },
9228 ExcerptRange {
9229 context: Point::new(9, 0)..Point::new(10, 4),
9230 primary: None,
9231 },
9232 ],
9233 cx,
9234 );
9235 multibuffer
9236 });
9237
9238 let fs = FakeFs::new(cx.executor());
9239 fs.insert_tree(
9240 "/a",
9241 json!({
9242 "main.rs": sample_text_1,
9243 "other.rs": sample_text_2,
9244 "lib.rs": sample_text_3,
9245 }),
9246 )
9247 .await;
9248 let project = Project::test(fs, ["/a".as_ref()], cx).await;
9249 let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
9250 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9251 let multi_buffer_editor =
9252 cx.new_view(|cx| Editor::new(EditorMode::Full, multi_buffer, Some(project.clone()), cx));
9253 let multibuffer_item_id = workspace
9254 .update(cx, |workspace, cx| {
9255 assert!(
9256 workspace.active_item(cx).is_none(),
9257 "active item should be None before the first item is added"
9258 );
9259 workspace.add_item_to_active_pane(Box::new(multi_buffer_editor.clone()), cx);
9260 let active_item = workspace
9261 .active_item(cx)
9262 .expect("should have an active item after adding the multi buffer");
9263 assert!(
9264 !active_item.is_singleton(cx),
9265 "A multi buffer was expected to active after adding"
9266 );
9267 active_item.item_id()
9268 })
9269 .unwrap();
9270 cx.executor().run_until_parked();
9271
9272 multi_buffer_editor.update(cx, |editor, cx| {
9273 editor.change_selections(Some(Autoscroll::Next), cx, |s| s.select_ranges(Some(1..2)));
9274 editor.open_excerpts(&OpenExcerpts, cx);
9275 });
9276 cx.executor().run_until_parked();
9277 let first_item_id = workspace
9278 .update(cx, |workspace, cx| {
9279 let active_item = workspace
9280 .active_item(cx)
9281 .expect("should have an active item after navigating into the 1st buffer");
9282 let first_item_id = active_item.item_id();
9283 assert_ne!(
9284 first_item_id, multibuffer_item_id,
9285 "Should navigate into the 1st buffer and activate it"
9286 );
9287 assert!(
9288 active_item.is_singleton(cx),
9289 "New active item should be a singleton buffer"
9290 );
9291 assert_eq!(
9292 active_item
9293 .act_as::<Editor>(cx)
9294 .expect("should have navigated into an editor for the 1st buffer")
9295 .read(cx)
9296 .text(cx),
9297 sample_text_1
9298 );
9299
9300 workspace
9301 .go_back(workspace.active_pane().downgrade(), cx)
9302 .detach_and_log_err(cx);
9303
9304 first_item_id
9305 })
9306 .unwrap();
9307 cx.executor().run_until_parked();
9308 workspace
9309 .update(cx, |workspace, cx| {
9310 let active_item = workspace
9311 .active_item(cx)
9312 .expect("should have an active item after navigating back");
9313 assert_eq!(
9314 active_item.item_id(),
9315 multibuffer_item_id,
9316 "Should navigate back to the multi buffer"
9317 );
9318 assert!(!active_item.is_singleton(cx));
9319 })
9320 .unwrap();
9321
9322 multi_buffer_editor.update(cx, |editor, cx| {
9323 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
9324 s.select_ranges(Some(39..40))
9325 });
9326 editor.open_excerpts(&OpenExcerpts, cx);
9327 });
9328 cx.executor().run_until_parked();
9329 let second_item_id = workspace
9330 .update(cx, |workspace, cx| {
9331 let active_item = workspace
9332 .active_item(cx)
9333 .expect("should have an active item after navigating into the 2nd buffer");
9334 let second_item_id = active_item.item_id();
9335 assert_ne!(
9336 second_item_id, multibuffer_item_id,
9337 "Should navigate away from the multibuffer"
9338 );
9339 assert_ne!(
9340 second_item_id, first_item_id,
9341 "Should navigate into the 2nd buffer and activate it"
9342 );
9343 assert!(
9344 active_item.is_singleton(cx),
9345 "New active item should be a singleton buffer"
9346 );
9347 assert_eq!(
9348 active_item
9349 .act_as::<Editor>(cx)
9350 .expect("should have navigated into an editor")
9351 .read(cx)
9352 .text(cx),
9353 sample_text_2
9354 );
9355
9356 workspace
9357 .go_back(workspace.active_pane().downgrade(), cx)
9358 .detach_and_log_err(cx);
9359
9360 second_item_id
9361 })
9362 .unwrap();
9363 cx.executor().run_until_parked();
9364 workspace
9365 .update(cx, |workspace, cx| {
9366 let active_item = workspace
9367 .active_item(cx)
9368 .expect("should have an active item after navigating back from the 2nd buffer");
9369 assert_eq!(
9370 active_item.item_id(),
9371 multibuffer_item_id,
9372 "Should navigate back from the 2nd buffer to the multi buffer"
9373 );
9374 assert!(!active_item.is_singleton(cx));
9375 })
9376 .unwrap();
9377
9378 multi_buffer_editor.update(cx, |editor, cx| {
9379 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
9380 s.select_ranges(Some(60..70))
9381 });
9382 editor.open_excerpts(&OpenExcerpts, cx);
9383 });
9384 cx.executor().run_until_parked();
9385 workspace
9386 .update(cx, |workspace, cx| {
9387 let active_item = workspace
9388 .active_item(cx)
9389 .expect("should have an active item after navigating into the 3rd buffer");
9390 let third_item_id = active_item.item_id();
9391 assert_ne!(
9392 third_item_id, multibuffer_item_id,
9393 "Should navigate into the 3rd buffer and activate it"
9394 );
9395 assert_ne!(third_item_id, first_item_id);
9396 assert_ne!(third_item_id, second_item_id);
9397 assert!(
9398 active_item.is_singleton(cx),
9399 "New active item should be a singleton buffer"
9400 );
9401 assert_eq!(
9402 active_item
9403 .act_as::<Editor>(cx)
9404 .expect("should have navigated into an editor")
9405 .read(cx)
9406 .text(cx),
9407 sample_text_3
9408 );
9409
9410 workspace
9411 .go_back(workspace.active_pane().downgrade(), cx)
9412 .detach_and_log_err(cx);
9413 })
9414 .unwrap();
9415 cx.executor().run_until_parked();
9416 workspace
9417 .update(cx, |workspace, cx| {
9418 let active_item = workspace
9419 .active_item(cx)
9420 .expect("should have an active item after navigating back from the 3rd buffer");
9421 assert_eq!(
9422 active_item.item_id(),
9423 multibuffer_item_id,
9424 "Should navigate back from the 3rd buffer to the multi buffer"
9425 );
9426 assert!(!active_item.is_singleton(cx));
9427 })
9428 .unwrap();
9429}
9430
9431fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
9432 let point = DisplayPoint::new(row as u32, column as u32);
9433 point..point
9434}
9435
9436fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewContext<Editor>) {
9437 let (text, ranges) = marked_text_ranges(marked_text, true);
9438 assert_eq!(view.text(cx), text);
9439 assert_eq!(
9440 view.selections.ranges(cx),
9441 ranges,
9442 "Assert selections are {}",
9443 marked_text
9444 );
9445}
9446
9447/// Handle completion request passing a marked string specifying where the completion
9448/// should be triggered from using '|' character, what range should be replaced, and what completions
9449/// should be returned using '<' and '>' to delimit the range
9450pub fn handle_completion_request(
9451 cx: &mut EditorLspTestContext,
9452 marked_string: &str,
9453 completions: Vec<&'static str>,
9454) -> impl Future<Output = ()> {
9455 let complete_from_marker: TextRangeMarker = '|'.into();
9456 let replace_range_marker: TextRangeMarker = ('<', '>').into();
9457 let (_, mut marked_ranges) = marked_text_ranges_by(
9458 marked_string,
9459 vec![complete_from_marker.clone(), replace_range_marker.clone()],
9460 );
9461
9462 let complete_from_position =
9463 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
9464 let replace_range =
9465 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
9466
9467 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
9468 let completions = completions.clone();
9469 async move {
9470 assert_eq!(params.text_document_position.text_document.uri, url.clone());
9471 assert_eq!(
9472 params.text_document_position.position,
9473 complete_from_position
9474 );
9475 Ok(Some(lsp::CompletionResponse::Array(
9476 completions
9477 .iter()
9478 .map(|completion_text| lsp::CompletionItem {
9479 label: completion_text.to_string(),
9480 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9481 range: replace_range,
9482 new_text: completion_text.to_string(),
9483 })),
9484 ..Default::default()
9485 })
9486 .collect(),
9487 )))
9488 }
9489 });
9490
9491 async move {
9492 request.next().await;
9493 }
9494}
9495
9496fn handle_resolve_completion_request(
9497 cx: &mut EditorLspTestContext,
9498 edits: Option<Vec<(&'static str, &'static str)>>,
9499) -> impl Future<Output = ()> {
9500 let edits = edits.map(|edits| {
9501 edits
9502 .iter()
9503 .map(|(marked_string, new_text)| {
9504 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
9505 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
9506 lsp::TextEdit::new(replace_range, new_text.to_string())
9507 })
9508 .collect::<Vec<_>>()
9509 });
9510
9511 let mut request =
9512 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
9513 let edits = edits.clone();
9514 async move {
9515 Ok(lsp::CompletionItem {
9516 additional_text_edits: edits,
9517 ..Default::default()
9518 })
9519 }
9520 });
9521
9522 async move {
9523 request.next().await;
9524 }
9525}
9526
9527pub(crate) fn update_test_language_settings(
9528 cx: &mut TestAppContext,
9529 f: impl Fn(&mut AllLanguageSettingsContent),
9530) {
9531 _ = cx.update(|cx| {
9532 cx.update_global(|store: &mut SettingsStore, cx| {
9533 store.update_user_settings::<AllLanguageSettings>(cx, f);
9534 });
9535 });
9536}
9537
9538pub(crate) fn update_test_project_settings(
9539 cx: &mut TestAppContext,
9540 f: impl Fn(&mut ProjectSettings),
9541) {
9542 _ = cx.update(|cx| {
9543 cx.update_global(|store: &mut SettingsStore, cx| {
9544 store.update_user_settings::<ProjectSettings>(cx, f);
9545 });
9546 });
9547}
9548
9549pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
9550 _ = cx.update(|cx| {
9551 let store = SettingsStore::test(cx);
9552 cx.set_global(store);
9553 theme::init(theme::LoadThemes::JustBase, cx);
9554 release_channel::init("0.0.0", cx);
9555 client::init_settings(cx);
9556 language::init(cx);
9557 Project::init_settings(cx);
9558 workspace::init_settings(cx);
9559 crate::init(cx);
9560 });
9561
9562 update_test_language_settings(cx, f);
9563}
9564
9565pub(crate) fn rust_lang() -> Arc<Language> {
9566 Arc::new(Language::new(
9567 LanguageConfig {
9568 name: "Rust".into(),
9569 matcher: LanguageMatcher {
9570 path_suffixes: vec!["rs".to_string()],
9571 ..Default::default()
9572 },
9573 ..Default::default()
9574 },
9575 Some(tree_sitter_rust::language()),
9576 ))
9577}
9578
9579#[track_caller]
9580fn assert_hunk_revert(
9581 not_reverted_text_with_selections: &str,
9582 expected_not_reverted_hunk_statuses: Vec<DiffHunkStatus>,
9583 expected_reverted_text_with_selections: &str,
9584 base_text: &str,
9585 cx: &mut EditorLspTestContext,
9586) {
9587 cx.set_state(not_reverted_text_with_selections);
9588 cx.update_editor(|editor, cx| {
9589 editor
9590 .buffer()
9591 .read(cx)
9592 .as_singleton()
9593 .unwrap()
9594 .update(cx, |buffer, cx| {
9595 buffer.set_diff_base(Some(base_text.to_string()), cx);
9596 });
9597 });
9598 cx.executor().run_until_parked();
9599
9600 let reverted_hunk_statuses = cx.update_editor(|editor, cx| {
9601 let snapshot = editor
9602 .buffer()
9603 .read(cx)
9604 .as_singleton()
9605 .unwrap()
9606 .read(cx)
9607 .snapshot();
9608 let reverted_hunk_statuses = snapshot
9609 .git_diff_hunks_in_row_range(0..u32::MAX)
9610 .map(|hunk| hunk.status())
9611 .collect::<Vec<_>>();
9612
9613 editor.revert_selected_hunks(&RevertSelectedHunks, cx);
9614 reverted_hunk_statuses
9615 });
9616 cx.executor().run_until_parked();
9617 cx.assert_editor_state(expected_reverted_text_with_selections);
9618 assert_eq!(reverted_hunk_statuses, expected_not_reverted_hunk_statuses);
9619}