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