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