1use super::*;
2use crate::{
3 scroll::scroll_amount::ScrollAmount,
4 test::{
5 assert_text_with_selections, build_editor, editor_hunks,
6 editor_lsp_test_context::EditorLspTestContext, editor_test_context::EditorTestContext,
7 expanded_hunks, expanded_hunks_background_highlights, select_ranges,
8 },
9 JoinLines,
10};
11use futures::StreamExt;
12use gpui::{div, TestAppContext, VisualTestContext, WindowOptions};
13use indoc::indoc;
14use language::{
15 language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
16 BracketPairConfig,
17 Capability::ReadWrite,
18 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, 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::local("123456", cx);
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::local("123456", cx));
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::local("abcde", cx);
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 = BufferId::new(999).ok();
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_comments: vec!["//".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 =
2345 cx.new_model(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
2346 let rust_buffer = cx.new_model(|cx| {
2347 Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx)
2348 });
2349 let multibuffer = cx.new_model(|cx| {
2350 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
2351 multibuffer.push_excerpts(
2352 toml_buffer.clone(),
2353 [ExcerptRange {
2354 context: Point::new(0, 0)..Point::new(2, 0),
2355 primary: None,
2356 }],
2357 cx,
2358 );
2359 multibuffer.push_excerpts(
2360 rust_buffer.clone(),
2361 [ExcerptRange {
2362 context: Point::new(0, 0)..Point::new(1, 0),
2363 primary: None,
2364 }],
2365 cx,
2366 );
2367 multibuffer
2368 });
2369
2370 cx.add_window(|cx| {
2371 let mut editor = build_editor(multibuffer, cx);
2372
2373 assert_eq!(
2374 editor.text(cx),
2375 indoc! {"
2376 a = 1
2377 b = 2
2378
2379 const c: usize = 3;
2380 "}
2381 );
2382
2383 select_ranges(
2384 &mut editor,
2385 indoc! {"
2386 «aˇ» = 1
2387 b = 2
2388
2389 «const c:ˇ» usize = 3;
2390 "},
2391 cx,
2392 );
2393
2394 editor.tab(&Tab, cx);
2395 assert_text_with_selections(
2396 &mut editor,
2397 indoc! {"
2398 «aˇ» = 1
2399 b = 2
2400
2401 «const c:ˇ» usize = 3;
2402 "},
2403 cx,
2404 );
2405 editor.tab_prev(&TabPrev, cx);
2406 assert_text_with_selections(
2407 &mut editor,
2408 indoc! {"
2409 «aˇ» = 1
2410 b = 2
2411
2412 «const c:ˇ» usize = 3;
2413 "},
2414 cx,
2415 );
2416
2417 editor
2418 });
2419}
2420
2421#[gpui::test]
2422async fn test_backspace(cx: &mut gpui::TestAppContext) {
2423 init_test(cx, |_| {});
2424
2425 let mut cx = EditorTestContext::new(cx).await;
2426
2427 // Basic backspace
2428 cx.set_state(indoc! {"
2429 onˇe two three
2430 fou«rˇ» five six
2431 seven «ˇeight nine
2432 »ten
2433 "});
2434 cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
2435 cx.assert_editor_state(indoc! {"
2436 oˇe two three
2437 fouˇ five six
2438 seven ˇten
2439 "});
2440
2441 // Test backspace inside and around indents
2442 cx.set_state(indoc! {"
2443 zero
2444 ˇone
2445 ˇtwo
2446 ˇ ˇ ˇ three
2447 ˇ ˇ four
2448 "});
2449 cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
2450 cx.assert_editor_state(indoc! {"
2451 zero
2452 ˇone
2453 ˇtwo
2454 ˇ threeˇ four
2455 "});
2456
2457 // Test backspace with line_mode set to true
2458 cx.update_editor(|e, _| e.selections.line_mode = true);
2459 cx.set_state(indoc! {"
2460 The ˇquick ˇbrown
2461 fox jumps over
2462 the lazy dog
2463 ˇThe qu«ick bˇ»rown"});
2464 cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
2465 cx.assert_editor_state(indoc! {"
2466 ˇfox jumps over
2467 the lazy dogˇ"});
2468}
2469
2470#[gpui::test]
2471async fn test_delete(cx: &mut gpui::TestAppContext) {
2472 init_test(cx, |_| {});
2473
2474 let mut cx = EditorTestContext::new(cx).await;
2475 cx.set_state(indoc! {"
2476 onˇe two three
2477 fou«rˇ» five six
2478 seven «ˇeight nine
2479 »ten
2480 "});
2481 cx.update_editor(|e, cx| e.delete(&Delete, cx));
2482 cx.assert_editor_state(indoc! {"
2483 onˇ two three
2484 fouˇ five six
2485 seven ˇten
2486 "});
2487
2488 // Test backspace with line_mode set to true
2489 cx.update_editor(|e, _| e.selections.line_mode = true);
2490 cx.set_state(indoc! {"
2491 The ˇquick ˇbrown
2492 fox «ˇjum»ps over
2493 the lazy dog
2494 ˇThe qu«ick bˇ»rown"});
2495 cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
2496 cx.assert_editor_state("ˇthe lazy dogˇ");
2497}
2498
2499#[gpui::test]
2500fn test_delete_line(cx: &mut TestAppContext) {
2501 init_test(cx, |_| {});
2502
2503 let view = cx.add_window(|cx| {
2504 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
2505 build_editor(buffer, cx)
2506 });
2507 _ = view.update(cx, |view, cx| {
2508 view.change_selections(None, cx, |s| {
2509 s.select_display_ranges([
2510 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
2511 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
2512 DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
2513 ])
2514 });
2515 view.delete_line(&DeleteLine, cx);
2516 assert_eq!(view.display_text(cx), "ghi");
2517 assert_eq!(
2518 view.selections.display_ranges(cx),
2519 vec![
2520 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
2521 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)
2522 ]
2523 );
2524 });
2525
2526 let view = cx.add_window(|cx| {
2527 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
2528 build_editor(buffer, cx)
2529 });
2530 _ = view.update(cx, |view, cx| {
2531 view.change_selections(None, cx, |s| {
2532 s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)])
2533 });
2534 view.delete_line(&DeleteLine, cx);
2535 assert_eq!(view.display_text(cx), "ghi\n");
2536 assert_eq!(
2537 view.selections.display_ranges(cx),
2538 vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)]
2539 );
2540 });
2541}
2542
2543#[gpui::test]
2544fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
2545 init_test(cx, |_| {});
2546
2547 cx.add_window(|cx| {
2548 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
2549 let mut editor = build_editor(buffer.clone(), cx);
2550 let buffer = buffer.read(cx).as_singleton().unwrap();
2551
2552 assert_eq!(
2553 editor.selections.ranges::<Point>(cx),
2554 &[Point::new(0, 0)..Point::new(0, 0)]
2555 );
2556
2557 // When on single line, replace newline at end by space
2558 editor.join_lines(&JoinLines, cx);
2559 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
2560 assert_eq!(
2561 editor.selections.ranges::<Point>(cx),
2562 &[Point::new(0, 3)..Point::new(0, 3)]
2563 );
2564
2565 // When multiple lines are selected, remove newlines that are spanned by the selection
2566 editor.change_selections(None, cx, |s| {
2567 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
2568 });
2569 editor.join_lines(&JoinLines, cx);
2570 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
2571 assert_eq!(
2572 editor.selections.ranges::<Point>(cx),
2573 &[Point::new(0, 11)..Point::new(0, 11)]
2574 );
2575
2576 // Undo should be transactional
2577 editor.undo(&Undo, cx);
2578 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
2579 assert_eq!(
2580 editor.selections.ranges::<Point>(cx),
2581 &[Point::new(0, 5)..Point::new(2, 2)]
2582 );
2583
2584 // When joining an empty line don't insert a space
2585 editor.change_selections(None, cx, |s| {
2586 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
2587 });
2588 editor.join_lines(&JoinLines, cx);
2589 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
2590 assert_eq!(
2591 editor.selections.ranges::<Point>(cx),
2592 [Point::new(2, 3)..Point::new(2, 3)]
2593 );
2594
2595 // We can remove trailing newlines
2596 editor.join_lines(&JoinLines, cx);
2597 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
2598 assert_eq!(
2599 editor.selections.ranges::<Point>(cx),
2600 [Point::new(2, 3)..Point::new(2, 3)]
2601 );
2602
2603 // We don't blow up on the last line
2604 editor.join_lines(&JoinLines, cx);
2605 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
2606 assert_eq!(
2607 editor.selections.ranges::<Point>(cx),
2608 [Point::new(2, 3)..Point::new(2, 3)]
2609 );
2610
2611 // reset to test indentation
2612 editor.buffer.update(cx, |buffer, cx| {
2613 buffer.edit(
2614 [
2615 (Point::new(1, 0)..Point::new(1, 2), " "),
2616 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
2617 ],
2618 None,
2619 cx,
2620 )
2621 });
2622
2623 // We remove any leading spaces
2624 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
2625 editor.change_selections(None, cx, |s| {
2626 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
2627 });
2628 editor.join_lines(&JoinLines, cx);
2629 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
2630
2631 // We don't insert a space for a line containing only spaces
2632 editor.join_lines(&JoinLines, cx);
2633 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
2634
2635 // We ignore any leading tabs
2636 editor.join_lines(&JoinLines, cx);
2637 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
2638
2639 editor
2640 });
2641}
2642
2643#[gpui::test]
2644fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
2645 init_test(cx, |_| {});
2646
2647 cx.add_window(|cx| {
2648 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
2649 let mut editor = build_editor(buffer.clone(), cx);
2650 let buffer = buffer.read(cx).as_singleton().unwrap();
2651
2652 editor.change_selections(None, cx, |s| {
2653 s.select_ranges([
2654 Point::new(0, 2)..Point::new(1, 1),
2655 Point::new(1, 2)..Point::new(1, 2),
2656 Point::new(3, 1)..Point::new(3, 2),
2657 ])
2658 });
2659
2660 editor.join_lines(&JoinLines, cx);
2661 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
2662
2663 assert_eq!(
2664 editor.selections.ranges::<Point>(cx),
2665 [
2666 Point::new(0, 7)..Point::new(0, 7),
2667 Point::new(1, 3)..Point::new(1, 3)
2668 ]
2669 );
2670 editor
2671 });
2672}
2673
2674#[gpui::test]
2675async fn test_join_lines_with_git_diff_base(
2676 executor: BackgroundExecutor,
2677 cx: &mut gpui::TestAppContext,
2678) {
2679 init_test(cx, |_| {});
2680
2681 let mut cx = EditorTestContext::new(cx).await;
2682
2683 let diff_base = r#"
2684 Line 0
2685 Line 1
2686 Line 2
2687 Line 3
2688 "#
2689 .unindent();
2690
2691 cx.set_state(
2692 &r#"
2693 ˇLine 0
2694 Line 1
2695 Line 2
2696 Line 3
2697 "#
2698 .unindent(),
2699 );
2700
2701 cx.set_diff_base(Some(&diff_base));
2702 executor.run_until_parked();
2703
2704 // Join lines
2705 cx.update_editor(|editor, cx| {
2706 editor.join_lines(&JoinLines, cx);
2707 });
2708 executor.run_until_parked();
2709
2710 cx.assert_editor_state(
2711 &r#"
2712 Line 0ˇ Line 1
2713 Line 2
2714 Line 3
2715 "#
2716 .unindent(),
2717 );
2718 // Join again
2719 cx.update_editor(|editor, cx| {
2720 editor.join_lines(&JoinLines, cx);
2721 });
2722 executor.run_until_parked();
2723
2724 cx.assert_editor_state(
2725 &r#"
2726 Line 0 Line 1ˇ Line 2
2727 Line 3
2728 "#
2729 .unindent(),
2730 );
2731}
2732
2733#[gpui::test]
2734async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
2735 init_test(cx, |_| {});
2736
2737 let mut cx = EditorTestContext::new(cx).await;
2738
2739 // Test sort_lines_case_insensitive()
2740 cx.set_state(indoc! {"
2741 «z
2742 y
2743 x
2744 Z
2745 Y
2746 Xˇ»
2747 "});
2748 cx.update_editor(|e, cx| e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, cx));
2749 cx.assert_editor_state(indoc! {"
2750 «x
2751 X
2752 y
2753 Y
2754 z
2755 Zˇ»
2756 "});
2757
2758 // Test reverse_lines()
2759 cx.set_state(indoc! {"
2760 «5
2761 4
2762 3
2763 2
2764 1ˇ»
2765 "});
2766 cx.update_editor(|e, cx| e.reverse_lines(&ReverseLines, cx));
2767 cx.assert_editor_state(indoc! {"
2768 «1
2769 2
2770 3
2771 4
2772 5ˇ»
2773 "});
2774
2775 // Skip testing shuffle_line()
2776
2777 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
2778 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
2779
2780 // Don't manipulate when cursor is on single line, but expand the selection
2781 cx.set_state(indoc! {"
2782 ddˇdd
2783 ccc
2784 bb
2785 a
2786 "});
2787 cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
2788 cx.assert_editor_state(indoc! {"
2789 «ddddˇ»
2790 ccc
2791 bb
2792 a
2793 "});
2794
2795 // Basic manipulate case
2796 // Start selection moves to column 0
2797 // End of selection shrinks to fit shorter line
2798 cx.set_state(indoc! {"
2799 dd«d
2800 ccc
2801 bb
2802 aaaaaˇ»
2803 "});
2804 cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
2805 cx.assert_editor_state(indoc! {"
2806 «aaaaa
2807 bb
2808 ccc
2809 dddˇ»
2810 "});
2811
2812 // Manipulate case with newlines
2813 cx.set_state(indoc! {"
2814 dd«d
2815 ccc
2816
2817 bb
2818 aaaaa
2819
2820 ˇ»
2821 "});
2822 cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
2823 cx.assert_editor_state(indoc! {"
2824 «
2825
2826 aaaaa
2827 bb
2828 ccc
2829 dddˇ»
2830
2831 "});
2832
2833 // Adding new line
2834 cx.set_state(indoc! {"
2835 aa«a
2836 bbˇ»b
2837 "});
2838 cx.update_editor(|e, cx| e.manipulate_lines(cx, |lines| lines.push("added_line")));
2839 cx.assert_editor_state(indoc! {"
2840 «aaa
2841 bbb
2842 added_lineˇ»
2843 "});
2844
2845 // Removing line
2846 cx.set_state(indoc! {"
2847 aa«a
2848 bbbˇ»
2849 "});
2850 cx.update_editor(|e, cx| {
2851 e.manipulate_lines(cx, |lines| {
2852 lines.pop();
2853 })
2854 });
2855 cx.assert_editor_state(indoc! {"
2856 «aaaˇ»
2857 "});
2858
2859 // Removing all lines
2860 cx.set_state(indoc! {"
2861 aa«a
2862 bbbˇ»
2863 "});
2864 cx.update_editor(|e, cx| {
2865 e.manipulate_lines(cx, |lines| {
2866 lines.drain(..);
2867 })
2868 });
2869 cx.assert_editor_state(indoc! {"
2870 ˇ
2871 "});
2872}
2873
2874#[gpui::test]
2875async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
2876 init_test(cx, |_| {});
2877
2878 let mut cx = EditorTestContext::new(cx).await;
2879
2880 // Consider continuous selection as single selection
2881 cx.set_state(indoc! {"
2882 Aaa«aa
2883 cˇ»c«c
2884 bb
2885 aaaˇ»aa
2886 "});
2887 cx.update_editor(|e, cx| e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, cx));
2888 cx.assert_editor_state(indoc! {"
2889 «Aaaaa
2890 ccc
2891 bb
2892 aaaaaˇ»
2893 "});
2894
2895 cx.set_state(indoc! {"
2896 Aaa«aa
2897 cˇ»c«c
2898 bb
2899 aaaˇ»aa
2900 "});
2901 cx.update_editor(|e, cx| e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, cx));
2902 cx.assert_editor_state(indoc! {"
2903 «Aaaaa
2904 ccc
2905 bbˇ»
2906 "});
2907
2908 // Consider non continuous selection as distinct dedup operations
2909 cx.set_state(indoc! {"
2910 «aaaaa
2911 bb
2912 aaaaa
2913 aaaaaˇ»
2914
2915 aaa«aaˇ»
2916 "});
2917 cx.update_editor(|e, cx| e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, cx));
2918 cx.assert_editor_state(indoc! {"
2919 «aaaaa
2920 bbˇ»
2921
2922 «aaaaaˇ»
2923 "});
2924}
2925
2926#[gpui::test]
2927async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
2928 init_test(cx, |_| {});
2929
2930 let mut cx = EditorTestContext::new(cx).await;
2931
2932 cx.set_state(indoc! {"
2933 «Aaa
2934 aAa
2935 Aaaˇ»
2936 "});
2937 cx.update_editor(|e, cx| e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, cx));
2938 cx.assert_editor_state(indoc! {"
2939 «Aaa
2940 aAaˇ»
2941 "});
2942
2943 cx.set_state(indoc! {"
2944 «Aaa
2945 aAa
2946 aaAˇ»
2947 "});
2948 cx.update_editor(|e, cx| e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, cx));
2949 cx.assert_editor_state(indoc! {"
2950 «Aaaˇ»
2951 "});
2952}
2953
2954#[gpui::test]
2955async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
2956 init_test(cx, |_| {});
2957
2958 let mut cx = EditorTestContext::new(cx).await;
2959
2960 // Manipulate with multiple selections on a single line
2961 cx.set_state(indoc! {"
2962 dd«dd
2963 cˇ»c«c
2964 bb
2965 aaaˇ»aa
2966 "});
2967 cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
2968 cx.assert_editor_state(indoc! {"
2969 «aaaaa
2970 bb
2971 ccc
2972 ddddˇ»
2973 "});
2974
2975 // Manipulate with multiple disjoin selections
2976 cx.set_state(indoc! {"
2977 5«
2978 4
2979 3
2980 2
2981 1ˇ»
2982
2983 dd«dd
2984 ccc
2985 bb
2986 aaaˇ»aa
2987 "});
2988 cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
2989 cx.assert_editor_state(indoc! {"
2990 «1
2991 2
2992 3
2993 4
2994 5ˇ»
2995
2996 «aaaaa
2997 bb
2998 ccc
2999 ddddˇ»
3000 "});
3001
3002 // Adding lines on each selection
3003 cx.set_state(indoc! {"
3004 2«
3005 1ˇ»
3006
3007 bb«bb
3008 aaaˇ»aa
3009 "});
3010 cx.update_editor(|e, cx| e.manipulate_lines(cx, |lines| lines.push("added line")));
3011 cx.assert_editor_state(indoc! {"
3012 «2
3013 1
3014 added lineˇ»
3015
3016 «bbbb
3017 aaaaa
3018 added lineˇ»
3019 "});
3020
3021 // Removing lines on each selection
3022 cx.set_state(indoc! {"
3023 2«
3024 1ˇ»
3025
3026 bb«bb
3027 aaaˇ»aa
3028 "});
3029 cx.update_editor(|e, cx| {
3030 e.manipulate_lines(cx, |lines| {
3031 lines.pop();
3032 })
3033 });
3034 cx.assert_editor_state(indoc! {"
3035 «2ˇ»
3036
3037 «bbbbˇ»
3038 "});
3039}
3040
3041#[gpui::test]
3042async fn test_manipulate_text(cx: &mut TestAppContext) {
3043 init_test(cx, |_| {});
3044
3045 let mut cx = EditorTestContext::new(cx).await;
3046
3047 // Test convert_to_upper_case()
3048 cx.set_state(indoc! {"
3049 «hello worldˇ»
3050 "});
3051 cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
3052 cx.assert_editor_state(indoc! {"
3053 «HELLO WORLDˇ»
3054 "});
3055
3056 // Test convert_to_lower_case()
3057 cx.set_state(indoc! {"
3058 «HELLO WORLDˇ»
3059 "});
3060 cx.update_editor(|e, cx| e.convert_to_lower_case(&ConvertToLowerCase, cx));
3061 cx.assert_editor_state(indoc! {"
3062 «hello worldˇ»
3063 "});
3064
3065 // Test multiple line, single selection case
3066 // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
3067 cx.set_state(indoc! {"
3068 «The quick brown
3069 fox jumps over
3070 the lazy dogˇ»
3071 "});
3072 cx.update_editor(|e, cx| e.convert_to_title_case(&ConvertToTitleCase, cx));
3073 cx.assert_editor_state(indoc! {"
3074 «The Quick Brown
3075 Fox Jumps Over
3076 The Lazy Dogˇ»
3077 "});
3078
3079 // Test multiple line, single selection case
3080 // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
3081 cx.set_state(indoc! {"
3082 «The quick brown
3083 fox jumps over
3084 the lazy dogˇ»
3085 "});
3086 cx.update_editor(|e, cx| e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, cx));
3087 cx.assert_editor_state(indoc! {"
3088 «TheQuickBrown
3089 FoxJumpsOver
3090 TheLazyDogˇ»
3091 "});
3092
3093 // From here on out, test more complex cases of manipulate_text()
3094
3095 // Test no selection case - should affect words cursors are in
3096 // Cursor at beginning, middle, and end of word
3097 cx.set_state(indoc! {"
3098 ˇhello big beauˇtiful worldˇ
3099 "});
3100 cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
3101 cx.assert_editor_state(indoc! {"
3102 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
3103 "});
3104
3105 // Test multiple selections on a single line and across multiple lines
3106 cx.set_state(indoc! {"
3107 «Theˇ» quick «brown
3108 foxˇ» jumps «overˇ»
3109 the «lazyˇ» dog
3110 "});
3111 cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
3112 cx.assert_editor_state(indoc! {"
3113 «THEˇ» quick «BROWN
3114 FOXˇ» jumps «OVERˇ»
3115 the «LAZYˇ» dog
3116 "});
3117
3118 // Test case where text length grows
3119 cx.set_state(indoc! {"
3120 «tschüߡ»
3121 "});
3122 cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
3123 cx.assert_editor_state(indoc! {"
3124 «TSCHÜSSˇ»
3125 "});
3126
3127 // Test to make sure we don't crash when text shrinks
3128 cx.set_state(indoc! {"
3129 aaa_bbbˇ
3130 "});
3131 cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
3132 cx.assert_editor_state(indoc! {"
3133 «aaaBbbˇ»
3134 "});
3135
3136 // Test to make sure we all aware of the fact that each word can grow and shrink
3137 // Final selections should be aware of this fact
3138 cx.set_state(indoc! {"
3139 aaa_bˇbb bbˇb_ccc ˇccc_ddd
3140 "});
3141 cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
3142 cx.assert_editor_state(indoc! {"
3143 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
3144 "});
3145}
3146
3147#[gpui::test]
3148fn test_duplicate_line(cx: &mut TestAppContext) {
3149 init_test(cx, |_| {});
3150
3151 let view = cx.add_window(|cx| {
3152 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3153 build_editor(buffer, cx)
3154 });
3155 _ = view.update(cx, |view, cx| {
3156 view.change_selections(None, cx, |s| {
3157 s.select_display_ranges([
3158 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
3159 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3160 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
3161 DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
3162 ])
3163 });
3164 view.duplicate_line_down(&DuplicateLineDown, cx);
3165 assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
3166 assert_eq!(
3167 view.selections.display_ranges(cx),
3168 vec![
3169 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
3170 DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
3171 DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
3172 DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0),
3173 ]
3174 );
3175 });
3176
3177 let view = cx.add_window(|cx| {
3178 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3179 build_editor(buffer, cx)
3180 });
3181 _ = view.update(cx, |view, cx| {
3182 view.change_selections(None, cx, |s| {
3183 s.select_display_ranges([
3184 DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1),
3185 DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1),
3186 ])
3187 });
3188 view.duplicate_line_down(&DuplicateLineDown, cx);
3189 assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
3190 assert_eq!(
3191 view.selections.display_ranges(cx),
3192 vec![
3193 DisplayPoint::new(3, 1)..DisplayPoint::new(4, 1),
3194 DisplayPoint::new(4, 2)..DisplayPoint::new(5, 1),
3195 ]
3196 );
3197 });
3198
3199 // With `move_upwards` the selections stay in place, except for
3200 // the lines inserted above them
3201 let view = cx.add_window(|cx| {
3202 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3203 build_editor(buffer, cx)
3204 });
3205 _ = view.update(cx, |view, cx| {
3206 view.change_selections(None, cx, |s| {
3207 s.select_display_ranges([
3208 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
3209 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3210 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
3211 DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
3212 ])
3213 });
3214 view.duplicate_line_up(&DuplicateLineUp, cx);
3215 assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
3216 assert_eq!(
3217 view.selections.display_ranges(cx),
3218 vec![
3219 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
3220 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3221 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0),
3222 DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0),
3223 ]
3224 );
3225 });
3226
3227 let view = cx.add_window(|cx| {
3228 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3229 build_editor(buffer, cx)
3230 });
3231 _ = view.update(cx, |view, cx| {
3232 view.change_selections(None, cx, |s| {
3233 s.select_display_ranges([
3234 DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1),
3235 DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1),
3236 ])
3237 });
3238 view.duplicate_line_up(&DuplicateLineUp, cx);
3239 assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
3240 assert_eq!(
3241 view.selections.display_ranges(cx),
3242 vec![
3243 DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1),
3244 DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1),
3245 ]
3246 );
3247 });
3248}
3249
3250#[gpui::test]
3251fn test_move_line_up_down(cx: &mut TestAppContext) {
3252 init_test(cx, |_| {});
3253
3254 let view = cx.add_window(|cx| {
3255 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
3256 build_editor(buffer, cx)
3257 });
3258 _ = view.update(cx, |view, cx| {
3259 view.fold_ranges(
3260 vec![
3261 Point::new(0, 2)..Point::new(1, 2),
3262 Point::new(2, 3)..Point::new(4, 1),
3263 Point::new(7, 0)..Point::new(8, 4),
3264 ],
3265 true,
3266 cx,
3267 );
3268 view.change_selections(None, cx, |s| {
3269 s.select_display_ranges([
3270 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
3271 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
3272 DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
3273 DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2),
3274 ])
3275 });
3276 assert_eq!(
3277 view.display_text(cx),
3278 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
3279 );
3280
3281 view.move_line_up(&MoveLineUp, cx);
3282 assert_eq!(
3283 view.display_text(cx),
3284 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
3285 );
3286 assert_eq!(
3287 view.selections.display_ranges(cx),
3288 vec![
3289 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
3290 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
3291 DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3),
3292 DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2)
3293 ]
3294 );
3295 });
3296
3297 _ = view.update(cx, |view, cx| {
3298 view.move_line_down(&MoveLineDown, cx);
3299 assert_eq!(
3300 view.display_text(cx),
3301 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
3302 );
3303 assert_eq!(
3304 view.selections.display_ranges(cx),
3305 vec![
3306 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
3307 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
3308 DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
3309 DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2)
3310 ]
3311 );
3312 });
3313
3314 _ = view.update(cx, |view, cx| {
3315 view.move_line_down(&MoveLineDown, cx);
3316 assert_eq!(
3317 view.display_text(cx),
3318 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
3319 );
3320 assert_eq!(
3321 view.selections.display_ranges(cx),
3322 vec![
3323 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
3324 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
3325 DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
3326 DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2)
3327 ]
3328 );
3329 });
3330
3331 _ = view.update(cx, |view, cx| {
3332 view.move_line_up(&MoveLineUp, cx);
3333 assert_eq!(
3334 view.display_text(cx),
3335 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
3336 );
3337 assert_eq!(
3338 view.selections.display_ranges(cx),
3339 vec![
3340 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
3341 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
3342 DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3),
3343 DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2)
3344 ]
3345 );
3346 });
3347}
3348
3349#[gpui::test]
3350fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
3351 init_test(cx, |_| {});
3352
3353 let editor = cx.add_window(|cx| {
3354 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
3355 build_editor(buffer, cx)
3356 });
3357 _ = editor.update(cx, |editor, cx| {
3358 let snapshot = editor.buffer.read(cx).snapshot(cx);
3359 editor.insert_blocks(
3360 [BlockProperties {
3361 style: BlockStyle::Fixed,
3362 position: snapshot.anchor_after(Point::new(2, 0)),
3363 disposition: BlockDisposition::Below,
3364 height: 1,
3365 render: Box::new(|_| div().into_any()),
3366 }],
3367 Some(Autoscroll::fit()),
3368 cx,
3369 );
3370 editor.change_selections(None, cx, |s| {
3371 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
3372 });
3373 editor.move_line_down(&MoveLineDown, cx);
3374 });
3375}
3376
3377#[gpui::test]
3378fn test_transpose(cx: &mut TestAppContext) {
3379 init_test(cx, |_| {});
3380
3381 _ = cx.add_window(|cx| {
3382 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx);
3383 editor.set_style(EditorStyle::default(), cx);
3384 editor.change_selections(None, cx, |s| s.select_ranges([1..1]));
3385 editor.transpose(&Default::default(), cx);
3386 assert_eq!(editor.text(cx), "bac");
3387 assert_eq!(editor.selections.ranges(cx), [2..2]);
3388
3389 editor.transpose(&Default::default(), cx);
3390 assert_eq!(editor.text(cx), "bca");
3391 assert_eq!(editor.selections.ranges(cx), [3..3]);
3392
3393 editor.transpose(&Default::default(), cx);
3394 assert_eq!(editor.text(cx), "bac");
3395 assert_eq!(editor.selections.ranges(cx), [3..3]);
3396
3397 editor
3398 });
3399
3400 _ = cx.add_window(|cx| {
3401 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
3402 editor.set_style(EditorStyle::default(), cx);
3403 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
3404 editor.transpose(&Default::default(), cx);
3405 assert_eq!(editor.text(cx), "acb\nde");
3406 assert_eq!(editor.selections.ranges(cx), [3..3]);
3407
3408 editor.change_selections(None, cx, |s| s.select_ranges([4..4]));
3409 editor.transpose(&Default::default(), cx);
3410 assert_eq!(editor.text(cx), "acbd\ne");
3411 assert_eq!(editor.selections.ranges(cx), [5..5]);
3412
3413 editor.transpose(&Default::default(), cx);
3414 assert_eq!(editor.text(cx), "acbde\n");
3415 assert_eq!(editor.selections.ranges(cx), [6..6]);
3416
3417 editor.transpose(&Default::default(), cx);
3418 assert_eq!(editor.text(cx), "acbd\ne");
3419 assert_eq!(editor.selections.ranges(cx), [6..6]);
3420
3421 editor
3422 });
3423
3424 _ = cx.add_window(|cx| {
3425 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
3426 editor.set_style(EditorStyle::default(), cx);
3427 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
3428 editor.transpose(&Default::default(), cx);
3429 assert_eq!(editor.text(cx), "bacd\ne");
3430 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
3431
3432 editor.transpose(&Default::default(), cx);
3433 assert_eq!(editor.text(cx), "bcade\n");
3434 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
3435
3436 editor.transpose(&Default::default(), cx);
3437 assert_eq!(editor.text(cx), "bcda\ne");
3438 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
3439
3440 editor.transpose(&Default::default(), cx);
3441 assert_eq!(editor.text(cx), "bcade\n");
3442 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
3443
3444 editor.transpose(&Default::default(), cx);
3445 assert_eq!(editor.text(cx), "bcaed\n");
3446 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
3447
3448 editor
3449 });
3450
3451 _ = cx.add_window(|cx| {
3452 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx);
3453 editor.set_style(EditorStyle::default(), cx);
3454 editor.change_selections(None, cx, |s| s.select_ranges([4..4]));
3455 editor.transpose(&Default::default(), cx);
3456 assert_eq!(editor.text(cx), "🏀🍐✋");
3457 assert_eq!(editor.selections.ranges(cx), [8..8]);
3458
3459 editor.transpose(&Default::default(), cx);
3460 assert_eq!(editor.text(cx), "🏀✋🍐");
3461 assert_eq!(editor.selections.ranges(cx), [11..11]);
3462
3463 editor.transpose(&Default::default(), cx);
3464 assert_eq!(editor.text(cx), "🏀🍐✋");
3465 assert_eq!(editor.selections.ranges(cx), [11..11]);
3466
3467 editor
3468 });
3469}
3470
3471#[gpui::test]
3472async fn test_clipboard(cx: &mut gpui::TestAppContext) {
3473 init_test(cx, |_| {});
3474
3475 let mut cx = EditorTestContext::new(cx).await;
3476
3477 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
3478 cx.update_editor(|e, cx| e.cut(&Cut, cx));
3479 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
3480
3481 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
3482 cx.set_state("two ˇfour ˇsix ˇ");
3483 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3484 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
3485
3486 // Paste again but with only two cursors. Since the number of cursors doesn't
3487 // match the number of slices in the clipboard, the entire clipboard text
3488 // is pasted at each cursor.
3489 cx.set_state("ˇtwo one✅ four three six five ˇ");
3490 cx.update_editor(|e, cx| {
3491 e.handle_input("( ", cx);
3492 e.paste(&Paste, cx);
3493 e.handle_input(") ", cx);
3494 });
3495 cx.assert_editor_state(
3496 &([
3497 "( one✅ ",
3498 "three ",
3499 "five ) ˇtwo one✅ four three six five ( one✅ ",
3500 "three ",
3501 "five ) ˇ",
3502 ]
3503 .join("\n")),
3504 );
3505
3506 // Cut with three selections, one of which is full-line.
3507 cx.set_state(indoc! {"
3508 1«2ˇ»3
3509 4ˇ567
3510 «8ˇ»9"});
3511 cx.update_editor(|e, cx| e.cut(&Cut, cx));
3512 cx.assert_editor_state(indoc! {"
3513 1ˇ3
3514 ˇ9"});
3515
3516 // Paste with three selections, noticing how the copied selection that was full-line
3517 // gets inserted before the second cursor.
3518 cx.set_state(indoc! {"
3519 1ˇ3
3520 9ˇ
3521 «oˇ»ne"});
3522 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3523 cx.assert_editor_state(indoc! {"
3524 12ˇ3
3525 4567
3526 9ˇ
3527 8ˇne"});
3528
3529 // Copy with a single cursor only, which writes the whole line into the clipboard.
3530 cx.set_state(indoc! {"
3531 The quick brown
3532 fox juˇmps over
3533 the lazy dog"});
3534 cx.update_editor(|e, cx| e.copy(&Copy, cx));
3535 assert_eq!(
3536 cx.read_from_clipboard().map(|item| item.text().to_owned()),
3537 Some("fox jumps over\n".to_owned())
3538 );
3539
3540 // Paste with three selections, noticing how the copied full-line selection is inserted
3541 // before the empty selections but replaces the selection that is non-empty.
3542 cx.set_state(indoc! {"
3543 Tˇhe quick brown
3544 «foˇ»x jumps over
3545 tˇhe lazy dog"});
3546 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3547 cx.assert_editor_state(indoc! {"
3548 fox jumps over
3549 Tˇhe quick brown
3550 fox jumps over
3551 ˇx jumps over
3552 fox jumps over
3553 tˇhe lazy dog"});
3554}
3555
3556#[gpui::test]
3557async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
3558 init_test(cx, |_| {});
3559
3560 let mut cx = EditorTestContext::new(cx).await;
3561 let language = Arc::new(Language::new(
3562 LanguageConfig::default(),
3563 Some(tree_sitter_rust::language()),
3564 ));
3565 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3566
3567 // Cut an indented block, without the leading whitespace.
3568 cx.set_state(indoc! {"
3569 const a: B = (
3570 c(),
3571 «d(
3572 e,
3573 f
3574 )ˇ»
3575 );
3576 "});
3577 cx.update_editor(|e, cx| e.cut(&Cut, cx));
3578 cx.assert_editor_state(indoc! {"
3579 const a: B = (
3580 c(),
3581 ˇ
3582 );
3583 "});
3584
3585 // Paste it at the same position.
3586 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3587 cx.assert_editor_state(indoc! {"
3588 const a: B = (
3589 c(),
3590 d(
3591 e,
3592 f
3593 )ˇ
3594 );
3595 "});
3596
3597 // Paste it at a line with a lower indent level.
3598 cx.set_state(indoc! {"
3599 ˇ
3600 const a: B = (
3601 c(),
3602 );
3603 "});
3604 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3605 cx.assert_editor_state(indoc! {"
3606 d(
3607 e,
3608 f
3609 )ˇ
3610 const a: B = (
3611 c(),
3612 );
3613 "});
3614
3615 // Cut an indented block, with the leading whitespace.
3616 cx.set_state(indoc! {"
3617 const a: B = (
3618 c(),
3619 « d(
3620 e,
3621 f
3622 )
3623 ˇ»);
3624 "});
3625 cx.update_editor(|e, cx| e.cut(&Cut, cx));
3626 cx.assert_editor_state(indoc! {"
3627 const a: B = (
3628 c(),
3629 ˇ);
3630 "});
3631
3632 // Paste it at the same position.
3633 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3634 cx.assert_editor_state(indoc! {"
3635 const a: B = (
3636 c(),
3637 d(
3638 e,
3639 f
3640 )
3641 ˇ);
3642 "});
3643
3644 // Paste it at a line with a higher indent level.
3645 cx.set_state(indoc! {"
3646 const a: B = (
3647 c(),
3648 d(
3649 e,
3650 fˇ
3651 )
3652 );
3653 "});
3654 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3655 cx.assert_editor_state(indoc! {"
3656 const a: B = (
3657 c(),
3658 d(
3659 e,
3660 f d(
3661 e,
3662 f
3663 )
3664 ˇ
3665 )
3666 );
3667 "});
3668}
3669
3670#[gpui::test]
3671fn test_select_all(cx: &mut TestAppContext) {
3672 init_test(cx, |_| {});
3673
3674 let view = cx.add_window(|cx| {
3675 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
3676 build_editor(buffer, cx)
3677 });
3678 _ = view.update(cx, |view, cx| {
3679 view.select_all(&SelectAll, cx);
3680 assert_eq!(
3681 view.selections.display_ranges(cx),
3682 &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)]
3683 );
3684 });
3685}
3686
3687#[gpui::test]
3688fn test_select_line(cx: &mut TestAppContext) {
3689 init_test(cx, |_| {});
3690
3691 let view = cx.add_window(|cx| {
3692 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
3693 build_editor(buffer, cx)
3694 });
3695 _ = view.update(cx, |view, cx| {
3696 view.change_selections(None, cx, |s| {
3697 s.select_display_ranges([
3698 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
3699 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3700 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
3701 DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2),
3702 ])
3703 });
3704 view.select_line(&SelectLine, cx);
3705 assert_eq!(
3706 view.selections.display_ranges(cx),
3707 vec![
3708 DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0),
3709 DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0),
3710 ]
3711 );
3712 });
3713
3714 _ = view.update(cx, |view, cx| {
3715 view.select_line(&SelectLine, cx);
3716 assert_eq!(
3717 view.selections.display_ranges(cx),
3718 vec![
3719 DisplayPoint::new(0, 0)..DisplayPoint::new(3, 0),
3720 DisplayPoint::new(4, 0)..DisplayPoint::new(5, 5),
3721 ]
3722 );
3723 });
3724
3725 _ = view.update(cx, |view, cx| {
3726 view.select_line(&SelectLine, cx);
3727 assert_eq!(
3728 view.selections.display_ranges(cx),
3729 vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)]
3730 );
3731 });
3732}
3733
3734#[gpui::test]
3735fn test_split_selection_into_lines(cx: &mut TestAppContext) {
3736 init_test(cx, |_| {});
3737
3738 let view = cx.add_window(|cx| {
3739 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
3740 build_editor(buffer, cx)
3741 });
3742 _ = view.update(cx, |view, cx| {
3743 view.fold_ranges(
3744 vec![
3745 Point::new(0, 2)..Point::new(1, 2),
3746 Point::new(2, 3)..Point::new(4, 1),
3747 Point::new(7, 0)..Point::new(8, 4),
3748 ],
3749 true,
3750 cx,
3751 );
3752 view.change_selections(None, cx, |s| {
3753 s.select_display_ranges([
3754 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
3755 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3756 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
3757 DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4),
3758 ])
3759 });
3760 assert_eq!(view.display_text(cx), "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i");
3761 });
3762
3763 _ = view.update(cx, |view, cx| {
3764 view.split_selection_into_lines(&SplitSelectionIntoLines, cx);
3765 assert_eq!(
3766 view.display_text(cx),
3767 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
3768 );
3769 assert_eq!(
3770 view.selections.display_ranges(cx),
3771 [
3772 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
3773 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3774 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0),
3775 DisplayPoint::new(5, 4)..DisplayPoint::new(5, 4)
3776 ]
3777 );
3778 });
3779
3780 _ = view.update(cx, |view, cx| {
3781 view.change_selections(None, cx, |s| {
3782 s.select_display_ranges([DisplayPoint::new(5, 0)..DisplayPoint::new(0, 1)])
3783 });
3784 view.split_selection_into_lines(&SplitSelectionIntoLines, cx);
3785 assert_eq!(
3786 view.display_text(cx),
3787 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
3788 );
3789 assert_eq!(
3790 view.selections.display_ranges(cx),
3791 [
3792 DisplayPoint::new(0, 5)..DisplayPoint::new(0, 5),
3793 DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5),
3794 DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
3795 DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5),
3796 DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5),
3797 DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5),
3798 DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5),
3799 DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0)
3800 ]
3801 );
3802 });
3803}
3804
3805#[gpui::test]
3806async fn test_add_selection_above_below(cx: &mut TestAppContext) {
3807 init_test(cx, |_| {});
3808
3809 let mut cx = EditorTestContext::new(cx).await;
3810
3811 // let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx);
3812 cx.set_state(indoc!(
3813 r#"abc
3814 defˇghi
3815
3816 jk
3817 nlmo
3818 "#
3819 ));
3820
3821 cx.update_editor(|editor, cx| {
3822 editor.add_selection_above(&Default::default(), cx);
3823 });
3824
3825 cx.assert_editor_state(indoc!(
3826 r#"abcˇ
3827 defˇghi
3828
3829 jk
3830 nlmo
3831 "#
3832 ));
3833
3834 cx.update_editor(|editor, cx| {
3835 editor.add_selection_above(&Default::default(), cx);
3836 });
3837
3838 cx.assert_editor_state(indoc!(
3839 r#"abcˇ
3840 defˇghi
3841
3842 jk
3843 nlmo
3844 "#
3845 ));
3846
3847 cx.update_editor(|view, cx| {
3848 view.add_selection_below(&Default::default(), cx);
3849 });
3850
3851 cx.assert_editor_state(indoc!(
3852 r#"abc
3853 defˇghi
3854
3855 jk
3856 nlmo
3857 "#
3858 ));
3859
3860 cx.update_editor(|view, cx| {
3861 view.undo_selection(&Default::default(), cx);
3862 });
3863
3864 cx.assert_editor_state(indoc!(
3865 r#"abcˇ
3866 defˇghi
3867
3868 jk
3869 nlmo
3870 "#
3871 ));
3872
3873 cx.update_editor(|view, cx| {
3874 view.redo_selection(&Default::default(), cx);
3875 });
3876
3877 cx.assert_editor_state(indoc!(
3878 r#"abc
3879 defˇghi
3880
3881 jk
3882 nlmo
3883 "#
3884 ));
3885
3886 cx.update_editor(|view, cx| {
3887 view.add_selection_below(&Default::default(), cx);
3888 });
3889
3890 cx.assert_editor_state(indoc!(
3891 r#"abc
3892 defˇghi
3893
3894 jk
3895 nlmˇo
3896 "#
3897 ));
3898
3899 cx.update_editor(|view, cx| {
3900 view.add_selection_below(&Default::default(), cx);
3901 });
3902
3903 cx.assert_editor_state(indoc!(
3904 r#"abc
3905 defˇghi
3906
3907 jk
3908 nlmˇo
3909 "#
3910 ));
3911
3912 // change selections
3913 cx.set_state(indoc!(
3914 r#"abc
3915 def«ˇg»hi
3916
3917 jk
3918 nlmo
3919 "#
3920 ));
3921
3922 cx.update_editor(|view, cx| {
3923 view.add_selection_below(&Default::default(), cx);
3924 });
3925
3926 cx.assert_editor_state(indoc!(
3927 r#"abc
3928 def«ˇg»hi
3929
3930 jk
3931 nlm«ˇo»
3932 "#
3933 ));
3934
3935 cx.update_editor(|view, cx| {
3936 view.add_selection_below(&Default::default(), cx);
3937 });
3938
3939 cx.assert_editor_state(indoc!(
3940 r#"abc
3941 def«ˇg»hi
3942
3943 jk
3944 nlm«ˇo»
3945 "#
3946 ));
3947
3948 cx.update_editor(|view, cx| {
3949 view.add_selection_above(&Default::default(), cx);
3950 });
3951
3952 cx.assert_editor_state(indoc!(
3953 r#"abc
3954 def«ˇg»hi
3955
3956 jk
3957 nlmo
3958 "#
3959 ));
3960
3961 cx.update_editor(|view, cx| {
3962 view.add_selection_above(&Default::default(), cx);
3963 });
3964
3965 cx.assert_editor_state(indoc!(
3966 r#"abc
3967 def«ˇg»hi
3968
3969 jk
3970 nlmo
3971 "#
3972 ));
3973
3974 // Change selections again
3975 cx.set_state(indoc!(
3976 r#"a«bc
3977 defgˇ»hi
3978
3979 jk
3980 nlmo
3981 "#
3982 ));
3983
3984 cx.update_editor(|view, cx| {
3985 view.add_selection_below(&Default::default(), cx);
3986 });
3987
3988 cx.assert_editor_state(indoc!(
3989 r#"a«bcˇ»
3990 d«efgˇ»hi
3991
3992 j«kˇ»
3993 nlmo
3994 "#
3995 ));
3996
3997 cx.update_editor(|view, cx| {
3998 view.add_selection_below(&Default::default(), cx);
3999 });
4000 cx.assert_editor_state(indoc!(
4001 r#"a«bcˇ»
4002 d«efgˇ»hi
4003
4004 j«kˇ»
4005 n«lmoˇ»
4006 "#
4007 ));
4008 cx.update_editor(|view, cx| {
4009 view.add_selection_above(&Default::default(), cx);
4010 });
4011
4012 cx.assert_editor_state(indoc!(
4013 r#"a«bcˇ»
4014 d«efgˇ»hi
4015
4016 j«kˇ»
4017 nlmo
4018 "#
4019 ));
4020
4021 // Change selections again
4022 cx.set_state(indoc!(
4023 r#"abc
4024 d«ˇefghi
4025
4026 jk
4027 nlm»o
4028 "#
4029 ));
4030
4031 cx.update_editor(|view, cx| {
4032 view.add_selection_above(&Default::default(), cx);
4033 });
4034
4035 cx.assert_editor_state(indoc!(
4036 r#"a«ˇbc»
4037 d«ˇef»ghi
4038
4039 j«ˇk»
4040 n«ˇlm»o
4041 "#
4042 ));
4043
4044 cx.update_editor(|view, cx| {
4045 view.add_selection_below(&Default::default(), cx);
4046 });
4047
4048 cx.assert_editor_state(indoc!(
4049 r#"abc
4050 d«ˇef»ghi
4051
4052 j«ˇk»
4053 n«ˇlm»o
4054 "#
4055 ));
4056}
4057
4058#[gpui::test]
4059async fn test_select_next(cx: &mut gpui::TestAppContext) {
4060 init_test(cx, |_| {});
4061
4062 let mut cx = EditorTestContext::new(cx).await;
4063 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
4064
4065 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
4066 .unwrap();
4067 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
4068
4069 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
4070 .unwrap();
4071 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
4072
4073 cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
4074 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
4075
4076 cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
4077 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
4078
4079 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
4080 .unwrap();
4081 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
4082
4083 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
4084 .unwrap();
4085 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
4086}
4087
4088#[gpui::test]
4089async fn test_select_all_matches(cx: &mut gpui::TestAppContext) {
4090 init_test(cx, |_| {});
4091
4092 let mut cx = EditorTestContext::new(cx).await;
4093 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
4094
4095 cx.update_editor(|e, cx| e.select_all_matches(&SelectAllMatches, cx))
4096 .unwrap();
4097 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
4098}
4099
4100#[gpui::test]
4101async fn test_select_next_with_multiple_carets(cx: &mut gpui::TestAppContext) {
4102 init_test(cx, |_| {});
4103
4104 let mut cx = EditorTestContext::new(cx).await;
4105 cx.set_state(
4106 r#"let foo = 2;
4107lˇet foo = 2;
4108let fooˇ = 2;
4109let foo = 2;
4110let foo = ˇ2;"#,
4111 );
4112
4113 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
4114 .unwrap();
4115 cx.assert_editor_state(
4116 r#"let foo = 2;
4117«letˇ» foo = 2;
4118let «fooˇ» = 2;
4119let foo = 2;
4120let foo = «2ˇ»;"#,
4121 );
4122
4123 // noop for multiple selections with different contents
4124 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
4125 .unwrap();
4126 cx.assert_editor_state(
4127 r#"let foo = 2;
4128«letˇ» foo = 2;
4129let «fooˇ» = 2;
4130let foo = 2;
4131let foo = «2ˇ»;"#,
4132 );
4133}
4134
4135#[gpui::test]
4136async fn test_select_previous_multibuffer(cx: &mut gpui::TestAppContext) {
4137 init_test(cx, |_| {});
4138
4139 let mut cx = EditorTestContext::new_multibuffer(
4140 cx,
4141 [
4142 indoc! {
4143 "aaa\n«bbb\nccc\n»ddd"
4144 },
4145 indoc! {
4146 "aaa\n«bbb\nccc\n»ddd"
4147 },
4148 ],
4149 );
4150
4151 cx.assert_editor_state(indoc! {"
4152 ˇbbb
4153 ccc
4154
4155 bbb
4156 ccc
4157 "});
4158 cx.dispatch_action(SelectPrevious::default());
4159 cx.assert_editor_state(indoc! {"
4160 «bbbˇ»
4161 ccc
4162
4163 bbb
4164 ccc
4165 "});
4166 cx.dispatch_action(SelectPrevious::default());
4167 cx.assert_editor_state(indoc! {"
4168 «bbbˇ»
4169 ccc
4170
4171 «bbbˇ»
4172 ccc
4173 "});
4174}
4175
4176#[gpui::test]
4177async fn test_select_previous_with_single_caret(cx: &mut gpui::TestAppContext) {
4178 init_test(cx, |_| {});
4179
4180 let mut cx = EditorTestContext::new(cx).await;
4181 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
4182
4183 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4184 .unwrap();
4185 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
4186
4187 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4188 .unwrap();
4189 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
4190
4191 cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
4192 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
4193
4194 cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
4195 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
4196
4197 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4198 .unwrap();
4199 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
4200
4201 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4202 .unwrap();
4203 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
4204
4205 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4206 .unwrap();
4207 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
4208}
4209
4210#[gpui::test]
4211async fn test_select_previous_with_multiple_carets(cx: &mut gpui::TestAppContext) {
4212 init_test(cx, |_| {});
4213
4214 let mut cx = EditorTestContext::new(cx).await;
4215 cx.set_state(
4216 r#"let foo = 2;
4217lˇet foo = 2;
4218let fooˇ = 2;
4219let foo = 2;
4220let foo = ˇ2;"#,
4221 );
4222
4223 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4224 .unwrap();
4225 cx.assert_editor_state(
4226 r#"let foo = 2;
4227«letˇ» foo = 2;
4228let «fooˇ» = 2;
4229let foo = 2;
4230let foo = «2ˇ»;"#,
4231 );
4232
4233 // noop for multiple selections with different contents
4234 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4235 .unwrap();
4236 cx.assert_editor_state(
4237 r#"let foo = 2;
4238«letˇ» foo = 2;
4239let «fooˇ» = 2;
4240let foo = 2;
4241let foo = «2ˇ»;"#,
4242 );
4243}
4244
4245#[gpui::test]
4246async fn test_select_previous_with_single_selection(cx: &mut gpui::TestAppContext) {
4247 init_test(cx, |_| {});
4248
4249 let mut cx = EditorTestContext::new(cx).await;
4250 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
4251
4252 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4253 .unwrap();
4254 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
4255
4256 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4257 .unwrap();
4258 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
4259
4260 cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
4261 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
4262
4263 cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
4264 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
4265
4266 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4267 .unwrap();
4268 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
4269
4270 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
4271 .unwrap();
4272 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
4273}
4274
4275#[gpui::test]
4276async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
4277 init_test(cx, |_| {});
4278
4279 let language = Arc::new(Language::new(
4280 LanguageConfig::default(),
4281 Some(tree_sitter_rust::language()),
4282 ));
4283
4284 let text = r#"
4285 use mod1::mod2::{mod3, mod4};
4286
4287 fn fn_1(param1: bool, param2: &str) {
4288 let var1 = "text";
4289 }
4290 "#
4291 .unindent();
4292
4293 let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx));
4294 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
4295 let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
4296
4297 view.condition::<crate::EditorEvent>(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
4298 .await;
4299
4300 _ = view.update(cx, |view, cx| {
4301 view.change_selections(None, cx, |s| {
4302 s.select_display_ranges([
4303 DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
4304 DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
4305 DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
4306 ]);
4307 });
4308 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
4309 });
4310 assert_eq!(
4311 view.update(cx, |view, cx| { view.selections.display_ranges(cx) }),
4312 &[
4313 DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
4314 DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
4315 DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21),
4316 ]
4317 );
4318
4319 _ = view.update(cx, |view, cx| {
4320 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
4321 });
4322 assert_eq!(
4323 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4324 &[
4325 DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
4326 DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
4327 ]
4328 );
4329
4330 _ = view.update(cx, |view, cx| {
4331 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
4332 });
4333 assert_eq!(
4334 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4335 &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
4336 );
4337
4338 // Trying to expand the selected syntax node one more time has no effect.
4339 _ = view.update(cx, |view, cx| {
4340 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
4341 });
4342 assert_eq!(
4343 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4344 &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
4345 );
4346
4347 _ = view.update(cx, |view, cx| {
4348 view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
4349 });
4350 assert_eq!(
4351 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4352 &[
4353 DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
4354 DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
4355 ]
4356 );
4357
4358 _ = view.update(cx, |view, cx| {
4359 view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
4360 });
4361 assert_eq!(
4362 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4363 &[
4364 DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
4365 DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
4366 DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21),
4367 ]
4368 );
4369
4370 _ = view.update(cx, |view, cx| {
4371 view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
4372 });
4373 assert_eq!(
4374 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4375 &[
4376 DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
4377 DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
4378 DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
4379 ]
4380 );
4381
4382 // Trying to shrink the selected syntax node one more time has no effect.
4383 _ = view.update(cx, |view, cx| {
4384 view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
4385 });
4386 assert_eq!(
4387 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4388 &[
4389 DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
4390 DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
4391 DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
4392 ]
4393 );
4394
4395 // Ensure that we keep expanding the selection if the larger selection starts or ends within
4396 // a fold.
4397 _ = view.update(cx, |view, cx| {
4398 view.fold_ranges(
4399 vec![
4400 Point::new(0, 21)..Point::new(0, 24),
4401 Point::new(3, 20)..Point::new(3, 22),
4402 ],
4403 true,
4404 cx,
4405 );
4406 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
4407 });
4408 assert_eq!(
4409 view.update(cx, |view, cx| view.selections.display_ranges(cx)),
4410 &[
4411 DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
4412 DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
4413 DisplayPoint::new(3, 4)..DisplayPoint::new(3, 23),
4414 ]
4415 );
4416}
4417
4418#[gpui::test]
4419async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
4420 init_test(cx, |_| {});
4421
4422 let language = Arc::new(
4423 Language::new(
4424 LanguageConfig {
4425 brackets: BracketPairConfig {
4426 pairs: vec![
4427 BracketPair {
4428 start: "{".to_string(),
4429 end: "}".to_string(),
4430 close: false,
4431 newline: true,
4432 },
4433 BracketPair {
4434 start: "(".to_string(),
4435 end: ")".to_string(),
4436 close: false,
4437 newline: true,
4438 },
4439 ],
4440 ..Default::default()
4441 },
4442 ..Default::default()
4443 },
4444 Some(tree_sitter_rust::language()),
4445 )
4446 .with_indents_query(
4447 r#"
4448 (_ "(" ")" @end) @indent
4449 (_ "{" "}" @end) @indent
4450 "#,
4451 )
4452 .unwrap(),
4453 );
4454
4455 let text = "fn a() {}";
4456
4457 let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx));
4458 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
4459 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
4460 editor
4461 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
4462 .await;
4463
4464 _ = editor.update(cx, |editor, cx| {
4465 editor.change_selections(None, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
4466 editor.newline(&Newline, cx);
4467 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
4468 assert_eq!(
4469 editor.selections.ranges(cx),
4470 &[
4471 Point::new(1, 4)..Point::new(1, 4),
4472 Point::new(3, 4)..Point::new(3, 4),
4473 Point::new(5, 0)..Point::new(5, 0)
4474 ]
4475 );
4476 });
4477}
4478
4479#[gpui::test]
4480async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
4481 init_test(cx, |_| {});
4482
4483 let mut cx = EditorTestContext::new(cx).await;
4484
4485 let language = Arc::new(Language::new(
4486 LanguageConfig {
4487 brackets: BracketPairConfig {
4488 pairs: vec![
4489 BracketPair {
4490 start: "{".to_string(),
4491 end: "}".to_string(),
4492 close: true,
4493 newline: true,
4494 },
4495 BracketPair {
4496 start: "(".to_string(),
4497 end: ")".to_string(),
4498 close: true,
4499 newline: true,
4500 },
4501 BracketPair {
4502 start: "/*".to_string(),
4503 end: " */".to_string(),
4504 close: true,
4505 newline: true,
4506 },
4507 BracketPair {
4508 start: "[".to_string(),
4509 end: "]".to_string(),
4510 close: false,
4511 newline: true,
4512 },
4513 BracketPair {
4514 start: "\"".to_string(),
4515 end: "\"".to_string(),
4516 close: true,
4517 newline: false,
4518 },
4519 ],
4520 ..Default::default()
4521 },
4522 autoclose_before: "})]".to_string(),
4523 ..Default::default()
4524 },
4525 Some(tree_sitter_rust::language()),
4526 ));
4527
4528 cx.language_registry().add(language.clone());
4529 cx.update_buffer(|buffer, cx| {
4530 buffer.set_language(Some(language), cx);
4531 });
4532
4533 cx.set_state(
4534 &r#"
4535 🏀ˇ
4536 εˇ
4537 ❤️ˇ
4538 "#
4539 .unindent(),
4540 );
4541
4542 // autoclose multiple nested brackets at multiple cursors
4543 cx.update_editor(|view, cx| {
4544 view.handle_input("{", cx);
4545 view.handle_input("{", cx);
4546 view.handle_input("{", cx);
4547 });
4548 cx.assert_editor_state(
4549 &"
4550 🏀{{{ˇ}}}
4551 ε{{{ˇ}}}
4552 ❤️{{{ˇ}}}
4553 "
4554 .unindent(),
4555 );
4556
4557 // insert a different closing bracket
4558 cx.update_editor(|view, cx| {
4559 view.handle_input(")", cx);
4560 });
4561 cx.assert_editor_state(
4562 &"
4563 🏀{{{)ˇ}}}
4564 ε{{{)ˇ}}}
4565 ❤️{{{)ˇ}}}
4566 "
4567 .unindent(),
4568 );
4569
4570 // skip over the auto-closed brackets when typing a closing bracket
4571 cx.update_editor(|view, cx| {
4572 view.move_right(&MoveRight, cx);
4573 view.handle_input("}", cx);
4574 view.handle_input("}", cx);
4575 view.handle_input("}", cx);
4576 });
4577 cx.assert_editor_state(
4578 &"
4579 🏀{{{)}}}}ˇ
4580 ε{{{)}}}}ˇ
4581 ❤️{{{)}}}}ˇ
4582 "
4583 .unindent(),
4584 );
4585
4586 // autoclose multi-character pairs
4587 cx.set_state(
4588 &"
4589 ˇ
4590 ˇ
4591 "
4592 .unindent(),
4593 );
4594 cx.update_editor(|view, cx| {
4595 view.handle_input("/", cx);
4596 view.handle_input("*", cx);
4597 });
4598 cx.assert_editor_state(
4599 &"
4600 /*ˇ */
4601 /*ˇ */
4602 "
4603 .unindent(),
4604 );
4605
4606 // one cursor autocloses a multi-character pair, one cursor
4607 // does not autoclose.
4608 cx.set_state(
4609 &"
4610 /ˇ
4611 ˇ
4612 "
4613 .unindent(),
4614 );
4615 cx.update_editor(|view, cx| view.handle_input("*", cx));
4616 cx.assert_editor_state(
4617 &"
4618 /*ˇ */
4619 *ˇ
4620 "
4621 .unindent(),
4622 );
4623
4624 // Don't autoclose if the next character isn't whitespace and isn't
4625 // listed in the language's "autoclose_before" section.
4626 cx.set_state("ˇa b");
4627 cx.update_editor(|view, cx| view.handle_input("{", cx));
4628 cx.assert_editor_state("{ˇa b");
4629
4630 // Don't autoclose if `close` is false for the bracket pair
4631 cx.set_state("ˇ");
4632 cx.update_editor(|view, cx| view.handle_input("[", cx));
4633 cx.assert_editor_state("[ˇ");
4634
4635 // Surround with brackets if text is selected
4636 cx.set_state("«aˇ» b");
4637 cx.update_editor(|view, cx| view.handle_input("{", cx));
4638 cx.assert_editor_state("{«aˇ»} b");
4639
4640 // Autclose pair where the start and end characters are the same
4641 cx.set_state("aˇ");
4642 cx.update_editor(|view, cx| view.handle_input("\"", cx));
4643 cx.assert_editor_state("a\"ˇ\"");
4644 cx.update_editor(|view, cx| view.handle_input("\"", cx));
4645 cx.assert_editor_state("a\"\"ˇ");
4646}
4647
4648#[gpui::test]
4649async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut gpui::TestAppContext) {
4650 init_test(cx, |settings| {
4651 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
4652 });
4653
4654 let mut cx = EditorTestContext::new(cx).await;
4655
4656 let language = Arc::new(Language::new(
4657 LanguageConfig {
4658 brackets: BracketPairConfig {
4659 pairs: vec![
4660 BracketPair {
4661 start: "{".to_string(),
4662 end: "}".to_string(),
4663 close: true,
4664 newline: true,
4665 },
4666 BracketPair {
4667 start: "(".to_string(),
4668 end: ")".to_string(),
4669 close: true,
4670 newline: true,
4671 },
4672 BracketPair {
4673 start: "[".to_string(),
4674 end: "]".to_string(),
4675 close: false,
4676 newline: true,
4677 },
4678 ],
4679 ..Default::default()
4680 },
4681 autoclose_before: "})]".to_string(),
4682 ..Default::default()
4683 },
4684 Some(tree_sitter_rust::language()),
4685 ));
4686
4687 cx.language_registry().add(language.clone());
4688 cx.update_buffer(|buffer, cx| {
4689 buffer.set_language(Some(language), cx);
4690 });
4691
4692 cx.set_state(
4693 &"
4694 ˇ
4695 ˇ
4696 ˇ
4697 "
4698 .unindent(),
4699 );
4700
4701 // ensure only matching closing brackets are skipped over
4702 cx.update_editor(|view, cx| {
4703 view.handle_input("}", cx);
4704 view.move_left(&MoveLeft, cx);
4705 view.handle_input(")", cx);
4706 view.move_left(&MoveLeft, cx);
4707 });
4708 cx.assert_editor_state(
4709 &"
4710 ˇ)}
4711 ˇ)}
4712 ˇ)}
4713 "
4714 .unindent(),
4715 );
4716
4717 // skip-over closing brackets at multiple cursors
4718 cx.update_editor(|view, cx| {
4719 view.handle_input(")", cx);
4720 view.handle_input("}", cx);
4721 });
4722 cx.assert_editor_state(
4723 &"
4724 )}ˇ
4725 )}ˇ
4726 )}ˇ
4727 "
4728 .unindent(),
4729 );
4730
4731 // ignore non-close brackets
4732 cx.update_editor(|view, cx| {
4733 view.handle_input("]", cx);
4734 view.move_left(&MoveLeft, cx);
4735 view.handle_input("]", cx);
4736 });
4737 cx.assert_editor_state(
4738 &"
4739 )}]ˇ]
4740 )}]ˇ]
4741 )}]ˇ]
4742 "
4743 .unindent(),
4744 );
4745}
4746
4747#[gpui::test]
4748async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
4749 init_test(cx, |_| {});
4750
4751 let mut cx = EditorTestContext::new(cx).await;
4752
4753 let html_language = Arc::new(
4754 Language::new(
4755 LanguageConfig {
4756 name: "HTML".into(),
4757 brackets: BracketPairConfig {
4758 pairs: vec![
4759 BracketPair {
4760 start: "<".into(),
4761 end: ">".into(),
4762 close: true,
4763 ..Default::default()
4764 },
4765 BracketPair {
4766 start: "{".into(),
4767 end: "}".into(),
4768 close: true,
4769 ..Default::default()
4770 },
4771 BracketPair {
4772 start: "(".into(),
4773 end: ")".into(),
4774 close: true,
4775 ..Default::default()
4776 },
4777 ],
4778 ..Default::default()
4779 },
4780 autoclose_before: "})]>".into(),
4781 ..Default::default()
4782 },
4783 Some(tree_sitter_html::language()),
4784 )
4785 .with_injection_query(
4786 r#"
4787 (script_element
4788 (raw_text) @content
4789 (#set! "language" "javascript"))
4790 "#,
4791 )
4792 .unwrap(),
4793 );
4794
4795 let javascript_language = Arc::new(Language::new(
4796 LanguageConfig {
4797 name: "JavaScript".into(),
4798 brackets: BracketPairConfig {
4799 pairs: vec![
4800 BracketPair {
4801 start: "/*".into(),
4802 end: " */".into(),
4803 close: true,
4804 ..Default::default()
4805 },
4806 BracketPair {
4807 start: "{".into(),
4808 end: "}".into(),
4809 close: true,
4810 ..Default::default()
4811 },
4812 BracketPair {
4813 start: "(".into(),
4814 end: ")".into(),
4815 close: true,
4816 ..Default::default()
4817 },
4818 ],
4819 ..Default::default()
4820 },
4821 autoclose_before: "})]>".into(),
4822 ..Default::default()
4823 },
4824 Some(tree_sitter_typescript::language_tsx()),
4825 ));
4826
4827 cx.language_registry().add(html_language.clone());
4828 cx.language_registry().add(javascript_language.clone());
4829
4830 cx.update_buffer(|buffer, cx| {
4831 buffer.set_language(Some(html_language), cx);
4832 });
4833
4834 cx.set_state(
4835 &r#"
4836 <body>ˇ
4837 <script>
4838 var x = 1;ˇ
4839 </script>
4840 </body>ˇ
4841 "#
4842 .unindent(),
4843 );
4844
4845 // Precondition: different languages are active at different locations.
4846 cx.update_editor(|editor, cx| {
4847 let snapshot = editor.snapshot(cx);
4848 let cursors = editor.selections.ranges::<usize>(cx);
4849 let languages = cursors
4850 .iter()
4851 .map(|c| snapshot.language_at(c.start).unwrap().name())
4852 .collect::<Vec<_>>();
4853 assert_eq!(
4854 languages,
4855 &["HTML".into(), "JavaScript".into(), "HTML".into()]
4856 );
4857 });
4858
4859 // Angle brackets autoclose in HTML, but not JavaScript.
4860 cx.update_editor(|editor, cx| {
4861 editor.handle_input("<", cx);
4862 editor.handle_input("a", cx);
4863 });
4864 cx.assert_editor_state(
4865 &r#"
4866 <body><aˇ>
4867 <script>
4868 var x = 1;<aˇ
4869 </script>
4870 </body><aˇ>
4871 "#
4872 .unindent(),
4873 );
4874
4875 // Curly braces and parens autoclose in both HTML and JavaScript.
4876 cx.update_editor(|editor, cx| {
4877 editor.handle_input(" b=", cx);
4878 editor.handle_input("{", cx);
4879 editor.handle_input("c", cx);
4880 editor.handle_input("(", cx);
4881 });
4882 cx.assert_editor_state(
4883 &r#"
4884 <body><a b={c(ˇ)}>
4885 <script>
4886 var x = 1;<a b={c(ˇ)}
4887 </script>
4888 </body><a b={c(ˇ)}>
4889 "#
4890 .unindent(),
4891 );
4892
4893 // Brackets that were already autoclosed are skipped.
4894 cx.update_editor(|editor, cx| {
4895 editor.handle_input(")", cx);
4896 editor.handle_input("d", cx);
4897 editor.handle_input("}", cx);
4898 });
4899 cx.assert_editor_state(
4900 &r#"
4901 <body><a b={c()d}ˇ>
4902 <script>
4903 var x = 1;<a b={c()d}ˇ
4904 </script>
4905 </body><a b={c()d}ˇ>
4906 "#
4907 .unindent(),
4908 );
4909 cx.update_editor(|editor, cx| {
4910 editor.handle_input(">", cx);
4911 });
4912 cx.assert_editor_state(
4913 &r#"
4914 <body><a b={c()d}>ˇ
4915 <script>
4916 var x = 1;<a b={c()d}>ˇ
4917 </script>
4918 </body><a b={c()d}>ˇ
4919 "#
4920 .unindent(),
4921 );
4922
4923 // Reset
4924 cx.set_state(
4925 &r#"
4926 <body>ˇ
4927 <script>
4928 var x = 1;ˇ
4929 </script>
4930 </body>ˇ
4931 "#
4932 .unindent(),
4933 );
4934
4935 cx.update_editor(|editor, cx| {
4936 editor.handle_input("<", cx);
4937 });
4938 cx.assert_editor_state(
4939 &r#"
4940 <body><ˇ>
4941 <script>
4942 var x = 1;<ˇ
4943 </script>
4944 </body><ˇ>
4945 "#
4946 .unindent(),
4947 );
4948
4949 // When backspacing, the closing angle brackets are removed.
4950 cx.update_editor(|editor, cx| {
4951 editor.backspace(&Backspace, cx);
4952 });
4953 cx.assert_editor_state(
4954 &r#"
4955 <body>ˇ
4956 <script>
4957 var x = 1;ˇ
4958 </script>
4959 </body>ˇ
4960 "#
4961 .unindent(),
4962 );
4963
4964 // Block comments autoclose in JavaScript, but not HTML.
4965 cx.update_editor(|editor, cx| {
4966 editor.handle_input("/", cx);
4967 editor.handle_input("*", cx);
4968 });
4969 cx.assert_editor_state(
4970 &r#"
4971 <body>/*ˇ
4972 <script>
4973 var x = 1;/*ˇ */
4974 </script>
4975 </body>/*ˇ
4976 "#
4977 .unindent(),
4978 );
4979}
4980
4981#[gpui::test]
4982async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
4983 init_test(cx, |_| {});
4984
4985 let mut cx = EditorTestContext::new(cx).await;
4986
4987 let rust_language = Arc::new(
4988 Language::new(
4989 LanguageConfig {
4990 name: "Rust".into(),
4991 brackets: serde_json::from_value(json!([
4992 { "start": "{", "end": "}", "close": true, "newline": true },
4993 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
4994 ]))
4995 .unwrap(),
4996 autoclose_before: "})]>".into(),
4997 ..Default::default()
4998 },
4999 Some(tree_sitter_rust::language()),
5000 )
5001 .with_override_query("(string_literal) @string")
5002 .unwrap(),
5003 );
5004
5005 cx.language_registry().add(rust_language.clone());
5006 cx.update_buffer(|buffer, cx| {
5007 buffer.set_language(Some(rust_language), cx);
5008 });
5009
5010 cx.set_state(
5011 &r#"
5012 let x = ˇ
5013 "#
5014 .unindent(),
5015 );
5016
5017 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
5018 cx.update_editor(|editor, cx| {
5019 editor.handle_input("\"", cx);
5020 });
5021 cx.assert_editor_state(
5022 &r#"
5023 let x = "ˇ"
5024 "#
5025 .unindent(),
5026 );
5027
5028 // Inserting another quotation mark. The cursor moves across the existing
5029 // automatically-inserted quotation mark.
5030 cx.update_editor(|editor, cx| {
5031 editor.handle_input("\"", cx);
5032 });
5033 cx.assert_editor_state(
5034 &r#"
5035 let x = ""ˇ
5036 "#
5037 .unindent(),
5038 );
5039
5040 // Reset
5041 cx.set_state(
5042 &r#"
5043 let x = ˇ
5044 "#
5045 .unindent(),
5046 );
5047
5048 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
5049 cx.update_editor(|editor, cx| {
5050 editor.handle_input("\"", cx);
5051 editor.handle_input(" ", cx);
5052 editor.move_left(&Default::default(), cx);
5053 editor.handle_input("\\", cx);
5054 editor.handle_input("\"", cx);
5055 });
5056 cx.assert_editor_state(
5057 &r#"
5058 let x = "\"ˇ "
5059 "#
5060 .unindent(),
5061 );
5062
5063 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
5064 // mark. Nothing is inserted.
5065 cx.update_editor(|editor, cx| {
5066 editor.move_right(&Default::default(), cx);
5067 editor.handle_input("\"", cx);
5068 });
5069 cx.assert_editor_state(
5070 &r#"
5071 let x = "\" "ˇ
5072 "#
5073 .unindent(),
5074 );
5075}
5076
5077#[gpui::test]
5078async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
5079 init_test(cx, |_| {});
5080
5081 let language = Arc::new(Language::new(
5082 LanguageConfig {
5083 brackets: BracketPairConfig {
5084 pairs: vec![
5085 BracketPair {
5086 start: "{".to_string(),
5087 end: "}".to_string(),
5088 close: true,
5089 newline: true,
5090 },
5091 BracketPair {
5092 start: "/* ".to_string(),
5093 end: "*/".to_string(),
5094 close: true,
5095 ..Default::default()
5096 },
5097 ],
5098 ..Default::default()
5099 },
5100 ..Default::default()
5101 },
5102 Some(tree_sitter_rust::language()),
5103 ));
5104
5105 let text = r#"
5106 a
5107 b
5108 c
5109 "#
5110 .unindent();
5111
5112 let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx));
5113 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
5114 let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
5115 view.condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
5116 .await;
5117
5118 _ = view.update(cx, |view, cx| {
5119 view.change_selections(None, cx, |s| {
5120 s.select_display_ranges([
5121 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
5122 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
5123 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1),
5124 ])
5125 });
5126
5127 view.handle_input("{", cx);
5128 view.handle_input("{", cx);
5129 view.handle_input("{", cx);
5130 assert_eq!(
5131 view.text(cx),
5132 "
5133 {{{a}}}
5134 {{{b}}}
5135 {{{c}}}
5136 "
5137 .unindent()
5138 );
5139 assert_eq!(
5140 view.selections.display_ranges(cx),
5141 [
5142 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 4),
5143 DisplayPoint::new(1, 3)..DisplayPoint::new(1, 4),
5144 DisplayPoint::new(2, 3)..DisplayPoint::new(2, 4)
5145 ]
5146 );
5147
5148 view.undo(&Undo, cx);
5149 view.undo(&Undo, cx);
5150 view.undo(&Undo, cx);
5151 assert_eq!(
5152 view.text(cx),
5153 "
5154 a
5155 b
5156 c
5157 "
5158 .unindent()
5159 );
5160 assert_eq!(
5161 view.selections.display_ranges(cx),
5162 [
5163 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
5164 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
5165 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1)
5166 ]
5167 );
5168
5169 // Ensure inserting the first character of a multi-byte bracket pair
5170 // doesn't surround the selections with the bracket.
5171 view.handle_input("/", cx);
5172 assert_eq!(
5173 view.text(cx),
5174 "
5175 /
5176 /
5177 /
5178 "
5179 .unindent()
5180 );
5181 assert_eq!(
5182 view.selections.display_ranges(cx),
5183 [
5184 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
5185 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
5186 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1)
5187 ]
5188 );
5189
5190 view.undo(&Undo, cx);
5191 assert_eq!(
5192 view.text(cx),
5193 "
5194 a
5195 b
5196 c
5197 "
5198 .unindent()
5199 );
5200 assert_eq!(
5201 view.selections.display_ranges(cx),
5202 [
5203 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
5204 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
5205 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1)
5206 ]
5207 );
5208
5209 // Ensure inserting the last character of a multi-byte bracket pair
5210 // doesn't surround the selections with the bracket.
5211 view.handle_input("*", cx);
5212 assert_eq!(
5213 view.text(cx),
5214 "
5215 *
5216 *
5217 *
5218 "
5219 .unindent()
5220 );
5221 assert_eq!(
5222 view.selections.display_ranges(cx),
5223 [
5224 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
5225 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
5226 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1)
5227 ]
5228 );
5229 });
5230}
5231
5232#[gpui::test]
5233async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
5234 init_test(cx, |_| {});
5235
5236 let language = Arc::new(Language::new(
5237 LanguageConfig {
5238 brackets: BracketPairConfig {
5239 pairs: vec![BracketPair {
5240 start: "{".to_string(),
5241 end: "}".to_string(),
5242 close: true,
5243 newline: true,
5244 }],
5245 ..Default::default()
5246 },
5247 autoclose_before: "}".to_string(),
5248 ..Default::default()
5249 },
5250 Some(tree_sitter_rust::language()),
5251 ));
5252
5253 let text = r#"
5254 a
5255 b
5256 c
5257 "#
5258 .unindent();
5259
5260 let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx));
5261 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
5262 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
5263 editor
5264 .condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
5265 .await;
5266
5267 _ = editor.update(cx, |editor, cx| {
5268 editor.change_selections(None, cx, |s| {
5269 s.select_ranges([
5270 Point::new(0, 1)..Point::new(0, 1),
5271 Point::new(1, 1)..Point::new(1, 1),
5272 Point::new(2, 1)..Point::new(2, 1),
5273 ])
5274 });
5275
5276 editor.handle_input("{", cx);
5277 editor.handle_input("{", cx);
5278 editor.handle_input("_", cx);
5279 assert_eq!(
5280 editor.text(cx),
5281 "
5282 a{{_}}
5283 b{{_}}
5284 c{{_}}
5285 "
5286 .unindent()
5287 );
5288 assert_eq!(
5289 editor.selections.ranges::<Point>(cx),
5290 [
5291 Point::new(0, 4)..Point::new(0, 4),
5292 Point::new(1, 4)..Point::new(1, 4),
5293 Point::new(2, 4)..Point::new(2, 4)
5294 ]
5295 );
5296
5297 editor.backspace(&Default::default(), cx);
5298 editor.backspace(&Default::default(), cx);
5299 assert_eq!(
5300 editor.text(cx),
5301 "
5302 a{}
5303 b{}
5304 c{}
5305 "
5306 .unindent()
5307 );
5308 assert_eq!(
5309 editor.selections.ranges::<Point>(cx),
5310 [
5311 Point::new(0, 2)..Point::new(0, 2),
5312 Point::new(1, 2)..Point::new(1, 2),
5313 Point::new(2, 2)..Point::new(2, 2)
5314 ]
5315 );
5316
5317 editor.delete_to_previous_word_start(&Default::default(), cx);
5318 assert_eq!(
5319 editor.text(cx),
5320 "
5321 a
5322 b
5323 c
5324 "
5325 .unindent()
5326 );
5327 assert_eq!(
5328 editor.selections.ranges::<Point>(cx),
5329 [
5330 Point::new(0, 1)..Point::new(0, 1),
5331 Point::new(1, 1)..Point::new(1, 1),
5332 Point::new(2, 1)..Point::new(2, 1)
5333 ]
5334 );
5335 });
5336}
5337
5338#[gpui::test]
5339async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut gpui::TestAppContext) {
5340 init_test(cx, |settings| {
5341 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
5342 });
5343
5344 let mut cx = EditorTestContext::new(cx).await;
5345
5346 let language = Arc::new(Language::new(
5347 LanguageConfig {
5348 brackets: BracketPairConfig {
5349 pairs: vec![
5350 BracketPair {
5351 start: "{".to_string(),
5352 end: "}".to_string(),
5353 close: true,
5354 newline: true,
5355 },
5356 BracketPair {
5357 start: "(".to_string(),
5358 end: ")".to_string(),
5359 close: true,
5360 newline: true,
5361 },
5362 BracketPair {
5363 start: "[".to_string(),
5364 end: "]".to_string(),
5365 close: false,
5366 newline: true,
5367 },
5368 ],
5369 ..Default::default()
5370 },
5371 autoclose_before: "})]".to_string(),
5372 ..Default::default()
5373 },
5374 Some(tree_sitter_rust::language()),
5375 ));
5376
5377 cx.language_registry().add(language.clone());
5378 cx.update_buffer(|buffer, cx| {
5379 buffer.set_language(Some(language), cx);
5380 });
5381
5382 cx.set_state(
5383 &"
5384 {(ˇ)}
5385 [[ˇ]]
5386 {(ˇ)}
5387 "
5388 .unindent(),
5389 );
5390
5391 cx.update_editor(|view, cx| {
5392 view.backspace(&Default::default(), cx);
5393 view.backspace(&Default::default(), cx);
5394 });
5395
5396 cx.assert_editor_state(
5397 &"
5398 ˇ
5399 ˇ]]
5400 ˇ
5401 "
5402 .unindent(),
5403 );
5404
5405 cx.update_editor(|view, cx| {
5406 view.handle_input("{", cx);
5407 view.handle_input("{", cx);
5408 view.move_right(&MoveRight, cx);
5409 view.move_right(&MoveRight, cx);
5410 view.move_left(&MoveLeft, cx);
5411 view.move_left(&MoveLeft, cx);
5412 view.backspace(&Default::default(), cx);
5413 });
5414
5415 cx.assert_editor_state(
5416 &"
5417 {ˇ}
5418 {ˇ}]]
5419 {ˇ}
5420 "
5421 .unindent(),
5422 );
5423
5424 cx.update_editor(|view, cx| {
5425 view.backspace(&Default::default(), cx);
5426 });
5427
5428 cx.assert_editor_state(
5429 &"
5430 ˇ
5431 ˇ]]
5432 ˇ
5433 "
5434 .unindent(),
5435 );
5436}
5437
5438#[gpui::test]
5439async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) {
5440 init_test(cx, |_| {});
5441
5442 let language = Arc::new(Language::new(
5443 LanguageConfig::default(),
5444 Some(tree_sitter_rust::language()),
5445 ));
5446
5447 let buffer = cx.new_model(|cx| Buffer::local("", cx).with_language(language, cx));
5448 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
5449 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
5450 editor
5451 .condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
5452 .await;
5453
5454 _ = editor.update(cx, |editor, cx| {
5455 editor.set_auto_replace_emoji_shortcode(true);
5456
5457 editor.handle_input("Hello ", cx);
5458 editor.handle_input(":wave", cx);
5459 assert_eq!(editor.text(cx), "Hello :wave".unindent());
5460
5461 editor.handle_input(":", cx);
5462 assert_eq!(editor.text(cx), "Hello 👋".unindent());
5463
5464 editor.handle_input(" :smile", cx);
5465 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
5466
5467 editor.handle_input(":", cx);
5468 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
5469
5470 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
5471 editor.handle_input(":wave", cx);
5472 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
5473
5474 editor.handle_input(":", cx);
5475 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
5476
5477 editor.handle_input(":1", cx);
5478 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
5479
5480 editor.handle_input(":", cx);
5481 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
5482
5483 // Ensure shortcode does not get replaced when it is part of a word
5484 editor.handle_input(" Test:wave", cx);
5485 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
5486
5487 editor.handle_input(":", cx);
5488 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
5489
5490 editor.set_auto_replace_emoji_shortcode(false);
5491
5492 // Ensure shortcode does not get replaced when auto replace is off
5493 editor.handle_input(" :wave", cx);
5494 assert_eq!(
5495 editor.text(cx),
5496 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
5497 );
5498
5499 editor.handle_input(":", cx);
5500 assert_eq!(
5501 editor.text(cx),
5502 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
5503 );
5504 });
5505}
5506
5507#[gpui::test]
5508async fn test_snippets(cx: &mut gpui::TestAppContext) {
5509 init_test(cx, |_| {});
5510
5511 let (text, insertion_ranges) = marked_text_ranges(
5512 indoc! {"
5513 a.ˇ b
5514 a.ˇ b
5515 a.ˇ b
5516 "},
5517 false,
5518 );
5519
5520 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
5521 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
5522
5523 _ = editor.update(cx, |editor, cx| {
5524 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
5525
5526 editor
5527 .insert_snippet(&insertion_ranges, snippet, cx)
5528 .unwrap();
5529
5530 fn assert(editor: &mut Editor, cx: &mut ViewContext<Editor>, marked_text: &str) {
5531 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
5532 assert_eq!(editor.text(cx), expected_text);
5533 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
5534 }
5535
5536 assert(
5537 editor,
5538 cx,
5539 indoc! {"
5540 a.f(«one», two, «three») b
5541 a.f(«one», two, «three») b
5542 a.f(«one», two, «three») b
5543 "},
5544 );
5545
5546 // Can't move earlier than the first tab stop
5547 assert!(!editor.move_to_prev_snippet_tabstop(cx));
5548 assert(
5549 editor,
5550 cx,
5551 indoc! {"
5552 a.f(«one», two, «three») b
5553 a.f(«one», two, «three») b
5554 a.f(«one», two, «three») b
5555 "},
5556 );
5557
5558 assert!(editor.move_to_next_snippet_tabstop(cx));
5559 assert(
5560 editor,
5561 cx,
5562 indoc! {"
5563 a.f(one, «two», three) b
5564 a.f(one, «two», three) b
5565 a.f(one, «two», three) b
5566 "},
5567 );
5568
5569 editor.move_to_prev_snippet_tabstop(cx);
5570 assert(
5571 editor,
5572 cx,
5573 indoc! {"
5574 a.f(«one», two, «three») b
5575 a.f(«one», two, «three») b
5576 a.f(«one», two, «three») b
5577 "},
5578 );
5579
5580 assert!(editor.move_to_next_snippet_tabstop(cx));
5581 assert(
5582 editor,
5583 cx,
5584 indoc! {"
5585 a.f(one, «two», three) b
5586 a.f(one, «two», three) b
5587 a.f(one, «two», three) b
5588 "},
5589 );
5590 assert!(editor.move_to_next_snippet_tabstop(cx));
5591 assert(
5592 editor,
5593 cx,
5594 indoc! {"
5595 a.f(one, two, three)ˇ b
5596 a.f(one, two, three)ˇ b
5597 a.f(one, two, three)ˇ b
5598 "},
5599 );
5600
5601 // As soon as the last tab stop is reached, snippet state is gone
5602 editor.move_to_prev_snippet_tabstop(cx);
5603 assert(
5604 editor,
5605 cx,
5606 indoc! {"
5607 a.f(one, two, three)ˇ b
5608 a.f(one, two, three)ˇ b
5609 a.f(one, two, three)ˇ b
5610 "},
5611 );
5612 });
5613}
5614
5615#[gpui::test]
5616async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
5617 init_test(cx, |_| {});
5618
5619 let fs = FakeFs::new(cx.executor());
5620 fs.insert_file("/file.rs", Default::default()).await;
5621
5622 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
5623
5624 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
5625 language_registry.add(rust_lang());
5626 let mut fake_servers = language_registry.register_fake_lsp_adapter(
5627 "Rust",
5628 FakeLspAdapter {
5629 capabilities: lsp::ServerCapabilities {
5630 document_formatting_provider: Some(lsp::OneOf::Left(true)),
5631 ..Default::default()
5632 },
5633 ..Default::default()
5634 },
5635 );
5636
5637 let buffer = project
5638 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
5639 .await
5640 .unwrap();
5641
5642 cx.executor().start_waiting();
5643 let fake_server = fake_servers.next().await.unwrap();
5644
5645 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
5646 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
5647 editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
5648 assert!(cx.read(|cx| editor.is_dirty(cx)));
5649
5650 let save = editor
5651 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
5652 .unwrap();
5653 fake_server
5654 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
5655 assert_eq!(
5656 params.text_document.uri,
5657 lsp::Url::from_file_path("/file.rs").unwrap()
5658 );
5659 assert_eq!(params.options.tab_size, 4);
5660 Ok(Some(vec![lsp::TextEdit::new(
5661 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
5662 ", ".to_string(),
5663 )]))
5664 })
5665 .next()
5666 .await;
5667 cx.executor().start_waiting();
5668 save.await;
5669
5670 assert_eq!(
5671 editor.update(cx, |editor, cx| editor.text(cx)),
5672 "one, two\nthree\n"
5673 );
5674 assert!(!cx.read(|cx| editor.is_dirty(cx)));
5675
5676 editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
5677 assert!(cx.read(|cx| editor.is_dirty(cx)));
5678
5679 // Ensure we can still save even if formatting hangs.
5680 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
5681 assert_eq!(
5682 params.text_document.uri,
5683 lsp::Url::from_file_path("/file.rs").unwrap()
5684 );
5685 futures::future::pending::<()>().await;
5686 unreachable!()
5687 });
5688 let save = editor
5689 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
5690 .unwrap();
5691 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
5692 cx.executor().start_waiting();
5693 save.await;
5694 assert_eq!(
5695 editor.update(cx, |editor, cx| editor.text(cx)),
5696 "one\ntwo\nthree\n"
5697 );
5698 assert!(!cx.read(|cx| editor.is_dirty(cx)));
5699
5700 // For non-dirty buffer, no formatting request should be sent
5701 let save = editor
5702 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
5703 .unwrap();
5704 let _pending_format_request = fake_server
5705 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
5706 panic!("Should not be invoked on non-dirty buffer");
5707 })
5708 .next();
5709 cx.executor().start_waiting();
5710 save.await;
5711
5712 // Set rust language override and assert overridden tabsize is sent to language server
5713 update_test_language_settings(cx, |settings| {
5714 settings.languages.insert(
5715 "Rust".into(),
5716 LanguageSettingsContent {
5717 tab_size: NonZeroU32::new(8),
5718 ..Default::default()
5719 },
5720 );
5721 });
5722
5723 editor.update(cx, |editor, cx| editor.set_text("somehting_new\n", cx));
5724 assert!(cx.read(|cx| editor.is_dirty(cx)));
5725 let save = editor
5726 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
5727 .unwrap();
5728 fake_server
5729 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
5730 assert_eq!(
5731 params.text_document.uri,
5732 lsp::Url::from_file_path("/file.rs").unwrap()
5733 );
5734 assert_eq!(params.options.tab_size, 8);
5735 Ok(Some(vec![]))
5736 })
5737 .next()
5738 .await;
5739 cx.executor().start_waiting();
5740 save.await;
5741}
5742
5743#[gpui::test]
5744async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) {
5745 init_test(cx, |_| {});
5746
5747 let cols = 4;
5748 let rows = 10;
5749 let sample_text_1 = sample_text(rows, cols, 'a');
5750 assert_eq!(
5751 sample_text_1,
5752 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
5753 );
5754 let sample_text_2 = sample_text(rows, cols, 'l');
5755 assert_eq!(
5756 sample_text_2,
5757 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
5758 );
5759 let sample_text_3 = sample_text(rows, cols, 'v');
5760 assert_eq!(
5761 sample_text_3,
5762 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
5763 );
5764
5765 let fs = FakeFs::new(cx.executor());
5766 fs.insert_tree(
5767 "/a",
5768 json!({
5769 "main.rs": sample_text_1,
5770 "other.rs": sample_text_2,
5771 "lib.rs": sample_text_3,
5772 }),
5773 )
5774 .await;
5775
5776 let project = Project::test(fs, ["/a".as_ref()], cx).await;
5777 let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
5778 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
5779
5780 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
5781 language_registry.add(rust_lang());
5782 let mut fake_servers = language_registry.register_fake_lsp_adapter(
5783 "Rust",
5784 FakeLspAdapter {
5785 capabilities: lsp::ServerCapabilities {
5786 document_formatting_provider: Some(lsp::OneOf::Left(true)),
5787 ..Default::default()
5788 },
5789 ..Default::default()
5790 },
5791 );
5792
5793 let worktree = project.update(cx, |project, _| {
5794 let mut worktrees = project.worktrees().collect::<Vec<_>>();
5795 assert_eq!(worktrees.len(), 1);
5796 worktrees.pop().unwrap()
5797 });
5798 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
5799
5800 let buffer_1 = project
5801 .update(cx, |project, cx| {
5802 project.open_buffer((worktree_id, "main.rs"), cx)
5803 })
5804 .await
5805 .unwrap();
5806 let buffer_2 = project
5807 .update(cx, |project, cx| {
5808 project.open_buffer((worktree_id, "other.rs"), cx)
5809 })
5810 .await
5811 .unwrap();
5812 let buffer_3 = project
5813 .update(cx, |project, cx| {
5814 project.open_buffer((worktree_id, "lib.rs"), cx)
5815 })
5816 .await
5817 .unwrap();
5818
5819 let multi_buffer = cx.new_model(|cx| {
5820 let mut multi_buffer = MultiBuffer::new(0, ReadWrite);
5821 multi_buffer.push_excerpts(
5822 buffer_1.clone(),
5823 [
5824 ExcerptRange {
5825 context: Point::new(0, 0)..Point::new(3, 0),
5826 primary: None,
5827 },
5828 ExcerptRange {
5829 context: Point::new(5, 0)..Point::new(7, 0),
5830 primary: None,
5831 },
5832 ExcerptRange {
5833 context: Point::new(9, 0)..Point::new(10, 4),
5834 primary: None,
5835 },
5836 ],
5837 cx,
5838 );
5839 multi_buffer.push_excerpts(
5840 buffer_2.clone(),
5841 [
5842 ExcerptRange {
5843 context: Point::new(0, 0)..Point::new(3, 0),
5844 primary: None,
5845 },
5846 ExcerptRange {
5847 context: Point::new(5, 0)..Point::new(7, 0),
5848 primary: None,
5849 },
5850 ExcerptRange {
5851 context: Point::new(9, 0)..Point::new(10, 4),
5852 primary: None,
5853 },
5854 ],
5855 cx,
5856 );
5857 multi_buffer.push_excerpts(
5858 buffer_3.clone(),
5859 [
5860 ExcerptRange {
5861 context: Point::new(0, 0)..Point::new(3, 0),
5862 primary: None,
5863 },
5864 ExcerptRange {
5865 context: Point::new(5, 0)..Point::new(7, 0),
5866 primary: None,
5867 },
5868 ExcerptRange {
5869 context: Point::new(9, 0)..Point::new(10, 4),
5870 primary: None,
5871 },
5872 ],
5873 cx,
5874 );
5875 multi_buffer
5876 });
5877 let multi_buffer_editor =
5878 cx.new_view(|cx| Editor::new(EditorMode::Full, multi_buffer, Some(project.clone()), cx));
5879
5880 multi_buffer_editor.update(cx, |editor, cx| {
5881 editor.change_selections(Some(Autoscroll::Next), cx, |s| s.select_ranges(Some(1..2)));
5882 editor.insert("|one|two|three|", cx);
5883 });
5884 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
5885 multi_buffer_editor.update(cx, |editor, cx| {
5886 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
5887 s.select_ranges(Some(60..70))
5888 });
5889 editor.insert("|four|five|six|", cx);
5890 });
5891 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
5892
5893 // First two buffers should be edited, but not the third one.
5894 assert_eq!(
5895 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
5896 "a|one|two|three|aa\nbbbb\ncccc\n\nffff\ngggg\n\njjjj\nllll\nmmmm\nnnnn|four|five|six|\nr\n\nuuuu\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}",
5897 );
5898 buffer_1.update(cx, |buffer, _| {
5899 assert!(buffer.is_dirty());
5900 assert_eq!(
5901 buffer.text(),
5902 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
5903 )
5904 });
5905 buffer_2.update(cx, |buffer, _| {
5906 assert!(buffer.is_dirty());
5907 assert_eq!(
5908 buffer.text(),
5909 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
5910 )
5911 });
5912 buffer_3.update(cx, |buffer, _| {
5913 assert!(!buffer.is_dirty());
5914 assert_eq!(buffer.text(), sample_text_3,)
5915 });
5916
5917 cx.executor().start_waiting();
5918 let save = multi_buffer_editor
5919 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
5920 .unwrap();
5921
5922 let fake_server = fake_servers.next().await.unwrap();
5923 fake_server
5924 .server
5925 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
5926 Ok(Some(vec![lsp::TextEdit::new(
5927 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
5928 format!("[{} formatted]", params.text_document.uri),
5929 )]))
5930 })
5931 .detach();
5932 save.await;
5933
5934 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
5935 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
5936 assert_eq!(
5937 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
5938 "a|o[file:///a/main.rs formatted]bbbb\ncccc\n\nffff\ngggg\n\njjjj\n\nlll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|\nr\n\nuuuu\n\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}",
5939 );
5940 buffer_1.update(cx, |buffer, _| {
5941 assert!(!buffer.is_dirty());
5942 assert_eq!(
5943 buffer.text(),
5944 "a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n",
5945 )
5946 });
5947 buffer_2.update(cx, |buffer, _| {
5948 assert!(!buffer.is_dirty());
5949 assert_eq!(
5950 buffer.text(),
5951 "lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n",
5952 )
5953 });
5954 buffer_3.update(cx, |buffer, _| {
5955 assert!(!buffer.is_dirty());
5956 assert_eq!(buffer.text(), sample_text_3,)
5957 });
5958}
5959
5960#[gpui::test]
5961async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
5962 init_test(cx, |_| {});
5963
5964 let fs = FakeFs::new(cx.executor());
5965 fs.insert_file("/file.rs", Default::default()).await;
5966
5967 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
5968
5969 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
5970 language_registry.add(rust_lang());
5971 let mut fake_servers = language_registry.register_fake_lsp_adapter(
5972 "Rust",
5973 FakeLspAdapter {
5974 capabilities: lsp::ServerCapabilities {
5975 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
5976 ..Default::default()
5977 },
5978 ..Default::default()
5979 },
5980 );
5981
5982 let buffer = project
5983 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
5984 .await
5985 .unwrap();
5986
5987 cx.executor().start_waiting();
5988 let fake_server = fake_servers.next().await.unwrap();
5989
5990 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
5991 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
5992 editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
5993 assert!(cx.read(|cx| editor.is_dirty(cx)));
5994
5995 let save = editor
5996 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
5997 .unwrap();
5998 fake_server
5999 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
6000 assert_eq!(
6001 params.text_document.uri,
6002 lsp::Url::from_file_path("/file.rs").unwrap()
6003 );
6004 assert_eq!(params.options.tab_size, 4);
6005 Ok(Some(vec![lsp::TextEdit::new(
6006 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
6007 ", ".to_string(),
6008 )]))
6009 })
6010 .next()
6011 .await;
6012 cx.executor().start_waiting();
6013 save.await;
6014 assert_eq!(
6015 editor.update(cx, |editor, cx| editor.text(cx)),
6016 "one, two\nthree\n"
6017 );
6018 assert!(!cx.read(|cx| editor.is_dirty(cx)));
6019
6020 editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
6021 assert!(cx.read(|cx| editor.is_dirty(cx)));
6022
6023 // Ensure we can still save even if formatting hangs.
6024 fake_server.handle_request::<lsp::request::RangeFormatting, _, _>(
6025 move |params, _| async move {
6026 assert_eq!(
6027 params.text_document.uri,
6028 lsp::Url::from_file_path("/file.rs").unwrap()
6029 );
6030 futures::future::pending::<()>().await;
6031 unreachable!()
6032 },
6033 );
6034 let save = editor
6035 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
6036 .unwrap();
6037 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
6038 cx.executor().start_waiting();
6039 save.await;
6040 assert_eq!(
6041 editor.update(cx, |editor, cx| editor.text(cx)),
6042 "one\ntwo\nthree\n"
6043 );
6044 assert!(!cx.read(|cx| editor.is_dirty(cx)));
6045
6046 // For non-dirty buffer, no formatting request should be sent
6047 let save = editor
6048 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
6049 .unwrap();
6050 let _pending_format_request = fake_server
6051 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
6052 panic!("Should not be invoked on non-dirty buffer");
6053 })
6054 .next();
6055 cx.executor().start_waiting();
6056 save.await;
6057
6058 // Set Rust language override and assert overridden tabsize is sent to language server
6059 update_test_language_settings(cx, |settings| {
6060 settings.languages.insert(
6061 "Rust".into(),
6062 LanguageSettingsContent {
6063 tab_size: NonZeroU32::new(8),
6064 ..Default::default()
6065 },
6066 );
6067 });
6068
6069 editor.update(cx, |editor, cx| editor.set_text("somehting_new\n", cx));
6070 assert!(cx.read(|cx| editor.is_dirty(cx)));
6071 let save = editor
6072 .update(cx, |editor, cx| editor.save(true, project.clone(), cx))
6073 .unwrap();
6074 fake_server
6075 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
6076 assert_eq!(
6077 params.text_document.uri,
6078 lsp::Url::from_file_path("/file.rs").unwrap()
6079 );
6080 assert_eq!(params.options.tab_size, 8);
6081 Ok(Some(vec![]))
6082 })
6083 .next()
6084 .await;
6085 cx.executor().start_waiting();
6086 save.await;
6087}
6088
6089#[gpui::test]
6090async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
6091 init_test(cx, |settings| {
6092 settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer)
6093 });
6094
6095 let fs = FakeFs::new(cx.executor());
6096 fs.insert_file("/file.rs", Default::default()).await;
6097
6098 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
6099
6100 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
6101 language_registry.add(Arc::new(Language::new(
6102 LanguageConfig {
6103 name: "Rust".into(),
6104 matcher: LanguageMatcher {
6105 path_suffixes: vec!["rs".to_string()],
6106 ..Default::default()
6107 },
6108 // Enable Prettier formatting for the same buffer, and ensure
6109 // LSP is called instead of Prettier.
6110 prettier_parser_name: Some("test_parser".to_string()),
6111 ..Default::default()
6112 },
6113 Some(tree_sitter_rust::language()),
6114 )));
6115 let mut fake_servers = language_registry.register_fake_lsp_adapter(
6116 "Rust",
6117 FakeLspAdapter {
6118 capabilities: lsp::ServerCapabilities {
6119 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6120 ..Default::default()
6121 },
6122 ..Default::default()
6123 },
6124 );
6125
6126 let buffer = project
6127 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
6128 .await
6129 .unwrap();
6130
6131 cx.executor().start_waiting();
6132 let fake_server = fake_servers.next().await.unwrap();
6133
6134 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
6135 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
6136 _ = editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
6137
6138 let format = editor
6139 .update(cx, |editor, cx| {
6140 editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
6141 })
6142 .unwrap();
6143 fake_server
6144 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
6145 assert_eq!(
6146 params.text_document.uri,
6147 lsp::Url::from_file_path("/file.rs").unwrap()
6148 );
6149 assert_eq!(params.options.tab_size, 4);
6150 Ok(Some(vec![lsp::TextEdit::new(
6151 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
6152 ", ".to_string(),
6153 )]))
6154 })
6155 .next()
6156 .await;
6157 cx.executor().start_waiting();
6158 format.await;
6159 assert_eq!(
6160 editor.update(cx, |editor, cx| editor.text(cx)),
6161 "one, two\nthree\n"
6162 );
6163
6164 _ = editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
6165 // Ensure we don't lock if formatting hangs.
6166 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
6167 assert_eq!(
6168 params.text_document.uri,
6169 lsp::Url::from_file_path("/file.rs").unwrap()
6170 );
6171 futures::future::pending::<()>().await;
6172 unreachable!()
6173 });
6174 let format = editor
6175 .update(cx, |editor, cx| {
6176 editor.perform_format(project, FormatTrigger::Manual, cx)
6177 })
6178 .unwrap();
6179 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
6180 cx.executor().start_waiting();
6181 format.await;
6182 assert_eq!(
6183 editor.update(cx, |editor, cx| editor.text(cx)),
6184 "one\ntwo\nthree\n"
6185 );
6186}
6187
6188#[gpui::test]
6189async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
6190 init_test(cx, |_| {});
6191
6192 let mut cx = EditorLspTestContext::new_rust(
6193 lsp::ServerCapabilities {
6194 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6195 ..Default::default()
6196 },
6197 cx,
6198 )
6199 .await;
6200
6201 cx.set_state(indoc! {"
6202 one.twoˇ
6203 "});
6204
6205 // The format request takes a long time. When it completes, it inserts
6206 // a newline and an indent before the `.`
6207 cx.lsp
6208 .handle_request::<lsp::request::Formatting, _, _>(move |_, cx| {
6209 let executor = cx.background_executor().clone();
6210 async move {
6211 executor.timer(Duration::from_millis(100)).await;
6212 Ok(Some(vec![lsp::TextEdit {
6213 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
6214 new_text: "\n ".into(),
6215 }]))
6216 }
6217 });
6218
6219 // Submit a format request.
6220 let format_1 = cx
6221 .update_editor(|editor, cx| editor.format(&Format, cx))
6222 .unwrap();
6223 cx.executor().run_until_parked();
6224
6225 // Submit a second format request.
6226 let format_2 = cx
6227 .update_editor(|editor, cx| editor.format(&Format, cx))
6228 .unwrap();
6229 cx.executor().run_until_parked();
6230
6231 // Wait for both format requests to complete
6232 cx.executor().advance_clock(Duration::from_millis(200));
6233 cx.executor().start_waiting();
6234 format_1.await.unwrap();
6235 cx.executor().start_waiting();
6236 format_2.await.unwrap();
6237
6238 // The formatting edits only happens once.
6239 cx.assert_editor_state(indoc! {"
6240 one
6241 .twoˇ
6242 "});
6243}
6244
6245#[gpui::test]
6246async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
6247 init_test(cx, |settings| {
6248 settings.defaults.formatter = Some(language_settings::Formatter::Auto)
6249 });
6250
6251 let mut cx = EditorLspTestContext::new_rust(
6252 lsp::ServerCapabilities {
6253 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6254 ..Default::default()
6255 },
6256 cx,
6257 )
6258 .await;
6259
6260 // Set up a buffer white some trailing whitespace and no trailing newline.
6261 cx.set_state(
6262 &[
6263 "one ", //
6264 "twoˇ", //
6265 "three ", //
6266 "four", //
6267 ]
6268 .join("\n"),
6269 );
6270
6271 // Submit a format request.
6272 let format = cx
6273 .update_editor(|editor, cx| editor.format(&Format, cx))
6274 .unwrap();
6275
6276 // Record which buffer changes have been sent to the language server
6277 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
6278 cx.lsp
6279 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
6280 let buffer_changes = buffer_changes.clone();
6281 move |params, _| {
6282 buffer_changes.lock().extend(
6283 params
6284 .content_changes
6285 .into_iter()
6286 .map(|e| (e.range.unwrap(), e.text)),
6287 );
6288 }
6289 });
6290
6291 // Handle formatting requests to the language server.
6292 cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
6293 let buffer_changes = buffer_changes.clone();
6294 move |_, _| {
6295 // When formatting is requested, trailing whitespace has already been stripped,
6296 // and the trailing newline has already been added.
6297 assert_eq!(
6298 &buffer_changes.lock()[1..],
6299 &[
6300 (
6301 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
6302 "".into()
6303 ),
6304 (
6305 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
6306 "".into()
6307 ),
6308 (
6309 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
6310 "\n".into()
6311 ),
6312 ]
6313 );
6314
6315 // Insert blank lines between each line of the buffer.
6316 async move {
6317 Ok(Some(vec![
6318 lsp::TextEdit {
6319 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
6320 new_text: "\n".into(),
6321 },
6322 lsp::TextEdit {
6323 range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
6324 new_text: "\n".into(),
6325 },
6326 ]))
6327 }
6328 }
6329 });
6330
6331 // After formatting the buffer, the trailing whitespace is stripped,
6332 // a newline is appended, and the edits provided by the language server
6333 // have been applied.
6334 format.await.unwrap();
6335 cx.assert_editor_state(
6336 &[
6337 "one", //
6338 "", //
6339 "twoˇ", //
6340 "", //
6341 "three", //
6342 "four", //
6343 "", //
6344 ]
6345 .join("\n"),
6346 );
6347
6348 // Undoing the formatting undoes the trailing whitespace removal, the
6349 // trailing newline, and the LSP edits.
6350 cx.update_buffer(|buffer, cx| buffer.undo(cx));
6351 cx.assert_editor_state(
6352 &[
6353 "one ", //
6354 "twoˇ", //
6355 "three ", //
6356 "four", //
6357 ]
6358 .join("\n"),
6359 );
6360}
6361
6362#[gpui::test]
6363async fn test_completion(cx: &mut gpui::TestAppContext) {
6364 init_test(cx, |_| {});
6365
6366 let mut cx = EditorLspTestContext::new_rust(
6367 lsp::ServerCapabilities {
6368 completion_provider: Some(lsp::CompletionOptions {
6369 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
6370 resolve_provider: Some(true),
6371 ..Default::default()
6372 }),
6373 ..Default::default()
6374 },
6375 cx,
6376 )
6377 .await;
6378
6379 cx.set_state(indoc! {"
6380 oneˇ
6381 two
6382 three
6383 "});
6384 cx.simulate_keystroke(".");
6385 handle_completion_request(
6386 &mut cx,
6387 indoc! {"
6388 one.|<>
6389 two
6390 three
6391 "},
6392 vec!["first_completion", "second_completion"],
6393 )
6394 .await;
6395 cx.condition(|editor, _| editor.context_menu_visible())
6396 .await;
6397 let apply_additional_edits = cx.update_editor(|editor, cx| {
6398 editor.context_menu_next(&Default::default(), cx);
6399 editor
6400 .confirm_completion(&ConfirmCompletion::default(), cx)
6401 .unwrap()
6402 });
6403 cx.assert_editor_state(indoc! {"
6404 one.second_completionˇ
6405 two
6406 three
6407 "});
6408
6409 handle_resolve_completion_request(
6410 &mut cx,
6411 Some(vec![
6412 (
6413 //This overlaps with the primary completion edit which is
6414 //misbehavior from the LSP spec, test that we filter it out
6415 indoc! {"
6416 one.second_ˇcompletion
6417 two
6418 threeˇ
6419 "},
6420 "overlapping additional edit",
6421 ),
6422 (
6423 indoc! {"
6424 one.second_completion
6425 two
6426 threeˇ
6427 "},
6428 "\nadditional edit",
6429 ),
6430 ]),
6431 )
6432 .await;
6433 apply_additional_edits.await.unwrap();
6434 cx.assert_editor_state(indoc! {"
6435 one.second_completionˇ
6436 two
6437 three
6438 additional edit
6439 "});
6440
6441 cx.set_state(indoc! {"
6442 one.second_completion
6443 twoˇ
6444 threeˇ
6445 additional edit
6446 "});
6447 cx.simulate_keystroke(" ");
6448 assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
6449 cx.simulate_keystroke("s");
6450 assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
6451
6452 cx.assert_editor_state(indoc! {"
6453 one.second_completion
6454 two sˇ
6455 three sˇ
6456 additional edit
6457 "});
6458 handle_completion_request(
6459 &mut cx,
6460 indoc! {"
6461 one.second_completion
6462 two s
6463 three <s|>
6464 additional edit
6465 "},
6466 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
6467 )
6468 .await;
6469 cx.condition(|editor, _| editor.context_menu_visible())
6470 .await;
6471
6472 cx.simulate_keystroke("i");
6473
6474 handle_completion_request(
6475 &mut cx,
6476 indoc! {"
6477 one.second_completion
6478 two si
6479 three <si|>
6480 additional edit
6481 "},
6482 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
6483 )
6484 .await;
6485 cx.condition(|editor, _| editor.context_menu_visible())
6486 .await;
6487
6488 let apply_additional_edits = cx.update_editor(|editor, cx| {
6489 editor
6490 .confirm_completion(&ConfirmCompletion::default(), cx)
6491 .unwrap()
6492 });
6493 cx.assert_editor_state(indoc! {"
6494 one.second_completion
6495 two sixth_completionˇ
6496 three sixth_completionˇ
6497 additional edit
6498 "});
6499
6500 handle_resolve_completion_request(&mut cx, None).await;
6501 apply_additional_edits.await.unwrap();
6502
6503 _ = cx.update(|cx| {
6504 cx.update_global::<SettingsStore, _>(|settings, cx| {
6505 settings.update_user_settings::<EditorSettings>(cx, |settings| {
6506 settings.show_completions_on_input = Some(false);
6507 });
6508 })
6509 });
6510 cx.set_state("editorˇ");
6511 cx.simulate_keystroke(".");
6512 assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
6513 cx.simulate_keystroke("c");
6514 cx.simulate_keystroke("l");
6515 cx.simulate_keystroke("o");
6516 cx.assert_editor_state("editor.cloˇ");
6517 assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
6518 cx.update_editor(|editor, cx| {
6519 editor.show_completions(&ShowCompletions, cx);
6520 });
6521 handle_completion_request(&mut cx, "editor.<clo|>", vec!["close", "clobber"]).await;
6522 cx.condition(|editor, _| editor.context_menu_visible())
6523 .await;
6524 let apply_additional_edits = cx.update_editor(|editor, cx| {
6525 editor
6526 .confirm_completion(&ConfirmCompletion::default(), cx)
6527 .unwrap()
6528 });
6529 cx.assert_editor_state("editor.closeˇ");
6530 handle_resolve_completion_request(&mut cx, None).await;
6531 apply_additional_edits.await.unwrap();
6532}
6533
6534#[gpui::test]
6535async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
6536 init_test(cx, |_| {});
6537 let mut cx = EditorTestContext::new(cx).await;
6538 let language = Arc::new(Language::new(
6539 LanguageConfig {
6540 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
6541 ..Default::default()
6542 },
6543 Some(tree_sitter_rust::language()),
6544 ));
6545 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6546
6547 // If multiple selections intersect a line, the line is only toggled once.
6548 cx.set_state(indoc! {"
6549 fn a() {
6550 «//b();
6551 ˇ»// «c();
6552 //ˇ» d();
6553 }
6554 "});
6555
6556 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
6557
6558 cx.assert_editor_state(indoc! {"
6559 fn a() {
6560 «b();
6561 c();
6562 ˇ» d();
6563 }
6564 "});
6565
6566 // The comment prefix is inserted at the same column for every line in a
6567 // selection.
6568 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
6569
6570 cx.assert_editor_state(indoc! {"
6571 fn a() {
6572 // «b();
6573 // c();
6574 ˇ»// d();
6575 }
6576 "});
6577
6578 // If a selection ends at the beginning of a line, that line is not toggled.
6579 cx.set_selections_state(indoc! {"
6580 fn a() {
6581 // b();
6582 «// c();
6583 ˇ» // d();
6584 }
6585 "});
6586
6587 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
6588
6589 cx.assert_editor_state(indoc! {"
6590 fn a() {
6591 // b();
6592 «c();
6593 ˇ» // d();
6594 }
6595 "});
6596
6597 // If a selection span a single line and is empty, the line is toggled.
6598 cx.set_state(indoc! {"
6599 fn a() {
6600 a();
6601 b();
6602 ˇ
6603 }
6604 "});
6605
6606 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
6607
6608 cx.assert_editor_state(indoc! {"
6609 fn a() {
6610 a();
6611 b();
6612 //•ˇ
6613 }
6614 "});
6615
6616 // If a selection span multiple lines, empty lines are not toggled.
6617 cx.set_state(indoc! {"
6618 fn a() {
6619 «a();
6620
6621 c();ˇ»
6622 }
6623 "});
6624
6625 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
6626
6627 cx.assert_editor_state(indoc! {"
6628 fn a() {
6629 // «a();
6630
6631 // c();ˇ»
6632 }
6633 "});
6634
6635 // If a selection includes multiple comment prefixes, all lines are uncommented.
6636 cx.set_state(indoc! {"
6637 fn a() {
6638 «// a();
6639 /// b();
6640 //! c();ˇ»
6641 }
6642 "});
6643
6644 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
6645
6646 cx.assert_editor_state(indoc! {"
6647 fn a() {
6648 «a();
6649 b();
6650 c();ˇ»
6651 }
6652 "});
6653}
6654
6655#[gpui::test]
6656async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
6657 init_test(cx, |_| {});
6658
6659 let language = Arc::new(Language::new(
6660 LanguageConfig {
6661 line_comments: vec!["// ".into()],
6662 ..Default::default()
6663 },
6664 Some(tree_sitter_rust::language()),
6665 ));
6666
6667 let mut cx = EditorTestContext::new(cx).await;
6668
6669 cx.language_registry().add(language.clone());
6670 cx.update_buffer(|buffer, cx| {
6671 buffer.set_language(Some(language), cx);
6672 });
6673
6674 let toggle_comments = &ToggleComments {
6675 advance_downwards: true,
6676 };
6677
6678 // Single cursor on one line -> advance
6679 // Cursor moves horizontally 3 characters as well on non-blank line
6680 cx.set_state(indoc!(
6681 "fn a() {
6682 ˇdog();
6683 cat();
6684 }"
6685 ));
6686 cx.update_editor(|editor, cx| {
6687 editor.toggle_comments(toggle_comments, cx);
6688 });
6689 cx.assert_editor_state(indoc!(
6690 "fn a() {
6691 // dog();
6692 catˇ();
6693 }"
6694 ));
6695
6696 // Single selection on one line -> don't advance
6697 cx.set_state(indoc!(
6698 "fn a() {
6699 «dog()ˇ»;
6700 cat();
6701 }"
6702 ));
6703 cx.update_editor(|editor, cx| {
6704 editor.toggle_comments(toggle_comments, cx);
6705 });
6706 cx.assert_editor_state(indoc!(
6707 "fn a() {
6708 // «dog()ˇ»;
6709 cat();
6710 }"
6711 ));
6712
6713 // Multiple cursors on one line -> advance
6714 cx.set_state(indoc!(
6715 "fn a() {
6716 ˇdˇog();
6717 cat();
6718 }"
6719 ));
6720 cx.update_editor(|editor, cx| {
6721 editor.toggle_comments(toggle_comments, cx);
6722 });
6723 cx.assert_editor_state(indoc!(
6724 "fn a() {
6725 // dog();
6726 catˇ(ˇ);
6727 }"
6728 ));
6729
6730 // Multiple cursors on one line, with selection -> don't advance
6731 cx.set_state(indoc!(
6732 "fn a() {
6733 ˇdˇog«()ˇ»;
6734 cat();
6735 }"
6736 ));
6737 cx.update_editor(|editor, cx| {
6738 editor.toggle_comments(toggle_comments, cx);
6739 });
6740 cx.assert_editor_state(indoc!(
6741 "fn a() {
6742 // ˇdˇog«()ˇ»;
6743 cat();
6744 }"
6745 ));
6746
6747 // Single cursor on one line -> advance
6748 // Cursor moves to column 0 on blank line
6749 cx.set_state(indoc!(
6750 "fn a() {
6751 ˇdog();
6752
6753 cat();
6754 }"
6755 ));
6756 cx.update_editor(|editor, cx| {
6757 editor.toggle_comments(toggle_comments, cx);
6758 });
6759 cx.assert_editor_state(indoc!(
6760 "fn a() {
6761 // dog();
6762 ˇ
6763 cat();
6764 }"
6765 ));
6766
6767 // Single cursor on one line -> advance
6768 // Cursor starts and ends at column 0
6769 cx.set_state(indoc!(
6770 "fn a() {
6771 ˇ dog();
6772 cat();
6773 }"
6774 ));
6775 cx.update_editor(|editor, cx| {
6776 editor.toggle_comments(toggle_comments, cx);
6777 });
6778 cx.assert_editor_state(indoc!(
6779 "fn a() {
6780 // dog();
6781 ˇ cat();
6782 }"
6783 ));
6784}
6785
6786#[gpui::test]
6787async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
6788 init_test(cx, |_| {});
6789
6790 let mut cx = EditorTestContext::new(cx).await;
6791
6792 let html_language = Arc::new(
6793 Language::new(
6794 LanguageConfig {
6795 name: "HTML".into(),
6796 block_comment: Some(("<!-- ".into(), " -->".into())),
6797 ..Default::default()
6798 },
6799 Some(tree_sitter_html::language()),
6800 )
6801 .with_injection_query(
6802 r#"
6803 (script_element
6804 (raw_text) @content
6805 (#set! "language" "javascript"))
6806 "#,
6807 )
6808 .unwrap(),
6809 );
6810
6811 let javascript_language = Arc::new(Language::new(
6812 LanguageConfig {
6813 name: "JavaScript".into(),
6814 line_comments: vec!["// ".into()],
6815 ..Default::default()
6816 },
6817 Some(tree_sitter_typescript::language_tsx()),
6818 ));
6819
6820 cx.language_registry().add(html_language.clone());
6821 cx.language_registry().add(javascript_language.clone());
6822 cx.update_buffer(|buffer, cx| {
6823 buffer.set_language(Some(html_language), cx);
6824 });
6825
6826 // Toggle comments for empty selections
6827 cx.set_state(
6828 &r#"
6829 <p>A</p>ˇ
6830 <p>B</p>ˇ
6831 <p>C</p>ˇ
6832 "#
6833 .unindent(),
6834 );
6835 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
6836 cx.assert_editor_state(
6837 &r#"
6838 <!-- <p>A</p>ˇ -->
6839 <!-- <p>B</p>ˇ -->
6840 <!-- <p>C</p>ˇ -->
6841 "#
6842 .unindent(),
6843 );
6844 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
6845 cx.assert_editor_state(
6846 &r#"
6847 <p>A</p>ˇ
6848 <p>B</p>ˇ
6849 <p>C</p>ˇ
6850 "#
6851 .unindent(),
6852 );
6853
6854 // Toggle comments for mixture of empty and non-empty selections, where
6855 // multiple selections occupy a given line.
6856 cx.set_state(
6857 &r#"
6858 <p>A«</p>
6859 <p>ˇ»B</p>ˇ
6860 <p>C«</p>
6861 <p>ˇ»D</p>ˇ
6862 "#
6863 .unindent(),
6864 );
6865
6866 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
6867 cx.assert_editor_state(
6868 &r#"
6869 <!-- <p>A«</p>
6870 <p>ˇ»B</p>ˇ -->
6871 <!-- <p>C«</p>
6872 <p>ˇ»D</p>ˇ -->
6873 "#
6874 .unindent(),
6875 );
6876 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
6877 cx.assert_editor_state(
6878 &r#"
6879 <p>A«</p>
6880 <p>ˇ»B</p>ˇ
6881 <p>C«</p>
6882 <p>ˇ»D</p>ˇ
6883 "#
6884 .unindent(),
6885 );
6886
6887 // Toggle comments when different languages are active for different
6888 // selections.
6889 cx.set_state(
6890 &r#"
6891 ˇ<script>
6892 ˇvar x = new Y();
6893 ˇ</script>
6894 "#
6895 .unindent(),
6896 );
6897 cx.executor().run_until_parked();
6898 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
6899 cx.assert_editor_state(
6900 &r#"
6901 <!-- ˇ<script> -->
6902 // ˇvar x = new Y();
6903 <!-- ˇ</script> -->
6904 "#
6905 .unindent(),
6906 );
6907}
6908
6909#[gpui::test]
6910fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
6911 init_test(cx, |_| {});
6912
6913 let buffer = cx.new_model(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
6914 let multibuffer = cx.new_model(|cx| {
6915 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
6916 multibuffer.push_excerpts(
6917 buffer.clone(),
6918 [
6919 ExcerptRange {
6920 context: Point::new(0, 0)..Point::new(0, 4),
6921 primary: None,
6922 },
6923 ExcerptRange {
6924 context: Point::new(1, 0)..Point::new(1, 4),
6925 primary: None,
6926 },
6927 ],
6928 cx,
6929 );
6930 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
6931 multibuffer
6932 });
6933
6934 let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
6935 _ = view.update(cx, |view, cx| {
6936 assert_eq!(view.text(cx), "aaaa\nbbbb");
6937 view.change_selections(None, cx, |s| {
6938 s.select_ranges([
6939 Point::new(0, 0)..Point::new(0, 0),
6940 Point::new(1, 0)..Point::new(1, 0),
6941 ])
6942 });
6943
6944 view.handle_input("X", cx);
6945 assert_eq!(view.text(cx), "Xaaaa\nXbbbb");
6946 assert_eq!(
6947 view.selections.ranges(cx),
6948 [
6949 Point::new(0, 1)..Point::new(0, 1),
6950 Point::new(1, 1)..Point::new(1, 1),
6951 ]
6952 );
6953
6954 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
6955 view.change_selections(None, cx, |s| {
6956 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
6957 });
6958 view.backspace(&Default::default(), cx);
6959 assert_eq!(view.text(cx), "Xa\nbbb");
6960 assert_eq!(
6961 view.selections.ranges(cx),
6962 [Point::new(1, 0)..Point::new(1, 0)]
6963 );
6964
6965 view.change_selections(None, cx, |s| {
6966 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
6967 });
6968 view.backspace(&Default::default(), cx);
6969 assert_eq!(view.text(cx), "X\nbb");
6970 assert_eq!(
6971 view.selections.ranges(cx),
6972 [Point::new(0, 1)..Point::new(0, 1)]
6973 );
6974 });
6975}
6976
6977#[gpui::test]
6978fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
6979 init_test(cx, |_| {});
6980
6981 let markers = vec![('[', ']').into(), ('(', ')').into()];
6982 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
6983 indoc! {"
6984 [aaaa
6985 (bbbb]
6986 cccc)",
6987 },
6988 markers.clone(),
6989 );
6990 let excerpt_ranges = markers.into_iter().map(|marker| {
6991 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
6992 ExcerptRange {
6993 context,
6994 primary: None,
6995 }
6996 });
6997 let buffer = cx.new_model(|cx| Buffer::local(initial_text, cx));
6998 let multibuffer = cx.new_model(|cx| {
6999 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
7000 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
7001 multibuffer
7002 });
7003
7004 let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
7005 _ = view.update(cx, |view, cx| {
7006 let (expected_text, selection_ranges) = marked_text_ranges(
7007 indoc! {"
7008 aaaa
7009 bˇbbb
7010 bˇbbˇb
7011 cccc"
7012 },
7013 true,
7014 );
7015 assert_eq!(view.text(cx), expected_text);
7016 view.change_selections(None, cx, |s| s.select_ranges(selection_ranges));
7017
7018 view.handle_input("X", cx);
7019
7020 let (expected_text, expected_selections) = marked_text_ranges(
7021 indoc! {"
7022 aaaa
7023 bXˇbbXb
7024 bXˇbbXˇb
7025 cccc"
7026 },
7027 false,
7028 );
7029 assert_eq!(view.text(cx), expected_text);
7030 assert_eq!(view.selections.ranges(cx), expected_selections);
7031
7032 view.newline(&Newline, cx);
7033 let (expected_text, expected_selections) = marked_text_ranges(
7034 indoc! {"
7035 aaaa
7036 bX
7037 ˇbbX
7038 b
7039 bX
7040 ˇbbX
7041 ˇb
7042 cccc"
7043 },
7044 false,
7045 );
7046 assert_eq!(view.text(cx), expected_text);
7047 assert_eq!(view.selections.ranges(cx), expected_selections);
7048 });
7049}
7050
7051#[gpui::test]
7052fn test_refresh_selections(cx: &mut TestAppContext) {
7053 init_test(cx, |_| {});
7054
7055 let buffer = cx.new_model(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
7056 let mut excerpt1_id = None;
7057 let multibuffer = cx.new_model(|cx| {
7058 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
7059 excerpt1_id = multibuffer
7060 .push_excerpts(
7061 buffer.clone(),
7062 [
7063 ExcerptRange {
7064 context: Point::new(0, 0)..Point::new(1, 4),
7065 primary: None,
7066 },
7067 ExcerptRange {
7068 context: Point::new(1, 0)..Point::new(2, 4),
7069 primary: None,
7070 },
7071 ],
7072 cx,
7073 )
7074 .into_iter()
7075 .next();
7076 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
7077 multibuffer
7078 });
7079
7080 let editor = cx.add_window(|cx| {
7081 let mut editor = build_editor(multibuffer.clone(), cx);
7082 let snapshot = editor.snapshot(cx);
7083 editor.change_selections(None, cx, |s| {
7084 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
7085 });
7086 editor.begin_selection(Point::new(2, 1).to_display_point(&snapshot), true, 1, cx);
7087 assert_eq!(
7088 editor.selections.ranges(cx),
7089 [
7090 Point::new(1, 3)..Point::new(1, 3),
7091 Point::new(2, 1)..Point::new(2, 1),
7092 ]
7093 );
7094 editor
7095 });
7096
7097 // Refreshing selections is a no-op when excerpts haven't changed.
7098 _ = editor.update(cx, |editor, cx| {
7099 editor.change_selections(None, cx, |s| s.refresh());
7100 assert_eq!(
7101 editor.selections.ranges(cx),
7102 [
7103 Point::new(1, 3)..Point::new(1, 3),
7104 Point::new(2, 1)..Point::new(2, 1),
7105 ]
7106 );
7107 });
7108
7109 _ = multibuffer.update(cx, |multibuffer, cx| {
7110 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
7111 });
7112 _ = editor.update(cx, |editor, cx| {
7113 // Removing an excerpt causes the first selection to become degenerate.
7114 assert_eq!(
7115 editor.selections.ranges(cx),
7116 [
7117 Point::new(0, 0)..Point::new(0, 0),
7118 Point::new(0, 1)..Point::new(0, 1)
7119 ]
7120 );
7121
7122 // Refreshing selections will relocate the first selection to the original buffer
7123 // location.
7124 editor.change_selections(None, cx, |s| s.refresh());
7125 assert_eq!(
7126 editor.selections.ranges(cx),
7127 [
7128 Point::new(0, 1)..Point::new(0, 1),
7129 Point::new(0, 3)..Point::new(0, 3)
7130 ]
7131 );
7132 assert!(editor.selections.pending_anchor().is_some());
7133 });
7134}
7135
7136#[gpui::test]
7137fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
7138 init_test(cx, |_| {});
7139
7140 let buffer = cx.new_model(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
7141 let mut excerpt1_id = None;
7142 let multibuffer = cx.new_model(|cx| {
7143 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
7144 excerpt1_id = multibuffer
7145 .push_excerpts(
7146 buffer.clone(),
7147 [
7148 ExcerptRange {
7149 context: Point::new(0, 0)..Point::new(1, 4),
7150 primary: None,
7151 },
7152 ExcerptRange {
7153 context: Point::new(1, 0)..Point::new(2, 4),
7154 primary: None,
7155 },
7156 ],
7157 cx,
7158 )
7159 .into_iter()
7160 .next();
7161 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
7162 multibuffer
7163 });
7164
7165 let editor = cx.add_window(|cx| {
7166 let mut editor = build_editor(multibuffer.clone(), cx);
7167 let snapshot = editor.snapshot(cx);
7168 editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx);
7169 assert_eq!(
7170 editor.selections.ranges(cx),
7171 [Point::new(1, 3)..Point::new(1, 3)]
7172 );
7173 editor
7174 });
7175
7176 _ = multibuffer.update(cx, |multibuffer, cx| {
7177 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
7178 });
7179 _ = editor.update(cx, |editor, cx| {
7180 assert_eq!(
7181 editor.selections.ranges(cx),
7182 [Point::new(0, 0)..Point::new(0, 0)]
7183 );
7184
7185 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
7186 editor.change_selections(None, cx, |s| s.refresh());
7187 assert_eq!(
7188 editor.selections.ranges(cx),
7189 [Point::new(0, 3)..Point::new(0, 3)]
7190 );
7191 assert!(editor.selections.pending_anchor().is_some());
7192 });
7193}
7194
7195#[gpui::test]
7196async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
7197 init_test(cx, |_| {});
7198
7199 let language = Arc::new(
7200 Language::new(
7201 LanguageConfig {
7202 brackets: BracketPairConfig {
7203 pairs: vec![
7204 BracketPair {
7205 start: "{".to_string(),
7206 end: "}".to_string(),
7207 close: true,
7208 newline: true,
7209 },
7210 BracketPair {
7211 start: "/* ".to_string(),
7212 end: " */".to_string(),
7213 close: true,
7214 newline: true,
7215 },
7216 ],
7217 ..Default::default()
7218 },
7219 ..Default::default()
7220 },
7221 Some(tree_sitter_rust::language()),
7222 )
7223 .with_indents_query("")
7224 .unwrap(),
7225 );
7226
7227 let text = concat!(
7228 "{ }\n", //
7229 " x\n", //
7230 " /* */\n", //
7231 "x\n", //
7232 "{{} }\n", //
7233 );
7234
7235 let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx));
7236 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
7237 let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
7238 view.condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
7239 .await;
7240
7241 _ = view.update(cx, |view, cx| {
7242 view.change_selections(None, cx, |s| {
7243 s.select_display_ranges([
7244 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3),
7245 DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
7246 DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4),
7247 ])
7248 });
7249 view.newline(&Newline, cx);
7250
7251 assert_eq!(
7252 view.buffer().read(cx).read(cx).text(),
7253 concat!(
7254 "{ \n", // Suppress rustfmt
7255 "\n", //
7256 "}\n", //
7257 " x\n", //
7258 " /* \n", //
7259 " \n", //
7260 " */\n", //
7261 "x\n", //
7262 "{{} \n", //
7263 "}\n", //
7264 )
7265 );
7266 });
7267}
7268
7269#[gpui::test]
7270fn test_highlighted_ranges(cx: &mut TestAppContext) {
7271 init_test(cx, |_| {});
7272
7273 let editor = cx.add_window(|cx| {
7274 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
7275 build_editor(buffer.clone(), cx)
7276 });
7277
7278 _ = editor.update(cx, |editor, cx| {
7279 struct Type1;
7280 struct Type2;
7281
7282 let buffer = editor.buffer.read(cx).snapshot(cx);
7283
7284 let anchor_range =
7285 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
7286
7287 editor.highlight_background::<Type1>(
7288 &[
7289 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
7290 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
7291 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
7292 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
7293 ],
7294 |_| Hsla::red(),
7295 cx,
7296 );
7297 editor.highlight_background::<Type2>(
7298 &[
7299 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
7300 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
7301 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
7302 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
7303 ],
7304 |_| Hsla::green(),
7305 cx,
7306 );
7307
7308 let snapshot = editor.snapshot(cx);
7309 let mut highlighted_ranges = editor.background_highlights_in_range(
7310 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
7311 &snapshot,
7312 cx.theme().colors(),
7313 );
7314 // Enforce a consistent ordering based on color without relying on the ordering of the
7315 // highlight's `TypeId` which is non-executor.
7316 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
7317 assert_eq!(
7318 highlighted_ranges,
7319 &[
7320 (
7321 DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4),
7322 Hsla::red(),
7323 ),
7324 (
7325 DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
7326 Hsla::red(),
7327 ),
7328 (
7329 DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5),
7330 Hsla::green(),
7331 ),
7332 (
7333 DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6),
7334 Hsla::green(),
7335 ),
7336 ]
7337 );
7338 assert_eq!(
7339 editor.background_highlights_in_range(
7340 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
7341 &snapshot,
7342 cx.theme().colors(),
7343 ),
7344 &[(
7345 DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
7346 Hsla::red(),
7347 )]
7348 );
7349 });
7350}
7351
7352#[gpui::test]
7353async fn test_following(cx: &mut gpui::TestAppContext) {
7354 init_test(cx, |_| {});
7355
7356 let fs = FakeFs::new(cx.executor());
7357 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
7358
7359 let buffer = project.update(cx, |project, cx| {
7360 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
7361 cx.new_model(|cx| MultiBuffer::singleton(buffer, cx))
7362 });
7363 let leader = cx.add_window(|cx| build_editor(buffer.clone(), cx));
7364 let follower = cx.update(|cx| {
7365 cx.open_window(
7366 WindowOptions {
7367 bounds: Some(Bounds::from_corners(
7368 gpui::Point::new(0.into(), 0.into()),
7369 gpui::Point::new(10.into(), 80.into()),
7370 )),
7371 ..Default::default()
7372 },
7373 |cx| cx.new_view(|cx| build_editor(buffer.clone(), cx)),
7374 )
7375 });
7376
7377 let is_still_following = Rc::new(RefCell::new(true));
7378 let follower_edit_event_count = Rc::new(RefCell::new(0));
7379 let pending_update = Rc::new(RefCell::new(None));
7380 _ = follower.update(cx, {
7381 let update = pending_update.clone();
7382 let is_still_following = is_still_following.clone();
7383 let follower_edit_event_count = follower_edit_event_count.clone();
7384 |_, cx| {
7385 cx.subscribe(
7386 &leader.root_view(cx).unwrap(),
7387 move |_, leader, event, cx| {
7388 leader
7389 .read(cx)
7390 .add_event_to_update_proto(event, &mut update.borrow_mut(), cx);
7391 },
7392 )
7393 .detach();
7394
7395 cx.subscribe(
7396 &follower.root_view(cx).unwrap(),
7397 move |_, _, event: &EditorEvent, _cx| {
7398 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
7399 *is_still_following.borrow_mut() = false;
7400 }
7401
7402 if let EditorEvent::BufferEdited = event {
7403 *follower_edit_event_count.borrow_mut() += 1;
7404 }
7405 },
7406 )
7407 .detach();
7408 }
7409 });
7410
7411 // Update the selections only
7412 _ = leader.update(cx, |leader, cx| {
7413 leader.change_selections(None, cx, |s| s.select_ranges([1..1]));
7414 });
7415 follower
7416 .update(cx, |follower, cx| {
7417 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
7418 })
7419 .unwrap()
7420 .await
7421 .unwrap();
7422 _ = follower.update(cx, |follower, cx| {
7423 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
7424 });
7425 assert_eq!(*is_still_following.borrow(), true);
7426 assert_eq!(*follower_edit_event_count.borrow(), 0);
7427
7428 // Update the scroll position only
7429 _ = leader.update(cx, |leader, cx| {
7430 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), cx);
7431 });
7432 follower
7433 .update(cx, |follower, cx| {
7434 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
7435 })
7436 .unwrap()
7437 .await
7438 .unwrap();
7439 assert_eq!(
7440 follower
7441 .update(cx, |follower, cx| follower.scroll_position(cx))
7442 .unwrap(),
7443 gpui::Point::new(1.5, 3.5)
7444 );
7445 assert_eq!(*is_still_following.borrow(), true);
7446 assert_eq!(*follower_edit_event_count.borrow(), 0);
7447
7448 // Update the selections and scroll position. The follower's scroll position is updated
7449 // via autoscroll, not via the leader's exact scroll position.
7450 _ = leader.update(cx, |leader, cx| {
7451 leader.change_selections(None, cx, |s| s.select_ranges([0..0]));
7452 leader.request_autoscroll(Autoscroll::newest(), cx);
7453 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), cx);
7454 });
7455 follower
7456 .update(cx, |follower, cx| {
7457 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
7458 })
7459 .unwrap()
7460 .await
7461 .unwrap();
7462 _ = follower.update(cx, |follower, cx| {
7463 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
7464 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
7465 });
7466 assert_eq!(*is_still_following.borrow(), true);
7467
7468 // Creating a pending selection that precedes another selection
7469 _ = leader.update(cx, |leader, cx| {
7470 leader.change_selections(None, cx, |s| s.select_ranges([1..1]));
7471 leader.begin_selection(DisplayPoint::new(0, 0), true, 1, cx);
7472 });
7473 follower
7474 .update(cx, |follower, cx| {
7475 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
7476 })
7477 .unwrap()
7478 .await
7479 .unwrap();
7480 _ = follower.update(cx, |follower, cx| {
7481 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
7482 });
7483 assert_eq!(*is_still_following.borrow(), true);
7484
7485 // Extend the pending selection so that it surrounds another selection
7486 _ = leader.update(cx, |leader, cx| {
7487 leader.extend_selection(DisplayPoint::new(0, 2), 1, cx);
7488 });
7489 follower
7490 .update(cx, |follower, cx| {
7491 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
7492 })
7493 .unwrap()
7494 .await
7495 .unwrap();
7496 _ = follower.update(cx, |follower, cx| {
7497 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
7498 });
7499
7500 // Scrolling locally breaks the follow
7501 _ = follower.update(cx, |follower, cx| {
7502 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
7503 follower.set_scroll_anchor(
7504 ScrollAnchor {
7505 anchor: top_anchor,
7506 offset: gpui::Point::new(0.0, 0.5),
7507 },
7508 cx,
7509 );
7510 });
7511 assert_eq!(*is_still_following.borrow(), false);
7512}
7513
7514#[gpui::test]
7515async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
7516 init_test(cx, |_| {});
7517
7518 let fs = FakeFs::new(cx.executor());
7519 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
7520 let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
7521 let pane = workspace
7522 .update(cx, |workspace, _| workspace.active_pane().clone())
7523 .unwrap();
7524
7525 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7526
7527 let leader = pane.update(cx, |_, cx| {
7528 let multibuffer = cx.new_model(|_| MultiBuffer::new(0, ReadWrite));
7529 cx.new_view(|cx| build_editor(multibuffer.clone(), cx))
7530 });
7531
7532 // Start following the editor when it has no excerpts.
7533 let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
7534 let follower_1 = cx
7535 .update_window(*workspace.deref(), |_, cx| {
7536 Editor::from_state_proto(
7537 pane.clone(),
7538 workspace.root_view(cx).unwrap(),
7539 ViewId {
7540 creator: Default::default(),
7541 id: 0,
7542 },
7543 &mut state_message,
7544 cx,
7545 )
7546 })
7547 .unwrap()
7548 .unwrap()
7549 .await
7550 .unwrap();
7551
7552 let update_message = Rc::new(RefCell::new(None));
7553 follower_1.update(cx, {
7554 let update = update_message.clone();
7555 |_, cx| {
7556 cx.subscribe(&leader, move |_, leader, event, cx| {
7557 leader
7558 .read(cx)
7559 .add_event_to_update_proto(event, &mut update.borrow_mut(), cx);
7560 })
7561 .detach();
7562 }
7563 });
7564
7565 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
7566 (
7567 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
7568 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
7569 )
7570 });
7571
7572 // Insert some excerpts.
7573 _ = leader.update(cx, |leader, cx| {
7574 leader.buffer.update(cx, |multibuffer, cx| {
7575 let excerpt_ids = multibuffer.push_excerpts(
7576 buffer_1.clone(),
7577 [
7578 ExcerptRange {
7579 context: 1..6,
7580 primary: None,
7581 },
7582 ExcerptRange {
7583 context: 12..15,
7584 primary: None,
7585 },
7586 ExcerptRange {
7587 context: 0..3,
7588 primary: None,
7589 },
7590 ],
7591 cx,
7592 );
7593 multibuffer.insert_excerpts_after(
7594 excerpt_ids[0],
7595 buffer_2.clone(),
7596 [
7597 ExcerptRange {
7598 context: 8..12,
7599 primary: None,
7600 },
7601 ExcerptRange {
7602 context: 0..6,
7603 primary: None,
7604 },
7605 ],
7606 cx,
7607 );
7608 });
7609 });
7610
7611 // Apply the update of adding the excerpts.
7612 follower_1
7613 .update(cx, |follower, cx| {
7614 follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
7615 })
7616 .await
7617 .unwrap();
7618 assert_eq!(
7619 follower_1.update(cx, |editor, cx| editor.text(cx)),
7620 leader.update(cx, |editor, cx| editor.text(cx))
7621 );
7622 update_message.borrow_mut().take();
7623
7624 // Start following separately after it already has excerpts.
7625 let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
7626 let follower_2 = cx
7627 .update_window(*workspace.deref(), |_, cx| {
7628 Editor::from_state_proto(
7629 pane.clone(),
7630 workspace.root_view(cx).unwrap().clone(),
7631 ViewId {
7632 creator: Default::default(),
7633 id: 0,
7634 },
7635 &mut state_message,
7636 cx,
7637 )
7638 })
7639 .unwrap()
7640 .unwrap()
7641 .await
7642 .unwrap();
7643 assert_eq!(
7644 follower_2.update(cx, |editor, cx| editor.text(cx)),
7645 leader.update(cx, |editor, cx| editor.text(cx))
7646 );
7647
7648 // Remove some excerpts.
7649 _ = leader.update(cx, |leader, cx| {
7650 leader.buffer.update(cx, |multibuffer, cx| {
7651 let excerpt_ids = multibuffer.excerpt_ids();
7652 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
7653 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
7654 });
7655 });
7656
7657 // Apply the update of removing the excerpts.
7658 follower_1
7659 .update(cx, |follower, cx| {
7660 follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
7661 })
7662 .await
7663 .unwrap();
7664 follower_2
7665 .update(cx, |follower, cx| {
7666 follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
7667 })
7668 .await
7669 .unwrap();
7670 update_message.borrow_mut().take();
7671 assert_eq!(
7672 follower_1.update(cx, |editor, cx| editor.text(cx)),
7673 leader.update(cx, |editor, cx| editor.text(cx))
7674 );
7675}
7676
7677#[gpui::test]
7678async fn go_to_prev_overlapping_diagnostic(
7679 executor: BackgroundExecutor,
7680 cx: &mut gpui::TestAppContext,
7681) {
7682 init_test(cx, |_| {});
7683
7684 let mut cx = EditorTestContext::new(cx).await;
7685 let project = cx.update_editor(|editor, _| editor.project.clone().unwrap());
7686
7687 cx.set_state(indoc! {"
7688 ˇfn func(abc def: i32) -> u32 {
7689 }
7690 "});
7691
7692 _ = cx.update(|cx| {
7693 _ = project.update(cx, |project, cx| {
7694 project
7695 .update_diagnostics(
7696 LanguageServerId(0),
7697 lsp::PublishDiagnosticsParams {
7698 uri: lsp::Url::from_file_path("/root/file").unwrap(),
7699 version: None,
7700 diagnostics: vec![
7701 lsp::Diagnostic {
7702 range: lsp::Range::new(
7703 lsp::Position::new(0, 11),
7704 lsp::Position::new(0, 12),
7705 ),
7706 severity: Some(lsp::DiagnosticSeverity::ERROR),
7707 ..Default::default()
7708 },
7709 lsp::Diagnostic {
7710 range: lsp::Range::new(
7711 lsp::Position::new(0, 12),
7712 lsp::Position::new(0, 15),
7713 ),
7714 severity: Some(lsp::DiagnosticSeverity::ERROR),
7715 ..Default::default()
7716 },
7717 lsp::Diagnostic {
7718 range: lsp::Range::new(
7719 lsp::Position::new(0, 25),
7720 lsp::Position::new(0, 28),
7721 ),
7722 severity: Some(lsp::DiagnosticSeverity::ERROR),
7723 ..Default::default()
7724 },
7725 ],
7726 },
7727 &[],
7728 cx,
7729 )
7730 .unwrap()
7731 });
7732 });
7733
7734 executor.run_until_parked();
7735
7736 cx.update_editor(|editor, cx| {
7737 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
7738 });
7739
7740 cx.assert_editor_state(indoc! {"
7741 fn func(abc def: i32) -> ˇu32 {
7742 }
7743 "});
7744
7745 cx.update_editor(|editor, cx| {
7746 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
7747 });
7748
7749 cx.assert_editor_state(indoc! {"
7750 fn func(abc ˇdef: i32) -> u32 {
7751 }
7752 "});
7753
7754 cx.update_editor(|editor, cx| {
7755 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
7756 });
7757
7758 cx.assert_editor_state(indoc! {"
7759 fn func(abcˇ def: i32) -> u32 {
7760 }
7761 "});
7762
7763 cx.update_editor(|editor, cx| {
7764 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
7765 });
7766
7767 cx.assert_editor_state(indoc! {"
7768 fn func(abc def: i32) -> ˇu32 {
7769 }
7770 "});
7771}
7772
7773#[gpui::test]
7774async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
7775 init_test(cx, |_| {});
7776
7777 let mut cx = EditorTestContext::new(cx).await;
7778
7779 let diff_base = r#"
7780 use some::mod;
7781
7782 const A: u32 = 42;
7783
7784 fn main() {
7785 println!("hello");
7786
7787 println!("world");
7788 }
7789 "#
7790 .unindent();
7791
7792 // Edits are modified, removed, modified, added
7793 cx.set_state(
7794 &r#"
7795 use some::modified;
7796
7797 ˇ
7798 fn main() {
7799 println!("hello there");
7800
7801 println!("around the");
7802 println!("world");
7803 }
7804 "#
7805 .unindent(),
7806 );
7807
7808 cx.set_diff_base(Some(&diff_base));
7809 executor.run_until_parked();
7810
7811 cx.update_editor(|editor, cx| {
7812 //Wrap around the bottom of the buffer
7813 for _ in 0..3 {
7814 editor.go_to_hunk(&GoToHunk, cx);
7815 }
7816 });
7817
7818 cx.assert_editor_state(
7819 &r#"
7820 ˇuse some::modified;
7821
7822
7823 fn main() {
7824 println!("hello there");
7825
7826 println!("around the");
7827 println!("world");
7828 }
7829 "#
7830 .unindent(),
7831 );
7832
7833 cx.update_editor(|editor, cx| {
7834 //Wrap around the top of the buffer
7835 for _ in 0..2 {
7836 editor.go_to_prev_hunk(&GoToPrevHunk, cx);
7837 }
7838 });
7839
7840 cx.assert_editor_state(
7841 &r#"
7842 use some::modified;
7843
7844
7845 fn main() {
7846 ˇ println!("hello there");
7847
7848 println!("around the");
7849 println!("world");
7850 }
7851 "#
7852 .unindent(),
7853 );
7854
7855 cx.update_editor(|editor, cx| {
7856 editor.go_to_prev_hunk(&GoToPrevHunk, cx);
7857 });
7858
7859 cx.assert_editor_state(
7860 &r#"
7861 use some::modified;
7862
7863 ˇ
7864 fn main() {
7865 println!("hello there");
7866
7867 println!("around the");
7868 println!("world");
7869 }
7870 "#
7871 .unindent(),
7872 );
7873
7874 cx.update_editor(|editor, cx| {
7875 for _ in 0..3 {
7876 editor.go_to_prev_hunk(&GoToPrevHunk, cx);
7877 }
7878 });
7879
7880 cx.assert_editor_state(
7881 &r#"
7882 use some::modified;
7883
7884
7885 fn main() {
7886 ˇ println!("hello there");
7887
7888 println!("around the");
7889 println!("world");
7890 }
7891 "#
7892 .unindent(),
7893 );
7894
7895 cx.update_editor(|editor, cx| {
7896 editor.fold(&Fold, cx);
7897
7898 //Make sure that the fold only gets one hunk
7899 for _ in 0..4 {
7900 editor.go_to_hunk(&GoToHunk, cx);
7901 }
7902 });
7903
7904 cx.assert_editor_state(
7905 &r#"
7906 ˇuse some::modified;
7907
7908
7909 fn main() {
7910 println!("hello there");
7911
7912 println!("around the");
7913 println!("world");
7914 }
7915 "#
7916 .unindent(),
7917 );
7918}
7919
7920#[test]
7921fn test_split_words() {
7922 fn split(text: &str) -> Vec<&str> {
7923 split_words(text).collect()
7924 }
7925
7926 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
7927 assert_eq!(split("hello_world"), &["hello_", "world"]);
7928 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
7929 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
7930 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
7931 assert_eq!(split("helloworld"), &["helloworld"]);
7932
7933 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
7934}
7935
7936#[gpui::test]
7937async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
7938 init_test(cx, |_| {});
7939
7940 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
7941 let mut assert = |before, after| {
7942 let _state_context = cx.set_state(before);
7943 cx.update_editor(|editor, cx| {
7944 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, cx)
7945 });
7946 cx.assert_editor_state(after);
7947 };
7948
7949 // Outside bracket jumps to outside of matching bracket
7950 assert("console.logˇ(var);", "console.log(var)ˇ;");
7951 assert("console.log(var)ˇ;", "console.logˇ(var);");
7952
7953 // Inside bracket jumps to inside of matching bracket
7954 assert("console.log(ˇvar);", "console.log(varˇ);");
7955 assert("console.log(varˇ);", "console.log(ˇvar);");
7956
7957 // When outside a bracket and inside, favor jumping to the inside bracket
7958 assert(
7959 "console.log('foo', [1, 2, 3]ˇ);",
7960 "console.log(ˇ'foo', [1, 2, 3]);",
7961 );
7962 assert(
7963 "console.log(ˇ'foo', [1, 2, 3]);",
7964 "console.log('foo', [1, 2, 3]ˇ);",
7965 );
7966
7967 // Bias forward if two options are equally likely
7968 assert(
7969 "let result = curried_fun()ˇ();",
7970 "let result = curried_fun()()ˇ;",
7971 );
7972
7973 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
7974 assert(
7975 indoc! {"
7976 function test() {
7977 console.log('test')ˇ
7978 }"},
7979 indoc! {"
7980 function test() {
7981 console.logˇ('test')
7982 }"},
7983 );
7984}
7985
7986#[gpui::test]
7987async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
7988 init_test(cx, |_| {});
7989
7990 let fs = FakeFs::new(cx.executor());
7991 fs.insert_tree(
7992 "/a",
7993 json!({
7994 "main.rs": "fn main() { let a = 5; }",
7995 "other.rs": "// Test file",
7996 }),
7997 )
7998 .await;
7999 let project = Project::test(fs, ["/a".as_ref()], cx).await;
8000
8001 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8002 language_registry.add(Arc::new(Language::new(
8003 LanguageConfig {
8004 name: "Rust".into(),
8005 matcher: LanguageMatcher {
8006 path_suffixes: vec!["rs".to_string()],
8007 ..Default::default()
8008 },
8009 brackets: BracketPairConfig {
8010 pairs: vec![BracketPair {
8011 start: "{".to_string(),
8012 end: "}".to_string(),
8013 close: true,
8014 newline: true,
8015 }],
8016 disabled_scopes_by_bracket_ix: Vec::new(),
8017 },
8018 ..Default::default()
8019 },
8020 Some(tree_sitter_rust::language()),
8021 )));
8022 let mut fake_servers = language_registry.register_fake_lsp_adapter(
8023 "Rust",
8024 FakeLspAdapter {
8025 capabilities: lsp::ServerCapabilities {
8026 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
8027 first_trigger_character: "{".to_string(),
8028 more_trigger_character: None,
8029 }),
8030 ..Default::default()
8031 },
8032 ..Default::default()
8033 },
8034 );
8035
8036 let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
8037
8038 let cx = &mut VisualTestContext::from_window(*workspace, cx);
8039
8040 let worktree_id = workspace
8041 .update(cx, |workspace, cx| {
8042 workspace.project().update(cx, |project, cx| {
8043 project.worktrees().next().unwrap().read(cx).id()
8044 })
8045 })
8046 .unwrap();
8047
8048 let buffer = project
8049 .update(cx, |project, cx| {
8050 project.open_local_buffer("/a/main.rs", cx)
8051 })
8052 .await
8053 .unwrap();
8054 cx.executor().run_until_parked();
8055 cx.executor().start_waiting();
8056 let fake_server = fake_servers.next().await.unwrap();
8057 let editor_handle = workspace
8058 .update(cx, |workspace, cx| {
8059 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
8060 })
8061 .unwrap()
8062 .await
8063 .unwrap()
8064 .downcast::<Editor>()
8065 .unwrap();
8066
8067 fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
8068 assert_eq!(
8069 params.text_document_position.text_document.uri,
8070 lsp::Url::from_file_path("/a/main.rs").unwrap(),
8071 );
8072 assert_eq!(
8073 params.text_document_position.position,
8074 lsp::Position::new(0, 21),
8075 );
8076
8077 Ok(Some(vec![lsp::TextEdit {
8078 new_text: "]".to_string(),
8079 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
8080 }]))
8081 });
8082
8083 editor_handle.update(cx, |editor, cx| {
8084 editor.focus(cx);
8085 editor.change_selections(None, cx, |s| {
8086 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
8087 });
8088 editor.handle_input("{", cx);
8089 });
8090
8091 cx.executor().run_until_parked();
8092
8093 _ = buffer.update(cx, |buffer, _| {
8094 assert_eq!(
8095 buffer.text(),
8096 "fn main() { let a = {5}; }",
8097 "No extra braces from on type formatting should appear in the buffer"
8098 )
8099 });
8100}
8101
8102#[gpui::test]
8103async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) {
8104 init_test(cx, |_| {});
8105
8106 let fs = FakeFs::new(cx.executor());
8107 fs.insert_tree(
8108 "/a",
8109 json!({
8110 "main.rs": "fn main() { let a = 5; }",
8111 "other.rs": "// Test file",
8112 }),
8113 )
8114 .await;
8115
8116 let project = Project::test(fs, ["/a".as_ref()], cx).await;
8117
8118 let server_restarts = Arc::new(AtomicUsize::new(0));
8119 let closure_restarts = Arc::clone(&server_restarts);
8120 let language_server_name = "test language server";
8121 let language_name: Arc<str> = "Rust".into();
8122
8123 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8124 language_registry.add(Arc::new(Language::new(
8125 LanguageConfig {
8126 name: Arc::clone(&language_name),
8127 matcher: LanguageMatcher {
8128 path_suffixes: vec!["rs".to_string()],
8129 ..Default::default()
8130 },
8131 ..Default::default()
8132 },
8133 Some(tree_sitter_rust::language()),
8134 )));
8135 let mut fake_servers = language_registry.register_fake_lsp_adapter(
8136 "Rust",
8137 FakeLspAdapter {
8138 name: language_server_name,
8139 initialization_options: Some(json!({
8140 "testOptionValue": true
8141 })),
8142 initializer: Some(Box::new(move |fake_server| {
8143 let task_restarts = Arc::clone(&closure_restarts);
8144 fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
8145 task_restarts.fetch_add(1, atomic::Ordering::Release);
8146 futures::future::ready(Ok(()))
8147 });
8148 })),
8149 ..Default::default()
8150 },
8151 );
8152
8153 let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
8154 let _buffer = project
8155 .update(cx, |project, cx| {
8156 project.open_local_buffer("/a/main.rs", cx)
8157 })
8158 .await
8159 .unwrap();
8160 let _fake_server = fake_servers.next().await.unwrap();
8161 update_test_language_settings(cx, |language_settings| {
8162 language_settings.languages.insert(
8163 Arc::clone(&language_name),
8164 LanguageSettingsContent {
8165 tab_size: NonZeroU32::new(8),
8166 ..Default::default()
8167 },
8168 );
8169 });
8170 cx.executor().run_until_parked();
8171 assert_eq!(
8172 server_restarts.load(atomic::Ordering::Acquire),
8173 0,
8174 "Should not restart LSP server on an unrelated change"
8175 );
8176
8177 update_test_project_settings(cx, |project_settings| {
8178 project_settings.lsp.insert(
8179 "Some other server name".into(),
8180 LspSettings {
8181 binary: None,
8182 settings: None,
8183 initialization_options: Some(json!({
8184 "some other init value": false
8185 })),
8186 },
8187 );
8188 });
8189 cx.executor().run_until_parked();
8190 assert_eq!(
8191 server_restarts.load(atomic::Ordering::Acquire),
8192 0,
8193 "Should not restart LSP server on an unrelated LSP settings change"
8194 );
8195
8196 update_test_project_settings(cx, |project_settings| {
8197 project_settings.lsp.insert(
8198 language_server_name.into(),
8199 LspSettings {
8200 binary: None,
8201 settings: None,
8202 initialization_options: Some(json!({
8203 "anotherInitValue": false
8204 })),
8205 },
8206 );
8207 });
8208 cx.executor().run_until_parked();
8209 assert_eq!(
8210 server_restarts.load(atomic::Ordering::Acquire),
8211 1,
8212 "Should restart LSP server on a related LSP settings change"
8213 );
8214
8215 update_test_project_settings(cx, |project_settings| {
8216 project_settings.lsp.insert(
8217 language_server_name.into(),
8218 LspSettings {
8219 binary: None,
8220 settings: None,
8221 initialization_options: Some(json!({
8222 "anotherInitValue": false
8223 })),
8224 },
8225 );
8226 });
8227 cx.executor().run_until_parked();
8228 assert_eq!(
8229 server_restarts.load(atomic::Ordering::Acquire),
8230 1,
8231 "Should not restart LSP server on a related LSP settings change that is the same"
8232 );
8233
8234 update_test_project_settings(cx, |project_settings| {
8235 project_settings.lsp.insert(
8236 language_server_name.into(),
8237 LspSettings {
8238 binary: None,
8239 settings: None,
8240 initialization_options: None,
8241 },
8242 );
8243 });
8244 cx.executor().run_until_parked();
8245 assert_eq!(
8246 server_restarts.load(atomic::Ordering::Acquire),
8247 2,
8248 "Should restart LSP server on another related LSP settings change"
8249 );
8250}
8251
8252#[gpui::test]
8253async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
8254 init_test(cx, |_| {});
8255
8256 let mut cx = EditorLspTestContext::new_rust(
8257 lsp::ServerCapabilities {
8258 completion_provider: Some(lsp::CompletionOptions {
8259 trigger_characters: Some(vec![".".to_string()]),
8260 resolve_provider: Some(true),
8261 ..Default::default()
8262 }),
8263 ..Default::default()
8264 },
8265 cx,
8266 )
8267 .await;
8268
8269 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
8270 cx.simulate_keystroke(".");
8271 let completion_item = lsp::CompletionItem {
8272 label: "some".into(),
8273 kind: Some(lsp::CompletionItemKind::SNIPPET),
8274 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
8275 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
8276 kind: lsp::MarkupKind::Markdown,
8277 value: "```rust\nSome(2)\n```".to_string(),
8278 })),
8279 deprecated: Some(false),
8280 sort_text: Some("fffffff2".to_string()),
8281 filter_text: Some("some".to_string()),
8282 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
8283 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8284 range: lsp::Range {
8285 start: lsp::Position {
8286 line: 0,
8287 character: 22,
8288 },
8289 end: lsp::Position {
8290 line: 0,
8291 character: 22,
8292 },
8293 },
8294 new_text: "Some(2)".to_string(),
8295 })),
8296 additional_text_edits: Some(vec![lsp::TextEdit {
8297 range: lsp::Range {
8298 start: lsp::Position {
8299 line: 0,
8300 character: 20,
8301 },
8302 end: lsp::Position {
8303 line: 0,
8304 character: 22,
8305 },
8306 },
8307 new_text: "".to_string(),
8308 }]),
8309 ..Default::default()
8310 };
8311
8312 let closure_completion_item = completion_item.clone();
8313 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
8314 let task_completion_item = closure_completion_item.clone();
8315 async move {
8316 Ok(Some(lsp::CompletionResponse::Array(vec![
8317 task_completion_item,
8318 ])))
8319 }
8320 });
8321
8322 request.next().await;
8323
8324 cx.condition(|editor, _| editor.context_menu_visible())
8325 .await;
8326 let apply_additional_edits = cx.update_editor(|editor, cx| {
8327 editor
8328 .confirm_completion(&ConfirmCompletion::default(), cx)
8329 .unwrap()
8330 });
8331 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
8332
8333 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
8334 let task_completion_item = completion_item.clone();
8335 async move { Ok(task_completion_item) }
8336 })
8337 .next()
8338 .await
8339 .unwrap();
8340 apply_additional_edits.await.unwrap();
8341 cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
8342}
8343
8344#[gpui::test]
8345async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
8346 init_test(cx, |_| {});
8347
8348 let mut cx = EditorLspTestContext::new(
8349 Language::new(
8350 LanguageConfig {
8351 matcher: LanguageMatcher {
8352 path_suffixes: vec!["jsx".into()],
8353 ..Default::default()
8354 },
8355 overrides: [(
8356 "element".into(),
8357 LanguageConfigOverride {
8358 word_characters: Override::Set(['-'].into_iter().collect()),
8359 ..Default::default()
8360 },
8361 )]
8362 .into_iter()
8363 .collect(),
8364 ..Default::default()
8365 },
8366 Some(tree_sitter_typescript::language_tsx()),
8367 )
8368 .with_override_query("(jsx_self_closing_element) @element")
8369 .unwrap(),
8370 lsp::ServerCapabilities {
8371 completion_provider: Some(lsp::CompletionOptions {
8372 trigger_characters: Some(vec![":".to_string()]),
8373 ..Default::default()
8374 }),
8375 ..Default::default()
8376 },
8377 cx,
8378 )
8379 .await;
8380
8381 cx.lsp
8382 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
8383 Ok(Some(lsp::CompletionResponse::Array(vec![
8384 lsp::CompletionItem {
8385 label: "bg-blue".into(),
8386 ..Default::default()
8387 },
8388 lsp::CompletionItem {
8389 label: "bg-red".into(),
8390 ..Default::default()
8391 },
8392 lsp::CompletionItem {
8393 label: "bg-yellow".into(),
8394 ..Default::default()
8395 },
8396 ])))
8397 });
8398
8399 cx.set_state(r#"<p class="bgˇ" />"#);
8400
8401 // Trigger completion when typing a dash, because the dash is an extra
8402 // word character in the 'element' scope, which contains the cursor.
8403 cx.simulate_keystroke("-");
8404 cx.executor().run_until_parked();
8405 cx.update_editor(|editor, _| {
8406 if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
8407 assert_eq!(
8408 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
8409 &["bg-red", "bg-blue", "bg-yellow"]
8410 );
8411 } else {
8412 panic!("expected completion menu to be open");
8413 }
8414 });
8415
8416 cx.simulate_keystroke("l");
8417 cx.executor().run_until_parked();
8418 cx.update_editor(|editor, _| {
8419 if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
8420 assert_eq!(
8421 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
8422 &["bg-blue", "bg-yellow"]
8423 );
8424 } else {
8425 panic!("expected completion menu to be open");
8426 }
8427 });
8428
8429 // When filtering completions, consider the character after the '-' to
8430 // be the start of a subword.
8431 cx.set_state(r#"<p class="yelˇ" />"#);
8432 cx.simulate_keystroke("l");
8433 cx.executor().run_until_parked();
8434 cx.update_editor(|editor, _| {
8435 if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
8436 assert_eq!(
8437 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
8438 &["bg-yellow"]
8439 );
8440 } else {
8441 panic!("expected completion menu to be open");
8442 }
8443 });
8444}
8445
8446#[gpui::test]
8447async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
8448 init_test(cx, |settings| {
8449 settings.defaults.formatter = Some(language_settings::Formatter::Prettier)
8450 });
8451
8452 let fs = FakeFs::new(cx.executor());
8453 fs.insert_file("/file.rs", Default::default()).await;
8454
8455 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
8456 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8457
8458 language_registry.add(Arc::new(Language::new(
8459 LanguageConfig {
8460 name: "Rust".into(),
8461 matcher: LanguageMatcher {
8462 path_suffixes: vec!["rs".to_string()],
8463 ..Default::default()
8464 },
8465 prettier_parser_name: Some("test_parser".to_string()),
8466 ..Default::default()
8467 },
8468 Some(tree_sitter_rust::language()),
8469 )));
8470
8471 let test_plugin = "test_plugin";
8472 let _ = language_registry.register_fake_lsp_adapter(
8473 "Rust",
8474 FakeLspAdapter {
8475 prettier_plugins: vec![test_plugin],
8476 ..Default::default()
8477 },
8478 );
8479
8480 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
8481 let buffer = project
8482 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
8483 .await
8484 .unwrap();
8485
8486 let buffer_text = "one\ntwo\nthree\n";
8487 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
8488 let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
8489 _ = editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx));
8490
8491 editor
8492 .update(cx, |editor, cx| {
8493 editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
8494 })
8495 .unwrap()
8496 .await;
8497 assert_eq!(
8498 editor.update(cx, |editor, cx| editor.text(cx)),
8499 buffer_text.to_string() + prettier_format_suffix,
8500 "Test prettier formatting was not applied to the original buffer text",
8501 );
8502
8503 update_test_language_settings(cx, |settings| {
8504 settings.defaults.formatter = Some(language_settings::Formatter::Auto)
8505 });
8506 let format = editor.update(cx, |editor, cx| {
8507 editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
8508 });
8509 format.await.unwrap();
8510 assert_eq!(
8511 editor.update(cx, |editor, cx| editor.text(cx)),
8512 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
8513 "Autoformatting (via test prettier) was not applied to the original buffer text",
8514 );
8515}
8516
8517#[gpui::test]
8518async fn test_addition_reverts(cx: &mut gpui::TestAppContext) {
8519 init_test(cx, |_| {});
8520 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
8521 let base_text = indoc! {r#"struct Row;
8522struct Row1;
8523struct Row2;
8524
8525struct Row4;
8526struct Row5;
8527struct Row6;
8528
8529struct Row8;
8530struct Row9;
8531struct Row10;"#};
8532
8533 // When addition hunks are not adjacent to carets, no hunk revert is performed
8534 assert_hunk_revert(
8535 indoc! {r#"struct Row;
8536 struct Row1;
8537 struct Row1.1;
8538 struct Row1.2;
8539 struct Row2;ˇ
8540
8541 struct Row4;
8542 struct Row5;
8543 struct Row6;
8544
8545 struct Row8;
8546 ˇstruct Row9;
8547 struct Row9.1;
8548 struct Row9.2;
8549 struct Row9.3;
8550 struct Row10;"#},
8551 vec![DiffHunkStatus::Added, DiffHunkStatus::Added],
8552 indoc! {r#"struct Row;
8553 struct Row1;
8554 struct Row1.1;
8555 struct Row1.2;
8556 struct Row2;ˇ
8557
8558 struct Row4;
8559 struct Row5;
8560 struct Row6;
8561
8562 struct Row8;
8563 ˇstruct Row9;
8564 struct Row9.1;
8565 struct Row9.2;
8566 struct Row9.3;
8567 struct Row10;"#},
8568 base_text,
8569 &mut cx,
8570 );
8571 // Same for selections
8572 assert_hunk_revert(
8573 indoc! {r#"struct Row;
8574 struct Row1;
8575 struct Row2;
8576 struct Row2.1;
8577 struct Row2.2;
8578 «ˇ
8579 struct Row4;
8580 struct» Row5;
8581 «struct Row6;
8582 ˇ»
8583 struct Row9.1;
8584 struct Row9.2;
8585 struct Row9.3;
8586 struct Row8;
8587 struct Row9;
8588 struct Row10;"#},
8589 vec![DiffHunkStatus::Added, DiffHunkStatus::Added],
8590 indoc! {r#"struct Row;
8591 struct Row1;
8592 struct Row2;
8593 struct Row2.1;
8594 struct Row2.2;
8595 «ˇ
8596 struct Row4;
8597 struct» Row5;
8598 «struct Row6;
8599 ˇ»
8600 struct Row9.1;
8601 struct Row9.2;
8602 struct Row9.3;
8603 struct Row8;
8604 struct Row9;
8605 struct Row10;"#},
8606 base_text,
8607 &mut cx,
8608 );
8609
8610 // When carets and selections intersect the addition hunks, those are reverted.
8611 // Adjacent carets got merged.
8612 assert_hunk_revert(
8613 indoc! {r#"struct Row;
8614 ˇ// something on the top
8615 struct Row1;
8616 struct Row2;
8617 struct Roˇw3.1;
8618 struct Row2.2;
8619 struct Row2.3;ˇ
8620
8621 struct Row4;
8622 struct ˇRow5.1;
8623 struct Row5.2;
8624 struct «Rowˇ»5.3;
8625 struct Row5;
8626 struct Row6;
8627 ˇ
8628 struct Row9.1;
8629 struct «Rowˇ»9.2;
8630 struct «ˇRow»9.3;
8631 struct Row8;
8632 struct Row9;
8633 «ˇ// something on bottom»
8634 struct Row10;"#},
8635 vec![
8636 DiffHunkStatus::Added,
8637 DiffHunkStatus::Added,
8638 DiffHunkStatus::Added,
8639 DiffHunkStatus::Added,
8640 DiffHunkStatus::Added,
8641 ],
8642 indoc! {r#"struct Row;
8643 ˇstruct Row1;
8644 struct Row2;
8645 ˇ
8646 struct Row4;
8647 ˇstruct Row5;
8648 struct Row6;
8649 ˇ
8650 ˇstruct Row8;
8651 struct Row9;
8652 ˇstruct Row10;"#},
8653 base_text,
8654 &mut cx,
8655 );
8656}
8657
8658#[gpui::test]
8659async fn test_modification_reverts(cx: &mut gpui::TestAppContext) {
8660 init_test(cx, |_| {});
8661 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
8662 let base_text = indoc! {r#"struct Row;
8663struct Row1;
8664struct Row2;
8665
8666struct Row4;
8667struct Row5;
8668struct Row6;
8669
8670struct Row8;
8671struct Row9;
8672struct Row10;"#};
8673
8674 // Modification hunks behave the same as the addition ones.
8675 assert_hunk_revert(
8676 indoc! {r#"struct Row;
8677 struct Row1;
8678 struct Row33;
8679 ˇ
8680 struct Row4;
8681 struct Row5;
8682 struct Row6;
8683 ˇ
8684 struct Row99;
8685 struct Row9;
8686 struct Row10;"#},
8687 vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified],
8688 indoc! {r#"struct Row;
8689 struct Row1;
8690 struct Row33;
8691 ˇ
8692 struct Row4;
8693 struct Row5;
8694 struct Row6;
8695 ˇ
8696 struct Row99;
8697 struct Row9;
8698 struct Row10;"#},
8699 base_text,
8700 &mut cx,
8701 );
8702 assert_hunk_revert(
8703 indoc! {r#"struct Row;
8704 struct Row1;
8705 struct Row33;
8706 «ˇ
8707 struct Row4;
8708 struct» Row5;
8709 «struct Row6;
8710 ˇ»
8711 struct Row99;
8712 struct Row9;
8713 struct Row10;"#},
8714 vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified],
8715 indoc! {r#"struct Row;
8716 struct Row1;
8717 struct Row33;
8718 «ˇ
8719 struct Row4;
8720 struct» Row5;
8721 «struct Row6;
8722 ˇ»
8723 struct Row99;
8724 struct Row9;
8725 struct Row10;"#},
8726 base_text,
8727 &mut cx,
8728 );
8729
8730 assert_hunk_revert(
8731 indoc! {r#"ˇstruct Row1.1;
8732 struct Row1;
8733 «ˇstr»uct Row22;
8734
8735 struct ˇRow44;
8736 struct Row5;
8737 struct «Rˇ»ow66;ˇ
8738
8739 «struˇ»ct Row88;
8740 struct Row9;
8741 struct Row1011;ˇ"#},
8742 vec![
8743 DiffHunkStatus::Modified,
8744 DiffHunkStatus::Modified,
8745 DiffHunkStatus::Modified,
8746 DiffHunkStatus::Modified,
8747 DiffHunkStatus::Modified,
8748 DiffHunkStatus::Modified,
8749 ],
8750 indoc! {r#"struct Row;
8751 ˇstruct Row1;
8752 struct Row2;
8753 ˇ
8754 struct Row4;
8755 ˇstruct Row5;
8756 struct Row6;
8757 ˇ
8758 struct Row8;
8759 ˇstruct Row9;
8760 struct Row10;ˇ"#},
8761 base_text,
8762 &mut cx,
8763 );
8764}
8765
8766#[gpui::test]
8767async fn test_deletion_reverts(cx: &mut gpui::TestAppContext) {
8768 init_test(cx, |_| {});
8769 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
8770 let base_text = indoc! {r#"struct Row;
8771struct Row1;
8772struct Row2;
8773
8774struct Row4;
8775struct Row5;
8776struct Row6;
8777
8778struct Row8;
8779struct Row9;
8780struct Row10;"#};
8781
8782 // Deletion hunks trigger with carets on ajacent rows, so carets and selections have to stay farther to avoid the revert
8783 assert_hunk_revert(
8784 indoc! {r#"struct Row;
8785 struct Row2;
8786
8787 ˇstruct Row4;
8788 struct Row5;
8789 struct Row6;
8790 ˇ
8791 struct Row8;
8792 struct Row10;"#},
8793 vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
8794 indoc! {r#"struct Row;
8795 struct Row2;
8796
8797 ˇstruct Row4;
8798 struct Row5;
8799 struct Row6;
8800 ˇ
8801 struct Row8;
8802 struct Row10;"#},
8803 base_text,
8804 &mut cx,
8805 );
8806 assert_hunk_revert(
8807 indoc! {r#"struct Row;
8808 struct Row2;
8809
8810 «ˇstruct Row4;
8811 struct» Row5;
8812 «struct Row6;
8813 ˇ»
8814 struct Row8;
8815 struct Row10;"#},
8816 vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
8817 indoc! {r#"struct Row;
8818 struct Row2;
8819
8820 «ˇstruct Row4;
8821 struct» Row5;
8822 «struct Row6;
8823 ˇ»
8824 struct Row8;
8825 struct Row10;"#},
8826 base_text,
8827 &mut cx,
8828 );
8829
8830 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
8831 assert_hunk_revert(
8832 indoc! {r#"struct Row;
8833 ˇstruct Row2;
8834
8835 struct Row4;
8836 struct Row5;
8837 struct Row6;
8838
8839 struct Row8;ˇ
8840 struct Row10;"#},
8841 vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
8842 indoc! {r#"struct Row;
8843 struct Row1;
8844 ˇstruct Row2;
8845
8846 struct Row4;
8847 struct Row5;
8848 struct Row6;
8849
8850 struct Row8;ˇ
8851 struct Row9;
8852 struct Row10;"#},
8853 base_text,
8854 &mut cx,
8855 );
8856 assert_hunk_revert(
8857 indoc! {r#"struct Row;
8858 struct Row2«ˇ;
8859 struct Row4;
8860 struct» Row5;
8861 «struct Row6;
8862
8863 struct Row8;ˇ»
8864 struct Row10;"#},
8865 vec![
8866 DiffHunkStatus::Removed,
8867 DiffHunkStatus::Removed,
8868 DiffHunkStatus::Removed,
8869 ],
8870 indoc! {r#"struct Row;
8871 struct Row1;
8872 struct Row2«ˇ;
8873
8874 struct Row4;
8875 struct» Row5;
8876 «struct Row6;
8877
8878 struct Row8;ˇ»
8879 struct Row9;
8880 struct Row10;"#},
8881 base_text,
8882 &mut cx,
8883 );
8884}
8885
8886#[gpui::test]
8887async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
8888 init_test(cx, |_| {});
8889
8890 let cols = 4;
8891 let rows = 10;
8892 let sample_text_1 = sample_text(rows, cols, 'a');
8893 assert_eq!(
8894 sample_text_1,
8895 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
8896 );
8897 let sample_text_2 = sample_text(rows, cols, 'l');
8898 assert_eq!(
8899 sample_text_2,
8900 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
8901 );
8902 let sample_text_3 = sample_text(rows, cols, 'v');
8903 assert_eq!(
8904 sample_text_3,
8905 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
8906 );
8907
8908 fn diff_every_buffer_row(
8909 buffer: &Model<Buffer>,
8910 sample_text: String,
8911 cols: usize,
8912 cx: &mut gpui::TestAppContext,
8913 ) {
8914 // revert first character in each row, creating one large diff hunk per buffer
8915 let is_first_char = |offset: usize| offset % cols == 0;
8916 buffer.update(cx, |buffer, cx| {
8917 buffer.set_text(
8918 sample_text
8919 .chars()
8920 .enumerate()
8921 .map(|(offset, c)| if is_first_char(offset) { 'X' } else { c })
8922 .collect::<String>(),
8923 cx,
8924 );
8925 buffer.set_diff_base(Some(sample_text), cx);
8926 });
8927 cx.executor().run_until_parked();
8928 }
8929
8930 let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text_1.clone(), cx));
8931 diff_every_buffer_row(&buffer_1, sample_text_1.clone(), cols, cx);
8932
8933 let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text_2.clone(), cx));
8934 diff_every_buffer_row(&buffer_2, sample_text_2.clone(), cols, cx);
8935
8936 let buffer_3 = cx.new_model(|cx| Buffer::local(sample_text_3.clone(), cx));
8937 diff_every_buffer_row(&buffer_3, sample_text_3.clone(), cols, cx);
8938
8939 let multibuffer = cx.new_model(|cx| {
8940 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
8941 multibuffer.push_excerpts(
8942 buffer_1.clone(),
8943 [
8944 ExcerptRange {
8945 context: Point::new(0, 0)..Point::new(3, 0),
8946 primary: None,
8947 },
8948 ExcerptRange {
8949 context: Point::new(5, 0)..Point::new(7, 0),
8950 primary: None,
8951 },
8952 ExcerptRange {
8953 context: Point::new(9, 0)..Point::new(10, 4),
8954 primary: None,
8955 },
8956 ],
8957 cx,
8958 );
8959 multibuffer.push_excerpts(
8960 buffer_2.clone(),
8961 [
8962 ExcerptRange {
8963 context: Point::new(0, 0)..Point::new(3, 0),
8964 primary: None,
8965 },
8966 ExcerptRange {
8967 context: Point::new(5, 0)..Point::new(7, 0),
8968 primary: None,
8969 },
8970 ExcerptRange {
8971 context: Point::new(9, 0)..Point::new(10, 4),
8972 primary: None,
8973 },
8974 ],
8975 cx,
8976 );
8977 multibuffer.push_excerpts(
8978 buffer_3.clone(),
8979 [
8980 ExcerptRange {
8981 context: Point::new(0, 0)..Point::new(3, 0),
8982 primary: None,
8983 },
8984 ExcerptRange {
8985 context: Point::new(5, 0)..Point::new(7, 0),
8986 primary: None,
8987 },
8988 ExcerptRange {
8989 context: Point::new(9, 0)..Point::new(10, 4),
8990 primary: None,
8991 },
8992 ],
8993 cx,
8994 );
8995 multibuffer
8996 });
8997
8998 let (editor, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
8999 editor.update(cx, |editor, cx| {
9000 assert_eq!(editor.text(cx), "XaaaXbbbX\nccXc\ndXdd\n\nhXhh\nXiiiXjjjX\n\nXlllXmmmX\nnnXn\noXoo\n\nsXss\nXtttXuuuX\n\nXvvvXwwwX\nxxXx\nyXyy\n\n}X}}\nX~~~X\u{7f}\u{7f}\u{7f}X\n");
9001 editor.select_all(&SelectAll, cx);
9002 editor.revert_selected_hunks(&RevertSelectedHunks, cx);
9003 });
9004 cx.executor().run_until_parked();
9005 // When all ranges are selected, all buffer hunks are reverted.
9006 editor.update(cx, |editor, cx| {
9007 assert_eq!(editor.text(cx), "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n\n\nllll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu\n\n\nvvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}\n\n");
9008 });
9009 buffer_1.update(cx, |buffer, _| {
9010 assert_eq!(buffer.text(), sample_text_1);
9011 });
9012 buffer_2.update(cx, |buffer, _| {
9013 assert_eq!(buffer.text(), sample_text_2);
9014 });
9015 buffer_3.update(cx, |buffer, _| {
9016 assert_eq!(buffer.text(), sample_text_3);
9017 });
9018
9019 diff_every_buffer_row(&buffer_1, sample_text_1.clone(), cols, cx);
9020 diff_every_buffer_row(&buffer_2, sample_text_2.clone(), cols, cx);
9021 diff_every_buffer_row(&buffer_3, sample_text_3.clone(), cols, cx);
9022 editor.update(cx, |editor, cx| {
9023 editor.change_selections(None, cx, |s| {
9024 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
9025 });
9026 editor.revert_selected_hunks(&RevertSelectedHunks, cx);
9027 });
9028 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
9029 // but not affect buffer_2 and its related excerpts.
9030 editor.update(cx, |editor, cx| {
9031 assert_eq!(
9032 editor.text(cx),
9033 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n\n\nXlllXmmmX\nnnXn\noXoo\nXpppXqqqX\nrrXr\nsXss\nXtttXuuuX\n\n\nXvvvXwwwX\nxxXx\nyXyy\nXzzzX{{{X\n||X|\n}X}}\nX~~~X\u{7f}\u{7f}\u{7f}X\n\n"
9034 );
9035 });
9036 buffer_1.update(cx, |buffer, _| {
9037 assert_eq!(buffer.text(), sample_text_1);
9038 });
9039 buffer_2.update(cx, |buffer, _| {
9040 assert_eq!(
9041 buffer.text(),
9042 "XlllXmmmX\nnnXn\noXoo\nXpppXqqqX\nrrXr\nsXss\nXtttXuuuX"
9043 );
9044 });
9045 buffer_3.update(cx, |buffer, _| {
9046 assert_eq!(
9047 buffer.text(),
9048 "XvvvXwwwX\nxxXx\nyXyy\nXzzzX{{{X\n||X|\n}X}}\nX~~~X\u{7f}\u{7f}\u{7f}X"
9049 );
9050 });
9051}
9052
9053#[gpui::test]
9054async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
9055 init_test(cx, |_| {});
9056
9057 let cols = 4;
9058 let rows = 10;
9059 let sample_text_1 = sample_text(rows, cols, 'a');
9060 assert_eq!(
9061 sample_text_1,
9062 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9063 );
9064 let sample_text_2 = sample_text(rows, cols, 'l');
9065 assert_eq!(
9066 sample_text_2,
9067 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9068 );
9069 let sample_text_3 = sample_text(rows, cols, 'v');
9070 assert_eq!(
9071 sample_text_3,
9072 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9073 );
9074
9075 let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text_1.clone(), cx));
9076 let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text_2.clone(), cx));
9077 let buffer_3 = cx.new_model(|cx| Buffer::local(sample_text_3.clone(), cx));
9078
9079 let multi_buffer = cx.new_model(|cx| {
9080 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
9081 multibuffer.push_excerpts(
9082 buffer_1.clone(),
9083 [
9084 ExcerptRange {
9085 context: Point::new(0, 0)..Point::new(3, 0),
9086 primary: None,
9087 },
9088 ExcerptRange {
9089 context: Point::new(5, 0)..Point::new(7, 0),
9090 primary: None,
9091 },
9092 ExcerptRange {
9093 context: Point::new(9, 0)..Point::new(10, 4),
9094 primary: None,
9095 },
9096 ],
9097 cx,
9098 );
9099 multibuffer.push_excerpts(
9100 buffer_2.clone(),
9101 [
9102 ExcerptRange {
9103 context: Point::new(0, 0)..Point::new(3, 0),
9104 primary: None,
9105 },
9106 ExcerptRange {
9107 context: Point::new(5, 0)..Point::new(7, 0),
9108 primary: None,
9109 },
9110 ExcerptRange {
9111 context: Point::new(9, 0)..Point::new(10, 4),
9112 primary: None,
9113 },
9114 ],
9115 cx,
9116 );
9117 multibuffer.push_excerpts(
9118 buffer_3.clone(),
9119 [
9120 ExcerptRange {
9121 context: Point::new(0, 0)..Point::new(3, 0),
9122 primary: None,
9123 },
9124 ExcerptRange {
9125 context: Point::new(5, 0)..Point::new(7, 0),
9126 primary: None,
9127 },
9128 ExcerptRange {
9129 context: Point::new(9, 0)..Point::new(10, 4),
9130 primary: None,
9131 },
9132 ],
9133 cx,
9134 );
9135 multibuffer
9136 });
9137
9138 let fs = FakeFs::new(cx.executor());
9139 fs.insert_tree(
9140 "/a",
9141 json!({
9142 "main.rs": sample_text_1,
9143 "other.rs": sample_text_2,
9144 "lib.rs": sample_text_3,
9145 }),
9146 )
9147 .await;
9148 let project = Project::test(fs, ["/a".as_ref()], cx).await;
9149 let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
9150 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9151 let multi_buffer_editor =
9152 cx.new_view(|cx| Editor::new(EditorMode::Full, multi_buffer, Some(project.clone()), cx));
9153 let multibuffer_item_id = workspace
9154 .update(cx, |workspace, cx| {
9155 assert!(
9156 workspace.active_item(cx).is_none(),
9157 "active item should be None before the first item is added"
9158 );
9159 workspace.add_item_to_active_pane(Box::new(multi_buffer_editor.clone()), None, cx);
9160 let active_item = workspace
9161 .active_item(cx)
9162 .expect("should have an active item after adding the multi buffer");
9163 assert!(
9164 !active_item.is_singleton(cx),
9165 "A multi buffer was expected to active after adding"
9166 );
9167 active_item.item_id()
9168 })
9169 .unwrap();
9170 cx.executor().run_until_parked();
9171
9172 multi_buffer_editor.update(cx, |editor, cx| {
9173 editor.change_selections(Some(Autoscroll::Next), cx, |s| s.select_ranges(Some(1..2)));
9174 editor.open_excerpts(&OpenExcerpts, cx);
9175 });
9176 cx.executor().run_until_parked();
9177 let first_item_id = workspace
9178 .update(cx, |workspace, cx| {
9179 let active_item = workspace
9180 .active_item(cx)
9181 .expect("should have an active item after navigating into the 1st buffer");
9182 let first_item_id = active_item.item_id();
9183 assert_ne!(
9184 first_item_id, multibuffer_item_id,
9185 "Should navigate into the 1st buffer and activate it"
9186 );
9187 assert!(
9188 active_item.is_singleton(cx),
9189 "New active item should be a singleton buffer"
9190 );
9191 assert_eq!(
9192 active_item
9193 .act_as::<Editor>(cx)
9194 .expect("should have navigated into an editor for the 1st buffer")
9195 .read(cx)
9196 .text(cx),
9197 sample_text_1
9198 );
9199
9200 workspace
9201 .go_back(workspace.active_pane().downgrade(), cx)
9202 .detach_and_log_err(cx);
9203
9204 first_item_id
9205 })
9206 .unwrap();
9207 cx.executor().run_until_parked();
9208 workspace
9209 .update(cx, |workspace, cx| {
9210 let active_item = workspace
9211 .active_item(cx)
9212 .expect("should have an active item after navigating back");
9213 assert_eq!(
9214 active_item.item_id(),
9215 multibuffer_item_id,
9216 "Should navigate back to the multi buffer"
9217 );
9218 assert!(!active_item.is_singleton(cx));
9219 })
9220 .unwrap();
9221
9222 multi_buffer_editor.update(cx, |editor, cx| {
9223 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
9224 s.select_ranges(Some(39..40))
9225 });
9226 editor.open_excerpts(&OpenExcerpts, cx);
9227 });
9228 cx.executor().run_until_parked();
9229 let second_item_id = workspace
9230 .update(cx, |workspace, cx| {
9231 let active_item = workspace
9232 .active_item(cx)
9233 .expect("should have an active item after navigating into the 2nd buffer");
9234 let second_item_id = active_item.item_id();
9235 assert_ne!(
9236 second_item_id, multibuffer_item_id,
9237 "Should navigate away from the multibuffer"
9238 );
9239 assert_ne!(
9240 second_item_id, first_item_id,
9241 "Should navigate into the 2nd buffer and activate it"
9242 );
9243 assert!(
9244 active_item.is_singleton(cx),
9245 "New active item should be a singleton buffer"
9246 );
9247 assert_eq!(
9248 active_item
9249 .act_as::<Editor>(cx)
9250 .expect("should have navigated into an editor")
9251 .read(cx)
9252 .text(cx),
9253 sample_text_2
9254 );
9255
9256 workspace
9257 .go_back(workspace.active_pane().downgrade(), cx)
9258 .detach_and_log_err(cx);
9259
9260 second_item_id
9261 })
9262 .unwrap();
9263 cx.executor().run_until_parked();
9264 workspace
9265 .update(cx, |workspace, cx| {
9266 let active_item = workspace
9267 .active_item(cx)
9268 .expect("should have an active item after navigating back from the 2nd buffer");
9269 assert_eq!(
9270 active_item.item_id(),
9271 multibuffer_item_id,
9272 "Should navigate back from the 2nd buffer to the multi buffer"
9273 );
9274 assert!(!active_item.is_singleton(cx));
9275 })
9276 .unwrap();
9277
9278 multi_buffer_editor.update(cx, |editor, cx| {
9279 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
9280 s.select_ranges(Some(60..70))
9281 });
9282 editor.open_excerpts(&OpenExcerpts, cx);
9283 });
9284 cx.executor().run_until_parked();
9285 workspace
9286 .update(cx, |workspace, cx| {
9287 let active_item = workspace
9288 .active_item(cx)
9289 .expect("should have an active item after navigating into the 3rd buffer");
9290 let third_item_id = active_item.item_id();
9291 assert_ne!(
9292 third_item_id, multibuffer_item_id,
9293 "Should navigate into the 3rd buffer and activate it"
9294 );
9295 assert_ne!(third_item_id, first_item_id);
9296 assert_ne!(third_item_id, second_item_id);
9297 assert!(
9298 active_item.is_singleton(cx),
9299 "New active item should be a singleton buffer"
9300 );
9301 assert_eq!(
9302 active_item
9303 .act_as::<Editor>(cx)
9304 .expect("should have navigated into an editor")
9305 .read(cx)
9306 .text(cx),
9307 sample_text_3
9308 );
9309
9310 workspace
9311 .go_back(workspace.active_pane().downgrade(), cx)
9312 .detach_and_log_err(cx);
9313 })
9314 .unwrap();
9315 cx.executor().run_until_parked();
9316 workspace
9317 .update(cx, |workspace, cx| {
9318 let active_item = workspace
9319 .active_item(cx)
9320 .expect("should have an active item after navigating back from the 3rd buffer");
9321 assert_eq!(
9322 active_item.item_id(),
9323 multibuffer_item_id,
9324 "Should navigate back from the 3rd buffer to the multi buffer"
9325 );
9326 assert!(!active_item.is_singleton(cx));
9327 })
9328 .unwrap();
9329}
9330
9331#[gpui::test]
9332async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
9333 init_test(cx, |_| {});
9334
9335 let mut cx = EditorTestContext::new(cx).await;
9336
9337 let diff_base = r#"
9338 use some::mod;
9339
9340 const A: u32 = 42;
9341
9342 fn main() {
9343 println!("hello");
9344
9345 println!("world");
9346 }
9347 "#
9348 .unindent();
9349
9350 cx.set_state(
9351 &r#"
9352 use some::modified;
9353
9354 ˇ
9355 fn main() {
9356 println!("hello there");
9357
9358 println!("around the");
9359 println!("world");
9360 }
9361 "#
9362 .unindent(),
9363 );
9364
9365 cx.set_diff_base(Some(&diff_base));
9366 executor.run_until_parked();
9367 let unexpanded_hunks = vec![
9368 (
9369 "use some::mod;\n".to_string(),
9370 DiffHunkStatus::Modified,
9371 0..1,
9372 ),
9373 (
9374 "const A: u32 = 42;\n".to_string(),
9375 DiffHunkStatus::Removed,
9376 2..2,
9377 ),
9378 (
9379 " println!(\"hello\");\n".to_string(),
9380 DiffHunkStatus::Modified,
9381 4..5,
9382 ),
9383 ("".to_string(), DiffHunkStatus::Added, 6..7),
9384 ];
9385 cx.update_editor(|editor, cx| {
9386 let snapshot = editor.snapshot(cx);
9387 let all_hunks = editor_hunks(editor, &snapshot, cx);
9388 assert_eq!(all_hunks, unexpanded_hunks);
9389 });
9390
9391 cx.update_editor(|editor, cx| {
9392 for _ in 0..4 {
9393 editor.go_to_hunk(&GoToHunk, cx);
9394 editor.toggle_hunk_diff(&ToggleHunkDiff, cx);
9395 }
9396 });
9397 executor.run_until_parked();
9398 cx.assert_editor_state(
9399 &r#"
9400 use some::modified;
9401
9402 ˇ
9403 fn main() {
9404 println!("hello there");
9405
9406 println!("around the");
9407 println!("world");
9408 }
9409 "#
9410 .unindent(),
9411 );
9412 cx.update_editor(|editor, cx| {
9413 let snapshot = editor.snapshot(cx);
9414 let all_hunks = editor_hunks(editor, &snapshot, cx);
9415 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
9416 assert_eq!(
9417 expanded_hunks_background_highlights(editor, &snapshot),
9418 vec![1..2, 7..8, 9..10],
9419 "After expanding, all git additions should be highlighted for Modified (split into added and removed) and Added hunks"
9420 );
9421 assert_eq!(
9422 all_hunks,
9423 vec![
9424 ("use some::mod;\n".to_string(), DiffHunkStatus::Modified, 1..2),
9425 ("const A: u32 = 42;\n".to_string(), DiffHunkStatus::Removed, 4..4),
9426 (" println!(\"hello\");\n".to_string(), DiffHunkStatus::Modified, 7..8),
9427 ("".to_string(), DiffHunkStatus::Added, 9..10),
9428 ],
9429 "After expanding, all hunks' display rows should have shifted by the amount of deleted lines added \
9430 (from modified and removed hunks)"
9431 );
9432 assert_eq!(
9433 all_hunks, all_expanded_hunks,
9434 "Editor hunks should not change and all be expanded"
9435 );
9436 });
9437
9438 cx.update_editor(|editor, cx| {
9439 editor.cancel(&Cancel, cx);
9440
9441 let snapshot = editor.snapshot(cx);
9442 let all_hunks = editor_hunks(editor, &snapshot, cx);
9443 let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
9444 assert_eq!(
9445 expanded_hunks_background_highlights(editor, &snapshot),
9446 Vec::new(),
9447 "After cancelling in editor, no git highlights should be left"
9448 );
9449 assert_eq!(
9450 all_expanded_hunks,
9451 Vec::new(),
9452 "After cancelling in editor, no hunks should be expanded"
9453 );
9454 assert_eq!(
9455 all_hunks, unexpanded_hunks,
9456 "After cancelling in editor, regular hunks' coordinates should get back to normal"
9457 );
9458 });
9459}
9460
9461#[gpui::test]
9462async fn test_toggled_diff_base_change(
9463 executor: BackgroundExecutor,
9464 cx: &mut gpui::TestAppContext,
9465) {
9466 init_test(cx, |_| {});
9467
9468 let mut cx = EditorTestContext::new(cx).await;
9469
9470 let diff_base = r#"
9471 use some::mod1;
9472 use some::mod2;
9473
9474 const A: u32 = 42;
9475 const B: u32 = 42;
9476 const C: u32 = 42;
9477
9478 fn main(ˇ) {
9479 println!("hello");
9480
9481 println!("world");
9482 }
9483 "#
9484 .unindent();
9485
9486 cx.set_state(
9487 &r#"
9488 use some::mod2;
9489
9490 const A: u32 = 42;
9491 const C: u32 = 42;
9492
9493 fn main(ˇ) {
9494 //println!("hello");
9495
9496 println!("world");
9497 //
9498 //
9499 }
9500 "#
9501 .unindent(),
9502 );
9503
9504 cx.set_diff_base(Some(&diff_base));
9505 executor.run_until_parked();
9506 cx.update_editor(|editor, cx| {
9507 let snapshot = editor.snapshot(cx);
9508 let all_hunks = editor_hunks(editor, &snapshot, cx);
9509 assert_eq!(
9510 all_hunks,
9511 vec![
9512 (
9513 "use some::mod1;\n".to_string(),
9514 DiffHunkStatus::Removed,
9515 0..0
9516 ),
9517 (
9518 "const B: u32 = 42;\n".to_string(),
9519 DiffHunkStatus::Removed,
9520 3..3
9521 ),
9522 (
9523 "fn main(ˇ) {\n println!(\"hello\");\n".to_string(),
9524 DiffHunkStatus::Modified,
9525 5..7
9526 ),
9527 ("".to_string(), DiffHunkStatus::Added, 9..11),
9528 ]
9529 );
9530 });
9531
9532 cx.update_editor(|editor, cx| {
9533 editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx);
9534 });
9535 executor.run_until_parked();
9536 cx.assert_editor_state(
9537 &r#"
9538 use some::mod2;
9539
9540 const A: u32 = 42;
9541 const C: u32 = 42;
9542
9543 fn main(ˇ) {
9544 //println!("hello");
9545
9546 println!("world");
9547 //
9548 //
9549 }
9550 "#
9551 .unindent(),
9552 );
9553 cx.update_editor(|editor, cx| {
9554 let snapshot = editor.snapshot(cx);
9555 let all_hunks = editor_hunks(editor, &snapshot, cx);
9556 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
9557 assert_eq!(
9558 expanded_hunks_background_highlights(editor, &snapshot),
9559 vec![9..11, 13..15],
9560 "After expanding, all git additions should be highlighted for Modified (split into added and removed) and Added hunks"
9561 );
9562 assert_eq!(
9563 all_hunks,
9564 vec![
9565 ("use some::mod1;\n".to_string(), DiffHunkStatus::Removed, 1..1),
9566 ("const B: u32 = 42;\n".to_string(), DiffHunkStatus::Removed, 5..5),
9567 ("fn main(ˇ) {\n println!(\"hello\");\n".to_string(), DiffHunkStatus::Modified, 9..11),
9568 ("".to_string(), DiffHunkStatus::Added, 13..15),
9569 ],
9570 "After expanding, all hunks' display rows should have shifted by the amount of deleted lines added \
9571 (from modified and removed hunks)"
9572 );
9573 assert_eq!(
9574 all_hunks, all_expanded_hunks,
9575 "Editor hunks should not change and all be expanded"
9576 );
9577 });
9578
9579 cx.set_diff_base(Some("new diff base!"));
9580 executor.run_until_parked();
9581
9582 cx.update_editor(|editor, cx| {
9583 let snapshot = editor.snapshot(cx);
9584 let all_hunks = editor_hunks(editor, &snapshot, cx);
9585 let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
9586 assert_eq!(
9587 expanded_hunks_background_highlights(editor, &snapshot),
9588 Vec::new(),
9589 "After diff base is changed, old git highlights should be removed"
9590 );
9591 assert_eq!(
9592 all_expanded_hunks,
9593 Vec::new(),
9594 "After diff base is changed, old git hunk expansions should be removed"
9595 );
9596 assert_eq!(
9597 all_hunks,
9598 vec![(
9599 "new diff base!".to_string(),
9600 DiffHunkStatus::Modified,
9601 0..snapshot.display_snapshot.max_point().row()
9602 )],
9603 "After diff base is changed, hunks should update"
9604 );
9605 });
9606}
9607
9608#[gpui::test]
9609async fn test_fold_unfold_diff(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
9610 init_test(cx, |_| {});
9611
9612 let mut cx = EditorTestContext::new(cx).await;
9613
9614 let diff_base = r#"
9615 use some::mod1;
9616 use some::mod2;
9617
9618 const A: u32 = 42;
9619 const B: u32 = 42;
9620 const C: u32 = 42;
9621
9622 fn main(ˇ) {
9623 println!("hello");
9624
9625 println!("world");
9626 }
9627
9628 fn another() {
9629 println!("another");
9630 }
9631
9632 fn another2() {
9633 println!("another2");
9634 }
9635 "#
9636 .unindent();
9637
9638 cx.set_state(
9639 &r#"
9640 «use some::mod2;
9641
9642 const A: u32 = 42;
9643 const C: u32 = 42;
9644
9645 fn main() {
9646 //println!("hello");
9647
9648 println!("world");
9649 //
9650 //ˇ»
9651 }
9652
9653 fn another() {
9654 println!("another");
9655 println!("another");
9656 }
9657
9658 println!("another2");
9659 }
9660 "#
9661 .unindent(),
9662 );
9663
9664 cx.set_diff_base(Some(&diff_base));
9665 executor.run_until_parked();
9666 cx.update_editor(|editor, cx| {
9667 let snapshot = editor.snapshot(cx);
9668 let all_hunks = editor_hunks(editor, &snapshot, cx);
9669 assert_eq!(
9670 all_hunks,
9671 vec![
9672 (
9673 "use some::mod1;\n".to_string(),
9674 DiffHunkStatus::Removed,
9675 0..0
9676 ),
9677 (
9678 "const B: u32 = 42;\n".to_string(),
9679 DiffHunkStatus::Removed,
9680 3..3
9681 ),
9682 (
9683 "fn main(ˇ) {\n println!(\"hello\");\n".to_string(),
9684 DiffHunkStatus::Modified,
9685 5..7
9686 ),
9687 ("".to_string(), DiffHunkStatus::Added, 9..11),
9688 ("".to_string(), DiffHunkStatus::Added, 15..16),
9689 (
9690 "fn another2() {\n".to_string(),
9691 DiffHunkStatus::Removed,
9692 18..18
9693 ),
9694 ]
9695 );
9696 });
9697
9698 cx.update_editor(|editor, cx| {
9699 editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx);
9700 });
9701 executor.run_until_parked();
9702 cx.assert_editor_state(
9703 &r#"
9704 «use some::mod2;
9705
9706 const A: u32 = 42;
9707 const C: u32 = 42;
9708
9709 fn main() {
9710 //println!("hello");
9711
9712 println!("world");
9713 //
9714 //ˇ»
9715 }
9716
9717 fn another() {
9718 println!("another");
9719 println!("another");
9720 }
9721
9722 println!("another2");
9723 }
9724 "#
9725 .unindent(),
9726 );
9727 cx.update_editor(|editor, cx| {
9728 let snapshot = editor.snapshot(cx);
9729 let all_hunks = editor_hunks(editor, &snapshot, cx);
9730 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
9731 assert_eq!(
9732 expanded_hunks_background_highlights(editor, &snapshot),
9733 vec![9..11, 13..15, 19..20]
9734 );
9735 assert_eq!(
9736 all_hunks,
9737 vec![
9738 (
9739 "use some::mod1;\n".to_string(),
9740 DiffHunkStatus::Removed,
9741 1..1
9742 ),
9743 (
9744 "const B: u32 = 42;\n".to_string(),
9745 DiffHunkStatus::Removed,
9746 5..5
9747 ),
9748 (
9749 "fn main(ˇ) {\n println!(\"hello\");\n".to_string(),
9750 DiffHunkStatus::Modified,
9751 9..11
9752 ),
9753 ("".to_string(), DiffHunkStatus::Added, 13..15),
9754 ("".to_string(), DiffHunkStatus::Added, 19..20),
9755 (
9756 "fn another2() {\n".to_string(),
9757 DiffHunkStatus::Removed,
9758 23..23
9759 ),
9760 ],
9761 );
9762 assert_eq!(all_hunks, all_expanded_hunks);
9763 });
9764
9765 cx.update_editor(|editor, cx| editor.fold_selected_ranges(&FoldSelectedRanges, cx));
9766 cx.executor().run_until_parked();
9767 cx.assert_editor_state(
9768 &r#"
9769 «use some::mod2;
9770
9771 const A: u32 = 42;
9772 const C: u32 = 42;
9773
9774 fn main() {
9775 //println!("hello");
9776
9777 println!("world");
9778 //
9779 //ˇ»
9780 }
9781
9782 fn another() {
9783 println!("another");
9784 println!("another");
9785 }
9786
9787 println!("another2");
9788 }
9789 "#
9790 .unindent(),
9791 );
9792 cx.update_editor(|editor, cx| {
9793 let snapshot = editor.snapshot(cx);
9794 let all_hunks = editor_hunks(editor, &snapshot, cx);
9795 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
9796 assert_eq!(
9797 expanded_hunks_background_highlights(editor, &snapshot),
9798 vec![5..6],
9799 "Only one hunk is left not folded, its highlight should be visible"
9800 );
9801 assert_eq!(
9802 all_hunks,
9803 vec![
9804 (
9805 "use some::mod1;\n".to_string(),
9806 DiffHunkStatus::Removed,
9807 0..0
9808 ),
9809 (
9810 "const B: u32 = 42;\n".to_string(),
9811 DiffHunkStatus::Removed,
9812 0..0
9813 ),
9814 (
9815 "fn main(ˇ) {\n println!(\"hello\");\n".to_string(),
9816 DiffHunkStatus::Modified,
9817 0..0
9818 ),
9819 ("".to_string(), DiffHunkStatus::Added, 0..1),
9820 ("".to_string(), DiffHunkStatus::Added, 5..6),
9821 (
9822 "fn another2() {\n".to_string(),
9823 DiffHunkStatus::Removed,
9824 9..9
9825 ),
9826 ],
9827 "Hunk list should still return shifted folded hunks"
9828 );
9829 assert_eq!(
9830 all_expanded_hunks,
9831 vec![
9832 ("".to_string(), DiffHunkStatus::Added, 5..6),
9833 (
9834 "fn another2() {\n".to_string(),
9835 DiffHunkStatus::Removed,
9836 9..9
9837 ),
9838 ],
9839 "Only non-folded hunks should be left expanded"
9840 );
9841 });
9842
9843 cx.update_editor(|editor, cx| {
9844 editor.select_all(&SelectAll, cx);
9845 editor.unfold_lines(&UnfoldLines, cx);
9846 });
9847 cx.executor().run_until_parked();
9848 cx.assert_editor_state(
9849 &r#"
9850 «use some::mod2;
9851
9852 const A: u32 = 42;
9853 const C: u32 = 42;
9854
9855 fn main() {
9856 //println!("hello");
9857
9858 println!("world");
9859 //
9860 //
9861 }
9862
9863 fn another() {
9864 println!("another");
9865 println!("another");
9866 }
9867
9868 println!("another2");
9869 }
9870 ˇ»"#
9871 .unindent(),
9872 );
9873 cx.update_editor(|editor, cx| {
9874 let snapshot = editor.snapshot(cx);
9875 let all_hunks = editor_hunks(editor, &snapshot, cx);
9876 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
9877 assert_eq!(
9878 expanded_hunks_background_highlights(editor, &snapshot),
9879 vec![9..11, 13..15, 19..20],
9880 "After unfolding, all hunk diffs should be visible again"
9881 );
9882 assert_eq!(
9883 all_hunks,
9884 vec![
9885 (
9886 "use some::mod1;\n".to_string(),
9887 DiffHunkStatus::Removed,
9888 1..1
9889 ),
9890 (
9891 "const B: u32 = 42;\n".to_string(),
9892 DiffHunkStatus::Removed,
9893 5..5
9894 ),
9895 (
9896 "fn main(ˇ) {\n println!(\"hello\");\n".to_string(),
9897 DiffHunkStatus::Modified,
9898 9..11
9899 ),
9900 ("".to_string(), DiffHunkStatus::Added, 13..15),
9901 ("".to_string(), DiffHunkStatus::Added, 19..20),
9902 (
9903 "fn another2() {\n".to_string(),
9904 DiffHunkStatus::Removed,
9905 23..23
9906 ),
9907 ],
9908 );
9909 assert_eq!(all_hunks, all_expanded_hunks);
9910 });
9911}
9912
9913#[gpui::test]
9914async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) {
9915 init_test(cx, |_| {});
9916
9917 let cols = 4;
9918 let rows = 10;
9919 let sample_text_1 = sample_text(rows, cols, 'a');
9920 assert_eq!(
9921 sample_text_1,
9922 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9923 );
9924 let modified_sample_text_1 = "aaaa\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
9925 let sample_text_2 = sample_text(rows, cols, 'l');
9926 assert_eq!(
9927 sample_text_2,
9928 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9929 );
9930 let modified_sample_text_2 = "llll\nmmmm\n1n1n1n1n1\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
9931 let sample_text_3 = sample_text(rows, cols, 'v');
9932 assert_eq!(
9933 sample_text_3,
9934 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9935 );
9936 let modified_sample_text_3 =
9937 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n@@@@\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
9938 let buffer_1 = cx.new_model(|cx| {
9939 let mut buffer = Buffer::local(modified_sample_text_1.to_string(), cx);
9940 buffer.set_diff_base(Some(sample_text_1.clone()), cx);
9941 buffer
9942 });
9943 let buffer_2 = cx.new_model(|cx| {
9944 let mut buffer = Buffer::local(modified_sample_text_2.to_string(), cx);
9945 buffer.set_diff_base(Some(sample_text_2.clone()), cx);
9946 buffer
9947 });
9948 let buffer_3 = cx.new_model(|cx| {
9949 let mut buffer = Buffer::local(modified_sample_text_3.to_string(), cx);
9950 buffer.set_diff_base(Some(sample_text_3.clone()), cx);
9951 buffer
9952 });
9953
9954 let multi_buffer = cx.new_model(|cx| {
9955 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
9956 multibuffer.push_excerpts(
9957 buffer_1.clone(),
9958 [
9959 ExcerptRange {
9960 context: Point::new(0, 0)..Point::new(3, 0),
9961 primary: None,
9962 },
9963 ExcerptRange {
9964 context: Point::new(5, 0)..Point::new(7, 0),
9965 primary: None,
9966 },
9967 ExcerptRange {
9968 context: Point::new(9, 0)..Point::new(10, 4),
9969 primary: None,
9970 },
9971 ],
9972 cx,
9973 );
9974 multibuffer.push_excerpts(
9975 buffer_2.clone(),
9976 [
9977 ExcerptRange {
9978 context: Point::new(0, 0)..Point::new(3, 0),
9979 primary: None,
9980 },
9981 ExcerptRange {
9982 context: Point::new(5, 0)..Point::new(7, 0),
9983 primary: None,
9984 },
9985 ExcerptRange {
9986 context: Point::new(9, 0)..Point::new(10, 4),
9987 primary: None,
9988 },
9989 ],
9990 cx,
9991 );
9992 multibuffer.push_excerpts(
9993 buffer_3.clone(),
9994 [
9995 ExcerptRange {
9996 context: Point::new(0, 0)..Point::new(3, 0),
9997 primary: None,
9998 },
9999 ExcerptRange {
10000 context: Point::new(5, 0)..Point::new(7, 0),
10001 primary: None,
10002 },
10003 ExcerptRange {
10004 context: Point::new(9, 0)..Point::new(10, 4),
10005 primary: None,
10006 },
10007 ],
10008 cx,
10009 );
10010 multibuffer
10011 });
10012
10013 let fs = FakeFs::new(cx.executor());
10014 fs.insert_tree(
10015 "/a",
10016 json!({
10017 "main.rs": modified_sample_text_1,
10018 "other.rs": modified_sample_text_2,
10019 "lib.rs": modified_sample_text_3,
10020 }),
10021 )
10022 .await;
10023
10024 let project = Project::test(fs, ["/a".as_ref()], cx).await;
10025 let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
10026 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10027 let multi_buffer_editor =
10028 cx.new_view(|cx| Editor::new(EditorMode::Full, multi_buffer, Some(project.clone()), cx));
10029 cx.executor().run_until_parked();
10030
10031 let expected_all_hunks = vec![
10032 ("bbbb\n".to_string(), DiffHunkStatus::Removed, 3..3),
10033 ("nnnn\n".to_string(), DiffHunkStatus::Modified, 16..17),
10034 ("".to_string(), DiffHunkStatus::Added, 31..32),
10035 ];
10036 let expected_all_hunks_shifted = vec![
10037 ("bbbb\n".to_string(), DiffHunkStatus::Removed, 4..4),
10038 ("nnnn\n".to_string(), DiffHunkStatus::Modified, 18..19),
10039 ("".to_string(), DiffHunkStatus::Added, 33..34),
10040 ];
10041
10042 multi_buffer_editor.update(cx, |editor, cx| {
10043 let snapshot = editor.snapshot(cx);
10044 let all_hunks = editor_hunks(editor, &snapshot, cx);
10045 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
10046 assert_eq!(
10047 expanded_hunks_background_highlights(editor, &snapshot),
10048 Vec::new(),
10049 );
10050 assert_eq!(all_hunks, expected_all_hunks);
10051 assert_eq!(all_expanded_hunks, Vec::new());
10052 });
10053
10054 multi_buffer_editor.update(cx, |editor, cx| {
10055 editor.select_all(&SelectAll, cx);
10056 editor.toggle_hunk_diff(&ToggleHunkDiff, cx);
10057 });
10058 cx.executor().run_until_parked();
10059 multi_buffer_editor.update(cx, |editor, cx| {
10060 let snapshot = editor.snapshot(cx);
10061 let all_hunks = editor_hunks(editor, &snapshot, cx);
10062 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
10063 assert_eq!(
10064 expanded_hunks_background_highlights(editor, &snapshot),
10065 vec![18..19, 33..34],
10066 );
10067 assert_eq!(all_hunks, expected_all_hunks_shifted);
10068 assert_eq!(all_hunks, all_expanded_hunks);
10069 });
10070
10071 multi_buffer_editor.update(cx, |editor, cx| {
10072 editor.toggle_hunk_diff(&ToggleHunkDiff, cx);
10073 });
10074 cx.executor().run_until_parked();
10075 multi_buffer_editor.update(cx, |editor, cx| {
10076 let snapshot = editor.snapshot(cx);
10077 let all_hunks = editor_hunks(editor, &snapshot, cx);
10078 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
10079 assert_eq!(
10080 expanded_hunks_background_highlights(editor, &snapshot),
10081 Vec::new(),
10082 );
10083 assert_eq!(all_hunks, expected_all_hunks);
10084 assert_eq!(all_expanded_hunks, Vec::new());
10085 });
10086
10087 multi_buffer_editor.update(cx, |editor, cx| {
10088 editor.toggle_hunk_diff(&ToggleHunkDiff, cx);
10089 });
10090 cx.executor().run_until_parked();
10091 multi_buffer_editor.update(cx, |editor, cx| {
10092 let snapshot = editor.snapshot(cx);
10093 let all_hunks = editor_hunks(editor, &snapshot, cx);
10094 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
10095 assert_eq!(
10096 expanded_hunks_background_highlights(editor, &snapshot),
10097 vec![18..19, 33..34],
10098 );
10099 assert_eq!(all_hunks, expected_all_hunks_shifted);
10100 assert_eq!(all_hunks, all_expanded_hunks);
10101 });
10102
10103 multi_buffer_editor.update(cx, |editor, cx| {
10104 editor.toggle_hunk_diff(&ToggleHunkDiff, cx);
10105 });
10106 cx.executor().run_until_parked();
10107 multi_buffer_editor.update(cx, |editor, cx| {
10108 let snapshot = editor.snapshot(cx);
10109 let all_hunks = editor_hunks(editor, &snapshot, cx);
10110 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
10111 assert_eq!(
10112 expanded_hunks_background_highlights(editor, &snapshot),
10113 Vec::new(),
10114 );
10115 assert_eq!(all_hunks, expected_all_hunks);
10116 assert_eq!(all_expanded_hunks, Vec::new());
10117 });
10118}
10119
10120#[gpui::test]
10121async fn test_edits_around_toggled_additions(
10122 executor: BackgroundExecutor,
10123 cx: &mut gpui::TestAppContext,
10124) {
10125 init_test(cx, |_| {});
10126
10127 let mut cx = EditorTestContext::new(cx).await;
10128
10129 let diff_base = r#"
10130 use some::mod1;
10131 use some::mod2;
10132
10133 const A: u32 = 42;
10134
10135 fn main() {
10136 println!("hello");
10137
10138 println!("world");
10139 }
10140 "#
10141 .unindent();
10142 executor.run_until_parked();
10143 cx.set_state(
10144 &r#"
10145 use some::mod1;
10146 use some::mod2;
10147
10148 const A: u32 = 42;
10149 const B: u32 = 42;
10150 const C: u32 = 42;
10151 ˇ
10152
10153 fn main() {
10154 println!("hello");
10155
10156 println!("world");
10157 }
10158 "#
10159 .unindent(),
10160 );
10161
10162 cx.set_diff_base(Some(&diff_base));
10163 executor.run_until_parked();
10164 cx.update_editor(|editor, cx| {
10165 let snapshot = editor.snapshot(cx);
10166 let all_hunks = editor_hunks(editor, &snapshot, cx);
10167 assert_eq!(
10168 all_hunks,
10169 vec![("".to_string(), DiffHunkStatus::Added, 4..7)]
10170 );
10171 });
10172 cx.update_editor(|editor, cx| {
10173 editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx);
10174 });
10175 executor.run_until_parked();
10176 cx.assert_editor_state(
10177 &r#"
10178 use some::mod1;
10179 use some::mod2;
10180
10181 const A: u32 = 42;
10182 const B: u32 = 42;
10183 const C: u32 = 42;
10184 ˇ
10185
10186 fn main() {
10187 println!("hello");
10188
10189 println!("world");
10190 }
10191 "#
10192 .unindent(),
10193 );
10194 cx.update_editor(|editor, cx| {
10195 let snapshot = editor.snapshot(cx);
10196 let all_hunks = editor_hunks(editor, &snapshot, cx);
10197 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
10198 assert_eq!(
10199 all_hunks,
10200 vec![("".to_string(), DiffHunkStatus::Added, 4..7)]
10201 );
10202 assert_eq!(
10203 expanded_hunks_background_highlights(editor, &snapshot),
10204 vec![4..7]
10205 );
10206 assert_eq!(all_hunks, all_expanded_hunks);
10207 });
10208
10209 cx.update_editor(|editor, cx| editor.handle_input("const D: u32 = 42;\n", cx));
10210 executor.run_until_parked();
10211 cx.assert_editor_state(
10212 &r#"
10213 use some::mod1;
10214 use some::mod2;
10215
10216 const A: u32 = 42;
10217 const B: u32 = 42;
10218 const C: u32 = 42;
10219 const D: u32 = 42;
10220 ˇ
10221
10222 fn main() {
10223 println!("hello");
10224
10225 println!("world");
10226 }
10227 "#
10228 .unindent(),
10229 );
10230 cx.update_editor(|editor, cx| {
10231 let snapshot = editor.snapshot(cx);
10232 let all_hunks = editor_hunks(editor, &snapshot, cx);
10233 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
10234 assert_eq!(
10235 all_hunks,
10236 vec![("".to_string(), DiffHunkStatus::Added, 4..8)]
10237 );
10238 assert_eq!(
10239 expanded_hunks_background_highlights(editor, &snapshot),
10240 vec![4..8],
10241 "Edited hunk should have one more line added"
10242 );
10243 assert_eq!(
10244 all_hunks, all_expanded_hunks,
10245 "Expanded hunk should also grow with the addition"
10246 );
10247 });
10248
10249 cx.update_editor(|editor, cx| editor.handle_input("const E: u32 = 42;\n", cx));
10250 executor.run_until_parked();
10251 cx.assert_editor_state(
10252 &r#"
10253 use some::mod1;
10254 use some::mod2;
10255
10256 const A: u32 = 42;
10257 const B: u32 = 42;
10258 const C: u32 = 42;
10259 const D: u32 = 42;
10260 const E: u32 = 42;
10261 ˇ
10262
10263 fn main() {
10264 println!("hello");
10265
10266 println!("world");
10267 }
10268 "#
10269 .unindent(),
10270 );
10271 cx.update_editor(|editor, cx| {
10272 let snapshot = editor.snapshot(cx);
10273 let all_hunks = editor_hunks(editor, &snapshot, cx);
10274 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
10275 assert_eq!(
10276 all_hunks,
10277 vec![("".to_string(), DiffHunkStatus::Added, 4..9)]
10278 );
10279 assert_eq!(
10280 expanded_hunks_background_highlights(editor, &snapshot),
10281 vec![4..9],
10282 "Edited hunk should have one more line added"
10283 );
10284 assert_eq!(all_hunks, all_expanded_hunks);
10285 });
10286
10287 cx.update_editor(|editor, cx| {
10288 editor.move_up(&MoveUp, cx);
10289 editor.delete_line(&DeleteLine, cx);
10290 });
10291 executor.run_until_parked();
10292 cx.assert_editor_state(
10293 &r#"
10294 use some::mod1;
10295 use some::mod2;
10296
10297 const A: u32 = 42;
10298 const B: u32 = 42;
10299 const C: u32 = 42;
10300 const D: u32 = 42;
10301 ˇ
10302
10303 fn main() {
10304 println!("hello");
10305
10306 println!("world");
10307 }
10308 "#
10309 .unindent(),
10310 );
10311 cx.update_editor(|editor, cx| {
10312 let snapshot = editor.snapshot(cx);
10313 let all_hunks = editor_hunks(editor, &snapshot, cx);
10314 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
10315 assert_eq!(
10316 all_hunks,
10317 vec![("".to_string(), DiffHunkStatus::Added, 4..8)]
10318 );
10319 assert_eq!(
10320 expanded_hunks_background_highlights(editor, &snapshot),
10321 vec![4..8],
10322 "Deleting a line should shrint the hunk"
10323 );
10324 assert_eq!(
10325 all_hunks, all_expanded_hunks,
10326 "Expanded hunk should also shrink with the addition"
10327 );
10328 });
10329
10330 cx.update_editor(|editor, cx| {
10331 editor.move_up(&MoveUp, cx);
10332 editor.delete_line(&DeleteLine, cx);
10333 editor.move_up(&MoveUp, cx);
10334 editor.delete_line(&DeleteLine, cx);
10335 editor.move_up(&MoveUp, cx);
10336 editor.delete_line(&DeleteLine, cx);
10337 });
10338 executor.run_until_parked();
10339 cx.assert_editor_state(
10340 &r#"
10341 use some::mod1;
10342 use some::mod2;
10343
10344 const A: u32 = 42;
10345 ˇ
10346
10347 fn main() {
10348 println!("hello");
10349
10350 println!("world");
10351 }
10352 "#
10353 .unindent(),
10354 );
10355 cx.update_editor(|editor, cx| {
10356 let snapshot = editor.snapshot(cx);
10357 let all_hunks = editor_hunks(editor, &snapshot, cx);
10358 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
10359 assert_eq!(
10360 all_hunks,
10361 vec![("".to_string(), DiffHunkStatus::Added, 5..6)]
10362 );
10363 assert_eq!(
10364 expanded_hunks_background_highlights(editor, &snapshot),
10365 vec![5..6]
10366 );
10367 assert_eq!(all_hunks, all_expanded_hunks);
10368 });
10369
10370 cx.update_editor(|editor, cx| {
10371 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, cx);
10372 editor.delete_line(&DeleteLine, cx);
10373 });
10374 executor.run_until_parked();
10375 cx.assert_editor_state(
10376 &r#"
10377 ˇ
10378
10379 fn main() {
10380 println!("hello");
10381
10382 println!("world");
10383 }
10384 "#
10385 .unindent(),
10386 );
10387 cx.update_editor(|editor, cx| {
10388 let snapshot = editor.snapshot(cx);
10389 let all_hunks = editor_hunks(editor, &snapshot, cx);
10390 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
10391 assert_eq!(
10392 all_hunks,
10393 vec![
10394 (
10395 "use some::mod1;\nuse some::mod2;\n".to_string(),
10396 DiffHunkStatus::Removed,
10397 0..0
10398 ),
10399 (
10400 "const A: u32 = 42;\n".to_string(),
10401 DiffHunkStatus::Removed,
10402 2..2
10403 )
10404 ]
10405 );
10406 assert_eq!(
10407 expanded_hunks_background_highlights(editor, &snapshot),
10408 Vec::new(),
10409 "Should close all stale expanded addition hunks"
10410 );
10411 assert_eq!(
10412 all_expanded_hunks,
10413 vec![(
10414 "const A: u32 = 42;\n".to_string(),
10415 DiffHunkStatus::Removed,
10416 2..2
10417 )],
10418 "Should open hunks that were adjacent to the stale addition one"
10419 );
10420 });
10421}
10422
10423#[gpui::test]
10424async fn test_edits_around_toggled_deletions(
10425 executor: BackgroundExecutor,
10426 cx: &mut gpui::TestAppContext,
10427) {
10428 init_test(cx, |_| {});
10429
10430 let mut cx = EditorTestContext::new(cx).await;
10431
10432 let diff_base = r#"
10433 use some::mod1;
10434 use some::mod2;
10435
10436 const A: u32 = 42;
10437 const B: u32 = 42;
10438 const C: u32 = 42;
10439
10440
10441 fn main() {
10442 println!("hello");
10443
10444 println!("world");
10445 }
10446 "#
10447 .unindent();
10448 executor.run_until_parked();
10449 cx.set_state(
10450 &r#"
10451 use some::mod1;
10452 use some::mod2;
10453
10454 ˇconst B: u32 = 42;
10455 const C: u32 = 42;
10456
10457
10458 fn main() {
10459 println!("hello");
10460
10461 println!("world");
10462 }
10463 "#
10464 .unindent(),
10465 );
10466
10467 cx.set_diff_base(Some(&diff_base));
10468 executor.run_until_parked();
10469 cx.update_editor(|editor, cx| {
10470 let snapshot = editor.snapshot(cx);
10471 let all_hunks = editor_hunks(editor, &snapshot, cx);
10472 assert_eq!(
10473 all_hunks,
10474 vec![(
10475 "const A: u32 = 42;\n".to_string(),
10476 DiffHunkStatus::Removed,
10477 3..3
10478 )]
10479 );
10480 });
10481 cx.update_editor(|editor, cx| {
10482 editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx);
10483 });
10484 executor.run_until_parked();
10485 cx.assert_editor_state(
10486 &r#"
10487 use some::mod1;
10488 use some::mod2;
10489
10490 ˇconst B: u32 = 42;
10491 const C: u32 = 42;
10492
10493
10494 fn main() {
10495 println!("hello");
10496
10497 println!("world");
10498 }
10499 "#
10500 .unindent(),
10501 );
10502 cx.update_editor(|editor, cx| {
10503 let snapshot = editor.snapshot(cx);
10504 let all_hunks = editor_hunks(editor, &snapshot, cx);
10505 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
10506 assert_eq!(
10507 expanded_hunks_background_highlights(editor, &snapshot),
10508 Vec::new()
10509 );
10510 assert_eq!(
10511 all_hunks,
10512 vec![(
10513 "const A: u32 = 42;\n".to_string(),
10514 DiffHunkStatus::Removed,
10515 4..4
10516 )]
10517 );
10518 assert_eq!(all_hunks, all_expanded_hunks);
10519 });
10520
10521 cx.update_editor(|editor, cx| {
10522 editor.delete_line(&DeleteLine, cx);
10523 });
10524 executor.run_until_parked();
10525 cx.assert_editor_state(
10526 &r#"
10527 use some::mod1;
10528 use some::mod2;
10529
10530 ˇconst C: u32 = 42;
10531
10532
10533 fn main() {
10534 println!("hello");
10535
10536 println!("world");
10537 }
10538 "#
10539 .unindent(),
10540 );
10541 cx.update_editor(|editor, cx| {
10542 let snapshot = editor.snapshot(cx);
10543 let all_hunks = editor_hunks(editor, &snapshot, cx);
10544 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
10545 assert_eq!(
10546 expanded_hunks_background_highlights(editor, &snapshot),
10547 Vec::new(),
10548 "Deleted hunks do not highlight current editor's background"
10549 );
10550 assert_eq!(
10551 all_hunks,
10552 vec![(
10553 "const A: u32 = 42;\nconst B: u32 = 42;\n".to_string(),
10554 DiffHunkStatus::Removed,
10555 5..5
10556 )]
10557 );
10558 assert_eq!(all_hunks, all_expanded_hunks);
10559 });
10560
10561 cx.update_editor(|editor, cx| {
10562 editor.delete_line(&DeleteLine, cx);
10563 });
10564 executor.run_until_parked();
10565 cx.assert_editor_state(
10566 &r#"
10567 use some::mod1;
10568 use some::mod2;
10569
10570 ˇ
10571
10572 fn main() {
10573 println!("hello");
10574
10575 println!("world");
10576 }
10577 "#
10578 .unindent(),
10579 );
10580 cx.update_editor(|editor, cx| {
10581 let snapshot = editor.snapshot(cx);
10582 let all_hunks = editor_hunks(editor, &snapshot, cx);
10583 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
10584 assert_eq!(
10585 expanded_hunks_background_highlights(editor, &snapshot),
10586 Vec::new()
10587 );
10588 assert_eq!(
10589 all_hunks,
10590 vec![(
10591 "const A: u32 = 42;\nconst B: u32 = 42;\nconst C: u32 = 42;\n".to_string(),
10592 DiffHunkStatus::Removed,
10593 6..6
10594 )]
10595 );
10596 assert_eq!(all_hunks, all_expanded_hunks);
10597 });
10598
10599 cx.update_editor(|editor, cx| {
10600 editor.handle_input("replacement", cx);
10601 });
10602 executor.run_until_parked();
10603 cx.assert_editor_state(
10604 &r#"
10605 use some::mod1;
10606 use some::mod2;
10607
10608 replacementˇ
10609
10610 fn main() {
10611 println!("hello");
10612
10613 println!("world");
10614 }
10615 "#
10616 .unindent(),
10617 );
10618 cx.update_editor(|editor, cx| {
10619 let snapshot = editor.snapshot(cx);
10620 let all_hunks = editor_hunks(editor, &snapshot, cx);
10621 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
10622 assert_eq!(
10623 all_hunks,
10624 vec![(
10625 "const A: u32 = 42;\nconst B: u32 = 42;\nconst C: u32 = 42;\n\n".to_string(),
10626 DiffHunkStatus::Modified,
10627 7..8
10628 )]
10629 );
10630 assert_eq!(
10631 expanded_hunks_background_highlights(editor, &snapshot),
10632 vec![7..8],
10633 "Modified expanded hunks should display additions and highlight their background"
10634 );
10635 assert_eq!(all_hunks, all_expanded_hunks);
10636 });
10637}
10638
10639#[gpui::test]
10640async fn test_edits_around_toggled_modifications(
10641 executor: BackgroundExecutor,
10642 cx: &mut gpui::TestAppContext,
10643) {
10644 init_test(cx, |_| {});
10645
10646 let mut cx = EditorTestContext::new(cx).await;
10647
10648 let diff_base = r#"
10649 use some::mod1;
10650 use some::mod2;
10651
10652 const A: u32 = 42;
10653 const B: u32 = 42;
10654 const C: u32 = 42;
10655 const D: u32 = 42;
10656
10657
10658 fn main() {
10659 println!("hello");
10660
10661 println!("world");
10662 }"#
10663 .unindent();
10664 executor.run_until_parked();
10665 cx.set_state(
10666 &r#"
10667 use some::mod1;
10668 use some::mod2;
10669
10670 const A: u32 = 42;
10671 const B: u32 = 42;
10672 const C: u32 = 43ˇ
10673 const D: u32 = 42;
10674
10675
10676 fn main() {
10677 println!("hello");
10678
10679 println!("world");
10680 }"#
10681 .unindent(),
10682 );
10683
10684 cx.set_diff_base(Some(&diff_base));
10685 executor.run_until_parked();
10686 cx.update_editor(|editor, cx| {
10687 let snapshot = editor.snapshot(cx);
10688 let all_hunks = editor_hunks(editor, &snapshot, cx);
10689 assert_eq!(
10690 all_hunks,
10691 vec![(
10692 "const C: u32 = 42;\n".to_string(),
10693 DiffHunkStatus::Modified,
10694 5..6
10695 )]
10696 );
10697 });
10698 cx.update_editor(|editor, cx| {
10699 editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx);
10700 });
10701 executor.run_until_parked();
10702 cx.assert_editor_state(
10703 &r#"
10704 use some::mod1;
10705 use some::mod2;
10706
10707 const A: u32 = 42;
10708 const B: u32 = 42;
10709 const C: u32 = 43ˇ
10710 const D: u32 = 42;
10711
10712
10713 fn main() {
10714 println!("hello");
10715
10716 println!("world");
10717 }"#
10718 .unindent(),
10719 );
10720 cx.update_editor(|editor, cx| {
10721 let snapshot = editor.snapshot(cx);
10722 let all_hunks = editor_hunks(editor, &snapshot, cx);
10723 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
10724 assert_eq!(
10725 expanded_hunks_background_highlights(editor, &snapshot),
10726 vec![6..7],
10727 );
10728 assert_eq!(
10729 all_hunks,
10730 vec![(
10731 "const C: u32 = 42;\n".to_string(),
10732 DiffHunkStatus::Modified,
10733 6..7
10734 )]
10735 );
10736 assert_eq!(all_hunks, all_expanded_hunks);
10737 });
10738
10739 cx.update_editor(|editor, cx| {
10740 editor.handle_input("\nnew_line\n", cx);
10741 });
10742 executor.run_until_parked();
10743 cx.assert_editor_state(
10744 &r#"
10745 use some::mod1;
10746 use some::mod2;
10747
10748 const A: u32 = 42;
10749 const B: u32 = 42;
10750 const C: u32 = 43
10751 new_line
10752 ˇ
10753 const D: u32 = 42;
10754
10755
10756 fn main() {
10757 println!("hello");
10758
10759 println!("world");
10760 }"#
10761 .unindent(),
10762 );
10763 cx.update_editor(|editor, cx| {
10764 let snapshot = editor.snapshot(cx);
10765 let all_hunks = editor_hunks(editor, &snapshot, cx);
10766 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
10767 assert_eq!(
10768 expanded_hunks_background_highlights(editor, &snapshot),
10769 vec![6..9],
10770 "Modified hunk should grow highlighted lines on more text additions"
10771 );
10772 assert_eq!(
10773 all_hunks,
10774 vec![(
10775 "const C: u32 = 42;\n".to_string(),
10776 DiffHunkStatus::Modified,
10777 6..9
10778 )]
10779 );
10780 assert_eq!(all_hunks, all_expanded_hunks);
10781 });
10782
10783 cx.update_editor(|editor, cx| {
10784 editor.move_up(&MoveUp, cx);
10785 editor.move_up(&MoveUp, cx);
10786 editor.move_up(&MoveUp, cx);
10787 editor.delete_line(&DeleteLine, cx);
10788 });
10789 executor.run_until_parked();
10790 cx.assert_editor_state(
10791 &r#"
10792 use some::mod1;
10793 use some::mod2;
10794
10795 const A: u32 = 42;
10796 ˇconst C: u32 = 43
10797 new_line
10798
10799 const D: u32 = 42;
10800
10801
10802 fn main() {
10803 println!("hello");
10804
10805 println!("world");
10806 }"#
10807 .unindent(),
10808 );
10809 cx.update_editor(|editor, cx| {
10810 let snapshot = editor.snapshot(cx);
10811 let all_hunks = editor_hunks(editor, &snapshot, cx);
10812 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
10813 assert_eq!(
10814 expanded_hunks_background_highlights(editor, &snapshot),
10815 vec![6..9],
10816 "Modified hunk should grow deleted lines on text deletions above"
10817 );
10818 assert_eq!(
10819 all_hunks,
10820 vec![(
10821 "const B: u32 = 42;\nconst C: u32 = 42;\n".to_string(),
10822 DiffHunkStatus::Modified,
10823 6..9
10824 )]
10825 );
10826 assert_eq!(all_hunks, all_expanded_hunks);
10827 });
10828
10829 cx.update_editor(|editor, cx| {
10830 editor.move_up(&MoveUp, cx);
10831 editor.handle_input("v", cx);
10832 });
10833 executor.run_until_parked();
10834 cx.assert_editor_state(
10835 &r#"
10836 use some::mod1;
10837 use some::mod2;
10838
10839 vˇconst A: u32 = 42;
10840 const C: u32 = 43
10841 new_line
10842
10843 const D: u32 = 42;
10844
10845
10846 fn main() {
10847 println!("hello");
10848
10849 println!("world");
10850 }"#
10851 .unindent(),
10852 );
10853 cx.update_editor(|editor, cx| {
10854 let snapshot = editor.snapshot(cx);
10855 let all_hunks = editor_hunks(editor, &snapshot, cx);
10856 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
10857 assert_eq!(
10858 expanded_hunks_background_highlights(editor, &snapshot),
10859 vec![6..10],
10860 "Modified hunk should grow deleted lines on text modifications above"
10861 );
10862 assert_eq!(
10863 all_hunks,
10864 vec![(
10865 "const A: u32 = 42;\nconst B: u32 = 42;\nconst C: u32 = 42;\n".to_string(),
10866 DiffHunkStatus::Modified,
10867 6..10
10868 )]
10869 );
10870 assert_eq!(all_hunks, all_expanded_hunks);
10871 });
10872
10873 cx.update_editor(|editor, cx| {
10874 editor.move_down(&MoveDown, cx);
10875 editor.move_down(&MoveDown, cx);
10876 editor.delete_line(&DeleteLine, cx)
10877 });
10878 executor.run_until_parked();
10879 cx.assert_editor_state(
10880 &r#"
10881 use some::mod1;
10882 use some::mod2;
10883
10884 vconst A: u32 = 42;
10885 const C: u32 = 43
10886 ˇ
10887 const D: u32 = 42;
10888
10889
10890 fn main() {
10891 println!("hello");
10892
10893 println!("world");
10894 }"#
10895 .unindent(),
10896 );
10897 cx.update_editor(|editor, cx| {
10898 let snapshot = editor.snapshot(cx);
10899 let all_hunks = editor_hunks(editor, &snapshot, cx);
10900 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
10901 assert_eq!(
10902 expanded_hunks_background_highlights(editor, &snapshot),
10903 vec![6..9],
10904 "Modified hunk should grow shrink lines on modification lines removal"
10905 );
10906 assert_eq!(
10907 all_hunks,
10908 vec![(
10909 "const A: u32 = 42;\nconst B: u32 = 42;\nconst C: u32 = 42;\n".to_string(),
10910 DiffHunkStatus::Modified,
10911 6..9
10912 )]
10913 );
10914 assert_eq!(all_hunks, all_expanded_hunks);
10915 });
10916
10917 cx.update_editor(|editor, cx| {
10918 editor.move_up(&MoveUp, cx);
10919 editor.move_up(&MoveUp, cx);
10920 editor.select_down_by_lines(&SelectDownByLines { lines: 4 }, cx);
10921 editor.delete_line(&DeleteLine, cx)
10922 });
10923 executor.run_until_parked();
10924 cx.assert_editor_state(
10925 &r#"
10926 use some::mod1;
10927 use some::mod2;
10928
10929 ˇ
10930
10931 fn main() {
10932 println!("hello");
10933
10934 println!("world");
10935 }"#
10936 .unindent(),
10937 );
10938 cx.update_editor(|editor, cx| {
10939 let snapshot = editor.snapshot(cx);
10940 let all_hunks = editor_hunks(editor, &snapshot, cx);
10941 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
10942 assert_eq!(
10943 expanded_hunks_background_highlights(editor, &snapshot),
10944 Vec::new(),
10945 "Modified hunk should turn into a removed one on all modified lines removal"
10946 );
10947 assert_eq!(
10948 all_hunks,
10949 vec![(
10950 "const A: u32 = 42;\nconst B: u32 = 42;\nconst C: u32 = 42;\nconst D: u32 = 42;\n"
10951 .to_string(),
10952 DiffHunkStatus::Removed,
10953 7..7
10954 )]
10955 );
10956 assert_eq!(all_hunks, all_expanded_hunks);
10957 });
10958}
10959
10960#[gpui::test]
10961async fn test_multiple_expanded_hunks_merge(
10962 executor: BackgroundExecutor,
10963 cx: &mut gpui::TestAppContext,
10964) {
10965 init_test(cx, |_| {});
10966
10967 let mut cx = EditorTestContext::new(cx).await;
10968
10969 let diff_base = r#"
10970 use some::mod1;
10971 use some::mod2;
10972
10973 const A: u32 = 42;
10974 const B: u32 = 42;
10975 const C: u32 = 42;
10976 const D: u32 = 42;
10977
10978
10979 fn main() {
10980 println!("hello");
10981
10982 println!("world");
10983 }"#
10984 .unindent();
10985 executor.run_until_parked();
10986 cx.set_state(
10987 &r#"
10988 use some::mod1;
10989 use some::mod2;
10990
10991 const A: u32 = 42;
10992 const B: u32 = 42;
10993 const C: u32 = 43ˇ
10994 const D: u32 = 42;
10995
10996
10997 fn main() {
10998 println!("hello");
10999
11000 println!("world");
11001 }"#
11002 .unindent(),
11003 );
11004
11005 cx.set_diff_base(Some(&diff_base));
11006 executor.run_until_parked();
11007 cx.update_editor(|editor, cx| {
11008 let snapshot = editor.snapshot(cx);
11009 let all_hunks = editor_hunks(editor, &snapshot, cx);
11010 assert_eq!(
11011 all_hunks,
11012 vec![(
11013 "const C: u32 = 42;\n".to_string(),
11014 DiffHunkStatus::Modified,
11015 5..6
11016 )]
11017 );
11018 });
11019 cx.update_editor(|editor, cx| {
11020 editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx);
11021 });
11022 executor.run_until_parked();
11023 cx.assert_editor_state(
11024 &r#"
11025 use some::mod1;
11026 use some::mod2;
11027
11028 const A: u32 = 42;
11029 const B: u32 = 42;
11030 const C: u32 = 43ˇ
11031 const D: u32 = 42;
11032
11033
11034 fn main() {
11035 println!("hello");
11036
11037 println!("world");
11038 }"#
11039 .unindent(),
11040 );
11041 cx.update_editor(|editor, cx| {
11042 let snapshot = editor.snapshot(cx);
11043 let all_hunks = editor_hunks(editor, &snapshot, cx);
11044 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
11045 assert_eq!(
11046 expanded_hunks_background_highlights(editor, &snapshot),
11047 vec![6..7],
11048 );
11049 assert_eq!(
11050 all_hunks,
11051 vec![(
11052 "const C: u32 = 42;\n".to_string(),
11053 DiffHunkStatus::Modified,
11054 6..7
11055 )]
11056 );
11057 assert_eq!(all_hunks, all_expanded_hunks);
11058 });
11059
11060 cx.update_editor(|editor, cx| {
11061 editor.handle_input("\nnew_line\n", cx);
11062 });
11063 executor.run_until_parked();
11064 cx.assert_editor_state(
11065 &r#"
11066 use some::mod1;
11067 use some::mod2;
11068
11069 const A: u32 = 42;
11070 const B: u32 = 42;
11071 const C: u32 = 43
11072 new_line
11073 ˇ
11074 const D: u32 = 42;
11075
11076
11077 fn main() {
11078 println!("hello");
11079
11080 println!("world");
11081 }"#
11082 .unindent(),
11083 );
11084}
11085
11086fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
11087 let point = DisplayPoint::new(row as u32, column as u32);
11088 point..point
11089}
11090
11091fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewContext<Editor>) {
11092 let (text, ranges) = marked_text_ranges(marked_text, true);
11093 assert_eq!(view.text(cx), text);
11094 assert_eq!(
11095 view.selections.ranges(cx),
11096 ranges,
11097 "Assert selections are {}",
11098 marked_text
11099 );
11100}
11101
11102/// Handle completion request passing a marked string specifying where the completion
11103/// should be triggered from using '|' character, what range should be replaced, and what completions
11104/// should be returned using '<' and '>' to delimit the range
11105pub fn handle_completion_request(
11106 cx: &mut EditorLspTestContext,
11107 marked_string: &str,
11108 completions: Vec<&'static str>,
11109) -> impl Future<Output = ()> {
11110 let complete_from_marker: TextRangeMarker = '|'.into();
11111 let replace_range_marker: TextRangeMarker = ('<', '>').into();
11112 let (_, mut marked_ranges) = marked_text_ranges_by(
11113 marked_string,
11114 vec![complete_from_marker.clone(), replace_range_marker.clone()],
11115 );
11116
11117 let complete_from_position =
11118 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
11119 let replace_range =
11120 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
11121
11122 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
11123 let completions = completions.clone();
11124 async move {
11125 assert_eq!(params.text_document_position.text_document.uri, url.clone());
11126 assert_eq!(
11127 params.text_document_position.position,
11128 complete_from_position
11129 );
11130 Ok(Some(lsp::CompletionResponse::Array(
11131 completions
11132 .iter()
11133 .map(|completion_text| lsp::CompletionItem {
11134 label: completion_text.to_string(),
11135 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11136 range: replace_range,
11137 new_text: completion_text.to_string(),
11138 })),
11139 ..Default::default()
11140 })
11141 .collect(),
11142 )))
11143 }
11144 });
11145
11146 async move {
11147 request.next().await;
11148 }
11149}
11150
11151fn handle_resolve_completion_request(
11152 cx: &mut EditorLspTestContext,
11153 edits: Option<Vec<(&'static str, &'static str)>>,
11154) -> impl Future<Output = ()> {
11155 let edits = edits.map(|edits| {
11156 edits
11157 .iter()
11158 .map(|(marked_string, new_text)| {
11159 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
11160 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
11161 lsp::TextEdit::new(replace_range, new_text.to_string())
11162 })
11163 .collect::<Vec<_>>()
11164 });
11165
11166 let mut request =
11167 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
11168 let edits = edits.clone();
11169 async move {
11170 Ok(lsp::CompletionItem {
11171 additional_text_edits: edits,
11172 ..Default::default()
11173 })
11174 }
11175 });
11176
11177 async move {
11178 request.next().await;
11179 }
11180}
11181
11182pub(crate) fn update_test_language_settings(
11183 cx: &mut TestAppContext,
11184 f: impl Fn(&mut AllLanguageSettingsContent),
11185) {
11186 _ = cx.update(|cx| {
11187 cx.update_global(|store: &mut SettingsStore, cx| {
11188 store.update_user_settings::<AllLanguageSettings>(cx, f);
11189 });
11190 });
11191}
11192
11193pub(crate) fn update_test_project_settings(
11194 cx: &mut TestAppContext,
11195 f: impl Fn(&mut ProjectSettings),
11196) {
11197 _ = cx.update(|cx| {
11198 cx.update_global(|store: &mut SettingsStore, cx| {
11199 store.update_user_settings::<ProjectSettings>(cx, f);
11200 });
11201 });
11202}
11203
11204pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
11205 _ = cx.update(|cx| {
11206 let store = SettingsStore::test(cx);
11207 cx.set_global(store);
11208 theme::init(theme::LoadThemes::JustBase, cx);
11209 release_channel::init("0.0.0", cx);
11210 client::init_settings(cx);
11211 language::init(cx);
11212 Project::init_settings(cx);
11213 workspace::init_settings(cx);
11214 crate::init(cx);
11215 });
11216
11217 update_test_language_settings(cx, f);
11218}
11219
11220pub(crate) fn rust_lang() -> Arc<Language> {
11221 Arc::new(Language::new(
11222 LanguageConfig {
11223 name: "Rust".into(),
11224 matcher: LanguageMatcher {
11225 path_suffixes: vec!["rs".to_string()],
11226 ..Default::default()
11227 },
11228 ..Default::default()
11229 },
11230 Some(tree_sitter_rust::language()),
11231 ))
11232}
11233
11234#[track_caller]
11235fn assert_hunk_revert(
11236 not_reverted_text_with_selections: &str,
11237 expected_not_reverted_hunk_statuses: Vec<DiffHunkStatus>,
11238 expected_reverted_text_with_selections: &str,
11239 base_text: &str,
11240 cx: &mut EditorLspTestContext,
11241) {
11242 cx.set_state(not_reverted_text_with_selections);
11243 cx.update_editor(|editor, cx| {
11244 editor
11245 .buffer()
11246 .read(cx)
11247 .as_singleton()
11248 .unwrap()
11249 .update(cx, |buffer, cx| {
11250 buffer.set_diff_base(Some(base_text.to_string()), cx);
11251 });
11252 });
11253 cx.executor().run_until_parked();
11254
11255 let reverted_hunk_statuses = cx.update_editor(|editor, cx| {
11256 let snapshot = editor
11257 .buffer()
11258 .read(cx)
11259 .as_singleton()
11260 .unwrap()
11261 .read(cx)
11262 .snapshot();
11263 let reverted_hunk_statuses = snapshot
11264 .git_diff_hunks_in_row_range(0..u32::MAX)
11265 .map(|hunk| hunk.status())
11266 .collect::<Vec<_>>();
11267
11268 editor.revert_selected_hunks(&RevertSelectedHunks, cx);
11269 reverted_hunk_statuses
11270 });
11271 cx.executor().run_until_parked();
11272 cx.assert_editor_state(expected_reverted_text_with_selections);
11273 assert_eq!(reverted_hunk_statuses, expected_not_reverted_hunk_statuses);
11274}