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