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