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, WindowBounds, WindowOptions};
13use indoc::indoc;
14use language::{
15 language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
16 BracketPairConfig,
17 Capability::ReadWrite,
18 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry, 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
2790#[gpui::test]
2791async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
2792 init_test(cx, |_| {});
2793
2794 let mut cx = EditorTestContext::new(cx).await;
2795
2796 // Manipulate with multiple selections on a single line
2797 cx.set_state(indoc! {"
2798 dd«dd
2799 cˇ»c«c
2800 bb
2801 aaaˇ»aa
2802 "});
2803 cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
2804 cx.assert_editor_state(indoc! {"
2805 «aaaaa
2806 bb
2807 ccc
2808 ddddˇ»
2809 "});
2810
2811 // Manipulate with multiple disjoin selections
2812 cx.set_state(indoc! {"
2813 5«
2814 4
2815 3
2816 2
2817 1ˇ»
2818
2819 dd«dd
2820 ccc
2821 bb
2822 aaaˇ»aa
2823 "});
2824 cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
2825 cx.assert_editor_state(indoc! {"
2826 «1
2827 2
2828 3
2829 4
2830 5ˇ»
2831
2832 «aaaaa
2833 bb
2834 ccc
2835 ddddˇ»
2836 "});
2837}
2838
2839#[gpui::test]
2840async fn test_manipulate_text(cx: &mut TestAppContext) {
2841 init_test(cx, |_| {});
2842
2843 let mut cx = EditorTestContext::new(cx).await;
2844
2845 // Test convert_to_upper_case()
2846 cx.set_state(indoc! {"
2847 «hello worldˇ»
2848 "});
2849 cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
2850 cx.assert_editor_state(indoc! {"
2851 «HELLO WORLDˇ»
2852 "});
2853
2854 // Test convert_to_lower_case()
2855 cx.set_state(indoc! {"
2856 «HELLO WORLDˇ»
2857 "});
2858 cx.update_editor(|e, cx| e.convert_to_lower_case(&ConvertToLowerCase, cx));
2859 cx.assert_editor_state(indoc! {"
2860 «hello worldˇ»
2861 "});
2862
2863 // Test multiple line, single selection case
2864 // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
2865 cx.set_state(indoc! {"
2866 «The quick brown
2867 fox jumps over
2868 the lazy dogˇ»
2869 "});
2870 cx.update_editor(|e, cx| e.convert_to_title_case(&ConvertToTitleCase, cx));
2871 cx.assert_editor_state(indoc! {"
2872 «The Quick Brown
2873 Fox Jumps Over
2874 The Lazy Dogˇ»
2875 "});
2876
2877 // Test multiple line, single selection case
2878 // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
2879 cx.set_state(indoc! {"
2880 «The quick brown
2881 fox jumps over
2882 the lazy dogˇ»
2883 "});
2884 cx.update_editor(|e, cx| e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, cx));
2885 cx.assert_editor_state(indoc! {"
2886 «TheQuickBrown
2887 FoxJumpsOver
2888 TheLazyDogˇ»
2889 "});
2890
2891 // From here on out, test more complex cases of manipulate_text()
2892
2893 // Test no selection case - should affect words cursors are in
2894 // Cursor at beginning, middle, and end of word
2895 cx.set_state(indoc! {"
2896 ˇhello big beauˇtiful worldˇ
2897 "});
2898 cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
2899 cx.assert_editor_state(indoc! {"
2900 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
2901 "});
2902
2903 // Test multiple selections on a single line and across multiple lines
2904 cx.set_state(indoc! {"
2905 «Theˇ» quick «brown
2906 foxˇ» jumps «overˇ»
2907 the «lazyˇ» dog
2908 "});
2909 cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
2910 cx.assert_editor_state(indoc! {"
2911 «THEˇ» quick «BROWN
2912 FOXˇ» jumps «OVERˇ»
2913 the «LAZYˇ» dog
2914 "});
2915
2916 // Test case where text length grows
2917 cx.set_state(indoc! {"
2918 «tschüߡ»
2919 "});
2920 cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
2921 cx.assert_editor_state(indoc! {"
2922 «TSCHÜSSˇ»
2923 "});
2924
2925 // Test to make sure we don't crash when text shrinks
2926 cx.set_state(indoc! {"
2927 aaa_bbbˇ
2928 "});
2929 cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
2930 cx.assert_editor_state(indoc! {"
2931 «aaaBbbˇ»
2932 "});
2933
2934 // Test to make sure we all aware of the fact that each word can grow and shrink
2935 // Final selections should be aware of this fact
2936 cx.set_state(indoc! {"
2937 aaa_bˇbb bbˇb_ccc ˇccc_ddd
2938 "});
2939 cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
2940 cx.assert_editor_state(indoc! {"
2941 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
2942 "});
2943}
2944
2945#[gpui::test]
2946fn test_duplicate_line(cx: &mut TestAppContext) {
2947 init_test(cx, |_| {});
2948
2949 let view = cx.add_window(|cx| {
2950 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
2951 build_editor(buffer, cx)
2952 });
2953 _ = view.update(cx, |view, cx| {
2954 view.change_selections(None, cx, |s| {
2955 s.select_display_ranges([
2956 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
2957 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
2958 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
2959 DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
2960 ])
2961 });
2962 view.duplicate_line(&DuplicateLine, cx);
2963 assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
2964 assert_eq!(
2965 view.selections.display_ranges(cx),
2966 vec![
2967 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
2968 DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
2969 DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
2970 DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0),
2971 ]
2972 );
2973 });
2974
2975 let view = cx.add_window(|cx| {
2976 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
2977 build_editor(buffer, cx)
2978 });
2979 _ = view.update(cx, |view, cx| {
2980 view.change_selections(None, cx, |s| {
2981 s.select_display_ranges([
2982 DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1),
2983 DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1),
2984 ])
2985 });
2986 view.duplicate_line(&DuplicateLine, cx);
2987 assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
2988 assert_eq!(
2989 view.selections.display_ranges(cx),
2990 vec![
2991 DisplayPoint::new(3, 1)..DisplayPoint::new(4, 1),
2992 DisplayPoint::new(4, 2)..DisplayPoint::new(5, 1),
2993 ]
2994 );
2995 });
2996}
2997
2998#[gpui::test]
2999fn test_move_line_up_down(cx: &mut TestAppContext) {
3000 init_test(cx, |_| {});
3001
3002 let view = cx.add_window(|cx| {
3003 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
3004 build_editor(buffer, cx)
3005 });
3006 _ = view.update(cx, |view, cx| {
3007 view.fold_ranges(
3008 vec![
3009 Point::new(0, 2)..Point::new(1, 2),
3010 Point::new(2, 3)..Point::new(4, 1),
3011 Point::new(7, 0)..Point::new(8, 4),
3012 ],
3013 true,
3014 cx,
3015 );
3016 view.change_selections(None, cx, |s| {
3017 s.select_display_ranges([
3018 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
3019 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
3020 DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
3021 DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2),
3022 ])
3023 });
3024 assert_eq!(
3025 view.display_text(cx),
3026 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
3027 );
3028
3029 view.move_line_up(&MoveLineUp, cx);
3030 assert_eq!(
3031 view.display_text(cx),
3032 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
3033 );
3034 assert_eq!(
3035 view.selections.display_ranges(cx),
3036 vec![
3037 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
3038 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
3039 DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3),
3040 DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2)
3041 ]
3042 );
3043 });
3044
3045 _ = view.update(cx, |view, cx| {
3046 view.move_line_down(&MoveLineDown, cx);
3047 assert_eq!(
3048 view.display_text(cx),
3049 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
3050 );
3051 assert_eq!(
3052 view.selections.display_ranges(cx),
3053 vec![
3054 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
3055 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
3056 DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
3057 DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2)
3058 ]
3059 );
3060 });
3061
3062 _ = view.update(cx, |view, cx| {
3063 view.move_line_down(&MoveLineDown, cx);
3064 assert_eq!(
3065 view.display_text(cx),
3066 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
3067 );
3068 assert_eq!(
3069 view.selections.display_ranges(cx),
3070 vec![
3071 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
3072 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
3073 DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
3074 DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2)
3075 ]
3076 );
3077 });
3078
3079 _ = view.update(cx, |view, cx| {
3080 view.move_line_up(&MoveLineUp, cx);
3081 assert_eq!(
3082 view.display_text(cx),
3083 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
3084 );
3085 assert_eq!(
3086 view.selections.display_ranges(cx),
3087 vec![
3088 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
3089 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
3090 DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3),
3091 DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2)
3092 ]
3093 );
3094 });
3095}
3096
3097#[gpui::test]
3098fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
3099 init_test(cx, |_| {});
3100
3101 let editor = cx.add_window(|cx| {
3102 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
3103 build_editor(buffer, cx)
3104 });
3105 _ = editor.update(cx, |editor, cx| {
3106 let snapshot = editor.buffer.read(cx).snapshot(cx);
3107 editor.insert_blocks(
3108 [BlockProperties {
3109 style: BlockStyle::Fixed,
3110 position: snapshot.anchor_after(Point::new(2, 0)),
3111 disposition: BlockDisposition::Below,
3112 height: 1,
3113 render: Arc::new(|_| div().into_any()),
3114 }],
3115 Some(Autoscroll::fit()),
3116 cx,
3117 );
3118 editor.change_selections(None, cx, |s| {
3119 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
3120 });
3121 editor.move_line_down(&MoveLineDown, cx);
3122 });
3123}
3124
3125#[gpui::test]
3126fn test_transpose(cx: &mut TestAppContext) {
3127 init_test(cx, |_| {});
3128
3129 _ = cx.add_window(|cx| {
3130 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx);
3131 editor.set_style(EditorStyle::default(), cx);
3132 editor.change_selections(None, cx, |s| s.select_ranges([1..1]));
3133 editor.transpose(&Default::default(), cx);
3134 assert_eq!(editor.text(cx), "bac");
3135 assert_eq!(editor.selections.ranges(cx), [2..2]);
3136
3137 editor.transpose(&Default::default(), cx);
3138 assert_eq!(editor.text(cx), "bca");
3139 assert_eq!(editor.selections.ranges(cx), [3..3]);
3140
3141 editor.transpose(&Default::default(), cx);
3142 assert_eq!(editor.text(cx), "bac");
3143 assert_eq!(editor.selections.ranges(cx), [3..3]);
3144
3145 editor
3146 });
3147
3148 _ = cx.add_window(|cx| {
3149 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
3150 editor.set_style(EditorStyle::default(), cx);
3151 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
3152 editor.transpose(&Default::default(), cx);
3153 assert_eq!(editor.text(cx), "acb\nde");
3154 assert_eq!(editor.selections.ranges(cx), [3..3]);
3155
3156 editor.change_selections(None, cx, |s| s.select_ranges([4..4]));
3157 editor.transpose(&Default::default(), cx);
3158 assert_eq!(editor.text(cx), "acbd\ne");
3159 assert_eq!(editor.selections.ranges(cx), [5..5]);
3160
3161 editor.transpose(&Default::default(), cx);
3162 assert_eq!(editor.text(cx), "acbde\n");
3163 assert_eq!(editor.selections.ranges(cx), [6..6]);
3164
3165 editor.transpose(&Default::default(), cx);
3166 assert_eq!(editor.text(cx), "acbd\ne");
3167 assert_eq!(editor.selections.ranges(cx), [6..6]);
3168
3169 editor
3170 });
3171
3172 _ = cx.add_window(|cx| {
3173 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
3174 editor.set_style(EditorStyle::default(), cx);
3175 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
3176 editor.transpose(&Default::default(), cx);
3177 assert_eq!(editor.text(cx), "bacd\ne");
3178 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
3179
3180 editor.transpose(&Default::default(), cx);
3181 assert_eq!(editor.text(cx), "bcade\n");
3182 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
3183
3184 editor.transpose(&Default::default(), cx);
3185 assert_eq!(editor.text(cx), "bcda\ne");
3186 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
3187
3188 editor.transpose(&Default::default(), cx);
3189 assert_eq!(editor.text(cx), "bcade\n");
3190 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
3191
3192 editor.transpose(&Default::default(), cx);
3193 assert_eq!(editor.text(cx), "bcaed\n");
3194 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
3195
3196 editor
3197 });
3198
3199 _ = cx.add_window(|cx| {
3200 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx);
3201 editor.set_style(EditorStyle::default(), cx);
3202 editor.change_selections(None, cx, |s| s.select_ranges([4..4]));
3203 editor.transpose(&Default::default(), cx);
3204 assert_eq!(editor.text(cx), "🏀🍐✋");
3205 assert_eq!(editor.selections.ranges(cx), [8..8]);
3206
3207 editor.transpose(&Default::default(), cx);
3208 assert_eq!(editor.text(cx), "🏀✋🍐");
3209 assert_eq!(editor.selections.ranges(cx), [11..11]);
3210
3211 editor.transpose(&Default::default(), cx);
3212 assert_eq!(editor.text(cx), "🏀🍐✋");
3213 assert_eq!(editor.selections.ranges(cx), [11..11]);
3214
3215 editor
3216 });
3217}
3218
3219#[gpui::test]
3220async fn test_clipboard(cx: &mut gpui::TestAppContext) {
3221 init_test(cx, |_| {});
3222
3223 let mut cx = EditorTestContext::new(cx).await;
3224
3225 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
3226 cx.update_editor(|e, cx| e.cut(&Cut, cx));
3227 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
3228
3229 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
3230 cx.set_state("two ˇfour ˇsix ˇ");
3231 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3232 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
3233
3234 // Paste again but with only two cursors. Since the number of cursors doesn't
3235 // match the number of slices in the clipboard, the entire clipboard text
3236 // is pasted at each cursor.
3237 cx.set_state("ˇtwo one✅ four three six five ˇ");
3238 cx.update_editor(|e, cx| {
3239 e.handle_input("( ", cx);
3240 e.paste(&Paste, cx);
3241 e.handle_input(") ", cx);
3242 });
3243 cx.assert_editor_state(
3244 &([
3245 "( one✅ ",
3246 "three ",
3247 "five ) ˇtwo one✅ four three six five ( one✅ ",
3248 "three ",
3249 "five ) ˇ",
3250 ]
3251 .join("\n")),
3252 );
3253
3254 // Cut with three selections, one of which is full-line.
3255 cx.set_state(indoc! {"
3256 1«2ˇ»3
3257 4ˇ567
3258 «8ˇ»9"});
3259 cx.update_editor(|e, cx| e.cut(&Cut, cx));
3260 cx.assert_editor_state(indoc! {"
3261 1ˇ3
3262 ˇ9"});
3263
3264 // Paste with three selections, noticing how the copied selection that was full-line
3265 // gets inserted before the second cursor.
3266 cx.set_state(indoc! {"
3267 1ˇ3
3268 9ˇ
3269 «oˇ»ne"});
3270 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3271 cx.assert_editor_state(indoc! {"
3272 12ˇ3
3273 4567
3274 9ˇ
3275 8ˇne"});
3276
3277 // Copy with a single cursor only, which writes the whole line into the clipboard.
3278 cx.set_state(indoc! {"
3279 The quick brown
3280 fox juˇmps over
3281 the lazy dog"});
3282 cx.update_editor(|e, cx| e.copy(&Copy, cx));
3283 assert_eq!(
3284 cx.read_from_clipboard().map(|item| item.text().to_owned()),
3285 Some("fox jumps over\n".to_owned())
3286 );
3287
3288 // Paste with three selections, noticing how the copied full-line selection is inserted
3289 // before the empty selections but replaces the selection that is non-empty.
3290 cx.set_state(indoc! {"
3291 Tˇhe quick brown
3292 «foˇ»x jumps over
3293 tˇhe lazy dog"});
3294 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3295 cx.assert_editor_state(indoc! {"
3296 fox jumps over
3297 Tˇhe quick brown
3298 fox jumps over
3299 ˇx jumps over
3300 fox jumps over
3301 tˇhe lazy dog"});
3302}
3303
3304#[gpui::test]
3305async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
3306 init_test(cx, |_| {});
3307
3308 let mut cx = EditorTestContext::new(cx).await;
3309 let language = Arc::new(Language::new(
3310 LanguageConfig::default(),
3311 Some(tree_sitter_rust::language()),
3312 ));
3313 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3314
3315 // Cut an indented block, without the leading whitespace.
3316 cx.set_state(indoc! {"
3317 const a: B = (
3318 c(),
3319 «d(
3320 e,
3321 f
3322 )ˇ»
3323 );
3324 "});
3325 cx.update_editor(|e, cx| e.cut(&Cut, cx));
3326 cx.assert_editor_state(indoc! {"
3327 const a: B = (
3328 c(),
3329 ˇ
3330 );
3331 "});
3332
3333 // Paste it at the same position.
3334 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3335 cx.assert_editor_state(indoc! {"
3336 const a: B = (
3337 c(),
3338 d(
3339 e,
3340 f
3341 )ˇ
3342 );
3343 "});
3344
3345 // Paste it at a line with a lower indent level.
3346 cx.set_state(indoc! {"
3347 ˇ
3348 const a: B = (
3349 c(),
3350 );
3351 "});
3352 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3353 cx.assert_editor_state(indoc! {"
3354 d(
3355 e,
3356 f
3357 )ˇ
3358 const a: B = (
3359 c(),
3360 );
3361 "});
3362
3363 // Cut an indented block, with the leading whitespace.
3364 cx.set_state(indoc! {"
3365 const a: B = (
3366 c(),
3367 « d(
3368 e,
3369 f
3370 )
3371 ˇ»);
3372 "});
3373 cx.update_editor(|e, cx| e.cut(&Cut, cx));
3374 cx.assert_editor_state(indoc! {"
3375 const a: B = (
3376 c(),
3377 ˇ);
3378 "});
3379
3380 // Paste it at the same position.
3381 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3382 cx.assert_editor_state(indoc! {"
3383 const a: B = (
3384 c(),
3385 d(
3386 e,
3387 f
3388 )
3389 ˇ);
3390 "});
3391
3392 // Paste it at a line with a higher indent level.
3393 cx.set_state(indoc! {"
3394 const a: B = (
3395 c(),
3396 d(
3397 e,
3398 fˇ
3399 )
3400 );
3401 "});
3402 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3403 cx.assert_editor_state(indoc! {"
3404 const a: B = (
3405 c(),
3406 d(
3407 e,
3408 f d(
3409 e,
3410 f
3411 )
3412 ˇ
3413 )
3414 );
3415 "});
3416}
3417
3418#[gpui::test]
3419fn test_select_all(cx: &mut TestAppContext) {
3420 init_test(cx, |_| {});
3421
3422 let view = cx.add_window(|cx| {
3423 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
3424 build_editor(buffer, cx)
3425 });
3426 _ = view.update(cx, |view, cx| {
3427 view.select_all(&SelectAll, cx);
3428 assert_eq!(
3429 view.selections.display_ranges(cx),
3430 &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)]
3431 );
3432 });
3433}
3434
3435#[gpui::test]
3436fn test_select_line(cx: &mut TestAppContext) {
3437 init_test(cx, |_| {});
3438
3439 let view = cx.add_window(|cx| {
3440 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
3441 build_editor(buffer, cx)
3442 });
3443 _ = view.update(cx, |view, cx| {
3444 view.change_selections(None, cx, |s| {
3445 s.select_display_ranges([
3446 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
3447 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3448 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
3449 DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2),
3450 ])
3451 });
3452 view.select_line(&SelectLine, cx);
3453 assert_eq!(
3454 view.selections.display_ranges(cx),
3455 vec![
3456 DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0),
3457 DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0),
3458 ]
3459 );
3460 });
3461
3462 _ = view.update(cx, |view, cx| {
3463 view.select_line(&SelectLine, cx);
3464 assert_eq!(
3465 view.selections.display_ranges(cx),
3466 vec![
3467 DisplayPoint::new(0, 0)..DisplayPoint::new(3, 0),
3468 DisplayPoint::new(4, 0)..DisplayPoint::new(5, 5),
3469 ]
3470 );
3471 });
3472
3473 _ = view.update(cx, |view, cx| {
3474 view.select_line(&SelectLine, cx);
3475 assert_eq!(
3476 view.selections.display_ranges(cx),
3477 vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)]
3478 );
3479 });
3480}
3481
3482#[gpui::test]
3483fn test_split_selection_into_lines(cx: &mut TestAppContext) {
3484 init_test(cx, |_| {});
3485
3486 let view = cx.add_window(|cx| {
3487 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
3488 build_editor(buffer, cx)
3489 });
3490 _ = view.update(cx, |view, cx| {
3491 view.fold_ranges(
3492 vec![
3493 Point::new(0, 2)..Point::new(1, 2),
3494 Point::new(2, 3)..Point::new(4, 1),
3495 Point::new(7, 0)..Point::new(8, 4),
3496 ],
3497 true,
3498 cx,
3499 );
3500 view.change_selections(None, cx, |s| {
3501 s.select_display_ranges([
3502 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
3503 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3504 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
3505 DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4),
3506 ])
3507 });
3508 assert_eq!(view.display_text(cx), "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i");
3509 });
3510
3511 _ = view.update(cx, |view, cx| {
3512 view.split_selection_into_lines(&SplitSelectionIntoLines, cx);
3513 assert_eq!(
3514 view.display_text(cx),
3515 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
3516 );
3517 assert_eq!(
3518 view.selections.display_ranges(cx),
3519 [
3520 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
3521 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3522 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0),
3523 DisplayPoint::new(5, 4)..DisplayPoint::new(5, 4)
3524 ]
3525 );
3526 });
3527
3528 _ = view.update(cx, |view, cx| {
3529 view.change_selections(None, cx, |s| {
3530 s.select_display_ranges([DisplayPoint::new(5, 0)..DisplayPoint::new(0, 1)])
3531 });
3532 view.split_selection_into_lines(&SplitSelectionIntoLines, cx);
3533 assert_eq!(
3534 view.display_text(cx),
3535 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
3536 );
3537 assert_eq!(
3538 view.selections.display_ranges(cx),
3539 [
3540 DisplayPoint::new(0, 5)..DisplayPoint::new(0, 5),
3541 DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5),
3542 DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
3543 DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5),
3544 DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5),
3545 DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5),
3546 DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5),
3547 DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0)
3548 ]
3549 );
3550 });
3551}
3552
3553#[gpui::test]
3554async fn test_add_selection_above_below(cx: &mut TestAppContext) {
3555 init_test(cx, |_| {});
3556
3557 let mut cx = EditorTestContext::new(cx).await;
3558
3559 // let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx);
3560 cx.set_state(indoc!(
3561 r#"abc
3562 defˇghi
3563
3564 jk
3565 nlmo
3566 "#
3567 ));
3568
3569 cx.update_editor(|editor, cx| {
3570 editor.add_selection_above(&Default::default(), cx);
3571 });
3572
3573 cx.assert_editor_state(indoc!(
3574 r#"abcˇ
3575 defˇghi
3576
3577 jk
3578 nlmo
3579 "#
3580 ));
3581
3582 cx.update_editor(|editor, cx| {
3583 editor.add_selection_above(&Default::default(), cx);
3584 });
3585
3586 cx.assert_editor_state(indoc!(
3587 r#"abcˇ
3588 defˇghi
3589
3590 jk
3591 nlmo
3592 "#
3593 ));
3594
3595 cx.update_editor(|view, cx| {
3596 view.add_selection_below(&Default::default(), cx);
3597 });
3598
3599 cx.assert_editor_state(indoc!(
3600 r#"abc
3601 defˇghi
3602
3603 jk
3604 nlmo
3605 "#
3606 ));
3607
3608 cx.update_editor(|view, cx| {
3609 view.undo_selection(&Default::default(), cx);
3610 });
3611
3612 cx.assert_editor_state(indoc!(
3613 r#"abcˇ
3614 defˇghi
3615
3616 jk
3617 nlmo
3618 "#
3619 ));
3620
3621 cx.update_editor(|view, cx| {
3622 view.redo_selection(&Default::default(), cx);
3623 });
3624
3625 cx.assert_editor_state(indoc!(
3626 r#"abc
3627 defˇghi
3628
3629 jk
3630 nlmo
3631 "#
3632 ));
3633
3634 cx.update_editor(|view, cx| {
3635 view.add_selection_below(&Default::default(), cx);
3636 });
3637
3638 cx.assert_editor_state(indoc!(
3639 r#"abc
3640 defˇghi
3641
3642 jk
3643 nlmˇo
3644 "#
3645 ));
3646
3647 cx.update_editor(|view, cx| {
3648 view.add_selection_below(&Default::default(), cx);
3649 });
3650
3651 cx.assert_editor_state(indoc!(
3652 r#"abc
3653 defˇghi
3654
3655 jk
3656 nlmˇo
3657 "#
3658 ));
3659
3660 // change selections
3661 cx.set_state(indoc!(
3662 r#"abc
3663 def«ˇg»hi
3664
3665 jk
3666 nlmo
3667 "#
3668 ));
3669
3670 cx.update_editor(|view, cx| {
3671 view.add_selection_below(&Default::default(), cx);
3672 });
3673
3674 cx.assert_editor_state(indoc!(
3675 r#"abc
3676 def«ˇg»hi
3677
3678 jk
3679 nlm«ˇo»
3680 "#
3681 ));
3682
3683 cx.update_editor(|view, cx| {
3684 view.add_selection_below(&Default::default(), cx);
3685 });
3686
3687 cx.assert_editor_state(indoc!(
3688 r#"abc
3689 def«ˇg»hi
3690
3691 jk
3692 nlm«ˇo»
3693 "#
3694 ));
3695
3696 cx.update_editor(|view, cx| {
3697 view.add_selection_above(&Default::default(), cx);
3698 });
3699
3700 cx.assert_editor_state(indoc!(
3701 r#"abc
3702 def«ˇg»hi
3703
3704 jk
3705 nlmo
3706 "#
3707 ));
3708
3709 cx.update_editor(|view, cx| {
3710 view.add_selection_above(&Default::default(), cx);
3711 });
3712
3713 cx.assert_editor_state(indoc!(
3714 r#"abc
3715 def«ˇg»hi
3716
3717 jk
3718 nlmo
3719 "#
3720 ));
3721
3722 // Change selections again
3723 cx.set_state(indoc!(
3724 r#"a«bc
3725 defgˇ»hi
3726
3727 jk
3728 nlmo
3729 "#
3730 ));
3731
3732 cx.update_editor(|view, cx| {
3733 view.add_selection_below(&Default::default(), cx);
3734 });
3735
3736 cx.assert_editor_state(indoc!(
3737 r#"a«bcˇ»
3738 d«efgˇ»hi
3739
3740 j«kˇ»
3741 nlmo
3742 "#
3743 ));
3744
3745 cx.update_editor(|view, cx| {
3746 view.add_selection_below(&Default::default(), cx);
3747 });
3748 cx.assert_editor_state(indoc!(
3749 r#"a«bcˇ»
3750 d«efgˇ»hi
3751
3752 j«kˇ»
3753 n«lmoˇ»
3754 "#
3755 ));
3756 cx.update_editor(|view, cx| {
3757 view.add_selection_above(&Default::default(), cx);
3758 });
3759
3760 cx.assert_editor_state(indoc!(
3761 r#"a«bcˇ»
3762 d«efgˇ»hi
3763
3764 j«kˇ»
3765 nlmo
3766 "#
3767 ));
3768
3769 // Change selections again
3770 cx.set_state(indoc!(
3771 r#"abc
3772 d«ˇefghi
3773
3774 jk
3775 nlm»o
3776 "#
3777 ));
3778
3779 cx.update_editor(|view, cx| {
3780 view.add_selection_above(&Default::default(), cx);
3781 });
3782
3783 cx.assert_editor_state(indoc!(
3784 r#"a«ˇbc»
3785 d«ˇef»ghi
3786
3787 j«ˇk»
3788 n«ˇlm»o
3789 "#
3790 ));
3791
3792 cx.update_editor(|view, cx| {
3793 view.add_selection_below(&Default::default(), cx);
3794 });
3795
3796 cx.assert_editor_state(indoc!(
3797 r#"abc
3798 d«ˇef»ghi
3799
3800 j«ˇk»
3801 n«ˇlm»o
3802 "#
3803 ));
3804}
3805
3806#[gpui::test]
3807async fn test_select_next(cx: &mut gpui::TestAppContext) {
3808 init_test(cx, |_| {});
3809
3810 let mut cx = EditorTestContext::new(cx).await;
3811 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
3812
3813 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
3814 .unwrap();
3815 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
3816
3817 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
3818 .unwrap();
3819 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
3820
3821 cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
3822 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
3823
3824 cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
3825 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
3826
3827 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
3828 .unwrap();
3829 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
3830
3831 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
3832 .unwrap();
3833 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
3834}
3835
3836#[gpui::test]
3837async fn test_select_all_matches(cx: &mut gpui::TestAppContext) {
3838 init_test(cx, |_| {});
3839
3840 let mut cx = EditorTestContext::new(cx).await;
3841 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
3842
3843 cx.update_editor(|e, cx| e.select_all_matches(&SelectAllMatches::default(), cx))
3844 .unwrap();
3845 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
3846}
3847
3848#[gpui::test]
3849async fn test_select_next_with_multiple_carets(cx: &mut gpui::TestAppContext) {
3850 init_test(cx, |_| {});
3851
3852 let mut cx = EditorTestContext::new(cx).await;
3853 cx.set_state(
3854 r#"let foo = 2;
3855lˇet foo = 2;
3856let fooˇ = 2;
3857let foo = 2;
3858let foo = ˇ2;"#,
3859 );
3860
3861 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
3862 .unwrap();
3863 cx.assert_editor_state(
3864 r#"let foo = 2;
3865«letˇ» foo = 2;
3866let «fooˇ» = 2;
3867let foo = 2;
3868let foo = «2ˇ»;"#,
3869 );
3870
3871 // noop for multiple selections with different contents
3872 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
3873 .unwrap();
3874 cx.assert_editor_state(
3875 r#"let foo = 2;
3876«letˇ» foo = 2;
3877let «fooˇ» = 2;
3878let foo = 2;
3879let foo = «2ˇ»;"#,
3880 );
3881}
3882
3883#[gpui::test]
3884async fn test_select_previous_with_single_caret(cx: &mut gpui::TestAppContext) {
3885 init_test(cx, |_| {});
3886
3887 let mut cx = EditorTestContext::new(cx).await;
3888 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
3889
3890 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
3891 .unwrap();
3892 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
3893
3894 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
3895 .unwrap();
3896 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
3897
3898 cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
3899 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
3900
3901 cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
3902 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
3903
3904 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
3905 .unwrap();
3906 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
3907
3908 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
3909 .unwrap();
3910 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
3911
3912 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
3913 .unwrap();
3914 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
3915}
3916
3917#[gpui::test]
3918async fn test_select_previous_with_multiple_carets(cx: &mut gpui::TestAppContext) {
3919 init_test(cx, |_| {});
3920
3921 let mut cx = EditorTestContext::new(cx).await;
3922 cx.set_state(
3923 r#"let foo = 2;
3924lˇet foo = 2;
3925let fooˇ = 2;
3926let foo = 2;
3927let foo = ˇ2;"#,
3928 );
3929
3930 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
3931 .unwrap();
3932 cx.assert_editor_state(
3933 r#"let foo = 2;
3934«letˇ» foo = 2;
3935let «fooˇ» = 2;
3936let foo = 2;
3937let foo = «2ˇ»;"#,
3938 );
3939
3940 // noop for multiple selections with different contents
3941 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
3942 .unwrap();
3943 cx.assert_editor_state(
3944 r#"let foo = 2;
3945«letˇ» foo = 2;
3946let «fooˇ» = 2;
3947let foo = 2;
3948let foo = «2ˇ»;"#,
3949 );
3950}
3951
3952#[gpui::test]
3953async fn test_select_previous_with_single_selection(cx: &mut gpui::TestAppContext) {
3954 init_test(cx, |_| {});
3955
3956 let mut cx = EditorTestContext::new(cx).await;
3957 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
3958
3959 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
3960 .unwrap();
3961 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
3962
3963 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
3964 .unwrap();
3965 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
3966
3967 cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
3968 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
3969
3970 cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
3971 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
3972
3973 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
3974 .unwrap();
3975 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
3976
3977 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
3978 .unwrap();
3979 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
3980}
3981
3982#[gpui::test]
3983async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
3984 init_test(cx, |_| {});
3985
3986 let language = Arc::new(Language::new(
3987 LanguageConfig::default(),
3988 Some(tree_sitter_rust::language()),
3989 ));
3990
3991 let text = r#"
3992 use mod1::mod2::{mod3, mod4};
3993
3994 fn fn_1(param1: bool, param2: &str) {
3995 let var1 = "text";
3996 }
3997 "#
3998 .unindent();
3999
4000 let buffer = cx.new_model(|cx| {
4001 Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text)
4002 .with_language(language, cx)
4003 });
4004 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
4005 let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
4006
4007 view.condition::<crate::EditorEvent>(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
4008 .await;
4009
4010 _ = view.update(cx, |view, cx| {
4011 view.change_selections(None, cx, |s| {
4012 s.select_display_ranges([
4013 DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
4014 DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
4015 DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
4016 ]);
4017 });
4018 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
4019 });
4020 assert_eq!(
4021 view.update(cx, |view, cx| { view.selections.display_ranges(cx) }),
4022 &[
4023 DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
4024 DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
4025 DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21),
4026 ]
4027 );
4028
4029 _ = view.update(cx, |view, cx| {
4030 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
4031 });
4032 assert_eq!(
4033 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4034 &[
4035 DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
4036 DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
4037 ]
4038 );
4039
4040 _ = view.update(cx, |view, cx| {
4041 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
4042 });
4043 assert_eq!(
4044 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4045 &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
4046 );
4047
4048 // Trying to expand the selected syntax node one more time has no effect.
4049 _ = view.update(cx, |view, cx| {
4050 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
4051 });
4052 assert_eq!(
4053 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4054 &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
4055 );
4056
4057 _ = view.update(cx, |view, cx| {
4058 view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
4059 });
4060 assert_eq!(
4061 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4062 &[
4063 DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
4064 DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
4065 ]
4066 );
4067
4068 _ = view.update(cx, |view, cx| {
4069 view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
4070 });
4071 assert_eq!(
4072 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4073 &[
4074 DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
4075 DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
4076 DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21),
4077 ]
4078 );
4079
4080 _ = view.update(cx, |view, cx| {
4081 view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
4082 });
4083 assert_eq!(
4084 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4085 &[
4086 DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
4087 DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
4088 DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
4089 ]
4090 );
4091
4092 // Trying to shrink the selected syntax node one more time has no effect.
4093 _ = view.update(cx, |view, cx| {
4094 view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
4095 });
4096 assert_eq!(
4097 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4098 &[
4099 DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
4100 DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
4101 DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
4102 ]
4103 );
4104
4105 // Ensure that we keep expanding the selection if the larger selection starts or ends within
4106 // a fold.
4107 _ = view.update(cx, |view, cx| {
4108 view.fold_ranges(
4109 vec![
4110 Point::new(0, 21)..Point::new(0, 24),
4111 Point::new(3, 20)..Point::new(3, 22),
4112 ],
4113 true,
4114 cx,
4115 );
4116 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
4117 });
4118 assert_eq!(
4119 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4120 &[
4121 DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
4122 DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
4123 DisplayPoint::new(3, 4)..DisplayPoint::new(3, 23),
4124 ]
4125 );
4126}
4127
4128#[gpui::test]
4129async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
4130 init_test(cx, |_| {});
4131
4132 let language = Arc::new(
4133 Language::new(
4134 LanguageConfig {
4135 brackets: BracketPairConfig {
4136 pairs: vec![
4137 BracketPair {
4138 start: "{".to_string(),
4139 end: "}".to_string(),
4140 close: false,
4141 newline: true,
4142 },
4143 BracketPair {
4144 start: "(".to_string(),
4145 end: ")".to_string(),
4146 close: false,
4147 newline: true,
4148 },
4149 ],
4150 ..Default::default()
4151 },
4152 ..Default::default()
4153 },
4154 Some(tree_sitter_rust::language()),
4155 )
4156 .with_indents_query(
4157 r#"
4158 (_ "(" ")" @end) @indent
4159 (_ "{" "}" @end) @indent
4160 "#,
4161 )
4162 .unwrap(),
4163 );
4164
4165 let text = "fn a() {}";
4166
4167 let buffer = cx.new_model(|cx| {
4168 Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text)
4169 .with_language(language, cx)
4170 });
4171 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
4172 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
4173 editor
4174 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
4175 .await;
4176
4177 _ = editor.update(cx, |editor, cx| {
4178 editor.change_selections(None, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
4179 editor.newline(&Newline, cx);
4180 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
4181 assert_eq!(
4182 editor.selections.ranges(cx),
4183 &[
4184 Point::new(1, 4)..Point::new(1, 4),
4185 Point::new(3, 4)..Point::new(3, 4),
4186 Point::new(5, 0)..Point::new(5, 0)
4187 ]
4188 );
4189 });
4190}
4191
4192#[gpui::test]
4193async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
4194 init_test(cx, |_| {});
4195
4196 let mut cx = EditorTestContext::new(cx).await;
4197
4198 let language = Arc::new(Language::new(
4199 LanguageConfig {
4200 brackets: BracketPairConfig {
4201 pairs: vec![
4202 BracketPair {
4203 start: "{".to_string(),
4204 end: "}".to_string(),
4205 close: true,
4206 newline: true,
4207 },
4208 BracketPair {
4209 start: "(".to_string(),
4210 end: ")".to_string(),
4211 close: true,
4212 newline: true,
4213 },
4214 BracketPair {
4215 start: "/*".to_string(),
4216 end: " */".to_string(),
4217 close: true,
4218 newline: true,
4219 },
4220 BracketPair {
4221 start: "[".to_string(),
4222 end: "]".to_string(),
4223 close: false,
4224 newline: true,
4225 },
4226 BracketPair {
4227 start: "\"".to_string(),
4228 end: "\"".to_string(),
4229 close: true,
4230 newline: false,
4231 },
4232 ],
4233 ..Default::default()
4234 },
4235 autoclose_before: "})]".to_string(),
4236 ..Default::default()
4237 },
4238 Some(tree_sitter_rust::language()),
4239 ));
4240
4241 let registry = Arc::new(LanguageRegistry::test());
4242 registry.add(language.clone());
4243 cx.update_buffer(|buffer, cx| {
4244 buffer.set_language_registry(registry);
4245 buffer.set_language(Some(language), cx);
4246 });
4247
4248 cx.set_state(
4249 &r#"
4250 🏀ˇ
4251 εˇ
4252 ❤️ˇ
4253 "#
4254 .unindent(),
4255 );
4256
4257 // autoclose multiple nested brackets at multiple cursors
4258 cx.update_editor(|view, cx| {
4259 view.handle_input("{", cx);
4260 view.handle_input("{", cx);
4261 view.handle_input("{", cx);
4262 });
4263 cx.assert_editor_state(
4264 &"
4265 🏀{{{ˇ}}}
4266 ε{{{ˇ}}}
4267 ❤️{{{ˇ}}}
4268 "
4269 .unindent(),
4270 );
4271
4272 // insert a different closing bracket
4273 cx.update_editor(|view, cx| {
4274 view.handle_input(")", cx);
4275 });
4276 cx.assert_editor_state(
4277 &"
4278 🏀{{{)ˇ}}}
4279 ε{{{)ˇ}}}
4280 ❤️{{{)ˇ}}}
4281 "
4282 .unindent(),
4283 );
4284
4285 // skip over the auto-closed brackets when typing a closing bracket
4286 cx.update_editor(|view, cx| {
4287 view.move_right(&MoveRight, cx);
4288 view.handle_input("}", cx);
4289 view.handle_input("}", cx);
4290 view.handle_input("}", cx);
4291 });
4292 cx.assert_editor_state(
4293 &"
4294 🏀{{{)}}}}ˇ
4295 ε{{{)}}}}ˇ
4296 ❤️{{{)}}}}ˇ
4297 "
4298 .unindent(),
4299 );
4300
4301 // autoclose multi-character pairs
4302 cx.set_state(
4303 &"
4304 ˇ
4305 ˇ
4306 "
4307 .unindent(),
4308 );
4309 cx.update_editor(|view, cx| {
4310 view.handle_input("/", cx);
4311 view.handle_input("*", cx);
4312 });
4313 cx.assert_editor_state(
4314 &"
4315 /*ˇ */
4316 /*ˇ */
4317 "
4318 .unindent(),
4319 );
4320
4321 // one cursor autocloses a multi-character pair, one cursor
4322 // does not autoclose.
4323 cx.set_state(
4324 &"
4325 /ˇ
4326 ˇ
4327 "
4328 .unindent(),
4329 );
4330 cx.update_editor(|view, cx| view.handle_input("*", cx));
4331 cx.assert_editor_state(
4332 &"
4333 /*ˇ */
4334 *ˇ
4335 "
4336 .unindent(),
4337 );
4338
4339 // Don't autoclose if the next character isn't whitespace and isn't
4340 // listed in the language's "autoclose_before" section.
4341 cx.set_state("ˇa b");
4342 cx.update_editor(|view, cx| view.handle_input("{", cx));
4343 cx.assert_editor_state("{ˇa b");
4344
4345 // Don't autoclose if `close` is false for the bracket pair
4346 cx.set_state("ˇ");
4347 cx.update_editor(|view, cx| view.handle_input("[", cx));
4348 cx.assert_editor_state("[ˇ");
4349
4350 // Surround with brackets if text is selected
4351 cx.set_state("«aˇ» b");
4352 cx.update_editor(|view, cx| view.handle_input("{", cx));
4353 cx.assert_editor_state("{«aˇ»} b");
4354
4355 // Autclose pair where the start and end characters are the same
4356 cx.set_state("aˇ");
4357 cx.update_editor(|view, cx| view.handle_input("\"", cx));
4358 cx.assert_editor_state("a\"ˇ\"");
4359 cx.update_editor(|view, cx| view.handle_input("\"", cx));
4360 cx.assert_editor_state("a\"\"ˇ");
4361}
4362
4363#[gpui::test]
4364async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
4365 init_test(cx, |_| {});
4366
4367 let mut cx = EditorTestContext::new(cx).await;
4368
4369 let html_language = Arc::new(
4370 Language::new(
4371 LanguageConfig {
4372 name: "HTML".into(),
4373 brackets: BracketPairConfig {
4374 pairs: vec![
4375 BracketPair {
4376 start: "<".into(),
4377 end: ">".into(),
4378 close: true,
4379 ..Default::default()
4380 },
4381 BracketPair {
4382 start: "{".into(),
4383 end: "}".into(),
4384 close: true,
4385 ..Default::default()
4386 },
4387 BracketPair {
4388 start: "(".into(),
4389 end: ")".into(),
4390 close: true,
4391 ..Default::default()
4392 },
4393 ],
4394 ..Default::default()
4395 },
4396 autoclose_before: "})]>".into(),
4397 ..Default::default()
4398 },
4399 Some(tree_sitter_html::language()),
4400 )
4401 .with_injection_query(
4402 r#"
4403 (script_element
4404 (raw_text) @content
4405 (#set! "language" "javascript"))
4406 "#,
4407 )
4408 .unwrap(),
4409 );
4410
4411 let javascript_language = Arc::new(Language::new(
4412 LanguageConfig {
4413 name: "JavaScript".into(),
4414 brackets: BracketPairConfig {
4415 pairs: vec![
4416 BracketPair {
4417 start: "/*".into(),
4418 end: " */".into(),
4419 close: true,
4420 ..Default::default()
4421 },
4422 BracketPair {
4423 start: "{".into(),
4424 end: "}".into(),
4425 close: true,
4426 ..Default::default()
4427 },
4428 BracketPair {
4429 start: "(".into(),
4430 end: ")".into(),
4431 close: true,
4432 ..Default::default()
4433 },
4434 ],
4435 ..Default::default()
4436 },
4437 autoclose_before: "})]>".into(),
4438 ..Default::default()
4439 },
4440 Some(tree_sitter_typescript::language_tsx()),
4441 ));
4442
4443 let registry = Arc::new(LanguageRegistry::test());
4444 registry.add(html_language.clone());
4445 registry.add(javascript_language.clone());
4446
4447 cx.update_buffer(|buffer, cx| {
4448 buffer.set_language_registry(registry);
4449 buffer.set_language(Some(html_language), cx);
4450 });
4451
4452 cx.set_state(
4453 &r#"
4454 <body>ˇ
4455 <script>
4456 var x = 1;ˇ
4457 </script>
4458 </body>ˇ
4459 "#
4460 .unindent(),
4461 );
4462
4463 // Precondition: different languages are active at different locations.
4464 cx.update_editor(|editor, cx| {
4465 let snapshot = editor.snapshot(cx);
4466 let cursors = editor.selections.ranges::<usize>(cx);
4467 let languages = cursors
4468 .iter()
4469 .map(|c| snapshot.language_at(c.start).unwrap().name())
4470 .collect::<Vec<_>>();
4471 assert_eq!(
4472 languages,
4473 &["HTML".into(), "JavaScript".into(), "HTML".into()]
4474 );
4475 });
4476
4477 // Angle brackets autoclose in HTML, but not JavaScript.
4478 cx.update_editor(|editor, cx| {
4479 editor.handle_input("<", cx);
4480 editor.handle_input("a", cx);
4481 });
4482 cx.assert_editor_state(
4483 &r#"
4484 <body><aˇ>
4485 <script>
4486 var x = 1;<aˇ
4487 </script>
4488 </body><aˇ>
4489 "#
4490 .unindent(),
4491 );
4492
4493 // Curly braces and parens autoclose in both HTML and JavaScript.
4494 cx.update_editor(|editor, cx| {
4495 editor.handle_input(" b=", cx);
4496 editor.handle_input("{", cx);
4497 editor.handle_input("c", cx);
4498 editor.handle_input("(", cx);
4499 });
4500 cx.assert_editor_state(
4501 &r#"
4502 <body><a b={c(ˇ)}>
4503 <script>
4504 var x = 1;<a b={c(ˇ)}
4505 </script>
4506 </body><a b={c(ˇ)}>
4507 "#
4508 .unindent(),
4509 );
4510
4511 // Brackets that were already autoclosed are skipped.
4512 cx.update_editor(|editor, cx| {
4513 editor.handle_input(")", cx);
4514 editor.handle_input("d", cx);
4515 editor.handle_input("}", cx);
4516 });
4517 cx.assert_editor_state(
4518 &r#"
4519 <body><a b={c()d}ˇ>
4520 <script>
4521 var x = 1;<a b={c()d}ˇ
4522 </script>
4523 </body><a b={c()d}ˇ>
4524 "#
4525 .unindent(),
4526 );
4527 cx.update_editor(|editor, cx| {
4528 editor.handle_input(">", cx);
4529 });
4530 cx.assert_editor_state(
4531 &r#"
4532 <body><a b={c()d}>ˇ
4533 <script>
4534 var x = 1;<a b={c()d}>ˇ
4535 </script>
4536 </body><a b={c()d}>ˇ
4537 "#
4538 .unindent(),
4539 );
4540
4541 // Reset
4542 cx.set_state(
4543 &r#"
4544 <body>ˇ
4545 <script>
4546 var x = 1;ˇ
4547 </script>
4548 </body>ˇ
4549 "#
4550 .unindent(),
4551 );
4552
4553 cx.update_editor(|editor, cx| {
4554 editor.handle_input("<", cx);
4555 });
4556 cx.assert_editor_state(
4557 &r#"
4558 <body><ˇ>
4559 <script>
4560 var x = 1;<ˇ
4561 </script>
4562 </body><ˇ>
4563 "#
4564 .unindent(),
4565 );
4566
4567 // When backspacing, the closing angle brackets are removed.
4568 cx.update_editor(|editor, cx| {
4569 editor.backspace(&Backspace, cx);
4570 });
4571 cx.assert_editor_state(
4572 &r#"
4573 <body>ˇ
4574 <script>
4575 var x = 1;ˇ
4576 </script>
4577 </body>ˇ
4578 "#
4579 .unindent(),
4580 );
4581
4582 // Block comments autoclose in JavaScript, but not HTML.
4583 cx.update_editor(|editor, cx| {
4584 editor.handle_input("/", cx);
4585 editor.handle_input("*", cx);
4586 });
4587 cx.assert_editor_state(
4588 &r#"
4589 <body>/*ˇ
4590 <script>
4591 var x = 1;/*ˇ */
4592 </script>
4593 </body>/*ˇ
4594 "#
4595 .unindent(),
4596 );
4597}
4598
4599#[gpui::test]
4600async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
4601 init_test(cx, |_| {});
4602
4603 let mut cx = EditorTestContext::new(cx).await;
4604
4605 let rust_language = Arc::new(
4606 Language::new(
4607 LanguageConfig {
4608 name: "Rust".into(),
4609 brackets: serde_json::from_value(json!([
4610 { "start": "{", "end": "}", "close": true, "newline": true },
4611 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
4612 ]))
4613 .unwrap(),
4614 autoclose_before: "})]>".into(),
4615 ..Default::default()
4616 },
4617 Some(tree_sitter_rust::language()),
4618 )
4619 .with_override_query("(string_literal) @string")
4620 .unwrap(),
4621 );
4622
4623 let registry = Arc::new(LanguageRegistry::test());
4624 registry.add(rust_language.clone());
4625
4626 cx.update_buffer(|buffer, cx| {
4627 buffer.set_language_registry(registry);
4628 buffer.set_language(Some(rust_language), cx);
4629 });
4630
4631 cx.set_state(
4632 &r#"
4633 let x = ˇ
4634 "#
4635 .unindent(),
4636 );
4637
4638 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
4639 cx.update_editor(|editor, cx| {
4640 editor.handle_input("\"", cx);
4641 });
4642 cx.assert_editor_state(
4643 &r#"
4644 let x = "ˇ"
4645 "#
4646 .unindent(),
4647 );
4648
4649 // Inserting another quotation mark. The cursor moves across the existing
4650 // automatically-inserted quotation mark.
4651 cx.update_editor(|editor, cx| {
4652 editor.handle_input("\"", cx);
4653 });
4654 cx.assert_editor_state(
4655 &r#"
4656 let x = ""ˇ
4657 "#
4658 .unindent(),
4659 );
4660
4661 // Reset
4662 cx.set_state(
4663 &r#"
4664 let x = ˇ
4665 "#
4666 .unindent(),
4667 );
4668
4669 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
4670 cx.update_editor(|editor, cx| {
4671 editor.handle_input("\"", cx);
4672 editor.handle_input(" ", cx);
4673 editor.move_left(&Default::default(), cx);
4674 editor.handle_input("\\", cx);
4675 editor.handle_input("\"", cx);
4676 });
4677 cx.assert_editor_state(
4678 &r#"
4679 let x = "\"ˇ "
4680 "#
4681 .unindent(),
4682 );
4683
4684 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
4685 // mark. Nothing is inserted.
4686 cx.update_editor(|editor, cx| {
4687 editor.move_right(&Default::default(), cx);
4688 editor.handle_input("\"", cx);
4689 });
4690 cx.assert_editor_state(
4691 &r#"
4692 let x = "\" "ˇ
4693 "#
4694 .unindent(),
4695 );
4696}
4697
4698#[gpui::test]
4699async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
4700 init_test(cx, |_| {});
4701
4702 let language = Arc::new(Language::new(
4703 LanguageConfig {
4704 brackets: BracketPairConfig {
4705 pairs: vec![
4706 BracketPair {
4707 start: "{".to_string(),
4708 end: "}".to_string(),
4709 close: true,
4710 newline: true,
4711 },
4712 BracketPair {
4713 start: "/* ".to_string(),
4714 end: "*/".to_string(),
4715 close: true,
4716 ..Default::default()
4717 },
4718 ],
4719 ..Default::default()
4720 },
4721 ..Default::default()
4722 },
4723 Some(tree_sitter_rust::language()),
4724 ));
4725
4726 let text = r#"
4727 a
4728 b
4729 c
4730 "#
4731 .unindent();
4732
4733 let buffer = cx.new_model(|cx| {
4734 Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text)
4735 .with_language(language, cx)
4736 });
4737 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
4738 let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
4739 view.condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
4740 .await;
4741
4742 _ = view.update(cx, |view, cx| {
4743 view.change_selections(None, cx, |s| {
4744 s.select_display_ranges([
4745 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
4746 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
4747 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1),
4748 ])
4749 });
4750
4751 view.handle_input("{", cx);
4752 view.handle_input("{", cx);
4753 view.handle_input("{", cx);
4754 assert_eq!(
4755 view.text(cx),
4756 "
4757 {{{a}}}
4758 {{{b}}}
4759 {{{c}}}
4760 "
4761 .unindent()
4762 );
4763 assert_eq!(
4764 view.selections.display_ranges(cx),
4765 [
4766 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 4),
4767 DisplayPoint::new(1, 3)..DisplayPoint::new(1, 4),
4768 DisplayPoint::new(2, 3)..DisplayPoint::new(2, 4)
4769 ]
4770 );
4771
4772 view.undo(&Undo, cx);
4773 view.undo(&Undo, cx);
4774 view.undo(&Undo, cx);
4775 assert_eq!(
4776 view.text(cx),
4777 "
4778 a
4779 b
4780 c
4781 "
4782 .unindent()
4783 );
4784 assert_eq!(
4785 view.selections.display_ranges(cx),
4786 [
4787 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
4788 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
4789 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1)
4790 ]
4791 );
4792
4793 // Ensure inserting the first character of a multi-byte bracket pair
4794 // doesn't surround the selections with the bracket.
4795 view.handle_input("/", cx);
4796 assert_eq!(
4797 view.text(cx),
4798 "
4799 /
4800 /
4801 /
4802 "
4803 .unindent()
4804 );
4805 assert_eq!(
4806 view.selections.display_ranges(cx),
4807 [
4808 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
4809 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
4810 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1)
4811 ]
4812 );
4813
4814 view.undo(&Undo, cx);
4815 assert_eq!(
4816 view.text(cx),
4817 "
4818 a
4819 b
4820 c
4821 "
4822 .unindent()
4823 );
4824 assert_eq!(
4825 view.selections.display_ranges(cx),
4826 [
4827 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
4828 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
4829 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1)
4830 ]
4831 );
4832
4833 // Ensure inserting the last character of a multi-byte bracket pair
4834 // doesn't surround the selections with the bracket.
4835 view.handle_input("*", cx);
4836 assert_eq!(
4837 view.text(cx),
4838 "
4839 *
4840 *
4841 *
4842 "
4843 .unindent()
4844 );
4845 assert_eq!(
4846 view.selections.display_ranges(cx),
4847 [
4848 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
4849 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
4850 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1)
4851 ]
4852 );
4853 });
4854}
4855
4856#[gpui::test]
4857async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
4858 init_test(cx, |_| {});
4859
4860 let language = Arc::new(Language::new(
4861 LanguageConfig {
4862 brackets: BracketPairConfig {
4863 pairs: vec![BracketPair {
4864 start: "{".to_string(),
4865 end: "}".to_string(),
4866 close: true,
4867 newline: true,
4868 }],
4869 ..Default::default()
4870 },
4871 autoclose_before: "}".to_string(),
4872 ..Default::default()
4873 },
4874 Some(tree_sitter_rust::language()),
4875 ));
4876
4877 let text = r#"
4878 a
4879 b
4880 c
4881 "#
4882 .unindent();
4883
4884 let buffer = cx.new_model(|cx| {
4885 Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text)
4886 .with_language(language, cx)
4887 });
4888 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
4889 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
4890 editor
4891 .condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
4892 .await;
4893
4894 _ = editor.update(cx, |editor, cx| {
4895 editor.change_selections(None, cx, |s| {
4896 s.select_ranges([
4897 Point::new(0, 1)..Point::new(0, 1),
4898 Point::new(1, 1)..Point::new(1, 1),
4899 Point::new(2, 1)..Point::new(2, 1),
4900 ])
4901 });
4902
4903 editor.handle_input("{", cx);
4904 editor.handle_input("{", cx);
4905 editor.handle_input("_", cx);
4906 assert_eq!(
4907 editor.text(cx),
4908 "
4909 a{{_}}
4910 b{{_}}
4911 c{{_}}
4912 "
4913 .unindent()
4914 );
4915 assert_eq!(
4916 editor.selections.ranges::<Point>(cx),
4917 [
4918 Point::new(0, 4)..Point::new(0, 4),
4919 Point::new(1, 4)..Point::new(1, 4),
4920 Point::new(2, 4)..Point::new(2, 4)
4921 ]
4922 );
4923
4924 editor.backspace(&Default::default(), cx);
4925 editor.backspace(&Default::default(), cx);
4926 assert_eq!(
4927 editor.text(cx),
4928 "
4929 a{}
4930 b{}
4931 c{}
4932 "
4933 .unindent()
4934 );
4935 assert_eq!(
4936 editor.selections.ranges::<Point>(cx),
4937 [
4938 Point::new(0, 2)..Point::new(0, 2),
4939 Point::new(1, 2)..Point::new(1, 2),
4940 Point::new(2, 2)..Point::new(2, 2)
4941 ]
4942 );
4943
4944 editor.delete_to_previous_word_start(&Default::default(), cx);
4945 assert_eq!(
4946 editor.text(cx),
4947 "
4948 a
4949 b
4950 c
4951 "
4952 .unindent()
4953 );
4954 assert_eq!(
4955 editor.selections.ranges::<Point>(cx),
4956 [
4957 Point::new(0, 1)..Point::new(0, 1),
4958 Point::new(1, 1)..Point::new(1, 1),
4959 Point::new(2, 1)..Point::new(2, 1)
4960 ]
4961 );
4962 });
4963}
4964
4965#[gpui::test]
4966async fn test_snippets(cx: &mut gpui::TestAppContext) {
4967 init_test(cx, |_| {});
4968
4969 let (text, insertion_ranges) = marked_text_ranges(
4970 indoc! {"
4971 a.ˇ b
4972 a.ˇ b
4973 a.ˇ b
4974 "},
4975 false,
4976 );
4977
4978 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
4979 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
4980
4981 _ = editor.update(cx, |editor, cx| {
4982 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
4983
4984 editor
4985 .insert_snippet(&insertion_ranges, snippet, cx)
4986 .unwrap();
4987
4988 fn assert(editor: &mut Editor, cx: &mut ViewContext<Editor>, marked_text: &str) {
4989 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
4990 assert_eq!(editor.text(cx), expected_text);
4991 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
4992 }
4993
4994 assert(
4995 editor,
4996 cx,
4997 indoc! {"
4998 a.f(«one», two, «three») b
4999 a.f(«one», two, «three») b
5000 a.f(«one», two, «three») b
5001 "},
5002 );
5003
5004 // Can't move earlier than the first tab stop
5005 assert!(!editor.move_to_prev_snippet_tabstop(cx));
5006 assert(
5007 editor,
5008 cx,
5009 indoc! {"
5010 a.f(«one», two, «three») b
5011 a.f(«one», two, «three») b
5012 a.f(«one», two, «three») b
5013 "},
5014 );
5015
5016 assert!(editor.move_to_next_snippet_tabstop(cx));
5017 assert(
5018 editor,
5019 cx,
5020 indoc! {"
5021 a.f(one, «two», three) b
5022 a.f(one, «two», three) b
5023 a.f(one, «two», three) b
5024 "},
5025 );
5026
5027 editor.move_to_prev_snippet_tabstop(cx);
5028 assert(
5029 editor,
5030 cx,
5031 indoc! {"
5032 a.f(«one», two, «three») b
5033 a.f(«one», two, «three») b
5034 a.f(«one», two, «three») b
5035 "},
5036 );
5037
5038 assert!(editor.move_to_next_snippet_tabstop(cx));
5039 assert(
5040 editor,
5041 cx,
5042 indoc! {"
5043 a.f(one, «two», three) b
5044 a.f(one, «two», three) b
5045 a.f(one, «two», three) b
5046 "},
5047 );
5048 assert!(editor.move_to_next_snippet_tabstop(cx));
5049 assert(
5050 editor,
5051 cx,
5052 indoc! {"
5053 a.f(one, two, three)ˇ b
5054 a.f(one, two, three)ˇ b
5055 a.f(one, two, three)ˇ b
5056 "},
5057 );
5058
5059 // As soon as the last tab stop is reached, snippet state is gone
5060 editor.move_to_prev_snippet_tabstop(cx);
5061 assert(
5062 editor,
5063 cx,
5064 indoc! {"
5065 a.f(one, two, three)ˇ b
5066 a.f(one, two, three)ˇ b
5067 a.f(one, two, three)ˇ b
5068 "},
5069 );
5070 });
5071}
5072
5073#[gpui::test]
5074async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
5075 init_test(cx, |_| {});
5076
5077 let mut language = Language::new(
5078 LanguageConfig {
5079 name: "Rust".into(),
5080 path_suffixes: vec!["rs".to_string()],
5081 ..Default::default()
5082 },
5083 Some(tree_sitter_rust::language()),
5084 );
5085 let mut fake_servers = language
5086 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
5087 capabilities: lsp::ServerCapabilities {
5088 document_formatting_provider: Some(lsp::OneOf::Left(true)),
5089 ..Default::default()
5090 },
5091 ..Default::default()
5092 }))
5093 .await;
5094
5095 let fs = FakeFs::new(cx.executor());
5096 fs.insert_file("/file.rs", Default::default()).await;
5097
5098 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
5099 _ = project.update(cx, |project, _| project.languages().add(Arc::new(language)));
5100 let buffer = project
5101 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
5102 .await
5103 .unwrap();
5104
5105 cx.executor().start_waiting();
5106 let fake_server = fake_servers.next().await.unwrap();
5107
5108 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
5109 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
5110 _ = editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
5111 assert!(cx.read(|cx| editor.is_dirty(cx)));
5112
5113 let save = editor
5114 .update(cx, |editor, cx| editor.save(project.clone(), cx))
5115 .unwrap();
5116 fake_server
5117 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
5118 assert_eq!(
5119 params.text_document.uri,
5120 lsp::Url::from_file_path("/file.rs").unwrap()
5121 );
5122 assert_eq!(params.options.tab_size, 4);
5123 Ok(Some(vec![lsp::TextEdit::new(
5124 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
5125 ", ".to_string(),
5126 )]))
5127 })
5128 .next()
5129 .await;
5130 cx.executor().start_waiting();
5131 let _x = save.await;
5132
5133 assert_eq!(
5134 editor.update(cx, |editor, cx| editor.text(cx)),
5135 "one, two\nthree\n"
5136 );
5137 assert!(!cx.read(|cx| editor.is_dirty(cx)));
5138
5139 _ = editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
5140 assert!(cx.read(|cx| editor.is_dirty(cx)));
5141
5142 // Ensure we can still save even if formatting hangs.
5143 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
5144 assert_eq!(
5145 params.text_document.uri,
5146 lsp::Url::from_file_path("/file.rs").unwrap()
5147 );
5148 futures::future::pending::<()>().await;
5149 unreachable!()
5150 });
5151 let save = editor
5152 .update(cx, |editor, cx| editor.save(project.clone(), cx))
5153 .unwrap();
5154 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
5155 cx.executor().start_waiting();
5156 save.await;
5157 assert_eq!(
5158 editor.update(cx, |editor, cx| editor.text(cx)),
5159 "one\ntwo\nthree\n"
5160 );
5161 assert!(!cx.read(|cx| editor.is_dirty(cx)));
5162
5163 // Set rust language override and assert overridden tabsize is sent to language server
5164 update_test_language_settings(cx, |settings| {
5165 settings.languages.insert(
5166 "Rust".into(),
5167 LanguageSettingsContent {
5168 tab_size: NonZeroU32::new(8),
5169 ..Default::default()
5170 },
5171 );
5172 });
5173
5174 let save = editor
5175 .update(cx, |editor, cx| editor.save(project.clone(), cx))
5176 .unwrap();
5177 fake_server
5178 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
5179 assert_eq!(
5180 params.text_document.uri,
5181 lsp::Url::from_file_path("/file.rs").unwrap()
5182 );
5183 assert_eq!(params.options.tab_size, 8);
5184 Ok(Some(vec![]))
5185 })
5186 .next()
5187 .await;
5188 cx.executor().start_waiting();
5189 save.await;
5190}
5191
5192#[gpui::test]
5193async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
5194 init_test(cx, |_| {});
5195
5196 let mut language = Language::new(
5197 LanguageConfig {
5198 name: "Rust".into(),
5199 path_suffixes: vec!["rs".to_string()],
5200 ..Default::default()
5201 },
5202 Some(tree_sitter_rust::language()),
5203 );
5204 let mut fake_servers = language
5205 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
5206 capabilities: lsp::ServerCapabilities {
5207 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
5208 ..Default::default()
5209 },
5210 ..Default::default()
5211 }))
5212 .await;
5213
5214 let fs = FakeFs::new(cx.executor());
5215 fs.insert_file("/file.rs", Default::default()).await;
5216
5217 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
5218 _ = project.update(cx, |project, _| project.languages().add(Arc::new(language)));
5219 let buffer = project
5220 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
5221 .await
5222 .unwrap();
5223
5224 cx.executor().start_waiting();
5225 let fake_server = fake_servers.next().await.unwrap();
5226
5227 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
5228 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
5229 _ = editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
5230 assert!(cx.read(|cx| editor.is_dirty(cx)));
5231
5232 let save = editor
5233 .update(cx, |editor, cx| editor.save(project.clone(), cx))
5234 .unwrap();
5235 fake_server
5236 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
5237 assert_eq!(
5238 params.text_document.uri,
5239 lsp::Url::from_file_path("/file.rs").unwrap()
5240 );
5241 assert_eq!(params.options.tab_size, 4);
5242 Ok(Some(vec![lsp::TextEdit::new(
5243 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
5244 ", ".to_string(),
5245 )]))
5246 })
5247 .next()
5248 .await;
5249 cx.executor().start_waiting();
5250 save.await;
5251 assert_eq!(
5252 editor.update(cx, |editor, cx| editor.text(cx)),
5253 "one, two\nthree\n"
5254 );
5255 assert!(!cx.read(|cx| editor.is_dirty(cx)));
5256
5257 _ = editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
5258 assert!(cx.read(|cx| editor.is_dirty(cx)));
5259
5260 // Ensure we can still save even if formatting hangs.
5261 fake_server.handle_request::<lsp::request::RangeFormatting, _, _>(
5262 move |params, _| async move {
5263 assert_eq!(
5264 params.text_document.uri,
5265 lsp::Url::from_file_path("/file.rs").unwrap()
5266 );
5267 futures::future::pending::<()>().await;
5268 unreachable!()
5269 },
5270 );
5271 let save = editor
5272 .update(cx, |editor, cx| editor.save(project.clone(), cx))
5273 .unwrap();
5274 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
5275 cx.executor().start_waiting();
5276 save.await;
5277 assert_eq!(
5278 editor.update(cx, |editor, cx| editor.text(cx)),
5279 "one\ntwo\nthree\n"
5280 );
5281 assert!(!cx.read(|cx| editor.is_dirty(cx)));
5282
5283 // Set rust language override and assert overridden tabsize is sent to language server
5284 update_test_language_settings(cx, |settings| {
5285 settings.languages.insert(
5286 "Rust".into(),
5287 LanguageSettingsContent {
5288 tab_size: NonZeroU32::new(8),
5289 ..Default::default()
5290 },
5291 );
5292 });
5293
5294 let save = editor
5295 .update(cx, |editor, cx| editor.save(project.clone(), cx))
5296 .unwrap();
5297 fake_server
5298 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
5299 assert_eq!(
5300 params.text_document.uri,
5301 lsp::Url::from_file_path("/file.rs").unwrap()
5302 );
5303 assert_eq!(params.options.tab_size, 8);
5304 Ok(Some(vec![]))
5305 })
5306 .next()
5307 .await;
5308 cx.executor().start_waiting();
5309 save.await;
5310}
5311
5312#[gpui::test]
5313async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
5314 init_test(cx, |settings| {
5315 settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer)
5316 });
5317
5318 let mut language = Language::new(
5319 LanguageConfig {
5320 name: "Rust".into(),
5321 path_suffixes: vec!["rs".to_string()],
5322 // Enable Prettier formatting for the same buffer, and ensure
5323 // LSP is called instead of Prettier.
5324 prettier_parser_name: Some("test_parser".to_string()),
5325 ..Default::default()
5326 },
5327 Some(tree_sitter_rust::language()),
5328 );
5329 let mut fake_servers = language
5330 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
5331 capabilities: lsp::ServerCapabilities {
5332 document_formatting_provider: Some(lsp::OneOf::Left(true)),
5333 ..Default::default()
5334 },
5335 ..Default::default()
5336 }))
5337 .await;
5338
5339 let fs = FakeFs::new(cx.executor());
5340 fs.insert_file("/file.rs", Default::default()).await;
5341
5342 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
5343 _ = project.update(cx, |project, _| {
5344 project.languages().add(Arc::new(language));
5345 });
5346 let buffer = project
5347 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
5348 .await
5349 .unwrap();
5350
5351 cx.executor().start_waiting();
5352 let fake_server = fake_servers.next().await.unwrap();
5353
5354 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
5355 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
5356 _ = editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
5357
5358 let format = editor
5359 .update(cx, |editor, cx| {
5360 editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
5361 })
5362 .unwrap();
5363 fake_server
5364 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
5365 assert_eq!(
5366 params.text_document.uri,
5367 lsp::Url::from_file_path("/file.rs").unwrap()
5368 );
5369 assert_eq!(params.options.tab_size, 4);
5370 Ok(Some(vec![lsp::TextEdit::new(
5371 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
5372 ", ".to_string(),
5373 )]))
5374 })
5375 .next()
5376 .await;
5377 cx.executor().start_waiting();
5378 format.await;
5379 assert_eq!(
5380 editor.update(cx, |editor, cx| editor.text(cx)),
5381 "one, two\nthree\n"
5382 );
5383
5384 _ = editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
5385 // Ensure we don't lock if formatting hangs.
5386 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
5387 assert_eq!(
5388 params.text_document.uri,
5389 lsp::Url::from_file_path("/file.rs").unwrap()
5390 );
5391 futures::future::pending::<()>().await;
5392 unreachable!()
5393 });
5394 let format = editor
5395 .update(cx, |editor, cx| {
5396 editor.perform_format(project, FormatTrigger::Manual, cx)
5397 })
5398 .unwrap();
5399 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
5400 cx.executor().start_waiting();
5401 format.await;
5402 assert_eq!(
5403 editor.update(cx, |editor, cx| editor.text(cx)),
5404 "one\ntwo\nthree\n"
5405 );
5406}
5407
5408#[gpui::test]
5409async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
5410 init_test(cx, |_| {});
5411
5412 let mut cx = EditorLspTestContext::new_rust(
5413 lsp::ServerCapabilities {
5414 document_formatting_provider: Some(lsp::OneOf::Left(true)),
5415 ..Default::default()
5416 },
5417 cx,
5418 )
5419 .await;
5420
5421 cx.set_state(indoc! {"
5422 one.twoˇ
5423 "});
5424
5425 // The format request takes a long time. When it completes, it inserts
5426 // a newline and an indent before the `.`
5427 cx.lsp
5428 .handle_request::<lsp::request::Formatting, _, _>(move |_, cx| {
5429 let executor = cx.background_executor().clone();
5430 async move {
5431 executor.timer(Duration::from_millis(100)).await;
5432 Ok(Some(vec![lsp::TextEdit {
5433 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
5434 new_text: "\n ".into(),
5435 }]))
5436 }
5437 });
5438
5439 // Submit a format request.
5440 let format_1 = cx
5441 .update_editor(|editor, cx| editor.format(&Format, cx))
5442 .unwrap();
5443 cx.executor().run_until_parked();
5444
5445 // Submit a second format request.
5446 let format_2 = cx
5447 .update_editor(|editor, cx| editor.format(&Format, cx))
5448 .unwrap();
5449 cx.executor().run_until_parked();
5450
5451 // Wait for both format requests to complete
5452 cx.executor().advance_clock(Duration::from_millis(200));
5453 cx.executor().start_waiting();
5454 format_1.await.unwrap();
5455 cx.executor().start_waiting();
5456 format_2.await.unwrap();
5457
5458 // The formatting edits only happens once.
5459 cx.assert_editor_state(indoc! {"
5460 one
5461 .twoˇ
5462 "});
5463}
5464
5465#[gpui::test]
5466async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
5467 init_test(cx, |settings| {
5468 settings.defaults.formatter = Some(language_settings::Formatter::Auto)
5469 });
5470
5471 let mut cx = EditorLspTestContext::new_rust(
5472 lsp::ServerCapabilities {
5473 document_formatting_provider: Some(lsp::OneOf::Left(true)),
5474 ..Default::default()
5475 },
5476 cx,
5477 )
5478 .await;
5479
5480 // Set up a buffer white some trailing whitespace and no trailing newline.
5481 cx.set_state(
5482 &[
5483 "one ", //
5484 "twoˇ", //
5485 "three ", //
5486 "four", //
5487 ]
5488 .join("\n"),
5489 );
5490
5491 // Submit a format request.
5492 let format = cx
5493 .update_editor(|editor, cx| editor.format(&Format, cx))
5494 .unwrap();
5495
5496 // Record which buffer changes have been sent to the language server
5497 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
5498 cx.lsp
5499 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
5500 let buffer_changes = buffer_changes.clone();
5501 move |params, _| {
5502 buffer_changes.lock().extend(
5503 params
5504 .content_changes
5505 .into_iter()
5506 .map(|e| (e.range.unwrap(), e.text)),
5507 );
5508 }
5509 });
5510
5511 // Handle formatting requests to the language server.
5512 cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
5513 let buffer_changes = buffer_changes.clone();
5514 move |_, _| {
5515 // When formatting is requested, trailing whitespace has already been stripped,
5516 // and the trailing newline has already been added.
5517 assert_eq!(
5518 &buffer_changes.lock()[1..],
5519 &[
5520 (
5521 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
5522 "".into()
5523 ),
5524 (
5525 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
5526 "".into()
5527 ),
5528 (
5529 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
5530 "\n".into()
5531 ),
5532 ]
5533 );
5534
5535 // Insert blank lines between each line of the buffer.
5536 async move {
5537 Ok(Some(vec![
5538 lsp::TextEdit {
5539 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
5540 new_text: "\n".into(),
5541 },
5542 lsp::TextEdit {
5543 range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
5544 new_text: "\n".into(),
5545 },
5546 ]))
5547 }
5548 }
5549 });
5550
5551 // After formatting the buffer, the trailing whitespace is stripped,
5552 // a newline is appended, and the edits provided by the language server
5553 // have been applied.
5554 format.await.unwrap();
5555 cx.assert_editor_state(
5556 &[
5557 "one", //
5558 "", //
5559 "twoˇ", //
5560 "", //
5561 "three", //
5562 "four", //
5563 "", //
5564 ]
5565 .join("\n"),
5566 );
5567
5568 // Undoing the formatting undoes the trailing whitespace removal, the
5569 // trailing newline, and the LSP edits.
5570 cx.update_buffer(|buffer, cx| buffer.undo(cx));
5571 cx.assert_editor_state(
5572 &[
5573 "one ", //
5574 "twoˇ", //
5575 "three ", //
5576 "four", //
5577 ]
5578 .join("\n"),
5579 );
5580}
5581
5582#[gpui::test]
5583async fn test_completion(cx: &mut gpui::TestAppContext) {
5584 init_test(cx, |_| {});
5585
5586 let mut cx = EditorLspTestContext::new_rust(
5587 lsp::ServerCapabilities {
5588 completion_provider: Some(lsp::CompletionOptions {
5589 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
5590 resolve_provider: Some(true),
5591 ..Default::default()
5592 }),
5593 ..Default::default()
5594 },
5595 cx,
5596 )
5597 .await;
5598
5599 cx.set_state(indoc! {"
5600 oneˇ
5601 two
5602 three
5603 "});
5604 cx.simulate_keystroke(".");
5605 handle_completion_request(
5606 &mut cx,
5607 indoc! {"
5608 one.|<>
5609 two
5610 three
5611 "},
5612 vec!["first_completion", "second_completion"],
5613 )
5614 .await;
5615 cx.condition(|editor, _| editor.context_menu_visible())
5616 .await;
5617 let apply_additional_edits = cx.update_editor(|editor, cx| {
5618 editor.context_menu_next(&Default::default(), cx);
5619 editor
5620 .confirm_completion(&ConfirmCompletion::default(), cx)
5621 .unwrap()
5622 });
5623 cx.assert_editor_state(indoc! {"
5624 one.second_completionˇ
5625 two
5626 three
5627 "});
5628
5629 handle_resolve_completion_request(
5630 &mut cx,
5631 Some(vec![
5632 (
5633 //This overlaps with the primary completion edit which is
5634 //misbehavior from the LSP spec, test that we filter it out
5635 indoc! {"
5636 one.second_ˇcompletion
5637 two
5638 threeˇ
5639 "},
5640 "overlapping additional edit",
5641 ),
5642 (
5643 indoc! {"
5644 one.second_completion
5645 two
5646 threeˇ
5647 "},
5648 "\nadditional edit",
5649 ),
5650 ]),
5651 )
5652 .await;
5653 apply_additional_edits.await.unwrap();
5654 cx.assert_editor_state(indoc! {"
5655 one.second_completionˇ
5656 two
5657 three
5658 additional edit
5659 "});
5660
5661 cx.set_state(indoc! {"
5662 one.second_completion
5663 twoˇ
5664 threeˇ
5665 additional edit
5666 "});
5667 cx.simulate_keystroke(" ");
5668 assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
5669 cx.simulate_keystroke("s");
5670 assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
5671
5672 cx.assert_editor_state(indoc! {"
5673 one.second_completion
5674 two sˇ
5675 three sˇ
5676 additional edit
5677 "});
5678 handle_completion_request(
5679 &mut cx,
5680 indoc! {"
5681 one.second_completion
5682 two s
5683 three <s|>
5684 additional edit
5685 "},
5686 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
5687 )
5688 .await;
5689 cx.condition(|editor, _| editor.context_menu_visible())
5690 .await;
5691
5692 cx.simulate_keystroke("i");
5693
5694 handle_completion_request(
5695 &mut cx,
5696 indoc! {"
5697 one.second_completion
5698 two si
5699 three <si|>
5700 additional edit
5701 "},
5702 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
5703 )
5704 .await;
5705 cx.condition(|editor, _| editor.context_menu_visible())
5706 .await;
5707
5708 let apply_additional_edits = cx.update_editor(|editor, cx| {
5709 editor
5710 .confirm_completion(&ConfirmCompletion::default(), cx)
5711 .unwrap()
5712 });
5713 cx.assert_editor_state(indoc! {"
5714 one.second_completion
5715 two sixth_completionˇ
5716 three sixth_completionˇ
5717 additional edit
5718 "});
5719
5720 handle_resolve_completion_request(&mut cx, None).await;
5721 apply_additional_edits.await.unwrap();
5722
5723 _ = cx.update(|cx| {
5724 cx.update_global::<SettingsStore, _>(|settings, cx| {
5725 settings.update_user_settings::<EditorSettings>(cx, |settings| {
5726 settings.show_completions_on_input = Some(false);
5727 });
5728 })
5729 });
5730 cx.set_state("editorˇ");
5731 cx.simulate_keystroke(".");
5732 assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
5733 cx.simulate_keystroke("c");
5734 cx.simulate_keystroke("l");
5735 cx.simulate_keystroke("o");
5736 cx.assert_editor_state("editor.cloˇ");
5737 assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
5738 cx.update_editor(|editor, cx| {
5739 editor.show_completions(&ShowCompletions, cx);
5740 });
5741 handle_completion_request(&mut cx, "editor.<clo|>", vec!["close", "clobber"]).await;
5742 cx.condition(|editor, _| editor.context_menu_visible())
5743 .await;
5744 let apply_additional_edits = cx.update_editor(|editor, cx| {
5745 editor
5746 .confirm_completion(&ConfirmCompletion::default(), cx)
5747 .unwrap()
5748 });
5749 cx.assert_editor_state("editor.closeˇ");
5750 handle_resolve_completion_request(&mut cx, None).await;
5751 apply_additional_edits.await.unwrap();
5752}
5753
5754#[gpui::test]
5755async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
5756 init_test(cx, |_| {});
5757 let mut cx = EditorTestContext::new(cx).await;
5758 let language = Arc::new(Language::new(
5759 LanguageConfig {
5760 line_comments: vec!["// ".into()],
5761 ..Default::default()
5762 },
5763 Some(tree_sitter_rust::language()),
5764 ));
5765 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5766
5767 // If multiple selections intersect a line, the line is only toggled once.
5768 cx.set_state(indoc! {"
5769 fn a() {
5770 «//b();
5771 ˇ»// «c();
5772 //ˇ» d();
5773 }
5774 "});
5775
5776 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
5777
5778 cx.assert_editor_state(indoc! {"
5779 fn a() {
5780 «b();
5781 c();
5782 ˇ» d();
5783 }
5784 "});
5785
5786 // The comment prefix is inserted at the same column for every line in a
5787 // selection.
5788 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
5789
5790 cx.assert_editor_state(indoc! {"
5791 fn a() {
5792 // «b();
5793 // c();
5794 ˇ»// d();
5795 }
5796 "});
5797
5798 // If a selection ends at the beginning of a line, that line is not toggled.
5799 cx.set_selections_state(indoc! {"
5800 fn a() {
5801 // b();
5802 «// c();
5803 ˇ» // d();
5804 }
5805 "});
5806
5807 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
5808
5809 cx.assert_editor_state(indoc! {"
5810 fn a() {
5811 // b();
5812 «c();
5813 ˇ» // d();
5814 }
5815 "});
5816
5817 // If a selection span a single line and is empty, the line is toggled.
5818 cx.set_state(indoc! {"
5819 fn a() {
5820 a();
5821 b();
5822 ˇ
5823 }
5824 "});
5825
5826 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
5827
5828 cx.assert_editor_state(indoc! {"
5829 fn a() {
5830 a();
5831 b();
5832 //•ˇ
5833 }
5834 "});
5835
5836 // If a selection span multiple lines, empty lines are not toggled.
5837 cx.set_state(indoc! {"
5838 fn a() {
5839 «a();
5840
5841 c();ˇ»
5842 }
5843 "});
5844
5845 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
5846
5847 cx.assert_editor_state(indoc! {"
5848 fn a() {
5849 // «a();
5850
5851 // c();ˇ»
5852 }
5853 "});
5854}
5855
5856#[gpui::test]
5857async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
5858 init_test(cx, |_| {});
5859
5860 let language = Arc::new(Language::new(
5861 LanguageConfig {
5862 line_comments: vec!["// ".into()],
5863 ..Default::default()
5864 },
5865 Some(tree_sitter_rust::language()),
5866 ));
5867
5868 let registry = Arc::new(LanguageRegistry::test());
5869 registry.add(language.clone());
5870
5871 let mut cx = EditorTestContext::new(cx).await;
5872 cx.update_buffer(|buffer, cx| {
5873 buffer.set_language_registry(registry);
5874 buffer.set_language(Some(language), cx);
5875 });
5876
5877 let toggle_comments = &ToggleComments {
5878 advance_downwards: true,
5879 };
5880
5881 // Single cursor on one line -> advance
5882 // Cursor moves horizontally 3 characters as well on non-blank line
5883 cx.set_state(indoc!(
5884 "fn a() {
5885 ˇdog();
5886 cat();
5887 }"
5888 ));
5889 cx.update_editor(|editor, cx| {
5890 editor.toggle_comments(toggle_comments, cx);
5891 });
5892 cx.assert_editor_state(indoc!(
5893 "fn a() {
5894 // dog();
5895 catˇ();
5896 }"
5897 ));
5898
5899 // Single selection on one line -> don't advance
5900 cx.set_state(indoc!(
5901 "fn a() {
5902 «dog()ˇ»;
5903 cat();
5904 }"
5905 ));
5906 cx.update_editor(|editor, cx| {
5907 editor.toggle_comments(toggle_comments, cx);
5908 });
5909 cx.assert_editor_state(indoc!(
5910 "fn a() {
5911 // «dog()ˇ»;
5912 cat();
5913 }"
5914 ));
5915
5916 // Multiple cursors on one line -> advance
5917 cx.set_state(indoc!(
5918 "fn a() {
5919 ˇdˇog();
5920 cat();
5921 }"
5922 ));
5923 cx.update_editor(|editor, cx| {
5924 editor.toggle_comments(toggle_comments, cx);
5925 });
5926 cx.assert_editor_state(indoc!(
5927 "fn a() {
5928 // dog();
5929 catˇ(ˇ);
5930 }"
5931 ));
5932
5933 // Multiple cursors on one line, with selection -> don't advance
5934 cx.set_state(indoc!(
5935 "fn a() {
5936 ˇdˇog«()ˇ»;
5937 cat();
5938 }"
5939 ));
5940 cx.update_editor(|editor, cx| {
5941 editor.toggle_comments(toggle_comments, cx);
5942 });
5943 cx.assert_editor_state(indoc!(
5944 "fn a() {
5945 // ˇdˇog«()ˇ»;
5946 cat();
5947 }"
5948 ));
5949
5950 // Single cursor on one line -> advance
5951 // Cursor moves to column 0 on blank line
5952 cx.set_state(indoc!(
5953 "fn a() {
5954 ˇdog();
5955
5956 cat();
5957 }"
5958 ));
5959 cx.update_editor(|editor, cx| {
5960 editor.toggle_comments(toggle_comments, cx);
5961 });
5962 cx.assert_editor_state(indoc!(
5963 "fn a() {
5964 // dog();
5965 ˇ
5966 cat();
5967 }"
5968 ));
5969
5970 // Single cursor on one line -> advance
5971 // Cursor starts and ends at column 0
5972 cx.set_state(indoc!(
5973 "fn a() {
5974 ˇ dog();
5975 cat();
5976 }"
5977 ));
5978 cx.update_editor(|editor, cx| {
5979 editor.toggle_comments(toggle_comments, cx);
5980 });
5981 cx.assert_editor_state(indoc!(
5982 "fn a() {
5983 // dog();
5984 ˇ cat();
5985 }"
5986 ));
5987}
5988
5989#[gpui::test]
5990async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
5991 init_test(cx, |_| {});
5992
5993 let mut cx = EditorTestContext::new(cx).await;
5994
5995 let html_language = Arc::new(
5996 Language::new(
5997 LanguageConfig {
5998 name: "HTML".into(),
5999 block_comment: Some(("<!-- ".into(), " -->".into())),
6000 ..Default::default()
6001 },
6002 Some(tree_sitter_html::language()),
6003 )
6004 .with_injection_query(
6005 r#"
6006 (script_element
6007 (raw_text) @content
6008 (#set! "language" "javascript"))
6009 "#,
6010 )
6011 .unwrap(),
6012 );
6013
6014 let javascript_language = Arc::new(Language::new(
6015 LanguageConfig {
6016 name: "JavaScript".into(),
6017 line_comments: vec!["// ".into()],
6018 ..Default::default()
6019 },
6020 Some(tree_sitter_typescript::language_tsx()),
6021 ));
6022
6023 let registry = Arc::new(LanguageRegistry::test());
6024 registry.add(html_language.clone());
6025 registry.add(javascript_language.clone());
6026
6027 cx.update_buffer(|buffer, cx| {
6028 buffer.set_language_registry(registry);
6029 buffer.set_language(Some(html_language), cx);
6030 });
6031
6032 // Toggle comments for empty selections
6033 cx.set_state(
6034 &r#"
6035 <p>A</p>ˇ
6036 <p>B</p>ˇ
6037 <p>C</p>ˇ
6038 "#
6039 .unindent(),
6040 );
6041 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
6042 cx.assert_editor_state(
6043 &r#"
6044 <!-- <p>A</p>ˇ -->
6045 <!-- <p>B</p>ˇ -->
6046 <!-- <p>C</p>ˇ -->
6047 "#
6048 .unindent(),
6049 );
6050 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
6051 cx.assert_editor_state(
6052 &r#"
6053 <p>A</p>ˇ
6054 <p>B</p>ˇ
6055 <p>C</p>ˇ
6056 "#
6057 .unindent(),
6058 );
6059
6060 // Toggle comments for mixture of empty and non-empty selections, where
6061 // multiple selections occupy a given line.
6062 cx.set_state(
6063 &r#"
6064 <p>A«</p>
6065 <p>ˇ»B</p>ˇ
6066 <p>C«</p>
6067 <p>ˇ»D</p>ˇ
6068 "#
6069 .unindent(),
6070 );
6071
6072 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
6073 cx.assert_editor_state(
6074 &r#"
6075 <!-- <p>A«</p>
6076 <p>ˇ»B</p>ˇ -->
6077 <!-- <p>C«</p>
6078 <p>ˇ»D</p>ˇ -->
6079 "#
6080 .unindent(),
6081 );
6082 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
6083 cx.assert_editor_state(
6084 &r#"
6085 <p>A«</p>
6086 <p>ˇ»B</p>ˇ
6087 <p>C«</p>
6088 <p>ˇ»D</p>ˇ
6089 "#
6090 .unindent(),
6091 );
6092
6093 // Toggle comments when different languages are active for different
6094 // selections.
6095 cx.set_state(
6096 &r#"
6097 ˇ<script>
6098 ˇvar x = new Y();
6099 ˇ</script>
6100 "#
6101 .unindent(),
6102 );
6103 cx.executor().run_until_parked();
6104 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
6105 cx.assert_editor_state(
6106 &r#"
6107 <!-- ˇ<script> -->
6108 // ˇvar x = new Y();
6109 <!-- ˇ</script> -->
6110 "#
6111 .unindent(),
6112 );
6113}
6114
6115#[gpui::test]
6116fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
6117 init_test(cx, |_| {});
6118
6119 let buffer = cx.new_model(|cx| {
6120 Buffer::new(
6121 0,
6122 BufferId::new(cx.entity_id().as_u64()).unwrap(),
6123 sample_text(3, 4, 'a'),
6124 )
6125 });
6126 let multibuffer = cx.new_model(|cx| {
6127 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
6128 multibuffer.push_excerpts(
6129 buffer.clone(),
6130 [
6131 ExcerptRange {
6132 context: Point::new(0, 0)..Point::new(0, 4),
6133 primary: None,
6134 },
6135 ExcerptRange {
6136 context: Point::new(1, 0)..Point::new(1, 4),
6137 primary: None,
6138 },
6139 ],
6140 cx,
6141 );
6142 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
6143 multibuffer
6144 });
6145
6146 let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
6147 _ = view.update(cx, |view, cx| {
6148 assert_eq!(view.text(cx), "aaaa\nbbbb");
6149 view.change_selections(None, cx, |s| {
6150 s.select_ranges([
6151 Point::new(0, 0)..Point::new(0, 0),
6152 Point::new(1, 0)..Point::new(1, 0),
6153 ])
6154 });
6155
6156 view.handle_input("X", cx);
6157 assert_eq!(view.text(cx), "Xaaaa\nXbbbb");
6158 assert_eq!(
6159 view.selections.ranges(cx),
6160 [
6161 Point::new(0, 1)..Point::new(0, 1),
6162 Point::new(1, 1)..Point::new(1, 1),
6163 ]
6164 );
6165
6166 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
6167 view.change_selections(None, cx, |s| {
6168 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
6169 });
6170 view.backspace(&Default::default(), cx);
6171 assert_eq!(view.text(cx), "Xa\nbbb");
6172 assert_eq!(
6173 view.selections.ranges(cx),
6174 [Point::new(1, 0)..Point::new(1, 0)]
6175 );
6176
6177 view.change_selections(None, cx, |s| {
6178 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
6179 });
6180 view.backspace(&Default::default(), cx);
6181 assert_eq!(view.text(cx), "X\nbb");
6182 assert_eq!(
6183 view.selections.ranges(cx),
6184 [Point::new(0, 1)..Point::new(0, 1)]
6185 );
6186 });
6187}
6188
6189#[gpui::test]
6190fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
6191 init_test(cx, |_| {});
6192
6193 let markers = vec![('[', ']').into(), ('(', ')').into()];
6194 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
6195 indoc! {"
6196 [aaaa
6197 (bbbb]
6198 cccc)",
6199 },
6200 markers.clone(),
6201 );
6202 let excerpt_ranges = markers.into_iter().map(|marker| {
6203 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
6204 ExcerptRange {
6205 context,
6206 primary: None,
6207 }
6208 });
6209 let buffer = cx.new_model(|cx| {
6210 Buffer::new(
6211 0,
6212 BufferId::new(cx.entity_id().as_u64()).unwrap(),
6213 initial_text,
6214 )
6215 });
6216 let multibuffer = cx.new_model(|cx| {
6217 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
6218 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
6219 multibuffer
6220 });
6221
6222 let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
6223 _ = view.update(cx, |view, cx| {
6224 let (expected_text, selection_ranges) = marked_text_ranges(
6225 indoc! {"
6226 aaaa
6227 bˇbbb
6228 bˇbbˇb
6229 cccc"
6230 },
6231 true,
6232 );
6233 assert_eq!(view.text(cx), expected_text);
6234 view.change_selections(None, cx, |s| s.select_ranges(selection_ranges));
6235
6236 view.handle_input("X", cx);
6237
6238 let (expected_text, expected_selections) = marked_text_ranges(
6239 indoc! {"
6240 aaaa
6241 bXˇbbXb
6242 bXˇbbXˇb
6243 cccc"
6244 },
6245 false,
6246 );
6247 assert_eq!(view.text(cx), expected_text);
6248 assert_eq!(view.selections.ranges(cx), expected_selections);
6249
6250 view.newline(&Newline, cx);
6251 let (expected_text, expected_selections) = marked_text_ranges(
6252 indoc! {"
6253 aaaa
6254 bX
6255 ˇbbX
6256 b
6257 bX
6258 ˇbbX
6259 ˇb
6260 cccc"
6261 },
6262 false,
6263 );
6264 assert_eq!(view.text(cx), expected_text);
6265 assert_eq!(view.selections.ranges(cx), expected_selections);
6266 });
6267}
6268
6269#[gpui::test]
6270fn test_refresh_selections(cx: &mut TestAppContext) {
6271 init_test(cx, |_| {});
6272
6273 let buffer = cx.new_model(|cx| {
6274 Buffer::new(
6275 0,
6276 BufferId::new(cx.entity_id().as_u64()).unwrap(),
6277 sample_text(3, 4, 'a'),
6278 )
6279 });
6280 let mut excerpt1_id = None;
6281 let multibuffer = cx.new_model(|cx| {
6282 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
6283 excerpt1_id = multibuffer
6284 .push_excerpts(
6285 buffer.clone(),
6286 [
6287 ExcerptRange {
6288 context: Point::new(0, 0)..Point::new(1, 4),
6289 primary: None,
6290 },
6291 ExcerptRange {
6292 context: Point::new(1, 0)..Point::new(2, 4),
6293 primary: None,
6294 },
6295 ],
6296 cx,
6297 )
6298 .into_iter()
6299 .next();
6300 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
6301 multibuffer
6302 });
6303
6304 let editor = cx.add_window(|cx| {
6305 let mut editor = build_editor(multibuffer.clone(), cx);
6306 let snapshot = editor.snapshot(cx);
6307 editor.change_selections(None, cx, |s| {
6308 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
6309 });
6310 editor.begin_selection(Point::new(2, 1).to_display_point(&snapshot), true, 1, cx);
6311 assert_eq!(
6312 editor.selections.ranges(cx),
6313 [
6314 Point::new(1, 3)..Point::new(1, 3),
6315 Point::new(2, 1)..Point::new(2, 1),
6316 ]
6317 );
6318 editor
6319 });
6320
6321 // Refreshing selections is a no-op when excerpts haven't changed.
6322 _ = editor.update(cx, |editor, cx| {
6323 editor.change_selections(None, cx, |s| s.refresh());
6324 assert_eq!(
6325 editor.selections.ranges(cx),
6326 [
6327 Point::new(1, 3)..Point::new(1, 3),
6328 Point::new(2, 1)..Point::new(2, 1),
6329 ]
6330 );
6331 });
6332
6333 _ = multibuffer.update(cx, |multibuffer, cx| {
6334 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
6335 });
6336 _ = editor.update(cx, |editor, cx| {
6337 // Removing an excerpt causes the first selection to become degenerate.
6338 assert_eq!(
6339 editor.selections.ranges(cx),
6340 [
6341 Point::new(0, 0)..Point::new(0, 0),
6342 Point::new(0, 1)..Point::new(0, 1)
6343 ]
6344 );
6345
6346 // Refreshing selections will relocate the first selection to the original buffer
6347 // location.
6348 editor.change_selections(None, cx, |s| s.refresh());
6349 assert_eq!(
6350 editor.selections.ranges(cx),
6351 [
6352 Point::new(0, 1)..Point::new(0, 1),
6353 Point::new(0, 3)..Point::new(0, 3)
6354 ]
6355 );
6356 assert!(editor.selections.pending_anchor().is_some());
6357 });
6358}
6359
6360#[gpui::test]
6361fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
6362 init_test(cx, |_| {});
6363
6364 let buffer = cx.new_model(|cx| {
6365 Buffer::new(
6366 0,
6367 BufferId::new(cx.entity_id().as_u64()).unwrap(),
6368 sample_text(3, 4, 'a'),
6369 )
6370 });
6371 let mut excerpt1_id = None;
6372 let multibuffer = cx.new_model(|cx| {
6373 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
6374 excerpt1_id = multibuffer
6375 .push_excerpts(
6376 buffer.clone(),
6377 [
6378 ExcerptRange {
6379 context: Point::new(0, 0)..Point::new(1, 4),
6380 primary: None,
6381 },
6382 ExcerptRange {
6383 context: Point::new(1, 0)..Point::new(2, 4),
6384 primary: None,
6385 },
6386 ],
6387 cx,
6388 )
6389 .into_iter()
6390 .next();
6391 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
6392 multibuffer
6393 });
6394
6395 let editor = cx.add_window(|cx| {
6396 let mut editor = build_editor(multibuffer.clone(), cx);
6397 let snapshot = editor.snapshot(cx);
6398 editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx);
6399 assert_eq!(
6400 editor.selections.ranges(cx),
6401 [Point::new(1, 3)..Point::new(1, 3)]
6402 );
6403 editor
6404 });
6405
6406 _ = multibuffer.update(cx, |multibuffer, cx| {
6407 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
6408 });
6409 _ = editor.update(cx, |editor, cx| {
6410 assert_eq!(
6411 editor.selections.ranges(cx),
6412 [Point::new(0, 0)..Point::new(0, 0)]
6413 );
6414
6415 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
6416 editor.change_selections(None, cx, |s| s.refresh());
6417 assert_eq!(
6418 editor.selections.ranges(cx),
6419 [Point::new(0, 3)..Point::new(0, 3)]
6420 );
6421 assert!(editor.selections.pending_anchor().is_some());
6422 });
6423}
6424
6425#[gpui::test]
6426async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
6427 init_test(cx, |_| {});
6428
6429 let language = Arc::new(
6430 Language::new(
6431 LanguageConfig {
6432 brackets: BracketPairConfig {
6433 pairs: vec![
6434 BracketPair {
6435 start: "{".to_string(),
6436 end: "}".to_string(),
6437 close: true,
6438 newline: true,
6439 },
6440 BracketPair {
6441 start: "/* ".to_string(),
6442 end: " */".to_string(),
6443 close: true,
6444 newline: true,
6445 },
6446 ],
6447 ..Default::default()
6448 },
6449 ..Default::default()
6450 },
6451 Some(tree_sitter_rust::language()),
6452 )
6453 .with_indents_query("")
6454 .unwrap(),
6455 );
6456
6457 let text = concat!(
6458 "{ }\n", //
6459 " x\n", //
6460 " /* */\n", //
6461 "x\n", //
6462 "{{} }\n", //
6463 );
6464
6465 let buffer = cx.new_model(|cx| {
6466 Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text)
6467 .with_language(language, cx)
6468 });
6469 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
6470 let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
6471 view.condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
6472 .await;
6473
6474 _ = view.update(cx, |view, cx| {
6475 view.change_selections(None, cx, |s| {
6476 s.select_display_ranges([
6477 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3),
6478 DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
6479 DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4),
6480 ])
6481 });
6482 view.newline(&Newline, cx);
6483
6484 assert_eq!(
6485 view.buffer().read(cx).read(cx).text(),
6486 concat!(
6487 "{ \n", // Suppress rustfmt
6488 "\n", //
6489 "}\n", //
6490 " x\n", //
6491 " /* \n", //
6492 " \n", //
6493 " */\n", //
6494 "x\n", //
6495 "{{} \n", //
6496 "}\n", //
6497 )
6498 );
6499 });
6500}
6501
6502#[gpui::test]
6503fn test_highlighted_ranges(cx: &mut TestAppContext) {
6504 init_test(cx, |_| {});
6505
6506 let editor = cx.add_window(|cx| {
6507 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
6508 build_editor(buffer.clone(), cx)
6509 });
6510
6511 _ = editor.update(cx, |editor, cx| {
6512 struct Type1;
6513 struct Type2;
6514
6515 let buffer = editor.buffer.read(cx).snapshot(cx);
6516
6517 let anchor_range =
6518 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
6519
6520 editor.highlight_background::<Type1>(
6521 vec![
6522 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
6523 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
6524 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
6525 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
6526 ],
6527 |_| Hsla::red(),
6528 cx,
6529 );
6530 editor.highlight_background::<Type2>(
6531 vec![
6532 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
6533 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
6534 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
6535 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
6536 ],
6537 |_| Hsla::green(),
6538 cx,
6539 );
6540
6541 let snapshot = editor.snapshot(cx);
6542 let mut highlighted_ranges = editor.background_highlights_in_range(
6543 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
6544 &snapshot,
6545 cx.theme().colors(),
6546 );
6547 // Enforce a consistent ordering based on color without relying on the ordering of the
6548 // highlight's `TypeId` which is non-executor.
6549 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
6550 assert_eq!(
6551 highlighted_ranges,
6552 &[
6553 (
6554 DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4),
6555 Hsla::red(),
6556 ),
6557 (
6558 DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
6559 Hsla::red(),
6560 ),
6561 (
6562 DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5),
6563 Hsla::green(),
6564 ),
6565 (
6566 DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6),
6567 Hsla::green(),
6568 ),
6569 ]
6570 );
6571 assert_eq!(
6572 editor.background_highlights_in_range(
6573 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
6574 &snapshot,
6575 cx.theme().colors(),
6576 ),
6577 &[(
6578 DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
6579 Hsla::red(),
6580 )]
6581 );
6582 });
6583}
6584
6585#[gpui::test]
6586async fn test_following(cx: &mut gpui::TestAppContext) {
6587 init_test(cx, |_| {});
6588
6589 let fs = FakeFs::new(cx.executor());
6590 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
6591
6592 let buffer = project.update(cx, |project, cx| {
6593 let buffer = project
6594 .create_buffer(&sample_text(16, 8, 'a'), None, cx)
6595 .unwrap();
6596 cx.new_model(|cx| MultiBuffer::singleton(buffer, cx))
6597 });
6598 let leader = cx.add_window(|cx| build_editor(buffer.clone(), cx));
6599 let follower = cx.update(|cx| {
6600 cx.open_window(
6601 WindowOptions {
6602 bounds: WindowBounds::Fixed(Bounds::from_corners(
6603 gpui::Point::new((0. as f64).into(), (0. as f64).into()),
6604 gpui::Point::new((10. as f64).into(), (80. as f64).into()),
6605 )),
6606 ..Default::default()
6607 },
6608 |cx| cx.new_view(|cx| build_editor(buffer.clone(), cx)),
6609 )
6610 });
6611
6612 let is_still_following = Rc::new(RefCell::new(true));
6613 let follower_edit_event_count = Rc::new(RefCell::new(0));
6614 let pending_update = Rc::new(RefCell::new(None));
6615 _ = follower.update(cx, {
6616 let update = pending_update.clone();
6617 let is_still_following = is_still_following.clone();
6618 let follower_edit_event_count = follower_edit_event_count.clone();
6619 |_, cx| {
6620 cx.subscribe(
6621 &leader.root_view(cx).unwrap(),
6622 move |_, leader, event, cx| {
6623 leader
6624 .read(cx)
6625 .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx);
6626 },
6627 )
6628 .detach();
6629
6630 cx.subscribe(
6631 &follower.root_view(cx).unwrap(),
6632 move |_, _, event: &EditorEvent, _cx| {
6633 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
6634 *is_still_following.borrow_mut() = false;
6635 }
6636
6637 if let EditorEvent::BufferEdited = event {
6638 *follower_edit_event_count.borrow_mut() += 1;
6639 }
6640 },
6641 )
6642 .detach();
6643 }
6644 });
6645
6646 // Update the selections only
6647 _ = leader.update(cx, |leader, cx| {
6648 leader.change_selections(None, cx, |s| s.select_ranges([1..1]));
6649 });
6650 follower
6651 .update(cx, |follower, cx| {
6652 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
6653 })
6654 .unwrap()
6655 .await
6656 .unwrap();
6657 _ = follower.update(cx, |follower, cx| {
6658 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
6659 });
6660 assert_eq!(*is_still_following.borrow(), true);
6661 assert_eq!(*follower_edit_event_count.borrow(), 0);
6662
6663 // Update the scroll position only
6664 _ = leader.update(cx, |leader, cx| {
6665 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), cx);
6666 });
6667 follower
6668 .update(cx, |follower, cx| {
6669 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
6670 })
6671 .unwrap()
6672 .await
6673 .unwrap();
6674 assert_eq!(
6675 follower
6676 .update(cx, |follower, cx| follower.scroll_position(cx))
6677 .unwrap(),
6678 gpui::Point::new(1.5, 3.5)
6679 );
6680 assert_eq!(*is_still_following.borrow(), true);
6681 assert_eq!(*follower_edit_event_count.borrow(), 0);
6682
6683 // Update the selections and scroll position. The follower's scroll position is updated
6684 // via autoscroll, not via the leader's exact scroll position.
6685 _ = leader.update(cx, |leader, cx| {
6686 leader.change_selections(None, cx, |s| s.select_ranges([0..0]));
6687 leader.request_autoscroll(Autoscroll::newest(), cx);
6688 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), cx);
6689 });
6690 follower
6691 .update(cx, |follower, cx| {
6692 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
6693 })
6694 .unwrap()
6695 .await
6696 .unwrap();
6697 _ = follower.update(cx, |follower, cx| {
6698 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
6699 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
6700 });
6701 assert_eq!(*is_still_following.borrow(), true);
6702
6703 // Creating a pending selection that precedes another selection
6704 _ = leader.update(cx, |leader, cx| {
6705 leader.change_selections(None, cx, |s| s.select_ranges([1..1]));
6706 leader.begin_selection(DisplayPoint::new(0, 0), true, 1, cx);
6707 });
6708 follower
6709 .update(cx, |follower, cx| {
6710 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
6711 })
6712 .unwrap()
6713 .await
6714 .unwrap();
6715 _ = follower.update(cx, |follower, cx| {
6716 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
6717 });
6718 assert_eq!(*is_still_following.borrow(), true);
6719
6720 // Extend the pending selection so that it surrounds another selection
6721 _ = leader.update(cx, |leader, cx| {
6722 leader.extend_selection(DisplayPoint::new(0, 2), 1, cx);
6723 });
6724 follower
6725 .update(cx, |follower, cx| {
6726 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
6727 })
6728 .unwrap()
6729 .await
6730 .unwrap();
6731 _ = follower.update(cx, |follower, cx| {
6732 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
6733 });
6734
6735 // Scrolling locally breaks the follow
6736 _ = follower.update(cx, |follower, cx| {
6737 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
6738 follower.set_scroll_anchor(
6739 ScrollAnchor {
6740 anchor: top_anchor,
6741 offset: gpui::Point::new(0.0, 0.5),
6742 },
6743 cx,
6744 );
6745 });
6746 assert_eq!(*is_still_following.borrow(), false);
6747}
6748
6749#[gpui::test]
6750async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
6751 init_test(cx, |_| {});
6752
6753 let fs = FakeFs::new(cx.executor());
6754 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
6755 let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
6756 let pane = workspace
6757 .update(cx, |workspace, _| workspace.active_pane().clone())
6758 .unwrap();
6759
6760 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
6761
6762 let leader = pane.update(cx, |_, cx| {
6763 let multibuffer = cx.new_model(|_| MultiBuffer::new(0, ReadWrite));
6764 cx.new_view(|cx| build_editor(multibuffer.clone(), cx))
6765 });
6766
6767 // Start following the editor when it has no excerpts.
6768 let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
6769 let follower_1 = cx
6770 .update_window(*workspace.deref(), |_, cx| {
6771 Editor::from_state_proto(
6772 pane.clone(),
6773 workspace.root_view(cx).unwrap(),
6774 ViewId {
6775 creator: Default::default(),
6776 id: 0,
6777 },
6778 &mut state_message,
6779 cx,
6780 )
6781 })
6782 .unwrap()
6783 .unwrap()
6784 .await
6785 .unwrap();
6786
6787 let update_message = Rc::new(RefCell::new(None));
6788 follower_1.update(cx, {
6789 let update = update_message.clone();
6790 |_, cx| {
6791 cx.subscribe(&leader, move |_, leader, event, cx| {
6792 leader
6793 .read(cx)
6794 .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx);
6795 })
6796 .detach();
6797 }
6798 });
6799
6800 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
6801 (
6802 project
6803 .create_buffer("abc\ndef\nghi\njkl\n", None, cx)
6804 .unwrap(),
6805 project
6806 .create_buffer("mno\npqr\nstu\nvwx\n", None, cx)
6807 .unwrap(),
6808 )
6809 });
6810
6811 // Insert some excerpts.
6812 _ = leader.update(cx, |leader, cx| {
6813 leader.buffer.update(cx, |multibuffer, cx| {
6814 let excerpt_ids = multibuffer.push_excerpts(
6815 buffer_1.clone(),
6816 [
6817 ExcerptRange {
6818 context: 1..6,
6819 primary: None,
6820 },
6821 ExcerptRange {
6822 context: 12..15,
6823 primary: None,
6824 },
6825 ExcerptRange {
6826 context: 0..3,
6827 primary: None,
6828 },
6829 ],
6830 cx,
6831 );
6832 multibuffer.insert_excerpts_after(
6833 excerpt_ids[0],
6834 buffer_2.clone(),
6835 [
6836 ExcerptRange {
6837 context: 8..12,
6838 primary: None,
6839 },
6840 ExcerptRange {
6841 context: 0..6,
6842 primary: None,
6843 },
6844 ],
6845 cx,
6846 );
6847 });
6848 });
6849
6850 // Apply the update of adding the excerpts.
6851 follower_1
6852 .update(cx, |follower, cx| {
6853 follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
6854 })
6855 .await
6856 .unwrap();
6857 assert_eq!(
6858 follower_1.update(cx, |editor, cx| editor.text(cx)),
6859 leader.update(cx, |editor, cx| editor.text(cx))
6860 );
6861 update_message.borrow_mut().take();
6862
6863 // Start following separately after it already has excerpts.
6864 let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
6865 let follower_2 = cx
6866 .update_window(*workspace.deref(), |_, cx| {
6867 Editor::from_state_proto(
6868 pane.clone(),
6869 workspace.root_view(cx).unwrap().clone(),
6870 ViewId {
6871 creator: Default::default(),
6872 id: 0,
6873 },
6874 &mut state_message,
6875 cx,
6876 )
6877 })
6878 .unwrap()
6879 .unwrap()
6880 .await
6881 .unwrap();
6882 assert_eq!(
6883 follower_2.update(cx, |editor, cx| editor.text(cx)),
6884 leader.update(cx, |editor, cx| editor.text(cx))
6885 );
6886
6887 // Remove some excerpts.
6888 _ = leader.update(cx, |leader, cx| {
6889 leader.buffer.update(cx, |multibuffer, cx| {
6890 let excerpt_ids = multibuffer.excerpt_ids();
6891 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
6892 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
6893 });
6894 });
6895
6896 // Apply the update of removing the excerpts.
6897 follower_1
6898 .update(cx, |follower, cx| {
6899 follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
6900 })
6901 .await
6902 .unwrap();
6903 follower_2
6904 .update(cx, |follower, cx| {
6905 follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
6906 })
6907 .await
6908 .unwrap();
6909 update_message.borrow_mut().take();
6910 assert_eq!(
6911 follower_1.update(cx, |editor, cx| editor.text(cx)),
6912 leader.update(cx, |editor, cx| editor.text(cx))
6913 );
6914}
6915
6916#[gpui::test]
6917async fn go_to_prev_overlapping_diagnostic(
6918 executor: BackgroundExecutor,
6919 cx: &mut gpui::TestAppContext,
6920) {
6921 init_test(cx, |_| {});
6922
6923 let mut cx = EditorTestContext::new(cx).await;
6924 let project = cx.update_editor(|editor, _| editor.project.clone().unwrap());
6925
6926 cx.set_state(indoc! {"
6927 ˇfn func(abc def: i32) -> u32 {
6928 }
6929 "});
6930
6931 _ = cx.update(|cx| {
6932 _ = project.update(cx, |project, cx| {
6933 project
6934 .update_diagnostics(
6935 LanguageServerId(0),
6936 lsp::PublishDiagnosticsParams {
6937 uri: lsp::Url::from_file_path("/root/file").unwrap(),
6938 version: None,
6939 diagnostics: vec![
6940 lsp::Diagnostic {
6941 range: lsp::Range::new(
6942 lsp::Position::new(0, 11),
6943 lsp::Position::new(0, 12),
6944 ),
6945 severity: Some(lsp::DiagnosticSeverity::ERROR),
6946 ..Default::default()
6947 },
6948 lsp::Diagnostic {
6949 range: lsp::Range::new(
6950 lsp::Position::new(0, 12),
6951 lsp::Position::new(0, 15),
6952 ),
6953 severity: Some(lsp::DiagnosticSeverity::ERROR),
6954 ..Default::default()
6955 },
6956 lsp::Diagnostic {
6957 range: lsp::Range::new(
6958 lsp::Position::new(0, 25),
6959 lsp::Position::new(0, 28),
6960 ),
6961 severity: Some(lsp::DiagnosticSeverity::ERROR),
6962 ..Default::default()
6963 },
6964 ],
6965 },
6966 &[],
6967 cx,
6968 )
6969 .unwrap()
6970 });
6971 });
6972
6973 executor.run_until_parked();
6974
6975 cx.update_editor(|editor, cx| {
6976 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
6977 });
6978
6979 cx.assert_editor_state(indoc! {"
6980 fn func(abc def: i32) -> ˇu32 {
6981 }
6982 "});
6983
6984 cx.update_editor(|editor, cx| {
6985 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
6986 });
6987
6988 cx.assert_editor_state(indoc! {"
6989 fn func(abc ˇdef: i32) -> u32 {
6990 }
6991 "});
6992
6993 cx.update_editor(|editor, cx| {
6994 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
6995 });
6996
6997 cx.assert_editor_state(indoc! {"
6998 fn func(abcˇ def: i32) -> u32 {
6999 }
7000 "});
7001
7002 cx.update_editor(|editor, cx| {
7003 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
7004 });
7005
7006 cx.assert_editor_state(indoc! {"
7007 fn func(abc def: i32) -> ˇu32 {
7008 }
7009 "});
7010}
7011
7012#[gpui::test]
7013async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
7014 init_test(cx, |_| {});
7015
7016 let mut cx = EditorTestContext::new(cx).await;
7017
7018 let diff_base = r#"
7019 use some::mod;
7020
7021 const A: u32 = 42;
7022
7023 fn main() {
7024 println!("hello");
7025
7026 println!("world");
7027 }
7028 "#
7029 .unindent();
7030
7031 // Edits are modified, removed, modified, added
7032 cx.set_state(
7033 &r#"
7034 use some::modified;
7035
7036 ˇ
7037 fn main() {
7038 println!("hello there");
7039
7040 println!("around the");
7041 println!("world");
7042 }
7043 "#
7044 .unindent(),
7045 );
7046
7047 cx.set_diff_base(Some(&diff_base));
7048 executor.run_until_parked();
7049
7050 cx.update_editor(|editor, cx| {
7051 //Wrap around the bottom of the buffer
7052 for _ in 0..3 {
7053 editor.go_to_hunk(&GoToHunk, cx);
7054 }
7055 });
7056
7057 cx.assert_editor_state(
7058 &r#"
7059 ˇuse some::modified;
7060
7061
7062 fn main() {
7063 println!("hello there");
7064
7065 println!("around the");
7066 println!("world");
7067 }
7068 "#
7069 .unindent(),
7070 );
7071
7072 cx.update_editor(|editor, cx| {
7073 //Wrap around the top of the buffer
7074 for _ in 0..2 {
7075 editor.go_to_prev_hunk(&GoToPrevHunk, cx);
7076 }
7077 });
7078
7079 cx.assert_editor_state(
7080 &r#"
7081 use some::modified;
7082
7083
7084 fn main() {
7085 ˇ println!("hello there");
7086
7087 println!("around the");
7088 println!("world");
7089 }
7090 "#
7091 .unindent(),
7092 );
7093
7094 cx.update_editor(|editor, cx| {
7095 editor.go_to_prev_hunk(&GoToPrevHunk, cx);
7096 });
7097
7098 cx.assert_editor_state(
7099 &r#"
7100 use some::modified;
7101
7102 ˇ
7103 fn main() {
7104 println!("hello there");
7105
7106 println!("around the");
7107 println!("world");
7108 }
7109 "#
7110 .unindent(),
7111 );
7112
7113 cx.update_editor(|editor, cx| {
7114 for _ in 0..3 {
7115 editor.go_to_prev_hunk(&GoToPrevHunk, cx);
7116 }
7117 });
7118
7119 cx.assert_editor_state(
7120 &r#"
7121 use some::modified;
7122
7123
7124 fn main() {
7125 ˇ println!("hello there");
7126
7127 println!("around the");
7128 println!("world");
7129 }
7130 "#
7131 .unindent(),
7132 );
7133
7134 cx.update_editor(|editor, cx| {
7135 editor.fold(&Fold, cx);
7136
7137 //Make sure that the fold only gets one hunk
7138 for _ in 0..4 {
7139 editor.go_to_hunk(&GoToHunk, cx);
7140 }
7141 });
7142
7143 cx.assert_editor_state(
7144 &r#"
7145 ˇuse some::modified;
7146
7147
7148 fn main() {
7149 println!("hello there");
7150
7151 println!("around the");
7152 println!("world");
7153 }
7154 "#
7155 .unindent(),
7156 );
7157}
7158
7159#[test]
7160fn test_split_words() {
7161 fn split<'a>(text: &'a str) -> Vec<&'a str> {
7162 split_words(text).collect()
7163 }
7164
7165 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
7166 assert_eq!(split("hello_world"), &["hello_", "world"]);
7167 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
7168 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
7169 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
7170 assert_eq!(split("helloworld"), &["helloworld"]);
7171}
7172
7173#[gpui::test]
7174async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
7175 init_test(cx, |_| {});
7176
7177 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
7178 let mut assert = |before, after| {
7179 let _state_context = cx.set_state(before);
7180 cx.update_editor(|editor, cx| {
7181 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, cx)
7182 });
7183 cx.assert_editor_state(after);
7184 };
7185
7186 // Outside bracket jumps to outside of matching bracket
7187 assert("console.logˇ(var);", "console.log(var)ˇ;");
7188 assert("console.log(var)ˇ;", "console.logˇ(var);");
7189
7190 // Inside bracket jumps to inside of matching bracket
7191 assert("console.log(ˇvar);", "console.log(varˇ);");
7192 assert("console.log(varˇ);", "console.log(ˇvar);");
7193
7194 // When outside a bracket and inside, favor jumping to the inside bracket
7195 assert(
7196 "console.log('foo', [1, 2, 3]ˇ);",
7197 "console.log(ˇ'foo', [1, 2, 3]);",
7198 );
7199 assert(
7200 "console.log(ˇ'foo', [1, 2, 3]);",
7201 "console.log('foo', [1, 2, 3]ˇ);",
7202 );
7203
7204 // Bias forward if two options are equally likely
7205 assert(
7206 "let result = curried_fun()ˇ();",
7207 "let result = curried_fun()()ˇ;",
7208 );
7209
7210 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
7211 assert(
7212 indoc! {"
7213 function test() {
7214 console.log('test')ˇ
7215 }"},
7216 indoc! {"
7217 function test() {
7218 console.logˇ('test')
7219 }"},
7220 );
7221}
7222
7223#[gpui::test(iterations = 10)]
7224async fn test_copilot(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
7225 // flaky
7226 init_test(cx, |_| {});
7227
7228 let (copilot, copilot_lsp) = Copilot::fake(cx);
7229 _ = cx.update(|cx| Copilot::set_global(copilot, cx));
7230 let mut cx = EditorLspTestContext::new_rust(
7231 lsp::ServerCapabilities {
7232 completion_provider: Some(lsp::CompletionOptions {
7233 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
7234 ..Default::default()
7235 }),
7236 ..Default::default()
7237 },
7238 cx,
7239 )
7240 .await;
7241
7242 // When inserting, ensure autocompletion is favored over Copilot suggestions.
7243 cx.set_state(indoc! {"
7244 oneˇ
7245 two
7246 three
7247 "});
7248 cx.simulate_keystroke(".");
7249 let _ = handle_completion_request(
7250 &mut cx,
7251 indoc! {"
7252 one.|<>
7253 two
7254 three
7255 "},
7256 vec!["completion_a", "completion_b"],
7257 );
7258 handle_copilot_completion_request(
7259 &copilot_lsp,
7260 vec![copilot::request::Completion {
7261 text: "one.copilot1".into(),
7262 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
7263 ..Default::default()
7264 }],
7265 vec![],
7266 );
7267 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7268 cx.update_editor(|editor, cx| {
7269 assert!(editor.context_menu_visible());
7270 assert!(!editor.has_active_copilot_suggestion(cx));
7271
7272 // Confirming a completion inserts it and hides the context menu, without showing
7273 // the copilot suggestion afterwards.
7274 editor
7275 .confirm_completion(&Default::default(), cx)
7276 .unwrap()
7277 .detach();
7278 assert!(!editor.context_menu_visible());
7279 assert!(!editor.has_active_copilot_suggestion(cx));
7280 assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");
7281 assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
7282 });
7283
7284 // Ensure Copilot suggestions are shown right away if no autocompletion is available.
7285 cx.set_state(indoc! {"
7286 oneˇ
7287 two
7288 three
7289 "});
7290 cx.simulate_keystroke(".");
7291 let _ = handle_completion_request(
7292 &mut cx,
7293 indoc! {"
7294 one.|<>
7295 two
7296 three
7297 "},
7298 vec![],
7299 );
7300 handle_copilot_completion_request(
7301 &copilot_lsp,
7302 vec![copilot::request::Completion {
7303 text: "one.copilot1".into(),
7304 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
7305 ..Default::default()
7306 }],
7307 vec![],
7308 );
7309 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7310 cx.update_editor(|editor, cx| {
7311 assert!(!editor.context_menu_visible());
7312 assert!(editor.has_active_copilot_suggestion(cx));
7313 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
7314 assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
7315 });
7316
7317 // Reset editor, and ensure autocompletion is still favored over Copilot suggestions.
7318 cx.set_state(indoc! {"
7319 oneˇ
7320 two
7321 three
7322 "});
7323 cx.simulate_keystroke(".");
7324 let _ = handle_completion_request(
7325 &mut cx,
7326 indoc! {"
7327 one.|<>
7328 two
7329 three
7330 "},
7331 vec!["completion_a", "completion_b"],
7332 );
7333 handle_copilot_completion_request(
7334 &copilot_lsp,
7335 vec![copilot::request::Completion {
7336 text: "one.copilot1".into(),
7337 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
7338 ..Default::default()
7339 }],
7340 vec![],
7341 );
7342 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7343 cx.update_editor(|editor, cx| {
7344 assert!(editor.context_menu_visible());
7345 assert!(!editor.has_active_copilot_suggestion(cx));
7346
7347 // When hiding the context menu, the Copilot suggestion becomes visible.
7348 editor.hide_context_menu(cx);
7349 assert!(!editor.context_menu_visible());
7350 assert!(editor.has_active_copilot_suggestion(cx));
7351 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
7352 assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
7353 });
7354
7355 // Ensure existing completion is interpolated when inserting again.
7356 cx.simulate_keystroke("c");
7357 executor.run_until_parked();
7358 cx.update_editor(|editor, cx| {
7359 assert!(!editor.context_menu_visible());
7360 assert!(editor.has_active_copilot_suggestion(cx));
7361 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
7362 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
7363 });
7364
7365 // After debouncing, new Copilot completions should be requested.
7366 handle_copilot_completion_request(
7367 &copilot_lsp,
7368 vec![copilot::request::Completion {
7369 text: "one.copilot2".into(),
7370 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
7371 ..Default::default()
7372 }],
7373 vec![],
7374 );
7375 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7376 cx.update_editor(|editor, cx| {
7377 assert!(!editor.context_menu_visible());
7378 assert!(editor.has_active_copilot_suggestion(cx));
7379 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
7380 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
7381
7382 // Canceling should remove the active Copilot suggestion.
7383 editor.cancel(&Default::default(), cx);
7384 assert!(!editor.has_active_copilot_suggestion(cx));
7385 assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
7386 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
7387
7388 // After canceling, tabbing shouldn't insert the previously shown suggestion.
7389 editor.tab(&Default::default(), cx);
7390 assert!(!editor.has_active_copilot_suggestion(cx));
7391 assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n");
7392 assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n");
7393
7394 // When undoing the previously active suggestion is shown again.
7395 editor.undo(&Default::default(), cx);
7396 assert!(editor.has_active_copilot_suggestion(cx));
7397 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
7398 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
7399 });
7400
7401 // If an edit occurs outside of this editor, the suggestion is still correctly interpolated.
7402 cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx));
7403 cx.update_editor(|editor, cx| {
7404 assert!(editor.has_active_copilot_suggestion(cx));
7405 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
7406 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
7407
7408 // Tabbing when there is an active suggestion inserts it.
7409 editor.tab(&Default::default(), cx);
7410 assert!(!editor.has_active_copilot_suggestion(cx));
7411 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
7412 assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n");
7413
7414 // When undoing the previously active suggestion is shown again.
7415 editor.undo(&Default::default(), cx);
7416 assert!(editor.has_active_copilot_suggestion(cx));
7417 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
7418 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
7419
7420 // Hide suggestion.
7421 editor.cancel(&Default::default(), cx);
7422 assert!(!editor.has_active_copilot_suggestion(cx));
7423 assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n");
7424 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
7425 });
7426
7427 // If an edit occurs outside of this editor but no suggestion is being shown,
7428 // we won't make it visible.
7429 cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx));
7430 cx.update_editor(|editor, cx| {
7431 assert!(!editor.has_active_copilot_suggestion(cx));
7432 assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
7433 assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
7434 });
7435
7436 // Reset the editor to verify how suggestions behave when tabbing on leading indentation.
7437 cx.update_editor(|editor, cx| {
7438 editor.set_text("fn foo() {\n \n}", cx);
7439 editor.change_selections(None, cx, |s| {
7440 s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
7441 });
7442 });
7443 handle_copilot_completion_request(
7444 &copilot_lsp,
7445 vec![copilot::request::Completion {
7446 text: " let x = 4;".into(),
7447 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
7448 ..Default::default()
7449 }],
7450 vec![],
7451 );
7452
7453 cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx));
7454 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7455 cx.update_editor(|editor, cx| {
7456 assert!(editor.has_active_copilot_suggestion(cx));
7457 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
7458 assert_eq!(editor.text(cx), "fn foo() {\n \n}");
7459
7460 // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
7461 editor.tab(&Default::default(), cx);
7462 assert!(editor.has_active_copilot_suggestion(cx));
7463 assert_eq!(editor.text(cx), "fn foo() {\n \n}");
7464 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
7465
7466 // Tabbing again accepts the suggestion.
7467 editor.tab(&Default::default(), cx);
7468 assert!(!editor.has_active_copilot_suggestion(cx));
7469 assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
7470 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
7471 });
7472}
7473
7474#[gpui::test]
7475async fn test_copilot_completion_invalidation(
7476 executor: BackgroundExecutor,
7477 cx: &mut gpui::TestAppContext,
7478) {
7479 init_test(cx, |_| {});
7480
7481 let (copilot, copilot_lsp) = Copilot::fake(cx);
7482 _ = cx.update(|cx| Copilot::set_global(copilot, cx));
7483 let mut cx = EditorLspTestContext::new_rust(
7484 lsp::ServerCapabilities {
7485 completion_provider: Some(lsp::CompletionOptions {
7486 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
7487 ..Default::default()
7488 }),
7489 ..Default::default()
7490 },
7491 cx,
7492 )
7493 .await;
7494
7495 cx.set_state(indoc! {"
7496 one
7497 twˇ
7498 three
7499 "});
7500
7501 handle_copilot_completion_request(
7502 &copilot_lsp,
7503 vec![copilot::request::Completion {
7504 text: "two.foo()".into(),
7505 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
7506 ..Default::default()
7507 }],
7508 vec![],
7509 );
7510 cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx));
7511 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7512 cx.update_editor(|editor, cx| {
7513 assert!(editor.has_active_copilot_suggestion(cx));
7514 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
7515 assert_eq!(editor.text(cx), "one\ntw\nthree\n");
7516
7517 editor.backspace(&Default::default(), cx);
7518 assert!(editor.has_active_copilot_suggestion(cx));
7519 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
7520 assert_eq!(editor.text(cx), "one\nt\nthree\n");
7521
7522 editor.backspace(&Default::default(), cx);
7523 assert!(editor.has_active_copilot_suggestion(cx));
7524 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
7525 assert_eq!(editor.text(cx), "one\n\nthree\n");
7526
7527 // Deleting across the original suggestion range invalidates it.
7528 editor.backspace(&Default::default(), cx);
7529 assert!(!editor.has_active_copilot_suggestion(cx));
7530 assert_eq!(editor.display_text(cx), "one\nthree\n");
7531 assert_eq!(editor.text(cx), "one\nthree\n");
7532
7533 // Undoing the deletion restores the suggestion.
7534 editor.undo(&Default::default(), cx);
7535 assert!(editor.has_active_copilot_suggestion(cx));
7536 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
7537 assert_eq!(editor.text(cx), "one\n\nthree\n");
7538 });
7539}
7540
7541#[gpui::test]
7542async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
7543 init_test(cx, |_| {});
7544
7545 let (copilot, copilot_lsp) = Copilot::fake(cx);
7546 _ = cx.update(|cx| Copilot::set_global(copilot, cx));
7547
7548 let buffer_1 = cx.new_model(|cx| {
7549 Buffer::new(
7550 0,
7551 BufferId::new(cx.entity_id().as_u64()).unwrap(),
7552 "a = 1\nb = 2\n",
7553 )
7554 });
7555 let buffer_2 = cx.new_model(|cx| {
7556 Buffer::new(
7557 0,
7558 BufferId::new(cx.entity_id().as_u64()).unwrap(),
7559 "c = 3\nd = 4\n",
7560 )
7561 });
7562 let multibuffer = cx.new_model(|cx| {
7563 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
7564 multibuffer.push_excerpts(
7565 buffer_1.clone(),
7566 [ExcerptRange {
7567 context: Point::new(0, 0)..Point::new(2, 0),
7568 primary: None,
7569 }],
7570 cx,
7571 );
7572 multibuffer.push_excerpts(
7573 buffer_2.clone(),
7574 [ExcerptRange {
7575 context: Point::new(0, 0)..Point::new(2, 0),
7576 primary: None,
7577 }],
7578 cx,
7579 );
7580 multibuffer
7581 });
7582 let editor = cx.add_window(|cx| build_editor(multibuffer, cx));
7583
7584 handle_copilot_completion_request(
7585 &copilot_lsp,
7586 vec![copilot::request::Completion {
7587 text: "b = 2 + a".into(),
7588 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
7589 ..Default::default()
7590 }],
7591 vec![],
7592 );
7593 _ = editor.update(cx, |editor, cx| {
7594 // Ensure copilot suggestions are shown for the first excerpt.
7595 editor.change_selections(None, cx, |s| {
7596 s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
7597 });
7598 editor.next_copilot_suggestion(&Default::default(), cx);
7599 });
7600 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7601 _ = editor.update(cx, |editor, cx| {
7602 assert!(editor.has_active_copilot_suggestion(cx));
7603 assert_eq!(
7604 editor.display_text(cx),
7605 "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n"
7606 );
7607 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
7608 });
7609
7610 handle_copilot_completion_request(
7611 &copilot_lsp,
7612 vec![copilot::request::Completion {
7613 text: "d = 4 + c".into(),
7614 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
7615 ..Default::default()
7616 }],
7617 vec![],
7618 );
7619 _ = editor.update(cx, |editor, cx| {
7620 // Move to another excerpt, ensuring the suggestion gets cleared.
7621 editor.change_selections(None, cx, |s| {
7622 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
7623 });
7624 assert!(!editor.has_active_copilot_suggestion(cx));
7625 assert_eq!(
7626 editor.display_text(cx),
7627 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n"
7628 );
7629 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
7630
7631 // Type a character, ensuring we don't even try to interpolate the previous suggestion.
7632 editor.handle_input(" ", cx);
7633 assert!(!editor.has_active_copilot_suggestion(cx));
7634 assert_eq!(
7635 editor.display_text(cx),
7636 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n"
7637 );
7638 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
7639 });
7640
7641 // Ensure the new suggestion is displayed when the debounce timeout expires.
7642 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7643 _ = editor.update(cx, |editor, cx| {
7644 assert!(editor.has_active_copilot_suggestion(cx));
7645 assert_eq!(
7646 editor.display_text(cx),
7647 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n"
7648 );
7649 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
7650 });
7651}
7652
7653#[gpui::test]
7654async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
7655 init_test(cx, |settings| {
7656 settings
7657 .copilot
7658 .get_or_insert(Default::default())
7659 .disabled_globs = Some(vec![".env*".to_string()]);
7660 });
7661
7662 let (copilot, copilot_lsp) = Copilot::fake(cx);
7663 _ = cx.update(|cx| Copilot::set_global(copilot, cx));
7664
7665 let fs = FakeFs::new(cx.executor());
7666 fs.insert_tree(
7667 "/test",
7668 json!({
7669 ".env": "SECRET=something\n",
7670 "README.md": "hello\n"
7671 }),
7672 )
7673 .await;
7674 let project = Project::test(fs, ["/test".as_ref()], cx).await;
7675
7676 let private_buffer = project
7677 .update(cx, |project, cx| {
7678 project.open_local_buffer("/test/.env", cx)
7679 })
7680 .await
7681 .unwrap();
7682 let public_buffer = project
7683 .update(cx, |project, cx| {
7684 project.open_local_buffer("/test/README.md", cx)
7685 })
7686 .await
7687 .unwrap();
7688
7689 let multibuffer = cx.new_model(|cx| {
7690 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
7691 multibuffer.push_excerpts(
7692 private_buffer.clone(),
7693 [ExcerptRange {
7694 context: Point::new(0, 0)..Point::new(1, 0),
7695 primary: None,
7696 }],
7697 cx,
7698 );
7699 multibuffer.push_excerpts(
7700 public_buffer.clone(),
7701 [ExcerptRange {
7702 context: Point::new(0, 0)..Point::new(1, 0),
7703 primary: None,
7704 }],
7705 cx,
7706 );
7707 multibuffer
7708 });
7709 let editor = cx.add_window(|cx| build_editor(multibuffer, cx));
7710
7711 let mut copilot_requests = copilot_lsp
7712 .handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| async move {
7713 Ok(copilot::request::GetCompletionsResult {
7714 completions: vec![copilot::request::Completion {
7715 text: "next line".into(),
7716 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
7717 ..Default::default()
7718 }],
7719 })
7720 });
7721
7722 _ = editor.update(cx, |editor, cx| {
7723 editor.change_selections(None, cx, |selections| {
7724 selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
7725 });
7726 editor.next_copilot_suggestion(&Default::default(), cx);
7727 });
7728
7729 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7730 assert!(copilot_requests.try_next().is_err());
7731
7732 _ = editor.update(cx, |editor, cx| {
7733 editor.change_selections(None, cx, |s| {
7734 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
7735 });
7736 editor.next_copilot_suggestion(&Default::default(), cx);
7737 });
7738
7739 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7740 assert!(copilot_requests.try_next().is_ok());
7741}
7742
7743#[gpui::test]
7744async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
7745 init_test(cx, |_| {});
7746
7747 let mut language = Language::new(
7748 LanguageConfig {
7749 name: "Rust".into(),
7750 path_suffixes: vec!["rs".to_string()],
7751 brackets: BracketPairConfig {
7752 pairs: vec![BracketPair {
7753 start: "{".to_string(),
7754 end: "}".to_string(),
7755 close: true,
7756 newline: true,
7757 }],
7758 disabled_scopes_by_bracket_ix: Vec::new(),
7759 },
7760 ..Default::default()
7761 },
7762 Some(tree_sitter_rust::language()),
7763 );
7764 let mut fake_servers = language
7765 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
7766 capabilities: lsp::ServerCapabilities {
7767 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
7768 first_trigger_character: "{".to_string(),
7769 more_trigger_character: None,
7770 }),
7771 ..Default::default()
7772 },
7773 ..Default::default()
7774 }))
7775 .await;
7776
7777 let fs = FakeFs::new(cx.executor());
7778 fs.insert_tree(
7779 "/a",
7780 json!({
7781 "main.rs": "fn main() { let a = 5; }",
7782 "other.rs": "// Test file",
7783 }),
7784 )
7785 .await;
7786 let project = Project::test(fs, ["/a".as_ref()], cx).await;
7787 _ = project.update(cx, |project, _| project.languages().add(Arc::new(language)));
7788 let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
7789
7790 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7791
7792 let worktree_id = workspace
7793 .update(cx, |workspace, cx| {
7794 workspace.project().update(cx, |project, cx| {
7795 project.worktrees().next().unwrap().read(cx).id()
7796 })
7797 })
7798 .unwrap();
7799
7800 let buffer = project
7801 .update(cx, |project, cx| {
7802 project.open_local_buffer("/a/main.rs", cx)
7803 })
7804 .await
7805 .unwrap();
7806 cx.executor().run_until_parked();
7807 cx.executor().start_waiting();
7808 let fake_server = fake_servers.next().await.unwrap();
7809 let editor_handle = workspace
7810 .update(cx, |workspace, cx| {
7811 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
7812 })
7813 .unwrap()
7814 .await
7815 .unwrap()
7816 .downcast::<Editor>()
7817 .unwrap();
7818
7819 fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
7820 assert_eq!(
7821 params.text_document_position.text_document.uri,
7822 lsp::Url::from_file_path("/a/main.rs").unwrap(),
7823 );
7824 assert_eq!(
7825 params.text_document_position.position,
7826 lsp::Position::new(0, 21),
7827 );
7828
7829 Ok(Some(vec![lsp::TextEdit {
7830 new_text: "]".to_string(),
7831 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
7832 }]))
7833 });
7834
7835 editor_handle.update(cx, |editor, cx| {
7836 editor.focus(cx);
7837 editor.change_selections(None, cx, |s| {
7838 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
7839 });
7840 editor.handle_input("{", cx);
7841 });
7842
7843 cx.executor().run_until_parked();
7844
7845 _ = buffer.update(cx, |buffer, _| {
7846 assert_eq!(
7847 buffer.text(),
7848 "fn main() { let a = {5}; }",
7849 "No extra braces from on type formatting should appear in the buffer"
7850 )
7851 });
7852}
7853
7854#[gpui::test]
7855async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) {
7856 init_test(cx, |_| {});
7857
7858 let language_name: Arc<str> = "Rust".into();
7859 let mut language = Language::new(
7860 LanguageConfig {
7861 name: Arc::clone(&language_name),
7862 path_suffixes: vec!["rs".to_string()],
7863 ..Default::default()
7864 },
7865 Some(tree_sitter_rust::language()),
7866 );
7867
7868 let server_restarts = Arc::new(AtomicUsize::new(0));
7869 let closure_restarts = Arc::clone(&server_restarts);
7870 let language_server_name = "test language server";
7871 let mut fake_servers = language
7872 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
7873 name: language_server_name,
7874 initialization_options: Some(json!({
7875 "testOptionValue": true
7876 })),
7877 initializer: Some(Box::new(move |fake_server| {
7878 let task_restarts = Arc::clone(&closure_restarts);
7879 fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
7880 task_restarts.fetch_add(1, atomic::Ordering::Release);
7881 futures::future::ready(Ok(()))
7882 });
7883 })),
7884 ..Default::default()
7885 }))
7886 .await;
7887
7888 let fs = FakeFs::new(cx.executor());
7889 fs.insert_tree(
7890 "/a",
7891 json!({
7892 "main.rs": "fn main() { let a = 5; }",
7893 "other.rs": "// Test file",
7894 }),
7895 )
7896 .await;
7897 let project = Project::test(fs, ["/a".as_ref()], cx).await;
7898 _ = project.update(cx, |project, _| project.languages().add(Arc::new(language)));
7899 let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
7900 let _buffer = project
7901 .update(cx, |project, cx| {
7902 project.open_local_buffer("/a/main.rs", cx)
7903 })
7904 .await
7905 .unwrap();
7906 let _fake_server = fake_servers.next().await.unwrap();
7907 update_test_language_settings(cx, |language_settings| {
7908 language_settings.languages.insert(
7909 Arc::clone(&language_name),
7910 LanguageSettingsContent {
7911 tab_size: NonZeroU32::new(8),
7912 ..Default::default()
7913 },
7914 );
7915 });
7916 cx.executor().run_until_parked();
7917 assert_eq!(
7918 server_restarts.load(atomic::Ordering::Acquire),
7919 0,
7920 "Should not restart LSP server on an unrelated change"
7921 );
7922
7923 update_test_project_settings(cx, |project_settings| {
7924 project_settings.lsp.insert(
7925 "Some other server name".into(),
7926 LspSettings {
7927 initialization_options: Some(json!({
7928 "some other init value": false
7929 })),
7930 },
7931 );
7932 });
7933 cx.executor().run_until_parked();
7934 assert_eq!(
7935 server_restarts.load(atomic::Ordering::Acquire),
7936 0,
7937 "Should not restart LSP server on an unrelated LSP settings change"
7938 );
7939
7940 update_test_project_settings(cx, |project_settings| {
7941 project_settings.lsp.insert(
7942 language_server_name.into(),
7943 LspSettings {
7944 initialization_options: Some(json!({
7945 "anotherInitValue": false
7946 })),
7947 },
7948 );
7949 });
7950 cx.executor().run_until_parked();
7951 assert_eq!(
7952 server_restarts.load(atomic::Ordering::Acquire),
7953 1,
7954 "Should restart LSP server on a related LSP settings change"
7955 );
7956
7957 update_test_project_settings(cx, |project_settings| {
7958 project_settings.lsp.insert(
7959 language_server_name.into(),
7960 LspSettings {
7961 initialization_options: Some(json!({
7962 "anotherInitValue": false
7963 })),
7964 },
7965 );
7966 });
7967 cx.executor().run_until_parked();
7968 assert_eq!(
7969 server_restarts.load(atomic::Ordering::Acquire),
7970 1,
7971 "Should not restart LSP server on a related LSP settings change that is the same"
7972 );
7973
7974 update_test_project_settings(cx, |project_settings| {
7975 project_settings.lsp.insert(
7976 language_server_name.into(),
7977 LspSettings {
7978 initialization_options: None,
7979 },
7980 );
7981 });
7982 cx.executor().run_until_parked();
7983 assert_eq!(
7984 server_restarts.load(atomic::Ordering::Acquire),
7985 2,
7986 "Should restart LSP server on another related LSP settings change"
7987 );
7988}
7989
7990#[gpui::test]
7991async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
7992 init_test(cx, |_| {});
7993
7994 let mut cx = EditorLspTestContext::new_rust(
7995 lsp::ServerCapabilities {
7996 completion_provider: Some(lsp::CompletionOptions {
7997 trigger_characters: Some(vec![".".to_string()]),
7998 resolve_provider: Some(true),
7999 ..Default::default()
8000 }),
8001 ..Default::default()
8002 },
8003 cx,
8004 )
8005 .await;
8006
8007 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
8008 cx.simulate_keystroke(".");
8009 let completion_item = lsp::CompletionItem {
8010 label: "some".into(),
8011 kind: Some(lsp::CompletionItemKind::SNIPPET),
8012 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
8013 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
8014 kind: lsp::MarkupKind::Markdown,
8015 value: "```rust\nSome(2)\n```".to_string(),
8016 })),
8017 deprecated: Some(false),
8018 sort_text: Some("fffffff2".to_string()),
8019 filter_text: Some("some".to_string()),
8020 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
8021 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8022 range: lsp::Range {
8023 start: lsp::Position {
8024 line: 0,
8025 character: 22,
8026 },
8027 end: lsp::Position {
8028 line: 0,
8029 character: 22,
8030 },
8031 },
8032 new_text: "Some(2)".to_string(),
8033 })),
8034 additional_text_edits: Some(vec![lsp::TextEdit {
8035 range: lsp::Range {
8036 start: lsp::Position {
8037 line: 0,
8038 character: 20,
8039 },
8040 end: lsp::Position {
8041 line: 0,
8042 character: 22,
8043 },
8044 },
8045 new_text: "".to_string(),
8046 }]),
8047 ..Default::default()
8048 };
8049
8050 let closure_completion_item = completion_item.clone();
8051 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
8052 let task_completion_item = closure_completion_item.clone();
8053 async move {
8054 Ok(Some(lsp::CompletionResponse::Array(vec![
8055 task_completion_item,
8056 ])))
8057 }
8058 });
8059
8060 request.next().await;
8061
8062 cx.condition(|editor, _| editor.context_menu_visible())
8063 .await;
8064 let apply_additional_edits = cx.update_editor(|editor, cx| {
8065 editor
8066 .confirm_completion(&ConfirmCompletion::default(), cx)
8067 .unwrap()
8068 });
8069 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
8070
8071 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
8072 let task_completion_item = completion_item.clone();
8073 async move { Ok(task_completion_item) }
8074 })
8075 .next()
8076 .await
8077 .unwrap();
8078 apply_additional_edits.await.unwrap();
8079 cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
8080}
8081
8082#[gpui::test]
8083async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
8084 init_test(cx, |_| {});
8085
8086 let mut cx = EditorLspTestContext::new(
8087 Language::new(
8088 LanguageConfig {
8089 path_suffixes: vec!["jsx".into()],
8090 overrides: [(
8091 "element".into(),
8092 LanguageConfigOverride {
8093 word_characters: Override::Set(['-'].into_iter().collect()),
8094 ..Default::default()
8095 },
8096 )]
8097 .into_iter()
8098 .collect(),
8099 ..Default::default()
8100 },
8101 Some(tree_sitter_typescript::language_tsx()),
8102 )
8103 .with_override_query("(jsx_self_closing_element) @element")
8104 .unwrap(),
8105 lsp::ServerCapabilities {
8106 completion_provider: Some(lsp::CompletionOptions {
8107 trigger_characters: Some(vec![":".to_string()]),
8108 ..Default::default()
8109 }),
8110 ..Default::default()
8111 },
8112 cx,
8113 )
8114 .await;
8115
8116 cx.lsp
8117 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
8118 Ok(Some(lsp::CompletionResponse::Array(vec![
8119 lsp::CompletionItem {
8120 label: "bg-blue".into(),
8121 ..Default::default()
8122 },
8123 lsp::CompletionItem {
8124 label: "bg-red".into(),
8125 ..Default::default()
8126 },
8127 lsp::CompletionItem {
8128 label: "bg-yellow".into(),
8129 ..Default::default()
8130 },
8131 ])))
8132 });
8133
8134 cx.set_state(r#"<p class="bgˇ" />"#);
8135
8136 // Trigger completion when typing a dash, because the dash is an extra
8137 // word character in the 'element' scope, which contains the cursor.
8138 cx.simulate_keystroke("-");
8139 cx.executor().run_until_parked();
8140 cx.update_editor(|editor, _| {
8141 if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
8142 assert_eq!(
8143 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
8144 &["bg-red", "bg-blue", "bg-yellow"]
8145 );
8146 } else {
8147 panic!("expected completion menu to be open");
8148 }
8149 });
8150
8151 cx.simulate_keystroke("l");
8152 cx.executor().run_until_parked();
8153 cx.update_editor(|editor, _| {
8154 if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
8155 assert_eq!(
8156 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
8157 &["bg-blue", "bg-yellow"]
8158 );
8159 } else {
8160 panic!("expected completion menu to be open");
8161 }
8162 });
8163
8164 // When filtering completions, consider the character after the '-' to
8165 // be the start of a subword.
8166 cx.set_state(r#"<p class="yelˇ" />"#);
8167 cx.simulate_keystroke("l");
8168 cx.executor().run_until_parked();
8169 cx.update_editor(|editor, _| {
8170 if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
8171 assert_eq!(
8172 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
8173 &["bg-yellow"]
8174 );
8175 } else {
8176 panic!("expected completion menu to be open");
8177 }
8178 });
8179}
8180
8181#[gpui::test]
8182async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
8183 init_test(cx, |settings| {
8184 settings.defaults.formatter = Some(language_settings::Formatter::Prettier)
8185 });
8186
8187 let mut language = Language::new(
8188 LanguageConfig {
8189 name: "Rust".into(),
8190 path_suffixes: vec!["rs".to_string()],
8191 prettier_parser_name: Some("test_parser".to_string()),
8192 ..Default::default()
8193 },
8194 Some(tree_sitter_rust::language()),
8195 );
8196
8197 let test_plugin = "test_plugin";
8198 let _ = language
8199 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
8200 prettier_plugins: vec![test_plugin],
8201 ..Default::default()
8202 }))
8203 .await;
8204
8205 let fs = FakeFs::new(cx.executor());
8206 fs.insert_file("/file.rs", Default::default()).await;
8207
8208 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
8209 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
8210 _ = project.update(cx, |project, _| {
8211 project.languages().add(Arc::new(language));
8212 });
8213 let buffer = project
8214 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
8215 .await
8216 .unwrap();
8217
8218 let buffer_text = "one\ntwo\nthree\n";
8219 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
8220 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
8221 _ = editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx));
8222
8223 editor
8224 .update(cx, |editor, cx| {
8225 editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
8226 })
8227 .unwrap()
8228 .await;
8229 assert_eq!(
8230 editor.update(cx, |editor, cx| editor.text(cx)),
8231 buffer_text.to_string() + prettier_format_suffix,
8232 "Test prettier formatting was not applied to the original buffer text",
8233 );
8234
8235 update_test_language_settings(cx, |settings| {
8236 settings.defaults.formatter = Some(language_settings::Formatter::Auto)
8237 });
8238 let format = editor.update(cx, |editor, cx| {
8239 editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
8240 });
8241 format.await.unwrap();
8242 assert_eq!(
8243 editor.update(cx, |editor, cx| editor.text(cx)),
8244 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
8245 "Autoformatting (via test prettier) was not applied to the original buffer text",
8246 );
8247}
8248
8249fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
8250 let point = DisplayPoint::new(row as u32, column as u32);
8251 point..point
8252}
8253
8254fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewContext<Editor>) {
8255 let (text, ranges) = marked_text_ranges(marked_text, true);
8256 assert_eq!(view.text(cx), text);
8257 assert_eq!(
8258 view.selections.ranges(cx),
8259 ranges,
8260 "Assert selections are {}",
8261 marked_text
8262 );
8263}
8264
8265/// Handle completion request passing a marked string specifying where the completion
8266/// should be triggered from using '|' character, what range should be replaced, and what completions
8267/// should be returned using '<' and '>' to delimit the range
8268pub fn handle_completion_request(
8269 cx: &mut EditorLspTestContext,
8270 marked_string: &str,
8271 completions: Vec<&'static str>,
8272) -> impl Future<Output = ()> {
8273 let complete_from_marker: TextRangeMarker = '|'.into();
8274 let replace_range_marker: TextRangeMarker = ('<', '>').into();
8275 let (_, mut marked_ranges) = marked_text_ranges_by(
8276 marked_string,
8277 vec![complete_from_marker.clone(), replace_range_marker.clone()],
8278 );
8279
8280 let complete_from_position =
8281 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
8282 let replace_range =
8283 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
8284
8285 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
8286 let completions = completions.clone();
8287 async move {
8288 assert_eq!(params.text_document_position.text_document.uri, url.clone());
8289 assert_eq!(
8290 params.text_document_position.position,
8291 complete_from_position
8292 );
8293 Ok(Some(lsp::CompletionResponse::Array(
8294 completions
8295 .iter()
8296 .map(|completion_text| lsp::CompletionItem {
8297 label: completion_text.to_string(),
8298 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8299 range: replace_range,
8300 new_text: completion_text.to_string(),
8301 })),
8302 ..Default::default()
8303 })
8304 .collect(),
8305 )))
8306 }
8307 });
8308
8309 async move {
8310 request.next().await;
8311 }
8312}
8313
8314fn handle_resolve_completion_request(
8315 cx: &mut EditorLspTestContext,
8316 edits: Option<Vec<(&'static str, &'static str)>>,
8317) -> impl Future<Output = ()> {
8318 let edits = edits.map(|edits| {
8319 edits
8320 .iter()
8321 .map(|(marked_string, new_text)| {
8322 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
8323 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
8324 lsp::TextEdit::new(replace_range, new_text.to_string())
8325 })
8326 .collect::<Vec<_>>()
8327 });
8328
8329 let mut request =
8330 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
8331 let edits = edits.clone();
8332 async move {
8333 Ok(lsp::CompletionItem {
8334 additional_text_edits: edits,
8335 ..Default::default()
8336 })
8337 }
8338 });
8339
8340 async move {
8341 request.next().await;
8342 }
8343}
8344
8345fn handle_copilot_completion_request(
8346 lsp: &lsp::FakeLanguageServer,
8347 completions: Vec<copilot::request::Completion>,
8348 completions_cycling: Vec<copilot::request::Completion>,
8349) {
8350 lsp.handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| {
8351 let completions = completions.clone();
8352 async move {
8353 Ok(copilot::request::GetCompletionsResult {
8354 completions: completions.clone(),
8355 })
8356 }
8357 });
8358 lsp.handle_request::<copilot::request::GetCompletionsCycling, _, _>(move |_params, _cx| {
8359 let completions_cycling = completions_cycling.clone();
8360 async move {
8361 Ok(copilot::request::GetCompletionsResult {
8362 completions: completions_cycling.clone(),
8363 })
8364 }
8365 });
8366}
8367
8368pub(crate) fn update_test_language_settings(
8369 cx: &mut TestAppContext,
8370 f: impl Fn(&mut AllLanguageSettingsContent),
8371) {
8372 _ = cx.update(|cx| {
8373 cx.update_global(|store: &mut SettingsStore, cx| {
8374 store.update_user_settings::<AllLanguageSettings>(cx, f);
8375 });
8376 });
8377}
8378
8379pub(crate) fn update_test_project_settings(
8380 cx: &mut TestAppContext,
8381 f: impl Fn(&mut ProjectSettings),
8382) {
8383 _ = cx.update(|cx| {
8384 cx.update_global(|store: &mut SettingsStore, cx| {
8385 store.update_user_settings::<ProjectSettings>(cx, f);
8386 });
8387 });
8388}
8389
8390pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
8391 _ = cx.update(|cx| {
8392 let store = SettingsStore::test(cx);
8393 cx.set_global(store);
8394 theme::init(theme::LoadThemes::JustBase, cx);
8395 release_channel::init("0.0.0", cx);
8396 client::init_settings(cx);
8397 language::init(cx);
8398 Project::init_settings(cx);
8399 workspace::init_settings(cx);
8400 crate::init(cx);
8401 });
8402
8403 update_test_language_settings(cx, f);
8404}