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