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