1use super::*;
2use crate::{
3 scroll::scroll_amount::ScrollAmount,
4 test::{
5 assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
6 editor_test_context::EditorTestContext, select_ranges,
7 },
8 JoinLines,
9};
10use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
11use futures::StreamExt;
12use gpui::{
13 div, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
14 WindowBounds, WindowOptions,
15};
16use indoc::indoc;
17use language::{
18 language_settings::{
19 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
20 LanguageSettingsContent, PrettierSettings,
21 },
22 BracketPairConfig,
23 Capability::ReadWrite,
24 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
25 Override, Point,
26};
27use language_settings::{Formatter, FormatterList, IndentGuideSettings};
28use multi_buffer::{IndentGuide, PathKey};
29use parking_lot::Mutex;
30use pretty_assertions::{assert_eq, assert_ne};
31use project::project_settings::{LspSettings, ProjectSettings};
32use project::FakeFs;
33use serde_json::{self, json};
34use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
35use std::{
36 iter,
37 sync::atomic::{self, AtomicUsize},
38};
39use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
40use text::ToPoint as _;
41use unindent::Unindent;
42use util::{
43 assert_set_eq, path,
44 test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
45 uri,
46};
47use workspace::{
48 item::{FollowEvent, FollowableItem, Item, ItemHandle},
49 NavigationEntry, ViewId,
50};
51
52#[gpui::test]
53fn test_edit_events(cx: &mut TestAppContext) {
54 init_test(cx, |_| {});
55
56 let buffer = cx.new(|cx| {
57 let mut buffer = language::Buffer::local("123456", cx);
58 buffer.set_group_interval(Duration::from_secs(1));
59 buffer
60 });
61
62 let events = Rc::new(RefCell::new(Vec::new()));
63 let editor1 = cx.add_window({
64 let events = events.clone();
65 |window, cx| {
66 let entity = cx.entity().clone();
67 cx.subscribe_in(
68 &entity,
69 window,
70 move |_, _, event: &EditorEvent, _, _| match event {
71 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
72 EditorEvent::BufferEdited => {
73 events.borrow_mut().push(("editor1", "buffer edited"))
74 }
75 _ => {}
76 },
77 )
78 .detach();
79 Editor::for_buffer(buffer.clone(), None, window, cx)
80 }
81 });
82
83 let editor2 = cx.add_window({
84 let events = events.clone();
85 |window, cx| {
86 cx.subscribe_in(
87 &cx.entity().clone(),
88 window,
89 move |_, _, event: &EditorEvent, _, _| match event {
90 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
91 EditorEvent::BufferEdited => {
92 events.borrow_mut().push(("editor2", "buffer edited"))
93 }
94 _ => {}
95 },
96 )
97 .detach();
98 Editor::for_buffer(buffer.clone(), None, window, cx)
99 }
100 });
101
102 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
103
104 // Mutating editor 1 will emit an `Edited` event only for that editor.
105 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
106 assert_eq!(
107 mem::take(&mut *events.borrow_mut()),
108 [
109 ("editor1", "edited"),
110 ("editor1", "buffer edited"),
111 ("editor2", "buffer edited"),
112 ]
113 );
114
115 // Mutating editor 2 will emit an `Edited` event only for that editor.
116 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
117 assert_eq!(
118 mem::take(&mut *events.borrow_mut()),
119 [
120 ("editor2", "edited"),
121 ("editor1", "buffer edited"),
122 ("editor2", "buffer edited"),
123 ]
124 );
125
126 // Undoing on editor 1 will emit an `Edited` event only for that editor.
127 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
128 assert_eq!(
129 mem::take(&mut *events.borrow_mut()),
130 [
131 ("editor1", "edited"),
132 ("editor1", "buffer edited"),
133 ("editor2", "buffer edited"),
134 ]
135 );
136
137 // Redoing on editor 1 will emit an `Edited` event only for that editor.
138 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
139 assert_eq!(
140 mem::take(&mut *events.borrow_mut()),
141 [
142 ("editor1", "edited"),
143 ("editor1", "buffer edited"),
144 ("editor2", "buffer edited"),
145 ]
146 );
147
148 // Undoing on editor 2 will emit an `Edited` event only for that editor.
149 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
150 assert_eq!(
151 mem::take(&mut *events.borrow_mut()),
152 [
153 ("editor2", "edited"),
154 ("editor1", "buffer edited"),
155 ("editor2", "buffer edited"),
156 ]
157 );
158
159 // Redoing on editor 2 will emit an `Edited` event only for that editor.
160 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
161 assert_eq!(
162 mem::take(&mut *events.borrow_mut()),
163 [
164 ("editor2", "edited"),
165 ("editor1", "buffer edited"),
166 ("editor2", "buffer edited"),
167 ]
168 );
169
170 // No event is emitted when the mutation is a no-op.
171 _ = editor2.update(cx, |editor, window, cx| {
172 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
173
174 editor.backspace(&Backspace, window, cx);
175 });
176 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
177}
178
179#[gpui::test]
180fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
181 init_test(cx, |_| {});
182
183 let mut now = Instant::now();
184 let group_interval = Duration::from_millis(1);
185 let buffer = cx.new(|cx| {
186 let mut buf = language::Buffer::local("123456", cx);
187 buf.set_group_interval(group_interval);
188 buf
189 });
190 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
191 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
192
193 _ = editor.update(cx, |editor, window, cx| {
194 editor.start_transaction_at(now, window, cx);
195 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
196
197 editor.insert("cd", window, cx);
198 editor.end_transaction_at(now, cx);
199 assert_eq!(editor.text(cx), "12cd56");
200 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
201
202 editor.start_transaction_at(now, window, cx);
203 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
204 editor.insert("e", window, cx);
205 editor.end_transaction_at(now, cx);
206 assert_eq!(editor.text(cx), "12cde6");
207 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
208
209 now += group_interval + Duration::from_millis(1);
210 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
211
212 // Simulate an edit in another editor
213 buffer.update(cx, |buffer, cx| {
214 buffer.start_transaction_at(now, cx);
215 buffer.edit([(0..1, "a")], None, cx);
216 buffer.edit([(1..1, "b")], None, cx);
217 buffer.end_transaction_at(now, cx);
218 });
219
220 assert_eq!(editor.text(cx), "ab2cde6");
221 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
222
223 // Last transaction happened past the group interval in a different editor.
224 // Undo it individually and don't restore selections.
225 editor.undo(&Undo, window, cx);
226 assert_eq!(editor.text(cx), "12cde6");
227 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
228
229 // First two transactions happened within the group interval in this editor.
230 // Undo them together and restore selections.
231 editor.undo(&Undo, window, cx);
232 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
233 assert_eq!(editor.text(cx), "123456");
234 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
235
236 // Redo the first two transactions together.
237 editor.redo(&Redo, window, cx);
238 assert_eq!(editor.text(cx), "12cde6");
239 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
240
241 // Redo the last transaction on its own.
242 editor.redo(&Redo, window, cx);
243 assert_eq!(editor.text(cx), "ab2cde6");
244 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
245
246 // Test empty transactions.
247 editor.start_transaction_at(now, window, cx);
248 editor.end_transaction_at(now, cx);
249 editor.undo(&Undo, window, cx);
250 assert_eq!(editor.text(cx), "12cde6");
251 });
252}
253
254#[gpui::test]
255fn test_ime_composition(cx: &mut TestAppContext) {
256 init_test(cx, |_| {});
257
258 let buffer = cx.new(|cx| {
259 let mut buffer = language::Buffer::local("abcde", cx);
260 // Ensure automatic grouping doesn't occur.
261 buffer.set_group_interval(Duration::ZERO);
262 buffer
263 });
264
265 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
266 cx.add_window(|window, cx| {
267 let mut editor = build_editor(buffer.clone(), window, cx);
268
269 // Start a new IME composition.
270 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
271 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
272 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
273 assert_eq!(editor.text(cx), "äbcde");
274 assert_eq!(
275 editor.marked_text_ranges(cx),
276 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
277 );
278
279 // Finalize IME composition.
280 editor.replace_text_in_range(None, "ā", window, cx);
281 assert_eq!(editor.text(cx), "ābcde");
282 assert_eq!(editor.marked_text_ranges(cx), None);
283
284 // IME composition edits are grouped and are undone/redone at once.
285 editor.undo(&Default::default(), window, cx);
286 assert_eq!(editor.text(cx), "abcde");
287 assert_eq!(editor.marked_text_ranges(cx), None);
288 editor.redo(&Default::default(), window, cx);
289 assert_eq!(editor.text(cx), "ābcde");
290 assert_eq!(editor.marked_text_ranges(cx), None);
291
292 // Start a new IME composition.
293 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
294 assert_eq!(
295 editor.marked_text_ranges(cx),
296 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
297 );
298
299 // Undoing during an IME composition cancels it.
300 editor.undo(&Default::default(), window, cx);
301 assert_eq!(editor.text(cx), "ābcde");
302 assert_eq!(editor.marked_text_ranges(cx), None);
303
304 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
305 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
306 assert_eq!(editor.text(cx), "ābcdè");
307 assert_eq!(
308 editor.marked_text_ranges(cx),
309 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
310 );
311
312 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
313 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
314 assert_eq!(editor.text(cx), "ābcdę");
315 assert_eq!(editor.marked_text_ranges(cx), None);
316
317 // Start a new IME composition with multiple cursors.
318 editor.change_selections(None, window, cx, |s| {
319 s.select_ranges([
320 OffsetUtf16(1)..OffsetUtf16(1),
321 OffsetUtf16(3)..OffsetUtf16(3),
322 OffsetUtf16(5)..OffsetUtf16(5),
323 ])
324 });
325 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
326 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
327 assert_eq!(
328 editor.marked_text_ranges(cx),
329 Some(vec![
330 OffsetUtf16(0)..OffsetUtf16(3),
331 OffsetUtf16(4)..OffsetUtf16(7),
332 OffsetUtf16(8)..OffsetUtf16(11)
333 ])
334 );
335
336 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
337 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
338 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
339 assert_eq!(
340 editor.marked_text_ranges(cx),
341 Some(vec![
342 OffsetUtf16(1)..OffsetUtf16(2),
343 OffsetUtf16(5)..OffsetUtf16(6),
344 OffsetUtf16(9)..OffsetUtf16(10)
345 ])
346 );
347
348 // Finalize IME composition with multiple cursors.
349 editor.replace_text_in_range(Some(9..10), "2", window, cx);
350 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
351 assert_eq!(editor.marked_text_ranges(cx), None);
352
353 editor
354 });
355}
356
357#[gpui::test]
358fn test_selection_with_mouse(cx: &mut TestAppContext) {
359 init_test(cx, |_| {});
360
361 let editor = cx.add_window(|window, cx| {
362 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
363 build_editor(buffer, window, cx)
364 });
365
366 _ = editor.update(cx, |editor, window, cx| {
367 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
368 });
369 assert_eq!(
370 editor
371 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
372 .unwrap(),
373 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
374 );
375
376 _ = editor.update(cx, |editor, window, cx| {
377 editor.update_selection(
378 DisplayPoint::new(DisplayRow(3), 3),
379 0,
380 gpui::Point::<f32>::default(),
381 window,
382 cx,
383 );
384 });
385
386 assert_eq!(
387 editor
388 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
389 .unwrap(),
390 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
391 );
392
393 _ = editor.update(cx, |editor, window, cx| {
394 editor.update_selection(
395 DisplayPoint::new(DisplayRow(1), 1),
396 0,
397 gpui::Point::<f32>::default(),
398 window,
399 cx,
400 );
401 });
402
403 assert_eq!(
404 editor
405 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
406 .unwrap(),
407 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
408 );
409
410 _ = editor.update(cx, |editor, window, cx| {
411 editor.end_selection(window, cx);
412 editor.update_selection(
413 DisplayPoint::new(DisplayRow(3), 3),
414 0,
415 gpui::Point::<f32>::default(),
416 window,
417 cx,
418 );
419 });
420
421 assert_eq!(
422 editor
423 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
424 .unwrap(),
425 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
426 );
427
428 _ = editor.update(cx, |editor, window, cx| {
429 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
430 editor.update_selection(
431 DisplayPoint::new(DisplayRow(0), 0),
432 0,
433 gpui::Point::<f32>::default(),
434 window,
435 cx,
436 );
437 });
438
439 assert_eq!(
440 editor
441 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
442 .unwrap(),
443 [
444 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
445 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
446 ]
447 );
448
449 _ = editor.update(cx, |editor, window, cx| {
450 editor.end_selection(window, cx);
451 });
452
453 assert_eq!(
454 editor
455 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
456 .unwrap(),
457 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
458 );
459}
460
461#[gpui::test]
462fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
463 init_test(cx, |_| {});
464
465 let editor = cx.add_window(|window, cx| {
466 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
467 build_editor(buffer, window, cx)
468 });
469
470 _ = editor.update(cx, |editor, window, cx| {
471 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
472 });
473
474 _ = editor.update(cx, |editor, window, cx| {
475 editor.end_selection(window, cx);
476 });
477
478 _ = editor.update(cx, |editor, window, cx| {
479 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
480 });
481
482 _ = editor.update(cx, |editor, window, cx| {
483 editor.end_selection(window, cx);
484 });
485
486 assert_eq!(
487 editor
488 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
489 .unwrap(),
490 [
491 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
492 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
493 ]
494 );
495
496 _ = editor.update(cx, |editor, window, cx| {
497 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
498 });
499
500 _ = editor.update(cx, |editor, window, cx| {
501 editor.end_selection(window, cx);
502 });
503
504 assert_eq!(
505 editor
506 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
507 .unwrap(),
508 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
509 );
510}
511
512#[gpui::test]
513fn test_canceling_pending_selection(cx: &mut TestAppContext) {
514 init_test(cx, |_| {});
515
516 let editor = cx.add_window(|window, cx| {
517 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
518 build_editor(buffer, window, cx)
519 });
520
521 _ = editor.update(cx, |editor, window, cx| {
522 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
523 assert_eq!(
524 editor.selections.display_ranges(cx),
525 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
526 );
527 });
528
529 _ = editor.update(cx, |editor, window, cx| {
530 editor.update_selection(
531 DisplayPoint::new(DisplayRow(3), 3),
532 0,
533 gpui::Point::<f32>::default(),
534 window,
535 cx,
536 );
537 assert_eq!(
538 editor.selections.display_ranges(cx),
539 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
540 );
541 });
542
543 _ = editor.update(cx, |editor, window, cx| {
544 editor.cancel(&Cancel, window, cx);
545 editor.update_selection(
546 DisplayPoint::new(DisplayRow(1), 1),
547 0,
548 gpui::Point::<f32>::default(),
549 window,
550 cx,
551 );
552 assert_eq!(
553 editor.selections.display_ranges(cx),
554 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
555 );
556 });
557}
558
559#[gpui::test]
560fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
561 init_test(cx, |_| {});
562
563 let editor = cx.add_window(|window, cx| {
564 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
565 build_editor(buffer, window, cx)
566 });
567
568 _ = editor.update(cx, |editor, window, cx| {
569 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
570 assert_eq!(
571 editor.selections.display_ranges(cx),
572 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
573 );
574
575 editor.move_down(&Default::default(), window, cx);
576 assert_eq!(
577 editor.selections.display_ranges(cx),
578 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
579 );
580
581 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
582 assert_eq!(
583 editor.selections.display_ranges(cx),
584 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
585 );
586
587 editor.move_up(&Default::default(), window, cx);
588 assert_eq!(
589 editor.selections.display_ranges(cx),
590 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
591 );
592 });
593}
594
595#[gpui::test]
596fn test_clone(cx: &mut TestAppContext) {
597 init_test(cx, |_| {});
598
599 let (text, selection_ranges) = marked_text_ranges(
600 indoc! {"
601 one
602 two
603 threeˇ
604 four
605 fiveˇ
606 "},
607 true,
608 );
609
610 let editor = cx.add_window(|window, cx| {
611 let buffer = MultiBuffer::build_simple(&text, cx);
612 build_editor(buffer, window, cx)
613 });
614
615 _ = editor.update(cx, |editor, window, cx| {
616 editor.change_selections(None, window, cx, |s| {
617 s.select_ranges(selection_ranges.clone())
618 });
619 editor.fold_creases(
620 vec![
621 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
622 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
623 ],
624 true,
625 window,
626 cx,
627 );
628 });
629
630 let cloned_editor = editor
631 .update(cx, |editor, _, cx| {
632 cx.open_window(Default::default(), |window, cx| {
633 cx.new(|cx| editor.clone(window, cx))
634 })
635 })
636 .unwrap()
637 .unwrap();
638
639 let snapshot = editor
640 .update(cx, |e, window, cx| e.snapshot(window, cx))
641 .unwrap();
642 let cloned_snapshot = cloned_editor
643 .update(cx, |e, window, cx| e.snapshot(window, cx))
644 .unwrap();
645
646 assert_eq!(
647 cloned_editor
648 .update(cx, |e, _, cx| e.display_text(cx))
649 .unwrap(),
650 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
651 );
652 assert_eq!(
653 cloned_snapshot
654 .folds_in_range(0..text.len())
655 .collect::<Vec<_>>(),
656 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
657 );
658 assert_set_eq!(
659 cloned_editor
660 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
661 .unwrap(),
662 editor
663 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
664 .unwrap()
665 );
666 assert_set_eq!(
667 cloned_editor
668 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
669 .unwrap(),
670 editor
671 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
672 .unwrap()
673 );
674}
675
676#[gpui::test]
677async fn test_navigation_history(cx: &mut TestAppContext) {
678 init_test(cx, |_| {});
679
680 use workspace::item::Item;
681
682 let fs = FakeFs::new(cx.executor());
683 let project = Project::test(fs, [], cx).await;
684 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
685 let pane = workspace
686 .update(cx, |workspace, _, _| workspace.active_pane().clone())
687 .unwrap();
688
689 _ = workspace.update(cx, |_v, window, cx| {
690 cx.new(|cx| {
691 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
692 let mut editor = build_editor(buffer.clone(), window, cx);
693 let handle = cx.entity();
694 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
695
696 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
697 editor.nav_history.as_mut().unwrap().pop_backward(cx)
698 }
699
700 // Move the cursor a small distance.
701 // Nothing is added to the navigation history.
702 editor.change_selections(None, window, cx, |s| {
703 s.select_display_ranges([
704 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
705 ])
706 });
707 editor.change_selections(None, window, cx, |s| {
708 s.select_display_ranges([
709 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
710 ])
711 });
712 assert!(pop_history(&mut editor, cx).is_none());
713
714 // Move the cursor a large distance.
715 // The history can jump back to the previous position.
716 editor.change_selections(None, window, cx, |s| {
717 s.select_display_ranges([
718 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
719 ])
720 });
721 let nav_entry = pop_history(&mut editor, cx).unwrap();
722 editor.navigate(nav_entry.data.unwrap(), window, cx);
723 assert_eq!(nav_entry.item.id(), cx.entity_id());
724 assert_eq!(
725 editor.selections.display_ranges(cx),
726 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
727 );
728 assert!(pop_history(&mut editor, cx).is_none());
729
730 // Move the cursor a small distance via the mouse.
731 // Nothing is added to the navigation history.
732 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
733 editor.end_selection(window, cx);
734 assert_eq!(
735 editor.selections.display_ranges(cx),
736 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
737 );
738 assert!(pop_history(&mut editor, cx).is_none());
739
740 // Move the cursor a large distance via the mouse.
741 // The history can jump back to the previous position.
742 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
743 editor.end_selection(window, cx);
744 assert_eq!(
745 editor.selections.display_ranges(cx),
746 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
747 );
748 let nav_entry = pop_history(&mut editor, cx).unwrap();
749 editor.navigate(nav_entry.data.unwrap(), window, cx);
750 assert_eq!(nav_entry.item.id(), cx.entity_id());
751 assert_eq!(
752 editor.selections.display_ranges(cx),
753 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
754 );
755 assert!(pop_history(&mut editor, cx).is_none());
756
757 // Set scroll position to check later
758 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
759 let original_scroll_position = editor.scroll_manager.anchor();
760
761 // Jump to the end of the document and adjust scroll
762 editor.move_to_end(&MoveToEnd, window, cx);
763 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
764 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
765
766 let nav_entry = pop_history(&mut editor, cx).unwrap();
767 editor.navigate(nav_entry.data.unwrap(), window, cx);
768 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
769
770 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
771 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
772 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
773 let invalid_point = Point::new(9999, 0);
774 editor.navigate(
775 Box::new(NavigationData {
776 cursor_anchor: invalid_anchor,
777 cursor_position: invalid_point,
778 scroll_anchor: ScrollAnchor {
779 anchor: invalid_anchor,
780 offset: Default::default(),
781 },
782 scroll_top_row: invalid_point.row,
783 }),
784 window,
785 cx,
786 );
787 assert_eq!(
788 editor.selections.display_ranges(cx),
789 &[editor.max_point(cx)..editor.max_point(cx)]
790 );
791 assert_eq!(
792 editor.scroll_position(cx),
793 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
794 );
795
796 editor
797 })
798 });
799}
800
801#[gpui::test]
802fn test_cancel(cx: &mut TestAppContext) {
803 init_test(cx, |_| {});
804
805 let editor = cx.add_window(|window, cx| {
806 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
807 build_editor(buffer, window, cx)
808 });
809
810 _ = editor.update(cx, |editor, window, cx| {
811 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
812 editor.update_selection(
813 DisplayPoint::new(DisplayRow(1), 1),
814 0,
815 gpui::Point::<f32>::default(),
816 window,
817 cx,
818 );
819 editor.end_selection(window, cx);
820
821 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
822 editor.update_selection(
823 DisplayPoint::new(DisplayRow(0), 3),
824 0,
825 gpui::Point::<f32>::default(),
826 window,
827 cx,
828 );
829 editor.end_selection(window, cx);
830 assert_eq!(
831 editor.selections.display_ranges(cx),
832 [
833 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
834 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
835 ]
836 );
837 });
838
839 _ = editor.update(cx, |editor, window, cx| {
840 editor.cancel(&Cancel, window, cx);
841 assert_eq!(
842 editor.selections.display_ranges(cx),
843 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
844 );
845 });
846
847 _ = editor.update(cx, |editor, window, cx| {
848 editor.cancel(&Cancel, window, cx);
849 assert_eq!(
850 editor.selections.display_ranges(cx),
851 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
852 );
853 });
854}
855
856#[gpui::test]
857fn test_fold_action(cx: &mut TestAppContext) {
858 init_test(cx, |_| {});
859
860 let editor = cx.add_window(|window, cx| {
861 let buffer = MultiBuffer::build_simple(
862 &"
863 impl Foo {
864 // Hello!
865
866 fn a() {
867 1
868 }
869
870 fn b() {
871 2
872 }
873
874 fn c() {
875 3
876 }
877 }
878 "
879 .unindent(),
880 cx,
881 );
882 build_editor(buffer.clone(), window, cx)
883 });
884
885 _ = editor.update(cx, |editor, window, cx| {
886 editor.change_selections(None, window, cx, |s| {
887 s.select_display_ranges([
888 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
889 ]);
890 });
891 editor.fold(&Fold, window, cx);
892 assert_eq!(
893 editor.display_text(cx),
894 "
895 impl Foo {
896 // Hello!
897
898 fn a() {
899 1
900 }
901
902 fn b() {⋯
903 }
904
905 fn c() {⋯
906 }
907 }
908 "
909 .unindent(),
910 );
911
912 editor.fold(&Fold, window, cx);
913 assert_eq!(
914 editor.display_text(cx),
915 "
916 impl Foo {⋯
917 }
918 "
919 .unindent(),
920 );
921
922 editor.unfold_lines(&UnfoldLines, window, cx);
923 assert_eq!(
924 editor.display_text(cx),
925 "
926 impl Foo {
927 // Hello!
928
929 fn a() {
930 1
931 }
932
933 fn b() {⋯
934 }
935
936 fn c() {⋯
937 }
938 }
939 "
940 .unindent(),
941 );
942
943 editor.unfold_lines(&UnfoldLines, window, cx);
944 assert_eq!(
945 editor.display_text(cx),
946 editor.buffer.read(cx).read(cx).text()
947 );
948 });
949}
950
951#[gpui::test]
952fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
953 init_test(cx, |_| {});
954
955 let editor = cx.add_window(|window, cx| {
956 let buffer = MultiBuffer::build_simple(
957 &"
958 class Foo:
959 # Hello!
960
961 def a():
962 print(1)
963
964 def b():
965 print(2)
966
967 def c():
968 print(3)
969 "
970 .unindent(),
971 cx,
972 );
973 build_editor(buffer.clone(), window, cx)
974 });
975
976 _ = editor.update(cx, |editor, window, cx| {
977 editor.change_selections(None, window, cx, |s| {
978 s.select_display_ranges([
979 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
980 ]);
981 });
982 editor.fold(&Fold, window, cx);
983 assert_eq!(
984 editor.display_text(cx),
985 "
986 class Foo:
987 # Hello!
988
989 def a():
990 print(1)
991
992 def b():⋯
993
994 def c():⋯
995 "
996 .unindent(),
997 );
998
999 editor.fold(&Fold, window, cx);
1000 assert_eq!(
1001 editor.display_text(cx),
1002 "
1003 class Foo:⋯
1004 "
1005 .unindent(),
1006 );
1007
1008 editor.unfold_lines(&UnfoldLines, window, cx);
1009 assert_eq!(
1010 editor.display_text(cx),
1011 "
1012 class Foo:
1013 # Hello!
1014
1015 def a():
1016 print(1)
1017
1018 def b():⋯
1019
1020 def c():⋯
1021 "
1022 .unindent(),
1023 );
1024
1025 editor.unfold_lines(&UnfoldLines, window, cx);
1026 assert_eq!(
1027 editor.display_text(cx),
1028 editor.buffer.read(cx).read(cx).text()
1029 );
1030 });
1031}
1032
1033#[gpui::test]
1034fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1035 init_test(cx, |_| {});
1036
1037 let editor = cx.add_window(|window, cx| {
1038 let buffer = MultiBuffer::build_simple(
1039 &"
1040 class Foo:
1041 # Hello!
1042
1043 def a():
1044 print(1)
1045
1046 def b():
1047 print(2)
1048
1049
1050 def c():
1051 print(3)
1052
1053
1054 "
1055 .unindent(),
1056 cx,
1057 );
1058 build_editor(buffer.clone(), window, cx)
1059 });
1060
1061 _ = editor.update(cx, |editor, window, cx| {
1062 editor.change_selections(None, window, cx, |s| {
1063 s.select_display_ranges([
1064 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1065 ]);
1066 });
1067 editor.fold(&Fold, window, cx);
1068 assert_eq!(
1069 editor.display_text(cx),
1070 "
1071 class Foo:
1072 # Hello!
1073
1074 def a():
1075 print(1)
1076
1077 def b():⋯
1078
1079
1080 def c():⋯
1081
1082
1083 "
1084 .unindent(),
1085 );
1086
1087 editor.fold(&Fold, window, cx);
1088 assert_eq!(
1089 editor.display_text(cx),
1090 "
1091 class Foo:⋯
1092
1093
1094 "
1095 .unindent(),
1096 );
1097
1098 editor.unfold_lines(&UnfoldLines, window, cx);
1099 assert_eq!(
1100 editor.display_text(cx),
1101 "
1102 class Foo:
1103 # Hello!
1104
1105 def a():
1106 print(1)
1107
1108 def b():⋯
1109
1110
1111 def c():⋯
1112
1113
1114 "
1115 .unindent(),
1116 );
1117
1118 editor.unfold_lines(&UnfoldLines, window, cx);
1119 assert_eq!(
1120 editor.display_text(cx),
1121 editor.buffer.read(cx).read(cx).text()
1122 );
1123 });
1124}
1125
1126#[gpui::test]
1127fn test_fold_at_level(cx: &mut TestAppContext) {
1128 init_test(cx, |_| {});
1129
1130 let editor = cx.add_window(|window, cx| {
1131 let buffer = MultiBuffer::build_simple(
1132 &"
1133 class Foo:
1134 # Hello!
1135
1136 def a():
1137 print(1)
1138
1139 def b():
1140 print(2)
1141
1142
1143 class Bar:
1144 # World!
1145
1146 def a():
1147 print(1)
1148
1149 def b():
1150 print(2)
1151
1152
1153 "
1154 .unindent(),
1155 cx,
1156 );
1157 build_editor(buffer.clone(), window, cx)
1158 });
1159
1160 _ = editor.update(cx, |editor, window, cx| {
1161 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1162 assert_eq!(
1163 editor.display_text(cx),
1164 "
1165 class Foo:
1166 # Hello!
1167
1168 def a():⋯
1169
1170 def b():⋯
1171
1172
1173 class Bar:
1174 # World!
1175
1176 def a():⋯
1177
1178 def b():⋯
1179
1180
1181 "
1182 .unindent(),
1183 );
1184
1185 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1186 assert_eq!(
1187 editor.display_text(cx),
1188 "
1189 class Foo:⋯
1190
1191
1192 class Bar:⋯
1193
1194
1195 "
1196 .unindent(),
1197 );
1198
1199 editor.unfold_all(&UnfoldAll, window, cx);
1200 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1201 assert_eq!(
1202 editor.display_text(cx),
1203 "
1204 class Foo:
1205 # Hello!
1206
1207 def a():
1208 print(1)
1209
1210 def b():
1211 print(2)
1212
1213
1214 class Bar:
1215 # World!
1216
1217 def a():
1218 print(1)
1219
1220 def b():
1221 print(2)
1222
1223
1224 "
1225 .unindent(),
1226 );
1227
1228 assert_eq!(
1229 editor.display_text(cx),
1230 editor.buffer.read(cx).read(cx).text()
1231 );
1232 });
1233}
1234
1235#[gpui::test]
1236fn test_move_cursor(cx: &mut TestAppContext) {
1237 init_test(cx, |_| {});
1238
1239 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1240 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1241
1242 buffer.update(cx, |buffer, cx| {
1243 buffer.edit(
1244 vec![
1245 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1246 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1247 ],
1248 None,
1249 cx,
1250 );
1251 });
1252 _ = editor.update(cx, |editor, window, cx| {
1253 assert_eq!(
1254 editor.selections.display_ranges(cx),
1255 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1256 );
1257
1258 editor.move_down(&MoveDown, window, cx);
1259 assert_eq!(
1260 editor.selections.display_ranges(cx),
1261 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1262 );
1263
1264 editor.move_right(&MoveRight, window, cx);
1265 assert_eq!(
1266 editor.selections.display_ranges(cx),
1267 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1268 );
1269
1270 editor.move_left(&MoveLeft, window, cx);
1271 assert_eq!(
1272 editor.selections.display_ranges(cx),
1273 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1274 );
1275
1276 editor.move_up(&MoveUp, window, cx);
1277 assert_eq!(
1278 editor.selections.display_ranges(cx),
1279 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1280 );
1281
1282 editor.move_to_end(&MoveToEnd, window, cx);
1283 assert_eq!(
1284 editor.selections.display_ranges(cx),
1285 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1286 );
1287
1288 editor.move_to_beginning(&MoveToBeginning, window, cx);
1289 assert_eq!(
1290 editor.selections.display_ranges(cx),
1291 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1292 );
1293
1294 editor.change_selections(None, window, cx, |s| {
1295 s.select_display_ranges([
1296 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1297 ]);
1298 });
1299 editor.select_to_beginning(&SelectToBeginning, window, cx);
1300 assert_eq!(
1301 editor.selections.display_ranges(cx),
1302 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1303 );
1304
1305 editor.select_to_end(&SelectToEnd, window, cx);
1306 assert_eq!(
1307 editor.selections.display_ranges(cx),
1308 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1309 );
1310 });
1311}
1312
1313// TODO: Re-enable this test
1314#[cfg(target_os = "macos")]
1315#[gpui::test]
1316fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1317 init_test(cx, |_| {});
1318
1319 let editor = cx.add_window(|window, cx| {
1320 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1321 build_editor(buffer.clone(), window, cx)
1322 });
1323
1324 assert_eq!('🟥'.len_utf8(), 4);
1325 assert_eq!('α'.len_utf8(), 2);
1326
1327 _ = editor.update(cx, |editor, window, cx| {
1328 editor.fold_creases(
1329 vec![
1330 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1331 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1332 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1333 ],
1334 true,
1335 window,
1336 cx,
1337 );
1338 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1339
1340 editor.move_right(&MoveRight, window, cx);
1341 assert_eq!(
1342 editor.selections.display_ranges(cx),
1343 &[empty_range(0, "🟥".len())]
1344 );
1345 editor.move_right(&MoveRight, window, cx);
1346 assert_eq!(
1347 editor.selections.display_ranges(cx),
1348 &[empty_range(0, "🟥🟧".len())]
1349 );
1350 editor.move_right(&MoveRight, window, cx);
1351 assert_eq!(
1352 editor.selections.display_ranges(cx),
1353 &[empty_range(0, "🟥🟧⋯".len())]
1354 );
1355
1356 editor.move_down(&MoveDown, window, cx);
1357 assert_eq!(
1358 editor.selections.display_ranges(cx),
1359 &[empty_range(1, "ab⋯e".len())]
1360 );
1361 editor.move_left(&MoveLeft, window, cx);
1362 assert_eq!(
1363 editor.selections.display_ranges(cx),
1364 &[empty_range(1, "ab⋯".len())]
1365 );
1366 editor.move_left(&MoveLeft, window, cx);
1367 assert_eq!(
1368 editor.selections.display_ranges(cx),
1369 &[empty_range(1, "ab".len())]
1370 );
1371 editor.move_left(&MoveLeft, window, cx);
1372 assert_eq!(
1373 editor.selections.display_ranges(cx),
1374 &[empty_range(1, "a".len())]
1375 );
1376
1377 editor.move_down(&MoveDown, window, cx);
1378 assert_eq!(
1379 editor.selections.display_ranges(cx),
1380 &[empty_range(2, "α".len())]
1381 );
1382 editor.move_right(&MoveRight, window, cx);
1383 assert_eq!(
1384 editor.selections.display_ranges(cx),
1385 &[empty_range(2, "αβ".len())]
1386 );
1387 editor.move_right(&MoveRight, window, cx);
1388 assert_eq!(
1389 editor.selections.display_ranges(cx),
1390 &[empty_range(2, "αβ⋯".len())]
1391 );
1392 editor.move_right(&MoveRight, window, cx);
1393 assert_eq!(
1394 editor.selections.display_ranges(cx),
1395 &[empty_range(2, "αβ⋯ε".len())]
1396 );
1397
1398 editor.move_up(&MoveUp, window, cx);
1399 assert_eq!(
1400 editor.selections.display_ranges(cx),
1401 &[empty_range(1, "ab⋯e".len())]
1402 );
1403 editor.move_down(&MoveDown, window, cx);
1404 assert_eq!(
1405 editor.selections.display_ranges(cx),
1406 &[empty_range(2, "αβ⋯ε".len())]
1407 );
1408 editor.move_up(&MoveUp, window, cx);
1409 assert_eq!(
1410 editor.selections.display_ranges(cx),
1411 &[empty_range(1, "ab⋯e".len())]
1412 );
1413
1414 editor.move_up(&MoveUp, window, cx);
1415 assert_eq!(
1416 editor.selections.display_ranges(cx),
1417 &[empty_range(0, "🟥🟧".len())]
1418 );
1419 editor.move_left(&MoveLeft, window, cx);
1420 assert_eq!(
1421 editor.selections.display_ranges(cx),
1422 &[empty_range(0, "🟥".len())]
1423 );
1424 editor.move_left(&MoveLeft, window, cx);
1425 assert_eq!(
1426 editor.selections.display_ranges(cx),
1427 &[empty_range(0, "".len())]
1428 );
1429 });
1430}
1431
1432#[gpui::test]
1433fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1434 init_test(cx, |_| {});
1435
1436 let editor = cx.add_window(|window, cx| {
1437 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1438 build_editor(buffer.clone(), window, cx)
1439 });
1440 _ = editor.update(cx, |editor, window, cx| {
1441 editor.change_selections(None, window, cx, |s| {
1442 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1443 });
1444
1445 // moving above start of document should move selection to start of document,
1446 // but the next move down should still be at the original goal_x
1447 editor.move_up(&MoveUp, window, cx);
1448 assert_eq!(
1449 editor.selections.display_ranges(cx),
1450 &[empty_range(0, "".len())]
1451 );
1452
1453 editor.move_down(&MoveDown, window, cx);
1454 assert_eq!(
1455 editor.selections.display_ranges(cx),
1456 &[empty_range(1, "abcd".len())]
1457 );
1458
1459 editor.move_down(&MoveDown, window, cx);
1460 assert_eq!(
1461 editor.selections.display_ranges(cx),
1462 &[empty_range(2, "αβγ".len())]
1463 );
1464
1465 editor.move_down(&MoveDown, window, cx);
1466 assert_eq!(
1467 editor.selections.display_ranges(cx),
1468 &[empty_range(3, "abcd".len())]
1469 );
1470
1471 editor.move_down(&MoveDown, window, cx);
1472 assert_eq!(
1473 editor.selections.display_ranges(cx),
1474 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1475 );
1476
1477 // moving past end of document should not change goal_x
1478 editor.move_down(&MoveDown, window, cx);
1479 assert_eq!(
1480 editor.selections.display_ranges(cx),
1481 &[empty_range(5, "".len())]
1482 );
1483
1484 editor.move_down(&MoveDown, window, cx);
1485 assert_eq!(
1486 editor.selections.display_ranges(cx),
1487 &[empty_range(5, "".len())]
1488 );
1489
1490 editor.move_up(&MoveUp, window, cx);
1491 assert_eq!(
1492 editor.selections.display_ranges(cx),
1493 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1494 );
1495
1496 editor.move_up(&MoveUp, window, cx);
1497 assert_eq!(
1498 editor.selections.display_ranges(cx),
1499 &[empty_range(3, "abcd".len())]
1500 );
1501
1502 editor.move_up(&MoveUp, window, cx);
1503 assert_eq!(
1504 editor.selections.display_ranges(cx),
1505 &[empty_range(2, "αβγ".len())]
1506 );
1507 });
1508}
1509
1510#[gpui::test]
1511fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1512 init_test(cx, |_| {});
1513 let move_to_beg = MoveToBeginningOfLine {
1514 stop_at_soft_wraps: true,
1515 stop_at_indent: true,
1516 };
1517
1518 let delete_to_beg = DeleteToBeginningOfLine {
1519 stop_at_indent: false,
1520 };
1521
1522 let move_to_end = MoveToEndOfLine {
1523 stop_at_soft_wraps: true,
1524 };
1525
1526 let editor = cx.add_window(|window, cx| {
1527 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1528 build_editor(buffer, window, cx)
1529 });
1530 _ = editor.update(cx, |editor, window, cx| {
1531 editor.change_selections(None, window, cx, |s| {
1532 s.select_display_ranges([
1533 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1534 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1535 ]);
1536 });
1537 });
1538
1539 _ = editor.update(cx, |editor, window, cx| {
1540 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1541 assert_eq!(
1542 editor.selections.display_ranges(cx),
1543 &[
1544 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1545 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1546 ]
1547 );
1548 });
1549
1550 _ = editor.update(cx, |editor, window, cx| {
1551 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1552 assert_eq!(
1553 editor.selections.display_ranges(cx),
1554 &[
1555 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1556 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1557 ]
1558 );
1559 });
1560
1561 _ = editor.update(cx, |editor, window, cx| {
1562 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1563 assert_eq!(
1564 editor.selections.display_ranges(cx),
1565 &[
1566 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1567 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1568 ]
1569 );
1570 });
1571
1572 _ = editor.update(cx, |editor, window, cx| {
1573 editor.move_to_end_of_line(&move_to_end, window, cx);
1574 assert_eq!(
1575 editor.selections.display_ranges(cx),
1576 &[
1577 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1578 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1579 ]
1580 );
1581 });
1582
1583 // Moving to the end of line again is a no-op.
1584 _ = editor.update(cx, |editor, window, cx| {
1585 editor.move_to_end_of_line(&move_to_end, window, cx);
1586 assert_eq!(
1587 editor.selections.display_ranges(cx),
1588 &[
1589 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1590 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1591 ]
1592 );
1593 });
1594
1595 _ = editor.update(cx, |editor, window, cx| {
1596 editor.move_left(&MoveLeft, window, cx);
1597 editor.select_to_beginning_of_line(
1598 &SelectToBeginningOfLine {
1599 stop_at_soft_wraps: true,
1600 stop_at_indent: true,
1601 },
1602 window,
1603 cx,
1604 );
1605 assert_eq!(
1606 editor.selections.display_ranges(cx),
1607 &[
1608 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1609 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1610 ]
1611 );
1612 });
1613
1614 _ = editor.update(cx, |editor, window, cx| {
1615 editor.select_to_beginning_of_line(
1616 &SelectToBeginningOfLine {
1617 stop_at_soft_wraps: true,
1618 stop_at_indent: true,
1619 },
1620 window,
1621 cx,
1622 );
1623 assert_eq!(
1624 editor.selections.display_ranges(cx),
1625 &[
1626 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1627 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1628 ]
1629 );
1630 });
1631
1632 _ = editor.update(cx, |editor, window, cx| {
1633 editor.select_to_beginning_of_line(
1634 &SelectToBeginningOfLine {
1635 stop_at_soft_wraps: true,
1636 stop_at_indent: true,
1637 },
1638 window,
1639 cx,
1640 );
1641 assert_eq!(
1642 editor.selections.display_ranges(cx),
1643 &[
1644 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1645 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1646 ]
1647 );
1648 });
1649
1650 _ = editor.update(cx, |editor, window, cx| {
1651 editor.select_to_end_of_line(
1652 &SelectToEndOfLine {
1653 stop_at_soft_wraps: true,
1654 },
1655 window,
1656 cx,
1657 );
1658 assert_eq!(
1659 editor.selections.display_ranges(cx),
1660 &[
1661 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1662 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1663 ]
1664 );
1665 });
1666
1667 _ = editor.update(cx, |editor, window, cx| {
1668 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1669 assert_eq!(editor.display_text(cx), "ab\n de");
1670 assert_eq!(
1671 editor.selections.display_ranges(cx),
1672 &[
1673 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1674 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1675 ]
1676 );
1677 });
1678
1679 _ = editor.update(cx, |editor, window, cx| {
1680 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1681 assert_eq!(editor.display_text(cx), "\n");
1682 assert_eq!(
1683 editor.selections.display_ranges(cx),
1684 &[
1685 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1686 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1687 ]
1688 );
1689 });
1690}
1691
1692#[gpui::test]
1693fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1694 init_test(cx, |_| {});
1695 let move_to_beg = MoveToBeginningOfLine {
1696 stop_at_soft_wraps: false,
1697 stop_at_indent: false,
1698 };
1699
1700 let move_to_end = MoveToEndOfLine {
1701 stop_at_soft_wraps: false,
1702 };
1703
1704 let editor = cx.add_window(|window, cx| {
1705 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1706 build_editor(buffer, window, cx)
1707 });
1708
1709 _ = editor.update(cx, |editor, window, cx| {
1710 editor.set_wrap_width(Some(140.0.into()), cx);
1711
1712 // We expect the following lines after wrapping
1713 // ```
1714 // thequickbrownfox
1715 // jumpedoverthelazydo
1716 // gs
1717 // ```
1718 // The final `gs` was soft-wrapped onto a new line.
1719 assert_eq!(
1720 "thequickbrownfox\njumpedoverthelaz\nydogs",
1721 editor.display_text(cx),
1722 );
1723
1724 // First, let's assert behavior on the first line, that was not soft-wrapped.
1725 // Start the cursor at the `k` on the first line
1726 editor.change_selections(None, window, cx, |s| {
1727 s.select_display_ranges([
1728 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1729 ]);
1730 });
1731
1732 // Moving to the beginning of the line should put us at the beginning of the line.
1733 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1734 assert_eq!(
1735 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1736 editor.selections.display_ranges(cx)
1737 );
1738
1739 // Moving to the end of the line should put us at the end of the line.
1740 editor.move_to_end_of_line(&move_to_end, window, cx);
1741 assert_eq!(
1742 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1743 editor.selections.display_ranges(cx)
1744 );
1745
1746 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1747 // Start the cursor at the last line (`y` that was wrapped to a new line)
1748 editor.change_selections(None, window, cx, |s| {
1749 s.select_display_ranges([
1750 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1751 ]);
1752 });
1753
1754 // Moving to the beginning of the line should put us at the start of the second line of
1755 // display text, i.e., the `j`.
1756 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1757 assert_eq!(
1758 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1759 editor.selections.display_ranges(cx)
1760 );
1761
1762 // Moving to the beginning of the line again should be a no-op.
1763 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1764 assert_eq!(
1765 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1766 editor.selections.display_ranges(cx)
1767 );
1768
1769 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1770 // next display line.
1771 editor.move_to_end_of_line(&move_to_end, window, cx);
1772 assert_eq!(
1773 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1774 editor.selections.display_ranges(cx)
1775 );
1776
1777 // Moving to the end of the line again should be a no-op.
1778 editor.move_to_end_of_line(&move_to_end, window, cx);
1779 assert_eq!(
1780 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1781 editor.selections.display_ranges(cx)
1782 );
1783 });
1784}
1785
1786#[gpui::test]
1787fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1788 init_test(cx, |_| {});
1789
1790 let move_to_beg = MoveToBeginningOfLine {
1791 stop_at_soft_wraps: true,
1792 stop_at_indent: true,
1793 };
1794
1795 let select_to_beg = SelectToBeginningOfLine {
1796 stop_at_soft_wraps: true,
1797 stop_at_indent: true,
1798 };
1799
1800 let delete_to_beg = DeleteToBeginningOfLine {
1801 stop_at_indent: true,
1802 };
1803
1804 let move_to_end = MoveToEndOfLine {
1805 stop_at_soft_wraps: false,
1806 };
1807
1808 let editor = cx.add_window(|window, cx| {
1809 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1810 build_editor(buffer, window, cx)
1811 });
1812
1813 _ = editor.update(cx, |editor, window, cx| {
1814 editor.change_selections(None, window, cx, |s| {
1815 s.select_display_ranges([
1816 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1817 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1818 ]);
1819 });
1820
1821 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1822 // and the second cursor at the first non-whitespace character in the line.
1823 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1824 assert_eq!(
1825 editor.selections.display_ranges(cx),
1826 &[
1827 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1828 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1829 ]
1830 );
1831
1832 // Moving to the beginning of the line again should be a no-op for the first cursor,
1833 // and should move the second cursor to the beginning of the line.
1834 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1835 assert_eq!(
1836 editor.selections.display_ranges(cx),
1837 &[
1838 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1839 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1840 ]
1841 );
1842
1843 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1844 // and should move the second cursor back to the first non-whitespace character in the line.
1845 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1846 assert_eq!(
1847 editor.selections.display_ranges(cx),
1848 &[
1849 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1850 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1851 ]
1852 );
1853
1854 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1855 // and to the first non-whitespace character in the line for the second cursor.
1856 editor.move_to_end_of_line(&move_to_end, window, cx);
1857 editor.move_left(&MoveLeft, window, cx);
1858 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1859 assert_eq!(
1860 editor.selections.display_ranges(cx),
1861 &[
1862 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1863 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1864 ]
1865 );
1866
1867 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1868 // and should select to the beginning of the line for the second cursor.
1869 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1870 assert_eq!(
1871 editor.selections.display_ranges(cx),
1872 &[
1873 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1874 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1875 ]
1876 );
1877
1878 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1879 // and should delete to the first non-whitespace character in the line for the second cursor.
1880 editor.move_to_end_of_line(&move_to_end, window, cx);
1881 editor.move_left(&MoveLeft, window, cx);
1882 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1883 assert_eq!(editor.text(cx), "c\n f");
1884 });
1885}
1886
1887#[gpui::test]
1888fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1889 init_test(cx, |_| {});
1890
1891 let editor = cx.add_window(|window, cx| {
1892 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1893 build_editor(buffer, window, cx)
1894 });
1895 _ = editor.update(cx, |editor, window, cx| {
1896 editor.change_selections(None, window, cx, |s| {
1897 s.select_display_ranges([
1898 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1899 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1900 ])
1901 });
1902
1903 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1904 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1905
1906 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1907 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1908
1909 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1910 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1911
1912 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1913 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1914
1915 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1916 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1917
1918 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1919 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1920
1921 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1922 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1923
1924 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1925 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1926
1927 editor.move_right(&MoveRight, window, cx);
1928 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1929 assert_selection_ranges(
1930 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1931 editor,
1932 cx,
1933 );
1934
1935 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1936 assert_selection_ranges(
1937 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1938 editor,
1939 cx,
1940 );
1941
1942 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1943 assert_selection_ranges(
1944 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1945 editor,
1946 cx,
1947 );
1948 });
1949}
1950
1951#[gpui::test]
1952fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1953 init_test(cx, |_| {});
1954
1955 let editor = cx.add_window(|window, cx| {
1956 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1957 build_editor(buffer, window, cx)
1958 });
1959
1960 _ = editor.update(cx, |editor, window, cx| {
1961 editor.set_wrap_width(Some(140.0.into()), cx);
1962 assert_eq!(
1963 editor.display_text(cx),
1964 "use one::{\n two::three::\n four::five\n};"
1965 );
1966
1967 editor.change_selections(None, window, cx, |s| {
1968 s.select_display_ranges([
1969 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1970 ]);
1971 });
1972
1973 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1974 assert_eq!(
1975 editor.selections.display_ranges(cx),
1976 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1977 );
1978
1979 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1980 assert_eq!(
1981 editor.selections.display_ranges(cx),
1982 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1983 );
1984
1985 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1986 assert_eq!(
1987 editor.selections.display_ranges(cx),
1988 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1989 );
1990
1991 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1992 assert_eq!(
1993 editor.selections.display_ranges(cx),
1994 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
1995 );
1996
1997 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1998 assert_eq!(
1999 editor.selections.display_ranges(cx),
2000 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2001 );
2002
2003 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2004 assert_eq!(
2005 editor.selections.display_ranges(cx),
2006 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2007 );
2008 });
2009}
2010
2011#[gpui::test]
2012async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2013 init_test(cx, |_| {});
2014 let mut cx = EditorTestContext::new(cx).await;
2015
2016 let line_height = cx.editor(|editor, window, _| {
2017 editor
2018 .style()
2019 .unwrap()
2020 .text
2021 .line_height_in_pixels(window.rem_size())
2022 });
2023 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2024
2025 cx.set_state(
2026 &r#"ˇone
2027 two
2028
2029 three
2030 fourˇ
2031 five
2032
2033 six"#
2034 .unindent(),
2035 );
2036
2037 cx.update_editor(|editor, window, cx| {
2038 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2039 });
2040 cx.assert_editor_state(
2041 &r#"one
2042 two
2043 ˇ
2044 three
2045 four
2046 five
2047 ˇ
2048 six"#
2049 .unindent(),
2050 );
2051
2052 cx.update_editor(|editor, window, cx| {
2053 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2054 });
2055 cx.assert_editor_state(
2056 &r#"one
2057 two
2058
2059 three
2060 four
2061 five
2062 ˇ
2063 sixˇ"#
2064 .unindent(),
2065 );
2066
2067 cx.update_editor(|editor, window, cx| {
2068 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2069 });
2070 cx.assert_editor_state(
2071 &r#"one
2072 two
2073
2074 three
2075 four
2076 five
2077
2078 sixˇ"#
2079 .unindent(),
2080 );
2081
2082 cx.update_editor(|editor, window, cx| {
2083 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2084 });
2085 cx.assert_editor_state(
2086 &r#"one
2087 two
2088
2089 three
2090 four
2091 five
2092 ˇ
2093 six"#
2094 .unindent(),
2095 );
2096
2097 cx.update_editor(|editor, window, cx| {
2098 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2099 });
2100 cx.assert_editor_state(
2101 &r#"one
2102 two
2103 ˇ
2104 three
2105 four
2106 five
2107
2108 six"#
2109 .unindent(),
2110 );
2111
2112 cx.update_editor(|editor, window, cx| {
2113 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2114 });
2115 cx.assert_editor_state(
2116 &r#"ˇone
2117 two
2118
2119 three
2120 four
2121 five
2122
2123 six"#
2124 .unindent(),
2125 );
2126}
2127
2128#[gpui::test]
2129async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2130 init_test(cx, |_| {});
2131 let mut cx = EditorTestContext::new(cx).await;
2132 let line_height = cx.editor(|editor, window, _| {
2133 editor
2134 .style()
2135 .unwrap()
2136 .text
2137 .line_height_in_pixels(window.rem_size())
2138 });
2139 let window = cx.window;
2140 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2141
2142 cx.set_state(
2143 r#"ˇone
2144 two
2145 three
2146 four
2147 five
2148 six
2149 seven
2150 eight
2151 nine
2152 ten
2153 "#,
2154 );
2155
2156 cx.update_editor(|editor, window, cx| {
2157 assert_eq!(
2158 editor.snapshot(window, cx).scroll_position(),
2159 gpui::Point::new(0., 0.)
2160 );
2161 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2162 assert_eq!(
2163 editor.snapshot(window, cx).scroll_position(),
2164 gpui::Point::new(0., 3.)
2165 );
2166 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2167 assert_eq!(
2168 editor.snapshot(window, cx).scroll_position(),
2169 gpui::Point::new(0., 6.)
2170 );
2171 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2172 assert_eq!(
2173 editor.snapshot(window, cx).scroll_position(),
2174 gpui::Point::new(0., 3.)
2175 );
2176
2177 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2178 assert_eq!(
2179 editor.snapshot(window, cx).scroll_position(),
2180 gpui::Point::new(0., 1.)
2181 );
2182 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2183 assert_eq!(
2184 editor.snapshot(window, cx).scroll_position(),
2185 gpui::Point::new(0., 3.)
2186 );
2187 });
2188}
2189
2190#[gpui::test]
2191async fn test_autoscroll(cx: &mut TestAppContext) {
2192 init_test(cx, |_| {});
2193 let mut cx = EditorTestContext::new(cx).await;
2194
2195 let line_height = cx.update_editor(|editor, window, cx| {
2196 editor.set_vertical_scroll_margin(2, cx);
2197 editor
2198 .style()
2199 .unwrap()
2200 .text
2201 .line_height_in_pixels(window.rem_size())
2202 });
2203 let window = cx.window;
2204 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2205
2206 cx.set_state(
2207 r#"ˇone
2208 two
2209 three
2210 four
2211 five
2212 six
2213 seven
2214 eight
2215 nine
2216 ten
2217 "#,
2218 );
2219 cx.update_editor(|editor, window, cx| {
2220 assert_eq!(
2221 editor.snapshot(window, cx).scroll_position(),
2222 gpui::Point::new(0., 0.0)
2223 );
2224 });
2225
2226 // Add a cursor below the visible area. Since both cursors cannot fit
2227 // on screen, the editor autoscrolls to reveal the newest cursor, and
2228 // allows the vertical scroll margin below that cursor.
2229 cx.update_editor(|editor, window, cx| {
2230 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2231 selections.select_ranges([
2232 Point::new(0, 0)..Point::new(0, 0),
2233 Point::new(6, 0)..Point::new(6, 0),
2234 ]);
2235 })
2236 });
2237 cx.update_editor(|editor, window, cx| {
2238 assert_eq!(
2239 editor.snapshot(window, cx).scroll_position(),
2240 gpui::Point::new(0., 3.0)
2241 );
2242 });
2243
2244 // Move down. The editor cursor scrolls down to track the newest cursor.
2245 cx.update_editor(|editor, window, cx| {
2246 editor.move_down(&Default::default(), window, cx);
2247 });
2248 cx.update_editor(|editor, window, cx| {
2249 assert_eq!(
2250 editor.snapshot(window, cx).scroll_position(),
2251 gpui::Point::new(0., 4.0)
2252 );
2253 });
2254
2255 // Add a cursor above the visible area. Since both cursors fit on screen,
2256 // the editor scrolls to show both.
2257 cx.update_editor(|editor, window, cx| {
2258 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2259 selections.select_ranges([
2260 Point::new(1, 0)..Point::new(1, 0),
2261 Point::new(6, 0)..Point::new(6, 0),
2262 ]);
2263 })
2264 });
2265 cx.update_editor(|editor, window, cx| {
2266 assert_eq!(
2267 editor.snapshot(window, cx).scroll_position(),
2268 gpui::Point::new(0., 1.0)
2269 );
2270 });
2271}
2272
2273#[gpui::test]
2274async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2275 init_test(cx, |_| {});
2276 let mut cx = EditorTestContext::new(cx).await;
2277
2278 let line_height = cx.editor(|editor, window, _cx| {
2279 editor
2280 .style()
2281 .unwrap()
2282 .text
2283 .line_height_in_pixels(window.rem_size())
2284 });
2285 let window = cx.window;
2286 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2287 cx.set_state(
2288 &r#"
2289 ˇone
2290 two
2291 threeˇ
2292 four
2293 five
2294 six
2295 seven
2296 eight
2297 nine
2298 ten
2299 "#
2300 .unindent(),
2301 );
2302
2303 cx.update_editor(|editor, window, cx| {
2304 editor.move_page_down(&MovePageDown::default(), window, cx)
2305 });
2306 cx.assert_editor_state(
2307 &r#"
2308 one
2309 two
2310 three
2311 ˇfour
2312 five
2313 sixˇ
2314 seven
2315 eight
2316 nine
2317 ten
2318 "#
2319 .unindent(),
2320 );
2321
2322 cx.update_editor(|editor, window, cx| {
2323 editor.move_page_down(&MovePageDown::default(), window, cx)
2324 });
2325 cx.assert_editor_state(
2326 &r#"
2327 one
2328 two
2329 three
2330 four
2331 five
2332 six
2333 ˇseven
2334 eight
2335 nineˇ
2336 ten
2337 "#
2338 .unindent(),
2339 );
2340
2341 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2342 cx.assert_editor_state(
2343 &r#"
2344 one
2345 two
2346 three
2347 ˇfour
2348 five
2349 sixˇ
2350 seven
2351 eight
2352 nine
2353 ten
2354 "#
2355 .unindent(),
2356 );
2357
2358 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2359 cx.assert_editor_state(
2360 &r#"
2361 ˇone
2362 two
2363 threeˇ
2364 four
2365 five
2366 six
2367 seven
2368 eight
2369 nine
2370 ten
2371 "#
2372 .unindent(),
2373 );
2374
2375 // Test select collapsing
2376 cx.update_editor(|editor, window, cx| {
2377 editor.move_page_down(&MovePageDown::default(), window, cx);
2378 editor.move_page_down(&MovePageDown::default(), window, cx);
2379 editor.move_page_down(&MovePageDown::default(), window, cx);
2380 });
2381 cx.assert_editor_state(
2382 &r#"
2383 one
2384 two
2385 three
2386 four
2387 five
2388 six
2389 seven
2390 eight
2391 nine
2392 ˇten
2393 ˇ"#
2394 .unindent(),
2395 );
2396}
2397
2398#[gpui::test]
2399async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2400 init_test(cx, |_| {});
2401 let mut cx = EditorTestContext::new(cx).await;
2402 cx.set_state("one «two threeˇ» four");
2403 cx.update_editor(|editor, window, cx| {
2404 editor.delete_to_beginning_of_line(
2405 &DeleteToBeginningOfLine {
2406 stop_at_indent: false,
2407 },
2408 window,
2409 cx,
2410 );
2411 assert_eq!(editor.text(cx), " four");
2412 });
2413}
2414
2415#[gpui::test]
2416fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2417 init_test(cx, |_| {});
2418
2419 let editor = cx.add_window(|window, cx| {
2420 let buffer = MultiBuffer::build_simple("one two three four", cx);
2421 build_editor(buffer.clone(), window, cx)
2422 });
2423
2424 _ = editor.update(cx, |editor, window, cx| {
2425 editor.change_selections(None, window, cx, |s| {
2426 s.select_display_ranges([
2427 // an empty selection - the preceding word fragment is deleted
2428 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2429 // characters selected - they are deleted
2430 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2431 ])
2432 });
2433 editor.delete_to_previous_word_start(
2434 &DeleteToPreviousWordStart {
2435 ignore_newlines: false,
2436 },
2437 window,
2438 cx,
2439 );
2440 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2441 });
2442
2443 _ = editor.update(cx, |editor, window, cx| {
2444 editor.change_selections(None, window, cx, |s| {
2445 s.select_display_ranges([
2446 // an empty selection - the following word fragment is deleted
2447 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2448 // characters selected - they are deleted
2449 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2450 ])
2451 });
2452 editor.delete_to_next_word_end(
2453 &DeleteToNextWordEnd {
2454 ignore_newlines: false,
2455 },
2456 window,
2457 cx,
2458 );
2459 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2460 });
2461}
2462
2463#[gpui::test]
2464fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2465 init_test(cx, |_| {});
2466
2467 let editor = cx.add_window(|window, cx| {
2468 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2469 build_editor(buffer.clone(), window, cx)
2470 });
2471 let del_to_prev_word_start = DeleteToPreviousWordStart {
2472 ignore_newlines: false,
2473 };
2474 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2475 ignore_newlines: true,
2476 };
2477
2478 _ = editor.update(cx, |editor, window, cx| {
2479 editor.change_selections(None, window, cx, |s| {
2480 s.select_display_ranges([
2481 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2482 ])
2483 });
2484 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2485 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2486 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2487 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2488 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2489 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2490 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2491 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2492 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2493 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2494 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2495 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2496 });
2497}
2498
2499#[gpui::test]
2500fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2501 init_test(cx, |_| {});
2502
2503 let editor = cx.add_window(|window, cx| {
2504 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2505 build_editor(buffer.clone(), window, cx)
2506 });
2507 let del_to_next_word_end = DeleteToNextWordEnd {
2508 ignore_newlines: false,
2509 };
2510 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2511 ignore_newlines: true,
2512 };
2513
2514 _ = editor.update(cx, |editor, window, cx| {
2515 editor.change_selections(None, window, cx, |s| {
2516 s.select_display_ranges([
2517 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2518 ])
2519 });
2520 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2521 assert_eq!(
2522 editor.buffer.read(cx).read(cx).text(),
2523 "one\n two\nthree\n four"
2524 );
2525 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2526 assert_eq!(
2527 editor.buffer.read(cx).read(cx).text(),
2528 "\n two\nthree\n four"
2529 );
2530 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2531 assert_eq!(
2532 editor.buffer.read(cx).read(cx).text(),
2533 "two\nthree\n four"
2534 );
2535 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2536 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2537 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2538 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2539 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2540 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2541 });
2542}
2543
2544#[gpui::test]
2545fn test_newline(cx: &mut TestAppContext) {
2546 init_test(cx, |_| {});
2547
2548 let editor = cx.add_window(|window, cx| {
2549 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2550 build_editor(buffer.clone(), window, cx)
2551 });
2552
2553 _ = editor.update(cx, |editor, window, cx| {
2554 editor.change_selections(None, window, cx, |s| {
2555 s.select_display_ranges([
2556 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2557 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2558 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2559 ])
2560 });
2561
2562 editor.newline(&Newline, window, cx);
2563 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2564 });
2565}
2566
2567#[gpui::test]
2568fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2569 init_test(cx, |_| {});
2570
2571 let editor = cx.add_window(|window, cx| {
2572 let buffer = MultiBuffer::build_simple(
2573 "
2574 a
2575 b(
2576 X
2577 )
2578 c(
2579 X
2580 )
2581 "
2582 .unindent()
2583 .as_str(),
2584 cx,
2585 );
2586 let mut editor = build_editor(buffer.clone(), window, cx);
2587 editor.change_selections(None, window, cx, |s| {
2588 s.select_ranges([
2589 Point::new(2, 4)..Point::new(2, 5),
2590 Point::new(5, 4)..Point::new(5, 5),
2591 ])
2592 });
2593 editor
2594 });
2595
2596 _ = editor.update(cx, |editor, window, cx| {
2597 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2598 editor.buffer.update(cx, |buffer, cx| {
2599 buffer.edit(
2600 [
2601 (Point::new(1, 2)..Point::new(3, 0), ""),
2602 (Point::new(4, 2)..Point::new(6, 0), ""),
2603 ],
2604 None,
2605 cx,
2606 );
2607 assert_eq!(
2608 buffer.read(cx).text(),
2609 "
2610 a
2611 b()
2612 c()
2613 "
2614 .unindent()
2615 );
2616 });
2617 assert_eq!(
2618 editor.selections.ranges(cx),
2619 &[
2620 Point::new(1, 2)..Point::new(1, 2),
2621 Point::new(2, 2)..Point::new(2, 2),
2622 ],
2623 );
2624
2625 editor.newline(&Newline, window, cx);
2626 assert_eq!(
2627 editor.text(cx),
2628 "
2629 a
2630 b(
2631 )
2632 c(
2633 )
2634 "
2635 .unindent()
2636 );
2637
2638 // The selections are moved after the inserted newlines
2639 assert_eq!(
2640 editor.selections.ranges(cx),
2641 &[
2642 Point::new(2, 0)..Point::new(2, 0),
2643 Point::new(4, 0)..Point::new(4, 0),
2644 ],
2645 );
2646 });
2647}
2648
2649#[gpui::test]
2650async fn test_newline_above(cx: &mut TestAppContext) {
2651 init_test(cx, |settings| {
2652 settings.defaults.tab_size = NonZeroU32::new(4)
2653 });
2654
2655 let language = Arc::new(
2656 Language::new(
2657 LanguageConfig::default(),
2658 Some(tree_sitter_rust::LANGUAGE.into()),
2659 )
2660 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2661 .unwrap(),
2662 );
2663
2664 let mut cx = EditorTestContext::new(cx).await;
2665 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2666 cx.set_state(indoc! {"
2667 const a: ˇA = (
2668 (ˇ
2669 «const_functionˇ»(ˇ),
2670 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2671 )ˇ
2672 ˇ);ˇ
2673 "});
2674
2675 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2676 cx.assert_editor_state(indoc! {"
2677 ˇ
2678 const a: A = (
2679 ˇ
2680 (
2681 ˇ
2682 ˇ
2683 const_function(),
2684 ˇ
2685 ˇ
2686 ˇ
2687 ˇ
2688 something_else,
2689 ˇ
2690 )
2691 ˇ
2692 ˇ
2693 );
2694 "});
2695}
2696
2697#[gpui::test]
2698async fn test_newline_below(cx: &mut TestAppContext) {
2699 init_test(cx, |settings| {
2700 settings.defaults.tab_size = NonZeroU32::new(4)
2701 });
2702
2703 let language = Arc::new(
2704 Language::new(
2705 LanguageConfig::default(),
2706 Some(tree_sitter_rust::LANGUAGE.into()),
2707 )
2708 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2709 .unwrap(),
2710 );
2711
2712 let mut cx = EditorTestContext::new(cx).await;
2713 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2714 cx.set_state(indoc! {"
2715 const a: ˇA = (
2716 (ˇ
2717 «const_functionˇ»(ˇ),
2718 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2719 )ˇ
2720 ˇ);ˇ
2721 "});
2722
2723 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2724 cx.assert_editor_state(indoc! {"
2725 const a: A = (
2726 ˇ
2727 (
2728 ˇ
2729 const_function(),
2730 ˇ
2731 ˇ
2732 something_else,
2733 ˇ
2734 ˇ
2735 ˇ
2736 ˇ
2737 )
2738 ˇ
2739 );
2740 ˇ
2741 ˇ
2742 "});
2743}
2744
2745#[gpui::test]
2746async fn test_newline_comments(cx: &mut TestAppContext) {
2747 init_test(cx, |settings| {
2748 settings.defaults.tab_size = NonZeroU32::new(4)
2749 });
2750
2751 let language = Arc::new(Language::new(
2752 LanguageConfig {
2753 line_comments: vec!["//".into()],
2754 ..LanguageConfig::default()
2755 },
2756 None,
2757 ));
2758 {
2759 let mut cx = EditorTestContext::new(cx).await;
2760 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2761 cx.set_state(indoc! {"
2762 // Fooˇ
2763 "});
2764
2765 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2766 cx.assert_editor_state(indoc! {"
2767 // Foo
2768 //ˇ
2769 "});
2770 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2771 cx.set_state(indoc! {"
2772 ˇ// Foo
2773 "});
2774 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2775 cx.assert_editor_state(indoc! {"
2776
2777 ˇ// Foo
2778 "});
2779 }
2780 // Ensure that comment continuations can be disabled.
2781 update_test_language_settings(cx, |settings| {
2782 settings.defaults.extend_comment_on_newline = Some(false);
2783 });
2784 let mut cx = EditorTestContext::new(cx).await;
2785 cx.set_state(indoc! {"
2786 // Fooˇ
2787 "});
2788 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2789 cx.assert_editor_state(indoc! {"
2790 // Foo
2791 ˇ
2792 "});
2793}
2794
2795#[gpui::test]
2796fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2797 init_test(cx, |_| {});
2798
2799 let editor = cx.add_window(|window, cx| {
2800 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2801 let mut editor = build_editor(buffer.clone(), window, cx);
2802 editor.change_selections(None, window, cx, |s| {
2803 s.select_ranges([3..4, 11..12, 19..20])
2804 });
2805 editor
2806 });
2807
2808 _ = editor.update(cx, |editor, window, cx| {
2809 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2810 editor.buffer.update(cx, |buffer, cx| {
2811 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2812 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2813 });
2814 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2815
2816 editor.insert("Z", window, cx);
2817 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2818
2819 // The selections are moved after the inserted characters
2820 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2821 });
2822}
2823
2824#[gpui::test]
2825async fn test_tab(cx: &mut TestAppContext) {
2826 init_test(cx, |settings| {
2827 settings.defaults.tab_size = NonZeroU32::new(3)
2828 });
2829
2830 let mut cx = EditorTestContext::new(cx).await;
2831 cx.set_state(indoc! {"
2832 ˇabˇc
2833 ˇ🏀ˇ🏀ˇefg
2834 dˇ
2835 "});
2836 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2837 cx.assert_editor_state(indoc! {"
2838 ˇab ˇc
2839 ˇ🏀 ˇ🏀 ˇefg
2840 d ˇ
2841 "});
2842
2843 cx.set_state(indoc! {"
2844 a
2845 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2846 "});
2847 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2848 cx.assert_editor_state(indoc! {"
2849 a
2850 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2851 "});
2852}
2853
2854#[gpui::test]
2855async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
2856 init_test(cx, |_| {});
2857
2858 let mut cx = EditorTestContext::new(cx).await;
2859 let language = Arc::new(
2860 Language::new(
2861 LanguageConfig::default(),
2862 Some(tree_sitter_rust::LANGUAGE.into()),
2863 )
2864 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2865 .unwrap(),
2866 );
2867 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2868
2869 // cursors that are already at the suggested indent level insert
2870 // a soft tab. cursors that are to the left of the suggested indent
2871 // auto-indent their line.
2872 cx.set_state(indoc! {"
2873 ˇ
2874 const a: B = (
2875 c(
2876 d(
2877 ˇ
2878 )
2879 ˇ
2880 ˇ )
2881 );
2882 "});
2883 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2884 cx.assert_editor_state(indoc! {"
2885 ˇ
2886 const a: B = (
2887 c(
2888 d(
2889 ˇ
2890 )
2891 ˇ
2892 ˇ)
2893 );
2894 "});
2895
2896 // handle auto-indent when there are multiple cursors on the same line
2897 cx.set_state(indoc! {"
2898 const a: B = (
2899 c(
2900 ˇ ˇ
2901 ˇ )
2902 );
2903 "});
2904 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2905 cx.assert_editor_state(indoc! {"
2906 const a: B = (
2907 c(
2908 ˇ
2909 ˇ)
2910 );
2911 "});
2912}
2913
2914#[gpui::test]
2915async fn test_tab_with_mixed_whitespace(cx: &mut TestAppContext) {
2916 init_test(cx, |settings| {
2917 settings.defaults.tab_size = NonZeroU32::new(4)
2918 });
2919
2920 let language = Arc::new(
2921 Language::new(
2922 LanguageConfig::default(),
2923 Some(tree_sitter_rust::LANGUAGE.into()),
2924 )
2925 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2926 .unwrap(),
2927 );
2928
2929 let mut cx = EditorTestContext::new(cx).await;
2930 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2931 cx.set_state(indoc! {"
2932 fn a() {
2933 if b {
2934 \t ˇc
2935 }
2936 }
2937 "});
2938
2939 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2940 cx.assert_editor_state(indoc! {"
2941 fn a() {
2942 if b {
2943 ˇc
2944 }
2945 }
2946 "});
2947}
2948
2949#[gpui::test]
2950async fn test_indent_outdent(cx: &mut TestAppContext) {
2951 init_test(cx, |settings| {
2952 settings.defaults.tab_size = NonZeroU32::new(4);
2953 });
2954
2955 let mut cx = EditorTestContext::new(cx).await;
2956
2957 cx.set_state(indoc! {"
2958 «oneˇ» «twoˇ»
2959 three
2960 four
2961 "});
2962 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2963 cx.assert_editor_state(indoc! {"
2964 «oneˇ» «twoˇ»
2965 three
2966 four
2967 "});
2968
2969 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
2970 cx.assert_editor_state(indoc! {"
2971 «oneˇ» «twoˇ»
2972 three
2973 four
2974 "});
2975
2976 // select across line ending
2977 cx.set_state(indoc! {"
2978 one two
2979 t«hree
2980 ˇ» four
2981 "});
2982 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2983 cx.assert_editor_state(indoc! {"
2984 one two
2985 t«hree
2986 ˇ» four
2987 "});
2988
2989 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
2990 cx.assert_editor_state(indoc! {"
2991 one two
2992 t«hree
2993 ˇ» four
2994 "});
2995
2996 // Ensure that indenting/outdenting works when the cursor is at column 0.
2997 cx.set_state(indoc! {"
2998 one two
2999 ˇthree
3000 four
3001 "});
3002 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3003 cx.assert_editor_state(indoc! {"
3004 one two
3005 ˇthree
3006 four
3007 "});
3008
3009 cx.set_state(indoc! {"
3010 one two
3011 ˇ three
3012 four
3013 "});
3014 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3015 cx.assert_editor_state(indoc! {"
3016 one two
3017 ˇthree
3018 four
3019 "});
3020}
3021
3022#[gpui::test]
3023async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3024 init_test(cx, |settings| {
3025 settings.defaults.hard_tabs = Some(true);
3026 });
3027
3028 let mut cx = EditorTestContext::new(cx).await;
3029
3030 // select two ranges on one line
3031 cx.set_state(indoc! {"
3032 «oneˇ» «twoˇ»
3033 three
3034 four
3035 "});
3036 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3037 cx.assert_editor_state(indoc! {"
3038 \t«oneˇ» «twoˇ»
3039 three
3040 four
3041 "});
3042 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3043 cx.assert_editor_state(indoc! {"
3044 \t\t«oneˇ» «twoˇ»
3045 three
3046 four
3047 "});
3048 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3049 cx.assert_editor_state(indoc! {"
3050 \t«oneˇ» «twoˇ»
3051 three
3052 four
3053 "});
3054 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3055 cx.assert_editor_state(indoc! {"
3056 «oneˇ» «twoˇ»
3057 three
3058 four
3059 "});
3060
3061 // select across a line ending
3062 cx.set_state(indoc! {"
3063 one two
3064 t«hree
3065 ˇ»four
3066 "});
3067 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3068 cx.assert_editor_state(indoc! {"
3069 one two
3070 \tt«hree
3071 ˇ»four
3072 "});
3073 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3074 cx.assert_editor_state(indoc! {"
3075 one two
3076 \t\tt«hree
3077 ˇ»four
3078 "});
3079 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3080 cx.assert_editor_state(indoc! {"
3081 one two
3082 \tt«hree
3083 ˇ»four
3084 "});
3085 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3086 cx.assert_editor_state(indoc! {"
3087 one two
3088 t«hree
3089 ˇ»four
3090 "});
3091
3092 // Ensure that indenting/outdenting works when the cursor is at column 0.
3093 cx.set_state(indoc! {"
3094 one two
3095 ˇthree
3096 four
3097 "});
3098 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3099 cx.assert_editor_state(indoc! {"
3100 one two
3101 ˇthree
3102 four
3103 "});
3104 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3105 cx.assert_editor_state(indoc! {"
3106 one two
3107 \tˇthree
3108 four
3109 "});
3110 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3111 cx.assert_editor_state(indoc! {"
3112 one two
3113 ˇthree
3114 four
3115 "});
3116}
3117
3118#[gpui::test]
3119fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3120 init_test(cx, |settings| {
3121 settings.languages.extend([
3122 (
3123 "TOML".into(),
3124 LanguageSettingsContent {
3125 tab_size: NonZeroU32::new(2),
3126 ..Default::default()
3127 },
3128 ),
3129 (
3130 "Rust".into(),
3131 LanguageSettingsContent {
3132 tab_size: NonZeroU32::new(4),
3133 ..Default::default()
3134 },
3135 ),
3136 ]);
3137 });
3138
3139 let toml_language = Arc::new(Language::new(
3140 LanguageConfig {
3141 name: "TOML".into(),
3142 ..Default::default()
3143 },
3144 None,
3145 ));
3146 let rust_language = Arc::new(Language::new(
3147 LanguageConfig {
3148 name: "Rust".into(),
3149 ..Default::default()
3150 },
3151 None,
3152 ));
3153
3154 let toml_buffer =
3155 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3156 let rust_buffer =
3157 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3158 let multibuffer = cx.new(|cx| {
3159 let mut multibuffer = MultiBuffer::new(ReadWrite);
3160 multibuffer.push_excerpts(
3161 toml_buffer.clone(),
3162 [ExcerptRange {
3163 context: Point::new(0, 0)..Point::new(2, 0),
3164 primary: None,
3165 }],
3166 cx,
3167 );
3168 multibuffer.push_excerpts(
3169 rust_buffer.clone(),
3170 [ExcerptRange {
3171 context: Point::new(0, 0)..Point::new(1, 0),
3172 primary: None,
3173 }],
3174 cx,
3175 );
3176 multibuffer
3177 });
3178
3179 cx.add_window(|window, cx| {
3180 let mut editor = build_editor(multibuffer, window, cx);
3181
3182 assert_eq!(
3183 editor.text(cx),
3184 indoc! {"
3185 a = 1
3186 b = 2
3187
3188 const c: usize = 3;
3189 "}
3190 );
3191
3192 select_ranges(
3193 &mut editor,
3194 indoc! {"
3195 «aˇ» = 1
3196 b = 2
3197
3198 «const c:ˇ» usize = 3;
3199 "},
3200 window,
3201 cx,
3202 );
3203
3204 editor.tab(&Tab, window, cx);
3205 assert_text_with_selections(
3206 &mut editor,
3207 indoc! {"
3208 «aˇ» = 1
3209 b = 2
3210
3211 «const c:ˇ» usize = 3;
3212 "},
3213 cx,
3214 );
3215 editor.backtab(&Backtab, window, cx);
3216 assert_text_with_selections(
3217 &mut editor,
3218 indoc! {"
3219 «aˇ» = 1
3220 b = 2
3221
3222 «const c:ˇ» usize = 3;
3223 "},
3224 cx,
3225 );
3226
3227 editor
3228 });
3229}
3230
3231#[gpui::test]
3232async fn test_backspace(cx: &mut TestAppContext) {
3233 init_test(cx, |_| {});
3234
3235 let mut cx = EditorTestContext::new(cx).await;
3236
3237 // Basic backspace
3238 cx.set_state(indoc! {"
3239 onˇe two three
3240 fou«rˇ» five six
3241 seven «ˇeight nine
3242 »ten
3243 "});
3244 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3245 cx.assert_editor_state(indoc! {"
3246 oˇe two three
3247 fouˇ five six
3248 seven ˇten
3249 "});
3250
3251 // Test backspace inside and around indents
3252 cx.set_state(indoc! {"
3253 zero
3254 ˇone
3255 ˇtwo
3256 ˇ ˇ ˇ three
3257 ˇ ˇ four
3258 "});
3259 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3260 cx.assert_editor_state(indoc! {"
3261 zero
3262 ˇone
3263 ˇtwo
3264 ˇ threeˇ four
3265 "});
3266
3267 // Test backspace with line_mode set to true
3268 cx.update_editor(|e, _, _| e.selections.line_mode = true);
3269 cx.set_state(indoc! {"
3270 The ˇquick ˇbrown
3271 fox jumps over
3272 the lazy dog
3273 ˇThe qu«ick bˇ»rown"});
3274 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3275 cx.assert_editor_state(indoc! {"
3276 ˇfox jumps over
3277 the lazy dogˇ"});
3278}
3279
3280#[gpui::test]
3281async fn test_delete(cx: &mut TestAppContext) {
3282 init_test(cx, |_| {});
3283
3284 let mut cx = EditorTestContext::new(cx).await;
3285 cx.set_state(indoc! {"
3286 onˇe two three
3287 fou«rˇ» five six
3288 seven «ˇeight nine
3289 »ten
3290 "});
3291 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3292 cx.assert_editor_state(indoc! {"
3293 onˇ two three
3294 fouˇ five six
3295 seven ˇten
3296 "});
3297
3298 // Test backspace with line_mode set to true
3299 cx.update_editor(|e, _, _| e.selections.line_mode = true);
3300 cx.set_state(indoc! {"
3301 The ˇquick ˇbrown
3302 fox «ˇjum»ps over
3303 the lazy dog
3304 ˇThe qu«ick bˇ»rown"});
3305 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3306 cx.assert_editor_state("ˇthe lazy dogˇ");
3307}
3308
3309#[gpui::test]
3310fn test_delete_line(cx: &mut TestAppContext) {
3311 init_test(cx, |_| {});
3312
3313 let editor = cx.add_window(|window, cx| {
3314 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3315 build_editor(buffer, window, cx)
3316 });
3317 _ = editor.update(cx, |editor, window, cx| {
3318 editor.change_selections(None, window, cx, |s| {
3319 s.select_display_ranges([
3320 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3321 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3322 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3323 ])
3324 });
3325 editor.delete_line(&DeleteLine, window, cx);
3326 assert_eq!(editor.display_text(cx), "ghi");
3327 assert_eq!(
3328 editor.selections.display_ranges(cx),
3329 vec![
3330 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3331 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3332 ]
3333 );
3334 });
3335
3336 let editor = cx.add_window(|window, cx| {
3337 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3338 build_editor(buffer, window, cx)
3339 });
3340 _ = editor.update(cx, |editor, window, cx| {
3341 editor.change_selections(None, window, cx, |s| {
3342 s.select_display_ranges([
3343 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3344 ])
3345 });
3346 editor.delete_line(&DeleteLine, window, cx);
3347 assert_eq!(editor.display_text(cx), "ghi\n");
3348 assert_eq!(
3349 editor.selections.display_ranges(cx),
3350 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3351 );
3352 });
3353}
3354
3355#[gpui::test]
3356fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3357 init_test(cx, |_| {});
3358
3359 cx.add_window(|window, cx| {
3360 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3361 let mut editor = build_editor(buffer.clone(), window, cx);
3362 let buffer = buffer.read(cx).as_singleton().unwrap();
3363
3364 assert_eq!(
3365 editor.selections.ranges::<Point>(cx),
3366 &[Point::new(0, 0)..Point::new(0, 0)]
3367 );
3368
3369 // When on single line, replace newline at end by space
3370 editor.join_lines(&JoinLines, window, cx);
3371 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3372 assert_eq!(
3373 editor.selections.ranges::<Point>(cx),
3374 &[Point::new(0, 3)..Point::new(0, 3)]
3375 );
3376
3377 // When multiple lines are selected, remove newlines that are spanned by the selection
3378 editor.change_selections(None, window, cx, |s| {
3379 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3380 });
3381 editor.join_lines(&JoinLines, window, cx);
3382 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3383 assert_eq!(
3384 editor.selections.ranges::<Point>(cx),
3385 &[Point::new(0, 11)..Point::new(0, 11)]
3386 );
3387
3388 // Undo should be transactional
3389 editor.undo(&Undo, window, cx);
3390 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3391 assert_eq!(
3392 editor.selections.ranges::<Point>(cx),
3393 &[Point::new(0, 5)..Point::new(2, 2)]
3394 );
3395
3396 // When joining an empty line don't insert a space
3397 editor.change_selections(None, window, cx, |s| {
3398 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3399 });
3400 editor.join_lines(&JoinLines, window, cx);
3401 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3402 assert_eq!(
3403 editor.selections.ranges::<Point>(cx),
3404 [Point::new(2, 3)..Point::new(2, 3)]
3405 );
3406
3407 // We can remove trailing newlines
3408 editor.join_lines(&JoinLines, window, cx);
3409 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3410 assert_eq!(
3411 editor.selections.ranges::<Point>(cx),
3412 [Point::new(2, 3)..Point::new(2, 3)]
3413 );
3414
3415 // We don't blow up on the last line
3416 editor.join_lines(&JoinLines, window, cx);
3417 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3418 assert_eq!(
3419 editor.selections.ranges::<Point>(cx),
3420 [Point::new(2, 3)..Point::new(2, 3)]
3421 );
3422
3423 // reset to test indentation
3424 editor.buffer.update(cx, |buffer, cx| {
3425 buffer.edit(
3426 [
3427 (Point::new(1, 0)..Point::new(1, 2), " "),
3428 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3429 ],
3430 None,
3431 cx,
3432 )
3433 });
3434
3435 // We remove any leading spaces
3436 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3437 editor.change_selections(None, window, cx, |s| {
3438 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3439 });
3440 editor.join_lines(&JoinLines, window, cx);
3441 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3442
3443 // We don't insert a space for a line containing only spaces
3444 editor.join_lines(&JoinLines, window, cx);
3445 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3446
3447 // We ignore any leading tabs
3448 editor.join_lines(&JoinLines, window, cx);
3449 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3450
3451 editor
3452 });
3453}
3454
3455#[gpui::test]
3456fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3457 init_test(cx, |_| {});
3458
3459 cx.add_window(|window, cx| {
3460 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3461 let mut editor = build_editor(buffer.clone(), window, cx);
3462 let buffer = buffer.read(cx).as_singleton().unwrap();
3463
3464 editor.change_selections(None, window, cx, |s| {
3465 s.select_ranges([
3466 Point::new(0, 2)..Point::new(1, 1),
3467 Point::new(1, 2)..Point::new(1, 2),
3468 Point::new(3, 1)..Point::new(3, 2),
3469 ])
3470 });
3471
3472 editor.join_lines(&JoinLines, window, cx);
3473 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3474
3475 assert_eq!(
3476 editor.selections.ranges::<Point>(cx),
3477 [
3478 Point::new(0, 7)..Point::new(0, 7),
3479 Point::new(1, 3)..Point::new(1, 3)
3480 ]
3481 );
3482 editor
3483 });
3484}
3485
3486#[gpui::test]
3487async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3488 init_test(cx, |_| {});
3489
3490 let mut cx = EditorTestContext::new(cx).await;
3491
3492 let diff_base = r#"
3493 Line 0
3494 Line 1
3495 Line 2
3496 Line 3
3497 "#
3498 .unindent();
3499
3500 cx.set_state(
3501 &r#"
3502 ˇLine 0
3503 Line 1
3504 Line 2
3505 Line 3
3506 "#
3507 .unindent(),
3508 );
3509
3510 cx.set_head_text(&diff_base);
3511 executor.run_until_parked();
3512
3513 // Join lines
3514 cx.update_editor(|editor, window, cx| {
3515 editor.join_lines(&JoinLines, window, cx);
3516 });
3517 executor.run_until_parked();
3518
3519 cx.assert_editor_state(
3520 &r#"
3521 Line 0ˇ Line 1
3522 Line 2
3523 Line 3
3524 "#
3525 .unindent(),
3526 );
3527 // Join again
3528 cx.update_editor(|editor, window, cx| {
3529 editor.join_lines(&JoinLines, window, cx);
3530 });
3531 executor.run_until_parked();
3532
3533 cx.assert_editor_state(
3534 &r#"
3535 Line 0 Line 1ˇ Line 2
3536 Line 3
3537 "#
3538 .unindent(),
3539 );
3540}
3541
3542#[gpui::test]
3543async fn test_custom_newlines_cause_no_false_positive_diffs(
3544 executor: BackgroundExecutor,
3545 cx: &mut TestAppContext,
3546) {
3547 init_test(cx, |_| {});
3548 let mut cx = EditorTestContext::new(cx).await;
3549 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3550 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3551 executor.run_until_parked();
3552
3553 cx.update_editor(|editor, window, cx| {
3554 let snapshot = editor.snapshot(window, cx);
3555 assert_eq!(
3556 snapshot
3557 .buffer_snapshot
3558 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3559 .collect::<Vec<_>>(),
3560 Vec::new(),
3561 "Should not have any diffs for files with custom newlines"
3562 );
3563 });
3564}
3565
3566#[gpui::test]
3567async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3568 init_test(cx, |_| {});
3569
3570 let mut cx = EditorTestContext::new(cx).await;
3571
3572 // Test sort_lines_case_insensitive()
3573 cx.set_state(indoc! {"
3574 «z
3575 y
3576 x
3577 Z
3578 Y
3579 Xˇ»
3580 "});
3581 cx.update_editor(|e, window, cx| {
3582 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3583 });
3584 cx.assert_editor_state(indoc! {"
3585 «x
3586 X
3587 y
3588 Y
3589 z
3590 Zˇ»
3591 "});
3592
3593 // Test reverse_lines()
3594 cx.set_state(indoc! {"
3595 «5
3596 4
3597 3
3598 2
3599 1ˇ»
3600 "});
3601 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3602 cx.assert_editor_state(indoc! {"
3603 «1
3604 2
3605 3
3606 4
3607 5ˇ»
3608 "});
3609
3610 // Skip testing shuffle_line()
3611
3612 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3613 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3614
3615 // Don't manipulate when cursor is on single line, but expand the selection
3616 cx.set_state(indoc! {"
3617 ddˇdd
3618 ccc
3619 bb
3620 a
3621 "});
3622 cx.update_editor(|e, window, cx| {
3623 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3624 });
3625 cx.assert_editor_state(indoc! {"
3626 «ddddˇ»
3627 ccc
3628 bb
3629 a
3630 "});
3631
3632 // Basic manipulate case
3633 // Start selection moves to column 0
3634 // End of selection shrinks to fit shorter line
3635 cx.set_state(indoc! {"
3636 dd«d
3637 ccc
3638 bb
3639 aaaaaˇ»
3640 "});
3641 cx.update_editor(|e, window, cx| {
3642 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3643 });
3644 cx.assert_editor_state(indoc! {"
3645 «aaaaa
3646 bb
3647 ccc
3648 dddˇ»
3649 "});
3650
3651 // Manipulate case with newlines
3652 cx.set_state(indoc! {"
3653 dd«d
3654 ccc
3655
3656 bb
3657 aaaaa
3658
3659 ˇ»
3660 "});
3661 cx.update_editor(|e, window, cx| {
3662 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3663 });
3664 cx.assert_editor_state(indoc! {"
3665 «
3666
3667 aaaaa
3668 bb
3669 ccc
3670 dddˇ»
3671
3672 "});
3673
3674 // Adding new line
3675 cx.set_state(indoc! {"
3676 aa«a
3677 bbˇ»b
3678 "});
3679 cx.update_editor(|e, window, cx| {
3680 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3681 });
3682 cx.assert_editor_state(indoc! {"
3683 «aaa
3684 bbb
3685 added_lineˇ»
3686 "});
3687
3688 // Removing line
3689 cx.set_state(indoc! {"
3690 aa«a
3691 bbbˇ»
3692 "});
3693 cx.update_editor(|e, window, cx| {
3694 e.manipulate_lines(window, cx, |lines| {
3695 lines.pop();
3696 })
3697 });
3698 cx.assert_editor_state(indoc! {"
3699 «aaaˇ»
3700 "});
3701
3702 // Removing all lines
3703 cx.set_state(indoc! {"
3704 aa«a
3705 bbbˇ»
3706 "});
3707 cx.update_editor(|e, window, cx| {
3708 e.manipulate_lines(window, cx, |lines| {
3709 lines.drain(..);
3710 })
3711 });
3712 cx.assert_editor_state(indoc! {"
3713 ˇ
3714 "});
3715}
3716
3717#[gpui::test]
3718async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3719 init_test(cx, |_| {});
3720
3721 let mut cx = EditorTestContext::new(cx).await;
3722
3723 // Consider continuous selection as single selection
3724 cx.set_state(indoc! {"
3725 Aaa«aa
3726 cˇ»c«c
3727 bb
3728 aaaˇ»aa
3729 "});
3730 cx.update_editor(|e, window, cx| {
3731 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3732 });
3733 cx.assert_editor_state(indoc! {"
3734 «Aaaaa
3735 ccc
3736 bb
3737 aaaaaˇ»
3738 "});
3739
3740 cx.set_state(indoc! {"
3741 Aaa«aa
3742 cˇ»c«c
3743 bb
3744 aaaˇ»aa
3745 "});
3746 cx.update_editor(|e, window, cx| {
3747 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3748 });
3749 cx.assert_editor_state(indoc! {"
3750 «Aaaaa
3751 ccc
3752 bbˇ»
3753 "});
3754
3755 // Consider non continuous selection as distinct dedup operations
3756 cx.set_state(indoc! {"
3757 «aaaaa
3758 bb
3759 aaaaa
3760 aaaaaˇ»
3761
3762 aaa«aaˇ»
3763 "});
3764 cx.update_editor(|e, window, cx| {
3765 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3766 });
3767 cx.assert_editor_state(indoc! {"
3768 «aaaaa
3769 bbˇ»
3770
3771 «aaaaaˇ»
3772 "});
3773}
3774
3775#[gpui::test]
3776async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3777 init_test(cx, |_| {});
3778
3779 let mut cx = EditorTestContext::new(cx).await;
3780
3781 cx.set_state(indoc! {"
3782 «Aaa
3783 aAa
3784 Aaaˇ»
3785 "});
3786 cx.update_editor(|e, window, cx| {
3787 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3788 });
3789 cx.assert_editor_state(indoc! {"
3790 «Aaa
3791 aAaˇ»
3792 "});
3793
3794 cx.set_state(indoc! {"
3795 «Aaa
3796 aAa
3797 aaAˇ»
3798 "});
3799 cx.update_editor(|e, window, cx| {
3800 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3801 });
3802 cx.assert_editor_state(indoc! {"
3803 «Aaaˇ»
3804 "});
3805}
3806
3807#[gpui::test]
3808async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3809 init_test(cx, |_| {});
3810
3811 let mut cx = EditorTestContext::new(cx).await;
3812
3813 // Manipulate with multiple selections on a single line
3814 cx.set_state(indoc! {"
3815 dd«dd
3816 cˇ»c«c
3817 bb
3818 aaaˇ»aa
3819 "});
3820 cx.update_editor(|e, window, cx| {
3821 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3822 });
3823 cx.assert_editor_state(indoc! {"
3824 «aaaaa
3825 bb
3826 ccc
3827 ddddˇ»
3828 "});
3829
3830 // Manipulate with multiple disjoin selections
3831 cx.set_state(indoc! {"
3832 5«
3833 4
3834 3
3835 2
3836 1ˇ»
3837
3838 dd«dd
3839 ccc
3840 bb
3841 aaaˇ»aa
3842 "});
3843 cx.update_editor(|e, window, cx| {
3844 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3845 });
3846 cx.assert_editor_state(indoc! {"
3847 «1
3848 2
3849 3
3850 4
3851 5ˇ»
3852
3853 «aaaaa
3854 bb
3855 ccc
3856 ddddˇ»
3857 "});
3858
3859 // Adding lines on each selection
3860 cx.set_state(indoc! {"
3861 2«
3862 1ˇ»
3863
3864 bb«bb
3865 aaaˇ»aa
3866 "});
3867 cx.update_editor(|e, window, cx| {
3868 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3869 });
3870 cx.assert_editor_state(indoc! {"
3871 «2
3872 1
3873 added lineˇ»
3874
3875 «bbbb
3876 aaaaa
3877 added lineˇ»
3878 "});
3879
3880 // Removing lines on each selection
3881 cx.set_state(indoc! {"
3882 2«
3883 1ˇ»
3884
3885 bb«bb
3886 aaaˇ»aa
3887 "});
3888 cx.update_editor(|e, window, cx| {
3889 e.manipulate_lines(window, cx, |lines| {
3890 lines.pop();
3891 })
3892 });
3893 cx.assert_editor_state(indoc! {"
3894 «2ˇ»
3895
3896 «bbbbˇ»
3897 "});
3898}
3899
3900#[gpui::test]
3901async fn test_manipulate_text(cx: &mut TestAppContext) {
3902 init_test(cx, |_| {});
3903
3904 let mut cx = EditorTestContext::new(cx).await;
3905
3906 // Test convert_to_upper_case()
3907 cx.set_state(indoc! {"
3908 «hello worldˇ»
3909 "});
3910 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3911 cx.assert_editor_state(indoc! {"
3912 «HELLO WORLDˇ»
3913 "});
3914
3915 // Test convert_to_lower_case()
3916 cx.set_state(indoc! {"
3917 «HELLO WORLDˇ»
3918 "});
3919 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3920 cx.assert_editor_state(indoc! {"
3921 «hello worldˇ»
3922 "});
3923
3924 // Test multiple line, single selection case
3925 cx.set_state(indoc! {"
3926 «The quick brown
3927 fox jumps over
3928 the lazy dogˇ»
3929 "});
3930 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3931 cx.assert_editor_state(indoc! {"
3932 «The Quick Brown
3933 Fox Jumps Over
3934 The Lazy Dogˇ»
3935 "});
3936
3937 // Test multiple line, single selection case
3938 cx.set_state(indoc! {"
3939 «The quick brown
3940 fox jumps over
3941 the lazy dogˇ»
3942 "});
3943 cx.update_editor(|e, window, cx| {
3944 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
3945 });
3946 cx.assert_editor_state(indoc! {"
3947 «TheQuickBrown
3948 FoxJumpsOver
3949 TheLazyDogˇ»
3950 "});
3951
3952 // From here on out, test more complex cases of manipulate_text()
3953
3954 // Test no selection case - should affect words cursors are in
3955 // Cursor at beginning, middle, and end of word
3956 cx.set_state(indoc! {"
3957 ˇhello big beauˇtiful worldˇ
3958 "});
3959 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3960 cx.assert_editor_state(indoc! {"
3961 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
3962 "});
3963
3964 // Test multiple selections on a single line and across multiple lines
3965 cx.set_state(indoc! {"
3966 «Theˇ» quick «brown
3967 foxˇ» jumps «overˇ»
3968 the «lazyˇ» dog
3969 "});
3970 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3971 cx.assert_editor_state(indoc! {"
3972 «THEˇ» quick «BROWN
3973 FOXˇ» jumps «OVERˇ»
3974 the «LAZYˇ» dog
3975 "});
3976
3977 // Test case where text length grows
3978 cx.set_state(indoc! {"
3979 «tschüߡ»
3980 "});
3981 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3982 cx.assert_editor_state(indoc! {"
3983 «TSCHÜSSˇ»
3984 "});
3985
3986 // Test to make sure we don't crash when text shrinks
3987 cx.set_state(indoc! {"
3988 aaa_bbbˇ
3989 "});
3990 cx.update_editor(|e, window, cx| {
3991 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3992 });
3993 cx.assert_editor_state(indoc! {"
3994 «aaaBbbˇ»
3995 "});
3996
3997 // Test to make sure we all aware of the fact that each word can grow and shrink
3998 // Final selections should be aware of this fact
3999 cx.set_state(indoc! {"
4000 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4001 "});
4002 cx.update_editor(|e, window, cx| {
4003 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4004 });
4005 cx.assert_editor_state(indoc! {"
4006 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4007 "});
4008
4009 cx.set_state(indoc! {"
4010 «hElLo, WoRld!ˇ»
4011 "});
4012 cx.update_editor(|e, window, cx| {
4013 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4014 });
4015 cx.assert_editor_state(indoc! {"
4016 «HeLlO, wOrLD!ˇ»
4017 "});
4018}
4019
4020#[gpui::test]
4021fn test_duplicate_line(cx: &mut TestAppContext) {
4022 init_test(cx, |_| {});
4023
4024 let editor = cx.add_window(|window, cx| {
4025 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4026 build_editor(buffer, window, cx)
4027 });
4028 _ = editor.update(cx, |editor, window, cx| {
4029 editor.change_selections(None, window, cx, |s| {
4030 s.select_display_ranges([
4031 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4032 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4033 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4034 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4035 ])
4036 });
4037 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4038 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4039 assert_eq!(
4040 editor.selections.display_ranges(cx),
4041 vec![
4042 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4043 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4044 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4045 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4046 ]
4047 );
4048 });
4049
4050 let editor = cx.add_window(|window, cx| {
4051 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4052 build_editor(buffer, window, cx)
4053 });
4054 _ = editor.update(cx, |editor, window, cx| {
4055 editor.change_selections(None, window, cx, |s| {
4056 s.select_display_ranges([
4057 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4058 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4059 ])
4060 });
4061 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4062 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4063 assert_eq!(
4064 editor.selections.display_ranges(cx),
4065 vec![
4066 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4067 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4068 ]
4069 );
4070 });
4071
4072 // With `move_upwards` the selections stay in place, except for
4073 // the lines inserted above them
4074 let editor = cx.add_window(|window, cx| {
4075 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4076 build_editor(buffer, window, cx)
4077 });
4078 _ = editor.update(cx, |editor, window, cx| {
4079 editor.change_selections(None, window, cx, |s| {
4080 s.select_display_ranges([
4081 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4082 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4083 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4084 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4085 ])
4086 });
4087 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4088 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4089 assert_eq!(
4090 editor.selections.display_ranges(cx),
4091 vec![
4092 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4093 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4094 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4095 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4096 ]
4097 );
4098 });
4099
4100 let editor = cx.add_window(|window, cx| {
4101 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4102 build_editor(buffer, window, cx)
4103 });
4104 _ = editor.update(cx, |editor, window, cx| {
4105 editor.change_selections(None, window, cx, |s| {
4106 s.select_display_ranges([
4107 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4108 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4109 ])
4110 });
4111 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4112 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4113 assert_eq!(
4114 editor.selections.display_ranges(cx),
4115 vec![
4116 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4117 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4118 ]
4119 );
4120 });
4121
4122 let editor = cx.add_window(|window, cx| {
4123 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4124 build_editor(buffer, window, cx)
4125 });
4126 _ = editor.update(cx, |editor, window, cx| {
4127 editor.change_selections(None, window, cx, |s| {
4128 s.select_display_ranges([
4129 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4130 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4131 ])
4132 });
4133 editor.duplicate_selection(&DuplicateSelection, window, cx);
4134 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4135 assert_eq!(
4136 editor.selections.display_ranges(cx),
4137 vec![
4138 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4139 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4140 ]
4141 );
4142 });
4143}
4144
4145#[gpui::test]
4146fn test_move_line_up_down(cx: &mut TestAppContext) {
4147 init_test(cx, |_| {});
4148
4149 let editor = cx.add_window(|window, cx| {
4150 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4151 build_editor(buffer, window, cx)
4152 });
4153 _ = editor.update(cx, |editor, window, cx| {
4154 editor.fold_creases(
4155 vec![
4156 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4157 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4158 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4159 ],
4160 true,
4161 window,
4162 cx,
4163 );
4164 editor.change_selections(None, window, cx, |s| {
4165 s.select_display_ranges([
4166 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4167 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4168 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4169 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4170 ])
4171 });
4172 assert_eq!(
4173 editor.display_text(cx),
4174 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4175 );
4176
4177 editor.move_line_up(&MoveLineUp, window, cx);
4178 assert_eq!(
4179 editor.display_text(cx),
4180 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4181 );
4182 assert_eq!(
4183 editor.selections.display_ranges(cx),
4184 vec![
4185 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4186 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4187 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4188 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4189 ]
4190 );
4191 });
4192
4193 _ = editor.update(cx, |editor, window, cx| {
4194 editor.move_line_down(&MoveLineDown, window, cx);
4195 assert_eq!(
4196 editor.display_text(cx),
4197 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4198 );
4199 assert_eq!(
4200 editor.selections.display_ranges(cx),
4201 vec![
4202 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4203 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4204 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4205 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4206 ]
4207 );
4208 });
4209
4210 _ = editor.update(cx, |editor, window, cx| {
4211 editor.move_line_down(&MoveLineDown, window, cx);
4212 assert_eq!(
4213 editor.display_text(cx),
4214 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4215 );
4216 assert_eq!(
4217 editor.selections.display_ranges(cx),
4218 vec![
4219 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4220 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4221 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4222 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4223 ]
4224 );
4225 });
4226
4227 _ = editor.update(cx, |editor, window, cx| {
4228 editor.move_line_up(&MoveLineUp, window, cx);
4229 assert_eq!(
4230 editor.display_text(cx),
4231 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4232 );
4233 assert_eq!(
4234 editor.selections.display_ranges(cx),
4235 vec![
4236 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4237 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4238 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4239 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4240 ]
4241 );
4242 });
4243}
4244
4245#[gpui::test]
4246fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4247 init_test(cx, |_| {});
4248
4249 let editor = cx.add_window(|window, cx| {
4250 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4251 build_editor(buffer, window, cx)
4252 });
4253 _ = editor.update(cx, |editor, window, cx| {
4254 let snapshot = editor.buffer.read(cx).snapshot(cx);
4255 editor.insert_blocks(
4256 [BlockProperties {
4257 style: BlockStyle::Fixed,
4258 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4259 height: 1,
4260 render: Arc::new(|_| div().into_any()),
4261 priority: 0,
4262 }],
4263 Some(Autoscroll::fit()),
4264 cx,
4265 );
4266 editor.change_selections(None, window, cx, |s| {
4267 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4268 });
4269 editor.move_line_down(&MoveLineDown, window, cx);
4270 });
4271}
4272
4273#[gpui::test]
4274async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4275 init_test(cx, |_| {});
4276
4277 let mut cx = EditorTestContext::new(cx).await;
4278 cx.set_state(
4279 &"
4280 ˇzero
4281 one
4282 two
4283 three
4284 four
4285 five
4286 "
4287 .unindent(),
4288 );
4289
4290 // Create a four-line block that replaces three lines of text.
4291 cx.update_editor(|editor, window, cx| {
4292 let snapshot = editor.snapshot(window, cx);
4293 let snapshot = &snapshot.buffer_snapshot;
4294 let placement = BlockPlacement::Replace(
4295 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4296 );
4297 editor.insert_blocks(
4298 [BlockProperties {
4299 placement,
4300 height: 4,
4301 style: BlockStyle::Sticky,
4302 render: Arc::new(|_| gpui::div().into_any_element()),
4303 priority: 0,
4304 }],
4305 None,
4306 cx,
4307 );
4308 });
4309
4310 // Move down so that the cursor touches the block.
4311 cx.update_editor(|editor, window, cx| {
4312 editor.move_down(&Default::default(), window, cx);
4313 });
4314 cx.assert_editor_state(
4315 &"
4316 zero
4317 «one
4318 two
4319 threeˇ»
4320 four
4321 five
4322 "
4323 .unindent(),
4324 );
4325
4326 // Move down past the block.
4327 cx.update_editor(|editor, window, cx| {
4328 editor.move_down(&Default::default(), window, cx);
4329 });
4330 cx.assert_editor_state(
4331 &"
4332 zero
4333 one
4334 two
4335 three
4336 ˇfour
4337 five
4338 "
4339 .unindent(),
4340 );
4341}
4342
4343#[gpui::test]
4344fn test_transpose(cx: &mut TestAppContext) {
4345 init_test(cx, |_| {});
4346
4347 _ = cx.add_window(|window, cx| {
4348 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4349 editor.set_style(EditorStyle::default(), window, cx);
4350 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4351 editor.transpose(&Default::default(), window, cx);
4352 assert_eq!(editor.text(cx), "bac");
4353 assert_eq!(editor.selections.ranges(cx), [2..2]);
4354
4355 editor.transpose(&Default::default(), window, cx);
4356 assert_eq!(editor.text(cx), "bca");
4357 assert_eq!(editor.selections.ranges(cx), [3..3]);
4358
4359 editor.transpose(&Default::default(), window, cx);
4360 assert_eq!(editor.text(cx), "bac");
4361 assert_eq!(editor.selections.ranges(cx), [3..3]);
4362
4363 editor
4364 });
4365
4366 _ = cx.add_window(|window, cx| {
4367 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4368 editor.set_style(EditorStyle::default(), window, cx);
4369 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4370 editor.transpose(&Default::default(), window, cx);
4371 assert_eq!(editor.text(cx), "acb\nde");
4372 assert_eq!(editor.selections.ranges(cx), [3..3]);
4373
4374 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4375 editor.transpose(&Default::default(), window, cx);
4376 assert_eq!(editor.text(cx), "acbd\ne");
4377 assert_eq!(editor.selections.ranges(cx), [5..5]);
4378
4379 editor.transpose(&Default::default(), window, cx);
4380 assert_eq!(editor.text(cx), "acbde\n");
4381 assert_eq!(editor.selections.ranges(cx), [6..6]);
4382
4383 editor.transpose(&Default::default(), window, cx);
4384 assert_eq!(editor.text(cx), "acbd\ne");
4385 assert_eq!(editor.selections.ranges(cx), [6..6]);
4386
4387 editor
4388 });
4389
4390 _ = cx.add_window(|window, cx| {
4391 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4392 editor.set_style(EditorStyle::default(), window, cx);
4393 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4394 editor.transpose(&Default::default(), window, cx);
4395 assert_eq!(editor.text(cx), "bacd\ne");
4396 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4397
4398 editor.transpose(&Default::default(), window, cx);
4399 assert_eq!(editor.text(cx), "bcade\n");
4400 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4401
4402 editor.transpose(&Default::default(), window, cx);
4403 assert_eq!(editor.text(cx), "bcda\ne");
4404 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4405
4406 editor.transpose(&Default::default(), window, cx);
4407 assert_eq!(editor.text(cx), "bcade\n");
4408 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4409
4410 editor.transpose(&Default::default(), window, cx);
4411 assert_eq!(editor.text(cx), "bcaed\n");
4412 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4413
4414 editor
4415 });
4416
4417 _ = cx.add_window(|window, cx| {
4418 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4419 editor.set_style(EditorStyle::default(), window, cx);
4420 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4421 editor.transpose(&Default::default(), window, cx);
4422 assert_eq!(editor.text(cx), "🏀🍐✋");
4423 assert_eq!(editor.selections.ranges(cx), [8..8]);
4424
4425 editor.transpose(&Default::default(), window, cx);
4426 assert_eq!(editor.text(cx), "🏀✋🍐");
4427 assert_eq!(editor.selections.ranges(cx), [11..11]);
4428
4429 editor.transpose(&Default::default(), window, cx);
4430 assert_eq!(editor.text(cx), "🏀🍐✋");
4431 assert_eq!(editor.selections.ranges(cx), [11..11]);
4432
4433 editor
4434 });
4435}
4436
4437#[gpui::test]
4438async fn test_rewrap(cx: &mut TestAppContext) {
4439 init_test(cx, |settings| {
4440 settings.languages.extend([
4441 (
4442 "Markdown".into(),
4443 LanguageSettingsContent {
4444 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4445 ..Default::default()
4446 },
4447 ),
4448 (
4449 "Plain Text".into(),
4450 LanguageSettingsContent {
4451 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4452 ..Default::default()
4453 },
4454 ),
4455 ])
4456 });
4457
4458 let mut cx = EditorTestContext::new(cx).await;
4459
4460 let language_with_c_comments = Arc::new(Language::new(
4461 LanguageConfig {
4462 line_comments: vec!["// ".into()],
4463 ..LanguageConfig::default()
4464 },
4465 None,
4466 ));
4467 let language_with_pound_comments = Arc::new(Language::new(
4468 LanguageConfig {
4469 line_comments: vec!["# ".into()],
4470 ..LanguageConfig::default()
4471 },
4472 None,
4473 ));
4474 let markdown_language = Arc::new(Language::new(
4475 LanguageConfig {
4476 name: "Markdown".into(),
4477 ..LanguageConfig::default()
4478 },
4479 None,
4480 ));
4481 let language_with_doc_comments = Arc::new(Language::new(
4482 LanguageConfig {
4483 line_comments: vec!["// ".into(), "/// ".into()],
4484 ..LanguageConfig::default()
4485 },
4486 Some(tree_sitter_rust::LANGUAGE.into()),
4487 ));
4488
4489 let plaintext_language = Arc::new(Language::new(
4490 LanguageConfig {
4491 name: "Plain Text".into(),
4492 ..LanguageConfig::default()
4493 },
4494 None,
4495 ));
4496
4497 assert_rewrap(
4498 indoc! {"
4499 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4500 "},
4501 indoc! {"
4502 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4503 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4504 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4505 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4506 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4507 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4508 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4509 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4510 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4511 // porttitor id. Aliquam id accumsan eros.
4512 "},
4513 language_with_c_comments.clone(),
4514 &mut cx,
4515 );
4516
4517 // Test that rewrapping works inside of a selection
4518 assert_rewrap(
4519 indoc! {"
4520 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.ˇ»
4521 "},
4522 indoc! {"
4523 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4524 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4525 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4526 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4527 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4528 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4529 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4530 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4531 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4532 // porttitor id. Aliquam id accumsan eros.ˇ»
4533 "},
4534 language_with_c_comments.clone(),
4535 &mut cx,
4536 );
4537
4538 // Test that cursors that expand to the same region are collapsed.
4539 assert_rewrap(
4540 indoc! {"
4541 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4542 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4543 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4544 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4545 "},
4546 indoc! {"
4547 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4548 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4549 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4550 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4551 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4552 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4553 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4554 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4555 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4556 // porttitor id. Aliquam id accumsan eros.
4557 "},
4558 language_with_c_comments.clone(),
4559 &mut cx,
4560 );
4561
4562 // Test that non-contiguous selections are treated separately.
4563 assert_rewrap(
4564 indoc! {"
4565 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4566 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4567 //
4568 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4569 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4570 "},
4571 indoc! {"
4572 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4573 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4574 // auctor, eu lacinia sapien scelerisque.
4575 //
4576 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4577 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4578 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4579 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4580 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4581 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4582 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4583 "},
4584 language_with_c_comments.clone(),
4585 &mut cx,
4586 );
4587
4588 // Test that different comment prefixes are supported.
4589 assert_rewrap(
4590 indoc! {"
4591 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4592 "},
4593 indoc! {"
4594 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4595 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4596 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4597 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4598 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4599 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4600 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4601 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4602 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4603 # accumsan eros.
4604 "},
4605 language_with_pound_comments.clone(),
4606 &mut cx,
4607 );
4608
4609 // Test that rewrapping is ignored outside of comments in most languages.
4610 assert_rewrap(
4611 indoc! {"
4612 /// Adds two numbers.
4613 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4614 fn add(a: u32, b: u32) -> u32 {
4615 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
4616 }
4617 "},
4618 indoc! {"
4619 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4620 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4621 fn add(a: u32, b: u32) -> u32 {
4622 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
4623 }
4624 "},
4625 language_with_doc_comments.clone(),
4626 &mut cx,
4627 );
4628
4629 // Test that rewrapping works in Markdown and Plain Text languages.
4630 assert_rewrap(
4631 indoc! {"
4632 # Hello
4633
4634 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
4635 "},
4636 indoc! {"
4637 # Hello
4638
4639 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4640 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4641 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4642 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4643 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4644 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4645 Integer sit amet scelerisque nisi.
4646 "},
4647 markdown_language,
4648 &mut cx,
4649 );
4650
4651 assert_rewrap(
4652 indoc! {"
4653 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
4654 "},
4655 indoc! {"
4656 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4657 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4658 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4659 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4660 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4661 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4662 Integer sit amet scelerisque nisi.
4663 "},
4664 plaintext_language,
4665 &mut cx,
4666 );
4667
4668 // Test rewrapping unaligned comments in a selection.
4669 assert_rewrap(
4670 indoc! {"
4671 fn foo() {
4672 if true {
4673 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4674 // Praesent semper egestas tellus id dignissim.ˇ»
4675 do_something();
4676 } else {
4677 //
4678 }
4679 }
4680 "},
4681 indoc! {"
4682 fn foo() {
4683 if true {
4684 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4685 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4686 // egestas tellus id dignissim.ˇ»
4687 do_something();
4688 } else {
4689 //
4690 }
4691 }
4692 "},
4693 language_with_doc_comments.clone(),
4694 &mut cx,
4695 );
4696
4697 assert_rewrap(
4698 indoc! {"
4699 fn foo() {
4700 if true {
4701 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4702 // Praesent semper egestas tellus id dignissim.»
4703 do_something();
4704 } else {
4705 //
4706 }
4707
4708 }
4709 "},
4710 indoc! {"
4711 fn foo() {
4712 if true {
4713 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4714 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4715 // egestas tellus id dignissim.»
4716 do_something();
4717 } else {
4718 //
4719 }
4720
4721 }
4722 "},
4723 language_with_doc_comments.clone(),
4724 &mut cx,
4725 );
4726
4727 #[track_caller]
4728 fn assert_rewrap(
4729 unwrapped_text: &str,
4730 wrapped_text: &str,
4731 language: Arc<Language>,
4732 cx: &mut EditorTestContext,
4733 ) {
4734 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4735 cx.set_state(unwrapped_text);
4736 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4737 cx.assert_editor_state(wrapped_text);
4738 }
4739}
4740
4741#[gpui::test]
4742async fn test_hard_wrap(cx: &mut TestAppContext) {
4743 init_test(cx, |_| {});
4744 let mut cx = EditorTestContext::new(cx).await;
4745
4746 cx.update_editor(|editor, _, cx| {
4747 editor.set_hard_wrap(Some(14), cx);
4748 });
4749
4750 cx.set_state(indoc!(
4751 "
4752 one two three ˇ
4753 "
4754 ));
4755 cx.simulate_input("four");
4756 cx.run_until_parked();
4757
4758 cx.assert_editor_state(indoc!(
4759 "
4760 one two three
4761 fourˇ
4762 "
4763 ));
4764}
4765
4766#[gpui::test]
4767async fn test_clipboard(cx: &mut TestAppContext) {
4768 init_test(cx, |_| {});
4769
4770 let mut cx = EditorTestContext::new(cx).await;
4771
4772 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4773 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4774 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4775
4776 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4777 cx.set_state("two ˇfour ˇsix ˇ");
4778 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4779 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4780
4781 // Paste again but with only two cursors. Since the number of cursors doesn't
4782 // match the number of slices in the clipboard, the entire clipboard text
4783 // is pasted at each cursor.
4784 cx.set_state("ˇtwo one✅ four three six five ˇ");
4785 cx.update_editor(|e, window, cx| {
4786 e.handle_input("( ", window, cx);
4787 e.paste(&Paste, window, cx);
4788 e.handle_input(") ", window, cx);
4789 });
4790 cx.assert_editor_state(
4791 &([
4792 "( one✅ ",
4793 "three ",
4794 "five ) ˇtwo one✅ four three six five ( one✅ ",
4795 "three ",
4796 "five ) ˇ",
4797 ]
4798 .join("\n")),
4799 );
4800
4801 // Cut with three selections, one of which is full-line.
4802 cx.set_state(indoc! {"
4803 1«2ˇ»3
4804 4ˇ567
4805 «8ˇ»9"});
4806 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4807 cx.assert_editor_state(indoc! {"
4808 1ˇ3
4809 ˇ9"});
4810
4811 // Paste with three selections, noticing how the copied selection that was full-line
4812 // gets inserted before the second cursor.
4813 cx.set_state(indoc! {"
4814 1ˇ3
4815 9ˇ
4816 «oˇ»ne"});
4817 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4818 cx.assert_editor_state(indoc! {"
4819 12ˇ3
4820 4567
4821 9ˇ
4822 8ˇne"});
4823
4824 // Copy with a single cursor only, which writes the whole line into the clipboard.
4825 cx.set_state(indoc! {"
4826 The quick brown
4827 fox juˇmps over
4828 the lazy dog"});
4829 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4830 assert_eq!(
4831 cx.read_from_clipboard()
4832 .and_then(|item| item.text().as_deref().map(str::to_string)),
4833 Some("fox jumps over\n".to_string())
4834 );
4835
4836 // Paste with three selections, noticing how the copied full-line selection is inserted
4837 // before the empty selections but replaces the selection that is non-empty.
4838 cx.set_state(indoc! {"
4839 Tˇhe quick brown
4840 «foˇ»x jumps over
4841 tˇhe lazy dog"});
4842 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4843 cx.assert_editor_state(indoc! {"
4844 fox jumps over
4845 Tˇhe quick brown
4846 fox jumps over
4847 ˇx jumps over
4848 fox jumps over
4849 tˇhe lazy dog"});
4850}
4851
4852#[gpui::test]
4853async fn test_paste_multiline(cx: &mut TestAppContext) {
4854 init_test(cx, |_| {});
4855
4856 let mut cx = EditorTestContext::new(cx).await;
4857 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
4858
4859 // Cut an indented block, without the leading whitespace.
4860 cx.set_state(indoc! {"
4861 const a: B = (
4862 c(),
4863 «d(
4864 e,
4865 f
4866 )ˇ»
4867 );
4868 "});
4869 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4870 cx.assert_editor_state(indoc! {"
4871 const a: B = (
4872 c(),
4873 ˇ
4874 );
4875 "});
4876
4877 // Paste it at the same position.
4878 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4879 cx.assert_editor_state(indoc! {"
4880 const a: B = (
4881 c(),
4882 d(
4883 e,
4884 f
4885 )ˇ
4886 );
4887 "});
4888
4889 // Paste it at a line with a lower indent level.
4890 cx.set_state(indoc! {"
4891 ˇ
4892 const a: B = (
4893 c(),
4894 );
4895 "});
4896 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4897 cx.assert_editor_state(indoc! {"
4898 d(
4899 e,
4900 f
4901 )ˇ
4902 const a: B = (
4903 c(),
4904 );
4905 "});
4906
4907 // Cut an indented block, with the leading whitespace.
4908 cx.set_state(indoc! {"
4909 const a: B = (
4910 c(),
4911 « d(
4912 e,
4913 f
4914 )
4915 ˇ»);
4916 "});
4917 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4918 cx.assert_editor_state(indoc! {"
4919 const a: B = (
4920 c(),
4921 ˇ);
4922 "});
4923
4924 // Paste it at the same position.
4925 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4926 cx.assert_editor_state(indoc! {"
4927 const a: B = (
4928 c(),
4929 d(
4930 e,
4931 f
4932 )
4933 ˇ);
4934 "});
4935
4936 // Paste it at a line with a higher indent level.
4937 cx.set_state(indoc! {"
4938 const a: B = (
4939 c(),
4940 d(
4941 e,
4942 fˇ
4943 )
4944 );
4945 "});
4946 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4947 cx.assert_editor_state(indoc! {"
4948 const a: B = (
4949 c(),
4950 d(
4951 e,
4952 f d(
4953 e,
4954 f
4955 )
4956 ˇ
4957 )
4958 );
4959 "});
4960
4961 // Copy an indented block, starting mid-line
4962 cx.set_state(indoc! {"
4963 const a: B = (
4964 c(),
4965 somethin«g(
4966 e,
4967 f
4968 )ˇ»
4969 );
4970 "});
4971 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4972
4973 // Paste it on a line with a lower indent level
4974 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
4975 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4976 cx.assert_editor_state(indoc! {"
4977 const a: B = (
4978 c(),
4979 something(
4980 e,
4981 f
4982 )
4983 );
4984 g(
4985 e,
4986 f
4987 )ˇ"});
4988}
4989
4990#[gpui::test]
4991async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
4992 init_test(cx, |_| {});
4993
4994 cx.write_to_clipboard(ClipboardItem::new_string(
4995 " d(\n e\n );\n".into(),
4996 ));
4997
4998 let mut cx = EditorTestContext::new(cx).await;
4999 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5000
5001 cx.set_state(indoc! {"
5002 fn a() {
5003 b();
5004 if c() {
5005 ˇ
5006 }
5007 }
5008 "});
5009
5010 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5011 cx.assert_editor_state(indoc! {"
5012 fn a() {
5013 b();
5014 if c() {
5015 d(
5016 e
5017 );
5018 ˇ
5019 }
5020 }
5021 "});
5022
5023 cx.set_state(indoc! {"
5024 fn a() {
5025 b();
5026 ˇ
5027 }
5028 "});
5029
5030 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5031 cx.assert_editor_state(indoc! {"
5032 fn a() {
5033 b();
5034 d(
5035 e
5036 );
5037 ˇ
5038 }
5039 "});
5040}
5041
5042#[gpui::test]
5043fn test_select_all(cx: &mut TestAppContext) {
5044 init_test(cx, |_| {});
5045
5046 let editor = cx.add_window(|window, cx| {
5047 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5048 build_editor(buffer, window, cx)
5049 });
5050 _ = editor.update(cx, |editor, window, cx| {
5051 editor.select_all(&SelectAll, window, cx);
5052 assert_eq!(
5053 editor.selections.display_ranges(cx),
5054 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5055 );
5056 });
5057}
5058
5059#[gpui::test]
5060fn test_select_line(cx: &mut TestAppContext) {
5061 init_test(cx, |_| {});
5062
5063 let editor = cx.add_window(|window, cx| {
5064 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5065 build_editor(buffer, window, cx)
5066 });
5067 _ = editor.update(cx, |editor, window, cx| {
5068 editor.change_selections(None, window, cx, |s| {
5069 s.select_display_ranges([
5070 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5071 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5072 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5073 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5074 ])
5075 });
5076 editor.select_line(&SelectLine, window, cx);
5077 assert_eq!(
5078 editor.selections.display_ranges(cx),
5079 vec![
5080 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5081 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5082 ]
5083 );
5084 });
5085
5086 _ = editor.update(cx, |editor, window, cx| {
5087 editor.select_line(&SelectLine, window, cx);
5088 assert_eq!(
5089 editor.selections.display_ranges(cx),
5090 vec![
5091 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5092 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5093 ]
5094 );
5095 });
5096
5097 _ = editor.update(cx, |editor, window, cx| {
5098 editor.select_line(&SelectLine, window, cx);
5099 assert_eq!(
5100 editor.selections.display_ranges(cx),
5101 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5102 );
5103 });
5104}
5105
5106#[gpui::test]
5107async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5108 init_test(cx, |_| {});
5109 let mut cx = EditorTestContext::new(cx).await;
5110
5111 #[track_caller]
5112 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5113 cx.set_state(initial_state);
5114 cx.update_editor(|e, window, cx| {
5115 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5116 });
5117 cx.assert_editor_state(expected_state);
5118 }
5119
5120 // Selection starts and ends at the middle of lines, left-to-right
5121 test(
5122 &mut cx,
5123 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5124 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5125 );
5126 // Same thing, right-to-left
5127 test(
5128 &mut cx,
5129 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5130 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5131 );
5132
5133 // Whole buffer, left-to-right, last line *doesn't* end with newline
5134 test(
5135 &mut cx,
5136 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5137 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5138 );
5139 // Same thing, right-to-left
5140 test(
5141 &mut cx,
5142 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5143 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5144 );
5145
5146 // Whole buffer, left-to-right, last line ends with newline
5147 test(
5148 &mut cx,
5149 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5150 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5151 );
5152 // Same thing, right-to-left
5153 test(
5154 &mut cx,
5155 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5156 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5157 );
5158
5159 // Starts at the end of a line, ends at the start of another
5160 test(
5161 &mut cx,
5162 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5163 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5164 );
5165}
5166
5167#[gpui::test]
5168async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5169 init_test(cx, |_| {});
5170
5171 let editor = cx.add_window(|window, cx| {
5172 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5173 build_editor(buffer, window, cx)
5174 });
5175
5176 // setup
5177 _ = editor.update(cx, |editor, window, cx| {
5178 editor.fold_creases(
5179 vec![
5180 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5181 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5182 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5183 ],
5184 true,
5185 window,
5186 cx,
5187 );
5188 assert_eq!(
5189 editor.display_text(cx),
5190 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5191 );
5192 });
5193
5194 _ = editor.update(cx, |editor, window, cx| {
5195 editor.change_selections(None, window, cx, |s| {
5196 s.select_display_ranges([
5197 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5198 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5199 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5200 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5201 ])
5202 });
5203 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5204 assert_eq!(
5205 editor.display_text(cx),
5206 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5207 );
5208 });
5209 EditorTestContext::for_editor(editor, cx)
5210 .await
5211 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5212
5213 _ = editor.update(cx, |editor, window, cx| {
5214 editor.change_selections(None, window, cx, |s| {
5215 s.select_display_ranges([
5216 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5217 ])
5218 });
5219 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5220 assert_eq!(
5221 editor.display_text(cx),
5222 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5223 );
5224 assert_eq!(
5225 editor.selections.display_ranges(cx),
5226 [
5227 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5228 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5229 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5230 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5231 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5232 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5233 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5234 ]
5235 );
5236 });
5237 EditorTestContext::for_editor(editor, cx)
5238 .await
5239 .assert_editor_state(
5240 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5241 );
5242}
5243
5244#[gpui::test]
5245async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5246 init_test(cx, |_| {});
5247
5248 let mut cx = EditorTestContext::new(cx).await;
5249
5250 cx.set_state(indoc!(
5251 r#"abc
5252 defˇghi
5253
5254 jk
5255 nlmo
5256 "#
5257 ));
5258
5259 cx.update_editor(|editor, window, cx| {
5260 editor.add_selection_above(&Default::default(), window, cx);
5261 });
5262
5263 cx.assert_editor_state(indoc!(
5264 r#"abcˇ
5265 defˇghi
5266
5267 jk
5268 nlmo
5269 "#
5270 ));
5271
5272 cx.update_editor(|editor, window, cx| {
5273 editor.add_selection_above(&Default::default(), window, cx);
5274 });
5275
5276 cx.assert_editor_state(indoc!(
5277 r#"abcˇ
5278 defˇghi
5279
5280 jk
5281 nlmo
5282 "#
5283 ));
5284
5285 cx.update_editor(|editor, window, cx| {
5286 editor.add_selection_below(&Default::default(), window, cx);
5287 });
5288
5289 cx.assert_editor_state(indoc!(
5290 r#"abc
5291 defˇghi
5292
5293 jk
5294 nlmo
5295 "#
5296 ));
5297
5298 cx.update_editor(|editor, window, cx| {
5299 editor.undo_selection(&Default::default(), window, cx);
5300 });
5301
5302 cx.assert_editor_state(indoc!(
5303 r#"abcˇ
5304 defˇghi
5305
5306 jk
5307 nlmo
5308 "#
5309 ));
5310
5311 cx.update_editor(|editor, window, cx| {
5312 editor.redo_selection(&Default::default(), window, cx);
5313 });
5314
5315 cx.assert_editor_state(indoc!(
5316 r#"abc
5317 defˇghi
5318
5319 jk
5320 nlmo
5321 "#
5322 ));
5323
5324 cx.update_editor(|editor, window, cx| {
5325 editor.add_selection_below(&Default::default(), window, cx);
5326 });
5327
5328 cx.assert_editor_state(indoc!(
5329 r#"abc
5330 defˇghi
5331
5332 jk
5333 nlmˇo
5334 "#
5335 ));
5336
5337 cx.update_editor(|editor, window, cx| {
5338 editor.add_selection_below(&Default::default(), window, cx);
5339 });
5340
5341 cx.assert_editor_state(indoc!(
5342 r#"abc
5343 defˇghi
5344
5345 jk
5346 nlmˇo
5347 "#
5348 ));
5349
5350 // change selections
5351 cx.set_state(indoc!(
5352 r#"abc
5353 def«ˇg»hi
5354
5355 jk
5356 nlmo
5357 "#
5358 ));
5359
5360 cx.update_editor(|editor, window, cx| {
5361 editor.add_selection_below(&Default::default(), window, cx);
5362 });
5363
5364 cx.assert_editor_state(indoc!(
5365 r#"abc
5366 def«ˇg»hi
5367
5368 jk
5369 nlm«ˇo»
5370 "#
5371 ));
5372
5373 cx.update_editor(|editor, window, cx| {
5374 editor.add_selection_below(&Default::default(), window, cx);
5375 });
5376
5377 cx.assert_editor_state(indoc!(
5378 r#"abc
5379 def«ˇg»hi
5380
5381 jk
5382 nlm«ˇo»
5383 "#
5384 ));
5385
5386 cx.update_editor(|editor, window, cx| {
5387 editor.add_selection_above(&Default::default(), window, cx);
5388 });
5389
5390 cx.assert_editor_state(indoc!(
5391 r#"abc
5392 def«ˇg»hi
5393
5394 jk
5395 nlmo
5396 "#
5397 ));
5398
5399 cx.update_editor(|editor, window, cx| {
5400 editor.add_selection_above(&Default::default(), window, cx);
5401 });
5402
5403 cx.assert_editor_state(indoc!(
5404 r#"abc
5405 def«ˇg»hi
5406
5407 jk
5408 nlmo
5409 "#
5410 ));
5411
5412 // Change selections again
5413 cx.set_state(indoc!(
5414 r#"a«bc
5415 defgˇ»hi
5416
5417 jk
5418 nlmo
5419 "#
5420 ));
5421
5422 cx.update_editor(|editor, window, cx| {
5423 editor.add_selection_below(&Default::default(), window, cx);
5424 });
5425
5426 cx.assert_editor_state(indoc!(
5427 r#"a«bcˇ»
5428 d«efgˇ»hi
5429
5430 j«kˇ»
5431 nlmo
5432 "#
5433 ));
5434
5435 cx.update_editor(|editor, window, cx| {
5436 editor.add_selection_below(&Default::default(), window, cx);
5437 });
5438 cx.assert_editor_state(indoc!(
5439 r#"a«bcˇ»
5440 d«efgˇ»hi
5441
5442 j«kˇ»
5443 n«lmoˇ»
5444 "#
5445 ));
5446 cx.update_editor(|editor, window, cx| {
5447 editor.add_selection_above(&Default::default(), window, cx);
5448 });
5449
5450 cx.assert_editor_state(indoc!(
5451 r#"a«bcˇ»
5452 d«efgˇ»hi
5453
5454 j«kˇ»
5455 nlmo
5456 "#
5457 ));
5458
5459 // Change selections again
5460 cx.set_state(indoc!(
5461 r#"abc
5462 d«ˇefghi
5463
5464 jk
5465 nlm»o
5466 "#
5467 ));
5468
5469 cx.update_editor(|editor, window, cx| {
5470 editor.add_selection_above(&Default::default(), window, cx);
5471 });
5472
5473 cx.assert_editor_state(indoc!(
5474 r#"a«ˇbc»
5475 d«ˇef»ghi
5476
5477 j«ˇk»
5478 n«ˇlm»o
5479 "#
5480 ));
5481
5482 cx.update_editor(|editor, window, cx| {
5483 editor.add_selection_below(&Default::default(), window, cx);
5484 });
5485
5486 cx.assert_editor_state(indoc!(
5487 r#"abc
5488 d«ˇef»ghi
5489
5490 j«ˇk»
5491 n«ˇlm»o
5492 "#
5493 ));
5494}
5495
5496#[gpui::test]
5497async fn test_select_next(cx: &mut TestAppContext) {
5498 init_test(cx, |_| {});
5499
5500 let mut cx = EditorTestContext::new(cx).await;
5501 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5502
5503 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5504 .unwrap();
5505 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5506
5507 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5508 .unwrap();
5509 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5510
5511 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5512 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5513
5514 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5515 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5516
5517 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5518 .unwrap();
5519 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5520
5521 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5522 .unwrap();
5523 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5524}
5525
5526#[gpui::test]
5527async fn test_select_all_matches(cx: &mut TestAppContext) {
5528 init_test(cx, |_| {});
5529
5530 let mut cx = EditorTestContext::new(cx).await;
5531
5532 // Test caret-only selections
5533 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5534 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5535 .unwrap();
5536 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5537
5538 // Test left-to-right selections
5539 cx.set_state("abc\n«abcˇ»\nabc");
5540 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5541 .unwrap();
5542 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5543
5544 // Test right-to-left selections
5545 cx.set_state("abc\n«ˇabc»\nabc");
5546 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5547 .unwrap();
5548 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5549
5550 // Test selecting whitespace with caret selection
5551 cx.set_state("abc\nˇ abc\nabc");
5552 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5553 .unwrap();
5554 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5555
5556 // Test selecting whitespace with left-to-right selection
5557 cx.set_state("abc\n«ˇ »abc\nabc");
5558 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5559 .unwrap();
5560 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
5561
5562 // Test no matches with right-to-left selection
5563 cx.set_state("abc\n« ˇ»abc\nabc");
5564 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5565 .unwrap();
5566 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5567}
5568
5569#[gpui::test]
5570async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
5571 init_test(cx, |_| {});
5572
5573 let mut cx = EditorTestContext::new(cx).await;
5574 cx.set_state(
5575 r#"let foo = 2;
5576lˇet foo = 2;
5577let fooˇ = 2;
5578let foo = 2;
5579let foo = ˇ2;"#,
5580 );
5581
5582 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5583 .unwrap();
5584 cx.assert_editor_state(
5585 r#"let foo = 2;
5586«letˇ» foo = 2;
5587let «fooˇ» = 2;
5588let foo = 2;
5589let foo = «2ˇ»;"#,
5590 );
5591
5592 // noop for multiple selections with different contents
5593 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5594 .unwrap();
5595 cx.assert_editor_state(
5596 r#"let foo = 2;
5597«letˇ» foo = 2;
5598let «fooˇ» = 2;
5599let foo = 2;
5600let foo = «2ˇ»;"#,
5601 );
5602}
5603
5604#[gpui::test]
5605async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
5606 init_test(cx, |_| {});
5607
5608 let mut cx =
5609 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
5610
5611 cx.assert_editor_state(indoc! {"
5612 ˇbbb
5613 ccc
5614
5615 bbb
5616 ccc
5617 "});
5618 cx.dispatch_action(SelectPrevious::default());
5619 cx.assert_editor_state(indoc! {"
5620 «bbbˇ»
5621 ccc
5622
5623 bbb
5624 ccc
5625 "});
5626 cx.dispatch_action(SelectPrevious::default());
5627 cx.assert_editor_state(indoc! {"
5628 «bbbˇ»
5629 ccc
5630
5631 «bbbˇ»
5632 ccc
5633 "});
5634}
5635
5636#[gpui::test]
5637async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
5638 init_test(cx, |_| {});
5639
5640 let mut cx = EditorTestContext::new(cx).await;
5641 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5642
5643 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5644 .unwrap();
5645 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5646
5647 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5648 .unwrap();
5649 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5650
5651 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5652 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5653
5654 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5655 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5656
5657 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5658 .unwrap();
5659 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
5660
5661 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5662 .unwrap();
5663 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
5664
5665 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5666 .unwrap();
5667 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5668}
5669
5670#[gpui::test]
5671async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
5672 init_test(cx, |_| {});
5673
5674 let mut cx = EditorTestContext::new(cx).await;
5675 cx.set_state("aˇ");
5676
5677 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5678 .unwrap();
5679 cx.assert_editor_state("«aˇ»");
5680 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5681 .unwrap();
5682 cx.assert_editor_state("«aˇ»");
5683}
5684
5685#[gpui::test]
5686async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
5687 init_test(cx, |_| {});
5688
5689 let mut cx = EditorTestContext::new(cx).await;
5690 cx.set_state(
5691 r#"let foo = 2;
5692lˇet foo = 2;
5693let fooˇ = 2;
5694let foo = 2;
5695let foo = ˇ2;"#,
5696 );
5697
5698 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5699 .unwrap();
5700 cx.assert_editor_state(
5701 r#"let foo = 2;
5702«letˇ» foo = 2;
5703let «fooˇ» = 2;
5704let foo = 2;
5705let foo = «2ˇ»;"#,
5706 );
5707
5708 // noop for multiple selections with different contents
5709 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5710 .unwrap();
5711 cx.assert_editor_state(
5712 r#"let foo = 2;
5713«letˇ» foo = 2;
5714let «fooˇ» = 2;
5715let foo = 2;
5716let foo = «2ˇ»;"#,
5717 );
5718}
5719
5720#[gpui::test]
5721async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
5722 init_test(cx, |_| {});
5723
5724 let mut cx = EditorTestContext::new(cx).await;
5725 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5726
5727 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5728 .unwrap();
5729 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5730
5731 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5732 .unwrap();
5733 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5734
5735 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5736 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5737
5738 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5739 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5740
5741 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5742 .unwrap();
5743 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
5744
5745 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5746 .unwrap();
5747 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5748}
5749
5750#[gpui::test]
5751async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
5752 init_test(cx, |_| {});
5753
5754 let language = Arc::new(Language::new(
5755 LanguageConfig::default(),
5756 Some(tree_sitter_rust::LANGUAGE.into()),
5757 ));
5758
5759 let text = r#"
5760 use mod1::mod2::{mod3, mod4};
5761
5762 fn fn_1(param1: bool, param2: &str) {
5763 let var1 = "text";
5764 }
5765 "#
5766 .unindent();
5767
5768 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5769 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5770 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5771
5772 editor
5773 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5774 .await;
5775
5776 editor.update_in(cx, |editor, window, cx| {
5777 editor.change_selections(None, window, cx, |s| {
5778 s.select_display_ranges([
5779 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
5780 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
5781 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
5782 ]);
5783 });
5784 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5785 });
5786 editor.update(cx, |editor, cx| {
5787 assert_text_with_selections(
5788 editor,
5789 indoc! {r#"
5790 use mod1::mod2::{mod3, «mod4ˇ»};
5791
5792 fn fn_1«ˇ(param1: bool, param2: &str)» {
5793 let var1 = "«textˇ»";
5794 }
5795 "#},
5796 cx,
5797 );
5798 });
5799
5800 editor.update_in(cx, |editor, window, cx| {
5801 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5802 });
5803 editor.update(cx, |editor, cx| {
5804 assert_text_with_selections(
5805 editor,
5806 indoc! {r#"
5807 use mod1::mod2::«{mod3, mod4}ˇ»;
5808
5809 «ˇfn fn_1(param1: bool, param2: &str) {
5810 let var1 = "text";
5811 }»
5812 "#},
5813 cx,
5814 );
5815 });
5816
5817 editor.update_in(cx, |editor, window, cx| {
5818 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5819 });
5820 assert_eq!(
5821 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
5822 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
5823 );
5824
5825 // Trying to expand the selected syntax node one more time has no effect.
5826 editor.update_in(cx, |editor, window, cx| {
5827 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5828 });
5829 assert_eq!(
5830 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
5831 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
5832 );
5833
5834 editor.update_in(cx, |editor, window, cx| {
5835 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5836 });
5837 editor.update(cx, |editor, cx| {
5838 assert_text_with_selections(
5839 editor,
5840 indoc! {r#"
5841 use mod1::mod2::«{mod3, mod4}ˇ»;
5842
5843 «ˇfn fn_1(param1: bool, param2: &str) {
5844 let var1 = "text";
5845 }»
5846 "#},
5847 cx,
5848 );
5849 });
5850
5851 editor.update_in(cx, |editor, window, cx| {
5852 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5853 });
5854 editor.update(cx, |editor, cx| {
5855 assert_text_with_selections(
5856 editor,
5857 indoc! {r#"
5858 use mod1::mod2::{mod3, «mod4ˇ»};
5859
5860 fn fn_1«ˇ(param1: bool, param2: &str)» {
5861 let var1 = "«textˇ»";
5862 }
5863 "#},
5864 cx,
5865 );
5866 });
5867
5868 editor.update_in(cx, |editor, window, cx| {
5869 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5870 });
5871 editor.update(cx, |editor, cx| {
5872 assert_text_with_selections(
5873 editor,
5874 indoc! {r#"
5875 use mod1::mod2::{mod3, mo«ˇ»d4};
5876
5877 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
5878 let var1 = "te«ˇ»xt";
5879 }
5880 "#},
5881 cx,
5882 );
5883 });
5884
5885 // Trying to shrink the selected syntax node one more time has no effect.
5886 editor.update_in(cx, |editor, window, cx| {
5887 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5888 });
5889 editor.update_in(cx, |editor, _, cx| {
5890 assert_text_with_selections(
5891 editor,
5892 indoc! {r#"
5893 use mod1::mod2::{mod3, mo«ˇ»d4};
5894
5895 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
5896 let var1 = "te«ˇ»xt";
5897 }
5898 "#},
5899 cx,
5900 );
5901 });
5902
5903 // Ensure that we keep expanding the selection if the larger selection starts or ends within
5904 // a fold.
5905 editor.update_in(cx, |editor, window, cx| {
5906 editor.fold_creases(
5907 vec![
5908 Crease::simple(
5909 Point::new(0, 21)..Point::new(0, 24),
5910 FoldPlaceholder::test(),
5911 ),
5912 Crease::simple(
5913 Point::new(3, 20)..Point::new(3, 22),
5914 FoldPlaceholder::test(),
5915 ),
5916 ],
5917 true,
5918 window,
5919 cx,
5920 );
5921 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5922 });
5923 editor.update(cx, |editor, cx| {
5924 assert_text_with_selections(
5925 editor,
5926 indoc! {r#"
5927 use mod1::mod2::«{mod3, mod4}ˇ»;
5928
5929 fn fn_1«ˇ(param1: bool, param2: &str)» {
5930 «let var1 = "text";ˇ»
5931 }
5932 "#},
5933 cx,
5934 );
5935 });
5936}
5937
5938#[gpui::test]
5939async fn test_fold_function_bodies(cx: &mut TestAppContext) {
5940 init_test(cx, |_| {});
5941
5942 let base_text = r#"
5943 impl A {
5944 // this is an uncommitted comment
5945
5946 fn b() {
5947 c();
5948 }
5949
5950 // this is another uncommitted comment
5951
5952 fn d() {
5953 // e
5954 // f
5955 }
5956 }
5957
5958 fn g() {
5959 // h
5960 }
5961 "#
5962 .unindent();
5963
5964 let text = r#"
5965 ˇimpl A {
5966
5967 fn b() {
5968 c();
5969 }
5970
5971 fn d() {
5972 // e
5973 // f
5974 }
5975 }
5976
5977 fn g() {
5978 // h
5979 }
5980 "#
5981 .unindent();
5982
5983 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
5984 cx.set_state(&text);
5985 cx.set_head_text(&base_text);
5986 cx.update_editor(|editor, window, cx| {
5987 editor.expand_all_diff_hunks(&Default::default(), window, cx);
5988 });
5989
5990 cx.assert_state_with_diff(
5991 "
5992 ˇimpl A {
5993 - // this is an uncommitted comment
5994
5995 fn b() {
5996 c();
5997 }
5998
5999 - // this is another uncommitted comment
6000 -
6001 fn d() {
6002 // e
6003 // f
6004 }
6005 }
6006
6007 fn g() {
6008 // h
6009 }
6010 "
6011 .unindent(),
6012 );
6013
6014 let expected_display_text = "
6015 impl A {
6016 // this is an uncommitted comment
6017
6018 fn b() {
6019 ⋯
6020 }
6021
6022 // this is another uncommitted comment
6023
6024 fn d() {
6025 ⋯
6026 }
6027 }
6028
6029 fn g() {
6030 ⋯
6031 }
6032 "
6033 .unindent();
6034
6035 cx.update_editor(|editor, window, cx| {
6036 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
6037 assert_eq!(editor.display_text(cx), expected_display_text);
6038 });
6039}
6040
6041#[gpui::test]
6042async fn test_autoindent(cx: &mut TestAppContext) {
6043 init_test(cx, |_| {});
6044
6045 let language = Arc::new(
6046 Language::new(
6047 LanguageConfig {
6048 brackets: BracketPairConfig {
6049 pairs: vec![
6050 BracketPair {
6051 start: "{".to_string(),
6052 end: "}".to_string(),
6053 close: false,
6054 surround: false,
6055 newline: true,
6056 },
6057 BracketPair {
6058 start: "(".to_string(),
6059 end: ")".to_string(),
6060 close: false,
6061 surround: false,
6062 newline: true,
6063 },
6064 ],
6065 ..Default::default()
6066 },
6067 ..Default::default()
6068 },
6069 Some(tree_sitter_rust::LANGUAGE.into()),
6070 )
6071 .with_indents_query(
6072 r#"
6073 (_ "(" ")" @end) @indent
6074 (_ "{" "}" @end) @indent
6075 "#,
6076 )
6077 .unwrap(),
6078 );
6079
6080 let text = "fn a() {}";
6081
6082 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6083 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6084 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6085 editor
6086 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6087 .await;
6088
6089 editor.update_in(cx, |editor, window, cx| {
6090 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
6091 editor.newline(&Newline, window, cx);
6092 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
6093 assert_eq!(
6094 editor.selections.ranges(cx),
6095 &[
6096 Point::new(1, 4)..Point::new(1, 4),
6097 Point::new(3, 4)..Point::new(3, 4),
6098 Point::new(5, 0)..Point::new(5, 0)
6099 ]
6100 );
6101 });
6102}
6103
6104#[gpui::test]
6105async fn test_autoindent_selections(cx: &mut TestAppContext) {
6106 init_test(cx, |_| {});
6107
6108 {
6109 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6110 cx.set_state(indoc! {"
6111 impl A {
6112
6113 fn b() {}
6114
6115 «fn c() {
6116
6117 }ˇ»
6118 }
6119 "});
6120
6121 cx.update_editor(|editor, window, cx| {
6122 editor.autoindent(&Default::default(), window, cx);
6123 });
6124
6125 cx.assert_editor_state(indoc! {"
6126 impl A {
6127
6128 fn b() {}
6129
6130 «fn c() {
6131
6132 }ˇ»
6133 }
6134 "});
6135 }
6136
6137 {
6138 let mut cx = EditorTestContext::new_multibuffer(
6139 cx,
6140 [indoc! { "
6141 impl A {
6142 «
6143 // a
6144 fn b(){}
6145 »
6146 «
6147 }
6148 fn c(){}
6149 »
6150 "}],
6151 );
6152
6153 let buffer = cx.update_editor(|editor, _, cx| {
6154 let buffer = editor.buffer().update(cx, |buffer, _| {
6155 buffer.all_buffers().iter().next().unwrap().clone()
6156 });
6157 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6158 buffer
6159 });
6160
6161 cx.run_until_parked();
6162 cx.update_editor(|editor, window, cx| {
6163 editor.select_all(&Default::default(), window, cx);
6164 editor.autoindent(&Default::default(), window, cx)
6165 });
6166 cx.run_until_parked();
6167
6168 cx.update(|_, cx| {
6169 pretty_assertions::assert_eq!(
6170 buffer.read(cx).text(),
6171 indoc! { "
6172 impl A {
6173
6174 // a
6175 fn b(){}
6176
6177
6178 }
6179 fn c(){}
6180
6181 " }
6182 )
6183 });
6184 }
6185}
6186
6187#[gpui::test]
6188async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
6189 init_test(cx, |_| {});
6190
6191 let mut cx = EditorTestContext::new(cx).await;
6192
6193 let language = Arc::new(Language::new(
6194 LanguageConfig {
6195 brackets: BracketPairConfig {
6196 pairs: vec![
6197 BracketPair {
6198 start: "{".to_string(),
6199 end: "}".to_string(),
6200 close: true,
6201 surround: true,
6202 newline: true,
6203 },
6204 BracketPair {
6205 start: "(".to_string(),
6206 end: ")".to_string(),
6207 close: true,
6208 surround: true,
6209 newline: true,
6210 },
6211 BracketPair {
6212 start: "/*".to_string(),
6213 end: " */".to_string(),
6214 close: true,
6215 surround: true,
6216 newline: true,
6217 },
6218 BracketPair {
6219 start: "[".to_string(),
6220 end: "]".to_string(),
6221 close: false,
6222 surround: false,
6223 newline: true,
6224 },
6225 BracketPair {
6226 start: "\"".to_string(),
6227 end: "\"".to_string(),
6228 close: true,
6229 surround: true,
6230 newline: false,
6231 },
6232 BracketPair {
6233 start: "<".to_string(),
6234 end: ">".to_string(),
6235 close: false,
6236 surround: true,
6237 newline: true,
6238 },
6239 ],
6240 ..Default::default()
6241 },
6242 autoclose_before: "})]".to_string(),
6243 ..Default::default()
6244 },
6245 Some(tree_sitter_rust::LANGUAGE.into()),
6246 ));
6247
6248 cx.language_registry().add(language.clone());
6249 cx.update_buffer(|buffer, cx| {
6250 buffer.set_language(Some(language), cx);
6251 });
6252
6253 cx.set_state(
6254 &r#"
6255 🏀ˇ
6256 εˇ
6257 ❤️ˇ
6258 "#
6259 .unindent(),
6260 );
6261
6262 // autoclose multiple nested brackets at multiple cursors
6263 cx.update_editor(|editor, window, cx| {
6264 editor.handle_input("{", window, cx);
6265 editor.handle_input("{", window, cx);
6266 editor.handle_input("{", window, cx);
6267 });
6268 cx.assert_editor_state(
6269 &"
6270 🏀{{{ˇ}}}
6271 ε{{{ˇ}}}
6272 ❤️{{{ˇ}}}
6273 "
6274 .unindent(),
6275 );
6276
6277 // insert a different closing bracket
6278 cx.update_editor(|editor, window, cx| {
6279 editor.handle_input(")", window, cx);
6280 });
6281 cx.assert_editor_state(
6282 &"
6283 🏀{{{)ˇ}}}
6284 ε{{{)ˇ}}}
6285 ❤️{{{)ˇ}}}
6286 "
6287 .unindent(),
6288 );
6289
6290 // skip over the auto-closed brackets when typing a closing bracket
6291 cx.update_editor(|editor, window, cx| {
6292 editor.move_right(&MoveRight, window, cx);
6293 editor.handle_input("}", window, cx);
6294 editor.handle_input("}", window, cx);
6295 editor.handle_input("}", window, cx);
6296 });
6297 cx.assert_editor_state(
6298 &"
6299 🏀{{{)}}}}ˇ
6300 ε{{{)}}}}ˇ
6301 ❤️{{{)}}}}ˇ
6302 "
6303 .unindent(),
6304 );
6305
6306 // autoclose multi-character pairs
6307 cx.set_state(
6308 &"
6309 ˇ
6310 ˇ
6311 "
6312 .unindent(),
6313 );
6314 cx.update_editor(|editor, window, cx| {
6315 editor.handle_input("/", window, cx);
6316 editor.handle_input("*", window, cx);
6317 });
6318 cx.assert_editor_state(
6319 &"
6320 /*ˇ */
6321 /*ˇ */
6322 "
6323 .unindent(),
6324 );
6325
6326 // one cursor autocloses a multi-character pair, one cursor
6327 // does not autoclose.
6328 cx.set_state(
6329 &"
6330 /ˇ
6331 ˇ
6332 "
6333 .unindent(),
6334 );
6335 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6336 cx.assert_editor_state(
6337 &"
6338 /*ˇ */
6339 *ˇ
6340 "
6341 .unindent(),
6342 );
6343
6344 // Don't autoclose if the next character isn't whitespace and isn't
6345 // listed in the language's "autoclose_before" section.
6346 cx.set_state("ˇa b");
6347 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6348 cx.assert_editor_state("{ˇa b");
6349
6350 // Don't autoclose if `close` is false for the bracket pair
6351 cx.set_state("ˇ");
6352 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6353 cx.assert_editor_state("[ˇ");
6354
6355 // Surround with brackets if text is selected
6356 cx.set_state("«aˇ» b");
6357 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6358 cx.assert_editor_state("{«aˇ»} b");
6359
6360 // Autoclose when not immediately after a word character
6361 cx.set_state("a ˇ");
6362 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6363 cx.assert_editor_state("a \"ˇ\"");
6364
6365 // Autoclose pair where the start and end characters are the same
6366 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6367 cx.assert_editor_state("a \"\"ˇ");
6368
6369 // Don't autoclose when immediately after a word character
6370 cx.set_state("aˇ");
6371 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6372 cx.assert_editor_state("a\"ˇ");
6373
6374 // Do autoclose when after a non-word character
6375 cx.set_state("{ˇ");
6376 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6377 cx.assert_editor_state("{\"ˇ\"");
6378
6379 // Non identical pairs autoclose regardless of preceding character
6380 cx.set_state("aˇ");
6381 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6382 cx.assert_editor_state("a{ˇ}");
6383
6384 // Don't autoclose pair if autoclose is disabled
6385 cx.set_state("ˇ");
6386 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6387 cx.assert_editor_state("<ˇ");
6388
6389 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6390 cx.set_state("«aˇ» b");
6391 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6392 cx.assert_editor_state("<«aˇ»> b");
6393}
6394
6395#[gpui::test]
6396async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
6397 init_test(cx, |settings| {
6398 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6399 });
6400
6401 let mut cx = EditorTestContext::new(cx).await;
6402
6403 let language = Arc::new(Language::new(
6404 LanguageConfig {
6405 brackets: BracketPairConfig {
6406 pairs: vec![
6407 BracketPair {
6408 start: "{".to_string(),
6409 end: "}".to_string(),
6410 close: true,
6411 surround: true,
6412 newline: true,
6413 },
6414 BracketPair {
6415 start: "(".to_string(),
6416 end: ")".to_string(),
6417 close: true,
6418 surround: true,
6419 newline: true,
6420 },
6421 BracketPair {
6422 start: "[".to_string(),
6423 end: "]".to_string(),
6424 close: false,
6425 surround: false,
6426 newline: true,
6427 },
6428 ],
6429 ..Default::default()
6430 },
6431 autoclose_before: "})]".to_string(),
6432 ..Default::default()
6433 },
6434 Some(tree_sitter_rust::LANGUAGE.into()),
6435 ));
6436
6437 cx.language_registry().add(language.clone());
6438 cx.update_buffer(|buffer, cx| {
6439 buffer.set_language(Some(language), cx);
6440 });
6441
6442 cx.set_state(
6443 &"
6444 ˇ
6445 ˇ
6446 ˇ
6447 "
6448 .unindent(),
6449 );
6450
6451 // ensure only matching closing brackets are skipped over
6452 cx.update_editor(|editor, window, cx| {
6453 editor.handle_input("}", window, cx);
6454 editor.move_left(&MoveLeft, window, cx);
6455 editor.handle_input(")", window, cx);
6456 editor.move_left(&MoveLeft, window, cx);
6457 });
6458 cx.assert_editor_state(
6459 &"
6460 ˇ)}
6461 ˇ)}
6462 ˇ)}
6463 "
6464 .unindent(),
6465 );
6466
6467 // skip-over closing brackets at multiple cursors
6468 cx.update_editor(|editor, window, cx| {
6469 editor.handle_input(")", window, cx);
6470 editor.handle_input("}", window, cx);
6471 });
6472 cx.assert_editor_state(
6473 &"
6474 )}ˇ
6475 )}ˇ
6476 )}ˇ
6477 "
6478 .unindent(),
6479 );
6480
6481 // ignore non-close brackets
6482 cx.update_editor(|editor, window, cx| {
6483 editor.handle_input("]", window, cx);
6484 editor.move_left(&MoveLeft, window, cx);
6485 editor.handle_input("]", window, cx);
6486 });
6487 cx.assert_editor_state(
6488 &"
6489 )}]ˇ]
6490 )}]ˇ]
6491 )}]ˇ]
6492 "
6493 .unindent(),
6494 );
6495}
6496
6497#[gpui::test]
6498async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
6499 init_test(cx, |_| {});
6500
6501 let mut cx = EditorTestContext::new(cx).await;
6502
6503 let html_language = Arc::new(
6504 Language::new(
6505 LanguageConfig {
6506 name: "HTML".into(),
6507 brackets: BracketPairConfig {
6508 pairs: vec![
6509 BracketPair {
6510 start: "<".into(),
6511 end: ">".into(),
6512 close: true,
6513 ..Default::default()
6514 },
6515 BracketPair {
6516 start: "{".into(),
6517 end: "}".into(),
6518 close: true,
6519 ..Default::default()
6520 },
6521 BracketPair {
6522 start: "(".into(),
6523 end: ")".into(),
6524 close: true,
6525 ..Default::default()
6526 },
6527 ],
6528 ..Default::default()
6529 },
6530 autoclose_before: "})]>".into(),
6531 ..Default::default()
6532 },
6533 Some(tree_sitter_html::LANGUAGE.into()),
6534 )
6535 .with_injection_query(
6536 r#"
6537 (script_element
6538 (raw_text) @injection.content
6539 (#set! injection.language "javascript"))
6540 "#,
6541 )
6542 .unwrap(),
6543 );
6544
6545 let javascript_language = Arc::new(Language::new(
6546 LanguageConfig {
6547 name: "JavaScript".into(),
6548 brackets: BracketPairConfig {
6549 pairs: vec![
6550 BracketPair {
6551 start: "/*".into(),
6552 end: " */".into(),
6553 close: true,
6554 ..Default::default()
6555 },
6556 BracketPair {
6557 start: "{".into(),
6558 end: "}".into(),
6559 close: true,
6560 ..Default::default()
6561 },
6562 BracketPair {
6563 start: "(".into(),
6564 end: ")".into(),
6565 close: true,
6566 ..Default::default()
6567 },
6568 ],
6569 ..Default::default()
6570 },
6571 autoclose_before: "})]>".into(),
6572 ..Default::default()
6573 },
6574 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6575 ));
6576
6577 cx.language_registry().add(html_language.clone());
6578 cx.language_registry().add(javascript_language.clone());
6579
6580 cx.update_buffer(|buffer, cx| {
6581 buffer.set_language(Some(html_language), cx);
6582 });
6583
6584 cx.set_state(
6585 &r#"
6586 <body>ˇ
6587 <script>
6588 var x = 1;ˇ
6589 </script>
6590 </body>ˇ
6591 "#
6592 .unindent(),
6593 );
6594
6595 // Precondition: different languages are active at different locations.
6596 cx.update_editor(|editor, window, cx| {
6597 let snapshot = editor.snapshot(window, cx);
6598 let cursors = editor.selections.ranges::<usize>(cx);
6599 let languages = cursors
6600 .iter()
6601 .map(|c| snapshot.language_at(c.start).unwrap().name())
6602 .collect::<Vec<_>>();
6603 assert_eq!(
6604 languages,
6605 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6606 );
6607 });
6608
6609 // Angle brackets autoclose in HTML, but not JavaScript.
6610 cx.update_editor(|editor, window, cx| {
6611 editor.handle_input("<", window, cx);
6612 editor.handle_input("a", window, cx);
6613 });
6614 cx.assert_editor_state(
6615 &r#"
6616 <body><aˇ>
6617 <script>
6618 var x = 1;<aˇ
6619 </script>
6620 </body><aˇ>
6621 "#
6622 .unindent(),
6623 );
6624
6625 // Curly braces and parens autoclose in both HTML and JavaScript.
6626 cx.update_editor(|editor, window, cx| {
6627 editor.handle_input(" b=", window, cx);
6628 editor.handle_input("{", window, cx);
6629 editor.handle_input("c", window, cx);
6630 editor.handle_input("(", window, cx);
6631 });
6632 cx.assert_editor_state(
6633 &r#"
6634 <body><a b={c(ˇ)}>
6635 <script>
6636 var x = 1;<a b={c(ˇ)}
6637 </script>
6638 </body><a b={c(ˇ)}>
6639 "#
6640 .unindent(),
6641 );
6642
6643 // Brackets that were already autoclosed are skipped.
6644 cx.update_editor(|editor, window, cx| {
6645 editor.handle_input(")", window, cx);
6646 editor.handle_input("d", window, cx);
6647 editor.handle_input("}", window, cx);
6648 });
6649 cx.assert_editor_state(
6650 &r#"
6651 <body><a b={c()d}ˇ>
6652 <script>
6653 var x = 1;<a b={c()d}ˇ
6654 </script>
6655 </body><a b={c()d}ˇ>
6656 "#
6657 .unindent(),
6658 );
6659 cx.update_editor(|editor, window, cx| {
6660 editor.handle_input(">", window, cx);
6661 });
6662 cx.assert_editor_state(
6663 &r#"
6664 <body><a b={c()d}>ˇ
6665 <script>
6666 var x = 1;<a b={c()d}>ˇ
6667 </script>
6668 </body><a b={c()d}>ˇ
6669 "#
6670 .unindent(),
6671 );
6672
6673 // Reset
6674 cx.set_state(
6675 &r#"
6676 <body>ˇ
6677 <script>
6678 var x = 1;ˇ
6679 </script>
6680 </body>ˇ
6681 "#
6682 .unindent(),
6683 );
6684
6685 cx.update_editor(|editor, window, cx| {
6686 editor.handle_input("<", window, cx);
6687 });
6688 cx.assert_editor_state(
6689 &r#"
6690 <body><ˇ>
6691 <script>
6692 var x = 1;<ˇ
6693 </script>
6694 </body><ˇ>
6695 "#
6696 .unindent(),
6697 );
6698
6699 // When backspacing, the closing angle brackets are removed.
6700 cx.update_editor(|editor, window, cx| {
6701 editor.backspace(&Backspace, window, cx);
6702 });
6703 cx.assert_editor_state(
6704 &r#"
6705 <body>ˇ
6706 <script>
6707 var x = 1;ˇ
6708 </script>
6709 </body>ˇ
6710 "#
6711 .unindent(),
6712 );
6713
6714 // Block comments autoclose in JavaScript, but not HTML.
6715 cx.update_editor(|editor, window, cx| {
6716 editor.handle_input("/", window, cx);
6717 editor.handle_input("*", window, cx);
6718 });
6719 cx.assert_editor_state(
6720 &r#"
6721 <body>/*ˇ
6722 <script>
6723 var x = 1;/*ˇ */
6724 </script>
6725 </body>/*ˇ
6726 "#
6727 .unindent(),
6728 );
6729}
6730
6731#[gpui::test]
6732async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
6733 init_test(cx, |_| {});
6734
6735 let mut cx = EditorTestContext::new(cx).await;
6736
6737 let rust_language = Arc::new(
6738 Language::new(
6739 LanguageConfig {
6740 name: "Rust".into(),
6741 brackets: serde_json::from_value(json!([
6742 { "start": "{", "end": "}", "close": true, "newline": true },
6743 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
6744 ]))
6745 .unwrap(),
6746 autoclose_before: "})]>".into(),
6747 ..Default::default()
6748 },
6749 Some(tree_sitter_rust::LANGUAGE.into()),
6750 )
6751 .with_override_query("(string_literal) @string")
6752 .unwrap(),
6753 );
6754
6755 cx.language_registry().add(rust_language.clone());
6756 cx.update_buffer(|buffer, cx| {
6757 buffer.set_language(Some(rust_language), cx);
6758 });
6759
6760 cx.set_state(
6761 &r#"
6762 let x = ˇ
6763 "#
6764 .unindent(),
6765 );
6766
6767 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
6768 cx.update_editor(|editor, window, cx| {
6769 editor.handle_input("\"", window, cx);
6770 });
6771 cx.assert_editor_state(
6772 &r#"
6773 let x = "ˇ"
6774 "#
6775 .unindent(),
6776 );
6777
6778 // Inserting another quotation mark. The cursor moves across the existing
6779 // automatically-inserted quotation mark.
6780 cx.update_editor(|editor, window, cx| {
6781 editor.handle_input("\"", window, cx);
6782 });
6783 cx.assert_editor_state(
6784 &r#"
6785 let x = ""ˇ
6786 "#
6787 .unindent(),
6788 );
6789
6790 // Reset
6791 cx.set_state(
6792 &r#"
6793 let x = ˇ
6794 "#
6795 .unindent(),
6796 );
6797
6798 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
6799 cx.update_editor(|editor, window, cx| {
6800 editor.handle_input("\"", window, cx);
6801 editor.handle_input(" ", window, cx);
6802 editor.move_left(&Default::default(), window, cx);
6803 editor.handle_input("\\", window, cx);
6804 editor.handle_input("\"", window, cx);
6805 });
6806 cx.assert_editor_state(
6807 &r#"
6808 let x = "\"ˇ "
6809 "#
6810 .unindent(),
6811 );
6812
6813 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
6814 // mark. Nothing is inserted.
6815 cx.update_editor(|editor, window, cx| {
6816 editor.move_right(&Default::default(), window, cx);
6817 editor.handle_input("\"", window, cx);
6818 });
6819 cx.assert_editor_state(
6820 &r#"
6821 let x = "\" "ˇ
6822 "#
6823 .unindent(),
6824 );
6825}
6826
6827#[gpui::test]
6828async fn test_surround_with_pair(cx: &mut TestAppContext) {
6829 init_test(cx, |_| {});
6830
6831 let language = Arc::new(Language::new(
6832 LanguageConfig {
6833 brackets: BracketPairConfig {
6834 pairs: vec![
6835 BracketPair {
6836 start: "{".to_string(),
6837 end: "}".to_string(),
6838 close: true,
6839 surround: true,
6840 newline: true,
6841 },
6842 BracketPair {
6843 start: "/* ".to_string(),
6844 end: "*/".to_string(),
6845 close: true,
6846 surround: true,
6847 ..Default::default()
6848 },
6849 ],
6850 ..Default::default()
6851 },
6852 ..Default::default()
6853 },
6854 Some(tree_sitter_rust::LANGUAGE.into()),
6855 ));
6856
6857 let text = r#"
6858 a
6859 b
6860 c
6861 "#
6862 .unindent();
6863
6864 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6865 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6866 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6867 editor
6868 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6869 .await;
6870
6871 editor.update_in(cx, |editor, window, cx| {
6872 editor.change_selections(None, window, cx, |s| {
6873 s.select_display_ranges([
6874 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6875 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6876 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
6877 ])
6878 });
6879
6880 editor.handle_input("{", window, cx);
6881 editor.handle_input("{", window, cx);
6882 editor.handle_input("{", window, cx);
6883 assert_eq!(
6884 editor.text(cx),
6885 "
6886 {{{a}}}
6887 {{{b}}}
6888 {{{c}}}
6889 "
6890 .unindent()
6891 );
6892 assert_eq!(
6893 editor.selections.display_ranges(cx),
6894 [
6895 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
6896 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
6897 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
6898 ]
6899 );
6900
6901 editor.undo(&Undo, window, cx);
6902 editor.undo(&Undo, window, cx);
6903 editor.undo(&Undo, window, cx);
6904 assert_eq!(
6905 editor.text(cx),
6906 "
6907 a
6908 b
6909 c
6910 "
6911 .unindent()
6912 );
6913 assert_eq!(
6914 editor.selections.display_ranges(cx),
6915 [
6916 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6917 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6918 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
6919 ]
6920 );
6921
6922 // Ensure inserting the first character of a multi-byte bracket pair
6923 // doesn't surround the selections with the bracket.
6924 editor.handle_input("/", window, cx);
6925 assert_eq!(
6926 editor.text(cx),
6927 "
6928 /
6929 /
6930 /
6931 "
6932 .unindent()
6933 );
6934 assert_eq!(
6935 editor.selections.display_ranges(cx),
6936 [
6937 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6938 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6939 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
6940 ]
6941 );
6942
6943 editor.undo(&Undo, window, cx);
6944 assert_eq!(
6945 editor.text(cx),
6946 "
6947 a
6948 b
6949 c
6950 "
6951 .unindent()
6952 );
6953 assert_eq!(
6954 editor.selections.display_ranges(cx),
6955 [
6956 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6957 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6958 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
6959 ]
6960 );
6961
6962 // Ensure inserting the last character of a multi-byte bracket pair
6963 // doesn't surround the selections with the bracket.
6964 editor.handle_input("*", window, cx);
6965 assert_eq!(
6966 editor.text(cx),
6967 "
6968 *
6969 *
6970 *
6971 "
6972 .unindent()
6973 );
6974 assert_eq!(
6975 editor.selections.display_ranges(cx),
6976 [
6977 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6978 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6979 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
6980 ]
6981 );
6982 });
6983}
6984
6985#[gpui::test]
6986async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
6987 init_test(cx, |_| {});
6988
6989 let language = Arc::new(Language::new(
6990 LanguageConfig {
6991 brackets: BracketPairConfig {
6992 pairs: vec![BracketPair {
6993 start: "{".to_string(),
6994 end: "}".to_string(),
6995 close: true,
6996 surround: true,
6997 newline: true,
6998 }],
6999 ..Default::default()
7000 },
7001 autoclose_before: "}".to_string(),
7002 ..Default::default()
7003 },
7004 Some(tree_sitter_rust::LANGUAGE.into()),
7005 ));
7006
7007 let text = r#"
7008 a
7009 b
7010 c
7011 "#
7012 .unindent();
7013
7014 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7015 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7016 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7017 editor
7018 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7019 .await;
7020
7021 editor.update_in(cx, |editor, window, cx| {
7022 editor.change_selections(None, window, cx, |s| {
7023 s.select_ranges([
7024 Point::new(0, 1)..Point::new(0, 1),
7025 Point::new(1, 1)..Point::new(1, 1),
7026 Point::new(2, 1)..Point::new(2, 1),
7027 ])
7028 });
7029
7030 editor.handle_input("{", window, cx);
7031 editor.handle_input("{", window, cx);
7032 editor.handle_input("_", window, cx);
7033 assert_eq!(
7034 editor.text(cx),
7035 "
7036 a{{_}}
7037 b{{_}}
7038 c{{_}}
7039 "
7040 .unindent()
7041 );
7042 assert_eq!(
7043 editor.selections.ranges::<Point>(cx),
7044 [
7045 Point::new(0, 4)..Point::new(0, 4),
7046 Point::new(1, 4)..Point::new(1, 4),
7047 Point::new(2, 4)..Point::new(2, 4)
7048 ]
7049 );
7050
7051 editor.backspace(&Default::default(), window, cx);
7052 editor.backspace(&Default::default(), window, cx);
7053 assert_eq!(
7054 editor.text(cx),
7055 "
7056 a{}
7057 b{}
7058 c{}
7059 "
7060 .unindent()
7061 );
7062 assert_eq!(
7063 editor.selections.ranges::<Point>(cx),
7064 [
7065 Point::new(0, 2)..Point::new(0, 2),
7066 Point::new(1, 2)..Point::new(1, 2),
7067 Point::new(2, 2)..Point::new(2, 2)
7068 ]
7069 );
7070
7071 editor.delete_to_previous_word_start(&Default::default(), window, cx);
7072 assert_eq!(
7073 editor.text(cx),
7074 "
7075 a
7076 b
7077 c
7078 "
7079 .unindent()
7080 );
7081 assert_eq!(
7082 editor.selections.ranges::<Point>(cx),
7083 [
7084 Point::new(0, 1)..Point::new(0, 1),
7085 Point::new(1, 1)..Point::new(1, 1),
7086 Point::new(2, 1)..Point::new(2, 1)
7087 ]
7088 );
7089 });
7090}
7091
7092#[gpui::test]
7093async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
7094 init_test(cx, |settings| {
7095 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7096 });
7097
7098 let mut cx = EditorTestContext::new(cx).await;
7099
7100 let language = Arc::new(Language::new(
7101 LanguageConfig {
7102 brackets: BracketPairConfig {
7103 pairs: vec![
7104 BracketPair {
7105 start: "{".to_string(),
7106 end: "}".to_string(),
7107 close: true,
7108 surround: true,
7109 newline: true,
7110 },
7111 BracketPair {
7112 start: "(".to_string(),
7113 end: ")".to_string(),
7114 close: true,
7115 surround: true,
7116 newline: true,
7117 },
7118 BracketPair {
7119 start: "[".to_string(),
7120 end: "]".to_string(),
7121 close: false,
7122 surround: true,
7123 newline: true,
7124 },
7125 ],
7126 ..Default::default()
7127 },
7128 autoclose_before: "})]".to_string(),
7129 ..Default::default()
7130 },
7131 Some(tree_sitter_rust::LANGUAGE.into()),
7132 ));
7133
7134 cx.language_registry().add(language.clone());
7135 cx.update_buffer(|buffer, cx| {
7136 buffer.set_language(Some(language), cx);
7137 });
7138
7139 cx.set_state(
7140 &"
7141 {(ˇ)}
7142 [[ˇ]]
7143 {(ˇ)}
7144 "
7145 .unindent(),
7146 );
7147
7148 cx.update_editor(|editor, window, cx| {
7149 editor.backspace(&Default::default(), window, cx);
7150 editor.backspace(&Default::default(), window, cx);
7151 });
7152
7153 cx.assert_editor_state(
7154 &"
7155 ˇ
7156 ˇ]]
7157 ˇ
7158 "
7159 .unindent(),
7160 );
7161
7162 cx.update_editor(|editor, window, cx| {
7163 editor.handle_input("{", window, cx);
7164 editor.handle_input("{", window, cx);
7165 editor.move_right(&MoveRight, window, cx);
7166 editor.move_right(&MoveRight, window, cx);
7167 editor.move_left(&MoveLeft, window, cx);
7168 editor.move_left(&MoveLeft, window, cx);
7169 editor.backspace(&Default::default(), window, cx);
7170 });
7171
7172 cx.assert_editor_state(
7173 &"
7174 {ˇ}
7175 {ˇ}]]
7176 {ˇ}
7177 "
7178 .unindent(),
7179 );
7180
7181 cx.update_editor(|editor, window, cx| {
7182 editor.backspace(&Default::default(), window, cx);
7183 });
7184
7185 cx.assert_editor_state(
7186 &"
7187 ˇ
7188 ˇ]]
7189 ˇ
7190 "
7191 .unindent(),
7192 );
7193}
7194
7195#[gpui::test]
7196async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
7197 init_test(cx, |_| {});
7198
7199 let language = Arc::new(Language::new(
7200 LanguageConfig::default(),
7201 Some(tree_sitter_rust::LANGUAGE.into()),
7202 ));
7203
7204 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
7205 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7206 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7207 editor
7208 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7209 .await;
7210
7211 editor.update_in(cx, |editor, window, cx| {
7212 editor.set_auto_replace_emoji_shortcode(true);
7213
7214 editor.handle_input("Hello ", window, cx);
7215 editor.handle_input(":wave", window, cx);
7216 assert_eq!(editor.text(cx), "Hello :wave".unindent());
7217
7218 editor.handle_input(":", window, cx);
7219 assert_eq!(editor.text(cx), "Hello 👋".unindent());
7220
7221 editor.handle_input(" :smile", window, cx);
7222 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
7223
7224 editor.handle_input(":", window, cx);
7225 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
7226
7227 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
7228 editor.handle_input(":wave", window, cx);
7229 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
7230
7231 editor.handle_input(":", window, cx);
7232 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
7233
7234 editor.handle_input(":1", window, cx);
7235 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
7236
7237 editor.handle_input(":", window, cx);
7238 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
7239
7240 // Ensure shortcode does not get replaced when it is part of a word
7241 editor.handle_input(" Test:wave", window, cx);
7242 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
7243
7244 editor.handle_input(":", window, cx);
7245 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
7246
7247 editor.set_auto_replace_emoji_shortcode(false);
7248
7249 // Ensure shortcode does not get replaced when auto replace is off
7250 editor.handle_input(" :wave", window, cx);
7251 assert_eq!(
7252 editor.text(cx),
7253 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
7254 );
7255
7256 editor.handle_input(":", window, cx);
7257 assert_eq!(
7258 editor.text(cx),
7259 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
7260 );
7261 });
7262}
7263
7264#[gpui::test]
7265async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
7266 init_test(cx, |_| {});
7267
7268 let (text, insertion_ranges) = marked_text_ranges(
7269 indoc! {"
7270 ˇ
7271 "},
7272 false,
7273 );
7274
7275 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7276 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7277
7278 _ = editor.update_in(cx, |editor, window, cx| {
7279 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7280
7281 editor
7282 .insert_snippet(&insertion_ranges, snippet, window, cx)
7283 .unwrap();
7284
7285 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7286 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7287 assert_eq!(editor.text(cx), expected_text);
7288 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7289 }
7290
7291 assert(
7292 editor,
7293 cx,
7294 indoc! {"
7295 type «» =•
7296 "},
7297 );
7298
7299 assert!(editor.context_menu_visible(), "There should be a matches");
7300 });
7301}
7302
7303#[gpui::test]
7304async fn test_snippets(cx: &mut TestAppContext) {
7305 init_test(cx, |_| {});
7306
7307 let (text, insertion_ranges) = marked_text_ranges(
7308 indoc! {"
7309 a.ˇ b
7310 a.ˇ b
7311 a.ˇ b
7312 "},
7313 false,
7314 );
7315
7316 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7317 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7318
7319 editor.update_in(cx, |editor, window, cx| {
7320 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
7321
7322 editor
7323 .insert_snippet(&insertion_ranges, snippet, window, cx)
7324 .unwrap();
7325
7326 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7327 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7328 assert_eq!(editor.text(cx), expected_text);
7329 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7330 }
7331
7332 assert(
7333 editor,
7334 cx,
7335 indoc! {"
7336 a.f(«one», two, «three») b
7337 a.f(«one», two, «three») b
7338 a.f(«one», two, «three») b
7339 "},
7340 );
7341
7342 // Can't move earlier than the first tab stop
7343 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7344 assert(
7345 editor,
7346 cx,
7347 indoc! {"
7348 a.f(«one», two, «three») b
7349 a.f(«one», two, «three») b
7350 a.f(«one», two, «three») b
7351 "},
7352 );
7353
7354 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7355 assert(
7356 editor,
7357 cx,
7358 indoc! {"
7359 a.f(one, «two», three) b
7360 a.f(one, «two», three) b
7361 a.f(one, «two», three) b
7362 "},
7363 );
7364
7365 editor.move_to_prev_snippet_tabstop(window, cx);
7366 assert(
7367 editor,
7368 cx,
7369 indoc! {"
7370 a.f(«one», two, «three») b
7371 a.f(«one», two, «three») b
7372 a.f(«one», two, «three») b
7373 "},
7374 );
7375
7376 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7377 assert(
7378 editor,
7379 cx,
7380 indoc! {"
7381 a.f(one, «two», three) b
7382 a.f(one, «two», three) b
7383 a.f(one, «two», three) b
7384 "},
7385 );
7386 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7387 assert(
7388 editor,
7389 cx,
7390 indoc! {"
7391 a.f(one, two, three)ˇ b
7392 a.f(one, two, three)ˇ b
7393 a.f(one, two, three)ˇ b
7394 "},
7395 );
7396
7397 // As soon as the last tab stop is reached, snippet state is gone
7398 editor.move_to_prev_snippet_tabstop(window, cx);
7399 assert(
7400 editor,
7401 cx,
7402 indoc! {"
7403 a.f(one, two, three)ˇ b
7404 a.f(one, two, three)ˇ b
7405 a.f(one, two, three)ˇ b
7406 "},
7407 );
7408 });
7409}
7410
7411#[gpui::test]
7412async fn test_document_format_during_save(cx: &mut TestAppContext) {
7413 init_test(cx, |_| {});
7414
7415 let fs = FakeFs::new(cx.executor());
7416 fs.insert_file(path!("/file.rs"), Default::default()).await;
7417
7418 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
7419
7420 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7421 language_registry.add(rust_lang());
7422 let mut fake_servers = language_registry.register_fake_lsp(
7423 "Rust",
7424 FakeLspAdapter {
7425 capabilities: lsp::ServerCapabilities {
7426 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7427 ..Default::default()
7428 },
7429 ..Default::default()
7430 },
7431 );
7432
7433 let buffer = project
7434 .update(cx, |project, cx| {
7435 project.open_local_buffer(path!("/file.rs"), cx)
7436 })
7437 .await
7438 .unwrap();
7439
7440 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7441 let (editor, cx) = cx.add_window_view(|window, cx| {
7442 build_editor_with_project(project.clone(), buffer, window, cx)
7443 });
7444 editor.update_in(cx, |editor, window, cx| {
7445 editor.set_text("one\ntwo\nthree\n", window, cx)
7446 });
7447 assert!(cx.read(|cx| editor.is_dirty(cx)));
7448
7449 cx.executor().start_waiting();
7450 let fake_server = fake_servers.next().await.unwrap();
7451
7452 let save = editor
7453 .update_in(cx, |editor, window, cx| {
7454 editor.save(true, project.clone(), window, cx)
7455 })
7456 .unwrap();
7457 fake_server
7458 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7459 assert_eq!(
7460 params.text_document.uri,
7461 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7462 );
7463 assert_eq!(params.options.tab_size, 4);
7464 Ok(Some(vec![lsp::TextEdit::new(
7465 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7466 ", ".to_string(),
7467 )]))
7468 })
7469 .next()
7470 .await;
7471 cx.executor().start_waiting();
7472 save.await;
7473
7474 assert_eq!(
7475 editor.update(cx, |editor, cx| editor.text(cx)),
7476 "one, two\nthree\n"
7477 );
7478 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7479
7480 editor.update_in(cx, |editor, window, cx| {
7481 editor.set_text("one\ntwo\nthree\n", window, cx)
7482 });
7483 assert!(cx.read(|cx| editor.is_dirty(cx)));
7484
7485 // Ensure we can still save even if formatting hangs.
7486 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7487 assert_eq!(
7488 params.text_document.uri,
7489 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7490 );
7491 futures::future::pending::<()>().await;
7492 unreachable!()
7493 });
7494 let save = editor
7495 .update_in(cx, |editor, window, cx| {
7496 editor.save(true, project.clone(), window, cx)
7497 })
7498 .unwrap();
7499 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7500 cx.executor().start_waiting();
7501 save.await;
7502 assert_eq!(
7503 editor.update(cx, |editor, cx| editor.text(cx)),
7504 "one\ntwo\nthree\n"
7505 );
7506 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7507
7508 // For non-dirty buffer, no formatting request should be sent
7509 let save = editor
7510 .update_in(cx, |editor, window, cx| {
7511 editor.save(true, project.clone(), window, cx)
7512 })
7513 .unwrap();
7514 let _pending_format_request = fake_server
7515 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7516 panic!("Should not be invoked on non-dirty buffer");
7517 })
7518 .next();
7519 cx.executor().start_waiting();
7520 save.await;
7521
7522 // Set rust language override and assert overridden tabsize is sent to language server
7523 update_test_language_settings(cx, |settings| {
7524 settings.languages.insert(
7525 "Rust".into(),
7526 LanguageSettingsContent {
7527 tab_size: NonZeroU32::new(8),
7528 ..Default::default()
7529 },
7530 );
7531 });
7532
7533 editor.update_in(cx, |editor, window, cx| {
7534 editor.set_text("somehting_new\n", window, cx)
7535 });
7536 assert!(cx.read(|cx| editor.is_dirty(cx)));
7537 let save = editor
7538 .update_in(cx, |editor, window, cx| {
7539 editor.save(true, project.clone(), window, cx)
7540 })
7541 .unwrap();
7542 fake_server
7543 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7544 assert_eq!(
7545 params.text_document.uri,
7546 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7547 );
7548 assert_eq!(params.options.tab_size, 8);
7549 Ok(Some(vec![]))
7550 })
7551 .next()
7552 .await;
7553 cx.executor().start_waiting();
7554 save.await;
7555}
7556
7557#[gpui::test]
7558async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
7559 init_test(cx, |_| {});
7560
7561 let cols = 4;
7562 let rows = 10;
7563 let sample_text_1 = sample_text(rows, cols, 'a');
7564 assert_eq!(
7565 sample_text_1,
7566 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7567 );
7568 let sample_text_2 = sample_text(rows, cols, 'l');
7569 assert_eq!(
7570 sample_text_2,
7571 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7572 );
7573 let sample_text_3 = sample_text(rows, cols, 'v');
7574 assert_eq!(
7575 sample_text_3,
7576 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7577 );
7578
7579 let fs = FakeFs::new(cx.executor());
7580 fs.insert_tree(
7581 path!("/a"),
7582 json!({
7583 "main.rs": sample_text_1,
7584 "other.rs": sample_text_2,
7585 "lib.rs": sample_text_3,
7586 }),
7587 )
7588 .await;
7589
7590 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
7591 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7592 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7593
7594 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7595 language_registry.add(rust_lang());
7596 let mut fake_servers = language_registry.register_fake_lsp(
7597 "Rust",
7598 FakeLspAdapter {
7599 capabilities: lsp::ServerCapabilities {
7600 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7601 ..Default::default()
7602 },
7603 ..Default::default()
7604 },
7605 );
7606
7607 let worktree = project.update(cx, |project, cx| {
7608 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7609 assert_eq!(worktrees.len(), 1);
7610 worktrees.pop().unwrap()
7611 });
7612 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
7613
7614 let buffer_1 = project
7615 .update(cx, |project, cx| {
7616 project.open_buffer((worktree_id, "main.rs"), cx)
7617 })
7618 .await
7619 .unwrap();
7620 let buffer_2 = project
7621 .update(cx, |project, cx| {
7622 project.open_buffer((worktree_id, "other.rs"), cx)
7623 })
7624 .await
7625 .unwrap();
7626 let buffer_3 = project
7627 .update(cx, |project, cx| {
7628 project.open_buffer((worktree_id, "lib.rs"), cx)
7629 })
7630 .await
7631 .unwrap();
7632
7633 let multi_buffer = cx.new(|cx| {
7634 let mut multi_buffer = MultiBuffer::new(ReadWrite);
7635 multi_buffer.push_excerpts(
7636 buffer_1.clone(),
7637 [
7638 ExcerptRange {
7639 context: Point::new(0, 0)..Point::new(3, 0),
7640 primary: None,
7641 },
7642 ExcerptRange {
7643 context: Point::new(5, 0)..Point::new(7, 0),
7644 primary: None,
7645 },
7646 ExcerptRange {
7647 context: Point::new(9, 0)..Point::new(10, 4),
7648 primary: None,
7649 },
7650 ],
7651 cx,
7652 );
7653 multi_buffer.push_excerpts(
7654 buffer_2.clone(),
7655 [
7656 ExcerptRange {
7657 context: Point::new(0, 0)..Point::new(3, 0),
7658 primary: None,
7659 },
7660 ExcerptRange {
7661 context: Point::new(5, 0)..Point::new(7, 0),
7662 primary: None,
7663 },
7664 ExcerptRange {
7665 context: Point::new(9, 0)..Point::new(10, 4),
7666 primary: None,
7667 },
7668 ],
7669 cx,
7670 );
7671 multi_buffer.push_excerpts(
7672 buffer_3.clone(),
7673 [
7674 ExcerptRange {
7675 context: Point::new(0, 0)..Point::new(3, 0),
7676 primary: None,
7677 },
7678 ExcerptRange {
7679 context: Point::new(5, 0)..Point::new(7, 0),
7680 primary: None,
7681 },
7682 ExcerptRange {
7683 context: Point::new(9, 0)..Point::new(10, 4),
7684 primary: None,
7685 },
7686 ],
7687 cx,
7688 );
7689 multi_buffer
7690 });
7691 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
7692 Editor::new(
7693 EditorMode::Full,
7694 multi_buffer,
7695 Some(project.clone()),
7696 window,
7697 cx,
7698 )
7699 });
7700
7701 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7702 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7703 s.select_ranges(Some(1..2))
7704 });
7705 editor.insert("|one|two|three|", window, cx);
7706 });
7707 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7708 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7709 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7710 s.select_ranges(Some(60..70))
7711 });
7712 editor.insert("|four|five|six|", window, cx);
7713 });
7714 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7715
7716 // First two buffers should be edited, but not the third one.
7717 assert_eq!(
7718 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7719 "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}",
7720 );
7721 buffer_1.update(cx, |buffer, _| {
7722 assert!(buffer.is_dirty());
7723 assert_eq!(
7724 buffer.text(),
7725 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
7726 )
7727 });
7728 buffer_2.update(cx, |buffer, _| {
7729 assert!(buffer.is_dirty());
7730 assert_eq!(
7731 buffer.text(),
7732 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
7733 )
7734 });
7735 buffer_3.update(cx, |buffer, _| {
7736 assert!(!buffer.is_dirty());
7737 assert_eq!(buffer.text(), sample_text_3,)
7738 });
7739 cx.executor().run_until_parked();
7740
7741 cx.executor().start_waiting();
7742 let save = multi_buffer_editor
7743 .update_in(cx, |editor, window, cx| {
7744 editor.save(true, project.clone(), window, cx)
7745 })
7746 .unwrap();
7747
7748 let fake_server = fake_servers.next().await.unwrap();
7749 fake_server
7750 .server
7751 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7752 Ok(Some(vec![lsp::TextEdit::new(
7753 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7754 format!("[{} formatted]", params.text_document.uri),
7755 )]))
7756 })
7757 .detach();
7758 save.await;
7759
7760 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
7761 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
7762 assert_eq!(
7763 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7764 uri!("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}"),
7765 );
7766 buffer_1.update(cx, |buffer, _| {
7767 assert!(!buffer.is_dirty());
7768 assert_eq!(
7769 buffer.text(),
7770 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
7771 )
7772 });
7773 buffer_2.update(cx, |buffer, _| {
7774 assert!(!buffer.is_dirty());
7775 assert_eq!(
7776 buffer.text(),
7777 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
7778 )
7779 });
7780 buffer_3.update(cx, |buffer, _| {
7781 assert!(!buffer.is_dirty());
7782 assert_eq!(buffer.text(), sample_text_3,)
7783 });
7784}
7785
7786#[gpui::test]
7787async fn test_range_format_during_save(cx: &mut TestAppContext) {
7788 init_test(cx, |_| {});
7789
7790 let fs = FakeFs::new(cx.executor());
7791 fs.insert_file(path!("/file.rs"), Default::default()).await;
7792
7793 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
7794
7795 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7796 language_registry.add(rust_lang());
7797 let mut fake_servers = language_registry.register_fake_lsp(
7798 "Rust",
7799 FakeLspAdapter {
7800 capabilities: lsp::ServerCapabilities {
7801 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
7802 ..Default::default()
7803 },
7804 ..Default::default()
7805 },
7806 );
7807
7808 let buffer = project
7809 .update(cx, |project, cx| {
7810 project.open_local_buffer(path!("/file.rs"), cx)
7811 })
7812 .await
7813 .unwrap();
7814
7815 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7816 let (editor, cx) = cx.add_window_view(|window, cx| {
7817 build_editor_with_project(project.clone(), buffer, window, cx)
7818 });
7819 editor.update_in(cx, |editor, window, cx| {
7820 editor.set_text("one\ntwo\nthree\n", window, cx)
7821 });
7822 assert!(cx.read(|cx| editor.is_dirty(cx)));
7823
7824 cx.executor().start_waiting();
7825 let fake_server = fake_servers.next().await.unwrap();
7826
7827 let save = editor
7828 .update_in(cx, |editor, window, cx| {
7829 editor.save(true, project.clone(), window, cx)
7830 })
7831 .unwrap();
7832 fake_server
7833 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
7834 assert_eq!(
7835 params.text_document.uri,
7836 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7837 );
7838 assert_eq!(params.options.tab_size, 4);
7839 Ok(Some(vec![lsp::TextEdit::new(
7840 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7841 ", ".to_string(),
7842 )]))
7843 })
7844 .next()
7845 .await;
7846 cx.executor().start_waiting();
7847 save.await;
7848 assert_eq!(
7849 editor.update(cx, |editor, cx| editor.text(cx)),
7850 "one, two\nthree\n"
7851 );
7852 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7853
7854 editor.update_in(cx, |editor, window, cx| {
7855 editor.set_text("one\ntwo\nthree\n", window, cx)
7856 });
7857 assert!(cx.read(|cx| editor.is_dirty(cx)));
7858
7859 // Ensure we can still save even if formatting hangs.
7860 fake_server.handle_request::<lsp::request::RangeFormatting, _, _>(
7861 move |params, _| async move {
7862 assert_eq!(
7863 params.text_document.uri,
7864 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7865 );
7866 futures::future::pending::<()>().await;
7867 unreachable!()
7868 },
7869 );
7870 let save = editor
7871 .update_in(cx, |editor, window, cx| {
7872 editor.save(true, project.clone(), window, cx)
7873 })
7874 .unwrap();
7875 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7876 cx.executor().start_waiting();
7877 save.await;
7878 assert_eq!(
7879 editor.update(cx, |editor, cx| editor.text(cx)),
7880 "one\ntwo\nthree\n"
7881 );
7882 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7883
7884 // For non-dirty buffer, no formatting request should be sent
7885 let save = editor
7886 .update_in(cx, |editor, window, cx| {
7887 editor.save(true, project.clone(), window, cx)
7888 })
7889 .unwrap();
7890 let _pending_format_request = fake_server
7891 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7892 panic!("Should not be invoked on non-dirty buffer");
7893 })
7894 .next();
7895 cx.executor().start_waiting();
7896 save.await;
7897
7898 // Set Rust language override and assert overridden tabsize is sent to language server
7899 update_test_language_settings(cx, |settings| {
7900 settings.languages.insert(
7901 "Rust".into(),
7902 LanguageSettingsContent {
7903 tab_size: NonZeroU32::new(8),
7904 ..Default::default()
7905 },
7906 );
7907 });
7908
7909 editor.update_in(cx, |editor, window, cx| {
7910 editor.set_text("somehting_new\n", window, cx)
7911 });
7912 assert!(cx.read(|cx| editor.is_dirty(cx)));
7913 let save = editor
7914 .update_in(cx, |editor, window, cx| {
7915 editor.save(true, project.clone(), window, cx)
7916 })
7917 .unwrap();
7918 fake_server
7919 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
7920 assert_eq!(
7921 params.text_document.uri,
7922 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7923 );
7924 assert_eq!(params.options.tab_size, 8);
7925 Ok(Some(vec![]))
7926 })
7927 .next()
7928 .await;
7929 cx.executor().start_waiting();
7930 save.await;
7931}
7932
7933#[gpui::test]
7934async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
7935 init_test(cx, |settings| {
7936 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
7937 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
7938 ))
7939 });
7940
7941 let fs = FakeFs::new(cx.executor());
7942 fs.insert_file(path!("/file.rs"), Default::default()).await;
7943
7944 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
7945
7946 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7947 language_registry.add(Arc::new(Language::new(
7948 LanguageConfig {
7949 name: "Rust".into(),
7950 matcher: LanguageMatcher {
7951 path_suffixes: vec!["rs".to_string()],
7952 ..Default::default()
7953 },
7954 ..LanguageConfig::default()
7955 },
7956 Some(tree_sitter_rust::LANGUAGE.into()),
7957 )));
7958 update_test_language_settings(cx, |settings| {
7959 // Enable Prettier formatting for the same buffer, and ensure
7960 // LSP is called instead of Prettier.
7961 settings.defaults.prettier = Some(PrettierSettings {
7962 allowed: true,
7963 ..PrettierSettings::default()
7964 });
7965 });
7966 let mut fake_servers = language_registry.register_fake_lsp(
7967 "Rust",
7968 FakeLspAdapter {
7969 capabilities: lsp::ServerCapabilities {
7970 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7971 ..Default::default()
7972 },
7973 ..Default::default()
7974 },
7975 );
7976
7977 let buffer = project
7978 .update(cx, |project, cx| {
7979 project.open_local_buffer(path!("/file.rs"), cx)
7980 })
7981 .await
7982 .unwrap();
7983
7984 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7985 let (editor, cx) = cx.add_window_view(|window, cx| {
7986 build_editor_with_project(project.clone(), buffer, window, cx)
7987 });
7988 editor.update_in(cx, |editor, window, cx| {
7989 editor.set_text("one\ntwo\nthree\n", window, cx)
7990 });
7991
7992 cx.executor().start_waiting();
7993 let fake_server = fake_servers.next().await.unwrap();
7994
7995 let format = editor
7996 .update_in(cx, |editor, window, cx| {
7997 editor.perform_format(
7998 project.clone(),
7999 FormatTrigger::Manual,
8000 FormatTarget::Buffers,
8001 window,
8002 cx,
8003 )
8004 })
8005 .unwrap();
8006 fake_server
8007 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8008 assert_eq!(
8009 params.text_document.uri,
8010 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8011 );
8012 assert_eq!(params.options.tab_size, 4);
8013 Ok(Some(vec![lsp::TextEdit::new(
8014 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8015 ", ".to_string(),
8016 )]))
8017 })
8018 .next()
8019 .await;
8020 cx.executor().start_waiting();
8021 format.await;
8022 assert_eq!(
8023 editor.update(cx, |editor, cx| editor.text(cx)),
8024 "one, two\nthree\n"
8025 );
8026
8027 editor.update_in(cx, |editor, window, cx| {
8028 editor.set_text("one\ntwo\nthree\n", window, cx)
8029 });
8030 // Ensure we don't lock if formatting hangs.
8031 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8032 assert_eq!(
8033 params.text_document.uri,
8034 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8035 );
8036 futures::future::pending::<()>().await;
8037 unreachable!()
8038 });
8039 let format = editor
8040 .update_in(cx, |editor, window, cx| {
8041 editor.perform_format(
8042 project,
8043 FormatTrigger::Manual,
8044 FormatTarget::Buffers,
8045 window,
8046 cx,
8047 )
8048 })
8049 .unwrap();
8050 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8051 cx.executor().start_waiting();
8052 format.await;
8053 assert_eq!(
8054 editor.update(cx, |editor, cx| editor.text(cx)),
8055 "one\ntwo\nthree\n"
8056 );
8057}
8058
8059#[gpui::test]
8060async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
8061 init_test(cx, |settings| {
8062 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8063 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8064 ))
8065 });
8066
8067 let fs = FakeFs::new(cx.executor());
8068 fs.insert_file(path!("/file.ts"), Default::default()).await;
8069
8070 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8071
8072 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8073 language_registry.add(Arc::new(Language::new(
8074 LanguageConfig {
8075 name: "TypeScript".into(),
8076 matcher: LanguageMatcher {
8077 path_suffixes: vec!["ts".to_string()],
8078 ..Default::default()
8079 },
8080 ..LanguageConfig::default()
8081 },
8082 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8083 )));
8084 update_test_language_settings(cx, |settings| {
8085 settings.defaults.prettier = Some(PrettierSettings {
8086 allowed: true,
8087 ..PrettierSettings::default()
8088 });
8089 });
8090 let mut fake_servers = language_registry.register_fake_lsp(
8091 "TypeScript",
8092 FakeLspAdapter {
8093 capabilities: lsp::ServerCapabilities {
8094 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8095 ..Default::default()
8096 },
8097 ..Default::default()
8098 },
8099 );
8100
8101 let buffer = project
8102 .update(cx, |project, cx| {
8103 project.open_local_buffer(path!("/file.ts"), cx)
8104 })
8105 .await
8106 .unwrap();
8107
8108 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8109 let (editor, cx) = cx.add_window_view(|window, cx| {
8110 build_editor_with_project(project.clone(), buffer, window, cx)
8111 });
8112 editor.update_in(cx, |editor, window, cx| {
8113 editor.set_text(
8114 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8115 window,
8116 cx,
8117 )
8118 });
8119
8120 cx.executor().start_waiting();
8121 let fake_server = fake_servers.next().await.unwrap();
8122
8123 let format = editor
8124 .update_in(cx, |editor, window, cx| {
8125 editor.perform_code_action_kind(
8126 project.clone(),
8127 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8128 window,
8129 cx,
8130 )
8131 })
8132 .unwrap();
8133 fake_server
8134 .handle_request::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
8135 assert_eq!(
8136 params.text_document.uri,
8137 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8138 );
8139 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
8140 lsp::CodeAction {
8141 title: "Organize Imports".to_string(),
8142 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
8143 edit: Some(lsp::WorkspaceEdit {
8144 changes: Some(
8145 [(
8146 params.text_document.uri.clone(),
8147 vec![lsp::TextEdit::new(
8148 lsp::Range::new(
8149 lsp::Position::new(1, 0),
8150 lsp::Position::new(2, 0),
8151 ),
8152 "".to_string(),
8153 )],
8154 )]
8155 .into_iter()
8156 .collect(),
8157 ),
8158 ..Default::default()
8159 }),
8160 ..Default::default()
8161 },
8162 )]))
8163 })
8164 .next()
8165 .await;
8166 cx.executor().start_waiting();
8167 format.await;
8168 assert_eq!(
8169 editor.update(cx, |editor, cx| editor.text(cx)),
8170 "import { a } from 'module';\n\nconst x = a;\n"
8171 );
8172
8173 editor.update_in(cx, |editor, window, cx| {
8174 editor.set_text(
8175 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8176 window,
8177 cx,
8178 )
8179 });
8180 // Ensure we don't lock if code action hangs.
8181 fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
8182 move |params, _| async move {
8183 assert_eq!(
8184 params.text_document.uri,
8185 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8186 );
8187 futures::future::pending::<()>().await;
8188 unreachable!()
8189 },
8190 );
8191 let format = editor
8192 .update_in(cx, |editor, window, cx| {
8193 editor.perform_code_action_kind(
8194 project,
8195 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8196 window,
8197 cx,
8198 )
8199 })
8200 .unwrap();
8201 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
8202 cx.executor().start_waiting();
8203 format.await;
8204 assert_eq!(
8205 editor.update(cx, |editor, cx| editor.text(cx)),
8206 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
8207 );
8208}
8209
8210#[gpui::test]
8211async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
8212 init_test(cx, |_| {});
8213
8214 let mut cx = EditorLspTestContext::new_rust(
8215 lsp::ServerCapabilities {
8216 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8217 ..Default::default()
8218 },
8219 cx,
8220 )
8221 .await;
8222
8223 cx.set_state(indoc! {"
8224 one.twoˇ
8225 "});
8226
8227 // The format request takes a long time. When it completes, it inserts
8228 // a newline and an indent before the `.`
8229 cx.lsp
8230 .handle_request::<lsp::request::Formatting, _, _>(move |_, cx| {
8231 let executor = cx.background_executor().clone();
8232 async move {
8233 executor.timer(Duration::from_millis(100)).await;
8234 Ok(Some(vec![lsp::TextEdit {
8235 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
8236 new_text: "\n ".into(),
8237 }]))
8238 }
8239 });
8240
8241 // Submit a format request.
8242 let format_1 = cx
8243 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8244 .unwrap();
8245 cx.executor().run_until_parked();
8246
8247 // Submit a second format request.
8248 let format_2 = cx
8249 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8250 .unwrap();
8251 cx.executor().run_until_parked();
8252
8253 // Wait for both format requests to complete
8254 cx.executor().advance_clock(Duration::from_millis(200));
8255 cx.executor().start_waiting();
8256 format_1.await.unwrap();
8257 cx.executor().start_waiting();
8258 format_2.await.unwrap();
8259
8260 // The formatting edits only happens once.
8261 cx.assert_editor_state(indoc! {"
8262 one
8263 .twoˇ
8264 "});
8265}
8266
8267#[gpui::test]
8268async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
8269 init_test(cx, |settings| {
8270 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
8271 });
8272
8273 let mut cx = EditorLspTestContext::new_rust(
8274 lsp::ServerCapabilities {
8275 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8276 ..Default::default()
8277 },
8278 cx,
8279 )
8280 .await;
8281
8282 // Set up a buffer white some trailing whitespace and no trailing newline.
8283 cx.set_state(
8284 &[
8285 "one ", //
8286 "twoˇ", //
8287 "three ", //
8288 "four", //
8289 ]
8290 .join("\n"),
8291 );
8292
8293 // Submit a format request.
8294 let format = cx
8295 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8296 .unwrap();
8297
8298 // Record which buffer changes have been sent to the language server
8299 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
8300 cx.lsp
8301 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
8302 let buffer_changes = buffer_changes.clone();
8303 move |params, _| {
8304 buffer_changes.lock().extend(
8305 params
8306 .content_changes
8307 .into_iter()
8308 .map(|e| (e.range.unwrap(), e.text)),
8309 );
8310 }
8311 });
8312
8313 // Handle formatting requests to the language server.
8314 cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
8315 let buffer_changes = buffer_changes.clone();
8316 move |_, _| {
8317 // When formatting is requested, trailing whitespace has already been stripped,
8318 // and the trailing newline has already been added.
8319 assert_eq!(
8320 &buffer_changes.lock()[1..],
8321 &[
8322 (
8323 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
8324 "".into()
8325 ),
8326 (
8327 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
8328 "".into()
8329 ),
8330 (
8331 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
8332 "\n".into()
8333 ),
8334 ]
8335 );
8336
8337 // Insert blank lines between each line of the buffer.
8338 async move {
8339 Ok(Some(vec![
8340 lsp::TextEdit {
8341 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
8342 new_text: "\n".into(),
8343 },
8344 lsp::TextEdit {
8345 range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
8346 new_text: "\n".into(),
8347 },
8348 ]))
8349 }
8350 }
8351 });
8352
8353 // After formatting the buffer, the trailing whitespace is stripped,
8354 // a newline is appended, and the edits provided by the language server
8355 // have been applied.
8356 format.await.unwrap();
8357 cx.assert_editor_state(
8358 &[
8359 "one", //
8360 "", //
8361 "twoˇ", //
8362 "", //
8363 "three", //
8364 "four", //
8365 "", //
8366 ]
8367 .join("\n"),
8368 );
8369
8370 // Undoing the formatting undoes the trailing whitespace removal, the
8371 // trailing newline, and the LSP edits.
8372 cx.update_buffer(|buffer, cx| buffer.undo(cx));
8373 cx.assert_editor_state(
8374 &[
8375 "one ", //
8376 "twoˇ", //
8377 "three ", //
8378 "four", //
8379 ]
8380 .join("\n"),
8381 );
8382}
8383
8384#[gpui::test]
8385async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
8386 cx: &mut TestAppContext,
8387) {
8388 init_test(cx, |_| {});
8389
8390 cx.update(|cx| {
8391 cx.update_global::<SettingsStore, _>(|settings, cx| {
8392 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8393 settings.auto_signature_help = Some(true);
8394 });
8395 });
8396 });
8397
8398 let mut cx = EditorLspTestContext::new_rust(
8399 lsp::ServerCapabilities {
8400 signature_help_provider: Some(lsp::SignatureHelpOptions {
8401 ..Default::default()
8402 }),
8403 ..Default::default()
8404 },
8405 cx,
8406 )
8407 .await;
8408
8409 let language = Language::new(
8410 LanguageConfig {
8411 name: "Rust".into(),
8412 brackets: BracketPairConfig {
8413 pairs: vec![
8414 BracketPair {
8415 start: "{".to_string(),
8416 end: "}".to_string(),
8417 close: true,
8418 surround: true,
8419 newline: true,
8420 },
8421 BracketPair {
8422 start: "(".to_string(),
8423 end: ")".to_string(),
8424 close: true,
8425 surround: true,
8426 newline: true,
8427 },
8428 BracketPair {
8429 start: "/*".to_string(),
8430 end: " */".to_string(),
8431 close: true,
8432 surround: true,
8433 newline: true,
8434 },
8435 BracketPair {
8436 start: "[".to_string(),
8437 end: "]".to_string(),
8438 close: false,
8439 surround: false,
8440 newline: true,
8441 },
8442 BracketPair {
8443 start: "\"".to_string(),
8444 end: "\"".to_string(),
8445 close: true,
8446 surround: true,
8447 newline: false,
8448 },
8449 BracketPair {
8450 start: "<".to_string(),
8451 end: ">".to_string(),
8452 close: false,
8453 surround: true,
8454 newline: true,
8455 },
8456 ],
8457 ..Default::default()
8458 },
8459 autoclose_before: "})]".to_string(),
8460 ..Default::default()
8461 },
8462 Some(tree_sitter_rust::LANGUAGE.into()),
8463 );
8464 let language = Arc::new(language);
8465
8466 cx.language_registry().add(language.clone());
8467 cx.update_buffer(|buffer, cx| {
8468 buffer.set_language(Some(language), cx);
8469 });
8470
8471 cx.set_state(
8472 &r#"
8473 fn main() {
8474 sampleˇ
8475 }
8476 "#
8477 .unindent(),
8478 );
8479
8480 cx.update_editor(|editor, window, cx| {
8481 editor.handle_input("(", window, cx);
8482 });
8483 cx.assert_editor_state(
8484 &"
8485 fn main() {
8486 sample(ˇ)
8487 }
8488 "
8489 .unindent(),
8490 );
8491
8492 let mocked_response = lsp::SignatureHelp {
8493 signatures: vec![lsp::SignatureInformation {
8494 label: "fn sample(param1: u8, param2: u8)".to_string(),
8495 documentation: None,
8496 parameters: Some(vec![
8497 lsp::ParameterInformation {
8498 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8499 documentation: None,
8500 },
8501 lsp::ParameterInformation {
8502 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8503 documentation: None,
8504 },
8505 ]),
8506 active_parameter: None,
8507 }],
8508 active_signature: Some(0),
8509 active_parameter: Some(0),
8510 };
8511 handle_signature_help_request(&mut cx, mocked_response).await;
8512
8513 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8514 .await;
8515
8516 cx.editor(|editor, _, _| {
8517 let signature_help_state = editor.signature_help_state.popover().cloned();
8518 assert_eq!(
8519 signature_help_state.unwrap().label,
8520 "param1: u8, param2: u8"
8521 );
8522 });
8523}
8524
8525#[gpui::test]
8526async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
8527 init_test(cx, |_| {});
8528
8529 cx.update(|cx| {
8530 cx.update_global::<SettingsStore, _>(|settings, cx| {
8531 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8532 settings.auto_signature_help = Some(false);
8533 settings.show_signature_help_after_edits = Some(false);
8534 });
8535 });
8536 });
8537
8538 let mut cx = EditorLspTestContext::new_rust(
8539 lsp::ServerCapabilities {
8540 signature_help_provider: Some(lsp::SignatureHelpOptions {
8541 ..Default::default()
8542 }),
8543 ..Default::default()
8544 },
8545 cx,
8546 )
8547 .await;
8548
8549 let language = Language::new(
8550 LanguageConfig {
8551 name: "Rust".into(),
8552 brackets: BracketPairConfig {
8553 pairs: vec![
8554 BracketPair {
8555 start: "{".to_string(),
8556 end: "}".to_string(),
8557 close: true,
8558 surround: true,
8559 newline: true,
8560 },
8561 BracketPair {
8562 start: "(".to_string(),
8563 end: ")".to_string(),
8564 close: true,
8565 surround: true,
8566 newline: true,
8567 },
8568 BracketPair {
8569 start: "/*".to_string(),
8570 end: " */".to_string(),
8571 close: true,
8572 surround: true,
8573 newline: true,
8574 },
8575 BracketPair {
8576 start: "[".to_string(),
8577 end: "]".to_string(),
8578 close: false,
8579 surround: false,
8580 newline: true,
8581 },
8582 BracketPair {
8583 start: "\"".to_string(),
8584 end: "\"".to_string(),
8585 close: true,
8586 surround: true,
8587 newline: false,
8588 },
8589 BracketPair {
8590 start: "<".to_string(),
8591 end: ">".to_string(),
8592 close: false,
8593 surround: true,
8594 newline: true,
8595 },
8596 ],
8597 ..Default::default()
8598 },
8599 autoclose_before: "})]".to_string(),
8600 ..Default::default()
8601 },
8602 Some(tree_sitter_rust::LANGUAGE.into()),
8603 );
8604 let language = Arc::new(language);
8605
8606 cx.language_registry().add(language.clone());
8607 cx.update_buffer(|buffer, cx| {
8608 buffer.set_language(Some(language), cx);
8609 });
8610
8611 // Ensure that signature_help is not called when no signature help is enabled.
8612 cx.set_state(
8613 &r#"
8614 fn main() {
8615 sampleˇ
8616 }
8617 "#
8618 .unindent(),
8619 );
8620 cx.update_editor(|editor, window, cx| {
8621 editor.handle_input("(", window, cx);
8622 });
8623 cx.assert_editor_state(
8624 &"
8625 fn main() {
8626 sample(ˇ)
8627 }
8628 "
8629 .unindent(),
8630 );
8631 cx.editor(|editor, _, _| {
8632 assert!(editor.signature_help_state.task().is_none());
8633 });
8634
8635 let mocked_response = lsp::SignatureHelp {
8636 signatures: vec![lsp::SignatureInformation {
8637 label: "fn sample(param1: u8, param2: u8)".to_string(),
8638 documentation: None,
8639 parameters: Some(vec![
8640 lsp::ParameterInformation {
8641 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8642 documentation: None,
8643 },
8644 lsp::ParameterInformation {
8645 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8646 documentation: None,
8647 },
8648 ]),
8649 active_parameter: None,
8650 }],
8651 active_signature: Some(0),
8652 active_parameter: Some(0),
8653 };
8654
8655 // Ensure that signature_help is called when enabled afte edits
8656 cx.update(|_, cx| {
8657 cx.update_global::<SettingsStore, _>(|settings, cx| {
8658 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8659 settings.auto_signature_help = Some(false);
8660 settings.show_signature_help_after_edits = Some(true);
8661 });
8662 });
8663 });
8664 cx.set_state(
8665 &r#"
8666 fn main() {
8667 sampleˇ
8668 }
8669 "#
8670 .unindent(),
8671 );
8672 cx.update_editor(|editor, window, cx| {
8673 editor.handle_input("(", window, cx);
8674 });
8675 cx.assert_editor_state(
8676 &"
8677 fn main() {
8678 sample(ˇ)
8679 }
8680 "
8681 .unindent(),
8682 );
8683 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8684 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8685 .await;
8686 cx.update_editor(|editor, _, _| {
8687 let signature_help_state = editor.signature_help_state.popover().cloned();
8688 assert!(signature_help_state.is_some());
8689 assert_eq!(
8690 signature_help_state.unwrap().label,
8691 "param1: u8, param2: u8"
8692 );
8693 editor.signature_help_state = SignatureHelpState::default();
8694 });
8695
8696 // Ensure that signature_help is called when auto signature help override is enabled
8697 cx.update(|_, cx| {
8698 cx.update_global::<SettingsStore, _>(|settings, cx| {
8699 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8700 settings.auto_signature_help = Some(true);
8701 settings.show_signature_help_after_edits = Some(false);
8702 });
8703 });
8704 });
8705 cx.set_state(
8706 &r#"
8707 fn main() {
8708 sampleˇ
8709 }
8710 "#
8711 .unindent(),
8712 );
8713 cx.update_editor(|editor, window, cx| {
8714 editor.handle_input("(", window, cx);
8715 });
8716 cx.assert_editor_state(
8717 &"
8718 fn main() {
8719 sample(ˇ)
8720 }
8721 "
8722 .unindent(),
8723 );
8724 handle_signature_help_request(&mut cx, mocked_response).await;
8725 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8726 .await;
8727 cx.editor(|editor, _, _| {
8728 let signature_help_state = editor.signature_help_state.popover().cloned();
8729 assert!(signature_help_state.is_some());
8730 assert_eq!(
8731 signature_help_state.unwrap().label,
8732 "param1: u8, param2: u8"
8733 );
8734 });
8735}
8736
8737#[gpui::test]
8738async fn test_signature_help(cx: &mut TestAppContext) {
8739 init_test(cx, |_| {});
8740 cx.update(|cx| {
8741 cx.update_global::<SettingsStore, _>(|settings, cx| {
8742 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8743 settings.auto_signature_help = Some(true);
8744 });
8745 });
8746 });
8747
8748 let mut cx = EditorLspTestContext::new_rust(
8749 lsp::ServerCapabilities {
8750 signature_help_provider: Some(lsp::SignatureHelpOptions {
8751 ..Default::default()
8752 }),
8753 ..Default::default()
8754 },
8755 cx,
8756 )
8757 .await;
8758
8759 // A test that directly calls `show_signature_help`
8760 cx.update_editor(|editor, window, cx| {
8761 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8762 });
8763
8764 let mocked_response = lsp::SignatureHelp {
8765 signatures: vec![lsp::SignatureInformation {
8766 label: "fn sample(param1: u8, param2: u8)".to_string(),
8767 documentation: None,
8768 parameters: Some(vec![
8769 lsp::ParameterInformation {
8770 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8771 documentation: None,
8772 },
8773 lsp::ParameterInformation {
8774 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8775 documentation: None,
8776 },
8777 ]),
8778 active_parameter: None,
8779 }],
8780 active_signature: Some(0),
8781 active_parameter: Some(0),
8782 };
8783 handle_signature_help_request(&mut cx, mocked_response).await;
8784
8785 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8786 .await;
8787
8788 cx.editor(|editor, _, _| {
8789 let signature_help_state = editor.signature_help_state.popover().cloned();
8790 assert!(signature_help_state.is_some());
8791 assert_eq!(
8792 signature_help_state.unwrap().label,
8793 "param1: u8, param2: u8"
8794 );
8795 });
8796
8797 // When exiting outside from inside the brackets, `signature_help` is closed.
8798 cx.set_state(indoc! {"
8799 fn main() {
8800 sample(ˇ);
8801 }
8802
8803 fn sample(param1: u8, param2: u8) {}
8804 "});
8805
8806 cx.update_editor(|editor, window, cx| {
8807 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
8808 });
8809
8810 let mocked_response = lsp::SignatureHelp {
8811 signatures: Vec::new(),
8812 active_signature: None,
8813 active_parameter: None,
8814 };
8815 handle_signature_help_request(&mut cx, mocked_response).await;
8816
8817 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
8818 .await;
8819
8820 cx.editor(|editor, _, _| {
8821 assert!(!editor.signature_help_state.is_shown());
8822 });
8823
8824 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
8825 cx.set_state(indoc! {"
8826 fn main() {
8827 sample(ˇ);
8828 }
8829
8830 fn sample(param1: u8, param2: u8) {}
8831 "});
8832
8833 let mocked_response = lsp::SignatureHelp {
8834 signatures: vec![lsp::SignatureInformation {
8835 label: "fn sample(param1: u8, param2: u8)".to_string(),
8836 documentation: None,
8837 parameters: Some(vec![
8838 lsp::ParameterInformation {
8839 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8840 documentation: None,
8841 },
8842 lsp::ParameterInformation {
8843 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8844 documentation: None,
8845 },
8846 ]),
8847 active_parameter: None,
8848 }],
8849 active_signature: Some(0),
8850 active_parameter: Some(0),
8851 };
8852 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8853 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8854 .await;
8855 cx.editor(|editor, _, _| {
8856 assert!(editor.signature_help_state.is_shown());
8857 });
8858
8859 // Restore the popover with more parameter input
8860 cx.set_state(indoc! {"
8861 fn main() {
8862 sample(param1, param2ˇ);
8863 }
8864
8865 fn sample(param1: u8, param2: u8) {}
8866 "});
8867
8868 let mocked_response = lsp::SignatureHelp {
8869 signatures: vec![lsp::SignatureInformation {
8870 label: "fn sample(param1: u8, param2: u8)".to_string(),
8871 documentation: None,
8872 parameters: Some(vec![
8873 lsp::ParameterInformation {
8874 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8875 documentation: None,
8876 },
8877 lsp::ParameterInformation {
8878 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8879 documentation: None,
8880 },
8881 ]),
8882 active_parameter: None,
8883 }],
8884 active_signature: Some(0),
8885 active_parameter: Some(1),
8886 };
8887 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8888 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8889 .await;
8890
8891 // When selecting a range, the popover is gone.
8892 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
8893 cx.update_editor(|editor, window, cx| {
8894 editor.change_selections(None, window, cx, |s| {
8895 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
8896 })
8897 });
8898 cx.assert_editor_state(indoc! {"
8899 fn main() {
8900 sample(param1, «ˇparam2»);
8901 }
8902
8903 fn sample(param1: u8, param2: u8) {}
8904 "});
8905 cx.editor(|editor, _, _| {
8906 assert!(!editor.signature_help_state.is_shown());
8907 });
8908
8909 // When unselecting again, the popover is back if within the brackets.
8910 cx.update_editor(|editor, window, cx| {
8911 editor.change_selections(None, window, cx, |s| {
8912 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8913 })
8914 });
8915 cx.assert_editor_state(indoc! {"
8916 fn main() {
8917 sample(param1, ˇparam2);
8918 }
8919
8920 fn sample(param1: u8, param2: u8) {}
8921 "});
8922 handle_signature_help_request(&mut cx, mocked_response).await;
8923 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8924 .await;
8925 cx.editor(|editor, _, _| {
8926 assert!(editor.signature_help_state.is_shown());
8927 });
8928
8929 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
8930 cx.update_editor(|editor, window, cx| {
8931 editor.change_selections(None, window, cx, |s| {
8932 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
8933 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8934 })
8935 });
8936 cx.assert_editor_state(indoc! {"
8937 fn main() {
8938 sample(param1, ˇparam2);
8939 }
8940
8941 fn sample(param1: u8, param2: u8) {}
8942 "});
8943
8944 let mocked_response = lsp::SignatureHelp {
8945 signatures: vec![lsp::SignatureInformation {
8946 label: "fn sample(param1: u8, param2: u8)".to_string(),
8947 documentation: None,
8948 parameters: Some(vec![
8949 lsp::ParameterInformation {
8950 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8951 documentation: None,
8952 },
8953 lsp::ParameterInformation {
8954 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8955 documentation: None,
8956 },
8957 ]),
8958 active_parameter: None,
8959 }],
8960 active_signature: Some(0),
8961 active_parameter: Some(1),
8962 };
8963 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8964 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8965 .await;
8966 cx.update_editor(|editor, _, cx| {
8967 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
8968 });
8969 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
8970 .await;
8971 cx.update_editor(|editor, window, cx| {
8972 editor.change_selections(None, window, cx, |s| {
8973 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
8974 })
8975 });
8976 cx.assert_editor_state(indoc! {"
8977 fn main() {
8978 sample(param1, «ˇparam2»);
8979 }
8980
8981 fn sample(param1: u8, param2: u8) {}
8982 "});
8983 cx.update_editor(|editor, window, cx| {
8984 editor.change_selections(None, window, cx, |s| {
8985 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8986 })
8987 });
8988 cx.assert_editor_state(indoc! {"
8989 fn main() {
8990 sample(param1, ˇparam2);
8991 }
8992
8993 fn sample(param1: u8, param2: u8) {}
8994 "});
8995 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
8996 .await;
8997}
8998
8999#[gpui::test]
9000async fn test_completion(cx: &mut TestAppContext) {
9001 init_test(cx, |_| {});
9002
9003 let mut cx = EditorLspTestContext::new_rust(
9004 lsp::ServerCapabilities {
9005 completion_provider: Some(lsp::CompletionOptions {
9006 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9007 resolve_provider: Some(true),
9008 ..Default::default()
9009 }),
9010 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9011 ..Default::default()
9012 },
9013 cx,
9014 )
9015 .await;
9016 let counter = Arc::new(AtomicUsize::new(0));
9017
9018 cx.set_state(indoc! {"
9019 oneˇ
9020 two
9021 three
9022 "});
9023 cx.simulate_keystroke(".");
9024 handle_completion_request(
9025 &mut cx,
9026 indoc! {"
9027 one.|<>
9028 two
9029 three
9030 "},
9031 vec!["first_completion", "second_completion"],
9032 counter.clone(),
9033 )
9034 .await;
9035 cx.condition(|editor, _| editor.context_menu_visible())
9036 .await;
9037 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9038
9039 let _handler = handle_signature_help_request(
9040 &mut cx,
9041 lsp::SignatureHelp {
9042 signatures: vec![lsp::SignatureInformation {
9043 label: "test signature".to_string(),
9044 documentation: None,
9045 parameters: Some(vec![lsp::ParameterInformation {
9046 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
9047 documentation: None,
9048 }]),
9049 active_parameter: None,
9050 }],
9051 active_signature: None,
9052 active_parameter: None,
9053 },
9054 );
9055 cx.update_editor(|editor, window, cx| {
9056 assert!(
9057 !editor.signature_help_state.is_shown(),
9058 "No signature help was called for"
9059 );
9060 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9061 });
9062 cx.run_until_parked();
9063 cx.update_editor(|editor, _, _| {
9064 assert!(
9065 !editor.signature_help_state.is_shown(),
9066 "No signature help should be shown when completions menu is open"
9067 );
9068 });
9069
9070 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9071 editor.context_menu_next(&Default::default(), window, cx);
9072 editor
9073 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9074 .unwrap()
9075 });
9076 cx.assert_editor_state(indoc! {"
9077 one.second_completionˇ
9078 two
9079 three
9080 "});
9081
9082 handle_resolve_completion_request(
9083 &mut cx,
9084 Some(vec![
9085 (
9086 //This overlaps with the primary completion edit which is
9087 //misbehavior from the LSP spec, test that we filter it out
9088 indoc! {"
9089 one.second_ˇcompletion
9090 two
9091 threeˇ
9092 "},
9093 "overlapping additional edit",
9094 ),
9095 (
9096 indoc! {"
9097 one.second_completion
9098 two
9099 threeˇ
9100 "},
9101 "\nadditional edit",
9102 ),
9103 ]),
9104 )
9105 .await;
9106 apply_additional_edits.await.unwrap();
9107 cx.assert_editor_state(indoc! {"
9108 one.second_completionˇ
9109 two
9110 three
9111 additional edit
9112 "});
9113
9114 cx.set_state(indoc! {"
9115 one.second_completion
9116 twoˇ
9117 threeˇ
9118 additional edit
9119 "});
9120 cx.simulate_keystroke(" ");
9121 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9122 cx.simulate_keystroke("s");
9123 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9124
9125 cx.assert_editor_state(indoc! {"
9126 one.second_completion
9127 two sˇ
9128 three sˇ
9129 additional edit
9130 "});
9131 handle_completion_request(
9132 &mut cx,
9133 indoc! {"
9134 one.second_completion
9135 two s
9136 three <s|>
9137 additional edit
9138 "},
9139 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
9140 counter.clone(),
9141 )
9142 .await;
9143 cx.condition(|editor, _| editor.context_menu_visible())
9144 .await;
9145 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
9146
9147 cx.simulate_keystroke("i");
9148
9149 handle_completion_request(
9150 &mut cx,
9151 indoc! {"
9152 one.second_completion
9153 two si
9154 three <si|>
9155 additional edit
9156 "},
9157 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
9158 counter.clone(),
9159 )
9160 .await;
9161 cx.condition(|editor, _| editor.context_menu_visible())
9162 .await;
9163 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
9164
9165 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9166 editor
9167 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9168 .unwrap()
9169 });
9170 cx.assert_editor_state(indoc! {"
9171 one.second_completion
9172 two sixth_completionˇ
9173 three sixth_completionˇ
9174 additional edit
9175 "});
9176
9177 apply_additional_edits.await.unwrap();
9178
9179 update_test_language_settings(&mut cx, |settings| {
9180 settings.defaults.show_completions_on_input = Some(false);
9181 });
9182 cx.set_state("editorˇ");
9183 cx.simulate_keystroke(".");
9184 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9185 cx.simulate_keystroke("c");
9186 cx.simulate_keystroke("l");
9187 cx.simulate_keystroke("o");
9188 cx.assert_editor_state("editor.cloˇ");
9189 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9190 cx.update_editor(|editor, window, cx| {
9191 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9192 });
9193 handle_completion_request(
9194 &mut cx,
9195 "editor.<clo|>",
9196 vec!["close", "clobber"],
9197 counter.clone(),
9198 )
9199 .await;
9200 cx.condition(|editor, _| editor.context_menu_visible())
9201 .await;
9202 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
9203
9204 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9205 editor
9206 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9207 .unwrap()
9208 });
9209 cx.assert_editor_state("editor.closeˇ");
9210 handle_resolve_completion_request(&mut cx, None).await;
9211 apply_additional_edits.await.unwrap();
9212}
9213
9214#[gpui::test]
9215async fn test_word_completion(cx: &mut TestAppContext) {
9216 let lsp_fetch_timeout_ms = 10;
9217 init_test(cx, |language_settings| {
9218 language_settings.defaults.completions = Some(CompletionSettings {
9219 words: WordsCompletionMode::Fallback,
9220 lsp: true,
9221 lsp_fetch_timeout_ms: 10,
9222 });
9223 });
9224
9225 let mut cx = EditorLspTestContext::new_rust(
9226 lsp::ServerCapabilities {
9227 completion_provider: Some(lsp::CompletionOptions {
9228 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9229 ..lsp::CompletionOptions::default()
9230 }),
9231 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9232 ..lsp::ServerCapabilities::default()
9233 },
9234 cx,
9235 )
9236 .await;
9237
9238 let throttle_completions = Arc::new(AtomicBool::new(false));
9239
9240 let lsp_throttle_completions = throttle_completions.clone();
9241 let _completion_requests_handler =
9242 cx.lsp
9243 .server
9244 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
9245 let lsp_throttle_completions = lsp_throttle_completions.clone();
9246 async move {
9247 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
9248 cx.background_executor()
9249 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
9250 .await;
9251 }
9252 Ok(Some(lsp::CompletionResponse::Array(vec![
9253 lsp::CompletionItem {
9254 label: "first".into(),
9255 ..lsp::CompletionItem::default()
9256 },
9257 lsp::CompletionItem {
9258 label: "last".into(),
9259 ..lsp::CompletionItem::default()
9260 },
9261 ])))
9262 }
9263 });
9264
9265 cx.set_state(indoc! {"
9266 oneˇ
9267 two
9268 three
9269 "});
9270 cx.simulate_keystroke(".");
9271 cx.executor().run_until_parked();
9272 cx.condition(|editor, _| editor.context_menu_visible())
9273 .await;
9274 cx.update_editor(|editor, window, cx| {
9275 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9276 {
9277 assert_eq!(
9278 completion_menu_entries(&menu),
9279 &["first", "last"],
9280 "When LSP server is fast to reply, no fallback word completions are used"
9281 );
9282 } else {
9283 panic!("expected completion menu to be open");
9284 }
9285 editor.cancel(&Cancel, window, cx);
9286 });
9287 cx.executor().run_until_parked();
9288 cx.condition(|editor, _| !editor.context_menu_visible())
9289 .await;
9290
9291 throttle_completions.store(true, atomic::Ordering::Release);
9292 cx.simulate_keystroke(".");
9293 cx.executor()
9294 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
9295 cx.executor().run_until_parked();
9296 cx.condition(|editor, _| editor.context_menu_visible())
9297 .await;
9298 cx.update_editor(|editor, _, _| {
9299 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9300 {
9301 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
9302 "When LSP server is slow, document words can be shown instead, if configured accordingly");
9303 } else {
9304 panic!("expected completion menu to be open");
9305 }
9306 });
9307}
9308
9309#[gpui::test]
9310async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
9311 init_test(cx, |language_settings| {
9312 language_settings.defaults.completions = Some(CompletionSettings {
9313 words: WordsCompletionMode::Enabled,
9314 lsp: true,
9315 lsp_fetch_timeout_ms: 0,
9316 });
9317 });
9318
9319 let mut cx = EditorLspTestContext::new_rust(
9320 lsp::ServerCapabilities {
9321 completion_provider: Some(lsp::CompletionOptions {
9322 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9323 ..lsp::CompletionOptions::default()
9324 }),
9325 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9326 ..lsp::ServerCapabilities::default()
9327 },
9328 cx,
9329 )
9330 .await;
9331
9332 let _completion_requests_handler =
9333 cx.lsp
9334 .server
9335 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9336 Ok(Some(lsp::CompletionResponse::Array(vec![
9337 lsp::CompletionItem {
9338 label: "first".into(),
9339 ..lsp::CompletionItem::default()
9340 },
9341 lsp::CompletionItem {
9342 label: "last".into(),
9343 ..lsp::CompletionItem::default()
9344 },
9345 ])))
9346 });
9347
9348 cx.set_state(indoc! {"ˇ
9349 first
9350 last
9351 second
9352 "});
9353 cx.simulate_keystroke(".");
9354 cx.executor().run_until_parked();
9355 cx.condition(|editor, _| editor.context_menu_visible())
9356 .await;
9357 cx.update_editor(|editor, _, _| {
9358 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9359 {
9360 assert_eq!(
9361 completion_menu_entries(&menu),
9362 &["first", "last", "second"],
9363 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
9364 );
9365 } else {
9366 panic!("expected completion menu to be open");
9367 }
9368 });
9369}
9370
9371#[gpui::test]
9372async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
9373 init_test(cx, |language_settings| {
9374 language_settings.defaults.completions = Some(CompletionSettings {
9375 words: WordsCompletionMode::Disabled,
9376 lsp: true,
9377 lsp_fetch_timeout_ms: 0,
9378 });
9379 });
9380
9381 let mut cx = EditorLspTestContext::new_rust(
9382 lsp::ServerCapabilities {
9383 completion_provider: Some(lsp::CompletionOptions {
9384 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9385 ..lsp::CompletionOptions::default()
9386 }),
9387 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9388 ..lsp::ServerCapabilities::default()
9389 },
9390 cx,
9391 )
9392 .await;
9393
9394 let _completion_requests_handler =
9395 cx.lsp
9396 .server
9397 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9398 panic!("LSP completions should not be queried when dealing with word completions")
9399 });
9400
9401 cx.set_state(indoc! {"ˇ
9402 first
9403 last
9404 second
9405 "});
9406 cx.update_editor(|editor, window, cx| {
9407 editor.show_word_completions(&ShowWordCompletions, window, cx);
9408 });
9409 cx.executor().run_until_parked();
9410 cx.condition(|editor, _| editor.context_menu_visible())
9411 .await;
9412 cx.update_editor(|editor, _, _| {
9413 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9414 {
9415 assert_eq!(
9416 completion_menu_entries(&menu),
9417 &["first", "last", "second"],
9418 "`ShowWordCompletions` action should show word completions"
9419 );
9420 } else {
9421 panic!("expected completion menu to be open");
9422 }
9423 });
9424
9425 cx.simulate_keystroke("s");
9426 cx.executor().run_until_parked();
9427 cx.condition(|editor, _| editor.context_menu_visible())
9428 .await;
9429 cx.update_editor(|editor, _, _| {
9430 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9431 {
9432 assert_eq!(
9433 completion_menu_entries(&menu),
9434 &["second"],
9435 "After showing word completions, further editing should filter them and not query the LSP"
9436 );
9437 } else {
9438 panic!("expected completion menu to be open");
9439 }
9440 });
9441}
9442
9443#[gpui::test]
9444async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
9445 init_test(cx, |language_settings| {
9446 language_settings.defaults.completions = Some(CompletionSettings {
9447 words: WordsCompletionMode::Fallback,
9448 lsp: false,
9449 lsp_fetch_timeout_ms: 0,
9450 });
9451 });
9452
9453 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
9454
9455 cx.set_state(indoc! {"ˇ
9456 0_usize
9457 let
9458 33
9459 4.5f32
9460 "});
9461 cx.update_editor(|editor, window, cx| {
9462 editor.show_completions(&ShowCompletions::default(), window, cx);
9463 });
9464 cx.executor().run_until_parked();
9465 cx.condition(|editor, _| editor.context_menu_visible())
9466 .await;
9467 cx.update_editor(|editor, window, cx| {
9468 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9469 {
9470 assert_eq!(
9471 completion_menu_entries(&menu),
9472 &["let"],
9473 "With no digits in the completion query, no digits should be in the word completions"
9474 );
9475 } else {
9476 panic!("expected completion menu to be open");
9477 }
9478 editor.cancel(&Cancel, window, cx);
9479 });
9480
9481 cx.set_state(indoc! {"3ˇ
9482 0_usize
9483 let
9484 3
9485 33.35f32
9486 "});
9487 cx.update_editor(|editor, window, cx| {
9488 editor.show_completions(&ShowCompletions::default(), window, cx);
9489 });
9490 cx.executor().run_until_parked();
9491 cx.condition(|editor, _| editor.context_menu_visible())
9492 .await;
9493 cx.update_editor(|editor, _, _| {
9494 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9495 {
9496 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
9497 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
9498 } else {
9499 panic!("expected completion menu to be open");
9500 }
9501 });
9502}
9503
9504#[gpui::test]
9505async fn test_multiline_completion(cx: &mut TestAppContext) {
9506 init_test(cx, |_| {});
9507
9508 let fs = FakeFs::new(cx.executor());
9509 fs.insert_tree(
9510 path!("/a"),
9511 json!({
9512 "main.ts": "a",
9513 }),
9514 )
9515 .await;
9516
9517 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9518 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9519 let typescript_language = Arc::new(Language::new(
9520 LanguageConfig {
9521 name: "TypeScript".into(),
9522 matcher: LanguageMatcher {
9523 path_suffixes: vec!["ts".to_string()],
9524 ..LanguageMatcher::default()
9525 },
9526 line_comments: vec!["// ".into()],
9527 ..LanguageConfig::default()
9528 },
9529 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9530 ));
9531 language_registry.add(typescript_language.clone());
9532 let mut fake_servers = language_registry.register_fake_lsp(
9533 "TypeScript",
9534 FakeLspAdapter {
9535 capabilities: lsp::ServerCapabilities {
9536 completion_provider: Some(lsp::CompletionOptions {
9537 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9538 ..lsp::CompletionOptions::default()
9539 }),
9540 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9541 ..lsp::ServerCapabilities::default()
9542 },
9543 // Emulate vtsls label generation
9544 label_for_completion: Some(Box::new(|item, _| {
9545 let text = if let Some(description) = item
9546 .label_details
9547 .as_ref()
9548 .and_then(|label_details| label_details.description.as_ref())
9549 {
9550 format!("{} {}", item.label, description)
9551 } else if let Some(detail) = &item.detail {
9552 format!("{} {}", item.label, detail)
9553 } else {
9554 item.label.clone()
9555 };
9556 let len = text.len();
9557 Some(language::CodeLabel {
9558 text,
9559 runs: Vec::new(),
9560 filter_range: 0..len,
9561 })
9562 })),
9563 ..FakeLspAdapter::default()
9564 },
9565 );
9566 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9567 let cx = &mut VisualTestContext::from_window(*workspace, cx);
9568 let worktree_id = workspace
9569 .update(cx, |workspace, _window, cx| {
9570 workspace.project().update(cx, |project, cx| {
9571 project.worktrees(cx).next().unwrap().read(cx).id()
9572 })
9573 })
9574 .unwrap();
9575 let _buffer = project
9576 .update(cx, |project, cx| {
9577 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
9578 })
9579 .await
9580 .unwrap();
9581 let editor = workspace
9582 .update(cx, |workspace, window, cx| {
9583 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
9584 })
9585 .unwrap()
9586 .await
9587 .unwrap()
9588 .downcast::<Editor>()
9589 .unwrap();
9590 let fake_server = fake_servers.next().await.unwrap();
9591
9592 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
9593 let multiline_label_2 = "a\nb\nc\n";
9594 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
9595 let multiline_description = "d\ne\nf\n";
9596 let multiline_detail_2 = "g\nh\ni\n";
9597
9598 let mut completion_handle =
9599 fake_server.handle_request::<lsp::request::Completion, _, _>(move |params, _| async move {
9600 Ok(Some(lsp::CompletionResponse::Array(vec![
9601 lsp::CompletionItem {
9602 label: multiline_label.to_string(),
9603 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9604 range: lsp::Range {
9605 start: lsp::Position {
9606 line: params.text_document_position.position.line,
9607 character: params.text_document_position.position.character,
9608 },
9609 end: lsp::Position {
9610 line: params.text_document_position.position.line,
9611 character: params.text_document_position.position.character,
9612 },
9613 },
9614 new_text: "new_text_1".to_string(),
9615 })),
9616 ..lsp::CompletionItem::default()
9617 },
9618 lsp::CompletionItem {
9619 label: "single line label 1".to_string(),
9620 detail: Some(multiline_detail.to_string()),
9621 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9622 range: lsp::Range {
9623 start: lsp::Position {
9624 line: params.text_document_position.position.line,
9625 character: params.text_document_position.position.character,
9626 },
9627 end: lsp::Position {
9628 line: params.text_document_position.position.line,
9629 character: params.text_document_position.position.character,
9630 },
9631 },
9632 new_text: "new_text_2".to_string(),
9633 })),
9634 ..lsp::CompletionItem::default()
9635 },
9636 lsp::CompletionItem {
9637 label: "single line label 2".to_string(),
9638 label_details: Some(lsp::CompletionItemLabelDetails {
9639 description: Some(multiline_description.to_string()),
9640 detail: None,
9641 }),
9642 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9643 range: lsp::Range {
9644 start: lsp::Position {
9645 line: params.text_document_position.position.line,
9646 character: params.text_document_position.position.character,
9647 },
9648 end: lsp::Position {
9649 line: params.text_document_position.position.line,
9650 character: params.text_document_position.position.character,
9651 },
9652 },
9653 new_text: "new_text_2".to_string(),
9654 })),
9655 ..lsp::CompletionItem::default()
9656 },
9657 lsp::CompletionItem {
9658 label: multiline_label_2.to_string(),
9659 detail: Some(multiline_detail_2.to_string()),
9660 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9661 range: lsp::Range {
9662 start: lsp::Position {
9663 line: params.text_document_position.position.line,
9664 character: params.text_document_position.position.character,
9665 },
9666 end: lsp::Position {
9667 line: params.text_document_position.position.line,
9668 character: params.text_document_position.position.character,
9669 },
9670 },
9671 new_text: "new_text_3".to_string(),
9672 })),
9673 ..lsp::CompletionItem::default()
9674 },
9675 lsp::CompletionItem {
9676 label: "Label with many spaces and \t but without newlines".to_string(),
9677 detail: Some(
9678 "Details with many spaces and \t but without newlines".to_string(),
9679 ),
9680 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9681 range: lsp::Range {
9682 start: lsp::Position {
9683 line: params.text_document_position.position.line,
9684 character: params.text_document_position.position.character,
9685 },
9686 end: lsp::Position {
9687 line: params.text_document_position.position.line,
9688 character: params.text_document_position.position.character,
9689 },
9690 },
9691 new_text: "new_text_4".to_string(),
9692 })),
9693 ..lsp::CompletionItem::default()
9694 },
9695 ])))
9696 });
9697
9698 editor.update_in(cx, |editor, window, cx| {
9699 cx.focus_self(window);
9700 editor.move_to_end(&MoveToEnd, window, cx);
9701 editor.handle_input(".", window, cx);
9702 });
9703 cx.run_until_parked();
9704 completion_handle.next().await.unwrap();
9705
9706 editor.update(cx, |editor, _| {
9707 assert!(editor.context_menu_visible());
9708 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9709 {
9710 let completion_labels = menu
9711 .completions
9712 .borrow()
9713 .iter()
9714 .map(|c| c.label.text.clone())
9715 .collect::<Vec<_>>();
9716 assert_eq!(
9717 completion_labels,
9718 &[
9719 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
9720 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
9721 "single line label 2 d e f ",
9722 "a b c g h i ",
9723 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
9724 ],
9725 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
9726 );
9727
9728 for completion in menu
9729 .completions
9730 .borrow()
9731 .iter() {
9732 assert_eq!(
9733 completion.label.filter_range,
9734 0..completion.label.text.len(),
9735 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
9736 );
9737 }
9738
9739 } else {
9740 panic!("expected completion menu to be open");
9741 }
9742 });
9743}
9744
9745#[gpui::test]
9746async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
9747 init_test(cx, |_| {});
9748 let mut cx = EditorLspTestContext::new_rust(
9749 lsp::ServerCapabilities {
9750 completion_provider: Some(lsp::CompletionOptions {
9751 trigger_characters: Some(vec![".".to_string()]),
9752 ..Default::default()
9753 }),
9754 ..Default::default()
9755 },
9756 cx,
9757 )
9758 .await;
9759 cx.lsp
9760 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9761 Ok(Some(lsp::CompletionResponse::Array(vec![
9762 lsp::CompletionItem {
9763 label: "first".into(),
9764 ..Default::default()
9765 },
9766 lsp::CompletionItem {
9767 label: "last".into(),
9768 ..Default::default()
9769 },
9770 ])))
9771 });
9772 cx.set_state("variableˇ");
9773 cx.simulate_keystroke(".");
9774 cx.executor().run_until_parked();
9775
9776 cx.update_editor(|editor, _, _| {
9777 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9778 {
9779 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
9780 } else {
9781 panic!("expected completion menu to be open");
9782 }
9783 });
9784
9785 cx.update_editor(|editor, window, cx| {
9786 editor.move_page_down(&MovePageDown::default(), window, cx);
9787 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9788 {
9789 assert!(
9790 menu.selected_item == 1,
9791 "expected PageDown to select the last item from the context menu"
9792 );
9793 } else {
9794 panic!("expected completion menu to stay open after PageDown");
9795 }
9796 });
9797
9798 cx.update_editor(|editor, window, cx| {
9799 editor.move_page_up(&MovePageUp::default(), window, cx);
9800 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9801 {
9802 assert!(
9803 menu.selected_item == 0,
9804 "expected PageUp to select the first item from the context menu"
9805 );
9806 } else {
9807 panic!("expected completion menu to stay open after PageUp");
9808 }
9809 });
9810}
9811
9812#[gpui::test]
9813async fn test_completion_sort(cx: &mut TestAppContext) {
9814 init_test(cx, |_| {});
9815 let mut cx = EditorLspTestContext::new_rust(
9816 lsp::ServerCapabilities {
9817 completion_provider: Some(lsp::CompletionOptions {
9818 trigger_characters: Some(vec![".".to_string()]),
9819 ..Default::default()
9820 }),
9821 ..Default::default()
9822 },
9823 cx,
9824 )
9825 .await;
9826 cx.lsp
9827 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9828 Ok(Some(lsp::CompletionResponse::Array(vec![
9829 lsp::CompletionItem {
9830 label: "Range".into(),
9831 sort_text: Some("a".into()),
9832 ..Default::default()
9833 },
9834 lsp::CompletionItem {
9835 label: "r".into(),
9836 sort_text: Some("b".into()),
9837 ..Default::default()
9838 },
9839 lsp::CompletionItem {
9840 label: "ret".into(),
9841 sort_text: Some("c".into()),
9842 ..Default::default()
9843 },
9844 lsp::CompletionItem {
9845 label: "return".into(),
9846 sort_text: Some("d".into()),
9847 ..Default::default()
9848 },
9849 lsp::CompletionItem {
9850 label: "slice".into(),
9851 sort_text: Some("d".into()),
9852 ..Default::default()
9853 },
9854 ])))
9855 });
9856 cx.set_state("rˇ");
9857 cx.executor().run_until_parked();
9858 cx.update_editor(|editor, window, cx| {
9859 editor.show_completions(
9860 &ShowCompletions {
9861 trigger: Some("r".into()),
9862 },
9863 window,
9864 cx,
9865 );
9866 });
9867 cx.executor().run_until_parked();
9868
9869 cx.update_editor(|editor, _, _| {
9870 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9871 {
9872 assert_eq!(
9873 completion_menu_entries(&menu),
9874 &["r", "ret", "Range", "return"]
9875 );
9876 } else {
9877 panic!("expected completion menu to be open");
9878 }
9879 });
9880}
9881
9882#[gpui::test]
9883async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
9884 init_test(cx, |_| {});
9885
9886 let mut cx = EditorLspTestContext::new_rust(
9887 lsp::ServerCapabilities {
9888 completion_provider: Some(lsp::CompletionOptions {
9889 trigger_characters: Some(vec![".".to_string()]),
9890 resolve_provider: Some(true),
9891 ..Default::default()
9892 }),
9893 ..Default::default()
9894 },
9895 cx,
9896 )
9897 .await;
9898
9899 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
9900 cx.simulate_keystroke(".");
9901 let completion_item = lsp::CompletionItem {
9902 label: "Some".into(),
9903 kind: Some(lsp::CompletionItemKind::SNIPPET),
9904 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
9905 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
9906 kind: lsp::MarkupKind::Markdown,
9907 value: "```rust\nSome(2)\n```".to_string(),
9908 })),
9909 deprecated: Some(false),
9910 sort_text: Some("Some".to_string()),
9911 filter_text: Some("Some".to_string()),
9912 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
9913 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9914 range: lsp::Range {
9915 start: lsp::Position {
9916 line: 0,
9917 character: 22,
9918 },
9919 end: lsp::Position {
9920 line: 0,
9921 character: 22,
9922 },
9923 },
9924 new_text: "Some(2)".to_string(),
9925 })),
9926 additional_text_edits: Some(vec![lsp::TextEdit {
9927 range: lsp::Range {
9928 start: lsp::Position {
9929 line: 0,
9930 character: 20,
9931 },
9932 end: lsp::Position {
9933 line: 0,
9934 character: 22,
9935 },
9936 },
9937 new_text: "".to_string(),
9938 }]),
9939 ..Default::default()
9940 };
9941
9942 let closure_completion_item = completion_item.clone();
9943 let counter = Arc::new(AtomicUsize::new(0));
9944 let counter_clone = counter.clone();
9945 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
9946 let task_completion_item = closure_completion_item.clone();
9947 counter_clone.fetch_add(1, atomic::Ordering::Release);
9948 async move {
9949 Ok(Some(lsp::CompletionResponse::Array(vec![
9950 task_completion_item,
9951 ])))
9952 }
9953 });
9954
9955 cx.condition(|editor, _| editor.context_menu_visible())
9956 .await;
9957 cx.assert_editor_state(indoc! {"fn main() { let a = 2.ˇ; }"});
9958 assert!(request.next().await.is_some());
9959 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9960
9961 cx.simulate_keystroke("S");
9962 cx.simulate_keystroke("o");
9963 cx.simulate_keystroke("m");
9964 cx.condition(|editor, _| editor.context_menu_visible())
9965 .await;
9966 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Somˇ; }"});
9967 assert!(request.next().await.is_some());
9968 assert!(request.next().await.is_some());
9969 assert!(request.next().await.is_some());
9970 request.close();
9971 assert!(request.next().await.is_none());
9972 assert_eq!(
9973 counter.load(atomic::Ordering::Acquire),
9974 4,
9975 "With the completions menu open, only one LSP request should happen per input"
9976 );
9977}
9978
9979#[gpui::test]
9980async fn test_toggle_comment(cx: &mut TestAppContext) {
9981 init_test(cx, |_| {});
9982 let mut cx = EditorTestContext::new(cx).await;
9983 let language = Arc::new(Language::new(
9984 LanguageConfig {
9985 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
9986 ..Default::default()
9987 },
9988 Some(tree_sitter_rust::LANGUAGE.into()),
9989 ));
9990 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
9991
9992 // If multiple selections intersect a line, the line is only toggled once.
9993 cx.set_state(indoc! {"
9994 fn a() {
9995 «//b();
9996 ˇ»// «c();
9997 //ˇ» d();
9998 }
9999 "});
10000
10001 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10002
10003 cx.assert_editor_state(indoc! {"
10004 fn a() {
10005 «b();
10006 c();
10007 ˇ» d();
10008 }
10009 "});
10010
10011 // The comment prefix is inserted at the same column for every line in a
10012 // selection.
10013 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10014
10015 cx.assert_editor_state(indoc! {"
10016 fn a() {
10017 // «b();
10018 // c();
10019 ˇ»// d();
10020 }
10021 "});
10022
10023 // If a selection ends at the beginning of a line, that line is not toggled.
10024 cx.set_selections_state(indoc! {"
10025 fn a() {
10026 // b();
10027 «// c();
10028 ˇ» // d();
10029 }
10030 "});
10031
10032 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10033
10034 cx.assert_editor_state(indoc! {"
10035 fn a() {
10036 // b();
10037 «c();
10038 ˇ» // d();
10039 }
10040 "});
10041
10042 // If a selection span a single line and is empty, the line is toggled.
10043 cx.set_state(indoc! {"
10044 fn a() {
10045 a();
10046 b();
10047 ˇ
10048 }
10049 "});
10050
10051 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10052
10053 cx.assert_editor_state(indoc! {"
10054 fn a() {
10055 a();
10056 b();
10057 //•ˇ
10058 }
10059 "});
10060
10061 // If a selection span multiple lines, empty lines are not toggled.
10062 cx.set_state(indoc! {"
10063 fn a() {
10064 «a();
10065
10066 c();ˇ»
10067 }
10068 "});
10069
10070 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10071
10072 cx.assert_editor_state(indoc! {"
10073 fn a() {
10074 // «a();
10075
10076 // c();ˇ»
10077 }
10078 "});
10079
10080 // If a selection includes multiple comment prefixes, all lines are uncommented.
10081 cx.set_state(indoc! {"
10082 fn a() {
10083 «// a();
10084 /// b();
10085 //! c();ˇ»
10086 }
10087 "});
10088
10089 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10090
10091 cx.assert_editor_state(indoc! {"
10092 fn a() {
10093 «a();
10094 b();
10095 c();ˇ»
10096 }
10097 "});
10098}
10099
10100#[gpui::test]
10101async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
10102 init_test(cx, |_| {});
10103 let mut cx = EditorTestContext::new(cx).await;
10104 let language = Arc::new(Language::new(
10105 LanguageConfig {
10106 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10107 ..Default::default()
10108 },
10109 Some(tree_sitter_rust::LANGUAGE.into()),
10110 ));
10111 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10112
10113 let toggle_comments = &ToggleComments {
10114 advance_downwards: false,
10115 ignore_indent: true,
10116 };
10117
10118 // If multiple selections intersect a line, the line is only toggled once.
10119 cx.set_state(indoc! {"
10120 fn a() {
10121 // «b();
10122 // c();
10123 // ˇ» d();
10124 }
10125 "});
10126
10127 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10128
10129 cx.assert_editor_state(indoc! {"
10130 fn a() {
10131 «b();
10132 c();
10133 ˇ» d();
10134 }
10135 "});
10136
10137 // The comment prefix is inserted at the beginning of each line
10138 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10139
10140 cx.assert_editor_state(indoc! {"
10141 fn a() {
10142 // «b();
10143 // c();
10144 // ˇ» d();
10145 }
10146 "});
10147
10148 // If a selection ends at the beginning of a line, that line is not toggled.
10149 cx.set_selections_state(indoc! {"
10150 fn a() {
10151 // b();
10152 // «c();
10153 ˇ»// d();
10154 }
10155 "});
10156
10157 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10158
10159 cx.assert_editor_state(indoc! {"
10160 fn a() {
10161 // b();
10162 «c();
10163 ˇ»// d();
10164 }
10165 "});
10166
10167 // If a selection span a single line and is empty, the line is toggled.
10168 cx.set_state(indoc! {"
10169 fn a() {
10170 a();
10171 b();
10172 ˇ
10173 }
10174 "});
10175
10176 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10177
10178 cx.assert_editor_state(indoc! {"
10179 fn a() {
10180 a();
10181 b();
10182 //ˇ
10183 }
10184 "});
10185
10186 // If a selection span multiple lines, empty lines are not toggled.
10187 cx.set_state(indoc! {"
10188 fn a() {
10189 «a();
10190
10191 c();ˇ»
10192 }
10193 "});
10194
10195 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10196
10197 cx.assert_editor_state(indoc! {"
10198 fn a() {
10199 // «a();
10200
10201 // c();ˇ»
10202 }
10203 "});
10204
10205 // If a selection includes multiple comment prefixes, all lines are uncommented.
10206 cx.set_state(indoc! {"
10207 fn a() {
10208 // «a();
10209 /// b();
10210 //! c();ˇ»
10211 }
10212 "});
10213
10214 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10215
10216 cx.assert_editor_state(indoc! {"
10217 fn a() {
10218 «a();
10219 b();
10220 c();ˇ»
10221 }
10222 "});
10223}
10224
10225#[gpui::test]
10226async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
10227 init_test(cx, |_| {});
10228
10229 let language = Arc::new(Language::new(
10230 LanguageConfig {
10231 line_comments: vec!["// ".into()],
10232 ..Default::default()
10233 },
10234 Some(tree_sitter_rust::LANGUAGE.into()),
10235 ));
10236
10237 let mut cx = EditorTestContext::new(cx).await;
10238
10239 cx.language_registry().add(language.clone());
10240 cx.update_buffer(|buffer, cx| {
10241 buffer.set_language(Some(language), cx);
10242 });
10243
10244 let toggle_comments = &ToggleComments {
10245 advance_downwards: true,
10246 ignore_indent: false,
10247 };
10248
10249 // Single cursor on one line -> advance
10250 // Cursor moves horizontally 3 characters as well on non-blank line
10251 cx.set_state(indoc!(
10252 "fn a() {
10253 ˇdog();
10254 cat();
10255 }"
10256 ));
10257 cx.update_editor(|editor, window, cx| {
10258 editor.toggle_comments(toggle_comments, window, cx);
10259 });
10260 cx.assert_editor_state(indoc!(
10261 "fn a() {
10262 // dog();
10263 catˇ();
10264 }"
10265 ));
10266
10267 // Single selection on one line -> don't advance
10268 cx.set_state(indoc!(
10269 "fn a() {
10270 «dog()ˇ»;
10271 cat();
10272 }"
10273 ));
10274 cx.update_editor(|editor, window, cx| {
10275 editor.toggle_comments(toggle_comments, window, cx);
10276 });
10277 cx.assert_editor_state(indoc!(
10278 "fn a() {
10279 // «dog()ˇ»;
10280 cat();
10281 }"
10282 ));
10283
10284 // Multiple cursors on one line -> advance
10285 cx.set_state(indoc!(
10286 "fn a() {
10287 ˇdˇog();
10288 cat();
10289 }"
10290 ));
10291 cx.update_editor(|editor, window, cx| {
10292 editor.toggle_comments(toggle_comments, window, cx);
10293 });
10294 cx.assert_editor_state(indoc!(
10295 "fn a() {
10296 // dog();
10297 catˇ(ˇ);
10298 }"
10299 ));
10300
10301 // Multiple cursors on one line, with selection -> don't advance
10302 cx.set_state(indoc!(
10303 "fn a() {
10304 ˇdˇog«()ˇ»;
10305 cat();
10306 }"
10307 ));
10308 cx.update_editor(|editor, window, cx| {
10309 editor.toggle_comments(toggle_comments, window, cx);
10310 });
10311 cx.assert_editor_state(indoc!(
10312 "fn a() {
10313 // ˇdˇog«()ˇ»;
10314 cat();
10315 }"
10316 ));
10317
10318 // Single cursor on one line -> advance
10319 // Cursor moves to column 0 on blank line
10320 cx.set_state(indoc!(
10321 "fn a() {
10322 ˇdog();
10323
10324 cat();
10325 }"
10326 ));
10327 cx.update_editor(|editor, window, cx| {
10328 editor.toggle_comments(toggle_comments, window, cx);
10329 });
10330 cx.assert_editor_state(indoc!(
10331 "fn a() {
10332 // dog();
10333 ˇ
10334 cat();
10335 }"
10336 ));
10337
10338 // Single cursor on one line -> advance
10339 // Cursor starts and ends at column 0
10340 cx.set_state(indoc!(
10341 "fn a() {
10342 ˇ dog();
10343 cat();
10344 }"
10345 ));
10346 cx.update_editor(|editor, window, cx| {
10347 editor.toggle_comments(toggle_comments, window, cx);
10348 });
10349 cx.assert_editor_state(indoc!(
10350 "fn a() {
10351 // dog();
10352 ˇ cat();
10353 }"
10354 ));
10355}
10356
10357#[gpui::test]
10358async fn test_toggle_block_comment(cx: &mut TestAppContext) {
10359 init_test(cx, |_| {});
10360
10361 let mut cx = EditorTestContext::new(cx).await;
10362
10363 let html_language = Arc::new(
10364 Language::new(
10365 LanguageConfig {
10366 name: "HTML".into(),
10367 block_comment: Some(("<!-- ".into(), " -->".into())),
10368 ..Default::default()
10369 },
10370 Some(tree_sitter_html::LANGUAGE.into()),
10371 )
10372 .with_injection_query(
10373 r#"
10374 (script_element
10375 (raw_text) @injection.content
10376 (#set! injection.language "javascript"))
10377 "#,
10378 )
10379 .unwrap(),
10380 );
10381
10382 let javascript_language = Arc::new(Language::new(
10383 LanguageConfig {
10384 name: "JavaScript".into(),
10385 line_comments: vec!["// ".into()],
10386 ..Default::default()
10387 },
10388 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10389 ));
10390
10391 cx.language_registry().add(html_language.clone());
10392 cx.language_registry().add(javascript_language.clone());
10393 cx.update_buffer(|buffer, cx| {
10394 buffer.set_language(Some(html_language), cx);
10395 });
10396
10397 // Toggle comments for empty selections
10398 cx.set_state(
10399 &r#"
10400 <p>A</p>ˇ
10401 <p>B</p>ˇ
10402 <p>C</p>ˇ
10403 "#
10404 .unindent(),
10405 );
10406 cx.update_editor(|editor, window, cx| {
10407 editor.toggle_comments(&ToggleComments::default(), window, cx)
10408 });
10409 cx.assert_editor_state(
10410 &r#"
10411 <!-- <p>A</p>ˇ -->
10412 <!-- <p>B</p>ˇ -->
10413 <!-- <p>C</p>ˇ -->
10414 "#
10415 .unindent(),
10416 );
10417 cx.update_editor(|editor, window, cx| {
10418 editor.toggle_comments(&ToggleComments::default(), window, cx)
10419 });
10420 cx.assert_editor_state(
10421 &r#"
10422 <p>A</p>ˇ
10423 <p>B</p>ˇ
10424 <p>C</p>ˇ
10425 "#
10426 .unindent(),
10427 );
10428
10429 // Toggle comments for mixture of empty and non-empty selections, where
10430 // multiple selections occupy a given line.
10431 cx.set_state(
10432 &r#"
10433 <p>A«</p>
10434 <p>ˇ»B</p>ˇ
10435 <p>C«</p>
10436 <p>ˇ»D</p>ˇ
10437 "#
10438 .unindent(),
10439 );
10440
10441 cx.update_editor(|editor, window, cx| {
10442 editor.toggle_comments(&ToggleComments::default(), window, cx)
10443 });
10444 cx.assert_editor_state(
10445 &r#"
10446 <!-- <p>A«</p>
10447 <p>ˇ»B</p>ˇ -->
10448 <!-- <p>C«</p>
10449 <p>ˇ»D</p>ˇ -->
10450 "#
10451 .unindent(),
10452 );
10453 cx.update_editor(|editor, window, cx| {
10454 editor.toggle_comments(&ToggleComments::default(), window, cx)
10455 });
10456 cx.assert_editor_state(
10457 &r#"
10458 <p>A«</p>
10459 <p>ˇ»B</p>ˇ
10460 <p>C«</p>
10461 <p>ˇ»D</p>ˇ
10462 "#
10463 .unindent(),
10464 );
10465
10466 // Toggle comments when different languages are active for different
10467 // selections.
10468 cx.set_state(
10469 &r#"
10470 ˇ<script>
10471 ˇvar x = new Y();
10472 ˇ</script>
10473 "#
10474 .unindent(),
10475 );
10476 cx.executor().run_until_parked();
10477 cx.update_editor(|editor, window, cx| {
10478 editor.toggle_comments(&ToggleComments::default(), window, cx)
10479 });
10480 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
10481 // Uncommenting and commenting from this position brings in even more wrong artifacts.
10482 cx.assert_editor_state(
10483 &r#"
10484 <!-- ˇ<script> -->
10485 // ˇvar x = new Y();
10486 <!-- ˇ</script> -->
10487 "#
10488 .unindent(),
10489 );
10490}
10491
10492#[gpui::test]
10493fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
10494 init_test(cx, |_| {});
10495
10496 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10497 let multibuffer = cx.new(|cx| {
10498 let mut multibuffer = MultiBuffer::new(ReadWrite);
10499 multibuffer.push_excerpts(
10500 buffer.clone(),
10501 [
10502 ExcerptRange {
10503 context: Point::new(0, 0)..Point::new(0, 4),
10504 primary: None,
10505 },
10506 ExcerptRange {
10507 context: Point::new(1, 0)..Point::new(1, 4),
10508 primary: None,
10509 },
10510 ],
10511 cx,
10512 );
10513 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
10514 multibuffer
10515 });
10516
10517 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
10518 editor.update_in(cx, |editor, window, cx| {
10519 assert_eq!(editor.text(cx), "aaaa\nbbbb");
10520 editor.change_selections(None, window, cx, |s| {
10521 s.select_ranges([
10522 Point::new(0, 0)..Point::new(0, 0),
10523 Point::new(1, 0)..Point::new(1, 0),
10524 ])
10525 });
10526
10527 editor.handle_input("X", window, cx);
10528 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
10529 assert_eq!(
10530 editor.selections.ranges(cx),
10531 [
10532 Point::new(0, 1)..Point::new(0, 1),
10533 Point::new(1, 1)..Point::new(1, 1),
10534 ]
10535 );
10536
10537 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
10538 editor.change_selections(None, window, cx, |s| {
10539 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
10540 });
10541 editor.backspace(&Default::default(), window, cx);
10542 assert_eq!(editor.text(cx), "Xa\nbbb");
10543 assert_eq!(
10544 editor.selections.ranges(cx),
10545 [Point::new(1, 0)..Point::new(1, 0)]
10546 );
10547
10548 editor.change_selections(None, window, cx, |s| {
10549 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
10550 });
10551 editor.backspace(&Default::default(), window, cx);
10552 assert_eq!(editor.text(cx), "X\nbb");
10553 assert_eq!(
10554 editor.selections.ranges(cx),
10555 [Point::new(0, 1)..Point::new(0, 1)]
10556 );
10557 });
10558}
10559
10560#[gpui::test]
10561fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
10562 init_test(cx, |_| {});
10563
10564 let markers = vec![('[', ']').into(), ('(', ')').into()];
10565 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
10566 indoc! {"
10567 [aaaa
10568 (bbbb]
10569 cccc)",
10570 },
10571 markers.clone(),
10572 );
10573 let excerpt_ranges = markers.into_iter().map(|marker| {
10574 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
10575 ExcerptRange {
10576 context,
10577 primary: None,
10578 }
10579 });
10580 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
10581 let multibuffer = cx.new(|cx| {
10582 let mut multibuffer = MultiBuffer::new(ReadWrite);
10583 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
10584 multibuffer
10585 });
10586
10587 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
10588 editor.update_in(cx, |editor, window, cx| {
10589 let (expected_text, selection_ranges) = marked_text_ranges(
10590 indoc! {"
10591 aaaa
10592 bˇbbb
10593 bˇbbˇb
10594 cccc"
10595 },
10596 true,
10597 );
10598 assert_eq!(editor.text(cx), expected_text);
10599 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
10600
10601 editor.handle_input("X", window, cx);
10602
10603 let (expected_text, expected_selections) = marked_text_ranges(
10604 indoc! {"
10605 aaaa
10606 bXˇbbXb
10607 bXˇbbXˇb
10608 cccc"
10609 },
10610 false,
10611 );
10612 assert_eq!(editor.text(cx), expected_text);
10613 assert_eq!(editor.selections.ranges(cx), expected_selections);
10614
10615 editor.newline(&Newline, window, cx);
10616 let (expected_text, expected_selections) = marked_text_ranges(
10617 indoc! {"
10618 aaaa
10619 bX
10620 ˇbbX
10621 b
10622 bX
10623 ˇbbX
10624 ˇb
10625 cccc"
10626 },
10627 false,
10628 );
10629 assert_eq!(editor.text(cx), expected_text);
10630 assert_eq!(editor.selections.ranges(cx), expected_selections);
10631 });
10632}
10633
10634#[gpui::test]
10635fn test_refresh_selections(cx: &mut TestAppContext) {
10636 init_test(cx, |_| {});
10637
10638 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10639 let mut excerpt1_id = None;
10640 let multibuffer = cx.new(|cx| {
10641 let mut multibuffer = MultiBuffer::new(ReadWrite);
10642 excerpt1_id = multibuffer
10643 .push_excerpts(
10644 buffer.clone(),
10645 [
10646 ExcerptRange {
10647 context: Point::new(0, 0)..Point::new(1, 4),
10648 primary: None,
10649 },
10650 ExcerptRange {
10651 context: Point::new(1, 0)..Point::new(2, 4),
10652 primary: None,
10653 },
10654 ],
10655 cx,
10656 )
10657 .into_iter()
10658 .next();
10659 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
10660 multibuffer
10661 });
10662
10663 let editor = cx.add_window(|window, cx| {
10664 let mut editor = build_editor(multibuffer.clone(), window, cx);
10665 let snapshot = editor.snapshot(window, cx);
10666 editor.change_selections(None, window, cx, |s| {
10667 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
10668 });
10669 editor.begin_selection(
10670 Point::new(2, 1).to_display_point(&snapshot),
10671 true,
10672 1,
10673 window,
10674 cx,
10675 );
10676 assert_eq!(
10677 editor.selections.ranges(cx),
10678 [
10679 Point::new(1, 3)..Point::new(1, 3),
10680 Point::new(2, 1)..Point::new(2, 1),
10681 ]
10682 );
10683 editor
10684 });
10685
10686 // Refreshing selections is a no-op when excerpts haven't changed.
10687 _ = editor.update(cx, |editor, window, cx| {
10688 editor.change_selections(None, window, cx, |s| s.refresh());
10689 assert_eq!(
10690 editor.selections.ranges(cx),
10691 [
10692 Point::new(1, 3)..Point::new(1, 3),
10693 Point::new(2, 1)..Point::new(2, 1),
10694 ]
10695 );
10696 });
10697
10698 multibuffer.update(cx, |multibuffer, cx| {
10699 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
10700 });
10701 _ = editor.update(cx, |editor, window, cx| {
10702 // Removing an excerpt causes the first selection to become degenerate.
10703 assert_eq!(
10704 editor.selections.ranges(cx),
10705 [
10706 Point::new(0, 0)..Point::new(0, 0),
10707 Point::new(0, 1)..Point::new(0, 1)
10708 ]
10709 );
10710
10711 // Refreshing selections will relocate the first selection to the original buffer
10712 // location.
10713 editor.change_selections(None, window, cx, |s| s.refresh());
10714 assert_eq!(
10715 editor.selections.ranges(cx),
10716 [
10717 Point::new(0, 1)..Point::new(0, 1),
10718 Point::new(0, 3)..Point::new(0, 3)
10719 ]
10720 );
10721 assert!(editor.selections.pending_anchor().is_some());
10722 });
10723}
10724
10725#[gpui::test]
10726fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
10727 init_test(cx, |_| {});
10728
10729 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10730 let mut excerpt1_id = None;
10731 let multibuffer = cx.new(|cx| {
10732 let mut multibuffer = MultiBuffer::new(ReadWrite);
10733 excerpt1_id = multibuffer
10734 .push_excerpts(
10735 buffer.clone(),
10736 [
10737 ExcerptRange {
10738 context: Point::new(0, 0)..Point::new(1, 4),
10739 primary: None,
10740 },
10741 ExcerptRange {
10742 context: Point::new(1, 0)..Point::new(2, 4),
10743 primary: None,
10744 },
10745 ],
10746 cx,
10747 )
10748 .into_iter()
10749 .next();
10750 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
10751 multibuffer
10752 });
10753
10754 let editor = cx.add_window(|window, cx| {
10755 let mut editor = build_editor(multibuffer.clone(), window, cx);
10756 let snapshot = editor.snapshot(window, cx);
10757 editor.begin_selection(
10758 Point::new(1, 3).to_display_point(&snapshot),
10759 false,
10760 1,
10761 window,
10762 cx,
10763 );
10764 assert_eq!(
10765 editor.selections.ranges(cx),
10766 [Point::new(1, 3)..Point::new(1, 3)]
10767 );
10768 editor
10769 });
10770
10771 multibuffer.update(cx, |multibuffer, cx| {
10772 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
10773 });
10774 _ = editor.update(cx, |editor, window, cx| {
10775 assert_eq!(
10776 editor.selections.ranges(cx),
10777 [Point::new(0, 0)..Point::new(0, 0)]
10778 );
10779
10780 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
10781 editor.change_selections(None, window, cx, |s| s.refresh());
10782 assert_eq!(
10783 editor.selections.ranges(cx),
10784 [Point::new(0, 3)..Point::new(0, 3)]
10785 );
10786 assert!(editor.selections.pending_anchor().is_some());
10787 });
10788}
10789
10790#[gpui::test]
10791async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
10792 init_test(cx, |_| {});
10793
10794 let language = Arc::new(
10795 Language::new(
10796 LanguageConfig {
10797 brackets: BracketPairConfig {
10798 pairs: vec![
10799 BracketPair {
10800 start: "{".to_string(),
10801 end: "}".to_string(),
10802 close: true,
10803 surround: true,
10804 newline: true,
10805 },
10806 BracketPair {
10807 start: "/* ".to_string(),
10808 end: " */".to_string(),
10809 close: true,
10810 surround: true,
10811 newline: true,
10812 },
10813 ],
10814 ..Default::default()
10815 },
10816 ..Default::default()
10817 },
10818 Some(tree_sitter_rust::LANGUAGE.into()),
10819 )
10820 .with_indents_query("")
10821 .unwrap(),
10822 );
10823
10824 let text = concat!(
10825 "{ }\n", //
10826 " x\n", //
10827 " /* */\n", //
10828 "x\n", //
10829 "{{} }\n", //
10830 );
10831
10832 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10833 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10834 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10835 editor
10836 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10837 .await;
10838
10839 editor.update_in(cx, |editor, window, cx| {
10840 editor.change_selections(None, window, cx, |s| {
10841 s.select_display_ranges([
10842 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
10843 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
10844 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
10845 ])
10846 });
10847 editor.newline(&Newline, window, cx);
10848
10849 assert_eq!(
10850 editor.buffer().read(cx).read(cx).text(),
10851 concat!(
10852 "{ \n", // Suppress rustfmt
10853 "\n", //
10854 "}\n", //
10855 " x\n", //
10856 " /* \n", //
10857 " \n", //
10858 " */\n", //
10859 "x\n", //
10860 "{{} \n", //
10861 "}\n", //
10862 )
10863 );
10864 });
10865}
10866
10867#[gpui::test]
10868fn test_highlighted_ranges(cx: &mut TestAppContext) {
10869 init_test(cx, |_| {});
10870
10871 let editor = cx.add_window(|window, cx| {
10872 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
10873 build_editor(buffer.clone(), window, cx)
10874 });
10875
10876 _ = editor.update(cx, |editor, window, cx| {
10877 struct Type1;
10878 struct Type2;
10879
10880 let buffer = editor.buffer.read(cx).snapshot(cx);
10881
10882 let anchor_range =
10883 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
10884
10885 editor.highlight_background::<Type1>(
10886 &[
10887 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
10888 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
10889 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
10890 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
10891 ],
10892 |_| Hsla::red(),
10893 cx,
10894 );
10895 editor.highlight_background::<Type2>(
10896 &[
10897 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
10898 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
10899 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
10900 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
10901 ],
10902 |_| Hsla::green(),
10903 cx,
10904 );
10905
10906 let snapshot = editor.snapshot(window, cx);
10907 let mut highlighted_ranges = editor.background_highlights_in_range(
10908 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
10909 &snapshot,
10910 cx.theme().colors(),
10911 );
10912 // Enforce a consistent ordering based on color without relying on the ordering of the
10913 // highlight's `TypeId` which is non-executor.
10914 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
10915 assert_eq!(
10916 highlighted_ranges,
10917 &[
10918 (
10919 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
10920 Hsla::red(),
10921 ),
10922 (
10923 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
10924 Hsla::red(),
10925 ),
10926 (
10927 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
10928 Hsla::green(),
10929 ),
10930 (
10931 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
10932 Hsla::green(),
10933 ),
10934 ]
10935 );
10936 assert_eq!(
10937 editor.background_highlights_in_range(
10938 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
10939 &snapshot,
10940 cx.theme().colors(),
10941 ),
10942 &[(
10943 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
10944 Hsla::red(),
10945 )]
10946 );
10947 });
10948}
10949
10950#[gpui::test]
10951async fn test_following(cx: &mut TestAppContext) {
10952 init_test(cx, |_| {});
10953
10954 let fs = FakeFs::new(cx.executor());
10955 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
10956
10957 let buffer = project.update(cx, |project, cx| {
10958 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
10959 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
10960 });
10961 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
10962 let follower = cx.update(|cx| {
10963 cx.open_window(
10964 WindowOptions {
10965 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
10966 gpui::Point::new(px(0.), px(0.)),
10967 gpui::Point::new(px(10.), px(80.)),
10968 ))),
10969 ..Default::default()
10970 },
10971 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
10972 )
10973 .unwrap()
10974 });
10975
10976 let is_still_following = Rc::new(RefCell::new(true));
10977 let follower_edit_event_count = Rc::new(RefCell::new(0));
10978 let pending_update = Rc::new(RefCell::new(None));
10979 let leader_entity = leader.root(cx).unwrap();
10980 let follower_entity = follower.root(cx).unwrap();
10981 _ = follower.update(cx, {
10982 let update = pending_update.clone();
10983 let is_still_following = is_still_following.clone();
10984 let follower_edit_event_count = follower_edit_event_count.clone();
10985 |_, window, cx| {
10986 cx.subscribe_in(
10987 &leader_entity,
10988 window,
10989 move |_, leader, event, window, cx| {
10990 leader.read(cx).add_event_to_update_proto(
10991 event,
10992 &mut update.borrow_mut(),
10993 window,
10994 cx,
10995 );
10996 },
10997 )
10998 .detach();
10999
11000 cx.subscribe_in(
11001 &follower_entity,
11002 window,
11003 move |_, _, event: &EditorEvent, _window, _cx| {
11004 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
11005 *is_still_following.borrow_mut() = false;
11006 }
11007
11008 if let EditorEvent::BufferEdited = event {
11009 *follower_edit_event_count.borrow_mut() += 1;
11010 }
11011 },
11012 )
11013 .detach();
11014 }
11015 });
11016
11017 // Update the selections only
11018 _ = leader.update(cx, |leader, window, cx| {
11019 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11020 });
11021 follower
11022 .update(cx, |follower, window, cx| {
11023 follower.apply_update_proto(
11024 &project,
11025 pending_update.borrow_mut().take().unwrap(),
11026 window,
11027 cx,
11028 )
11029 })
11030 .unwrap()
11031 .await
11032 .unwrap();
11033 _ = follower.update(cx, |follower, _, cx| {
11034 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
11035 });
11036 assert!(*is_still_following.borrow());
11037 assert_eq!(*follower_edit_event_count.borrow(), 0);
11038
11039 // Update the scroll position only
11040 _ = leader.update(cx, |leader, window, cx| {
11041 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11042 });
11043 follower
11044 .update(cx, |follower, window, cx| {
11045 follower.apply_update_proto(
11046 &project,
11047 pending_update.borrow_mut().take().unwrap(),
11048 window,
11049 cx,
11050 )
11051 })
11052 .unwrap()
11053 .await
11054 .unwrap();
11055 assert_eq!(
11056 follower
11057 .update(cx, |follower, _, cx| follower.scroll_position(cx))
11058 .unwrap(),
11059 gpui::Point::new(1.5, 3.5)
11060 );
11061 assert!(*is_still_following.borrow());
11062 assert_eq!(*follower_edit_event_count.borrow(), 0);
11063
11064 // Update the selections and scroll position. The follower's scroll position is updated
11065 // via autoscroll, not via the leader's exact scroll position.
11066 _ = leader.update(cx, |leader, window, cx| {
11067 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
11068 leader.request_autoscroll(Autoscroll::newest(), cx);
11069 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11070 });
11071 follower
11072 .update(cx, |follower, window, cx| {
11073 follower.apply_update_proto(
11074 &project,
11075 pending_update.borrow_mut().take().unwrap(),
11076 window,
11077 cx,
11078 )
11079 })
11080 .unwrap()
11081 .await
11082 .unwrap();
11083 _ = follower.update(cx, |follower, _, cx| {
11084 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
11085 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
11086 });
11087 assert!(*is_still_following.borrow());
11088
11089 // Creating a pending selection that precedes another selection
11090 _ = leader.update(cx, |leader, window, cx| {
11091 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11092 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
11093 });
11094 follower
11095 .update(cx, |follower, window, cx| {
11096 follower.apply_update_proto(
11097 &project,
11098 pending_update.borrow_mut().take().unwrap(),
11099 window,
11100 cx,
11101 )
11102 })
11103 .unwrap()
11104 .await
11105 .unwrap();
11106 _ = follower.update(cx, |follower, _, cx| {
11107 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
11108 });
11109 assert!(*is_still_following.borrow());
11110
11111 // Extend the pending selection so that it surrounds another selection
11112 _ = leader.update(cx, |leader, window, cx| {
11113 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
11114 });
11115 follower
11116 .update(cx, |follower, window, cx| {
11117 follower.apply_update_proto(
11118 &project,
11119 pending_update.borrow_mut().take().unwrap(),
11120 window,
11121 cx,
11122 )
11123 })
11124 .unwrap()
11125 .await
11126 .unwrap();
11127 _ = follower.update(cx, |follower, _, cx| {
11128 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
11129 });
11130
11131 // Scrolling locally breaks the follow
11132 _ = follower.update(cx, |follower, window, cx| {
11133 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
11134 follower.set_scroll_anchor(
11135 ScrollAnchor {
11136 anchor: top_anchor,
11137 offset: gpui::Point::new(0.0, 0.5),
11138 },
11139 window,
11140 cx,
11141 );
11142 });
11143 assert!(!(*is_still_following.borrow()));
11144}
11145
11146#[gpui::test]
11147async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
11148 init_test(cx, |_| {});
11149
11150 let fs = FakeFs::new(cx.executor());
11151 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11152 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11153 let pane = workspace
11154 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11155 .unwrap();
11156
11157 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11158
11159 let leader = pane.update_in(cx, |_, window, cx| {
11160 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
11161 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
11162 });
11163
11164 // Start following the editor when it has no excerpts.
11165 let mut state_message =
11166 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
11167 let workspace_entity = workspace.root(cx).unwrap();
11168 let follower_1 = cx
11169 .update_window(*workspace.deref(), |_, window, cx| {
11170 Editor::from_state_proto(
11171 workspace_entity,
11172 ViewId {
11173 creator: Default::default(),
11174 id: 0,
11175 },
11176 &mut state_message,
11177 window,
11178 cx,
11179 )
11180 })
11181 .unwrap()
11182 .unwrap()
11183 .await
11184 .unwrap();
11185
11186 let update_message = Rc::new(RefCell::new(None));
11187 follower_1.update_in(cx, {
11188 let update = update_message.clone();
11189 |_, window, cx| {
11190 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
11191 leader.read(cx).add_event_to_update_proto(
11192 event,
11193 &mut update.borrow_mut(),
11194 window,
11195 cx,
11196 );
11197 })
11198 .detach();
11199 }
11200 });
11201
11202 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
11203 (
11204 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
11205 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
11206 )
11207 });
11208
11209 // Insert some excerpts.
11210 leader.update(cx, |leader, cx| {
11211 leader.buffer.update(cx, |multibuffer, cx| {
11212 let excerpt_ids = multibuffer.push_excerpts(
11213 buffer_1.clone(),
11214 [
11215 ExcerptRange {
11216 context: 1..6,
11217 primary: None,
11218 },
11219 ExcerptRange {
11220 context: 12..15,
11221 primary: None,
11222 },
11223 ExcerptRange {
11224 context: 0..3,
11225 primary: None,
11226 },
11227 ],
11228 cx,
11229 );
11230 multibuffer.insert_excerpts_after(
11231 excerpt_ids[0],
11232 buffer_2.clone(),
11233 [
11234 ExcerptRange {
11235 context: 8..12,
11236 primary: None,
11237 },
11238 ExcerptRange {
11239 context: 0..6,
11240 primary: None,
11241 },
11242 ],
11243 cx,
11244 );
11245 });
11246 });
11247
11248 // Apply the update of adding the excerpts.
11249 follower_1
11250 .update_in(cx, |follower, window, cx| {
11251 follower.apply_update_proto(
11252 &project,
11253 update_message.borrow().clone().unwrap(),
11254 window,
11255 cx,
11256 )
11257 })
11258 .await
11259 .unwrap();
11260 assert_eq!(
11261 follower_1.update(cx, |editor, cx| editor.text(cx)),
11262 leader.update(cx, |editor, cx| editor.text(cx))
11263 );
11264 update_message.borrow_mut().take();
11265
11266 // Start following separately after it already has excerpts.
11267 let mut state_message =
11268 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
11269 let workspace_entity = workspace.root(cx).unwrap();
11270 let follower_2 = cx
11271 .update_window(*workspace.deref(), |_, window, cx| {
11272 Editor::from_state_proto(
11273 workspace_entity,
11274 ViewId {
11275 creator: Default::default(),
11276 id: 0,
11277 },
11278 &mut state_message,
11279 window,
11280 cx,
11281 )
11282 })
11283 .unwrap()
11284 .unwrap()
11285 .await
11286 .unwrap();
11287 assert_eq!(
11288 follower_2.update(cx, |editor, cx| editor.text(cx)),
11289 leader.update(cx, |editor, cx| editor.text(cx))
11290 );
11291
11292 // Remove some excerpts.
11293 leader.update(cx, |leader, cx| {
11294 leader.buffer.update(cx, |multibuffer, cx| {
11295 let excerpt_ids = multibuffer.excerpt_ids();
11296 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
11297 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
11298 });
11299 });
11300
11301 // Apply the update of removing the excerpts.
11302 follower_1
11303 .update_in(cx, |follower, window, cx| {
11304 follower.apply_update_proto(
11305 &project,
11306 update_message.borrow().clone().unwrap(),
11307 window,
11308 cx,
11309 )
11310 })
11311 .await
11312 .unwrap();
11313 follower_2
11314 .update_in(cx, |follower, window, cx| {
11315 follower.apply_update_proto(
11316 &project,
11317 update_message.borrow().clone().unwrap(),
11318 window,
11319 cx,
11320 )
11321 })
11322 .await
11323 .unwrap();
11324 update_message.borrow_mut().take();
11325 assert_eq!(
11326 follower_1.update(cx, |editor, cx| editor.text(cx)),
11327 leader.update(cx, |editor, cx| editor.text(cx))
11328 );
11329}
11330
11331#[gpui::test]
11332async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
11333 init_test(cx, |_| {});
11334
11335 let mut cx = EditorTestContext::new(cx).await;
11336 let lsp_store =
11337 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11338
11339 cx.set_state(indoc! {"
11340 ˇfn func(abc def: i32) -> u32 {
11341 }
11342 "});
11343
11344 cx.update(|_, cx| {
11345 lsp_store.update(cx, |lsp_store, cx| {
11346 lsp_store
11347 .update_diagnostics(
11348 LanguageServerId(0),
11349 lsp::PublishDiagnosticsParams {
11350 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11351 version: None,
11352 diagnostics: vec![
11353 lsp::Diagnostic {
11354 range: lsp::Range::new(
11355 lsp::Position::new(0, 11),
11356 lsp::Position::new(0, 12),
11357 ),
11358 severity: Some(lsp::DiagnosticSeverity::ERROR),
11359 ..Default::default()
11360 },
11361 lsp::Diagnostic {
11362 range: lsp::Range::new(
11363 lsp::Position::new(0, 12),
11364 lsp::Position::new(0, 15),
11365 ),
11366 severity: Some(lsp::DiagnosticSeverity::ERROR),
11367 ..Default::default()
11368 },
11369 lsp::Diagnostic {
11370 range: lsp::Range::new(
11371 lsp::Position::new(0, 25),
11372 lsp::Position::new(0, 28),
11373 ),
11374 severity: Some(lsp::DiagnosticSeverity::ERROR),
11375 ..Default::default()
11376 },
11377 ],
11378 },
11379 &[],
11380 cx,
11381 )
11382 .unwrap()
11383 });
11384 });
11385
11386 executor.run_until_parked();
11387
11388 cx.update_editor(|editor, window, cx| {
11389 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11390 });
11391
11392 cx.assert_editor_state(indoc! {"
11393 fn func(abc def: i32) -> ˇu32 {
11394 }
11395 "});
11396
11397 cx.update_editor(|editor, window, cx| {
11398 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11399 });
11400
11401 cx.assert_editor_state(indoc! {"
11402 fn func(abc ˇdef: i32) -> u32 {
11403 }
11404 "});
11405
11406 cx.update_editor(|editor, window, cx| {
11407 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11408 });
11409
11410 cx.assert_editor_state(indoc! {"
11411 fn func(abcˇ def: i32) -> u32 {
11412 }
11413 "});
11414
11415 cx.update_editor(|editor, window, cx| {
11416 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11417 });
11418
11419 cx.assert_editor_state(indoc! {"
11420 fn func(abc def: i32) -> ˇu32 {
11421 }
11422 "});
11423}
11424
11425#[gpui::test]
11426async fn cycle_through_same_place_diagnostics(
11427 executor: BackgroundExecutor,
11428 cx: &mut TestAppContext,
11429) {
11430 init_test(cx, |_| {});
11431
11432 let mut cx = EditorTestContext::new(cx).await;
11433 let lsp_store =
11434 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11435
11436 cx.set_state(indoc! {"
11437 ˇfn func(abc def: i32) -> u32 {
11438 }
11439 "});
11440
11441 cx.update(|_, cx| {
11442 lsp_store.update(cx, |lsp_store, cx| {
11443 lsp_store
11444 .update_diagnostics(
11445 LanguageServerId(0),
11446 lsp::PublishDiagnosticsParams {
11447 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11448 version: None,
11449 diagnostics: vec![
11450 lsp::Diagnostic {
11451 range: lsp::Range::new(
11452 lsp::Position::new(0, 11),
11453 lsp::Position::new(0, 12),
11454 ),
11455 severity: Some(lsp::DiagnosticSeverity::ERROR),
11456 ..Default::default()
11457 },
11458 lsp::Diagnostic {
11459 range: lsp::Range::new(
11460 lsp::Position::new(0, 12),
11461 lsp::Position::new(0, 15),
11462 ),
11463 severity: Some(lsp::DiagnosticSeverity::ERROR),
11464 ..Default::default()
11465 },
11466 lsp::Diagnostic {
11467 range: lsp::Range::new(
11468 lsp::Position::new(0, 12),
11469 lsp::Position::new(0, 15),
11470 ),
11471 severity: Some(lsp::DiagnosticSeverity::ERROR),
11472 ..Default::default()
11473 },
11474 lsp::Diagnostic {
11475 range: lsp::Range::new(
11476 lsp::Position::new(0, 25),
11477 lsp::Position::new(0, 28),
11478 ),
11479 severity: Some(lsp::DiagnosticSeverity::ERROR),
11480 ..Default::default()
11481 },
11482 ],
11483 },
11484 &[],
11485 cx,
11486 )
11487 .unwrap()
11488 });
11489 });
11490 executor.run_until_parked();
11491
11492 //// Backward
11493
11494 // Fourth diagnostic
11495 cx.update_editor(|editor, window, cx| {
11496 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11497 });
11498 cx.assert_editor_state(indoc! {"
11499 fn func(abc def: i32) -> ˇu32 {
11500 }
11501 "});
11502
11503 // Third diagnostic
11504 cx.update_editor(|editor, window, cx| {
11505 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11506 });
11507 cx.assert_editor_state(indoc! {"
11508 fn func(abc ˇdef: i32) -> u32 {
11509 }
11510 "});
11511
11512 // Second diagnostic, same place
11513 cx.update_editor(|editor, window, cx| {
11514 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11515 });
11516 cx.assert_editor_state(indoc! {"
11517 fn func(abc ˇdef: i32) -> u32 {
11518 }
11519 "});
11520
11521 // First diagnostic
11522 cx.update_editor(|editor, window, cx| {
11523 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11524 });
11525 cx.assert_editor_state(indoc! {"
11526 fn func(abcˇ def: i32) -> u32 {
11527 }
11528 "});
11529
11530 // Wrapped over, fourth diagnostic
11531 cx.update_editor(|editor, window, cx| {
11532 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11533 });
11534 cx.assert_editor_state(indoc! {"
11535 fn func(abc def: i32) -> ˇu32 {
11536 }
11537 "});
11538
11539 cx.update_editor(|editor, window, cx| {
11540 editor.move_to_beginning(&MoveToBeginning, window, cx);
11541 });
11542 cx.assert_editor_state(indoc! {"
11543 ˇfn func(abc def: i32) -> u32 {
11544 }
11545 "});
11546
11547 //// Forward
11548
11549 // First diagnostic
11550 cx.update_editor(|editor, window, cx| {
11551 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11552 });
11553 cx.assert_editor_state(indoc! {"
11554 fn func(abcˇ def: i32) -> u32 {
11555 }
11556 "});
11557
11558 // Second diagnostic
11559 cx.update_editor(|editor, window, cx| {
11560 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11561 });
11562 cx.assert_editor_state(indoc! {"
11563 fn func(abc ˇdef: i32) -> u32 {
11564 }
11565 "});
11566
11567 // Third diagnostic, same place
11568 cx.update_editor(|editor, window, cx| {
11569 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11570 });
11571 cx.assert_editor_state(indoc! {"
11572 fn func(abc ˇdef: i32) -> u32 {
11573 }
11574 "});
11575
11576 // Fourth diagnostic
11577 cx.update_editor(|editor, window, cx| {
11578 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11579 });
11580 cx.assert_editor_state(indoc! {"
11581 fn func(abc def: i32) -> ˇu32 {
11582 }
11583 "});
11584
11585 // Wrapped around, first diagnostic
11586 cx.update_editor(|editor, window, cx| {
11587 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11588 });
11589 cx.assert_editor_state(indoc! {"
11590 fn func(abcˇ def: i32) -> u32 {
11591 }
11592 "});
11593}
11594
11595#[gpui::test]
11596async fn active_diagnostics_dismiss_after_invalidation(
11597 executor: BackgroundExecutor,
11598 cx: &mut TestAppContext,
11599) {
11600 init_test(cx, |_| {});
11601
11602 let mut cx = EditorTestContext::new(cx).await;
11603 let lsp_store =
11604 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11605
11606 cx.set_state(indoc! {"
11607 ˇfn func(abc def: i32) -> u32 {
11608 }
11609 "});
11610
11611 let message = "Something's wrong!";
11612 cx.update(|_, cx| {
11613 lsp_store.update(cx, |lsp_store, cx| {
11614 lsp_store
11615 .update_diagnostics(
11616 LanguageServerId(0),
11617 lsp::PublishDiagnosticsParams {
11618 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11619 version: None,
11620 diagnostics: vec![lsp::Diagnostic {
11621 range: lsp::Range::new(
11622 lsp::Position::new(0, 11),
11623 lsp::Position::new(0, 12),
11624 ),
11625 severity: Some(lsp::DiagnosticSeverity::ERROR),
11626 message: message.to_string(),
11627 ..Default::default()
11628 }],
11629 },
11630 &[],
11631 cx,
11632 )
11633 .unwrap()
11634 });
11635 });
11636 executor.run_until_parked();
11637
11638 cx.update_editor(|editor, window, cx| {
11639 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11640 assert_eq!(
11641 editor
11642 .active_diagnostics
11643 .as_ref()
11644 .map(|diagnostics_group| diagnostics_group.primary_message.as_str()),
11645 Some(message),
11646 "Should have a diagnostics group activated"
11647 );
11648 });
11649 cx.assert_editor_state(indoc! {"
11650 fn func(abcˇ def: i32) -> u32 {
11651 }
11652 "});
11653
11654 cx.update(|_, cx| {
11655 lsp_store.update(cx, |lsp_store, cx| {
11656 lsp_store
11657 .update_diagnostics(
11658 LanguageServerId(0),
11659 lsp::PublishDiagnosticsParams {
11660 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11661 version: None,
11662 diagnostics: Vec::new(),
11663 },
11664 &[],
11665 cx,
11666 )
11667 .unwrap()
11668 });
11669 });
11670 executor.run_until_parked();
11671 cx.update_editor(|editor, _, _| {
11672 assert_eq!(
11673 editor.active_diagnostics, None,
11674 "After no diagnostics set to the editor, no diagnostics should be active"
11675 );
11676 });
11677 cx.assert_editor_state(indoc! {"
11678 fn func(abcˇ def: i32) -> u32 {
11679 }
11680 "});
11681
11682 cx.update_editor(|editor, window, cx| {
11683 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11684 assert_eq!(
11685 editor.active_diagnostics, None,
11686 "Should be no diagnostics to go to and activate"
11687 );
11688 });
11689 cx.assert_editor_state(indoc! {"
11690 fn func(abcˇ def: i32) -> u32 {
11691 }
11692 "});
11693}
11694
11695#[gpui::test]
11696async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
11697 init_test(cx, |_| {});
11698
11699 let mut cx = EditorTestContext::new(cx).await;
11700
11701 cx.set_state(indoc! {"
11702 fn func(abˇc def: i32) -> u32 {
11703 }
11704 "});
11705 let lsp_store =
11706 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11707
11708 cx.update(|_, cx| {
11709 lsp_store.update(cx, |lsp_store, cx| {
11710 lsp_store.update_diagnostics(
11711 LanguageServerId(0),
11712 lsp::PublishDiagnosticsParams {
11713 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11714 version: None,
11715 diagnostics: vec![lsp::Diagnostic {
11716 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
11717 severity: Some(lsp::DiagnosticSeverity::ERROR),
11718 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
11719 ..Default::default()
11720 }],
11721 },
11722 &[],
11723 cx,
11724 )
11725 })
11726 }).unwrap();
11727 cx.run_until_parked();
11728 cx.update_editor(|editor, window, cx| {
11729 hover_popover::hover(editor, &Default::default(), window, cx)
11730 });
11731 cx.run_until_parked();
11732 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
11733}
11734
11735#[gpui::test]
11736async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
11737 init_test(cx, |_| {});
11738
11739 let mut cx = EditorTestContext::new(cx).await;
11740
11741 let diff_base = r#"
11742 use some::mod;
11743
11744 const A: u32 = 42;
11745
11746 fn main() {
11747 println!("hello");
11748
11749 println!("world");
11750 }
11751 "#
11752 .unindent();
11753
11754 // Edits are modified, removed, modified, added
11755 cx.set_state(
11756 &r#"
11757 use some::modified;
11758
11759 ˇ
11760 fn main() {
11761 println!("hello there");
11762
11763 println!("around the");
11764 println!("world");
11765 }
11766 "#
11767 .unindent(),
11768 );
11769
11770 cx.set_head_text(&diff_base);
11771 executor.run_until_parked();
11772
11773 cx.update_editor(|editor, window, cx| {
11774 //Wrap around the bottom of the buffer
11775 for _ in 0..3 {
11776 editor.go_to_next_hunk(&GoToHunk, window, cx);
11777 }
11778 });
11779
11780 cx.assert_editor_state(
11781 &r#"
11782 ˇuse some::modified;
11783
11784
11785 fn main() {
11786 println!("hello there");
11787
11788 println!("around the");
11789 println!("world");
11790 }
11791 "#
11792 .unindent(),
11793 );
11794
11795 cx.update_editor(|editor, window, cx| {
11796 //Wrap around the top of the buffer
11797 for _ in 0..2 {
11798 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
11799 }
11800 });
11801
11802 cx.assert_editor_state(
11803 &r#"
11804 use some::modified;
11805
11806
11807 fn main() {
11808 ˇ println!("hello there");
11809
11810 println!("around the");
11811 println!("world");
11812 }
11813 "#
11814 .unindent(),
11815 );
11816
11817 cx.update_editor(|editor, window, cx| {
11818 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
11819 });
11820
11821 cx.assert_editor_state(
11822 &r#"
11823 use some::modified;
11824
11825 ˇ
11826 fn main() {
11827 println!("hello there");
11828
11829 println!("around the");
11830 println!("world");
11831 }
11832 "#
11833 .unindent(),
11834 );
11835
11836 cx.update_editor(|editor, window, cx| {
11837 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
11838 });
11839
11840 cx.assert_editor_state(
11841 &r#"
11842 ˇuse some::modified;
11843
11844
11845 fn main() {
11846 println!("hello there");
11847
11848 println!("around the");
11849 println!("world");
11850 }
11851 "#
11852 .unindent(),
11853 );
11854
11855 cx.update_editor(|editor, window, cx| {
11856 for _ in 0..2 {
11857 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
11858 }
11859 });
11860
11861 cx.assert_editor_state(
11862 &r#"
11863 use some::modified;
11864
11865
11866 fn main() {
11867 ˇ println!("hello there");
11868
11869 println!("around the");
11870 println!("world");
11871 }
11872 "#
11873 .unindent(),
11874 );
11875
11876 cx.update_editor(|editor, window, cx| {
11877 editor.fold(&Fold, window, cx);
11878 });
11879
11880 cx.update_editor(|editor, window, cx| {
11881 editor.go_to_next_hunk(&GoToHunk, window, cx);
11882 });
11883
11884 cx.assert_editor_state(
11885 &r#"
11886 ˇuse some::modified;
11887
11888
11889 fn main() {
11890 println!("hello there");
11891
11892 println!("around the");
11893 println!("world");
11894 }
11895 "#
11896 .unindent(),
11897 );
11898}
11899
11900#[test]
11901fn test_split_words() {
11902 fn split(text: &str) -> Vec<&str> {
11903 split_words(text).collect()
11904 }
11905
11906 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
11907 assert_eq!(split("hello_world"), &["hello_", "world"]);
11908 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
11909 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
11910 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
11911 assert_eq!(split("helloworld"), &["helloworld"]);
11912
11913 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
11914}
11915
11916#[gpui::test]
11917async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
11918 init_test(cx, |_| {});
11919
11920 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
11921 let mut assert = |before, after| {
11922 let _state_context = cx.set_state(before);
11923 cx.run_until_parked();
11924 cx.update_editor(|editor, window, cx| {
11925 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
11926 });
11927 cx.assert_editor_state(after);
11928 };
11929
11930 // Outside bracket jumps to outside of matching bracket
11931 assert("console.logˇ(var);", "console.log(var)ˇ;");
11932 assert("console.log(var)ˇ;", "console.logˇ(var);");
11933
11934 // Inside bracket jumps to inside of matching bracket
11935 assert("console.log(ˇvar);", "console.log(varˇ);");
11936 assert("console.log(varˇ);", "console.log(ˇvar);");
11937
11938 // When outside a bracket and inside, favor jumping to the inside bracket
11939 assert(
11940 "console.log('foo', [1, 2, 3]ˇ);",
11941 "console.log(ˇ'foo', [1, 2, 3]);",
11942 );
11943 assert(
11944 "console.log(ˇ'foo', [1, 2, 3]);",
11945 "console.log('foo', [1, 2, 3]ˇ);",
11946 );
11947
11948 // Bias forward if two options are equally likely
11949 assert(
11950 "let result = curried_fun()ˇ();",
11951 "let result = curried_fun()()ˇ;",
11952 );
11953
11954 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
11955 assert(
11956 indoc! {"
11957 function test() {
11958 console.log('test')ˇ
11959 }"},
11960 indoc! {"
11961 function test() {
11962 console.logˇ('test')
11963 }"},
11964 );
11965}
11966
11967#[gpui::test]
11968async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
11969 init_test(cx, |_| {});
11970
11971 let fs = FakeFs::new(cx.executor());
11972 fs.insert_tree(
11973 path!("/a"),
11974 json!({
11975 "main.rs": "fn main() { let a = 5; }",
11976 "other.rs": "// Test file",
11977 }),
11978 )
11979 .await;
11980 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11981
11982 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11983 language_registry.add(Arc::new(Language::new(
11984 LanguageConfig {
11985 name: "Rust".into(),
11986 matcher: LanguageMatcher {
11987 path_suffixes: vec!["rs".to_string()],
11988 ..Default::default()
11989 },
11990 brackets: BracketPairConfig {
11991 pairs: vec![BracketPair {
11992 start: "{".to_string(),
11993 end: "}".to_string(),
11994 close: true,
11995 surround: true,
11996 newline: true,
11997 }],
11998 disabled_scopes_by_bracket_ix: Vec::new(),
11999 },
12000 ..Default::default()
12001 },
12002 Some(tree_sitter_rust::LANGUAGE.into()),
12003 )));
12004 let mut fake_servers = language_registry.register_fake_lsp(
12005 "Rust",
12006 FakeLspAdapter {
12007 capabilities: lsp::ServerCapabilities {
12008 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
12009 first_trigger_character: "{".to_string(),
12010 more_trigger_character: None,
12011 }),
12012 ..Default::default()
12013 },
12014 ..Default::default()
12015 },
12016 );
12017
12018 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12019
12020 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12021
12022 let worktree_id = workspace
12023 .update(cx, |workspace, _, cx| {
12024 workspace.project().update(cx, |project, cx| {
12025 project.worktrees(cx).next().unwrap().read(cx).id()
12026 })
12027 })
12028 .unwrap();
12029
12030 let buffer = project
12031 .update(cx, |project, cx| {
12032 project.open_local_buffer(path!("/a/main.rs"), cx)
12033 })
12034 .await
12035 .unwrap();
12036 let editor_handle = workspace
12037 .update(cx, |workspace, window, cx| {
12038 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
12039 })
12040 .unwrap()
12041 .await
12042 .unwrap()
12043 .downcast::<Editor>()
12044 .unwrap();
12045
12046 cx.executor().start_waiting();
12047 let fake_server = fake_servers.next().await.unwrap();
12048
12049 fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
12050 assert_eq!(
12051 params.text_document_position.text_document.uri,
12052 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
12053 );
12054 assert_eq!(
12055 params.text_document_position.position,
12056 lsp::Position::new(0, 21),
12057 );
12058
12059 Ok(Some(vec![lsp::TextEdit {
12060 new_text: "]".to_string(),
12061 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12062 }]))
12063 });
12064
12065 editor_handle.update_in(cx, |editor, window, cx| {
12066 window.focus(&editor.focus_handle(cx));
12067 editor.change_selections(None, window, cx, |s| {
12068 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
12069 });
12070 editor.handle_input("{", window, cx);
12071 });
12072
12073 cx.executor().run_until_parked();
12074
12075 buffer.update(cx, |buffer, _| {
12076 assert_eq!(
12077 buffer.text(),
12078 "fn main() { let a = {5}; }",
12079 "No extra braces from on type formatting should appear in the buffer"
12080 )
12081 });
12082}
12083
12084#[gpui::test]
12085async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
12086 init_test(cx, |_| {});
12087
12088 let fs = FakeFs::new(cx.executor());
12089 fs.insert_tree(
12090 path!("/a"),
12091 json!({
12092 "main.rs": "fn main() { let a = 5; }",
12093 "other.rs": "// Test file",
12094 }),
12095 )
12096 .await;
12097
12098 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12099
12100 let server_restarts = Arc::new(AtomicUsize::new(0));
12101 let closure_restarts = Arc::clone(&server_restarts);
12102 let language_server_name = "test language server";
12103 let language_name: LanguageName = "Rust".into();
12104
12105 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12106 language_registry.add(Arc::new(Language::new(
12107 LanguageConfig {
12108 name: language_name.clone(),
12109 matcher: LanguageMatcher {
12110 path_suffixes: vec!["rs".to_string()],
12111 ..Default::default()
12112 },
12113 ..Default::default()
12114 },
12115 Some(tree_sitter_rust::LANGUAGE.into()),
12116 )));
12117 let mut fake_servers = language_registry.register_fake_lsp(
12118 "Rust",
12119 FakeLspAdapter {
12120 name: language_server_name,
12121 initialization_options: Some(json!({
12122 "testOptionValue": true
12123 })),
12124 initializer: Some(Box::new(move |fake_server| {
12125 let task_restarts = Arc::clone(&closure_restarts);
12126 fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
12127 task_restarts.fetch_add(1, atomic::Ordering::Release);
12128 futures::future::ready(Ok(()))
12129 });
12130 })),
12131 ..Default::default()
12132 },
12133 );
12134
12135 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12136 let _buffer = project
12137 .update(cx, |project, cx| {
12138 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
12139 })
12140 .await
12141 .unwrap();
12142 let _fake_server = fake_servers.next().await.unwrap();
12143 update_test_language_settings(cx, |language_settings| {
12144 language_settings.languages.insert(
12145 language_name.clone(),
12146 LanguageSettingsContent {
12147 tab_size: NonZeroU32::new(8),
12148 ..Default::default()
12149 },
12150 );
12151 });
12152 cx.executor().run_until_parked();
12153 assert_eq!(
12154 server_restarts.load(atomic::Ordering::Acquire),
12155 0,
12156 "Should not restart LSP server on an unrelated change"
12157 );
12158
12159 update_test_project_settings(cx, |project_settings| {
12160 project_settings.lsp.insert(
12161 "Some other server name".into(),
12162 LspSettings {
12163 binary: None,
12164 settings: None,
12165 initialization_options: Some(json!({
12166 "some other init value": false
12167 })),
12168 },
12169 );
12170 });
12171 cx.executor().run_until_parked();
12172 assert_eq!(
12173 server_restarts.load(atomic::Ordering::Acquire),
12174 0,
12175 "Should not restart LSP server on an unrelated LSP settings change"
12176 );
12177
12178 update_test_project_settings(cx, |project_settings| {
12179 project_settings.lsp.insert(
12180 language_server_name.into(),
12181 LspSettings {
12182 binary: None,
12183 settings: None,
12184 initialization_options: Some(json!({
12185 "anotherInitValue": false
12186 })),
12187 },
12188 );
12189 });
12190 cx.executor().run_until_parked();
12191 assert_eq!(
12192 server_restarts.load(atomic::Ordering::Acquire),
12193 1,
12194 "Should restart LSP server on a related LSP settings change"
12195 );
12196
12197 update_test_project_settings(cx, |project_settings| {
12198 project_settings.lsp.insert(
12199 language_server_name.into(),
12200 LspSettings {
12201 binary: None,
12202 settings: None,
12203 initialization_options: Some(json!({
12204 "anotherInitValue": false
12205 })),
12206 },
12207 );
12208 });
12209 cx.executor().run_until_parked();
12210 assert_eq!(
12211 server_restarts.load(atomic::Ordering::Acquire),
12212 1,
12213 "Should not restart LSP server on a related LSP settings change that is the same"
12214 );
12215
12216 update_test_project_settings(cx, |project_settings| {
12217 project_settings.lsp.insert(
12218 language_server_name.into(),
12219 LspSettings {
12220 binary: None,
12221 settings: None,
12222 initialization_options: None,
12223 },
12224 );
12225 });
12226 cx.executor().run_until_parked();
12227 assert_eq!(
12228 server_restarts.load(atomic::Ordering::Acquire),
12229 2,
12230 "Should restart LSP server on another related LSP settings change"
12231 );
12232}
12233
12234#[gpui::test]
12235async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
12236 init_test(cx, |_| {});
12237
12238 let mut cx = EditorLspTestContext::new_rust(
12239 lsp::ServerCapabilities {
12240 completion_provider: Some(lsp::CompletionOptions {
12241 trigger_characters: Some(vec![".".to_string()]),
12242 resolve_provider: Some(true),
12243 ..Default::default()
12244 }),
12245 ..Default::default()
12246 },
12247 cx,
12248 )
12249 .await;
12250
12251 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
12252 cx.simulate_keystroke(".");
12253 let completion_item = lsp::CompletionItem {
12254 label: "some".into(),
12255 kind: Some(lsp::CompletionItemKind::SNIPPET),
12256 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12257 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12258 kind: lsp::MarkupKind::Markdown,
12259 value: "```rust\nSome(2)\n```".to_string(),
12260 })),
12261 deprecated: Some(false),
12262 sort_text: Some("fffffff2".to_string()),
12263 filter_text: Some("some".to_string()),
12264 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12265 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12266 range: lsp::Range {
12267 start: lsp::Position {
12268 line: 0,
12269 character: 22,
12270 },
12271 end: lsp::Position {
12272 line: 0,
12273 character: 22,
12274 },
12275 },
12276 new_text: "Some(2)".to_string(),
12277 })),
12278 additional_text_edits: Some(vec![lsp::TextEdit {
12279 range: lsp::Range {
12280 start: lsp::Position {
12281 line: 0,
12282 character: 20,
12283 },
12284 end: lsp::Position {
12285 line: 0,
12286 character: 22,
12287 },
12288 },
12289 new_text: "".to_string(),
12290 }]),
12291 ..Default::default()
12292 };
12293
12294 let closure_completion_item = completion_item.clone();
12295 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
12296 let task_completion_item = closure_completion_item.clone();
12297 async move {
12298 Ok(Some(lsp::CompletionResponse::Array(vec![
12299 task_completion_item,
12300 ])))
12301 }
12302 });
12303
12304 request.next().await;
12305
12306 cx.condition(|editor, _| editor.context_menu_visible())
12307 .await;
12308 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12309 editor
12310 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12311 .unwrap()
12312 });
12313 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
12314
12315 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
12316 let task_completion_item = completion_item.clone();
12317 async move { Ok(task_completion_item) }
12318 })
12319 .next()
12320 .await
12321 .unwrap();
12322 apply_additional_edits.await.unwrap();
12323 cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
12324}
12325
12326#[gpui::test]
12327async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
12328 init_test(cx, |_| {});
12329
12330 let mut cx = EditorLspTestContext::new_rust(
12331 lsp::ServerCapabilities {
12332 completion_provider: Some(lsp::CompletionOptions {
12333 trigger_characters: Some(vec![".".to_string()]),
12334 resolve_provider: Some(true),
12335 ..Default::default()
12336 }),
12337 ..Default::default()
12338 },
12339 cx,
12340 )
12341 .await;
12342
12343 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
12344 cx.simulate_keystroke(".");
12345
12346 let item1 = lsp::CompletionItem {
12347 label: "method id()".to_string(),
12348 filter_text: Some("id".to_string()),
12349 detail: None,
12350 documentation: None,
12351 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12352 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12353 new_text: ".id".to_string(),
12354 })),
12355 ..lsp::CompletionItem::default()
12356 };
12357
12358 let item2 = lsp::CompletionItem {
12359 label: "other".to_string(),
12360 filter_text: Some("other".to_string()),
12361 detail: None,
12362 documentation: None,
12363 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12364 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12365 new_text: ".other".to_string(),
12366 })),
12367 ..lsp::CompletionItem::default()
12368 };
12369
12370 let item1 = item1.clone();
12371 cx.handle_request::<lsp::request::Completion, _, _>({
12372 let item1 = item1.clone();
12373 move |_, _, _| {
12374 let item1 = item1.clone();
12375 let item2 = item2.clone();
12376 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
12377 }
12378 })
12379 .next()
12380 .await;
12381
12382 cx.condition(|editor, _| editor.context_menu_visible())
12383 .await;
12384 cx.update_editor(|editor, _, _| {
12385 let context_menu = editor.context_menu.borrow_mut();
12386 let context_menu = context_menu
12387 .as_ref()
12388 .expect("Should have the context menu deployed");
12389 match context_menu {
12390 CodeContextMenu::Completions(completions_menu) => {
12391 let completions = completions_menu.completions.borrow_mut();
12392 assert_eq!(
12393 completions
12394 .iter()
12395 .map(|completion| &completion.label.text)
12396 .collect::<Vec<_>>(),
12397 vec!["method id()", "other"]
12398 )
12399 }
12400 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12401 }
12402 });
12403
12404 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>({
12405 let item1 = item1.clone();
12406 move |_, item_to_resolve, _| {
12407 let item1 = item1.clone();
12408 async move {
12409 if item1 == item_to_resolve {
12410 Ok(lsp::CompletionItem {
12411 label: "method id()".to_string(),
12412 filter_text: Some("id".to_string()),
12413 detail: Some("Now resolved!".to_string()),
12414 documentation: Some(lsp::Documentation::String("Docs".to_string())),
12415 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12416 range: lsp::Range::new(
12417 lsp::Position::new(0, 22),
12418 lsp::Position::new(0, 22),
12419 ),
12420 new_text: ".id".to_string(),
12421 })),
12422 ..lsp::CompletionItem::default()
12423 })
12424 } else {
12425 Ok(item_to_resolve)
12426 }
12427 }
12428 }
12429 })
12430 .next()
12431 .await
12432 .unwrap();
12433 cx.run_until_parked();
12434
12435 cx.update_editor(|editor, window, cx| {
12436 editor.context_menu_next(&Default::default(), window, cx);
12437 });
12438
12439 cx.update_editor(|editor, _, _| {
12440 let context_menu = editor.context_menu.borrow_mut();
12441 let context_menu = context_menu
12442 .as_ref()
12443 .expect("Should have the context menu deployed");
12444 match context_menu {
12445 CodeContextMenu::Completions(completions_menu) => {
12446 let completions = completions_menu.completions.borrow_mut();
12447 assert_eq!(
12448 completions
12449 .iter()
12450 .map(|completion| &completion.label.text)
12451 .collect::<Vec<_>>(),
12452 vec!["method id() Now resolved!", "other"],
12453 "Should update first completion label, but not second as the filter text did not match."
12454 );
12455 }
12456 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12457 }
12458 });
12459}
12460
12461#[gpui::test]
12462async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
12463 init_test(cx, |_| {});
12464
12465 let mut cx = EditorLspTestContext::new_rust(
12466 lsp::ServerCapabilities {
12467 completion_provider: Some(lsp::CompletionOptions {
12468 trigger_characters: Some(vec![".".to_string()]),
12469 resolve_provider: Some(true),
12470 ..Default::default()
12471 }),
12472 ..Default::default()
12473 },
12474 cx,
12475 )
12476 .await;
12477
12478 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
12479 cx.simulate_keystroke(".");
12480
12481 let unresolved_item_1 = lsp::CompletionItem {
12482 label: "id".to_string(),
12483 filter_text: Some("id".to_string()),
12484 detail: None,
12485 documentation: None,
12486 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12487 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12488 new_text: ".id".to_string(),
12489 })),
12490 ..lsp::CompletionItem::default()
12491 };
12492 let resolved_item_1 = lsp::CompletionItem {
12493 additional_text_edits: Some(vec![lsp::TextEdit {
12494 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
12495 new_text: "!!".to_string(),
12496 }]),
12497 ..unresolved_item_1.clone()
12498 };
12499 let unresolved_item_2 = lsp::CompletionItem {
12500 label: "other".to_string(),
12501 filter_text: Some("other".to_string()),
12502 detail: None,
12503 documentation: None,
12504 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12505 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12506 new_text: ".other".to_string(),
12507 })),
12508 ..lsp::CompletionItem::default()
12509 };
12510 let resolved_item_2 = lsp::CompletionItem {
12511 additional_text_edits: Some(vec![lsp::TextEdit {
12512 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
12513 new_text: "??".to_string(),
12514 }]),
12515 ..unresolved_item_2.clone()
12516 };
12517
12518 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
12519 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
12520 cx.lsp
12521 .server
12522 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
12523 let unresolved_item_1 = unresolved_item_1.clone();
12524 let resolved_item_1 = resolved_item_1.clone();
12525 let unresolved_item_2 = unresolved_item_2.clone();
12526 let resolved_item_2 = resolved_item_2.clone();
12527 let resolve_requests_1 = resolve_requests_1.clone();
12528 let resolve_requests_2 = resolve_requests_2.clone();
12529 move |unresolved_request, _| {
12530 let unresolved_item_1 = unresolved_item_1.clone();
12531 let resolved_item_1 = resolved_item_1.clone();
12532 let unresolved_item_2 = unresolved_item_2.clone();
12533 let resolved_item_2 = resolved_item_2.clone();
12534 let resolve_requests_1 = resolve_requests_1.clone();
12535 let resolve_requests_2 = resolve_requests_2.clone();
12536 async move {
12537 if unresolved_request == unresolved_item_1 {
12538 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
12539 Ok(resolved_item_1.clone())
12540 } else if unresolved_request == unresolved_item_2 {
12541 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
12542 Ok(resolved_item_2.clone())
12543 } else {
12544 panic!("Unexpected completion item {unresolved_request:?}")
12545 }
12546 }
12547 }
12548 })
12549 .detach();
12550
12551 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
12552 let unresolved_item_1 = unresolved_item_1.clone();
12553 let unresolved_item_2 = unresolved_item_2.clone();
12554 async move {
12555 Ok(Some(lsp::CompletionResponse::Array(vec![
12556 unresolved_item_1,
12557 unresolved_item_2,
12558 ])))
12559 }
12560 })
12561 .next()
12562 .await;
12563
12564 cx.condition(|editor, _| editor.context_menu_visible())
12565 .await;
12566 cx.update_editor(|editor, _, _| {
12567 let context_menu = editor.context_menu.borrow_mut();
12568 let context_menu = context_menu
12569 .as_ref()
12570 .expect("Should have the context menu deployed");
12571 match context_menu {
12572 CodeContextMenu::Completions(completions_menu) => {
12573 let completions = completions_menu.completions.borrow_mut();
12574 assert_eq!(
12575 completions
12576 .iter()
12577 .map(|completion| &completion.label.text)
12578 .collect::<Vec<_>>(),
12579 vec!["id", "other"]
12580 )
12581 }
12582 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12583 }
12584 });
12585 cx.run_until_parked();
12586
12587 cx.update_editor(|editor, window, cx| {
12588 editor.context_menu_next(&ContextMenuNext, window, cx);
12589 });
12590 cx.run_until_parked();
12591 cx.update_editor(|editor, window, cx| {
12592 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
12593 });
12594 cx.run_until_parked();
12595 cx.update_editor(|editor, window, cx| {
12596 editor.context_menu_next(&ContextMenuNext, window, cx);
12597 });
12598 cx.run_until_parked();
12599 cx.update_editor(|editor, window, cx| {
12600 editor
12601 .compose_completion(&ComposeCompletion::default(), window, cx)
12602 .expect("No task returned")
12603 })
12604 .await
12605 .expect("Completion failed");
12606 cx.run_until_parked();
12607
12608 cx.update_editor(|editor, _, cx| {
12609 assert_eq!(
12610 resolve_requests_1.load(atomic::Ordering::Acquire),
12611 1,
12612 "Should always resolve once despite multiple selections"
12613 );
12614 assert_eq!(
12615 resolve_requests_2.load(atomic::Ordering::Acquire),
12616 1,
12617 "Should always resolve once after multiple selections and applying the completion"
12618 );
12619 assert_eq!(
12620 editor.text(cx),
12621 "fn main() { let a = ??.other; }",
12622 "Should use resolved data when applying the completion"
12623 );
12624 });
12625}
12626
12627#[gpui::test]
12628async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
12629 init_test(cx, |_| {});
12630
12631 let item_0 = lsp::CompletionItem {
12632 label: "abs".into(),
12633 insert_text: Some("abs".into()),
12634 data: Some(json!({ "very": "special"})),
12635 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
12636 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12637 lsp::InsertReplaceEdit {
12638 new_text: "abs".to_string(),
12639 insert: lsp::Range::default(),
12640 replace: lsp::Range::default(),
12641 },
12642 )),
12643 ..lsp::CompletionItem::default()
12644 };
12645 let items = iter::once(item_0.clone())
12646 .chain((11..51).map(|i| lsp::CompletionItem {
12647 label: format!("item_{}", i),
12648 insert_text: Some(format!("item_{}", i)),
12649 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
12650 ..lsp::CompletionItem::default()
12651 }))
12652 .collect::<Vec<_>>();
12653
12654 let default_commit_characters = vec!["?".to_string()];
12655 let default_data = json!({ "default": "data"});
12656 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
12657 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
12658 let default_edit_range = lsp::Range {
12659 start: lsp::Position {
12660 line: 0,
12661 character: 5,
12662 },
12663 end: lsp::Position {
12664 line: 0,
12665 character: 5,
12666 },
12667 };
12668
12669 let mut cx = EditorLspTestContext::new_rust(
12670 lsp::ServerCapabilities {
12671 completion_provider: Some(lsp::CompletionOptions {
12672 trigger_characters: Some(vec![".".to_string()]),
12673 resolve_provider: Some(true),
12674 ..Default::default()
12675 }),
12676 ..Default::default()
12677 },
12678 cx,
12679 )
12680 .await;
12681
12682 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
12683 cx.simulate_keystroke(".");
12684
12685 let completion_data = default_data.clone();
12686 let completion_characters = default_commit_characters.clone();
12687 let completion_items = items.clone();
12688 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
12689 let default_data = completion_data.clone();
12690 let default_commit_characters = completion_characters.clone();
12691 let items = completion_items.clone();
12692 async move {
12693 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
12694 items,
12695 item_defaults: Some(lsp::CompletionListItemDefaults {
12696 data: Some(default_data.clone()),
12697 commit_characters: Some(default_commit_characters.clone()),
12698 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
12699 default_edit_range,
12700 )),
12701 insert_text_format: Some(default_insert_text_format),
12702 insert_text_mode: Some(default_insert_text_mode),
12703 }),
12704 ..lsp::CompletionList::default()
12705 })))
12706 }
12707 })
12708 .next()
12709 .await;
12710
12711 let resolved_items = Arc::new(Mutex::new(Vec::new()));
12712 cx.lsp
12713 .server
12714 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
12715 let closure_resolved_items = resolved_items.clone();
12716 move |item_to_resolve, _| {
12717 let closure_resolved_items = closure_resolved_items.clone();
12718 async move {
12719 closure_resolved_items.lock().push(item_to_resolve.clone());
12720 Ok(item_to_resolve)
12721 }
12722 }
12723 })
12724 .detach();
12725
12726 cx.condition(|editor, _| editor.context_menu_visible())
12727 .await;
12728 cx.run_until_parked();
12729 cx.update_editor(|editor, _, _| {
12730 let menu = editor.context_menu.borrow_mut();
12731 match menu.as_ref().expect("should have the completions menu") {
12732 CodeContextMenu::Completions(completions_menu) => {
12733 assert_eq!(
12734 completions_menu
12735 .entries
12736 .borrow()
12737 .iter()
12738 .map(|mat| mat.string.clone())
12739 .collect::<Vec<String>>(),
12740 items
12741 .iter()
12742 .map(|completion| completion.label.clone())
12743 .collect::<Vec<String>>()
12744 );
12745 }
12746 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
12747 }
12748 });
12749 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
12750 // with 4 from the end.
12751 assert_eq!(
12752 *resolved_items.lock(),
12753 [&items[0..16], &items[items.len() - 4..items.len()]]
12754 .concat()
12755 .iter()
12756 .cloned()
12757 .map(|mut item| {
12758 if item.data.is_none() {
12759 item.data = Some(default_data.clone());
12760 }
12761 item
12762 })
12763 .collect::<Vec<lsp::CompletionItem>>(),
12764 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
12765 );
12766 resolved_items.lock().clear();
12767
12768 cx.update_editor(|editor, window, cx| {
12769 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
12770 });
12771 cx.run_until_parked();
12772 // Completions that have already been resolved are skipped.
12773 assert_eq!(
12774 *resolved_items.lock(),
12775 items[items.len() - 16..items.len() - 4]
12776 .iter()
12777 .cloned()
12778 .map(|mut item| {
12779 if item.data.is_none() {
12780 item.data = Some(default_data.clone());
12781 }
12782 item
12783 })
12784 .collect::<Vec<lsp::CompletionItem>>()
12785 );
12786 resolved_items.lock().clear();
12787}
12788
12789#[gpui::test]
12790async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
12791 init_test(cx, |_| {});
12792
12793 let mut cx = EditorLspTestContext::new(
12794 Language::new(
12795 LanguageConfig {
12796 matcher: LanguageMatcher {
12797 path_suffixes: vec!["jsx".into()],
12798 ..Default::default()
12799 },
12800 overrides: [(
12801 "element".into(),
12802 LanguageConfigOverride {
12803 word_characters: Override::Set(['-'].into_iter().collect()),
12804 ..Default::default()
12805 },
12806 )]
12807 .into_iter()
12808 .collect(),
12809 ..Default::default()
12810 },
12811 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12812 )
12813 .with_override_query("(jsx_self_closing_element) @element")
12814 .unwrap(),
12815 lsp::ServerCapabilities {
12816 completion_provider: Some(lsp::CompletionOptions {
12817 trigger_characters: Some(vec![":".to_string()]),
12818 ..Default::default()
12819 }),
12820 ..Default::default()
12821 },
12822 cx,
12823 )
12824 .await;
12825
12826 cx.lsp
12827 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12828 Ok(Some(lsp::CompletionResponse::Array(vec![
12829 lsp::CompletionItem {
12830 label: "bg-blue".into(),
12831 ..Default::default()
12832 },
12833 lsp::CompletionItem {
12834 label: "bg-red".into(),
12835 ..Default::default()
12836 },
12837 lsp::CompletionItem {
12838 label: "bg-yellow".into(),
12839 ..Default::default()
12840 },
12841 ])))
12842 });
12843
12844 cx.set_state(r#"<p class="bgˇ" />"#);
12845
12846 // Trigger completion when typing a dash, because the dash is an extra
12847 // word character in the 'element' scope, which contains the cursor.
12848 cx.simulate_keystroke("-");
12849 cx.executor().run_until_parked();
12850 cx.update_editor(|editor, _, _| {
12851 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12852 {
12853 assert_eq!(
12854 completion_menu_entries(&menu),
12855 &["bg-red", "bg-blue", "bg-yellow"]
12856 );
12857 } else {
12858 panic!("expected completion menu to be open");
12859 }
12860 });
12861
12862 cx.simulate_keystroke("l");
12863 cx.executor().run_until_parked();
12864 cx.update_editor(|editor, _, _| {
12865 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12866 {
12867 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
12868 } else {
12869 panic!("expected completion menu to be open");
12870 }
12871 });
12872
12873 // When filtering completions, consider the character after the '-' to
12874 // be the start of a subword.
12875 cx.set_state(r#"<p class="yelˇ" />"#);
12876 cx.simulate_keystroke("l");
12877 cx.executor().run_until_parked();
12878 cx.update_editor(|editor, _, _| {
12879 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12880 {
12881 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
12882 } else {
12883 panic!("expected completion menu to be open");
12884 }
12885 });
12886}
12887
12888fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
12889 let entries = menu.entries.borrow();
12890 entries.iter().map(|mat| mat.string.clone()).collect()
12891}
12892
12893#[gpui::test]
12894async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
12895 init_test(cx, |settings| {
12896 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
12897 FormatterList(vec![Formatter::Prettier].into()),
12898 ))
12899 });
12900
12901 let fs = FakeFs::new(cx.executor());
12902 fs.insert_file(path!("/file.ts"), Default::default()).await;
12903
12904 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
12905 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12906
12907 language_registry.add(Arc::new(Language::new(
12908 LanguageConfig {
12909 name: "TypeScript".into(),
12910 matcher: LanguageMatcher {
12911 path_suffixes: vec!["ts".to_string()],
12912 ..Default::default()
12913 },
12914 ..Default::default()
12915 },
12916 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12917 )));
12918 update_test_language_settings(cx, |settings| {
12919 settings.defaults.prettier = Some(PrettierSettings {
12920 allowed: true,
12921 ..PrettierSettings::default()
12922 });
12923 });
12924
12925 let test_plugin = "test_plugin";
12926 let _ = language_registry.register_fake_lsp(
12927 "TypeScript",
12928 FakeLspAdapter {
12929 prettier_plugins: vec![test_plugin],
12930 ..Default::default()
12931 },
12932 );
12933
12934 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
12935 let buffer = project
12936 .update(cx, |project, cx| {
12937 project.open_local_buffer(path!("/file.ts"), cx)
12938 })
12939 .await
12940 .unwrap();
12941
12942 let buffer_text = "one\ntwo\nthree\n";
12943 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12944 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12945 editor.update_in(cx, |editor, window, cx| {
12946 editor.set_text(buffer_text, window, cx)
12947 });
12948
12949 editor
12950 .update_in(cx, |editor, window, cx| {
12951 editor.perform_format(
12952 project.clone(),
12953 FormatTrigger::Manual,
12954 FormatTarget::Buffers,
12955 window,
12956 cx,
12957 )
12958 })
12959 .unwrap()
12960 .await;
12961 assert_eq!(
12962 editor.update(cx, |editor, cx| editor.text(cx)),
12963 buffer_text.to_string() + prettier_format_suffix,
12964 "Test prettier formatting was not applied to the original buffer text",
12965 );
12966
12967 update_test_language_settings(cx, |settings| {
12968 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
12969 });
12970 let format = editor.update_in(cx, |editor, window, cx| {
12971 editor.perform_format(
12972 project.clone(),
12973 FormatTrigger::Manual,
12974 FormatTarget::Buffers,
12975 window,
12976 cx,
12977 )
12978 });
12979 format.await.unwrap();
12980 assert_eq!(
12981 editor.update(cx, |editor, cx| editor.text(cx)),
12982 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
12983 "Autoformatting (via test prettier) was not applied to the original buffer text",
12984 );
12985}
12986
12987#[gpui::test]
12988async fn test_addition_reverts(cx: &mut TestAppContext) {
12989 init_test(cx, |_| {});
12990 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12991 let base_text = indoc! {r#"
12992 struct Row;
12993 struct Row1;
12994 struct Row2;
12995
12996 struct Row4;
12997 struct Row5;
12998 struct Row6;
12999
13000 struct Row8;
13001 struct Row9;
13002 struct Row10;"#};
13003
13004 // When addition hunks are not adjacent to carets, no hunk revert is performed
13005 assert_hunk_revert(
13006 indoc! {r#"struct Row;
13007 struct Row1;
13008 struct Row1.1;
13009 struct Row1.2;
13010 struct Row2;ˇ
13011
13012 struct Row4;
13013 struct Row5;
13014 struct Row6;
13015
13016 struct Row8;
13017 ˇstruct Row9;
13018 struct Row9.1;
13019 struct Row9.2;
13020 struct Row9.3;
13021 struct Row10;"#},
13022 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13023 indoc! {r#"struct Row;
13024 struct Row1;
13025 struct Row1.1;
13026 struct Row1.2;
13027 struct Row2;ˇ
13028
13029 struct Row4;
13030 struct Row5;
13031 struct Row6;
13032
13033 struct Row8;
13034 ˇstruct Row9;
13035 struct Row9.1;
13036 struct Row9.2;
13037 struct Row9.3;
13038 struct Row10;"#},
13039 base_text,
13040 &mut cx,
13041 );
13042 // Same for selections
13043 assert_hunk_revert(
13044 indoc! {r#"struct Row;
13045 struct Row1;
13046 struct Row2;
13047 struct Row2.1;
13048 struct Row2.2;
13049 «ˇ
13050 struct Row4;
13051 struct» Row5;
13052 «struct Row6;
13053 ˇ»
13054 struct Row9.1;
13055 struct Row9.2;
13056 struct Row9.3;
13057 struct Row8;
13058 struct Row9;
13059 struct Row10;"#},
13060 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13061 indoc! {r#"struct Row;
13062 struct Row1;
13063 struct Row2;
13064 struct Row2.1;
13065 struct Row2.2;
13066 «ˇ
13067 struct Row4;
13068 struct» Row5;
13069 «struct Row6;
13070 ˇ»
13071 struct Row9.1;
13072 struct Row9.2;
13073 struct Row9.3;
13074 struct Row8;
13075 struct Row9;
13076 struct Row10;"#},
13077 base_text,
13078 &mut cx,
13079 );
13080
13081 // When carets and selections intersect the addition hunks, those are reverted.
13082 // Adjacent carets got merged.
13083 assert_hunk_revert(
13084 indoc! {r#"struct Row;
13085 ˇ// something on the top
13086 struct Row1;
13087 struct Row2;
13088 struct Roˇw3.1;
13089 struct Row2.2;
13090 struct Row2.3;ˇ
13091
13092 struct Row4;
13093 struct ˇRow5.1;
13094 struct Row5.2;
13095 struct «Rowˇ»5.3;
13096 struct Row5;
13097 struct Row6;
13098 ˇ
13099 struct Row9.1;
13100 struct «Rowˇ»9.2;
13101 struct «ˇRow»9.3;
13102 struct Row8;
13103 struct Row9;
13104 «ˇ// something on bottom»
13105 struct Row10;"#},
13106 vec![
13107 DiffHunkStatusKind::Added,
13108 DiffHunkStatusKind::Added,
13109 DiffHunkStatusKind::Added,
13110 DiffHunkStatusKind::Added,
13111 DiffHunkStatusKind::Added,
13112 ],
13113 indoc! {r#"struct Row;
13114 ˇstruct Row1;
13115 struct Row2;
13116 ˇ
13117 struct Row4;
13118 ˇstruct Row5;
13119 struct Row6;
13120 ˇ
13121 ˇstruct Row8;
13122 struct Row9;
13123 ˇstruct Row10;"#},
13124 base_text,
13125 &mut cx,
13126 );
13127}
13128
13129#[gpui::test]
13130async fn test_modification_reverts(cx: &mut TestAppContext) {
13131 init_test(cx, |_| {});
13132 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13133 let base_text = indoc! {r#"
13134 struct Row;
13135 struct Row1;
13136 struct Row2;
13137
13138 struct Row4;
13139 struct Row5;
13140 struct Row6;
13141
13142 struct Row8;
13143 struct Row9;
13144 struct Row10;"#};
13145
13146 // Modification hunks behave the same as the addition ones.
13147 assert_hunk_revert(
13148 indoc! {r#"struct Row;
13149 struct Row1;
13150 struct Row33;
13151 ˇ
13152 struct Row4;
13153 struct Row5;
13154 struct Row6;
13155 ˇ
13156 struct Row99;
13157 struct Row9;
13158 struct Row10;"#},
13159 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13160 indoc! {r#"struct Row;
13161 struct Row1;
13162 struct Row33;
13163 ˇ
13164 struct Row4;
13165 struct Row5;
13166 struct Row6;
13167 ˇ
13168 struct Row99;
13169 struct Row9;
13170 struct Row10;"#},
13171 base_text,
13172 &mut cx,
13173 );
13174 assert_hunk_revert(
13175 indoc! {r#"struct Row;
13176 struct Row1;
13177 struct Row33;
13178 «ˇ
13179 struct Row4;
13180 struct» Row5;
13181 «struct Row6;
13182 ˇ»
13183 struct Row99;
13184 struct Row9;
13185 struct Row10;"#},
13186 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13187 indoc! {r#"struct Row;
13188 struct Row1;
13189 struct Row33;
13190 «ˇ
13191 struct Row4;
13192 struct» Row5;
13193 «struct Row6;
13194 ˇ»
13195 struct Row99;
13196 struct Row9;
13197 struct Row10;"#},
13198 base_text,
13199 &mut cx,
13200 );
13201
13202 assert_hunk_revert(
13203 indoc! {r#"ˇstruct Row1.1;
13204 struct Row1;
13205 «ˇstr»uct Row22;
13206
13207 struct ˇRow44;
13208 struct Row5;
13209 struct «Rˇ»ow66;ˇ
13210
13211 «struˇ»ct Row88;
13212 struct Row9;
13213 struct Row1011;ˇ"#},
13214 vec![
13215 DiffHunkStatusKind::Modified,
13216 DiffHunkStatusKind::Modified,
13217 DiffHunkStatusKind::Modified,
13218 DiffHunkStatusKind::Modified,
13219 DiffHunkStatusKind::Modified,
13220 DiffHunkStatusKind::Modified,
13221 ],
13222 indoc! {r#"struct Row;
13223 ˇstruct Row1;
13224 struct Row2;
13225 ˇ
13226 struct Row4;
13227 ˇstruct Row5;
13228 struct Row6;
13229 ˇ
13230 struct Row8;
13231 ˇstruct Row9;
13232 struct Row10;ˇ"#},
13233 base_text,
13234 &mut cx,
13235 );
13236}
13237
13238#[gpui::test]
13239async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
13240 init_test(cx, |_| {});
13241 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13242 let base_text = indoc! {r#"
13243 one
13244
13245 two
13246 three
13247 "#};
13248
13249 cx.set_head_text(base_text);
13250 cx.set_state("\nˇ\n");
13251 cx.executor().run_until_parked();
13252 cx.update_editor(|editor, _window, cx| {
13253 editor.expand_selected_diff_hunks(cx);
13254 });
13255 cx.executor().run_until_parked();
13256 cx.update_editor(|editor, window, cx| {
13257 editor.backspace(&Default::default(), window, cx);
13258 });
13259 cx.run_until_parked();
13260 cx.assert_state_with_diff(
13261 indoc! {r#"
13262
13263 - two
13264 - threeˇ
13265 +
13266 "#}
13267 .to_string(),
13268 );
13269}
13270
13271#[gpui::test]
13272async fn test_deletion_reverts(cx: &mut TestAppContext) {
13273 init_test(cx, |_| {});
13274 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13275 let base_text = indoc! {r#"struct Row;
13276struct Row1;
13277struct Row2;
13278
13279struct Row4;
13280struct Row5;
13281struct Row6;
13282
13283struct Row8;
13284struct Row9;
13285struct Row10;"#};
13286
13287 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
13288 assert_hunk_revert(
13289 indoc! {r#"struct Row;
13290 struct Row2;
13291
13292 ˇstruct Row4;
13293 struct Row5;
13294 struct Row6;
13295 ˇ
13296 struct Row8;
13297 struct Row10;"#},
13298 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13299 indoc! {r#"struct Row;
13300 struct Row2;
13301
13302 ˇstruct Row4;
13303 struct Row5;
13304 struct Row6;
13305 ˇ
13306 struct Row8;
13307 struct Row10;"#},
13308 base_text,
13309 &mut cx,
13310 );
13311 assert_hunk_revert(
13312 indoc! {r#"struct Row;
13313 struct Row2;
13314
13315 «ˇstruct Row4;
13316 struct» Row5;
13317 «struct Row6;
13318 ˇ»
13319 struct Row8;
13320 struct Row10;"#},
13321 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13322 indoc! {r#"struct Row;
13323 struct Row2;
13324
13325 «ˇstruct Row4;
13326 struct» Row5;
13327 «struct Row6;
13328 ˇ»
13329 struct Row8;
13330 struct Row10;"#},
13331 base_text,
13332 &mut cx,
13333 );
13334
13335 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
13336 assert_hunk_revert(
13337 indoc! {r#"struct Row;
13338 ˇstruct Row2;
13339
13340 struct Row4;
13341 struct Row5;
13342 struct Row6;
13343
13344 struct Row8;ˇ
13345 struct Row10;"#},
13346 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13347 indoc! {r#"struct Row;
13348 struct Row1;
13349 ˇstruct Row2;
13350
13351 struct Row4;
13352 struct Row5;
13353 struct Row6;
13354
13355 struct Row8;ˇ
13356 struct Row9;
13357 struct Row10;"#},
13358 base_text,
13359 &mut cx,
13360 );
13361 assert_hunk_revert(
13362 indoc! {r#"struct Row;
13363 struct Row2«ˇ;
13364 struct Row4;
13365 struct» Row5;
13366 «struct Row6;
13367
13368 struct Row8;ˇ»
13369 struct Row10;"#},
13370 vec![
13371 DiffHunkStatusKind::Deleted,
13372 DiffHunkStatusKind::Deleted,
13373 DiffHunkStatusKind::Deleted,
13374 ],
13375 indoc! {r#"struct Row;
13376 struct Row1;
13377 struct Row2«ˇ;
13378
13379 struct Row4;
13380 struct» Row5;
13381 «struct Row6;
13382
13383 struct Row8;ˇ»
13384 struct Row9;
13385 struct Row10;"#},
13386 base_text,
13387 &mut cx,
13388 );
13389}
13390
13391#[gpui::test]
13392async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
13393 init_test(cx, |_| {});
13394
13395 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
13396 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
13397 let base_text_3 =
13398 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
13399
13400 let text_1 = edit_first_char_of_every_line(base_text_1);
13401 let text_2 = edit_first_char_of_every_line(base_text_2);
13402 let text_3 = edit_first_char_of_every_line(base_text_3);
13403
13404 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
13405 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
13406 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
13407
13408 let multibuffer = cx.new(|cx| {
13409 let mut multibuffer = MultiBuffer::new(ReadWrite);
13410 multibuffer.push_excerpts(
13411 buffer_1.clone(),
13412 [
13413 ExcerptRange {
13414 context: Point::new(0, 0)..Point::new(3, 0),
13415 primary: None,
13416 },
13417 ExcerptRange {
13418 context: Point::new(5, 0)..Point::new(7, 0),
13419 primary: None,
13420 },
13421 ExcerptRange {
13422 context: Point::new(9, 0)..Point::new(10, 4),
13423 primary: None,
13424 },
13425 ],
13426 cx,
13427 );
13428 multibuffer.push_excerpts(
13429 buffer_2.clone(),
13430 [
13431 ExcerptRange {
13432 context: Point::new(0, 0)..Point::new(3, 0),
13433 primary: None,
13434 },
13435 ExcerptRange {
13436 context: Point::new(5, 0)..Point::new(7, 0),
13437 primary: None,
13438 },
13439 ExcerptRange {
13440 context: Point::new(9, 0)..Point::new(10, 4),
13441 primary: None,
13442 },
13443 ],
13444 cx,
13445 );
13446 multibuffer.push_excerpts(
13447 buffer_3.clone(),
13448 [
13449 ExcerptRange {
13450 context: Point::new(0, 0)..Point::new(3, 0),
13451 primary: None,
13452 },
13453 ExcerptRange {
13454 context: Point::new(5, 0)..Point::new(7, 0),
13455 primary: None,
13456 },
13457 ExcerptRange {
13458 context: Point::new(9, 0)..Point::new(10, 4),
13459 primary: None,
13460 },
13461 ],
13462 cx,
13463 );
13464 multibuffer
13465 });
13466
13467 let fs = FakeFs::new(cx.executor());
13468 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13469 let (editor, cx) = cx
13470 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
13471 editor.update_in(cx, |editor, _window, cx| {
13472 for (buffer, diff_base) in [
13473 (buffer_1.clone(), base_text_1),
13474 (buffer_2.clone(), base_text_2),
13475 (buffer_3.clone(), base_text_3),
13476 ] {
13477 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
13478 editor
13479 .buffer
13480 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
13481 }
13482 });
13483 cx.executor().run_until_parked();
13484
13485 editor.update_in(cx, |editor, window, cx| {
13486 assert_eq!(editor.text(cx), "Xaaa\nXbbb\nXccc\n\nXfff\nXggg\n\nXjjj\nXlll\nXmmm\nXnnn\n\nXqqq\nXrrr\n\nXuuu\nXvvv\nXwww\nXxxx\n\nX{{{\nX|||\n\nX\u{7f}\u{7f}\u{7f}");
13487 editor.select_all(&SelectAll, window, cx);
13488 editor.git_restore(&Default::default(), window, cx);
13489 });
13490 cx.executor().run_until_parked();
13491
13492 // When all ranges are selected, all buffer hunks are reverted.
13493 editor.update(cx, |editor, cx| {
13494 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");
13495 });
13496 buffer_1.update(cx, |buffer, _| {
13497 assert_eq!(buffer.text(), base_text_1);
13498 });
13499 buffer_2.update(cx, |buffer, _| {
13500 assert_eq!(buffer.text(), base_text_2);
13501 });
13502 buffer_3.update(cx, |buffer, _| {
13503 assert_eq!(buffer.text(), base_text_3);
13504 });
13505
13506 editor.update_in(cx, |editor, window, cx| {
13507 editor.undo(&Default::default(), window, cx);
13508 });
13509
13510 editor.update_in(cx, |editor, window, cx| {
13511 editor.change_selections(None, window, cx, |s| {
13512 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
13513 });
13514 editor.git_restore(&Default::default(), window, cx);
13515 });
13516
13517 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
13518 // but not affect buffer_2 and its related excerpts.
13519 editor.update(cx, |editor, cx| {
13520 assert_eq!(
13521 editor.text(cx),
13522 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n\n\nXlll\nXmmm\nXnnn\n\nXqqq\nXrrr\n\nXuuu\nXvvv\nXwww\nXxxx\n\nX{{{\nX|||\n\nX\u{7f}\u{7f}\u{7f}"
13523 );
13524 });
13525 buffer_1.update(cx, |buffer, _| {
13526 assert_eq!(buffer.text(), base_text_1);
13527 });
13528 buffer_2.update(cx, |buffer, _| {
13529 assert_eq!(
13530 buffer.text(),
13531 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
13532 );
13533 });
13534 buffer_3.update(cx, |buffer, _| {
13535 assert_eq!(
13536 buffer.text(),
13537 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
13538 );
13539 });
13540
13541 fn edit_first_char_of_every_line(text: &str) -> String {
13542 text.split('\n')
13543 .map(|line| format!("X{}", &line[1..]))
13544 .collect::<Vec<_>>()
13545 .join("\n")
13546 }
13547}
13548
13549#[gpui::test]
13550async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
13551 init_test(cx, |_| {});
13552
13553 let cols = 4;
13554 let rows = 10;
13555 let sample_text_1 = sample_text(rows, cols, 'a');
13556 assert_eq!(
13557 sample_text_1,
13558 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
13559 );
13560 let sample_text_2 = sample_text(rows, cols, 'l');
13561 assert_eq!(
13562 sample_text_2,
13563 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
13564 );
13565 let sample_text_3 = sample_text(rows, cols, 'v');
13566 assert_eq!(
13567 sample_text_3,
13568 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
13569 );
13570
13571 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
13572 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
13573 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
13574
13575 let multi_buffer = cx.new(|cx| {
13576 let mut multibuffer = MultiBuffer::new(ReadWrite);
13577 multibuffer.push_excerpts(
13578 buffer_1.clone(),
13579 [
13580 ExcerptRange {
13581 context: Point::new(0, 0)..Point::new(3, 0),
13582 primary: None,
13583 },
13584 ExcerptRange {
13585 context: Point::new(5, 0)..Point::new(7, 0),
13586 primary: None,
13587 },
13588 ExcerptRange {
13589 context: Point::new(9, 0)..Point::new(10, 4),
13590 primary: None,
13591 },
13592 ],
13593 cx,
13594 );
13595 multibuffer.push_excerpts(
13596 buffer_2.clone(),
13597 [
13598 ExcerptRange {
13599 context: Point::new(0, 0)..Point::new(3, 0),
13600 primary: None,
13601 },
13602 ExcerptRange {
13603 context: Point::new(5, 0)..Point::new(7, 0),
13604 primary: None,
13605 },
13606 ExcerptRange {
13607 context: Point::new(9, 0)..Point::new(10, 4),
13608 primary: None,
13609 },
13610 ],
13611 cx,
13612 );
13613 multibuffer.push_excerpts(
13614 buffer_3.clone(),
13615 [
13616 ExcerptRange {
13617 context: Point::new(0, 0)..Point::new(3, 0),
13618 primary: None,
13619 },
13620 ExcerptRange {
13621 context: Point::new(5, 0)..Point::new(7, 0),
13622 primary: None,
13623 },
13624 ExcerptRange {
13625 context: Point::new(9, 0)..Point::new(10, 4),
13626 primary: None,
13627 },
13628 ],
13629 cx,
13630 );
13631 multibuffer
13632 });
13633
13634 let fs = FakeFs::new(cx.executor());
13635 fs.insert_tree(
13636 "/a",
13637 json!({
13638 "main.rs": sample_text_1,
13639 "other.rs": sample_text_2,
13640 "lib.rs": sample_text_3,
13641 }),
13642 )
13643 .await;
13644 let project = Project::test(fs, ["/a".as_ref()], cx).await;
13645 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13646 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13647 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
13648 Editor::new(
13649 EditorMode::Full,
13650 multi_buffer,
13651 Some(project.clone()),
13652 window,
13653 cx,
13654 )
13655 });
13656 let multibuffer_item_id = workspace
13657 .update(cx, |workspace, window, cx| {
13658 assert!(
13659 workspace.active_item(cx).is_none(),
13660 "active item should be None before the first item is added"
13661 );
13662 workspace.add_item_to_active_pane(
13663 Box::new(multi_buffer_editor.clone()),
13664 None,
13665 true,
13666 window,
13667 cx,
13668 );
13669 let active_item = workspace
13670 .active_item(cx)
13671 .expect("should have an active item after adding the multi buffer");
13672 assert!(
13673 !active_item.is_singleton(cx),
13674 "A multi buffer was expected to active after adding"
13675 );
13676 active_item.item_id()
13677 })
13678 .unwrap();
13679 cx.executor().run_until_parked();
13680
13681 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13682 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
13683 s.select_ranges(Some(1..2))
13684 });
13685 editor.open_excerpts(&OpenExcerpts, window, cx);
13686 });
13687 cx.executor().run_until_parked();
13688 let first_item_id = workspace
13689 .update(cx, |workspace, window, cx| {
13690 let active_item = workspace
13691 .active_item(cx)
13692 .expect("should have an active item after navigating into the 1st buffer");
13693 let first_item_id = active_item.item_id();
13694 assert_ne!(
13695 first_item_id, multibuffer_item_id,
13696 "Should navigate into the 1st buffer and activate it"
13697 );
13698 assert!(
13699 active_item.is_singleton(cx),
13700 "New active item should be a singleton buffer"
13701 );
13702 assert_eq!(
13703 active_item
13704 .act_as::<Editor>(cx)
13705 .expect("should have navigated into an editor for the 1st buffer")
13706 .read(cx)
13707 .text(cx),
13708 sample_text_1
13709 );
13710
13711 workspace
13712 .go_back(workspace.active_pane().downgrade(), window, cx)
13713 .detach_and_log_err(cx);
13714
13715 first_item_id
13716 })
13717 .unwrap();
13718 cx.executor().run_until_parked();
13719 workspace
13720 .update(cx, |workspace, _, cx| {
13721 let active_item = workspace
13722 .active_item(cx)
13723 .expect("should have an active item after navigating back");
13724 assert_eq!(
13725 active_item.item_id(),
13726 multibuffer_item_id,
13727 "Should navigate back to the multi buffer"
13728 );
13729 assert!(!active_item.is_singleton(cx));
13730 })
13731 .unwrap();
13732
13733 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13734 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
13735 s.select_ranges(Some(39..40))
13736 });
13737 editor.open_excerpts(&OpenExcerpts, window, cx);
13738 });
13739 cx.executor().run_until_parked();
13740 let second_item_id = workspace
13741 .update(cx, |workspace, window, cx| {
13742 let active_item = workspace
13743 .active_item(cx)
13744 .expect("should have an active item after navigating into the 2nd buffer");
13745 let second_item_id = active_item.item_id();
13746 assert_ne!(
13747 second_item_id, multibuffer_item_id,
13748 "Should navigate away from the multibuffer"
13749 );
13750 assert_ne!(
13751 second_item_id, first_item_id,
13752 "Should navigate into the 2nd buffer and activate it"
13753 );
13754 assert!(
13755 active_item.is_singleton(cx),
13756 "New active item should be a singleton buffer"
13757 );
13758 assert_eq!(
13759 active_item
13760 .act_as::<Editor>(cx)
13761 .expect("should have navigated into an editor")
13762 .read(cx)
13763 .text(cx),
13764 sample_text_2
13765 );
13766
13767 workspace
13768 .go_back(workspace.active_pane().downgrade(), window, cx)
13769 .detach_and_log_err(cx);
13770
13771 second_item_id
13772 })
13773 .unwrap();
13774 cx.executor().run_until_parked();
13775 workspace
13776 .update(cx, |workspace, _, cx| {
13777 let active_item = workspace
13778 .active_item(cx)
13779 .expect("should have an active item after navigating back from the 2nd buffer");
13780 assert_eq!(
13781 active_item.item_id(),
13782 multibuffer_item_id,
13783 "Should navigate back from the 2nd buffer to the multi buffer"
13784 );
13785 assert!(!active_item.is_singleton(cx));
13786 })
13787 .unwrap();
13788
13789 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13790 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
13791 s.select_ranges(Some(70..70))
13792 });
13793 editor.open_excerpts(&OpenExcerpts, window, cx);
13794 });
13795 cx.executor().run_until_parked();
13796 workspace
13797 .update(cx, |workspace, window, cx| {
13798 let active_item = workspace
13799 .active_item(cx)
13800 .expect("should have an active item after navigating into the 3rd buffer");
13801 let third_item_id = active_item.item_id();
13802 assert_ne!(
13803 third_item_id, multibuffer_item_id,
13804 "Should navigate into the 3rd buffer and activate it"
13805 );
13806 assert_ne!(third_item_id, first_item_id);
13807 assert_ne!(third_item_id, second_item_id);
13808 assert!(
13809 active_item.is_singleton(cx),
13810 "New active item should be a singleton buffer"
13811 );
13812 assert_eq!(
13813 active_item
13814 .act_as::<Editor>(cx)
13815 .expect("should have navigated into an editor")
13816 .read(cx)
13817 .text(cx),
13818 sample_text_3
13819 );
13820
13821 workspace
13822 .go_back(workspace.active_pane().downgrade(), window, cx)
13823 .detach_and_log_err(cx);
13824 })
13825 .unwrap();
13826 cx.executor().run_until_parked();
13827 workspace
13828 .update(cx, |workspace, _, cx| {
13829 let active_item = workspace
13830 .active_item(cx)
13831 .expect("should have an active item after navigating back from the 3rd buffer");
13832 assert_eq!(
13833 active_item.item_id(),
13834 multibuffer_item_id,
13835 "Should navigate back from the 3rd buffer to the multi buffer"
13836 );
13837 assert!(!active_item.is_singleton(cx));
13838 })
13839 .unwrap();
13840}
13841
13842#[gpui::test]
13843async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13844 init_test(cx, |_| {});
13845
13846 let mut cx = EditorTestContext::new(cx).await;
13847
13848 let diff_base = r#"
13849 use some::mod;
13850
13851 const A: u32 = 42;
13852
13853 fn main() {
13854 println!("hello");
13855
13856 println!("world");
13857 }
13858 "#
13859 .unindent();
13860
13861 cx.set_state(
13862 &r#"
13863 use some::modified;
13864
13865 ˇ
13866 fn main() {
13867 println!("hello there");
13868
13869 println!("around the");
13870 println!("world");
13871 }
13872 "#
13873 .unindent(),
13874 );
13875
13876 cx.set_head_text(&diff_base);
13877 executor.run_until_parked();
13878
13879 cx.update_editor(|editor, window, cx| {
13880 editor.go_to_next_hunk(&GoToHunk, window, cx);
13881 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13882 });
13883 executor.run_until_parked();
13884 cx.assert_state_with_diff(
13885 r#"
13886 use some::modified;
13887
13888
13889 fn main() {
13890 - println!("hello");
13891 + ˇ println!("hello there");
13892
13893 println!("around the");
13894 println!("world");
13895 }
13896 "#
13897 .unindent(),
13898 );
13899
13900 cx.update_editor(|editor, window, cx| {
13901 for _ in 0..2 {
13902 editor.go_to_next_hunk(&GoToHunk, window, cx);
13903 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13904 }
13905 });
13906 executor.run_until_parked();
13907 cx.assert_state_with_diff(
13908 r#"
13909 - use some::mod;
13910 + ˇuse some::modified;
13911
13912
13913 fn main() {
13914 - println!("hello");
13915 + println!("hello there");
13916
13917 + println!("around the");
13918 println!("world");
13919 }
13920 "#
13921 .unindent(),
13922 );
13923
13924 cx.update_editor(|editor, window, cx| {
13925 editor.go_to_next_hunk(&GoToHunk, window, cx);
13926 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13927 });
13928 executor.run_until_parked();
13929 cx.assert_state_with_diff(
13930 r#"
13931 - use some::mod;
13932 + use some::modified;
13933
13934 - const A: u32 = 42;
13935 ˇ
13936 fn main() {
13937 - println!("hello");
13938 + println!("hello there");
13939
13940 + println!("around the");
13941 println!("world");
13942 }
13943 "#
13944 .unindent(),
13945 );
13946
13947 cx.update_editor(|editor, window, cx| {
13948 editor.cancel(&Cancel, window, cx);
13949 });
13950
13951 cx.assert_state_with_diff(
13952 r#"
13953 use some::modified;
13954
13955 ˇ
13956 fn main() {
13957 println!("hello there");
13958
13959 println!("around the");
13960 println!("world");
13961 }
13962 "#
13963 .unindent(),
13964 );
13965}
13966
13967#[gpui::test]
13968async fn test_diff_base_change_with_expanded_diff_hunks(
13969 executor: BackgroundExecutor,
13970 cx: &mut TestAppContext,
13971) {
13972 init_test(cx, |_| {});
13973
13974 let mut cx = EditorTestContext::new(cx).await;
13975
13976 let diff_base = r#"
13977 use some::mod1;
13978 use some::mod2;
13979
13980 const A: u32 = 42;
13981 const B: u32 = 42;
13982 const C: u32 = 42;
13983
13984 fn main() {
13985 println!("hello");
13986
13987 println!("world");
13988 }
13989 "#
13990 .unindent();
13991
13992 cx.set_state(
13993 &r#"
13994 use some::mod2;
13995
13996 const A: u32 = 42;
13997 const C: u32 = 42;
13998
13999 fn main(ˇ) {
14000 //println!("hello");
14001
14002 println!("world");
14003 //
14004 //
14005 }
14006 "#
14007 .unindent(),
14008 );
14009
14010 cx.set_head_text(&diff_base);
14011 executor.run_until_parked();
14012
14013 cx.update_editor(|editor, window, cx| {
14014 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14015 });
14016 executor.run_until_parked();
14017 cx.assert_state_with_diff(
14018 r#"
14019 - use some::mod1;
14020 use some::mod2;
14021
14022 const A: u32 = 42;
14023 - const B: u32 = 42;
14024 const C: u32 = 42;
14025
14026 fn main(ˇ) {
14027 - println!("hello");
14028 + //println!("hello");
14029
14030 println!("world");
14031 + //
14032 + //
14033 }
14034 "#
14035 .unindent(),
14036 );
14037
14038 cx.set_head_text("new diff base!");
14039 executor.run_until_parked();
14040 cx.assert_state_with_diff(
14041 r#"
14042 - new diff base!
14043 + use some::mod2;
14044 +
14045 + const A: u32 = 42;
14046 + const C: u32 = 42;
14047 +
14048 + fn main(ˇ) {
14049 + //println!("hello");
14050 +
14051 + println!("world");
14052 + //
14053 + //
14054 + }
14055 "#
14056 .unindent(),
14057 );
14058}
14059
14060#[gpui::test]
14061async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
14062 init_test(cx, |_| {});
14063
14064 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14065 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14066 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14067 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14068 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
14069 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
14070
14071 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
14072 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
14073 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
14074
14075 let multi_buffer = cx.new(|cx| {
14076 let mut multibuffer = MultiBuffer::new(ReadWrite);
14077 multibuffer.push_excerpts(
14078 buffer_1.clone(),
14079 [
14080 ExcerptRange {
14081 context: Point::new(0, 0)..Point::new(3, 0),
14082 primary: None,
14083 },
14084 ExcerptRange {
14085 context: Point::new(5, 0)..Point::new(7, 0),
14086 primary: None,
14087 },
14088 ExcerptRange {
14089 context: Point::new(9, 0)..Point::new(10, 3),
14090 primary: None,
14091 },
14092 ],
14093 cx,
14094 );
14095 multibuffer.push_excerpts(
14096 buffer_2.clone(),
14097 [
14098 ExcerptRange {
14099 context: Point::new(0, 0)..Point::new(3, 0),
14100 primary: None,
14101 },
14102 ExcerptRange {
14103 context: Point::new(5, 0)..Point::new(7, 0),
14104 primary: None,
14105 },
14106 ExcerptRange {
14107 context: Point::new(9, 0)..Point::new(10, 3),
14108 primary: None,
14109 },
14110 ],
14111 cx,
14112 );
14113 multibuffer.push_excerpts(
14114 buffer_3.clone(),
14115 [
14116 ExcerptRange {
14117 context: Point::new(0, 0)..Point::new(3, 0),
14118 primary: None,
14119 },
14120 ExcerptRange {
14121 context: Point::new(5, 0)..Point::new(7, 0),
14122 primary: None,
14123 },
14124 ExcerptRange {
14125 context: Point::new(9, 0)..Point::new(10, 3),
14126 primary: None,
14127 },
14128 ],
14129 cx,
14130 );
14131 multibuffer
14132 });
14133
14134 let editor =
14135 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14136 editor
14137 .update(cx, |editor, _window, cx| {
14138 for (buffer, diff_base) in [
14139 (buffer_1.clone(), file_1_old),
14140 (buffer_2.clone(), file_2_old),
14141 (buffer_3.clone(), file_3_old),
14142 ] {
14143 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14144 editor
14145 .buffer
14146 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14147 }
14148 })
14149 .unwrap();
14150
14151 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14152 cx.run_until_parked();
14153
14154 cx.assert_editor_state(
14155 &"
14156 ˇaaa
14157 ccc
14158 ddd
14159
14160 ggg
14161 hhh
14162
14163
14164 lll
14165 mmm
14166 NNN
14167
14168 qqq
14169 rrr
14170
14171 uuu
14172 111
14173 222
14174 333
14175
14176 666
14177 777
14178
14179 000
14180 !!!"
14181 .unindent(),
14182 );
14183
14184 cx.update_editor(|editor, window, cx| {
14185 editor.select_all(&SelectAll, window, cx);
14186 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14187 });
14188 cx.executor().run_until_parked();
14189
14190 cx.assert_state_with_diff(
14191 "
14192 «aaa
14193 - bbb
14194 ccc
14195 ddd
14196
14197 ggg
14198 hhh
14199
14200
14201 lll
14202 mmm
14203 - nnn
14204 + NNN
14205
14206 qqq
14207 rrr
14208
14209 uuu
14210 111
14211 222
14212 333
14213
14214 + 666
14215 777
14216
14217 000
14218 !!!ˇ»"
14219 .unindent(),
14220 );
14221}
14222
14223#[gpui::test]
14224async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
14225 init_test(cx, |_| {});
14226
14227 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
14228 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
14229
14230 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
14231 let multi_buffer = cx.new(|cx| {
14232 let mut multibuffer = MultiBuffer::new(ReadWrite);
14233 multibuffer.push_excerpts(
14234 buffer.clone(),
14235 [
14236 ExcerptRange {
14237 context: Point::new(0, 0)..Point::new(2, 0),
14238 primary: None,
14239 },
14240 ExcerptRange {
14241 context: Point::new(4, 0)..Point::new(7, 0),
14242 primary: None,
14243 },
14244 ExcerptRange {
14245 context: Point::new(9, 0)..Point::new(10, 0),
14246 primary: None,
14247 },
14248 ],
14249 cx,
14250 );
14251 multibuffer
14252 });
14253
14254 let editor =
14255 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14256 editor
14257 .update(cx, |editor, _window, cx| {
14258 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
14259 editor
14260 .buffer
14261 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
14262 })
14263 .unwrap();
14264
14265 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14266 cx.run_until_parked();
14267
14268 cx.update_editor(|editor, window, cx| {
14269 editor.expand_all_diff_hunks(&Default::default(), window, cx)
14270 });
14271 cx.executor().run_until_parked();
14272
14273 // When the start of a hunk coincides with the start of its excerpt,
14274 // the hunk is expanded. When the start of a a hunk is earlier than
14275 // the start of its excerpt, the hunk is not expanded.
14276 cx.assert_state_with_diff(
14277 "
14278 ˇaaa
14279 - bbb
14280 + BBB
14281
14282 - ddd
14283 - eee
14284 + DDD
14285 + EEE
14286 fff
14287
14288 iii
14289 "
14290 .unindent(),
14291 );
14292}
14293
14294#[gpui::test]
14295async fn test_edits_around_expanded_insertion_hunks(
14296 executor: BackgroundExecutor,
14297 cx: &mut TestAppContext,
14298) {
14299 init_test(cx, |_| {});
14300
14301 let mut cx = EditorTestContext::new(cx).await;
14302
14303 let diff_base = r#"
14304 use some::mod1;
14305 use some::mod2;
14306
14307 const A: u32 = 42;
14308
14309 fn main() {
14310 println!("hello");
14311
14312 println!("world");
14313 }
14314 "#
14315 .unindent();
14316 executor.run_until_parked();
14317 cx.set_state(
14318 &r#"
14319 use some::mod1;
14320 use some::mod2;
14321
14322 const A: u32 = 42;
14323 const B: u32 = 42;
14324 const C: u32 = 42;
14325 ˇ
14326
14327 fn main() {
14328 println!("hello");
14329
14330 println!("world");
14331 }
14332 "#
14333 .unindent(),
14334 );
14335
14336 cx.set_head_text(&diff_base);
14337 executor.run_until_parked();
14338
14339 cx.update_editor(|editor, window, cx| {
14340 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14341 });
14342 executor.run_until_parked();
14343
14344 cx.assert_state_with_diff(
14345 r#"
14346 use some::mod1;
14347 use some::mod2;
14348
14349 const A: u32 = 42;
14350 + const B: u32 = 42;
14351 + const C: u32 = 42;
14352 + ˇ
14353
14354 fn main() {
14355 println!("hello");
14356
14357 println!("world");
14358 }
14359 "#
14360 .unindent(),
14361 );
14362
14363 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
14364 executor.run_until_parked();
14365
14366 cx.assert_state_with_diff(
14367 r#"
14368 use some::mod1;
14369 use some::mod2;
14370
14371 const A: u32 = 42;
14372 + const B: u32 = 42;
14373 + const C: u32 = 42;
14374 + const D: u32 = 42;
14375 + ˇ
14376
14377 fn main() {
14378 println!("hello");
14379
14380 println!("world");
14381 }
14382 "#
14383 .unindent(),
14384 );
14385
14386 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
14387 executor.run_until_parked();
14388
14389 cx.assert_state_with_diff(
14390 r#"
14391 use some::mod1;
14392 use some::mod2;
14393
14394 const A: u32 = 42;
14395 + const B: u32 = 42;
14396 + const C: u32 = 42;
14397 + const D: u32 = 42;
14398 + const E: u32 = 42;
14399 + ˇ
14400
14401 fn main() {
14402 println!("hello");
14403
14404 println!("world");
14405 }
14406 "#
14407 .unindent(),
14408 );
14409
14410 cx.update_editor(|editor, window, cx| {
14411 editor.delete_line(&DeleteLine, window, cx);
14412 });
14413 executor.run_until_parked();
14414
14415 cx.assert_state_with_diff(
14416 r#"
14417 use some::mod1;
14418 use some::mod2;
14419
14420 const A: u32 = 42;
14421 + const B: u32 = 42;
14422 + const C: u32 = 42;
14423 + const D: u32 = 42;
14424 + const E: u32 = 42;
14425 ˇ
14426 fn main() {
14427 println!("hello");
14428
14429 println!("world");
14430 }
14431 "#
14432 .unindent(),
14433 );
14434
14435 cx.update_editor(|editor, window, cx| {
14436 editor.move_up(&MoveUp, window, cx);
14437 editor.delete_line(&DeleteLine, window, cx);
14438 editor.move_up(&MoveUp, window, cx);
14439 editor.delete_line(&DeleteLine, window, cx);
14440 editor.move_up(&MoveUp, window, cx);
14441 editor.delete_line(&DeleteLine, window, cx);
14442 });
14443 executor.run_until_parked();
14444 cx.assert_state_with_diff(
14445 r#"
14446 use some::mod1;
14447 use some::mod2;
14448
14449 const A: u32 = 42;
14450 + const B: u32 = 42;
14451 ˇ
14452 fn main() {
14453 println!("hello");
14454
14455 println!("world");
14456 }
14457 "#
14458 .unindent(),
14459 );
14460
14461 cx.update_editor(|editor, window, cx| {
14462 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
14463 editor.delete_line(&DeleteLine, window, cx);
14464 });
14465 executor.run_until_parked();
14466 cx.assert_state_with_diff(
14467 r#"
14468 ˇ
14469 fn main() {
14470 println!("hello");
14471
14472 println!("world");
14473 }
14474 "#
14475 .unindent(),
14476 );
14477}
14478
14479#[gpui::test]
14480async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
14481 init_test(cx, |_| {});
14482
14483 let mut cx = EditorTestContext::new(cx).await;
14484 cx.set_head_text(indoc! { "
14485 one
14486 two
14487 three
14488 four
14489 five
14490 "
14491 });
14492 cx.set_state(indoc! { "
14493 one
14494 ˇthree
14495 five
14496 "});
14497 cx.run_until_parked();
14498 cx.update_editor(|editor, window, cx| {
14499 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14500 });
14501 cx.assert_state_with_diff(
14502 indoc! { "
14503 one
14504 - two
14505 ˇthree
14506 - four
14507 five
14508 "}
14509 .to_string(),
14510 );
14511 cx.update_editor(|editor, window, cx| {
14512 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14513 });
14514
14515 cx.assert_state_with_diff(
14516 indoc! { "
14517 one
14518 ˇthree
14519 five
14520 "}
14521 .to_string(),
14522 );
14523
14524 cx.set_state(indoc! { "
14525 one
14526 ˇTWO
14527 three
14528 four
14529 five
14530 "});
14531 cx.run_until_parked();
14532 cx.update_editor(|editor, window, cx| {
14533 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14534 });
14535
14536 cx.assert_state_with_diff(
14537 indoc! { "
14538 one
14539 - two
14540 + ˇTWO
14541 three
14542 four
14543 five
14544 "}
14545 .to_string(),
14546 );
14547 cx.update_editor(|editor, window, cx| {
14548 editor.move_up(&Default::default(), window, cx);
14549 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14550 });
14551 cx.assert_state_with_diff(
14552 indoc! { "
14553 one
14554 ˇTWO
14555 three
14556 four
14557 five
14558 "}
14559 .to_string(),
14560 );
14561}
14562
14563#[gpui::test]
14564async fn test_edits_around_expanded_deletion_hunks(
14565 executor: BackgroundExecutor,
14566 cx: &mut TestAppContext,
14567) {
14568 init_test(cx, |_| {});
14569
14570 let mut cx = EditorTestContext::new(cx).await;
14571
14572 let diff_base = r#"
14573 use some::mod1;
14574 use some::mod2;
14575
14576 const A: u32 = 42;
14577 const B: u32 = 42;
14578 const C: u32 = 42;
14579
14580
14581 fn main() {
14582 println!("hello");
14583
14584 println!("world");
14585 }
14586 "#
14587 .unindent();
14588 executor.run_until_parked();
14589 cx.set_state(
14590 &r#"
14591 use some::mod1;
14592 use some::mod2;
14593
14594 ˇconst B: u32 = 42;
14595 const C: u32 = 42;
14596
14597
14598 fn main() {
14599 println!("hello");
14600
14601 println!("world");
14602 }
14603 "#
14604 .unindent(),
14605 );
14606
14607 cx.set_head_text(&diff_base);
14608 executor.run_until_parked();
14609
14610 cx.update_editor(|editor, window, cx| {
14611 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14612 });
14613 executor.run_until_parked();
14614
14615 cx.assert_state_with_diff(
14616 r#"
14617 use some::mod1;
14618 use some::mod2;
14619
14620 - const A: u32 = 42;
14621 ˇconst B: u32 = 42;
14622 const C: u32 = 42;
14623
14624
14625 fn main() {
14626 println!("hello");
14627
14628 println!("world");
14629 }
14630 "#
14631 .unindent(),
14632 );
14633
14634 cx.update_editor(|editor, window, cx| {
14635 editor.delete_line(&DeleteLine, window, cx);
14636 });
14637 executor.run_until_parked();
14638 cx.assert_state_with_diff(
14639 r#"
14640 use some::mod1;
14641 use some::mod2;
14642
14643 - const A: u32 = 42;
14644 - const B: u32 = 42;
14645 ˇconst C: u32 = 42;
14646
14647
14648 fn main() {
14649 println!("hello");
14650
14651 println!("world");
14652 }
14653 "#
14654 .unindent(),
14655 );
14656
14657 cx.update_editor(|editor, window, cx| {
14658 editor.delete_line(&DeleteLine, window, cx);
14659 });
14660 executor.run_until_parked();
14661 cx.assert_state_with_diff(
14662 r#"
14663 use some::mod1;
14664 use some::mod2;
14665
14666 - const A: u32 = 42;
14667 - const B: u32 = 42;
14668 - const C: u32 = 42;
14669 ˇ
14670
14671 fn main() {
14672 println!("hello");
14673
14674 println!("world");
14675 }
14676 "#
14677 .unindent(),
14678 );
14679
14680 cx.update_editor(|editor, window, cx| {
14681 editor.handle_input("replacement", window, cx);
14682 });
14683 executor.run_until_parked();
14684 cx.assert_state_with_diff(
14685 r#"
14686 use some::mod1;
14687 use some::mod2;
14688
14689 - const A: u32 = 42;
14690 - const B: u32 = 42;
14691 - const C: u32 = 42;
14692 -
14693 + replacementˇ
14694
14695 fn main() {
14696 println!("hello");
14697
14698 println!("world");
14699 }
14700 "#
14701 .unindent(),
14702 );
14703}
14704
14705#[gpui::test]
14706async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14707 init_test(cx, |_| {});
14708
14709 let mut cx = EditorTestContext::new(cx).await;
14710
14711 let base_text = r#"
14712 one
14713 two
14714 three
14715 four
14716 five
14717 "#
14718 .unindent();
14719 executor.run_until_parked();
14720 cx.set_state(
14721 &r#"
14722 one
14723 two
14724 fˇour
14725 five
14726 "#
14727 .unindent(),
14728 );
14729
14730 cx.set_head_text(&base_text);
14731 executor.run_until_parked();
14732
14733 cx.update_editor(|editor, window, cx| {
14734 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14735 });
14736 executor.run_until_parked();
14737
14738 cx.assert_state_with_diff(
14739 r#"
14740 one
14741 two
14742 - three
14743 fˇour
14744 five
14745 "#
14746 .unindent(),
14747 );
14748
14749 cx.update_editor(|editor, window, cx| {
14750 editor.backspace(&Backspace, window, cx);
14751 editor.backspace(&Backspace, window, cx);
14752 });
14753 executor.run_until_parked();
14754 cx.assert_state_with_diff(
14755 r#"
14756 one
14757 two
14758 - threeˇ
14759 - four
14760 + our
14761 five
14762 "#
14763 .unindent(),
14764 );
14765}
14766
14767#[gpui::test]
14768async fn test_edit_after_expanded_modification_hunk(
14769 executor: BackgroundExecutor,
14770 cx: &mut TestAppContext,
14771) {
14772 init_test(cx, |_| {});
14773
14774 let mut cx = EditorTestContext::new(cx).await;
14775
14776 let diff_base = r#"
14777 use some::mod1;
14778 use some::mod2;
14779
14780 const A: u32 = 42;
14781 const B: u32 = 42;
14782 const C: u32 = 42;
14783 const D: u32 = 42;
14784
14785
14786 fn main() {
14787 println!("hello");
14788
14789 println!("world");
14790 }"#
14791 .unindent();
14792
14793 cx.set_state(
14794 &r#"
14795 use some::mod1;
14796 use some::mod2;
14797
14798 const A: u32 = 42;
14799 const B: u32 = 42;
14800 const C: u32 = 43ˇ
14801 const D: u32 = 42;
14802
14803
14804 fn main() {
14805 println!("hello");
14806
14807 println!("world");
14808 }"#
14809 .unindent(),
14810 );
14811
14812 cx.set_head_text(&diff_base);
14813 executor.run_until_parked();
14814 cx.update_editor(|editor, window, cx| {
14815 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14816 });
14817 executor.run_until_parked();
14818
14819 cx.assert_state_with_diff(
14820 r#"
14821 use some::mod1;
14822 use some::mod2;
14823
14824 const A: u32 = 42;
14825 const B: u32 = 42;
14826 - const C: u32 = 42;
14827 + const C: u32 = 43ˇ
14828 const D: u32 = 42;
14829
14830
14831 fn main() {
14832 println!("hello");
14833
14834 println!("world");
14835 }"#
14836 .unindent(),
14837 );
14838
14839 cx.update_editor(|editor, window, cx| {
14840 editor.handle_input("\nnew_line\n", window, cx);
14841 });
14842 executor.run_until_parked();
14843
14844 cx.assert_state_with_diff(
14845 r#"
14846 use some::mod1;
14847 use some::mod2;
14848
14849 const A: u32 = 42;
14850 const B: u32 = 42;
14851 - const C: u32 = 42;
14852 + const C: u32 = 43
14853 + new_line
14854 + ˇ
14855 const D: u32 = 42;
14856
14857
14858 fn main() {
14859 println!("hello");
14860
14861 println!("world");
14862 }"#
14863 .unindent(),
14864 );
14865}
14866
14867#[gpui::test]
14868async fn test_stage_and_unstage_added_file_hunk(
14869 executor: BackgroundExecutor,
14870 cx: &mut TestAppContext,
14871) {
14872 init_test(cx, |_| {});
14873
14874 let mut cx = EditorTestContext::new(cx).await;
14875 cx.update_editor(|editor, _, cx| {
14876 editor.set_expand_all_diff_hunks(cx);
14877 });
14878
14879 let working_copy = r#"
14880 ˇfn main() {
14881 println!("hello, world!");
14882 }
14883 "#
14884 .unindent();
14885
14886 cx.set_state(&working_copy);
14887 executor.run_until_parked();
14888
14889 cx.assert_state_with_diff(
14890 r#"
14891 + ˇfn main() {
14892 + println!("hello, world!");
14893 + }
14894 "#
14895 .unindent(),
14896 );
14897 cx.assert_index_text(None);
14898
14899 cx.update_editor(|editor, window, cx| {
14900 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
14901 });
14902 executor.run_until_parked();
14903 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
14904 cx.assert_state_with_diff(
14905 r#"
14906 + ˇfn main() {
14907 + println!("hello, world!");
14908 + }
14909 "#
14910 .unindent(),
14911 );
14912
14913 cx.update_editor(|editor, window, cx| {
14914 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
14915 });
14916 executor.run_until_parked();
14917 cx.assert_index_text(None);
14918}
14919
14920async fn setup_indent_guides_editor(
14921 text: &str,
14922 cx: &mut TestAppContext,
14923) -> (BufferId, EditorTestContext) {
14924 init_test(cx, |_| {});
14925
14926 let mut cx = EditorTestContext::new(cx).await;
14927
14928 let buffer_id = cx.update_editor(|editor, window, cx| {
14929 editor.set_text(text, window, cx);
14930 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
14931
14932 buffer_ids[0]
14933 });
14934
14935 (buffer_id, cx)
14936}
14937
14938fn assert_indent_guides(
14939 range: Range<u32>,
14940 expected: Vec<IndentGuide>,
14941 active_indices: Option<Vec<usize>>,
14942 cx: &mut EditorTestContext,
14943) {
14944 let indent_guides = cx.update_editor(|editor, window, cx| {
14945 let snapshot = editor.snapshot(window, cx).display_snapshot;
14946 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
14947 editor,
14948 MultiBufferRow(range.start)..MultiBufferRow(range.end),
14949 true,
14950 &snapshot,
14951 cx,
14952 );
14953
14954 indent_guides.sort_by(|a, b| {
14955 a.depth.cmp(&b.depth).then(
14956 a.start_row
14957 .cmp(&b.start_row)
14958 .then(a.end_row.cmp(&b.end_row)),
14959 )
14960 });
14961 indent_guides
14962 });
14963
14964 if let Some(expected) = active_indices {
14965 let active_indices = cx.update_editor(|editor, window, cx| {
14966 let snapshot = editor.snapshot(window, cx).display_snapshot;
14967 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
14968 });
14969
14970 assert_eq!(
14971 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
14972 expected,
14973 "Active indent guide indices do not match"
14974 );
14975 }
14976
14977 assert_eq!(indent_guides, expected, "Indent guides do not match");
14978}
14979
14980fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
14981 IndentGuide {
14982 buffer_id,
14983 start_row: MultiBufferRow(start_row),
14984 end_row: MultiBufferRow(end_row),
14985 depth,
14986 tab_size: 4,
14987 settings: IndentGuideSettings {
14988 enabled: true,
14989 line_width: 1,
14990 active_line_width: 1,
14991 ..Default::default()
14992 },
14993 }
14994}
14995
14996#[gpui::test]
14997async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
14998 let (buffer_id, mut cx) = setup_indent_guides_editor(
14999 &"
15000 fn main() {
15001 let a = 1;
15002 }"
15003 .unindent(),
15004 cx,
15005 )
15006 .await;
15007
15008 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15009}
15010
15011#[gpui::test]
15012async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
15013 let (buffer_id, mut cx) = setup_indent_guides_editor(
15014 &"
15015 fn main() {
15016 let a = 1;
15017 let b = 2;
15018 }"
15019 .unindent(),
15020 cx,
15021 )
15022 .await;
15023
15024 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
15025}
15026
15027#[gpui::test]
15028async fn test_indent_guide_nested(cx: &mut TestAppContext) {
15029 let (buffer_id, mut cx) = setup_indent_guides_editor(
15030 &"
15031 fn main() {
15032 let a = 1;
15033 if a == 3 {
15034 let b = 2;
15035 } else {
15036 let c = 3;
15037 }
15038 }"
15039 .unindent(),
15040 cx,
15041 )
15042 .await;
15043
15044 assert_indent_guides(
15045 0..8,
15046 vec![
15047 indent_guide(buffer_id, 1, 6, 0),
15048 indent_guide(buffer_id, 3, 3, 1),
15049 indent_guide(buffer_id, 5, 5, 1),
15050 ],
15051 None,
15052 &mut cx,
15053 );
15054}
15055
15056#[gpui::test]
15057async fn test_indent_guide_tab(cx: &mut TestAppContext) {
15058 let (buffer_id, mut cx) = setup_indent_guides_editor(
15059 &"
15060 fn main() {
15061 let a = 1;
15062 let b = 2;
15063 let c = 3;
15064 }"
15065 .unindent(),
15066 cx,
15067 )
15068 .await;
15069
15070 assert_indent_guides(
15071 0..5,
15072 vec![
15073 indent_guide(buffer_id, 1, 3, 0),
15074 indent_guide(buffer_id, 2, 2, 1),
15075 ],
15076 None,
15077 &mut cx,
15078 );
15079}
15080
15081#[gpui::test]
15082async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
15083 let (buffer_id, mut cx) = setup_indent_guides_editor(
15084 &"
15085 fn main() {
15086 let a = 1;
15087
15088 let c = 3;
15089 }"
15090 .unindent(),
15091 cx,
15092 )
15093 .await;
15094
15095 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
15096}
15097
15098#[gpui::test]
15099async fn test_indent_guide_complex(cx: &mut TestAppContext) {
15100 let (buffer_id, mut cx) = setup_indent_guides_editor(
15101 &"
15102 fn main() {
15103 let a = 1;
15104
15105 let c = 3;
15106
15107 if a == 3 {
15108 let b = 2;
15109 } else {
15110 let c = 3;
15111 }
15112 }"
15113 .unindent(),
15114 cx,
15115 )
15116 .await;
15117
15118 assert_indent_guides(
15119 0..11,
15120 vec![
15121 indent_guide(buffer_id, 1, 9, 0),
15122 indent_guide(buffer_id, 6, 6, 1),
15123 indent_guide(buffer_id, 8, 8, 1),
15124 ],
15125 None,
15126 &mut cx,
15127 );
15128}
15129
15130#[gpui::test]
15131async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
15132 let (buffer_id, mut cx) = setup_indent_guides_editor(
15133 &"
15134 fn main() {
15135 let a = 1;
15136
15137 let c = 3;
15138
15139 if a == 3 {
15140 let b = 2;
15141 } else {
15142 let c = 3;
15143 }
15144 }"
15145 .unindent(),
15146 cx,
15147 )
15148 .await;
15149
15150 assert_indent_guides(
15151 1..11,
15152 vec![
15153 indent_guide(buffer_id, 1, 9, 0),
15154 indent_guide(buffer_id, 6, 6, 1),
15155 indent_guide(buffer_id, 8, 8, 1),
15156 ],
15157 None,
15158 &mut cx,
15159 );
15160}
15161
15162#[gpui::test]
15163async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
15164 let (buffer_id, mut cx) = setup_indent_guides_editor(
15165 &"
15166 fn main() {
15167 let a = 1;
15168
15169 let c = 3;
15170
15171 if a == 3 {
15172 let b = 2;
15173 } else {
15174 let c = 3;
15175 }
15176 }"
15177 .unindent(),
15178 cx,
15179 )
15180 .await;
15181
15182 assert_indent_guides(
15183 1..10,
15184 vec![
15185 indent_guide(buffer_id, 1, 9, 0),
15186 indent_guide(buffer_id, 6, 6, 1),
15187 indent_guide(buffer_id, 8, 8, 1),
15188 ],
15189 None,
15190 &mut cx,
15191 );
15192}
15193
15194#[gpui::test]
15195async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
15196 let (buffer_id, mut cx) = setup_indent_guides_editor(
15197 &"
15198 block1
15199 block2
15200 block3
15201 block4
15202 block2
15203 block1
15204 block1"
15205 .unindent(),
15206 cx,
15207 )
15208 .await;
15209
15210 assert_indent_guides(
15211 1..10,
15212 vec![
15213 indent_guide(buffer_id, 1, 4, 0),
15214 indent_guide(buffer_id, 2, 3, 1),
15215 indent_guide(buffer_id, 3, 3, 2),
15216 ],
15217 None,
15218 &mut cx,
15219 );
15220}
15221
15222#[gpui::test]
15223async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
15224 let (buffer_id, mut cx) = setup_indent_guides_editor(
15225 &"
15226 block1
15227 block2
15228 block3
15229
15230 block1
15231 block1"
15232 .unindent(),
15233 cx,
15234 )
15235 .await;
15236
15237 assert_indent_guides(
15238 0..6,
15239 vec![
15240 indent_guide(buffer_id, 1, 2, 0),
15241 indent_guide(buffer_id, 2, 2, 1),
15242 ],
15243 None,
15244 &mut cx,
15245 );
15246}
15247
15248#[gpui::test]
15249async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
15250 let (buffer_id, mut cx) = setup_indent_guides_editor(
15251 &"
15252 block1
15253
15254
15255
15256 block2
15257 "
15258 .unindent(),
15259 cx,
15260 )
15261 .await;
15262
15263 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15264}
15265
15266#[gpui::test]
15267async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
15268 let (buffer_id, mut cx) = setup_indent_guides_editor(
15269 &"
15270 def a:
15271 \tb = 3
15272 \tif True:
15273 \t\tc = 4
15274 \t\td = 5
15275 \tprint(b)
15276 "
15277 .unindent(),
15278 cx,
15279 )
15280 .await;
15281
15282 assert_indent_guides(
15283 0..6,
15284 vec![
15285 indent_guide(buffer_id, 1, 6, 0),
15286 indent_guide(buffer_id, 3, 4, 1),
15287 ],
15288 None,
15289 &mut cx,
15290 );
15291}
15292
15293#[gpui::test]
15294async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
15295 let (buffer_id, mut cx) = setup_indent_guides_editor(
15296 &"
15297 fn main() {
15298 let a = 1;
15299 }"
15300 .unindent(),
15301 cx,
15302 )
15303 .await;
15304
15305 cx.update_editor(|editor, window, cx| {
15306 editor.change_selections(None, window, cx, |s| {
15307 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15308 });
15309 });
15310
15311 assert_indent_guides(
15312 0..3,
15313 vec![indent_guide(buffer_id, 1, 1, 0)],
15314 Some(vec![0]),
15315 &mut cx,
15316 );
15317}
15318
15319#[gpui::test]
15320async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
15321 let (buffer_id, mut cx) = setup_indent_guides_editor(
15322 &"
15323 fn main() {
15324 if 1 == 2 {
15325 let a = 1;
15326 }
15327 }"
15328 .unindent(),
15329 cx,
15330 )
15331 .await;
15332
15333 cx.update_editor(|editor, window, cx| {
15334 editor.change_selections(None, window, cx, |s| {
15335 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15336 });
15337 });
15338
15339 assert_indent_guides(
15340 0..4,
15341 vec![
15342 indent_guide(buffer_id, 1, 3, 0),
15343 indent_guide(buffer_id, 2, 2, 1),
15344 ],
15345 Some(vec![1]),
15346 &mut cx,
15347 );
15348
15349 cx.update_editor(|editor, window, cx| {
15350 editor.change_selections(None, window, cx, |s| {
15351 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15352 });
15353 });
15354
15355 assert_indent_guides(
15356 0..4,
15357 vec![
15358 indent_guide(buffer_id, 1, 3, 0),
15359 indent_guide(buffer_id, 2, 2, 1),
15360 ],
15361 Some(vec![1]),
15362 &mut cx,
15363 );
15364
15365 cx.update_editor(|editor, window, cx| {
15366 editor.change_selections(None, window, cx, |s| {
15367 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
15368 });
15369 });
15370
15371 assert_indent_guides(
15372 0..4,
15373 vec![
15374 indent_guide(buffer_id, 1, 3, 0),
15375 indent_guide(buffer_id, 2, 2, 1),
15376 ],
15377 Some(vec![0]),
15378 &mut cx,
15379 );
15380}
15381
15382#[gpui::test]
15383async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
15384 let (buffer_id, mut cx) = setup_indent_guides_editor(
15385 &"
15386 fn main() {
15387 let a = 1;
15388
15389 let b = 2;
15390 }"
15391 .unindent(),
15392 cx,
15393 )
15394 .await;
15395
15396 cx.update_editor(|editor, window, cx| {
15397 editor.change_selections(None, window, cx, |s| {
15398 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15399 });
15400 });
15401
15402 assert_indent_guides(
15403 0..5,
15404 vec![indent_guide(buffer_id, 1, 3, 0)],
15405 Some(vec![0]),
15406 &mut cx,
15407 );
15408}
15409
15410#[gpui::test]
15411async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
15412 let (buffer_id, mut cx) = setup_indent_guides_editor(
15413 &"
15414 def m:
15415 a = 1
15416 pass"
15417 .unindent(),
15418 cx,
15419 )
15420 .await;
15421
15422 cx.update_editor(|editor, window, cx| {
15423 editor.change_selections(None, window, cx, |s| {
15424 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15425 });
15426 });
15427
15428 assert_indent_guides(
15429 0..3,
15430 vec![indent_guide(buffer_id, 1, 2, 0)],
15431 Some(vec![0]),
15432 &mut cx,
15433 );
15434}
15435
15436#[gpui::test]
15437async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
15438 init_test(cx, |_| {});
15439 let mut cx = EditorTestContext::new(cx).await;
15440 let text = indoc! {
15441 "
15442 impl A {
15443 fn b() {
15444 0;
15445 3;
15446 5;
15447 6;
15448 7;
15449 }
15450 }
15451 "
15452 };
15453 let base_text = indoc! {
15454 "
15455 impl A {
15456 fn b() {
15457 0;
15458 1;
15459 2;
15460 3;
15461 4;
15462 }
15463 fn c() {
15464 5;
15465 6;
15466 7;
15467 }
15468 }
15469 "
15470 };
15471
15472 cx.update_editor(|editor, window, cx| {
15473 editor.set_text(text, window, cx);
15474
15475 editor.buffer().update(cx, |multibuffer, cx| {
15476 let buffer = multibuffer.as_singleton().unwrap();
15477 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
15478
15479 multibuffer.set_all_diff_hunks_expanded(cx);
15480 multibuffer.add_diff(diff, cx);
15481
15482 buffer.read(cx).remote_id()
15483 })
15484 });
15485 cx.run_until_parked();
15486
15487 cx.assert_state_with_diff(
15488 indoc! { "
15489 impl A {
15490 fn b() {
15491 0;
15492 - 1;
15493 - 2;
15494 3;
15495 - 4;
15496 - }
15497 - fn c() {
15498 5;
15499 6;
15500 7;
15501 }
15502 }
15503 ˇ"
15504 }
15505 .to_string(),
15506 );
15507
15508 let mut actual_guides = cx.update_editor(|editor, window, cx| {
15509 editor
15510 .snapshot(window, cx)
15511 .buffer_snapshot
15512 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
15513 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
15514 .collect::<Vec<_>>()
15515 });
15516 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
15517 assert_eq!(
15518 actual_guides,
15519 vec![
15520 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
15521 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
15522 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
15523 ]
15524 );
15525}
15526
15527#[gpui::test]
15528async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15529 init_test(cx, |_| {});
15530 let mut cx = EditorTestContext::new(cx).await;
15531
15532 let diff_base = r#"
15533 a
15534 b
15535 c
15536 "#
15537 .unindent();
15538
15539 cx.set_state(
15540 &r#"
15541 ˇA
15542 b
15543 C
15544 "#
15545 .unindent(),
15546 );
15547 cx.set_head_text(&diff_base);
15548 cx.update_editor(|editor, window, cx| {
15549 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15550 });
15551 executor.run_until_parked();
15552
15553 let both_hunks_expanded = r#"
15554 - a
15555 + ˇA
15556 b
15557 - c
15558 + C
15559 "#
15560 .unindent();
15561
15562 cx.assert_state_with_diff(both_hunks_expanded.clone());
15563
15564 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15565 let snapshot = editor.snapshot(window, cx);
15566 let hunks = editor
15567 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15568 .collect::<Vec<_>>();
15569 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15570 let buffer_id = hunks[0].buffer_id;
15571 hunks
15572 .into_iter()
15573 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15574 .collect::<Vec<_>>()
15575 });
15576 assert_eq!(hunk_ranges.len(), 2);
15577
15578 cx.update_editor(|editor, _, cx| {
15579 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15580 });
15581 executor.run_until_parked();
15582
15583 let second_hunk_expanded = r#"
15584 ˇA
15585 b
15586 - c
15587 + C
15588 "#
15589 .unindent();
15590
15591 cx.assert_state_with_diff(second_hunk_expanded);
15592
15593 cx.update_editor(|editor, _, cx| {
15594 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15595 });
15596 executor.run_until_parked();
15597
15598 cx.assert_state_with_diff(both_hunks_expanded.clone());
15599
15600 cx.update_editor(|editor, _, cx| {
15601 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15602 });
15603 executor.run_until_parked();
15604
15605 let first_hunk_expanded = r#"
15606 - a
15607 + ˇA
15608 b
15609 C
15610 "#
15611 .unindent();
15612
15613 cx.assert_state_with_diff(first_hunk_expanded);
15614
15615 cx.update_editor(|editor, _, cx| {
15616 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15617 });
15618 executor.run_until_parked();
15619
15620 cx.assert_state_with_diff(both_hunks_expanded);
15621
15622 cx.set_state(
15623 &r#"
15624 ˇA
15625 b
15626 "#
15627 .unindent(),
15628 );
15629 cx.run_until_parked();
15630
15631 // TODO this cursor position seems bad
15632 cx.assert_state_with_diff(
15633 r#"
15634 - ˇa
15635 + A
15636 b
15637 "#
15638 .unindent(),
15639 );
15640
15641 cx.update_editor(|editor, window, cx| {
15642 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15643 });
15644
15645 cx.assert_state_with_diff(
15646 r#"
15647 - ˇa
15648 + A
15649 b
15650 - c
15651 "#
15652 .unindent(),
15653 );
15654
15655 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15656 let snapshot = editor.snapshot(window, cx);
15657 let hunks = editor
15658 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15659 .collect::<Vec<_>>();
15660 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15661 let buffer_id = hunks[0].buffer_id;
15662 hunks
15663 .into_iter()
15664 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15665 .collect::<Vec<_>>()
15666 });
15667 assert_eq!(hunk_ranges.len(), 2);
15668
15669 cx.update_editor(|editor, _, cx| {
15670 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15671 });
15672 executor.run_until_parked();
15673
15674 cx.assert_state_with_diff(
15675 r#"
15676 - ˇa
15677 + A
15678 b
15679 "#
15680 .unindent(),
15681 );
15682}
15683
15684#[gpui::test]
15685async fn test_toggle_deletion_hunk_at_start_of_file(
15686 executor: BackgroundExecutor,
15687 cx: &mut TestAppContext,
15688) {
15689 init_test(cx, |_| {});
15690 let mut cx = EditorTestContext::new(cx).await;
15691
15692 let diff_base = r#"
15693 a
15694 b
15695 c
15696 "#
15697 .unindent();
15698
15699 cx.set_state(
15700 &r#"
15701 ˇb
15702 c
15703 "#
15704 .unindent(),
15705 );
15706 cx.set_head_text(&diff_base);
15707 cx.update_editor(|editor, window, cx| {
15708 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15709 });
15710 executor.run_until_parked();
15711
15712 let hunk_expanded = r#"
15713 - a
15714 ˇb
15715 c
15716 "#
15717 .unindent();
15718
15719 cx.assert_state_with_diff(hunk_expanded.clone());
15720
15721 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15722 let snapshot = editor.snapshot(window, cx);
15723 let hunks = editor
15724 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15725 .collect::<Vec<_>>();
15726 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15727 let buffer_id = hunks[0].buffer_id;
15728 hunks
15729 .into_iter()
15730 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15731 .collect::<Vec<_>>()
15732 });
15733 assert_eq!(hunk_ranges.len(), 1);
15734
15735 cx.update_editor(|editor, _, cx| {
15736 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15737 });
15738 executor.run_until_parked();
15739
15740 let hunk_collapsed = r#"
15741 ˇb
15742 c
15743 "#
15744 .unindent();
15745
15746 cx.assert_state_with_diff(hunk_collapsed);
15747
15748 cx.update_editor(|editor, _, cx| {
15749 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15750 });
15751 executor.run_until_parked();
15752
15753 cx.assert_state_with_diff(hunk_expanded.clone());
15754}
15755
15756#[gpui::test]
15757async fn test_display_diff_hunks(cx: &mut TestAppContext) {
15758 init_test(cx, |_| {});
15759
15760 let fs = FakeFs::new(cx.executor());
15761 fs.insert_tree(
15762 path!("/test"),
15763 json!({
15764 ".git": {},
15765 "file-1": "ONE\n",
15766 "file-2": "TWO\n",
15767 "file-3": "THREE\n",
15768 }),
15769 )
15770 .await;
15771
15772 fs.set_head_for_repo(
15773 path!("/test/.git").as_ref(),
15774 &[
15775 ("file-1".into(), "one\n".into()),
15776 ("file-2".into(), "two\n".into()),
15777 ("file-3".into(), "three\n".into()),
15778 ],
15779 );
15780
15781 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
15782 let mut buffers = vec![];
15783 for i in 1..=3 {
15784 let buffer = project
15785 .update(cx, |project, cx| {
15786 let path = format!(path!("/test/file-{}"), i);
15787 project.open_local_buffer(path, cx)
15788 })
15789 .await
15790 .unwrap();
15791 buffers.push(buffer);
15792 }
15793
15794 let multibuffer = cx.new(|cx| {
15795 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
15796 multibuffer.set_all_diff_hunks_expanded(cx);
15797 for buffer in &buffers {
15798 let snapshot = buffer.read(cx).snapshot();
15799 multibuffer.set_excerpts_for_path(
15800 PathKey::namespaced("", buffer.read(cx).file().unwrap().path().clone()),
15801 buffer.clone(),
15802 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
15803 DEFAULT_MULTIBUFFER_CONTEXT,
15804 cx,
15805 );
15806 }
15807 multibuffer
15808 });
15809
15810 let editor = cx.add_window(|window, cx| {
15811 Editor::new(EditorMode::Full, multibuffer, Some(project), window, cx)
15812 });
15813 cx.run_until_parked();
15814
15815 let snapshot = editor
15816 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
15817 .unwrap();
15818 let hunks = snapshot
15819 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
15820 .map(|hunk| match hunk {
15821 DisplayDiffHunk::Unfolded {
15822 display_row_range, ..
15823 } => display_row_range,
15824 DisplayDiffHunk::Folded { .. } => unreachable!(),
15825 })
15826 .collect::<Vec<_>>();
15827 assert_eq!(
15828 hunks,
15829 [
15830 DisplayRow(2)..DisplayRow(4),
15831 DisplayRow(7)..DisplayRow(9),
15832 DisplayRow(12)..DisplayRow(14),
15833 ]
15834 );
15835}
15836
15837#[gpui::test]
15838async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
15839 init_test(cx, |_| {});
15840
15841 let mut cx = EditorTestContext::new(cx).await;
15842 cx.set_head_text(indoc! { "
15843 one
15844 two
15845 three
15846 four
15847 five
15848 "
15849 });
15850 cx.set_index_text(indoc! { "
15851 one
15852 two
15853 three
15854 four
15855 five
15856 "
15857 });
15858 cx.set_state(indoc! {"
15859 one
15860 TWO
15861 ˇTHREE
15862 FOUR
15863 five
15864 "});
15865 cx.run_until_parked();
15866 cx.update_editor(|editor, window, cx| {
15867 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15868 });
15869 cx.run_until_parked();
15870 cx.assert_index_text(Some(indoc! {"
15871 one
15872 TWO
15873 THREE
15874 FOUR
15875 five
15876 "}));
15877 cx.set_state(indoc! { "
15878 one
15879 TWO
15880 ˇTHREE-HUNDRED
15881 FOUR
15882 five
15883 "});
15884 cx.run_until_parked();
15885 cx.update_editor(|editor, window, cx| {
15886 let snapshot = editor.snapshot(window, cx);
15887 let hunks = editor
15888 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15889 .collect::<Vec<_>>();
15890 assert_eq!(hunks.len(), 1);
15891 assert_eq!(
15892 hunks[0].status(),
15893 DiffHunkStatus {
15894 kind: DiffHunkStatusKind::Modified,
15895 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
15896 }
15897 );
15898
15899 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15900 });
15901 cx.run_until_parked();
15902 cx.assert_index_text(Some(indoc! {"
15903 one
15904 TWO
15905 THREE-HUNDRED
15906 FOUR
15907 five
15908 "}));
15909}
15910
15911#[gpui::test]
15912fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
15913 init_test(cx, |_| {});
15914
15915 let editor = cx.add_window(|window, cx| {
15916 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
15917 build_editor(buffer, window, cx)
15918 });
15919
15920 let render_args = Arc::new(Mutex::new(None));
15921 let snapshot = editor
15922 .update(cx, |editor, window, cx| {
15923 let snapshot = editor.buffer().read(cx).snapshot(cx);
15924 let range =
15925 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
15926
15927 struct RenderArgs {
15928 row: MultiBufferRow,
15929 folded: bool,
15930 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
15931 }
15932
15933 let crease = Crease::inline(
15934 range,
15935 FoldPlaceholder::test(),
15936 {
15937 let toggle_callback = render_args.clone();
15938 move |row, folded, callback, _window, _cx| {
15939 *toggle_callback.lock() = Some(RenderArgs {
15940 row,
15941 folded,
15942 callback,
15943 });
15944 div()
15945 }
15946 },
15947 |_row, _folded, _window, _cx| div(),
15948 );
15949
15950 editor.insert_creases(Some(crease), cx);
15951 let snapshot = editor.snapshot(window, cx);
15952 let _div = snapshot.render_crease_toggle(
15953 MultiBufferRow(1),
15954 false,
15955 cx.entity().clone(),
15956 window,
15957 cx,
15958 );
15959 snapshot
15960 })
15961 .unwrap();
15962
15963 let render_args = render_args.lock().take().unwrap();
15964 assert_eq!(render_args.row, MultiBufferRow(1));
15965 assert!(!render_args.folded);
15966 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
15967
15968 cx.update_window(*editor, |_, window, cx| {
15969 (render_args.callback)(true, window, cx)
15970 })
15971 .unwrap();
15972 let snapshot = editor
15973 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
15974 .unwrap();
15975 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
15976
15977 cx.update_window(*editor, |_, window, cx| {
15978 (render_args.callback)(false, window, cx)
15979 })
15980 .unwrap();
15981 let snapshot = editor
15982 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
15983 .unwrap();
15984 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
15985}
15986
15987#[gpui::test]
15988async fn test_input_text(cx: &mut TestAppContext) {
15989 init_test(cx, |_| {});
15990 let mut cx = EditorTestContext::new(cx).await;
15991
15992 cx.set_state(
15993 &r#"ˇone
15994 two
15995
15996 three
15997 fourˇ
15998 five
15999
16000 siˇx"#
16001 .unindent(),
16002 );
16003
16004 cx.dispatch_action(HandleInput(String::new()));
16005 cx.assert_editor_state(
16006 &r#"ˇone
16007 two
16008
16009 three
16010 fourˇ
16011 five
16012
16013 siˇx"#
16014 .unindent(),
16015 );
16016
16017 cx.dispatch_action(HandleInput("AAAA".to_string()));
16018 cx.assert_editor_state(
16019 &r#"AAAAˇone
16020 two
16021
16022 three
16023 fourAAAAˇ
16024 five
16025
16026 siAAAAˇx"#
16027 .unindent(),
16028 );
16029}
16030
16031#[gpui::test]
16032async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
16033 init_test(cx, |_| {});
16034
16035 let mut cx = EditorTestContext::new(cx).await;
16036 cx.set_state(
16037 r#"let foo = 1;
16038let foo = 2;
16039let foo = 3;
16040let fooˇ = 4;
16041let foo = 5;
16042let foo = 6;
16043let foo = 7;
16044let foo = 8;
16045let foo = 9;
16046let foo = 10;
16047let foo = 11;
16048let foo = 12;
16049let foo = 13;
16050let foo = 14;
16051let foo = 15;"#,
16052 );
16053
16054 cx.update_editor(|e, window, cx| {
16055 assert_eq!(
16056 e.next_scroll_position,
16057 NextScrollCursorCenterTopBottom::Center,
16058 "Default next scroll direction is center",
16059 );
16060
16061 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16062 assert_eq!(
16063 e.next_scroll_position,
16064 NextScrollCursorCenterTopBottom::Top,
16065 "After center, next scroll direction should be top",
16066 );
16067
16068 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16069 assert_eq!(
16070 e.next_scroll_position,
16071 NextScrollCursorCenterTopBottom::Bottom,
16072 "After top, next scroll direction should be bottom",
16073 );
16074
16075 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16076 assert_eq!(
16077 e.next_scroll_position,
16078 NextScrollCursorCenterTopBottom::Center,
16079 "After bottom, scrolling should start over",
16080 );
16081
16082 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16083 assert_eq!(
16084 e.next_scroll_position,
16085 NextScrollCursorCenterTopBottom::Top,
16086 "Scrolling continues if retriggered fast enough"
16087 );
16088 });
16089
16090 cx.executor()
16091 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
16092 cx.executor().run_until_parked();
16093 cx.update_editor(|e, _, _| {
16094 assert_eq!(
16095 e.next_scroll_position,
16096 NextScrollCursorCenterTopBottom::Center,
16097 "If scrolling is not triggered fast enough, it should reset"
16098 );
16099 });
16100}
16101
16102#[gpui::test]
16103async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
16104 init_test(cx, |_| {});
16105 let mut cx = EditorLspTestContext::new_rust(
16106 lsp::ServerCapabilities {
16107 definition_provider: Some(lsp::OneOf::Left(true)),
16108 references_provider: Some(lsp::OneOf::Left(true)),
16109 ..lsp::ServerCapabilities::default()
16110 },
16111 cx,
16112 )
16113 .await;
16114
16115 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
16116 let go_to_definition = cx.lsp.handle_request::<lsp::request::GotoDefinition, _, _>(
16117 move |params, _| async move {
16118 if empty_go_to_definition {
16119 Ok(None)
16120 } else {
16121 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
16122 uri: params.text_document_position_params.text_document.uri,
16123 range: lsp::Range::new(lsp::Position::new(4, 3), lsp::Position::new(4, 6)),
16124 })))
16125 }
16126 },
16127 );
16128 let references =
16129 cx.lsp
16130 .handle_request::<lsp::request::References, _, _>(move |params, _| async move {
16131 Ok(Some(vec![lsp::Location {
16132 uri: params.text_document_position.text_document.uri,
16133 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
16134 }]))
16135 });
16136 (go_to_definition, references)
16137 };
16138
16139 cx.set_state(
16140 &r#"fn one() {
16141 let mut a = ˇtwo();
16142 }
16143
16144 fn two() {}"#
16145 .unindent(),
16146 );
16147 set_up_lsp_handlers(false, &mut cx);
16148 let navigated = cx
16149 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16150 .await
16151 .expect("Failed to navigate to definition");
16152 assert_eq!(
16153 navigated,
16154 Navigated::Yes,
16155 "Should have navigated to definition from the GetDefinition response"
16156 );
16157 cx.assert_editor_state(
16158 &r#"fn one() {
16159 let mut a = two();
16160 }
16161
16162 fn «twoˇ»() {}"#
16163 .unindent(),
16164 );
16165
16166 let editors = cx.update_workspace(|workspace, _, cx| {
16167 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16168 });
16169 cx.update_editor(|_, _, test_editor_cx| {
16170 assert_eq!(
16171 editors.len(),
16172 1,
16173 "Initially, only one, test, editor should be open in the workspace"
16174 );
16175 assert_eq!(
16176 test_editor_cx.entity(),
16177 editors.last().expect("Asserted len is 1").clone()
16178 );
16179 });
16180
16181 set_up_lsp_handlers(true, &mut cx);
16182 let navigated = cx
16183 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16184 .await
16185 .expect("Failed to navigate to lookup references");
16186 assert_eq!(
16187 navigated,
16188 Navigated::Yes,
16189 "Should have navigated to references as a fallback after empty GoToDefinition response"
16190 );
16191 // We should not change the selections in the existing file,
16192 // if opening another milti buffer with the references
16193 cx.assert_editor_state(
16194 &r#"fn one() {
16195 let mut a = two();
16196 }
16197
16198 fn «twoˇ»() {}"#
16199 .unindent(),
16200 );
16201 let editors = cx.update_workspace(|workspace, _, cx| {
16202 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16203 });
16204 cx.update_editor(|_, _, test_editor_cx| {
16205 assert_eq!(
16206 editors.len(),
16207 2,
16208 "After falling back to references search, we open a new editor with the results"
16209 );
16210 let references_fallback_text = editors
16211 .into_iter()
16212 .find(|new_editor| *new_editor != test_editor_cx.entity())
16213 .expect("Should have one non-test editor now")
16214 .read(test_editor_cx)
16215 .text(test_editor_cx);
16216 assert_eq!(
16217 references_fallback_text, "fn one() {\n let mut a = two();\n}",
16218 "Should use the range from the references response and not the GoToDefinition one"
16219 );
16220 });
16221}
16222
16223#[gpui::test]
16224async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
16225 init_test(cx, |_| {});
16226
16227 let language = Arc::new(Language::new(
16228 LanguageConfig::default(),
16229 Some(tree_sitter_rust::LANGUAGE.into()),
16230 ));
16231
16232 let text = r#"
16233 #[cfg(test)]
16234 mod tests() {
16235 #[test]
16236 fn runnable_1() {
16237 let a = 1;
16238 }
16239
16240 #[test]
16241 fn runnable_2() {
16242 let a = 1;
16243 let b = 2;
16244 }
16245 }
16246 "#
16247 .unindent();
16248
16249 let fs = FakeFs::new(cx.executor());
16250 fs.insert_file("/file.rs", Default::default()).await;
16251
16252 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16253 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16254 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16255 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16256 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
16257
16258 let editor = cx.new_window_entity(|window, cx| {
16259 Editor::new(
16260 EditorMode::Full,
16261 multi_buffer,
16262 Some(project.clone()),
16263 window,
16264 cx,
16265 )
16266 });
16267
16268 editor.update_in(cx, |editor, window, cx| {
16269 let snapshot = editor.buffer().read(cx).snapshot(cx);
16270 editor.tasks.insert(
16271 (buffer.read(cx).remote_id(), 3),
16272 RunnableTasks {
16273 templates: vec![],
16274 offset: snapshot.anchor_before(43),
16275 column: 0,
16276 extra_variables: HashMap::default(),
16277 context_range: BufferOffset(43)..BufferOffset(85),
16278 },
16279 );
16280 editor.tasks.insert(
16281 (buffer.read(cx).remote_id(), 8),
16282 RunnableTasks {
16283 templates: vec![],
16284 offset: snapshot.anchor_before(86),
16285 column: 0,
16286 extra_variables: HashMap::default(),
16287 context_range: BufferOffset(86)..BufferOffset(191),
16288 },
16289 );
16290
16291 // Test finding task when cursor is inside function body
16292 editor.change_selections(None, window, cx, |s| {
16293 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
16294 });
16295 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16296 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
16297
16298 // Test finding task when cursor is on function name
16299 editor.change_selections(None, window, cx, |s| {
16300 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
16301 });
16302 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16303 assert_eq!(row, 8, "Should find task when cursor is on function name");
16304 });
16305}
16306
16307#[gpui::test]
16308async fn test_folding_buffers(cx: &mut TestAppContext) {
16309 init_test(cx, |_| {});
16310
16311 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
16312 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
16313 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
16314
16315 let fs = FakeFs::new(cx.executor());
16316 fs.insert_tree(
16317 path!("/a"),
16318 json!({
16319 "first.rs": sample_text_1,
16320 "second.rs": sample_text_2,
16321 "third.rs": sample_text_3,
16322 }),
16323 )
16324 .await;
16325 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16326 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16327 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16328 let worktree = project.update(cx, |project, cx| {
16329 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16330 assert_eq!(worktrees.len(), 1);
16331 worktrees.pop().unwrap()
16332 });
16333 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16334
16335 let buffer_1 = project
16336 .update(cx, |project, cx| {
16337 project.open_buffer((worktree_id, "first.rs"), cx)
16338 })
16339 .await
16340 .unwrap();
16341 let buffer_2 = project
16342 .update(cx, |project, cx| {
16343 project.open_buffer((worktree_id, "second.rs"), cx)
16344 })
16345 .await
16346 .unwrap();
16347 let buffer_3 = project
16348 .update(cx, |project, cx| {
16349 project.open_buffer((worktree_id, "third.rs"), cx)
16350 })
16351 .await
16352 .unwrap();
16353
16354 let multi_buffer = cx.new(|cx| {
16355 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16356 multi_buffer.push_excerpts(
16357 buffer_1.clone(),
16358 [
16359 ExcerptRange {
16360 context: Point::new(0, 0)..Point::new(3, 0),
16361 primary: None,
16362 },
16363 ExcerptRange {
16364 context: Point::new(5, 0)..Point::new(7, 0),
16365 primary: None,
16366 },
16367 ExcerptRange {
16368 context: Point::new(9, 0)..Point::new(10, 4),
16369 primary: None,
16370 },
16371 ],
16372 cx,
16373 );
16374 multi_buffer.push_excerpts(
16375 buffer_2.clone(),
16376 [
16377 ExcerptRange {
16378 context: Point::new(0, 0)..Point::new(3, 0),
16379 primary: None,
16380 },
16381 ExcerptRange {
16382 context: Point::new(5, 0)..Point::new(7, 0),
16383 primary: None,
16384 },
16385 ExcerptRange {
16386 context: Point::new(9, 0)..Point::new(10, 4),
16387 primary: None,
16388 },
16389 ],
16390 cx,
16391 );
16392 multi_buffer.push_excerpts(
16393 buffer_3.clone(),
16394 [
16395 ExcerptRange {
16396 context: Point::new(0, 0)..Point::new(3, 0),
16397 primary: None,
16398 },
16399 ExcerptRange {
16400 context: Point::new(5, 0)..Point::new(7, 0),
16401 primary: None,
16402 },
16403 ExcerptRange {
16404 context: Point::new(9, 0)..Point::new(10, 4),
16405 primary: None,
16406 },
16407 ],
16408 cx,
16409 );
16410 multi_buffer
16411 });
16412 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16413 Editor::new(
16414 EditorMode::Full,
16415 multi_buffer.clone(),
16416 Some(project.clone()),
16417 window,
16418 cx,
16419 )
16420 });
16421
16422 assert_eq!(
16423 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16424 "\n\naaaa\nbbbb\ncccc\n\n\nffff\ngggg\n\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16425 );
16426
16427 multi_buffer_editor.update(cx, |editor, cx| {
16428 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
16429 });
16430 assert_eq!(
16431 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16432 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16433 "After folding the first buffer, its text should not be displayed"
16434 );
16435
16436 multi_buffer_editor.update(cx, |editor, cx| {
16437 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
16438 });
16439 assert_eq!(
16440 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16441 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16442 "After folding the second buffer, its text should not be displayed"
16443 );
16444
16445 multi_buffer_editor.update(cx, |editor, cx| {
16446 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
16447 });
16448 assert_eq!(
16449 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16450 "\n\n\n\n\n",
16451 "After folding the third buffer, its text should not be displayed"
16452 );
16453
16454 // Emulate selection inside the fold logic, that should work
16455 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16456 editor
16457 .snapshot(window, cx)
16458 .next_line_boundary(Point::new(0, 4));
16459 });
16460
16461 multi_buffer_editor.update(cx, |editor, cx| {
16462 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
16463 });
16464 assert_eq!(
16465 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16466 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
16467 "After unfolding the second buffer, its text should be displayed"
16468 );
16469
16470 // Typing inside of buffer 1 causes that buffer to be unfolded.
16471 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16472 assert_eq!(
16473 multi_buffer
16474 .read(cx)
16475 .snapshot(cx)
16476 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
16477 .collect::<String>(),
16478 "bbbb"
16479 );
16480 editor.change_selections(None, window, cx, |selections| {
16481 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
16482 });
16483 editor.handle_input("B", window, cx);
16484 });
16485
16486 assert_eq!(
16487 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16488 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
16489 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
16490 );
16491
16492 multi_buffer_editor.update(cx, |editor, cx| {
16493 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
16494 });
16495 assert_eq!(
16496 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16497 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16498 "After unfolding the all buffers, all original text should be displayed"
16499 );
16500}
16501
16502#[gpui::test]
16503async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
16504 init_test(cx, |_| {});
16505
16506 let sample_text_1 = "1111\n2222\n3333".to_string();
16507 let sample_text_2 = "4444\n5555\n6666".to_string();
16508 let sample_text_3 = "7777\n8888\n9999".to_string();
16509
16510 let fs = FakeFs::new(cx.executor());
16511 fs.insert_tree(
16512 path!("/a"),
16513 json!({
16514 "first.rs": sample_text_1,
16515 "second.rs": sample_text_2,
16516 "third.rs": sample_text_3,
16517 }),
16518 )
16519 .await;
16520 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16521 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16522 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16523 let worktree = project.update(cx, |project, cx| {
16524 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16525 assert_eq!(worktrees.len(), 1);
16526 worktrees.pop().unwrap()
16527 });
16528 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16529
16530 let buffer_1 = project
16531 .update(cx, |project, cx| {
16532 project.open_buffer((worktree_id, "first.rs"), cx)
16533 })
16534 .await
16535 .unwrap();
16536 let buffer_2 = project
16537 .update(cx, |project, cx| {
16538 project.open_buffer((worktree_id, "second.rs"), cx)
16539 })
16540 .await
16541 .unwrap();
16542 let buffer_3 = project
16543 .update(cx, |project, cx| {
16544 project.open_buffer((worktree_id, "third.rs"), cx)
16545 })
16546 .await
16547 .unwrap();
16548
16549 let multi_buffer = cx.new(|cx| {
16550 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16551 multi_buffer.push_excerpts(
16552 buffer_1.clone(),
16553 [ExcerptRange {
16554 context: Point::new(0, 0)..Point::new(3, 0),
16555 primary: None,
16556 }],
16557 cx,
16558 );
16559 multi_buffer.push_excerpts(
16560 buffer_2.clone(),
16561 [ExcerptRange {
16562 context: Point::new(0, 0)..Point::new(3, 0),
16563 primary: None,
16564 }],
16565 cx,
16566 );
16567 multi_buffer.push_excerpts(
16568 buffer_3.clone(),
16569 [ExcerptRange {
16570 context: Point::new(0, 0)..Point::new(3, 0),
16571 primary: None,
16572 }],
16573 cx,
16574 );
16575 multi_buffer
16576 });
16577
16578 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16579 Editor::new(
16580 EditorMode::Full,
16581 multi_buffer,
16582 Some(project.clone()),
16583 window,
16584 cx,
16585 )
16586 });
16587
16588 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
16589 assert_eq!(
16590 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16591 full_text,
16592 );
16593
16594 multi_buffer_editor.update(cx, |editor, cx| {
16595 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
16596 });
16597 assert_eq!(
16598 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16599 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
16600 "After folding the first buffer, its text should not be displayed"
16601 );
16602
16603 multi_buffer_editor.update(cx, |editor, cx| {
16604 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
16605 });
16606
16607 assert_eq!(
16608 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16609 "\n\n\n\n\n\n7777\n8888\n9999",
16610 "After folding the second buffer, its text should not be displayed"
16611 );
16612
16613 multi_buffer_editor.update(cx, |editor, cx| {
16614 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
16615 });
16616 assert_eq!(
16617 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16618 "\n\n\n\n\n",
16619 "After folding the third buffer, its text should not be displayed"
16620 );
16621
16622 multi_buffer_editor.update(cx, |editor, cx| {
16623 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
16624 });
16625 assert_eq!(
16626 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16627 "\n\n\n\n4444\n5555\n6666\n\n",
16628 "After unfolding the second buffer, its text should be displayed"
16629 );
16630
16631 multi_buffer_editor.update(cx, |editor, cx| {
16632 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
16633 });
16634 assert_eq!(
16635 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16636 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
16637 "After unfolding the first buffer, its text should be displayed"
16638 );
16639
16640 multi_buffer_editor.update(cx, |editor, cx| {
16641 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
16642 });
16643 assert_eq!(
16644 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16645 full_text,
16646 "After unfolding all buffers, all original text should be displayed"
16647 );
16648}
16649
16650#[gpui::test]
16651async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
16652 init_test(cx, |_| {});
16653
16654 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
16655
16656 let fs = FakeFs::new(cx.executor());
16657 fs.insert_tree(
16658 path!("/a"),
16659 json!({
16660 "main.rs": sample_text,
16661 }),
16662 )
16663 .await;
16664 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16665 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16666 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16667 let worktree = project.update(cx, |project, cx| {
16668 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16669 assert_eq!(worktrees.len(), 1);
16670 worktrees.pop().unwrap()
16671 });
16672 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16673
16674 let buffer_1 = project
16675 .update(cx, |project, cx| {
16676 project.open_buffer((worktree_id, "main.rs"), cx)
16677 })
16678 .await
16679 .unwrap();
16680
16681 let multi_buffer = cx.new(|cx| {
16682 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16683 multi_buffer.push_excerpts(
16684 buffer_1.clone(),
16685 [ExcerptRange {
16686 context: Point::new(0, 0)
16687 ..Point::new(
16688 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
16689 0,
16690 ),
16691 primary: None,
16692 }],
16693 cx,
16694 );
16695 multi_buffer
16696 });
16697 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16698 Editor::new(
16699 EditorMode::Full,
16700 multi_buffer,
16701 Some(project.clone()),
16702 window,
16703 cx,
16704 )
16705 });
16706
16707 let selection_range = Point::new(1, 0)..Point::new(2, 0);
16708 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16709 enum TestHighlight {}
16710 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
16711 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
16712 editor.highlight_text::<TestHighlight>(
16713 vec![highlight_range.clone()],
16714 HighlightStyle::color(Hsla::green()),
16715 cx,
16716 );
16717 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
16718 });
16719
16720 let full_text = format!("\n\n{sample_text}");
16721 assert_eq!(
16722 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16723 full_text,
16724 );
16725}
16726
16727#[gpui::test]
16728async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
16729 init_test(cx, |_| {});
16730 cx.update(|cx| {
16731 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
16732 "keymaps/default-linux.json",
16733 cx,
16734 )
16735 .unwrap();
16736 cx.bind_keys(default_key_bindings);
16737 });
16738
16739 let (editor, cx) = cx.add_window_view(|window, cx| {
16740 let multi_buffer = MultiBuffer::build_multi(
16741 [
16742 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
16743 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
16744 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
16745 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
16746 ],
16747 cx,
16748 );
16749 let mut editor = Editor::new(EditorMode::Full, multi_buffer.clone(), None, window, cx);
16750
16751 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
16752 // fold all but the second buffer, so that we test navigating between two
16753 // adjacent folded buffers, as well as folded buffers at the start and
16754 // end the multibuffer
16755 editor.fold_buffer(buffer_ids[0], cx);
16756 editor.fold_buffer(buffer_ids[2], cx);
16757 editor.fold_buffer(buffer_ids[3], cx);
16758
16759 editor
16760 });
16761 cx.simulate_resize(size(px(1000.), px(1000.)));
16762
16763 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
16764 cx.assert_excerpts_with_selections(indoc! {"
16765 [EXCERPT]
16766 ˇ[FOLDED]
16767 [EXCERPT]
16768 a1
16769 b1
16770 [EXCERPT]
16771 [FOLDED]
16772 [EXCERPT]
16773 [FOLDED]
16774 "
16775 });
16776 cx.simulate_keystroke("down");
16777 cx.assert_excerpts_with_selections(indoc! {"
16778 [EXCERPT]
16779 [FOLDED]
16780 [EXCERPT]
16781 ˇa1
16782 b1
16783 [EXCERPT]
16784 [FOLDED]
16785 [EXCERPT]
16786 [FOLDED]
16787 "
16788 });
16789 cx.simulate_keystroke("down");
16790 cx.assert_excerpts_with_selections(indoc! {"
16791 [EXCERPT]
16792 [FOLDED]
16793 [EXCERPT]
16794 a1
16795 ˇb1
16796 [EXCERPT]
16797 [FOLDED]
16798 [EXCERPT]
16799 [FOLDED]
16800 "
16801 });
16802 cx.simulate_keystroke("down");
16803 cx.assert_excerpts_with_selections(indoc! {"
16804 [EXCERPT]
16805 [FOLDED]
16806 [EXCERPT]
16807 a1
16808 b1
16809 ˇ[EXCERPT]
16810 [FOLDED]
16811 [EXCERPT]
16812 [FOLDED]
16813 "
16814 });
16815 cx.simulate_keystroke("down");
16816 cx.assert_excerpts_with_selections(indoc! {"
16817 [EXCERPT]
16818 [FOLDED]
16819 [EXCERPT]
16820 a1
16821 b1
16822 [EXCERPT]
16823 ˇ[FOLDED]
16824 [EXCERPT]
16825 [FOLDED]
16826 "
16827 });
16828 for _ in 0..5 {
16829 cx.simulate_keystroke("down");
16830 cx.assert_excerpts_with_selections(indoc! {"
16831 [EXCERPT]
16832 [FOLDED]
16833 [EXCERPT]
16834 a1
16835 b1
16836 [EXCERPT]
16837 [FOLDED]
16838 [EXCERPT]
16839 ˇ[FOLDED]
16840 "
16841 });
16842 }
16843
16844 cx.simulate_keystroke("up");
16845 cx.assert_excerpts_with_selections(indoc! {"
16846 [EXCERPT]
16847 [FOLDED]
16848 [EXCERPT]
16849 a1
16850 b1
16851 [EXCERPT]
16852 ˇ[FOLDED]
16853 [EXCERPT]
16854 [FOLDED]
16855 "
16856 });
16857 cx.simulate_keystroke("up");
16858 cx.assert_excerpts_with_selections(indoc! {"
16859 [EXCERPT]
16860 [FOLDED]
16861 [EXCERPT]
16862 a1
16863 b1
16864 ˇ[EXCERPT]
16865 [FOLDED]
16866 [EXCERPT]
16867 [FOLDED]
16868 "
16869 });
16870 cx.simulate_keystroke("up");
16871 cx.assert_excerpts_with_selections(indoc! {"
16872 [EXCERPT]
16873 [FOLDED]
16874 [EXCERPT]
16875 a1
16876 ˇb1
16877 [EXCERPT]
16878 [FOLDED]
16879 [EXCERPT]
16880 [FOLDED]
16881 "
16882 });
16883 cx.simulate_keystroke("up");
16884 cx.assert_excerpts_with_selections(indoc! {"
16885 [EXCERPT]
16886 [FOLDED]
16887 [EXCERPT]
16888 ˇa1
16889 b1
16890 [EXCERPT]
16891 [FOLDED]
16892 [EXCERPT]
16893 [FOLDED]
16894 "
16895 });
16896 for _ in 0..5 {
16897 cx.simulate_keystroke("up");
16898 cx.assert_excerpts_with_selections(indoc! {"
16899 [EXCERPT]
16900 ˇ[FOLDED]
16901 [EXCERPT]
16902 a1
16903 b1
16904 [EXCERPT]
16905 [FOLDED]
16906 [EXCERPT]
16907 [FOLDED]
16908 "
16909 });
16910 }
16911}
16912
16913#[gpui::test]
16914async fn test_inline_completion_text(cx: &mut TestAppContext) {
16915 init_test(cx, |_| {});
16916
16917 // Simple insertion
16918 assert_highlighted_edits(
16919 "Hello, world!",
16920 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
16921 true,
16922 cx,
16923 |highlighted_edits, cx| {
16924 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
16925 assert_eq!(highlighted_edits.highlights.len(), 1);
16926 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
16927 assert_eq!(
16928 highlighted_edits.highlights[0].1.background_color,
16929 Some(cx.theme().status().created_background)
16930 );
16931 },
16932 )
16933 .await;
16934
16935 // Replacement
16936 assert_highlighted_edits(
16937 "This is a test.",
16938 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
16939 false,
16940 cx,
16941 |highlighted_edits, cx| {
16942 assert_eq!(highlighted_edits.text, "That is a test.");
16943 assert_eq!(highlighted_edits.highlights.len(), 1);
16944 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
16945 assert_eq!(
16946 highlighted_edits.highlights[0].1.background_color,
16947 Some(cx.theme().status().created_background)
16948 );
16949 },
16950 )
16951 .await;
16952
16953 // Multiple edits
16954 assert_highlighted_edits(
16955 "Hello, world!",
16956 vec![
16957 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
16958 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
16959 ],
16960 false,
16961 cx,
16962 |highlighted_edits, cx| {
16963 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
16964 assert_eq!(highlighted_edits.highlights.len(), 2);
16965 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
16966 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
16967 assert_eq!(
16968 highlighted_edits.highlights[0].1.background_color,
16969 Some(cx.theme().status().created_background)
16970 );
16971 assert_eq!(
16972 highlighted_edits.highlights[1].1.background_color,
16973 Some(cx.theme().status().created_background)
16974 );
16975 },
16976 )
16977 .await;
16978
16979 // Multiple lines with edits
16980 assert_highlighted_edits(
16981 "First line\nSecond line\nThird line\nFourth line",
16982 vec![
16983 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
16984 (
16985 Point::new(2, 0)..Point::new(2, 10),
16986 "New third line".to_string(),
16987 ),
16988 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
16989 ],
16990 false,
16991 cx,
16992 |highlighted_edits, cx| {
16993 assert_eq!(
16994 highlighted_edits.text,
16995 "Second modified\nNew third line\nFourth updated line"
16996 );
16997 assert_eq!(highlighted_edits.highlights.len(), 3);
16998 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
16999 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
17000 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
17001 for highlight in &highlighted_edits.highlights {
17002 assert_eq!(
17003 highlight.1.background_color,
17004 Some(cx.theme().status().created_background)
17005 );
17006 }
17007 },
17008 )
17009 .await;
17010}
17011
17012#[gpui::test]
17013async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
17014 init_test(cx, |_| {});
17015
17016 // Deletion
17017 assert_highlighted_edits(
17018 "Hello, world!",
17019 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
17020 true,
17021 cx,
17022 |highlighted_edits, cx| {
17023 assert_eq!(highlighted_edits.text, "Hello, world!");
17024 assert_eq!(highlighted_edits.highlights.len(), 1);
17025 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
17026 assert_eq!(
17027 highlighted_edits.highlights[0].1.background_color,
17028 Some(cx.theme().status().deleted_background)
17029 );
17030 },
17031 )
17032 .await;
17033
17034 // Insertion
17035 assert_highlighted_edits(
17036 "Hello, world!",
17037 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
17038 true,
17039 cx,
17040 |highlighted_edits, cx| {
17041 assert_eq!(highlighted_edits.highlights.len(), 1);
17042 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
17043 assert_eq!(
17044 highlighted_edits.highlights[0].1.background_color,
17045 Some(cx.theme().status().created_background)
17046 );
17047 },
17048 )
17049 .await;
17050}
17051
17052async fn assert_highlighted_edits(
17053 text: &str,
17054 edits: Vec<(Range<Point>, String)>,
17055 include_deletions: bool,
17056 cx: &mut TestAppContext,
17057 assertion_fn: impl Fn(HighlightedText, &App),
17058) {
17059 let window = cx.add_window(|window, cx| {
17060 let buffer = MultiBuffer::build_simple(text, cx);
17061 Editor::new(EditorMode::Full, buffer, None, window, cx)
17062 });
17063 let cx = &mut VisualTestContext::from_window(*window, cx);
17064
17065 let (buffer, snapshot) = window
17066 .update(cx, |editor, _window, cx| {
17067 (
17068 editor.buffer().clone(),
17069 editor.buffer().read(cx).snapshot(cx),
17070 )
17071 })
17072 .unwrap();
17073
17074 let edits = edits
17075 .into_iter()
17076 .map(|(range, edit)| {
17077 (
17078 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
17079 edit,
17080 )
17081 })
17082 .collect::<Vec<_>>();
17083
17084 let text_anchor_edits = edits
17085 .clone()
17086 .into_iter()
17087 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
17088 .collect::<Vec<_>>();
17089
17090 let edit_preview = window
17091 .update(cx, |_, _window, cx| {
17092 buffer
17093 .read(cx)
17094 .as_singleton()
17095 .unwrap()
17096 .read(cx)
17097 .preview_edits(text_anchor_edits.into(), cx)
17098 })
17099 .unwrap()
17100 .await;
17101
17102 cx.update(|_window, cx| {
17103 let highlighted_edits = inline_completion_edit_text(
17104 &snapshot.as_singleton().unwrap().2,
17105 &edits,
17106 &edit_preview,
17107 include_deletions,
17108 cx,
17109 );
17110 assertion_fn(highlighted_edits, cx)
17111 });
17112}
17113
17114#[gpui::test]
17115async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
17116 init_test(cx, |_| {});
17117 let capabilities = lsp::ServerCapabilities {
17118 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
17119 prepare_provider: Some(true),
17120 work_done_progress_options: Default::default(),
17121 })),
17122 ..Default::default()
17123 };
17124 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
17125
17126 cx.set_state(indoc! {"
17127 struct Fˇoo {}
17128 "});
17129
17130 cx.update_editor(|editor, _, cx| {
17131 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
17132 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
17133 editor.highlight_background::<DocumentHighlightRead>(
17134 &[highlight_range],
17135 |c| c.editor_document_highlight_read_background,
17136 cx,
17137 );
17138 });
17139
17140 let mut prepare_rename_handler =
17141 cx.handle_request::<lsp::request::PrepareRenameRequest, _, _>(move |_, _, _| async move {
17142 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
17143 start: lsp::Position {
17144 line: 0,
17145 character: 7,
17146 },
17147 end: lsp::Position {
17148 line: 0,
17149 character: 10,
17150 },
17151 })))
17152 });
17153 let prepare_rename_task = cx
17154 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
17155 .expect("Prepare rename was not started");
17156 prepare_rename_handler.next().await.unwrap();
17157 prepare_rename_task.await.expect("Prepare rename failed");
17158
17159 let mut rename_handler =
17160 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
17161 let edit = lsp::TextEdit {
17162 range: lsp::Range {
17163 start: lsp::Position {
17164 line: 0,
17165 character: 7,
17166 },
17167 end: lsp::Position {
17168 line: 0,
17169 character: 10,
17170 },
17171 },
17172 new_text: "FooRenamed".to_string(),
17173 };
17174 Ok(Some(lsp::WorkspaceEdit::new(
17175 // Specify the same edit twice
17176 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
17177 )))
17178 });
17179 let rename_task = cx
17180 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
17181 .expect("Confirm rename was not started");
17182 rename_handler.next().await.unwrap();
17183 rename_task.await.expect("Confirm rename failed");
17184 cx.run_until_parked();
17185
17186 // Despite two edits, only one is actually applied as those are identical
17187 cx.assert_editor_state(indoc! {"
17188 struct FooRenamedˇ {}
17189 "});
17190}
17191
17192#[gpui::test]
17193async fn test_rename_without_prepare(cx: &mut TestAppContext) {
17194 init_test(cx, |_| {});
17195 // These capabilities indicate that the server does not support prepare rename.
17196 let capabilities = lsp::ServerCapabilities {
17197 rename_provider: Some(lsp::OneOf::Left(true)),
17198 ..Default::default()
17199 };
17200 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
17201
17202 cx.set_state(indoc! {"
17203 struct Fˇoo {}
17204 "});
17205
17206 cx.update_editor(|editor, _window, cx| {
17207 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
17208 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
17209 editor.highlight_background::<DocumentHighlightRead>(
17210 &[highlight_range],
17211 |c| c.editor_document_highlight_read_background,
17212 cx,
17213 );
17214 });
17215
17216 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
17217 .expect("Prepare rename was not started")
17218 .await
17219 .expect("Prepare rename failed");
17220
17221 let mut rename_handler =
17222 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
17223 let edit = lsp::TextEdit {
17224 range: lsp::Range {
17225 start: lsp::Position {
17226 line: 0,
17227 character: 7,
17228 },
17229 end: lsp::Position {
17230 line: 0,
17231 character: 10,
17232 },
17233 },
17234 new_text: "FooRenamed".to_string(),
17235 };
17236 Ok(Some(lsp::WorkspaceEdit::new(
17237 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
17238 )))
17239 });
17240 let rename_task = cx
17241 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
17242 .expect("Confirm rename was not started");
17243 rename_handler.next().await.unwrap();
17244 rename_task.await.expect("Confirm rename failed");
17245 cx.run_until_parked();
17246
17247 // Correct range is renamed, as `surrounding_word` is used to find it.
17248 cx.assert_editor_state(indoc! {"
17249 struct FooRenamedˇ {}
17250 "});
17251}
17252
17253#[gpui::test]
17254async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
17255 init_test(cx, |_| {});
17256 let mut cx = EditorTestContext::new(cx).await;
17257
17258 let language = Arc::new(
17259 Language::new(
17260 LanguageConfig::default(),
17261 Some(tree_sitter_html::LANGUAGE.into()),
17262 )
17263 .with_brackets_query(
17264 r#"
17265 ("<" @open "/>" @close)
17266 ("</" @open ">" @close)
17267 ("<" @open ">" @close)
17268 ("\"" @open "\"" @close)
17269 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
17270 "#,
17271 )
17272 .unwrap(),
17273 );
17274 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
17275
17276 cx.set_state(indoc! {"
17277 <span>ˇ</span>
17278 "});
17279 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
17280 cx.assert_editor_state(indoc! {"
17281 <span>
17282 ˇ
17283 </span>
17284 "});
17285
17286 cx.set_state(indoc! {"
17287 <span><span></span>ˇ</span>
17288 "});
17289 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
17290 cx.assert_editor_state(indoc! {"
17291 <span><span></span>
17292 ˇ</span>
17293 "});
17294
17295 cx.set_state(indoc! {"
17296 <span>ˇ
17297 </span>
17298 "});
17299 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
17300 cx.assert_editor_state(indoc! {"
17301 <span>
17302 ˇ
17303 </span>
17304 "});
17305}
17306
17307#[gpui::test(iterations = 10)]
17308async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
17309 init_test(cx, |_| {});
17310
17311 let fs = FakeFs::new(cx.executor());
17312 fs.insert_tree(
17313 path!("/dir"),
17314 json!({
17315 "a.ts": "a",
17316 }),
17317 )
17318 .await;
17319
17320 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
17321 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17322 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17323
17324 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17325 language_registry.add(Arc::new(Language::new(
17326 LanguageConfig {
17327 name: "TypeScript".into(),
17328 matcher: LanguageMatcher {
17329 path_suffixes: vec!["ts".to_string()],
17330 ..Default::default()
17331 },
17332 ..Default::default()
17333 },
17334 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
17335 )));
17336 let mut fake_language_servers = language_registry.register_fake_lsp(
17337 "TypeScript",
17338 FakeLspAdapter {
17339 capabilities: lsp::ServerCapabilities {
17340 code_lens_provider: Some(lsp::CodeLensOptions {
17341 resolve_provider: Some(true),
17342 }),
17343 execute_command_provider: Some(lsp::ExecuteCommandOptions {
17344 commands: vec!["_the/command".to_string()],
17345 ..lsp::ExecuteCommandOptions::default()
17346 }),
17347 ..lsp::ServerCapabilities::default()
17348 },
17349 ..FakeLspAdapter::default()
17350 },
17351 );
17352
17353 let (buffer, _handle) = project
17354 .update(cx, |p, cx| {
17355 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
17356 })
17357 .await
17358 .unwrap();
17359 cx.executor().run_until_parked();
17360
17361 let fake_server = fake_language_servers.next().await.unwrap();
17362
17363 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
17364 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
17365 drop(buffer_snapshot);
17366 let actions = cx
17367 .update_window(*workspace, |_, window, cx| {
17368 project.code_actions(&buffer, anchor..anchor, window, cx)
17369 })
17370 .unwrap();
17371
17372 fake_server
17373 .handle_request::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
17374 Ok(Some(vec![
17375 lsp::CodeLens {
17376 range: lsp::Range::default(),
17377 command: Some(lsp::Command {
17378 title: "Code lens command".to_owned(),
17379 command: "_the/command".to_owned(),
17380 arguments: None,
17381 }),
17382 data: None,
17383 },
17384 lsp::CodeLens {
17385 range: lsp::Range::default(),
17386 command: Some(lsp::Command {
17387 title: "Command not in capabilities".to_owned(),
17388 command: "not in capabilities".to_owned(),
17389 arguments: None,
17390 }),
17391 data: None,
17392 },
17393 lsp::CodeLens {
17394 range: lsp::Range {
17395 start: lsp::Position {
17396 line: 1,
17397 character: 1,
17398 },
17399 end: lsp::Position {
17400 line: 1,
17401 character: 1,
17402 },
17403 },
17404 command: Some(lsp::Command {
17405 title: "Command not in range".to_owned(),
17406 command: "_the/command".to_owned(),
17407 arguments: None,
17408 }),
17409 data: None,
17410 },
17411 ]))
17412 })
17413 .next()
17414 .await;
17415
17416 let actions = actions.await.unwrap();
17417 assert_eq!(
17418 actions.len(),
17419 1,
17420 "Should have only one valid action for the 0..0 range"
17421 );
17422 let action = actions[0].clone();
17423 let apply = project.update(cx, |project, cx| {
17424 project.apply_code_action(buffer.clone(), action, true, cx)
17425 });
17426
17427 // Resolving the code action does not populate its edits. In absence of
17428 // edits, we must execute the given command.
17429 fake_server.handle_request::<lsp::request::CodeLensResolve, _, _>(|mut lens, _| async move {
17430 let lens_command = lens.command.as_mut().expect("should have a command");
17431 assert_eq!(lens_command.title, "Code lens command");
17432 lens_command.arguments = Some(vec![json!("the-argument")]);
17433 Ok(lens)
17434 });
17435
17436 // While executing the command, the language server sends the editor
17437 // a `workspaceEdit` request.
17438 fake_server
17439 .handle_request::<lsp::request::ExecuteCommand, _, _>({
17440 let fake = fake_server.clone();
17441 move |params, _| {
17442 assert_eq!(params.command, "_the/command");
17443 let fake = fake.clone();
17444 async move {
17445 fake.server
17446 .request::<lsp::request::ApplyWorkspaceEdit>(
17447 lsp::ApplyWorkspaceEditParams {
17448 label: None,
17449 edit: lsp::WorkspaceEdit {
17450 changes: Some(
17451 [(
17452 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
17453 vec![lsp::TextEdit {
17454 range: lsp::Range::new(
17455 lsp::Position::new(0, 0),
17456 lsp::Position::new(0, 0),
17457 ),
17458 new_text: "X".into(),
17459 }],
17460 )]
17461 .into_iter()
17462 .collect(),
17463 ),
17464 ..Default::default()
17465 },
17466 },
17467 )
17468 .await
17469 .unwrap();
17470 Ok(Some(json!(null)))
17471 }
17472 }
17473 })
17474 .next()
17475 .await;
17476
17477 // Applying the code lens command returns a project transaction containing the edits
17478 // sent by the language server in its `workspaceEdit` request.
17479 let transaction = apply.await.unwrap();
17480 assert!(transaction.0.contains_key(&buffer));
17481 buffer.update(cx, |buffer, cx| {
17482 assert_eq!(buffer.text(), "Xa");
17483 buffer.undo(cx);
17484 assert_eq!(buffer.text(), "a");
17485 });
17486}
17487
17488mod autoclose_tags {
17489 use super::*;
17490 use language::language_settings::JsxTagAutoCloseSettings;
17491 use languages::language;
17492
17493 async fn test_setup(cx: &mut TestAppContext) -> EditorTestContext {
17494 init_test(cx, |settings| {
17495 settings.defaults.jsx_tag_auto_close = Some(JsxTagAutoCloseSettings { enabled: true });
17496 });
17497
17498 let mut cx = EditorTestContext::new(cx).await;
17499 cx.update_buffer(|buffer, cx| {
17500 let language = language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into());
17501
17502 buffer.set_language(Some(language), cx)
17503 });
17504
17505 cx
17506 }
17507
17508 macro_rules! check {
17509 ($name:ident, $initial:literal + $input:literal => $expected:expr) => {
17510 #[gpui::test]
17511 async fn $name(cx: &mut TestAppContext) {
17512 let mut cx = test_setup(cx).await;
17513 cx.set_state($initial);
17514 cx.run_until_parked();
17515
17516 cx.update_editor(|editor, window, cx| {
17517 editor.handle_input($input, window, cx);
17518 });
17519 cx.run_until_parked();
17520 cx.assert_editor_state($expected);
17521 }
17522 };
17523 }
17524
17525 check!(
17526 test_basic,
17527 "<divˇ" + ">" => "<div>ˇ</div>"
17528 );
17529
17530 check!(
17531 test_basic_nested,
17532 "<div><divˇ</div>" + ">" => "<div><div>ˇ</div></div>"
17533 );
17534
17535 check!(
17536 test_basic_ignore_already_closed,
17537 "<div><divˇ</div></div>" + ">" => "<div><div>ˇ</div></div>"
17538 );
17539
17540 check!(
17541 test_doesnt_autoclose_closing_tag,
17542 "</divˇ" + ">" => "</div>ˇ"
17543 );
17544
17545 check!(
17546 test_jsx_attr,
17547 "<div attr={</div>}ˇ" + ">" => "<div attr={</div>}>ˇ</div>"
17548 );
17549
17550 check!(
17551 test_ignores_closing_tags_in_expr_block,
17552 "<div><divˇ{</div>}</div>" + ">" => "<div><div>ˇ</div>{</div>}</div>"
17553 );
17554
17555 check!(
17556 test_doesnt_autoclose_on_gt_in_expr,
17557 "<div attr={1 ˇ" + ">" => "<div attr={1 >ˇ"
17558 );
17559
17560 check!(
17561 test_ignores_closing_tags_with_different_tag_names,
17562 "<div><divˇ</div></span>" + ">" => "<div><div>ˇ</div></div></span>"
17563 );
17564
17565 check!(
17566 test_autocloses_in_jsx_expression,
17567 "<div>{<divˇ}</div>" + ">" => "<div>{<div>ˇ</div>}</div>"
17568 );
17569
17570 check!(
17571 test_doesnt_autoclose_already_closed_in_jsx_expression,
17572 "<div>{<divˇ</div>}</div>" + ">" => "<div>{<div>ˇ</div>}</div>"
17573 );
17574
17575 check!(
17576 test_autocloses_fragment,
17577 "<ˇ" + ">" => "<>ˇ</>"
17578 );
17579
17580 check!(
17581 test_does_not_include_type_argument_in_autoclose_tag_name,
17582 "<Component<T> attr={boolean_value}ˇ" + ">" => "<Component<T> attr={boolean_value}>ˇ</Component>"
17583 );
17584
17585 check!(
17586 test_does_not_autoclose_doctype,
17587 "<!DOCTYPE htmlˇ" + ">" => "<!DOCTYPE html>ˇ"
17588 );
17589
17590 check!(
17591 test_does_not_autoclose_comment,
17592 "<!-- comment --ˇ" + ">" => "<!-- comment -->ˇ"
17593 );
17594
17595 check!(
17596 test_multi_cursor_autoclose_same_tag,
17597 r#"
17598 <divˇ
17599 <divˇ
17600 "#
17601 + ">" =>
17602 r#"
17603 <div>ˇ</div>
17604 <div>ˇ</div>
17605 "#
17606 );
17607
17608 check!(
17609 test_multi_cursor_autoclose_different_tags,
17610 r#"
17611 <divˇ
17612 <spanˇ
17613 "#
17614 + ">" =>
17615 r#"
17616 <div>ˇ</div>
17617 <span>ˇ</span>
17618 "#
17619 );
17620
17621 check!(
17622 test_multi_cursor_autoclose_some_dont_autoclose_others,
17623 r#"
17624 <divˇ
17625 <div /ˇ
17626 <spanˇ</span>
17627 <!DOCTYPE htmlˇ
17628 </headˇ
17629 <Component<T>ˇ
17630 ˇ
17631 "#
17632 + ">" =>
17633 r#"
17634 <div>ˇ</div>
17635 <div />ˇ
17636 <span>ˇ</span>
17637 <!DOCTYPE html>ˇ
17638 </head>ˇ
17639 <Component<T>>ˇ</Component>
17640 >ˇ
17641 "#
17642 );
17643
17644 check!(
17645 test_doesnt_mess_up_trailing_text,
17646 "<divˇfoobar" + ">" => "<div>ˇ</div>foobar"
17647 );
17648
17649 #[gpui::test]
17650 async fn test_multibuffer(cx: &mut TestAppContext) {
17651 init_test(cx, |settings| {
17652 settings.defaults.jsx_tag_auto_close = Some(JsxTagAutoCloseSettings { enabled: true });
17653 });
17654
17655 let buffer_a = cx.new(|cx| {
17656 let mut buf = language::Buffer::local("<div", cx);
17657 buf.set_language(
17658 Some(language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into())),
17659 cx,
17660 );
17661 buf
17662 });
17663 let buffer_b = cx.new(|cx| {
17664 let mut buf = language::Buffer::local("<pre", cx);
17665 buf.set_language(
17666 Some(language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into())),
17667 cx,
17668 );
17669 buf
17670 });
17671 let buffer_c = cx.new(|cx| {
17672 let buf = language::Buffer::local("<span", cx);
17673 buf
17674 });
17675 let buffer = cx.new(|cx| {
17676 let mut buf = MultiBuffer::new(language::Capability::ReadWrite);
17677 buf.push_excerpts(
17678 buffer_a,
17679 [ExcerptRange {
17680 context: text::Anchor::MIN..text::Anchor::MAX,
17681 primary: None,
17682 }],
17683 cx,
17684 );
17685 buf.push_excerpts(
17686 buffer_b,
17687 [ExcerptRange {
17688 context: text::Anchor::MIN..text::Anchor::MAX,
17689 primary: None,
17690 }],
17691 cx,
17692 );
17693 buf.push_excerpts(
17694 buffer_c,
17695 [ExcerptRange {
17696 context: text::Anchor::MIN..text::Anchor::MAX,
17697 primary: None,
17698 }],
17699 cx,
17700 );
17701 buf
17702 });
17703 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
17704
17705 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17706
17707 cx.update_editor(|editor, window, cx| {
17708 editor.change_selections(None, window, cx, |selections| {
17709 selections.select(vec![
17710 Selection::from_offset(4),
17711 Selection::from_offset(9),
17712 Selection::from_offset(15),
17713 ])
17714 })
17715 });
17716 cx.run_until_parked();
17717
17718 cx.update_editor(|editor, window, cx| {
17719 editor.handle_input(">", window, cx);
17720 });
17721 cx.run_until_parked();
17722
17723 cx.assert_editor_state("<div>ˇ</div>\n<pre>ˇ</pre>\n<span>ˇ");
17724 }
17725}
17726
17727fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
17728 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
17729 point..point
17730}
17731
17732fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
17733 let (text, ranges) = marked_text_ranges(marked_text, true);
17734 assert_eq!(editor.text(cx), text);
17735 assert_eq!(
17736 editor.selections.ranges(cx),
17737 ranges,
17738 "Assert selections are {}",
17739 marked_text
17740 );
17741}
17742
17743pub fn handle_signature_help_request(
17744 cx: &mut EditorLspTestContext,
17745 mocked_response: lsp::SignatureHelp,
17746) -> impl Future<Output = ()> {
17747 let mut request =
17748 cx.handle_request::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
17749 let mocked_response = mocked_response.clone();
17750 async move { Ok(Some(mocked_response)) }
17751 });
17752
17753 async move {
17754 request.next().await;
17755 }
17756}
17757
17758/// Handle completion request passing a marked string specifying where the completion
17759/// should be triggered from using '|' character, what range should be replaced, and what completions
17760/// should be returned using '<' and '>' to delimit the range
17761pub fn handle_completion_request(
17762 cx: &mut EditorLspTestContext,
17763 marked_string: &str,
17764 completions: Vec<&'static str>,
17765 counter: Arc<AtomicUsize>,
17766) -> impl Future<Output = ()> {
17767 let complete_from_marker: TextRangeMarker = '|'.into();
17768 let replace_range_marker: TextRangeMarker = ('<', '>').into();
17769 let (_, mut marked_ranges) = marked_text_ranges_by(
17770 marked_string,
17771 vec![complete_from_marker.clone(), replace_range_marker.clone()],
17772 );
17773
17774 let complete_from_position =
17775 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
17776 let replace_range =
17777 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
17778
17779 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
17780 let completions = completions.clone();
17781 counter.fetch_add(1, atomic::Ordering::Release);
17782 async move {
17783 assert_eq!(params.text_document_position.text_document.uri, url.clone());
17784 assert_eq!(
17785 params.text_document_position.position,
17786 complete_from_position
17787 );
17788 Ok(Some(lsp::CompletionResponse::Array(
17789 completions
17790 .iter()
17791 .map(|completion_text| lsp::CompletionItem {
17792 label: completion_text.to_string(),
17793 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17794 range: replace_range,
17795 new_text: completion_text.to_string(),
17796 })),
17797 ..Default::default()
17798 })
17799 .collect(),
17800 )))
17801 }
17802 });
17803
17804 async move {
17805 request.next().await;
17806 }
17807}
17808
17809fn handle_resolve_completion_request(
17810 cx: &mut EditorLspTestContext,
17811 edits: Option<Vec<(&'static str, &'static str)>>,
17812) -> impl Future<Output = ()> {
17813 let edits = edits.map(|edits| {
17814 edits
17815 .iter()
17816 .map(|(marked_string, new_text)| {
17817 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
17818 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
17819 lsp::TextEdit::new(replace_range, new_text.to_string())
17820 })
17821 .collect::<Vec<_>>()
17822 });
17823
17824 let mut request =
17825 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17826 let edits = edits.clone();
17827 async move {
17828 Ok(lsp::CompletionItem {
17829 additional_text_edits: edits,
17830 ..Default::default()
17831 })
17832 }
17833 });
17834
17835 async move {
17836 request.next().await;
17837 }
17838}
17839
17840pub(crate) fn update_test_language_settings(
17841 cx: &mut TestAppContext,
17842 f: impl Fn(&mut AllLanguageSettingsContent),
17843) {
17844 cx.update(|cx| {
17845 SettingsStore::update_global(cx, |store, cx| {
17846 store.update_user_settings::<AllLanguageSettings>(cx, f);
17847 });
17848 });
17849}
17850
17851pub(crate) fn update_test_project_settings(
17852 cx: &mut TestAppContext,
17853 f: impl Fn(&mut ProjectSettings),
17854) {
17855 cx.update(|cx| {
17856 SettingsStore::update_global(cx, |store, cx| {
17857 store.update_user_settings::<ProjectSettings>(cx, f);
17858 });
17859 });
17860}
17861
17862pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
17863 cx.update(|cx| {
17864 assets::Assets.load_test_fonts(cx);
17865 let store = SettingsStore::test(cx);
17866 cx.set_global(store);
17867 theme::init(theme::LoadThemes::JustBase, cx);
17868 release_channel::init(SemanticVersion::default(), cx);
17869 client::init_settings(cx);
17870 language::init(cx);
17871 Project::init_settings(cx);
17872 workspace::init_settings(cx);
17873 crate::init(cx);
17874 });
17875
17876 update_test_language_settings(cx, f);
17877}
17878
17879#[track_caller]
17880fn assert_hunk_revert(
17881 not_reverted_text_with_selections: &str,
17882 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
17883 expected_reverted_text_with_selections: &str,
17884 base_text: &str,
17885 cx: &mut EditorLspTestContext,
17886) {
17887 cx.set_state(not_reverted_text_with_selections);
17888 cx.set_head_text(base_text);
17889 cx.executor().run_until_parked();
17890
17891 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
17892 let snapshot = editor.snapshot(window, cx);
17893 let reverted_hunk_statuses = snapshot
17894 .buffer_snapshot
17895 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
17896 .map(|hunk| hunk.status().kind)
17897 .collect::<Vec<_>>();
17898
17899 editor.git_restore(&Default::default(), window, cx);
17900 reverted_hunk_statuses
17901 });
17902 cx.executor().run_until_parked();
17903 cx.assert_editor_state(expected_reverted_text_with_selections);
17904 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
17905}