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