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