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, DiffHunkStatus};
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, LanguageSettingsContent, PrettierSettings,
20 },
21 BracketPairConfig,
22 Capability::ReadWrite,
23 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
24 Override, ParsedMarkdown, Point,
25};
26use language_settings::{Formatter, FormatterList, IndentGuideSettings};
27use multi_buffer::IndentGuide;
28use parking_lot::Mutex;
29use pretty_assertions::{assert_eq, assert_ne};
30use project::FakeFs;
31use project::{
32 lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT,
33 project_settings::{LspSettings, ProjectSettings},
34};
35use serde_json::{self, json};
36use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
37use std::{
38 iter,
39 sync::atomic::{self, AtomicUsize},
40};
41use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
42use unindent::Unindent;
43use util::{
44 assert_set_eq, path,
45 test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
46 uri,
47};
48use workspace::{
49 item::{FollowEvent, FollowableItem, Item, ItemHandle},
50 NavigationEntry, ViewId,
51};
52
53#[gpui::test]
54fn test_edit_events(cx: &mut TestAppContext) {
55 init_test(cx, |_| {});
56
57 let buffer = cx.new(|cx| {
58 let mut buffer = language::Buffer::local("123456", cx);
59 buffer.set_group_interval(Duration::from_secs(1));
60 buffer
61 });
62
63 let events = Rc::new(RefCell::new(Vec::new()));
64 let editor1 = cx.add_window({
65 let events = events.clone();
66 |window, cx| {
67 let entity = cx.entity().clone();
68 cx.subscribe_in(
69 &entity,
70 window,
71 move |_, _, event: &EditorEvent, _, _| match event {
72 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
73 EditorEvent::BufferEdited => {
74 events.borrow_mut().push(("editor1", "buffer edited"))
75 }
76 _ => {}
77 },
78 )
79 .detach();
80 Editor::for_buffer(buffer.clone(), None, window, cx)
81 }
82 });
83
84 let editor2 = cx.add_window({
85 let events = events.clone();
86 |window, cx| {
87 cx.subscribe_in(
88 &cx.entity().clone(),
89 window,
90 move |_, _, event: &EditorEvent, _, _| match event {
91 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
92 EditorEvent::BufferEdited => {
93 events.borrow_mut().push(("editor2", "buffer edited"))
94 }
95 _ => {}
96 },
97 )
98 .detach();
99 Editor::for_buffer(buffer.clone(), None, window, cx)
100 }
101 });
102
103 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
104
105 // Mutating editor 1 will emit an `Edited` event only for that editor.
106 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
107 assert_eq!(
108 mem::take(&mut *events.borrow_mut()),
109 [
110 ("editor1", "edited"),
111 ("editor1", "buffer edited"),
112 ("editor2", "buffer edited"),
113 ]
114 );
115
116 // Mutating editor 2 will emit an `Edited` event only for that editor.
117 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
118 assert_eq!(
119 mem::take(&mut *events.borrow_mut()),
120 [
121 ("editor2", "edited"),
122 ("editor1", "buffer edited"),
123 ("editor2", "buffer edited"),
124 ]
125 );
126
127 // Undoing on editor 1 will emit an `Edited` event only for that editor.
128 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
129 assert_eq!(
130 mem::take(&mut *events.borrow_mut()),
131 [
132 ("editor1", "edited"),
133 ("editor1", "buffer edited"),
134 ("editor2", "buffer edited"),
135 ]
136 );
137
138 // Redoing on editor 1 will emit an `Edited` event only for that editor.
139 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
140 assert_eq!(
141 mem::take(&mut *events.borrow_mut()),
142 [
143 ("editor1", "edited"),
144 ("editor1", "buffer edited"),
145 ("editor2", "buffer edited"),
146 ]
147 );
148
149 // Undoing on editor 2 will emit an `Edited` event only for that editor.
150 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
151 assert_eq!(
152 mem::take(&mut *events.borrow_mut()),
153 [
154 ("editor2", "edited"),
155 ("editor1", "buffer edited"),
156 ("editor2", "buffer edited"),
157 ]
158 );
159
160 // Redoing on editor 2 will emit an `Edited` event only for that editor.
161 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
162 assert_eq!(
163 mem::take(&mut *events.borrow_mut()),
164 [
165 ("editor2", "edited"),
166 ("editor1", "buffer edited"),
167 ("editor2", "buffer edited"),
168 ]
169 );
170
171 // No event is emitted when the mutation is a no-op.
172 _ = editor2.update(cx, |editor, window, cx| {
173 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
174
175 editor.backspace(&Backspace, window, cx);
176 });
177 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
178}
179
180#[gpui::test]
181fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
182 init_test(cx, |_| {});
183
184 let mut now = Instant::now();
185 let group_interval = Duration::from_millis(1);
186 let buffer = cx.new(|cx| {
187 let mut buf = language::Buffer::local("123456", cx);
188 buf.set_group_interval(group_interval);
189 buf
190 });
191 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
192 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
193
194 _ = editor.update(cx, |editor, window, cx| {
195 editor.start_transaction_at(now, window, cx);
196 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
197
198 editor.insert("cd", window, cx);
199 editor.end_transaction_at(now, cx);
200 assert_eq!(editor.text(cx), "12cd56");
201 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
202
203 editor.start_transaction_at(now, window, cx);
204 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
205 editor.insert("e", window, cx);
206 editor.end_transaction_at(now, cx);
207 assert_eq!(editor.text(cx), "12cde6");
208 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
209
210 now += group_interval + Duration::from_millis(1);
211 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
212
213 // Simulate an edit in another editor
214 buffer.update(cx, |buffer, cx| {
215 buffer.start_transaction_at(now, cx);
216 buffer.edit([(0..1, "a")], None, cx);
217 buffer.edit([(1..1, "b")], None, cx);
218 buffer.end_transaction_at(now, cx);
219 });
220
221 assert_eq!(editor.text(cx), "ab2cde6");
222 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
223
224 // Last transaction happened past the group interval in a different editor.
225 // Undo it individually and don't restore selections.
226 editor.undo(&Undo, window, cx);
227 assert_eq!(editor.text(cx), "12cde6");
228 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
229
230 // First two transactions happened within the group interval in this editor.
231 // Undo them together and restore selections.
232 editor.undo(&Undo, window, cx);
233 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
234 assert_eq!(editor.text(cx), "123456");
235 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
236
237 // Redo the first two transactions together.
238 editor.redo(&Redo, window, cx);
239 assert_eq!(editor.text(cx), "12cde6");
240 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
241
242 // Redo the last transaction on its own.
243 editor.redo(&Redo, window, cx);
244 assert_eq!(editor.text(cx), "ab2cde6");
245 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
246
247 // Test empty transactions.
248 editor.start_transaction_at(now, window, cx);
249 editor.end_transaction_at(now, cx);
250 editor.undo(&Undo, window, cx);
251 assert_eq!(editor.text(cx), "12cde6");
252 });
253}
254
255#[gpui::test]
256fn test_ime_composition(cx: &mut TestAppContext) {
257 init_test(cx, |_| {});
258
259 let buffer = cx.new(|cx| {
260 let mut buffer = language::Buffer::local("abcde", cx);
261 // Ensure automatic grouping doesn't occur.
262 buffer.set_group_interval(Duration::ZERO);
263 buffer
264 });
265
266 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
267 cx.add_window(|window, cx| {
268 let mut editor = build_editor(buffer.clone(), window, cx);
269
270 // Start a new IME composition.
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 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
274 assert_eq!(editor.text(cx), "äbcde");
275 assert_eq!(
276 editor.marked_text_ranges(cx),
277 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
278 );
279
280 // Finalize IME composition.
281 editor.replace_text_in_range(None, "ā", window, cx);
282 assert_eq!(editor.text(cx), "ābcde");
283 assert_eq!(editor.marked_text_ranges(cx), None);
284
285 // IME composition edits are grouped and are undone/redone at once.
286 editor.undo(&Default::default(), window, cx);
287 assert_eq!(editor.text(cx), "abcde");
288 assert_eq!(editor.marked_text_ranges(cx), None);
289 editor.redo(&Default::default(), window, cx);
290 assert_eq!(editor.text(cx), "ābcde");
291 assert_eq!(editor.marked_text_ranges(cx), None);
292
293 // Start a new IME composition.
294 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
295 assert_eq!(
296 editor.marked_text_ranges(cx),
297 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
298 );
299
300 // Undoing during an IME composition cancels it.
301 editor.undo(&Default::default(), window, cx);
302 assert_eq!(editor.text(cx), "ābcde");
303 assert_eq!(editor.marked_text_ranges(cx), None);
304
305 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
306 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
307 assert_eq!(editor.text(cx), "ābcdè");
308 assert_eq!(
309 editor.marked_text_ranges(cx),
310 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
311 );
312
313 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
314 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
315 assert_eq!(editor.text(cx), "ābcdę");
316 assert_eq!(editor.marked_text_ranges(cx), None);
317
318 // Start a new IME composition with multiple cursors.
319 editor.change_selections(None, window, cx, |s| {
320 s.select_ranges([
321 OffsetUtf16(1)..OffsetUtf16(1),
322 OffsetUtf16(3)..OffsetUtf16(3),
323 OffsetUtf16(5)..OffsetUtf16(5),
324 ])
325 });
326 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
327 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
328 assert_eq!(
329 editor.marked_text_ranges(cx),
330 Some(vec![
331 OffsetUtf16(0)..OffsetUtf16(3),
332 OffsetUtf16(4)..OffsetUtf16(7),
333 OffsetUtf16(8)..OffsetUtf16(11)
334 ])
335 );
336
337 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
338 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
339 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
340 assert_eq!(
341 editor.marked_text_ranges(cx),
342 Some(vec![
343 OffsetUtf16(1)..OffsetUtf16(2),
344 OffsetUtf16(5)..OffsetUtf16(6),
345 OffsetUtf16(9)..OffsetUtf16(10)
346 ])
347 );
348
349 // Finalize IME composition with multiple cursors.
350 editor.replace_text_in_range(Some(9..10), "2", window, cx);
351 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
352 assert_eq!(editor.marked_text_ranges(cx), None);
353
354 editor
355 });
356}
357
358#[gpui::test]
359fn test_selection_with_mouse(cx: &mut TestAppContext) {
360 init_test(cx, |_| {});
361
362 let editor = cx.add_window(|window, cx| {
363 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
364 build_editor(buffer, window, cx)
365 });
366
367 _ = editor.update(cx, |editor, window, cx| {
368 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
369 });
370 assert_eq!(
371 editor
372 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
373 .unwrap(),
374 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
375 );
376
377 _ = editor.update(cx, |editor, window, cx| {
378 editor.update_selection(
379 DisplayPoint::new(DisplayRow(3), 3),
380 0,
381 gpui::Point::<f32>::default(),
382 window,
383 cx,
384 );
385 });
386
387 assert_eq!(
388 editor
389 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
390 .unwrap(),
391 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
392 );
393
394 _ = editor.update(cx, |editor, window, cx| {
395 editor.update_selection(
396 DisplayPoint::new(DisplayRow(1), 1),
397 0,
398 gpui::Point::<f32>::default(),
399 window,
400 cx,
401 );
402 });
403
404 assert_eq!(
405 editor
406 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
407 .unwrap(),
408 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
409 );
410
411 _ = editor.update(cx, |editor, window, cx| {
412 editor.end_selection(window, cx);
413 editor.update_selection(
414 DisplayPoint::new(DisplayRow(3), 3),
415 0,
416 gpui::Point::<f32>::default(),
417 window,
418 cx,
419 );
420 });
421
422 assert_eq!(
423 editor
424 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
425 .unwrap(),
426 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
427 );
428
429 _ = editor.update(cx, |editor, window, cx| {
430 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
431 editor.update_selection(
432 DisplayPoint::new(DisplayRow(0), 0),
433 0,
434 gpui::Point::<f32>::default(),
435 window,
436 cx,
437 );
438 });
439
440 assert_eq!(
441 editor
442 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
443 .unwrap(),
444 [
445 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
446 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
447 ]
448 );
449
450 _ = editor.update(cx, |editor, window, cx| {
451 editor.end_selection(window, cx);
452 });
453
454 assert_eq!(
455 editor
456 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
457 .unwrap(),
458 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
459 );
460}
461
462#[gpui::test]
463fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
464 init_test(cx, |_| {});
465
466 let editor = cx.add_window(|window, cx| {
467 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
468 build_editor(buffer, window, cx)
469 });
470
471 _ = editor.update(cx, |editor, window, cx| {
472 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
473 });
474
475 _ = editor.update(cx, |editor, window, cx| {
476 editor.end_selection(window, cx);
477 });
478
479 _ = editor.update(cx, |editor, window, cx| {
480 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
481 });
482
483 _ = editor.update(cx, |editor, window, cx| {
484 editor.end_selection(window, cx);
485 });
486
487 assert_eq!(
488 editor
489 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
490 .unwrap(),
491 [
492 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
493 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
494 ]
495 );
496
497 _ = editor.update(cx, |editor, window, cx| {
498 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
499 });
500
501 _ = editor.update(cx, |editor, window, cx| {
502 editor.end_selection(window, cx);
503 });
504
505 assert_eq!(
506 editor
507 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
508 .unwrap(),
509 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
510 );
511}
512
513#[gpui::test]
514fn test_canceling_pending_selection(cx: &mut TestAppContext) {
515 init_test(cx, |_| {});
516
517 let editor = cx.add_window(|window, cx| {
518 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
519 build_editor(buffer, window, cx)
520 });
521
522 _ = editor.update(cx, |editor, window, cx| {
523 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
524 assert_eq!(
525 editor.selections.display_ranges(cx),
526 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
527 );
528 });
529
530 _ = editor.update(cx, |editor, window, cx| {
531 editor.update_selection(
532 DisplayPoint::new(DisplayRow(3), 3),
533 0,
534 gpui::Point::<f32>::default(),
535 window,
536 cx,
537 );
538 assert_eq!(
539 editor.selections.display_ranges(cx),
540 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
541 );
542 });
543
544 _ = editor.update(cx, |editor, window, cx| {
545 editor.cancel(&Cancel, window, cx);
546 editor.update_selection(
547 DisplayPoint::new(DisplayRow(1), 1),
548 0,
549 gpui::Point::<f32>::default(),
550 window,
551 cx,
552 );
553 assert_eq!(
554 editor.selections.display_ranges(cx),
555 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
556 );
557 });
558}
559
560#[gpui::test]
561fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
562 init_test(cx, |_| {});
563
564 let editor = cx.add_window(|window, cx| {
565 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
566 build_editor(buffer, window, cx)
567 });
568
569 _ = editor.update(cx, |editor, window, cx| {
570 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
571 assert_eq!(
572 editor.selections.display_ranges(cx),
573 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
574 );
575
576 editor.move_down(&Default::default(), window, cx);
577 assert_eq!(
578 editor.selections.display_ranges(cx),
579 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
580 );
581
582 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
583 assert_eq!(
584 editor.selections.display_ranges(cx),
585 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
586 );
587
588 editor.move_up(&Default::default(), window, cx);
589 assert_eq!(
590 editor.selections.display_ranges(cx),
591 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
592 );
593 });
594}
595
596#[gpui::test]
597fn test_clone(cx: &mut TestAppContext) {
598 init_test(cx, |_| {});
599
600 let (text, selection_ranges) = marked_text_ranges(
601 indoc! {"
602 one
603 two
604 threeˇ
605 four
606 fiveˇ
607 "},
608 true,
609 );
610
611 let editor = cx.add_window(|window, cx| {
612 let buffer = MultiBuffer::build_simple(&text, cx);
613 build_editor(buffer, window, cx)
614 });
615
616 _ = editor.update(cx, |editor, window, cx| {
617 editor.change_selections(None, window, cx, |s| {
618 s.select_ranges(selection_ranges.clone())
619 });
620 editor.fold_creases(
621 vec![
622 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
623 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
624 ],
625 true,
626 window,
627 cx,
628 );
629 });
630
631 let cloned_editor = editor
632 .update(cx, |editor, _, cx| {
633 cx.open_window(Default::default(), |window, cx| {
634 cx.new(|cx| editor.clone(window, cx))
635 })
636 })
637 .unwrap()
638 .unwrap();
639
640 let snapshot = editor
641 .update(cx, |e, window, cx| e.snapshot(window, cx))
642 .unwrap();
643 let cloned_snapshot = cloned_editor
644 .update(cx, |e, window, cx| e.snapshot(window, cx))
645 .unwrap();
646
647 assert_eq!(
648 cloned_editor
649 .update(cx, |e, _, cx| e.display_text(cx))
650 .unwrap(),
651 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
652 );
653 assert_eq!(
654 cloned_snapshot
655 .folds_in_range(0..text.len())
656 .collect::<Vec<_>>(),
657 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
658 );
659 assert_set_eq!(
660 cloned_editor
661 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
662 .unwrap(),
663 editor
664 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
665 .unwrap()
666 );
667 assert_set_eq!(
668 cloned_editor
669 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
670 .unwrap(),
671 editor
672 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
673 .unwrap()
674 );
675}
676
677#[gpui::test]
678async fn test_navigation_history(cx: &mut TestAppContext) {
679 init_test(cx, |_| {});
680
681 use workspace::item::Item;
682
683 let fs = FakeFs::new(cx.executor());
684 let project = Project::test(fs, [], cx).await;
685 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
686 let pane = workspace
687 .update(cx, |workspace, _, _| workspace.active_pane().clone())
688 .unwrap();
689
690 _ = workspace.update(cx, |_v, window, cx| {
691 cx.new(|cx| {
692 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
693 let mut editor = build_editor(buffer.clone(), window, cx);
694 let handle = cx.entity();
695 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
696
697 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
698 editor.nav_history.as_mut().unwrap().pop_backward(cx)
699 }
700
701 // Move the cursor a small distance.
702 // Nothing is added to the navigation history.
703 editor.change_selections(None, window, cx, |s| {
704 s.select_display_ranges([
705 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
706 ])
707 });
708 editor.change_selections(None, window, cx, |s| {
709 s.select_display_ranges([
710 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
711 ])
712 });
713 assert!(pop_history(&mut editor, cx).is_none());
714
715 // Move the cursor a large distance.
716 // The history can jump back to the previous position.
717 editor.change_selections(None, window, cx, |s| {
718 s.select_display_ranges([
719 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
720 ])
721 });
722 let nav_entry = pop_history(&mut editor, cx).unwrap();
723 editor.navigate(nav_entry.data.unwrap(), window, cx);
724 assert_eq!(nav_entry.item.id(), cx.entity_id());
725 assert_eq!(
726 editor.selections.display_ranges(cx),
727 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
728 );
729 assert!(pop_history(&mut editor, cx).is_none());
730
731 // Move the cursor a small distance via the mouse.
732 // Nothing is added to the navigation history.
733 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
734 editor.end_selection(window, cx);
735 assert_eq!(
736 editor.selections.display_ranges(cx),
737 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
738 );
739 assert!(pop_history(&mut editor, cx).is_none());
740
741 // Move the cursor a large distance via the mouse.
742 // The history can jump back to the previous position.
743 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
744 editor.end_selection(window, cx);
745 assert_eq!(
746 editor.selections.display_ranges(cx),
747 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
748 );
749 let nav_entry = pop_history(&mut editor, cx).unwrap();
750 editor.navigate(nav_entry.data.unwrap(), window, cx);
751 assert_eq!(nav_entry.item.id(), cx.entity_id());
752 assert_eq!(
753 editor.selections.display_ranges(cx),
754 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
755 );
756 assert!(pop_history(&mut editor, cx).is_none());
757
758 // Set scroll position to check later
759 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
760 let original_scroll_position = editor.scroll_manager.anchor();
761
762 // Jump to the end of the document and adjust scroll
763 editor.move_to_end(&MoveToEnd, window, cx);
764 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
765 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
766
767 let nav_entry = pop_history(&mut editor, cx).unwrap();
768 editor.navigate(nav_entry.data.unwrap(), window, cx);
769 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
770
771 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
772 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
773 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
774 let invalid_point = Point::new(9999, 0);
775 editor.navigate(
776 Box::new(NavigationData {
777 cursor_anchor: invalid_anchor,
778 cursor_position: invalid_point,
779 scroll_anchor: ScrollAnchor {
780 anchor: invalid_anchor,
781 offset: Default::default(),
782 },
783 scroll_top_row: invalid_point.row,
784 }),
785 window,
786 cx,
787 );
788 assert_eq!(
789 editor.selections.display_ranges(cx),
790 &[editor.max_point(cx)..editor.max_point(cx)]
791 );
792 assert_eq!(
793 editor.scroll_position(cx),
794 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
795 );
796
797 editor
798 })
799 });
800}
801
802#[gpui::test]
803fn test_cancel(cx: &mut TestAppContext) {
804 init_test(cx, |_| {});
805
806 let editor = cx.add_window(|window, cx| {
807 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
808 build_editor(buffer, window, cx)
809 });
810
811 _ = editor.update(cx, |editor, window, cx| {
812 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
813 editor.update_selection(
814 DisplayPoint::new(DisplayRow(1), 1),
815 0,
816 gpui::Point::<f32>::default(),
817 window,
818 cx,
819 );
820 editor.end_selection(window, cx);
821
822 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
823 editor.update_selection(
824 DisplayPoint::new(DisplayRow(0), 3),
825 0,
826 gpui::Point::<f32>::default(),
827 window,
828 cx,
829 );
830 editor.end_selection(window, cx);
831 assert_eq!(
832 editor.selections.display_ranges(cx),
833 [
834 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
835 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
836 ]
837 );
838 });
839
840 _ = editor.update(cx, |editor, window, cx| {
841 editor.cancel(&Cancel, window, cx);
842 assert_eq!(
843 editor.selections.display_ranges(cx),
844 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
845 );
846 });
847
848 _ = editor.update(cx, |editor, window, cx| {
849 editor.cancel(&Cancel, window, cx);
850 assert_eq!(
851 editor.selections.display_ranges(cx),
852 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
853 );
854 });
855}
856
857#[gpui::test]
858fn test_fold_action(cx: &mut TestAppContext) {
859 init_test(cx, |_| {});
860
861 let editor = cx.add_window(|window, cx| {
862 let buffer = MultiBuffer::build_simple(
863 &"
864 impl Foo {
865 // Hello!
866
867 fn a() {
868 1
869 }
870
871 fn b() {
872 2
873 }
874
875 fn c() {
876 3
877 }
878 }
879 "
880 .unindent(),
881 cx,
882 );
883 build_editor(buffer.clone(), window, cx)
884 });
885
886 _ = editor.update(cx, |editor, window, cx| {
887 editor.change_selections(None, window, cx, |s| {
888 s.select_display_ranges([
889 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
890 ]);
891 });
892 editor.fold(&Fold, window, cx);
893 assert_eq!(
894 editor.display_text(cx),
895 "
896 impl Foo {
897 // Hello!
898
899 fn a() {
900 1
901 }
902
903 fn b() {⋯
904 }
905
906 fn c() {⋯
907 }
908 }
909 "
910 .unindent(),
911 );
912
913 editor.fold(&Fold, window, cx);
914 assert_eq!(
915 editor.display_text(cx),
916 "
917 impl Foo {⋯
918 }
919 "
920 .unindent(),
921 );
922
923 editor.unfold_lines(&UnfoldLines, window, cx);
924 assert_eq!(
925 editor.display_text(cx),
926 "
927 impl Foo {
928 // Hello!
929
930 fn a() {
931 1
932 }
933
934 fn b() {⋯
935 }
936
937 fn c() {⋯
938 }
939 }
940 "
941 .unindent(),
942 );
943
944 editor.unfold_lines(&UnfoldLines, window, cx);
945 assert_eq!(
946 editor.display_text(cx),
947 editor.buffer.read(cx).read(cx).text()
948 );
949 });
950}
951
952#[gpui::test]
953fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
954 init_test(cx, |_| {});
955
956 let editor = cx.add_window(|window, cx| {
957 let buffer = MultiBuffer::build_simple(
958 &"
959 class Foo:
960 # Hello!
961
962 def a():
963 print(1)
964
965 def b():
966 print(2)
967
968 def c():
969 print(3)
970 "
971 .unindent(),
972 cx,
973 );
974 build_editor(buffer.clone(), window, cx)
975 });
976
977 _ = editor.update(cx, |editor, window, cx| {
978 editor.change_selections(None, window, cx, |s| {
979 s.select_display_ranges([
980 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
981 ]);
982 });
983 editor.fold(&Fold, window, cx);
984 assert_eq!(
985 editor.display_text(cx),
986 "
987 class Foo:
988 # Hello!
989
990 def a():
991 print(1)
992
993 def b():⋯
994
995 def c():⋯
996 "
997 .unindent(),
998 );
999
1000 editor.fold(&Fold, window, cx);
1001 assert_eq!(
1002 editor.display_text(cx),
1003 "
1004 class Foo:⋯
1005 "
1006 .unindent(),
1007 );
1008
1009 editor.unfold_lines(&UnfoldLines, window, cx);
1010 assert_eq!(
1011 editor.display_text(cx),
1012 "
1013 class Foo:
1014 # Hello!
1015
1016 def a():
1017 print(1)
1018
1019 def b():⋯
1020
1021 def c():⋯
1022 "
1023 .unindent(),
1024 );
1025
1026 editor.unfold_lines(&UnfoldLines, window, cx);
1027 assert_eq!(
1028 editor.display_text(cx),
1029 editor.buffer.read(cx).read(cx).text()
1030 );
1031 });
1032}
1033
1034#[gpui::test]
1035fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1036 init_test(cx, |_| {});
1037
1038 let editor = cx.add_window(|window, cx| {
1039 let buffer = MultiBuffer::build_simple(
1040 &"
1041 class Foo:
1042 # Hello!
1043
1044 def a():
1045 print(1)
1046
1047 def b():
1048 print(2)
1049
1050
1051 def c():
1052 print(3)
1053
1054
1055 "
1056 .unindent(),
1057 cx,
1058 );
1059 build_editor(buffer.clone(), window, cx)
1060 });
1061
1062 _ = editor.update(cx, |editor, window, cx| {
1063 editor.change_selections(None, window, cx, |s| {
1064 s.select_display_ranges([
1065 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1066 ]);
1067 });
1068 editor.fold(&Fold, window, cx);
1069 assert_eq!(
1070 editor.display_text(cx),
1071 "
1072 class Foo:
1073 # Hello!
1074
1075 def a():
1076 print(1)
1077
1078 def b():⋯
1079
1080
1081 def c():⋯
1082
1083
1084 "
1085 .unindent(),
1086 );
1087
1088 editor.fold(&Fold, window, cx);
1089 assert_eq!(
1090 editor.display_text(cx),
1091 "
1092 class Foo:⋯
1093
1094
1095 "
1096 .unindent(),
1097 );
1098
1099 editor.unfold_lines(&UnfoldLines, window, cx);
1100 assert_eq!(
1101 editor.display_text(cx),
1102 "
1103 class Foo:
1104 # Hello!
1105
1106 def a():
1107 print(1)
1108
1109 def b():⋯
1110
1111
1112 def c():⋯
1113
1114
1115 "
1116 .unindent(),
1117 );
1118
1119 editor.unfold_lines(&UnfoldLines, window, cx);
1120 assert_eq!(
1121 editor.display_text(cx),
1122 editor.buffer.read(cx).read(cx).text()
1123 );
1124 });
1125}
1126
1127#[gpui::test]
1128fn test_fold_at_level(cx: &mut TestAppContext) {
1129 init_test(cx, |_| {});
1130
1131 let editor = cx.add_window(|window, cx| {
1132 let buffer = MultiBuffer::build_simple(
1133 &"
1134 class Foo:
1135 # Hello!
1136
1137 def a():
1138 print(1)
1139
1140 def b():
1141 print(2)
1142
1143
1144 class Bar:
1145 # World!
1146
1147 def a():
1148 print(1)
1149
1150 def b():
1151 print(2)
1152
1153
1154 "
1155 .unindent(),
1156 cx,
1157 );
1158 build_editor(buffer.clone(), window, cx)
1159 });
1160
1161 _ = editor.update(cx, |editor, window, cx| {
1162 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1163 assert_eq!(
1164 editor.display_text(cx),
1165 "
1166 class Foo:
1167 # Hello!
1168
1169 def a():⋯
1170
1171 def b():⋯
1172
1173
1174 class Bar:
1175 # World!
1176
1177 def a():⋯
1178
1179 def b():⋯
1180
1181
1182 "
1183 .unindent(),
1184 );
1185
1186 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1187 assert_eq!(
1188 editor.display_text(cx),
1189 "
1190 class Foo:⋯
1191
1192
1193 class Bar:⋯
1194
1195
1196 "
1197 .unindent(),
1198 );
1199
1200 editor.unfold_all(&UnfoldAll, window, cx);
1201 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1202 assert_eq!(
1203 editor.display_text(cx),
1204 "
1205 class Foo:
1206 # Hello!
1207
1208 def a():
1209 print(1)
1210
1211 def b():
1212 print(2)
1213
1214
1215 class Bar:
1216 # World!
1217
1218 def a():
1219 print(1)
1220
1221 def b():
1222 print(2)
1223
1224
1225 "
1226 .unindent(),
1227 );
1228
1229 assert_eq!(
1230 editor.display_text(cx),
1231 editor.buffer.read(cx).read(cx).text()
1232 );
1233 });
1234}
1235
1236#[gpui::test]
1237fn test_move_cursor(cx: &mut TestAppContext) {
1238 init_test(cx, |_| {});
1239
1240 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1241 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1242
1243 buffer.update(cx, |buffer, cx| {
1244 buffer.edit(
1245 vec![
1246 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1247 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1248 ],
1249 None,
1250 cx,
1251 );
1252 });
1253 _ = editor.update(cx, |editor, window, cx| {
1254 assert_eq!(
1255 editor.selections.display_ranges(cx),
1256 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1257 );
1258
1259 editor.move_down(&MoveDown, window, cx);
1260 assert_eq!(
1261 editor.selections.display_ranges(cx),
1262 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1263 );
1264
1265 editor.move_right(&MoveRight, window, cx);
1266 assert_eq!(
1267 editor.selections.display_ranges(cx),
1268 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1269 );
1270
1271 editor.move_left(&MoveLeft, window, cx);
1272 assert_eq!(
1273 editor.selections.display_ranges(cx),
1274 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1275 );
1276
1277 editor.move_up(&MoveUp, window, cx);
1278 assert_eq!(
1279 editor.selections.display_ranges(cx),
1280 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1281 );
1282
1283 editor.move_to_end(&MoveToEnd, window, cx);
1284 assert_eq!(
1285 editor.selections.display_ranges(cx),
1286 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1287 );
1288
1289 editor.move_to_beginning(&MoveToBeginning, window, cx);
1290 assert_eq!(
1291 editor.selections.display_ranges(cx),
1292 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1293 );
1294
1295 editor.change_selections(None, window, cx, |s| {
1296 s.select_display_ranges([
1297 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1298 ]);
1299 });
1300 editor.select_to_beginning(&SelectToBeginning, window, cx);
1301 assert_eq!(
1302 editor.selections.display_ranges(cx),
1303 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1304 );
1305
1306 editor.select_to_end(&SelectToEnd, window, cx);
1307 assert_eq!(
1308 editor.selections.display_ranges(cx),
1309 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1310 );
1311 });
1312}
1313
1314// TODO: Re-enable this test
1315#[cfg(target_os = "macos")]
1316#[gpui::test]
1317fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1318 init_test(cx, |_| {});
1319
1320 let editor = cx.add_window(|window, cx| {
1321 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1322 build_editor(buffer.clone(), window, cx)
1323 });
1324
1325 assert_eq!('🟥'.len_utf8(), 4);
1326 assert_eq!('α'.len_utf8(), 2);
1327
1328 _ = editor.update(cx, |editor, window, cx| {
1329 editor.fold_creases(
1330 vec![
1331 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1332 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1333 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1334 ],
1335 true,
1336 window,
1337 cx,
1338 );
1339 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1340
1341 editor.move_right(&MoveRight, window, cx);
1342 assert_eq!(
1343 editor.selections.display_ranges(cx),
1344 &[empty_range(0, "🟥".len())]
1345 );
1346 editor.move_right(&MoveRight, window, cx);
1347 assert_eq!(
1348 editor.selections.display_ranges(cx),
1349 &[empty_range(0, "🟥🟧".len())]
1350 );
1351 editor.move_right(&MoveRight, window, cx);
1352 assert_eq!(
1353 editor.selections.display_ranges(cx),
1354 &[empty_range(0, "🟥🟧⋯".len())]
1355 );
1356
1357 editor.move_down(&MoveDown, window, cx);
1358 assert_eq!(
1359 editor.selections.display_ranges(cx),
1360 &[empty_range(1, "ab⋯e".len())]
1361 );
1362 editor.move_left(&MoveLeft, window, cx);
1363 assert_eq!(
1364 editor.selections.display_ranges(cx),
1365 &[empty_range(1, "ab⋯".len())]
1366 );
1367 editor.move_left(&MoveLeft, window, cx);
1368 assert_eq!(
1369 editor.selections.display_ranges(cx),
1370 &[empty_range(1, "ab".len())]
1371 );
1372 editor.move_left(&MoveLeft, window, cx);
1373 assert_eq!(
1374 editor.selections.display_ranges(cx),
1375 &[empty_range(1, "a".len())]
1376 );
1377
1378 editor.move_down(&MoveDown, window, cx);
1379 assert_eq!(
1380 editor.selections.display_ranges(cx),
1381 &[empty_range(2, "α".len())]
1382 );
1383 editor.move_right(&MoveRight, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[empty_range(2, "αβ".len())]
1387 );
1388 editor.move_right(&MoveRight, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[empty_range(2, "αβ⋯".len())]
1392 );
1393 editor.move_right(&MoveRight, window, cx);
1394 assert_eq!(
1395 editor.selections.display_ranges(cx),
1396 &[empty_range(2, "αβ⋯ε".len())]
1397 );
1398
1399 editor.move_up(&MoveUp, window, cx);
1400 assert_eq!(
1401 editor.selections.display_ranges(cx),
1402 &[empty_range(1, "ab⋯e".len())]
1403 );
1404 editor.move_down(&MoveDown, window, cx);
1405 assert_eq!(
1406 editor.selections.display_ranges(cx),
1407 &[empty_range(2, "αβ⋯ε".len())]
1408 );
1409 editor.move_up(&MoveUp, window, cx);
1410 assert_eq!(
1411 editor.selections.display_ranges(cx),
1412 &[empty_range(1, "ab⋯e".len())]
1413 );
1414
1415 editor.move_up(&MoveUp, window, cx);
1416 assert_eq!(
1417 editor.selections.display_ranges(cx),
1418 &[empty_range(0, "🟥🟧".len())]
1419 );
1420 editor.move_left(&MoveLeft, window, cx);
1421 assert_eq!(
1422 editor.selections.display_ranges(cx),
1423 &[empty_range(0, "🟥".len())]
1424 );
1425 editor.move_left(&MoveLeft, window, cx);
1426 assert_eq!(
1427 editor.selections.display_ranges(cx),
1428 &[empty_range(0, "".len())]
1429 );
1430 });
1431}
1432
1433#[gpui::test]
1434fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1435 init_test(cx, |_| {});
1436
1437 let editor = cx.add_window(|window, cx| {
1438 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1439 build_editor(buffer.clone(), window, cx)
1440 });
1441 _ = editor.update(cx, |editor, window, cx| {
1442 editor.change_selections(None, window, cx, |s| {
1443 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1444 });
1445
1446 // moving above start of document should move selection to start of document,
1447 // but the next move down should still be at the original goal_x
1448 editor.move_up(&MoveUp, window, cx);
1449 assert_eq!(
1450 editor.selections.display_ranges(cx),
1451 &[empty_range(0, "".len())]
1452 );
1453
1454 editor.move_down(&MoveDown, window, cx);
1455 assert_eq!(
1456 editor.selections.display_ranges(cx),
1457 &[empty_range(1, "abcd".len())]
1458 );
1459
1460 editor.move_down(&MoveDown, window, cx);
1461 assert_eq!(
1462 editor.selections.display_ranges(cx),
1463 &[empty_range(2, "αβγ".len())]
1464 );
1465
1466 editor.move_down(&MoveDown, window, cx);
1467 assert_eq!(
1468 editor.selections.display_ranges(cx),
1469 &[empty_range(3, "abcd".len())]
1470 );
1471
1472 editor.move_down(&MoveDown, window, cx);
1473 assert_eq!(
1474 editor.selections.display_ranges(cx),
1475 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1476 );
1477
1478 // moving past end of document should not change goal_x
1479 editor.move_down(&MoveDown, window, cx);
1480 assert_eq!(
1481 editor.selections.display_ranges(cx),
1482 &[empty_range(5, "".len())]
1483 );
1484
1485 editor.move_down(&MoveDown, window, cx);
1486 assert_eq!(
1487 editor.selections.display_ranges(cx),
1488 &[empty_range(5, "".len())]
1489 );
1490
1491 editor.move_up(&MoveUp, window, cx);
1492 assert_eq!(
1493 editor.selections.display_ranges(cx),
1494 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1495 );
1496
1497 editor.move_up(&MoveUp, window, cx);
1498 assert_eq!(
1499 editor.selections.display_ranges(cx),
1500 &[empty_range(3, "abcd".len())]
1501 );
1502
1503 editor.move_up(&MoveUp, window, cx);
1504 assert_eq!(
1505 editor.selections.display_ranges(cx),
1506 &[empty_range(2, "αβγ".len())]
1507 );
1508 });
1509}
1510
1511#[gpui::test]
1512fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1513 init_test(cx, |_| {});
1514 let move_to_beg = MoveToBeginningOfLine {
1515 stop_at_soft_wraps: true,
1516 };
1517
1518 let move_to_end = MoveToEndOfLine {
1519 stop_at_soft_wraps: true,
1520 };
1521
1522 let editor = cx.add_window(|window, cx| {
1523 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1524 build_editor(buffer, window, cx)
1525 });
1526 _ = editor.update(cx, |editor, window, cx| {
1527 editor.change_selections(None, window, cx, |s| {
1528 s.select_display_ranges([
1529 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1530 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1531 ]);
1532 });
1533 });
1534
1535 _ = editor.update(cx, |editor, window, cx| {
1536 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1537 assert_eq!(
1538 editor.selections.display_ranges(cx),
1539 &[
1540 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1541 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1542 ]
1543 );
1544 });
1545
1546 _ = editor.update(cx, |editor, window, cx| {
1547 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1548 assert_eq!(
1549 editor.selections.display_ranges(cx),
1550 &[
1551 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1552 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1553 ]
1554 );
1555 });
1556
1557 _ = editor.update(cx, |editor, window, cx| {
1558 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1559 assert_eq!(
1560 editor.selections.display_ranges(cx),
1561 &[
1562 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1563 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1564 ]
1565 );
1566 });
1567
1568 _ = editor.update(cx, |editor, window, cx| {
1569 editor.move_to_end_of_line(&move_to_end, window, cx);
1570 assert_eq!(
1571 editor.selections.display_ranges(cx),
1572 &[
1573 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1574 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1575 ]
1576 );
1577 });
1578
1579 // Moving to the end of line again is a no-op.
1580 _ = editor.update(cx, |editor, window, cx| {
1581 editor.move_to_end_of_line(&move_to_end, window, cx);
1582 assert_eq!(
1583 editor.selections.display_ranges(cx),
1584 &[
1585 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1586 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1587 ]
1588 );
1589 });
1590
1591 _ = editor.update(cx, |editor, window, cx| {
1592 editor.move_left(&MoveLeft, window, cx);
1593 editor.select_to_beginning_of_line(
1594 &SelectToBeginningOfLine {
1595 stop_at_soft_wraps: true,
1596 },
1597 window,
1598 cx,
1599 );
1600 assert_eq!(
1601 editor.selections.display_ranges(cx),
1602 &[
1603 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1604 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1605 ]
1606 );
1607 });
1608
1609 _ = editor.update(cx, |editor, window, cx| {
1610 editor.select_to_beginning_of_line(
1611 &SelectToBeginningOfLine {
1612 stop_at_soft_wraps: true,
1613 },
1614 window,
1615 cx,
1616 );
1617 assert_eq!(
1618 editor.selections.display_ranges(cx),
1619 &[
1620 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1621 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1622 ]
1623 );
1624 });
1625
1626 _ = editor.update(cx, |editor, window, cx| {
1627 editor.select_to_beginning_of_line(
1628 &SelectToBeginningOfLine {
1629 stop_at_soft_wraps: true,
1630 },
1631 window,
1632 cx,
1633 );
1634 assert_eq!(
1635 editor.selections.display_ranges(cx),
1636 &[
1637 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1638 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1639 ]
1640 );
1641 });
1642
1643 _ = editor.update(cx, |editor, window, cx| {
1644 editor.select_to_end_of_line(
1645 &SelectToEndOfLine {
1646 stop_at_soft_wraps: true,
1647 },
1648 window,
1649 cx,
1650 );
1651 assert_eq!(
1652 editor.selections.display_ranges(cx),
1653 &[
1654 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1655 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1656 ]
1657 );
1658 });
1659
1660 _ = editor.update(cx, |editor, window, cx| {
1661 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1662 assert_eq!(editor.display_text(cx), "ab\n de");
1663 assert_eq!(
1664 editor.selections.display_ranges(cx),
1665 &[
1666 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1667 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1668 ]
1669 );
1670 });
1671
1672 _ = editor.update(cx, |editor, window, cx| {
1673 editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx);
1674 assert_eq!(editor.display_text(cx), "\n");
1675 assert_eq!(
1676 editor.selections.display_ranges(cx),
1677 &[
1678 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1679 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1680 ]
1681 );
1682 });
1683}
1684
1685#[gpui::test]
1686fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1687 init_test(cx, |_| {});
1688 let move_to_beg = MoveToBeginningOfLine {
1689 stop_at_soft_wraps: false,
1690 };
1691
1692 let move_to_end = MoveToEndOfLine {
1693 stop_at_soft_wraps: false,
1694 };
1695
1696 let editor = cx.add_window(|window, cx| {
1697 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1698 build_editor(buffer, window, cx)
1699 });
1700
1701 _ = editor.update(cx, |editor, window, cx| {
1702 editor.set_wrap_width(Some(140.0.into()), cx);
1703
1704 // We expect the following lines after wrapping
1705 // ```
1706 // thequickbrownfox
1707 // jumpedoverthelazydo
1708 // gs
1709 // ```
1710 // The final `gs` was soft-wrapped onto a new line.
1711 assert_eq!(
1712 "thequickbrownfox\njumpedoverthelaz\nydogs",
1713 editor.display_text(cx),
1714 );
1715
1716 // First, let's assert behavior on the first line, that was not soft-wrapped.
1717 // Start the cursor at the `k` on the first line
1718 editor.change_selections(None, window, cx, |s| {
1719 s.select_display_ranges([
1720 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1721 ]);
1722 });
1723
1724 // Moving to the beginning of the line should put us at the beginning of the line.
1725 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1726 assert_eq!(
1727 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1728 editor.selections.display_ranges(cx)
1729 );
1730
1731 // Moving to the end of the line should put us at the end of the line.
1732 editor.move_to_end_of_line(&move_to_end, window, cx);
1733 assert_eq!(
1734 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1735 editor.selections.display_ranges(cx)
1736 );
1737
1738 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1739 // Start the cursor at the last line (`y` that was wrapped to a new line)
1740 editor.change_selections(None, window, cx, |s| {
1741 s.select_display_ranges([
1742 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1743 ]);
1744 });
1745
1746 // Moving to the beginning of the line should put us at the start of the second line of
1747 // display text, i.e., the `j`.
1748 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1749 assert_eq!(
1750 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1751 editor.selections.display_ranges(cx)
1752 );
1753
1754 // Moving to the beginning of the line again should be a no-op.
1755 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1756 assert_eq!(
1757 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1758 editor.selections.display_ranges(cx)
1759 );
1760
1761 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1762 // next display line.
1763 editor.move_to_end_of_line(&move_to_end, window, cx);
1764 assert_eq!(
1765 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1766 editor.selections.display_ranges(cx)
1767 );
1768
1769 // Moving to the end of the line again should be a no-op.
1770 editor.move_to_end_of_line(&move_to_end, window, cx);
1771 assert_eq!(
1772 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1773 editor.selections.display_ranges(cx)
1774 );
1775 });
1776}
1777
1778#[gpui::test]
1779fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1780 init_test(cx, |_| {});
1781
1782 let editor = cx.add_window(|window, cx| {
1783 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1784 build_editor(buffer, window, cx)
1785 });
1786 _ = editor.update(cx, |editor, window, cx| {
1787 editor.change_selections(None, window, cx, |s| {
1788 s.select_display_ranges([
1789 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1790 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1791 ])
1792 });
1793
1794 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1795 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1796
1797 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1798 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1799
1800 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1801 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1802
1803 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1804 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1805
1806 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1807 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1808
1809 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1810 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1811
1812 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1813 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1814
1815 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1816 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1817
1818 editor.move_right(&MoveRight, window, cx);
1819 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1820 assert_selection_ranges(
1821 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1822 editor,
1823 cx,
1824 );
1825
1826 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1827 assert_selection_ranges(
1828 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1829 editor,
1830 cx,
1831 );
1832
1833 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1834 assert_selection_ranges(
1835 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1836 editor,
1837 cx,
1838 );
1839 });
1840}
1841
1842#[gpui::test]
1843fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1844 init_test(cx, |_| {});
1845
1846 let editor = cx.add_window(|window, cx| {
1847 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1848 build_editor(buffer, window, cx)
1849 });
1850
1851 _ = editor.update(cx, |editor, window, cx| {
1852 editor.set_wrap_width(Some(140.0.into()), cx);
1853 assert_eq!(
1854 editor.display_text(cx),
1855 "use one::{\n two::three::\n four::five\n};"
1856 );
1857
1858 editor.change_selections(None, window, cx, |s| {
1859 s.select_display_ranges([
1860 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1861 ]);
1862 });
1863
1864 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1865 assert_eq!(
1866 editor.selections.display_ranges(cx),
1867 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1868 );
1869
1870 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1871 assert_eq!(
1872 editor.selections.display_ranges(cx),
1873 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1874 );
1875
1876 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1877 assert_eq!(
1878 editor.selections.display_ranges(cx),
1879 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1880 );
1881
1882 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1883 assert_eq!(
1884 editor.selections.display_ranges(cx),
1885 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
1886 );
1887
1888 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1889 assert_eq!(
1890 editor.selections.display_ranges(cx),
1891 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1892 );
1893
1894 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1895 assert_eq!(
1896 editor.selections.display_ranges(cx),
1897 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1898 );
1899 });
1900}
1901
1902#[gpui::test]
1903async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1904 init_test(cx, |_| {});
1905 let mut cx = EditorTestContext::new(cx).await;
1906
1907 let line_height = cx.editor(|editor, window, _| {
1908 editor
1909 .style()
1910 .unwrap()
1911 .text
1912 .line_height_in_pixels(window.rem_size())
1913 });
1914 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
1915
1916 cx.set_state(
1917 &r#"ˇone
1918 two
1919
1920 three
1921 fourˇ
1922 five
1923
1924 six"#
1925 .unindent(),
1926 );
1927
1928 cx.update_editor(|editor, window, cx| {
1929 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1930 });
1931 cx.assert_editor_state(
1932 &r#"one
1933 two
1934 ˇ
1935 three
1936 four
1937 five
1938 ˇ
1939 six"#
1940 .unindent(),
1941 );
1942
1943 cx.update_editor(|editor, window, cx| {
1944 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1945 });
1946 cx.assert_editor_state(
1947 &r#"one
1948 two
1949
1950 three
1951 four
1952 five
1953 ˇ
1954 sixˇ"#
1955 .unindent(),
1956 );
1957
1958 cx.update_editor(|editor, window, cx| {
1959 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1960 });
1961 cx.assert_editor_state(
1962 &r#"one
1963 two
1964
1965 three
1966 four
1967 five
1968
1969 sixˇ"#
1970 .unindent(),
1971 );
1972
1973 cx.update_editor(|editor, window, cx| {
1974 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
1975 });
1976 cx.assert_editor_state(
1977 &r#"one
1978 two
1979
1980 three
1981 four
1982 five
1983 ˇ
1984 six"#
1985 .unindent(),
1986 );
1987
1988 cx.update_editor(|editor, window, cx| {
1989 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
1990 });
1991 cx.assert_editor_state(
1992 &r#"one
1993 two
1994 ˇ
1995 three
1996 four
1997 five
1998
1999 six"#
2000 .unindent(),
2001 );
2002
2003 cx.update_editor(|editor, window, cx| {
2004 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2005 });
2006 cx.assert_editor_state(
2007 &r#"ˇone
2008 two
2009
2010 three
2011 four
2012 five
2013
2014 six"#
2015 .unindent(),
2016 );
2017}
2018
2019#[gpui::test]
2020async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
2021 init_test(cx, |_| {});
2022 let mut cx = EditorTestContext::new(cx).await;
2023 let line_height = cx.editor(|editor, window, _| {
2024 editor
2025 .style()
2026 .unwrap()
2027 .text
2028 .line_height_in_pixels(window.rem_size())
2029 });
2030 let window = cx.window;
2031 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2032
2033 cx.set_state(
2034 r#"ˇone
2035 two
2036 three
2037 four
2038 five
2039 six
2040 seven
2041 eight
2042 nine
2043 ten
2044 "#,
2045 );
2046
2047 cx.update_editor(|editor, window, cx| {
2048 assert_eq!(
2049 editor.snapshot(window, cx).scroll_position(),
2050 gpui::Point::new(0., 0.)
2051 );
2052 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2053 assert_eq!(
2054 editor.snapshot(window, cx).scroll_position(),
2055 gpui::Point::new(0., 3.)
2056 );
2057 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2058 assert_eq!(
2059 editor.snapshot(window, cx).scroll_position(),
2060 gpui::Point::new(0., 6.)
2061 );
2062 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2063 assert_eq!(
2064 editor.snapshot(window, cx).scroll_position(),
2065 gpui::Point::new(0., 3.)
2066 );
2067
2068 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2069 assert_eq!(
2070 editor.snapshot(window, cx).scroll_position(),
2071 gpui::Point::new(0., 1.)
2072 );
2073 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2074 assert_eq!(
2075 editor.snapshot(window, cx).scroll_position(),
2076 gpui::Point::new(0., 3.)
2077 );
2078 });
2079}
2080
2081#[gpui::test]
2082async fn test_autoscroll(cx: &mut gpui::TestAppContext) {
2083 init_test(cx, |_| {});
2084 let mut cx = EditorTestContext::new(cx).await;
2085
2086 let line_height = cx.update_editor(|editor, window, cx| {
2087 editor.set_vertical_scroll_margin(2, cx);
2088 editor
2089 .style()
2090 .unwrap()
2091 .text
2092 .line_height_in_pixels(window.rem_size())
2093 });
2094 let window = cx.window;
2095 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2096
2097 cx.set_state(
2098 r#"ˇone
2099 two
2100 three
2101 four
2102 five
2103 six
2104 seven
2105 eight
2106 nine
2107 ten
2108 "#,
2109 );
2110 cx.update_editor(|editor, window, cx| {
2111 assert_eq!(
2112 editor.snapshot(window, cx).scroll_position(),
2113 gpui::Point::new(0., 0.0)
2114 );
2115 });
2116
2117 // Add a cursor below the visible area. Since both cursors cannot fit
2118 // on screen, the editor autoscrolls to reveal the newest cursor, and
2119 // allows the vertical scroll margin below that cursor.
2120 cx.update_editor(|editor, window, cx| {
2121 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2122 selections.select_ranges([
2123 Point::new(0, 0)..Point::new(0, 0),
2124 Point::new(6, 0)..Point::new(6, 0),
2125 ]);
2126 })
2127 });
2128 cx.update_editor(|editor, window, cx| {
2129 assert_eq!(
2130 editor.snapshot(window, cx).scroll_position(),
2131 gpui::Point::new(0., 3.0)
2132 );
2133 });
2134
2135 // Move down. The editor cursor scrolls down to track the newest cursor.
2136 cx.update_editor(|editor, window, cx| {
2137 editor.move_down(&Default::default(), window, cx);
2138 });
2139 cx.update_editor(|editor, window, cx| {
2140 assert_eq!(
2141 editor.snapshot(window, cx).scroll_position(),
2142 gpui::Point::new(0., 4.0)
2143 );
2144 });
2145
2146 // Add a cursor above the visible area. Since both cursors fit on screen,
2147 // the editor scrolls to show both.
2148 cx.update_editor(|editor, window, cx| {
2149 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2150 selections.select_ranges([
2151 Point::new(1, 0)..Point::new(1, 0),
2152 Point::new(6, 0)..Point::new(6, 0),
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., 1.0)
2160 );
2161 });
2162}
2163
2164#[gpui::test]
2165async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
2166 init_test(cx, |_| {});
2167 let mut cx = EditorTestContext::new(cx).await;
2168
2169 let line_height = cx.editor(|editor, window, _cx| {
2170 editor
2171 .style()
2172 .unwrap()
2173 .text
2174 .line_height_in_pixels(window.rem_size())
2175 });
2176 let window = cx.window;
2177 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2178 cx.set_state(
2179 &r#"
2180 ˇone
2181 two
2182 threeˇ
2183 four
2184 five
2185 six
2186 seven
2187 eight
2188 nine
2189 ten
2190 "#
2191 .unindent(),
2192 );
2193
2194 cx.update_editor(|editor, window, cx| {
2195 editor.move_page_down(&MovePageDown::default(), window, cx)
2196 });
2197 cx.assert_editor_state(
2198 &r#"
2199 one
2200 two
2201 three
2202 ˇfour
2203 five
2204 sixˇ
2205 seven
2206 eight
2207 nine
2208 ten
2209 "#
2210 .unindent(),
2211 );
2212
2213 cx.update_editor(|editor, window, cx| {
2214 editor.move_page_down(&MovePageDown::default(), window, cx)
2215 });
2216 cx.assert_editor_state(
2217 &r#"
2218 one
2219 two
2220 three
2221 four
2222 five
2223 six
2224 ˇseven
2225 eight
2226 nineˇ
2227 ten
2228 "#
2229 .unindent(),
2230 );
2231
2232 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2233 cx.assert_editor_state(
2234 &r#"
2235 one
2236 two
2237 three
2238 ˇfour
2239 five
2240 sixˇ
2241 seven
2242 eight
2243 nine
2244 ten
2245 "#
2246 .unindent(),
2247 );
2248
2249 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2250 cx.assert_editor_state(
2251 &r#"
2252 ˇone
2253 two
2254 threeˇ
2255 four
2256 five
2257 six
2258 seven
2259 eight
2260 nine
2261 ten
2262 "#
2263 .unindent(),
2264 );
2265
2266 // Test select collapsing
2267 cx.update_editor(|editor, window, cx| {
2268 editor.move_page_down(&MovePageDown::default(), window, cx);
2269 editor.move_page_down(&MovePageDown::default(), window, cx);
2270 editor.move_page_down(&MovePageDown::default(), window, cx);
2271 });
2272 cx.assert_editor_state(
2273 &r#"
2274 one
2275 two
2276 three
2277 four
2278 five
2279 six
2280 seven
2281 eight
2282 nine
2283 ˇten
2284 ˇ"#
2285 .unindent(),
2286 );
2287}
2288
2289#[gpui::test]
2290async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
2291 init_test(cx, |_| {});
2292 let mut cx = EditorTestContext::new(cx).await;
2293 cx.set_state("one «two threeˇ» four");
2294 cx.update_editor(|editor, window, cx| {
2295 editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx);
2296 assert_eq!(editor.text(cx), " four");
2297 });
2298}
2299
2300#[gpui::test]
2301fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2302 init_test(cx, |_| {});
2303
2304 let editor = cx.add_window(|window, cx| {
2305 let buffer = MultiBuffer::build_simple("one two three four", cx);
2306 build_editor(buffer.clone(), window, cx)
2307 });
2308
2309 _ = editor.update(cx, |editor, window, cx| {
2310 editor.change_selections(None, window, cx, |s| {
2311 s.select_display_ranges([
2312 // an empty selection - the preceding word fragment is deleted
2313 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2314 // characters selected - they are deleted
2315 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2316 ])
2317 });
2318 editor.delete_to_previous_word_start(
2319 &DeleteToPreviousWordStart {
2320 ignore_newlines: false,
2321 },
2322 window,
2323 cx,
2324 );
2325 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2326 });
2327
2328 _ = editor.update(cx, |editor, window, cx| {
2329 editor.change_selections(None, window, cx, |s| {
2330 s.select_display_ranges([
2331 // an empty selection - the following word fragment is deleted
2332 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2333 // characters selected - they are deleted
2334 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2335 ])
2336 });
2337 editor.delete_to_next_word_end(
2338 &DeleteToNextWordEnd {
2339 ignore_newlines: false,
2340 },
2341 window,
2342 cx,
2343 );
2344 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2345 });
2346}
2347
2348#[gpui::test]
2349fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2350 init_test(cx, |_| {});
2351
2352 let editor = cx.add_window(|window, cx| {
2353 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2354 build_editor(buffer.clone(), window, cx)
2355 });
2356 let del_to_prev_word_start = DeleteToPreviousWordStart {
2357 ignore_newlines: false,
2358 };
2359 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2360 ignore_newlines: true,
2361 };
2362
2363 _ = editor.update(cx, |editor, window, cx| {
2364 editor.change_selections(None, window, cx, |s| {
2365 s.select_display_ranges([
2366 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2367 ])
2368 });
2369 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2370 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2371 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2372 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2373 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2374 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2375 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2376 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2377 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2378 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2379 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2380 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2381 });
2382}
2383
2384#[gpui::test]
2385fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2386 init_test(cx, |_| {});
2387
2388 let editor = cx.add_window(|window, cx| {
2389 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2390 build_editor(buffer.clone(), window, cx)
2391 });
2392 let del_to_next_word_end = DeleteToNextWordEnd {
2393 ignore_newlines: false,
2394 };
2395 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2396 ignore_newlines: true,
2397 };
2398
2399 _ = editor.update(cx, |editor, window, cx| {
2400 editor.change_selections(None, window, cx, |s| {
2401 s.select_display_ranges([
2402 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2403 ])
2404 });
2405 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2406 assert_eq!(
2407 editor.buffer.read(cx).read(cx).text(),
2408 "one\n two\nthree\n four"
2409 );
2410 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2411 assert_eq!(
2412 editor.buffer.read(cx).read(cx).text(),
2413 "\n two\nthree\n four"
2414 );
2415 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2416 assert_eq!(
2417 editor.buffer.read(cx).read(cx).text(),
2418 "two\nthree\n four"
2419 );
2420 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2421 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2422 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2423 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2424 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2425 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2426 });
2427}
2428
2429#[gpui::test]
2430fn test_newline(cx: &mut TestAppContext) {
2431 init_test(cx, |_| {});
2432
2433 let editor = cx.add_window(|window, cx| {
2434 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2435 build_editor(buffer.clone(), window, cx)
2436 });
2437
2438 _ = editor.update(cx, |editor, window, cx| {
2439 editor.change_selections(None, window, cx, |s| {
2440 s.select_display_ranges([
2441 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2442 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2443 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2444 ])
2445 });
2446
2447 editor.newline(&Newline, window, cx);
2448 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2449 });
2450}
2451
2452#[gpui::test]
2453fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2454 init_test(cx, |_| {});
2455
2456 let editor = cx.add_window(|window, cx| {
2457 let buffer = MultiBuffer::build_simple(
2458 "
2459 a
2460 b(
2461 X
2462 )
2463 c(
2464 X
2465 )
2466 "
2467 .unindent()
2468 .as_str(),
2469 cx,
2470 );
2471 let mut editor = build_editor(buffer.clone(), window, cx);
2472 editor.change_selections(None, window, cx, |s| {
2473 s.select_ranges([
2474 Point::new(2, 4)..Point::new(2, 5),
2475 Point::new(5, 4)..Point::new(5, 5),
2476 ])
2477 });
2478 editor
2479 });
2480
2481 _ = editor.update(cx, |editor, window, cx| {
2482 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2483 editor.buffer.update(cx, |buffer, cx| {
2484 buffer.edit(
2485 [
2486 (Point::new(1, 2)..Point::new(3, 0), ""),
2487 (Point::new(4, 2)..Point::new(6, 0), ""),
2488 ],
2489 None,
2490 cx,
2491 );
2492 assert_eq!(
2493 buffer.read(cx).text(),
2494 "
2495 a
2496 b()
2497 c()
2498 "
2499 .unindent()
2500 );
2501 });
2502 assert_eq!(
2503 editor.selections.ranges(cx),
2504 &[
2505 Point::new(1, 2)..Point::new(1, 2),
2506 Point::new(2, 2)..Point::new(2, 2),
2507 ],
2508 );
2509
2510 editor.newline(&Newline, window, cx);
2511 assert_eq!(
2512 editor.text(cx),
2513 "
2514 a
2515 b(
2516 )
2517 c(
2518 )
2519 "
2520 .unindent()
2521 );
2522
2523 // The selections are moved after the inserted newlines
2524 assert_eq!(
2525 editor.selections.ranges(cx),
2526 &[
2527 Point::new(2, 0)..Point::new(2, 0),
2528 Point::new(4, 0)..Point::new(4, 0),
2529 ],
2530 );
2531 });
2532}
2533
2534#[gpui::test]
2535async fn test_newline_above(cx: &mut gpui::TestAppContext) {
2536 init_test(cx, |settings| {
2537 settings.defaults.tab_size = NonZeroU32::new(4)
2538 });
2539
2540 let language = Arc::new(
2541 Language::new(
2542 LanguageConfig::default(),
2543 Some(tree_sitter_rust::LANGUAGE.into()),
2544 )
2545 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2546 .unwrap(),
2547 );
2548
2549 let mut cx = EditorTestContext::new(cx).await;
2550 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2551 cx.set_state(indoc! {"
2552 const a: ˇA = (
2553 (ˇ
2554 «const_functionˇ»(ˇ),
2555 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2556 )ˇ
2557 ˇ);ˇ
2558 "});
2559
2560 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2561 cx.assert_editor_state(indoc! {"
2562 ˇ
2563 const a: A = (
2564 ˇ
2565 (
2566 ˇ
2567 ˇ
2568 const_function(),
2569 ˇ
2570 ˇ
2571 ˇ
2572 ˇ
2573 something_else,
2574 ˇ
2575 )
2576 ˇ
2577 ˇ
2578 );
2579 "});
2580}
2581
2582#[gpui::test]
2583async fn test_newline_below(cx: &mut gpui::TestAppContext) {
2584 init_test(cx, |settings| {
2585 settings.defaults.tab_size = NonZeroU32::new(4)
2586 });
2587
2588 let language = Arc::new(
2589 Language::new(
2590 LanguageConfig::default(),
2591 Some(tree_sitter_rust::LANGUAGE.into()),
2592 )
2593 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2594 .unwrap(),
2595 );
2596
2597 let mut cx = EditorTestContext::new(cx).await;
2598 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2599 cx.set_state(indoc! {"
2600 const a: ˇA = (
2601 (ˇ
2602 «const_functionˇ»(ˇ),
2603 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2604 )ˇ
2605 ˇ);ˇ
2606 "});
2607
2608 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2609 cx.assert_editor_state(indoc! {"
2610 const a: A = (
2611 ˇ
2612 (
2613 ˇ
2614 const_function(),
2615 ˇ
2616 ˇ
2617 something_else,
2618 ˇ
2619 ˇ
2620 ˇ
2621 ˇ
2622 )
2623 ˇ
2624 );
2625 ˇ
2626 ˇ
2627 "});
2628}
2629
2630#[gpui::test]
2631async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
2632 init_test(cx, |settings| {
2633 settings.defaults.tab_size = NonZeroU32::new(4)
2634 });
2635
2636 let language = Arc::new(Language::new(
2637 LanguageConfig {
2638 line_comments: vec!["//".into()],
2639 ..LanguageConfig::default()
2640 },
2641 None,
2642 ));
2643 {
2644 let mut cx = EditorTestContext::new(cx).await;
2645 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2646 cx.set_state(indoc! {"
2647 // Fooˇ
2648 "});
2649
2650 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2651 cx.assert_editor_state(indoc! {"
2652 // Foo
2653 //ˇ
2654 "});
2655 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2656 cx.set_state(indoc! {"
2657 ˇ// Foo
2658 "});
2659 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2660 cx.assert_editor_state(indoc! {"
2661
2662 ˇ// Foo
2663 "});
2664 }
2665 // Ensure that comment continuations can be disabled.
2666 update_test_language_settings(cx, |settings| {
2667 settings.defaults.extend_comment_on_newline = Some(false);
2668 });
2669 let mut cx = EditorTestContext::new(cx).await;
2670 cx.set_state(indoc! {"
2671 // Fooˇ
2672 "});
2673 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2674 cx.assert_editor_state(indoc! {"
2675 // Foo
2676 ˇ
2677 "});
2678}
2679
2680#[gpui::test]
2681fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2682 init_test(cx, |_| {});
2683
2684 let editor = cx.add_window(|window, cx| {
2685 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2686 let mut editor = build_editor(buffer.clone(), window, cx);
2687 editor.change_selections(None, window, cx, |s| {
2688 s.select_ranges([3..4, 11..12, 19..20])
2689 });
2690 editor
2691 });
2692
2693 _ = editor.update(cx, |editor, window, cx| {
2694 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2695 editor.buffer.update(cx, |buffer, cx| {
2696 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2697 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2698 });
2699 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2700
2701 editor.insert("Z", window, cx);
2702 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2703
2704 // The selections are moved after the inserted characters
2705 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2706 });
2707}
2708
2709#[gpui::test]
2710async fn test_tab(cx: &mut gpui::TestAppContext) {
2711 init_test(cx, |settings| {
2712 settings.defaults.tab_size = NonZeroU32::new(3)
2713 });
2714
2715 let mut cx = EditorTestContext::new(cx).await;
2716 cx.set_state(indoc! {"
2717 ˇabˇc
2718 ˇ🏀ˇ🏀ˇefg
2719 dˇ
2720 "});
2721 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2722 cx.assert_editor_state(indoc! {"
2723 ˇab ˇc
2724 ˇ🏀 ˇ🏀 ˇefg
2725 d ˇ
2726 "});
2727
2728 cx.set_state(indoc! {"
2729 a
2730 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2731 "});
2732 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2733 cx.assert_editor_state(indoc! {"
2734 a
2735 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2736 "});
2737}
2738
2739#[gpui::test]
2740async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) {
2741 init_test(cx, |_| {});
2742
2743 let mut cx = EditorTestContext::new(cx).await;
2744 let language = Arc::new(
2745 Language::new(
2746 LanguageConfig::default(),
2747 Some(tree_sitter_rust::LANGUAGE.into()),
2748 )
2749 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2750 .unwrap(),
2751 );
2752 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2753
2754 // cursors that are already at the suggested indent level insert
2755 // a soft tab. cursors that are to the left of the suggested indent
2756 // auto-indent their line.
2757 cx.set_state(indoc! {"
2758 ˇ
2759 const a: B = (
2760 c(
2761 d(
2762 ˇ
2763 )
2764 ˇ
2765 ˇ )
2766 );
2767 "});
2768 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2769 cx.assert_editor_state(indoc! {"
2770 ˇ
2771 const a: B = (
2772 c(
2773 d(
2774 ˇ
2775 )
2776 ˇ
2777 ˇ)
2778 );
2779 "});
2780
2781 // handle auto-indent when there are multiple cursors on the same line
2782 cx.set_state(indoc! {"
2783 const a: B = (
2784 c(
2785 ˇ ˇ
2786 ˇ )
2787 );
2788 "});
2789 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2790 cx.assert_editor_state(indoc! {"
2791 const a: B = (
2792 c(
2793 ˇ
2794 ˇ)
2795 );
2796 "});
2797}
2798
2799#[gpui::test]
2800async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
2801 init_test(cx, |settings| {
2802 settings.defaults.tab_size = NonZeroU32::new(4)
2803 });
2804
2805 let language = Arc::new(
2806 Language::new(
2807 LanguageConfig::default(),
2808 Some(tree_sitter_rust::LANGUAGE.into()),
2809 )
2810 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2811 .unwrap(),
2812 );
2813
2814 let mut cx = EditorTestContext::new(cx).await;
2815 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2816 cx.set_state(indoc! {"
2817 fn a() {
2818 if b {
2819 \t ˇc
2820 }
2821 }
2822 "});
2823
2824 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2825 cx.assert_editor_state(indoc! {"
2826 fn a() {
2827 if b {
2828 ˇc
2829 }
2830 }
2831 "});
2832}
2833
2834#[gpui::test]
2835async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
2836 init_test(cx, |settings| {
2837 settings.defaults.tab_size = NonZeroU32::new(4);
2838 });
2839
2840 let mut cx = EditorTestContext::new(cx).await;
2841
2842 cx.set_state(indoc! {"
2843 «oneˇ» «twoˇ»
2844 three
2845 four
2846 "});
2847 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2848 cx.assert_editor_state(indoc! {"
2849 «oneˇ» «twoˇ»
2850 three
2851 four
2852 "});
2853
2854 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2855 cx.assert_editor_state(indoc! {"
2856 «oneˇ» «twoˇ»
2857 three
2858 four
2859 "});
2860
2861 // select across line ending
2862 cx.set_state(indoc! {"
2863 one two
2864 t«hree
2865 ˇ» four
2866 "});
2867 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2868 cx.assert_editor_state(indoc! {"
2869 one two
2870 t«hree
2871 ˇ» four
2872 "});
2873
2874 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2875 cx.assert_editor_state(indoc! {"
2876 one two
2877 t«hree
2878 ˇ» four
2879 "});
2880
2881 // Ensure that indenting/outdenting works when the cursor is at column 0.
2882 cx.set_state(indoc! {"
2883 one two
2884 ˇthree
2885 four
2886 "});
2887 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2888 cx.assert_editor_state(indoc! {"
2889 one two
2890 ˇthree
2891 four
2892 "});
2893
2894 cx.set_state(indoc! {"
2895 one two
2896 ˇ three
2897 four
2898 "});
2899 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2900 cx.assert_editor_state(indoc! {"
2901 one two
2902 ˇthree
2903 four
2904 "});
2905}
2906
2907#[gpui::test]
2908async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
2909 init_test(cx, |settings| {
2910 settings.defaults.hard_tabs = Some(true);
2911 });
2912
2913 let mut cx = EditorTestContext::new(cx).await;
2914
2915 // select two ranges on one line
2916 cx.set_state(indoc! {"
2917 «oneˇ» «twoˇ»
2918 three
2919 four
2920 "});
2921 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2922 cx.assert_editor_state(indoc! {"
2923 \t«oneˇ» «twoˇ»
2924 three
2925 four
2926 "});
2927 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2928 cx.assert_editor_state(indoc! {"
2929 \t\t«oneˇ» «twoˇ»
2930 three
2931 four
2932 "});
2933 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2934 cx.assert_editor_state(indoc! {"
2935 \t«oneˇ» «twoˇ»
2936 three
2937 four
2938 "});
2939 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2940 cx.assert_editor_state(indoc! {"
2941 «oneˇ» «twoˇ»
2942 three
2943 four
2944 "});
2945
2946 // select across a line ending
2947 cx.set_state(indoc! {"
2948 one two
2949 t«hree
2950 ˇ»four
2951 "});
2952 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2953 cx.assert_editor_state(indoc! {"
2954 one two
2955 \tt«hree
2956 ˇ»four
2957 "});
2958 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2959 cx.assert_editor_state(indoc! {"
2960 one two
2961 \t\tt«hree
2962 ˇ»four
2963 "});
2964 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2965 cx.assert_editor_state(indoc! {"
2966 one two
2967 \tt«hree
2968 ˇ»four
2969 "});
2970 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2971 cx.assert_editor_state(indoc! {"
2972 one two
2973 t«hree
2974 ˇ»four
2975 "});
2976
2977 // Ensure that indenting/outdenting works when the cursor is at column 0.
2978 cx.set_state(indoc! {"
2979 one two
2980 ˇthree
2981 four
2982 "});
2983 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2984 cx.assert_editor_state(indoc! {"
2985 one two
2986 ˇthree
2987 four
2988 "});
2989 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2990 cx.assert_editor_state(indoc! {"
2991 one two
2992 \tˇthree
2993 four
2994 "});
2995 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2996 cx.assert_editor_state(indoc! {"
2997 one two
2998 ˇthree
2999 four
3000 "});
3001}
3002
3003#[gpui::test]
3004fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3005 init_test(cx, |settings| {
3006 settings.languages.extend([
3007 (
3008 "TOML".into(),
3009 LanguageSettingsContent {
3010 tab_size: NonZeroU32::new(2),
3011 ..Default::default()
3012 },
3013 ),
3014 (
3015 "Rust".into(),
3016 LanguageSettingsContent {
3017 tab_size: NonZeroU32::new(4),
3018 ..Default::default()
3019 },
3020 ),
3021 ]);
3022 });
3023
3024 let toml_language = Arc::new(Language::new(
3025 LanguageConfig {
3026 name: "TOML".into(),
3027 ..Default::default()
3028 },
3029 None,
3030 ));
3031 let rust_language = Arc::new(Language::new(
3032 LanguageConfig {
3033 name: "Rust".into(),
3034 ..Default::default()
3035 },
3036 None,
3037 ));
3038
3039 let toml_buffer =
3040 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3041 let rust_buffer =
3042 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3043 let multibuffer = cx.new(|cx| {
3044 let mut multibuffer = MultiBuffer::new(ReadWrite);
3045 multibuffer.push_excerpts(
3046 toml_buffer.clone(),
3047 [ExcerptRange {
3048 context: Point::new(0, 0)..Point::new(2, 0),
3049 primary: None,
3050 }],
3051 cx,
3052 );
3053 multibuffer.push_excerpts(
3054 rust_buffer.clone(),
3055 [ExcerptRange {
3056 context: Point::new(0, 0)..Point::new(1, 0),
3057 primary: None,
3058 }],
3059 cx,
3060 );
3061 multibuffer
3062 });
3063
3064 cx.add_window(|window, cx| {
3065 let mut editor = build_editor(multibuffer, window, cx);
3066
3067 assert_eq!(
3068 editor.text(cx),
3069 indoc! {"
3070 a = 1
3071 b = 2
3072
3073 const c: usize = 3;
3074 "}
3075 );
3076
3077 select_ranges(
3078 &mut editor,
3079 indoc! {"
3080 «aˇ» = 1
3081 b = 2
3082
3083 «const c:ˇ» usize = 3;
3084 "},
3085 window,
3086 cx,
3087 );
3088
3089 editor.tab(&Tab, window, cx);
3090 assert_text_with_selections(
3091 &mut editor,
3092 indoc! {"
3093 «aˇ» = 1
3094 b = 2
3095
3096 «const c:ˇ» usize = 3;
3097 "},
3098 cx,
3099 );
3100 editor.tab_prev(&TabPrev, window, cx);
3101 assert_text_with_selections(
3102 &mut editor,
3103 indoc! {"
3104 «aˇ» = 1
3105 b = 2
3106
3107 «const c:ˇ» usize = 3;
3108 "},
3109 cx,
3110 );
3111
3112 editor
3113 });
3114}
3115
3116#[gpui::test]
3117async fn test_backspace(cx: &mut gpui::TestAppContext) {
3118 init_test(cx, |_| {});
3119
3120 let mut cx = EditorTestContext::new(cx).await;
3121
3122 // Basic backspace
3123 cx.set_state(indoc! {"
3124 onˇe two three
3125 fou«rˇ» five six
3126 seven «ˇeight nine
3127 »ten
3128 "});
3129 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3130 cx.assert_editor_state(indoc! {"
3131 oˇe two three
3132 fouˇ five six
3133 seven ˇten
3134 "});
3135
3136 // Test backspace inside and around indents
3137 cx.set_state(indoc! {"
3138 zero
3139 ˇone
3140 ˇtwo
3141 ˇ ˇ ˇ three
3142 ˇ ˇ four
3143 "});
3144 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3145 cx.assert_editor_state(indoc! {"
3146 zero
3147 ˇone
3148 ˇtwo
3149 ˇ threeˇ four
3150 "});
3151
3152 // Test backspace with line_mode set to true
3153 cx.update_editor(|e, _, _| e.selections.line_mode = true);
3154 cx.set_state(indoc! {"
3155 The ˇquick ˇbrown
3156 fox jumps over
3157 the lazy dog
3158 ˇThe qu«ick bˇ»rown"});
3159 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3160 cx.assert_editor_state(indoc! {"
3161 ˇfox jumps over
3162 the lazy dogˇ"});
3163}
3164
3165#[gpui::test]
3166async fn test_delete(cx: &mut gpui::TestAppContext) {
3167 init_test(cx, |_| {});
3168
3169 let mut cx = EditorTestContext::new(cx).await;
3170 cx.set_state(indoc! {"
3171 onˇe two three
3172 fou«rˇ» five six
3173 seven «ˇeight nine
3174 »ten
3175 "});
3176 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3177 cx.assert_editor_state(indoc! {"
3178 onˇ two three
3179 fouˇ five six
3180 seven ˇten
3181 "});
3182
3183 // Test backspace with line_mode set to true
3184 cx.update_editor(|e, _, _| e.selections.line_mode = true);
3185 cx.set_state(indoc! {"
3186 The ˇquick ˇbrown
3187 fox «ˇjum»ps over
3188 the lazy dog
3189 ˇThe qu«ick bˇ»rown"});
3190 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3191 cx.assert_editor_state("ˇthe lazy dogˇ");
3192}
3193
3194#[gpui::test]
3195fn test_delete_line(cx: &mut TestAppContext) {
3196 init_test(cx, |_| {});
3197
3198 let editor = cx.add_window(|window, cx| {
3199 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3200 build_editor(buffer, window, cx)
3201 });
3202 _ = editor.update(cx, |editor, window, cx| {
3203 editor.change_selections(None, window, cx, |s| {
3204 s.select_display_ranges([
3205 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3206 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3207 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3208 ])
3209 });
3210 editor.delete_line(&DeleteLine, window, cx);
3211 assert_eq!(editor.display_text(cx), "ghi");
3212 assert_eq!(
3213 editor.selections.display_ranges(cx),
3214 vec![
3215 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3216 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3217 ]
3218 );
3219 });
3220
3221 let editor = cx.add_window(|window, cx| {
3222 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3223 build_editor(buffer, window, cx)
3224 });
3225 _ = editor.update(cx, |editor, window, cx| {
3226 editor.change_selections(None, window, cx, |s| {
3227 s.select_display_ranges([
3228 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3229 ])
3230 });
3231 editor.delete_line(&DeleteLine, window, cx);
3232 assert_eq!(editor.display_text(cx), "ghi\n");
3233 assert_eq!(
3234 editor.selections.display_ranges(cx),
3235 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3236 );
3237 });
3238}
3239
3240#[gpui::test]
3241fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3242 init_test(cx, |_| {});
3243
3244 cx.add_window(|window, cx| {
3245 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3246 let mut editor = build_editor(buffer.clone(), window, cx);
3247 let buffer = buffer.read(cx).as_singleton().unwrap();
3248
3249 assert_eq!(
3250 editor.selections.ranges::<Point>(cx),
3251 &[Point::new(0, 0)..Point::new(0, 0)]
3252 );
3253
3254 // When on single line, replace newline at end by space
3255 editor.join_lines(&JoinLines, window, cx);
3256 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3257 assert_eq!(
3258 editor.selections.ranges::<Point>(cx),
3259 &[Point::new(0, 3)..Point::new(0, 3)]
3260 );
3261
3262 // When multiple lines are selected, remove newlines that are spanned by the selection
3263 editor.change_selections(None, window, cx, |s| {
3264 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3265 });
3266 editor.join_lines(&JoinLines, window, cx);
3267 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3268 assert_eq!(
3269 editor.selections.ranges::<Point>(cx),
3270 &[Point::new(0, 11)..Point::new(0, 11)]
3271 );
3272
3273 // Undo should be transactional
3274 editor.undo(&Undo, window, cx);
3275 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3276 assert_eq!(
3277 editor.selections.ranges::<Point>(cx),
3278 &[Point::new(0, 5)..Point::new(2, 2)]
3279 );
3280
3281 // When joining an empty line don't insert a space
3282 editor.change_selections(None, window, cx, |s| {
3283 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3284 });
3285 editor.join_lines(&JoinLines, window, cx);
3286 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3287 assert_eq!(
3288 editor.selections.ranges::<Point>(cx),
3289 [Point::new(2, 3)..Point::new(2, 3)]
3290 );
3291
3292 // We can remove trailing newlines
3293 editor.join_lines(&JoinLines, window, cx);
3294 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3295 assert_eq!(
3296 editor.selections.ranges::<Point>(cx),
3297 [Point::new(2, 3)..Point::new(2, 3)]
3298 );
3299
3300 // We don't blow up on the last line
3301 editor.join_lines(&JoinLines, window, cx);
3302 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3303 assert_eq!(
3304 editor.selections.ranges::<Point>(cx),
3305 [Point::new(2, 3)..Point::new(2, 3)]
3306 );
3307
3308 // reset to test indentation
3309 editor.buffer.update(cx, |buffer, cx| {
3310 buffer.edit(
3311 [
3312 (Point::new(1, 0)..Point::new(1, 2), " "),
3313 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3314 ],
3315 None,
3316 cx,
3317 )
3318 });
3319
3320 // We remove any leading spaces
3321 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3322 editor.change_selections(None, window, cx, |s| {
3323 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3324 });
3325 editor.join_lines(&JoinLines, window, cx);
3326 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3327
3328 // We don't insert a space for a line containing only spaces
3329 editor.join_lines(&JoinLines, window, cx);
3330 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3331
3332 // We ignore any leading tabs
3333 editor.join_lines(&JoinLines, window, cx);
3334 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3335
3336 editor
3337 });
3338}
3339
3340#[gpui::test]
3341fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3342 init_test(cx, |_| {});
3343
3344 cx.add_window(|window, cx| {
3345 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3346 let mut editor = build_editor(buffer.clone(), window, cx);
3347 let buffer = buffer.read(cx).as_singleton().unwrap();
3348
3349 editor.change_selections(None, window, cx, |s| {
3350 s.select_ranges([
3351 Point::new(0, 2)..Point::new(1, 1),
3352 Point::new(1, 2)..Point::new(1, 2),
3353 Point::new(3, 1)..Point::new(3, 2),
3354 ])
3355 });
3356
3357 editor.join_lines(&JoinLines, window, cx);
3358 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3359
3360 assert_eq!(
3361 editor.selections.ranges::<Point>(cx),
3362 [
3363 Point::new(0, 7)..Point::new(0, 7),
3364 Point::new(1, 3)..Point::new(1, 3)
3365 ]
3366 );
3367 editor
3368 });
3369}
3370
3371#[gpui::test]
3372async fn test_join_lines_with_git_diff_base(
3373 executor: BackgroundExecutor,
3374 cx: &mut gpui::TestAppContext,
3375) {
3376 init_test(cx, |_| {});
3377
3378 let mut cx = EditorTestContext::new(cx).await;
3379
3380 let diff_base = r#"
3381 Line 0
3382 Line 1
3383 Line 2
3384 Line 3
3385 "#
3386 .unindent();
3387
3388 cx.set_state(
3389 &r#"
3390 ˇLine 0
3391 Line 1
3392 Line 2
3393 Line 3
3394 "#
3395 .unindent(),
3396 );
3397
3398 cx.set_diff_base(&diff_base);
3399 executor.run_until_parked();
3400
3401 // Join lines
3402 cx.update_editor(|editor, window, cx| {
3403 editor.join_lines(&JoinLines, window, cx);
3404 });
3405 executor.run_until_parked();
3406
3407 cx.assert_editor_state(
3408 &r#"
3409 Line 0ˇ Line 1
3410 Line 2
3411 Line 3
3412 "#
3413 .unindent(),
3414 );
3415 // Join again
3416 cx.update_editor(|editor, window, cx| {
3417 editor.join_lines(&JoinLines, window, cx);
3418 });
3419 executor.run_until_parked();
3420
3421 cx.assert_editor_state(
3422 &r#"
3423 Line 0 Line 1ˇ Line 2
3424 Line 3
3425 "#
3426 .unindent(),
3427 );
3428}
3429
3430#[gpui::test]
3431async fn test_custom_newlines_cause_no_false_positive_diffs(
3432 executor: BackgroundExecutor,
3433 cx: &mut gpui::TestAppContext,
3434) {
3435 init_test(cx, |_| {});
3436 let mut cx = EditorTestContext::new(cx).await;
3437 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3438 cx.set_diff_base("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3439 executor.run_until_parked();
3440
3441 cx.update_editor(|editor, window, cx| {
3442 let snapshot = editor.snapshot(window, cx);
3443 assert_eq!(
3444 snapshot
3445 .buffer_snapshot
3446 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3447 .collect::<Vec<_>>(),
3448 Vec::new(),
3449 "Should not have any diffs for files with custom newlines"
3450 );
3451 });
3452}
3453
3454#[gpui::test]
3455async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3456 init_test(cx, |_| {});
3457
3458 let mut cx = EditorTestContext::new(cx).await;
3459
3460 // Test sort_lines_case_insensitive()
3461 cx.set_state(indoc! {"
3462 «z
3463 y
3464 x
3465 Z
3466 Y
3467 Xˇ»
3468 "});
3469 cx.update_editor(|e, window, cx| {
3470 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3471 });
3472 cx.assert_editor_state(indoc! {"
3473 «x
3474 X
3475 y
3476 Y
3477 z
3478 Zˇ»
3479 "});
3480
3481 // Test reverse_lines()
3482 cx.set_state(indoc! {"
3483 «5
3484 4
3485 3
3486 2
3487 1ˇ»
3488 "});
3489 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3490 cx.assert_editor_state(indoc! {"
3491 «1
3492 2
3493 3
3494 4
3495 5ˇ»
3496 "});
3497
3498 // Skip testing shuffle_line()
3499
3500 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3501 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3502
3503 // Don't manipulate when cursor is on single line, but expand the selection
3504 cx.set_state(indoc! {"
3505 ddˇdd
3506 ccc
3507 bb
3508 a
3509 "});
3510 cx.update_editor(|e, window, cx| {
3511 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3512 });
3513 cx.assert_editor_state(indoc! {"
3514 «ddddˇ»
3515 ccc
3516 bb
3517 a
3518 "});
3519
3520 // Basic manipulate case
3521 // Start selection moves to column 0
3522 // End of selection shrinks to fit shorter line
3523 cx.set_state(indoc! {"
3524 dd«d
3525 ccc
3526 bb
3527 aaaaaˇ»
3528 "});
3529 cx.update_editor(|e, window, cx| {
3530 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3531 });
3532 cx.assert_editor_state(indoc! {"
3533 «aaaaa
3534 bb
3535 ccc
3536 dddˇ»
3537 "});
3538
3539 // Manipulate case with newlines
3540 cx.set_state(indoc! {"
3541 dd«d
3542 ccc
3543
3544 bb
3545 aaaaa
3546
3547 ˇ»
3548 "});
3549 cx.update_editor(|e, window, cx| {
3550 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3551 });
3552 cx.assert_editor_state(indoc! {"
3553 «
3554
3555 aaaaa
3556 bb
3557 ccc
3558 dddˇ»
3559
3560 "});
3561
3562 // Adding new line
3563 cx.set_state(indoc! {"
3564 aa«a
3565 bbˇ»b
3566 "});
3567 cx.update_editor(|e, window, cx| {
3568 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3569 });
3570 cx.assert_editor_state(indoc! {"
3571 «aaa
3572 bbb
3573 added_lineˇ»
3574 "});
3575
3576 // Removing line
3577 cx.set_state(indoc! {"
3578 aa«a
3579 bbbˇ»
3580 "});
3581 cx.update_editor(|e, window, cx| {
3582 e.manipulate_lines(window, cx, |lines| {
3583 lines.pop();
3584 })
3585 });
3586 cx.assert_editor_state(indoc! {"
3587 «aaaˇ»
3588 "});
3589
3590 // Removing all lines
3591 cx.set_state(indoc! {"
3592 aa«a
3593 bbbˇ»
3594 "});
3595 cx.update_editor(|e, window, cx| {
3596 e.manipulate_lines(window, cx, |lines| {
3597 lines.drain(..);
3598 })
3599 });
3600 cx.assert_editor_state(indoc! {"
3601 ˇ
3602 "});
3603}
3604
3605#[gpui::test]
3606async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3607 init_test(cx, |_| {});
3608
3609 let mut cx = EditorTestContext::new(cx).await;
3610
3611 // Consider continuous selection as single selection
3612 cx.set_state(indoc! {"
3613 Aaa«aa
3614 cˇ»c«c
3615 bb
3616 aaaˇ»aa
3617 "});
3618 cx.update_editor(|e, window, cx| {
3619 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3620 });
3621 cx.assert_editor_state(indoc! {"
3622 «Aaaaa
3623 ccc
3624 bb
3625 aaaaaˇ»
3626 "});
3627
3628 cx.set_state(indoc! {"
3629 Aaa«aa
3630 cˇ»c«c
3631 bb
3632 aaaˇ»aa
3633 "});
3634 cx.update_editor(|e, window, cx| {
3635 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3636 });
3637 cx.assert_editor_state(indoc! {"
3638 «Aaaaa
3639 ccc
3640 bbˇ»
3641 "});
3642
3643 // Consider non continuous selection as distinct dedup operations
3644 cx.set_state(indoc! {"
3645 «aaaaa
3646 bb
3647 aaaaa
3648 aaaaaˇ»
3649
3650 aaa«aaˇ»
3651 "});
3652 cx.update_editor(|e, window, cx| {
3653 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3654 });
3655 cx.assert_editor_state(indoc! {"
3656 «aaaaa
3657 bbˇ»
3658
3659 «aaaaaˇ»
3660 "});
3661}
3662
3663#[gpui::test]
3664async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3665 init_test(cx, |_| {});
3666
3667 let mut cx = EditorTestContext::new(cx).await;
3668
3669 cx.set_state(indoc! {"
3670 «Aaa
3671 aAa
3672 Aaaˇ»
3673 "});
3674 cx.update_editor(|e, window, cx| {
3675 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3676 });
3677 cx.assert_editor_state(indoc! {"
3678 «Aaa
3679 aAaˇ»
3680 "});
3681
3682 cx.set_state(indoc! {"
3683 «Aaa
3684 aAa
3685 aaAˇ»
3686 "});
3687 cx.update_editor(|e, window, cx| {
3688 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3689 });
3690 cx.assert_editor_state(indoc! {"
3691 «Aaaˇ»
3692 "});
3693}
3694
3695#[gpui::test]
3696async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3697 init_test(cx, |_| {});
3698
3699 let mut cx = EditorTestContext::new(cx).await;
3700
3701 // Manipulate with multiple selections on a single line
3702 cx.set_state(indoc! {"
3703 dd«dd
3704 cˇ»c«c
3705 bb
3706 aaaˇ»aa
3707 "});
3708 cx.update_editor(|e, window, cx| {
3709 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3710 });
3711 cx.assert_editor_state(indoc! {"
3712 «aaaaa
3713 bb
3714 ccc
3715 ddddˇ»
3716 "});
3717
3718 // Manipulate with multiple disjoin selections
3719 cx.set_state(indoc! {"
3720 5«
3721 4
3722 3
3723 2
3724 1ˇ»
3725
3726 dd«dd
3727 ccc
3728 bb
3729 aaaˇ»aa
3730 "});
3731 cx.update_editor(|e, window, cx| {
3732 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3733 });
3734 cx.assert_editor_state(indoc! {"
3735 «1
3736 2
3737 3
3738 4
3739 5ˇ»
3740
3741 «aaaaa
3742 bb
3743 ccc
3744 ddddˇ»
3745 "});
3746
3747 // Adding lines on each selection
3748 cx.set_state(indoc! {"
3749 2«
3750 1ˇ»
3751
3752 bb«bb
3753 aaaˇ»aa
3754 "});
3755 cx.update_editor(|e, window, cx| {
3756 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3757 });
3758 cx.assert_editor_state(indoc! {"
3759 «2
3760 1
3761 added lineˇ»
3762
3763 «bbbb
3764 aaaaa
3765 added lineˇ»
3766 "});
3767
3768 // Removing lines on each selection
3769 cx.set_state(indoc! {"
3770 2«
3771 1ˇ»
3772
3773 bb«bb
3774 aaaˇ»aa
3775 "});
3776 cx.update_editor(|e, window, cx| {
3777 e.manipulate_lines(window, cx, |lines| {
3778 lines.pop();
3779 })
3780 });
3781 cx.assert_editor_state(indoc! {"
3782 «2ˇ»
3783
3784 «bbbbˇ»
3785 "});
3786}
3787
3788#[gpui::test]
3789async fn test_manipulate_text(cx: &mut TestAppContext) {
3790 init_test(cx, |_| {});
3791
3792 let mut cx = EditorTestContext::new(cx).await;
3793
3794 // Test convert_to_upper_case()
3795 cx.set_state(indoc! {"
3796 «hello worldˇ»
3797 "});
3798 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3799 cx.assert_editor_state(indoc! {"
3800 «HELLO WORLDˇ»
3801 "});
3802
3803 // Test convert_to_lower_case()
3804 cx.set_state(indoc! {"
3805 «HELLO WORLDˇ»
3806 "});
3807 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3808 cx.assert_editor_state(indoc! {"
3809 «hello worldˇ»
3810 "});
3811
3812 // Test multiple line, single selection case
3813 cx.set_state(indoc! {"
3814 «The quick brown
3815 fox jumps over
3816 the lazy dogˇ»
3817 "});
3818 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3819 cx.assert_editor_state(indoc! {"
3820 «The Quick Brown
3821 Fox Jumps Over
3822 The Lazy Dogˇ»
3823 "});
3824
3825 // Test multiple line, single selection case
3826 cx.set_state(indoc! {"
3827 «The quick brown
3828 fox jumps over
3829 the lazy dogˇ»
3830 "});
3831 cx.update_editor(|e, window, cx| {
3832 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
3833 });
3834 cx.assert_editor_state(indoc! {"
3835 «TheQuickBrown
3836 FoxJumpsOver
3837 TheLazyDogˇ»
3838 "});
3839
3840 // From here on out, test more complex cases of manipulate_text()
3841
3842 // Test no selection case - should affect words cursors are in
3843 // Cursor at beginning, middle, and end of word
3844 cx.set_state(indoc! {"
3845 ˇhello big beauˇtiful worldˇ
3846 "});
3847 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3848 cx.assert_editor_state(indoc! {"
3849 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
3850 "});
3851
3852 // Test multiple selections on a single line and across multiple lines
3853 cx.set_state(indoc! {"
3854 «Theˇ» quick «brown
3855 foxˇ» jumps «overˇ»
3856 the «lazyˇ» dog
3857 "});
3858 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3859 cx.assert_editor_state(indoc! {"
3860 «THEˇ» quick «BROWN
3861 FOXˇ» jumps «OVERˇ»
3862 the «LAZYˇ» dog
3863 "});
3864
3865 // Test case where text length grows
3866 cx.set_state(indoc! {"
3867 «tschüߡ»
3868 "});
3869 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3870 cx.assert_editor_state(indoc! {"
3871 «TSCHÜSSˇ»
3872 "});
3873
3874 // Test to make sure we don't crash when text shrinks
3875 cx.set_state(indoc! {"
3876 aaa_bbbˇ
3877 "});
3878 cx.update_editor(|e, window, cx| {
3879 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3880 });
3881 cx.assert_editor_state(indoc! {"
3882 «aaaBbbˇ»
3883 "});
3884
3885 // Test to make sure we all aware of the fact that each word can grow and shrink
3886 // Final selections should be aware of this fact
3887 cx.set_state(indoc! {"
3888 aaa_bˇbb bbˇb_ccc ˇccc_ddd
3889 "});
3890 cx.update_editor(|e, window, cx| {
3891 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3892 });
3893 cx.assert_editor_state(indoc! {"
3894 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
3895 "});
3896
3897 cx.set_state(indoc! {"
3898 «hElLo, WoRld!ˇ»
3899 "});
3900 cx.update_editor(|e, window, cx| {
3901 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
3902 });
3903 cx.assert_editor_state(indoc! {"
3904 «HeLlO, wOrLD!ˇ»
3905 "});
3906}
3907
3908#[gpui::test]
3909fn test_duplicate_line(cx: &mut TestAppContext) {
3910 init_test(cx, |_| {});
3911
3912 let editor = cx.add_window(|window, cx| {
3913 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3914 build_editor(buffer, window, cx)
3915 });
3916 _ = editor.update(cx, |editor, window, cx| {
3917 editor.change_selections(None, window, cx, |s| {
3918 s.select_display_ranges([
3919 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3920 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3921 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
3922 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3923 ])
3924 });
3925 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
3926 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
3927 assert_eq!(
3928 editor.selections.display_ranges(cx),
3929 vec![
3930 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3931 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3932 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3933 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
3934 ]
3935 );
3936 });
3937
3938 let editor = cx.add_window(|window, cx| {
3939 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3940 build_editor(buffer, window, cx)
3941 });
3942 _ = editor.update(cx, |editor, window, cx| {
3943 editor.change_selections(None, window, cx, |s| {
3944 s.select_display_ranges([
3945 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
3946 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
3947 ])
3948 });
3949 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
3950 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
3951 assert_eq!(
3952 editor.selections.display_ranges(cx),
3953 vec![
3954 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
3955 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
3956 ]
3957 );
3958 });
3959
3960 // With `move_upwards` the selections stay in place, except for
3961 // the lines inserted above them
3962 let editor = cx.add_window(|window, cx| {
3963 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3964 build_editor(buffer, window, cx)
3965 });
3966 _ = editor.update(cx, |editor, window, cx| {
3967 editor.change_selections(None, window, cx, |s| {
3968 s.select_display_ranges([
3969 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3970 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3971 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
3972 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3973 ])
3974 });
3975 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
3976 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
3977 assert_eq!(
3978 editor.selections.display_ranges(cx),
3979 vec![
3980 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3981 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3982 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
3983 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
3984 ]
3985 );
3986 });
3987
3988 let editor = cx.add_window(|window, cx| {
3989 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3990 build_editor(buffer, window, cx)
3991 });
3992 _ = editor.update(cx, |editor, window, cx| {
3993 editor.change_selections(None, window, cx, |s| {
3994 s.select_display_ranges([
3995 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
3996 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
3997 ])
3998 });
3999 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4000 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4001 assert_eq!(
4002 editor.selections.display_ranges(cx),
4003 vec![
4004 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4005 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4006 ]
4007 );
4008 });
4009
4010 let editor = cx.add_window(|window, cx| {
4011 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4012 build_editor(buffer, window, cx)
4013 });
4014 _ = editor.update(cx, |editor, window, cx| {
4015 editor.change_selections(None, window, cx, |s| {
4016 s.select_display_ranges([
4017 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4018 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4019 ])
4020 });
4021 editor.duplicate_selection(&DuplicateSelection, window, cx);
4022 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4023 assert_eq!(
4024 editor.selections.display_ranges(cx),
4025 vec![
4026 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4027 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4028 ]
4029 );
4030 });
4031}
4032
4033#[gpui::test]
4034fn test_move_line_up_down(cx: &mut TestAppContext) {
4035 init_test(cx, |_| {});
4036
4037 let editor = cx.add_window(|window, cx| {
4038 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4039 build_editor(buffer, window, cx)
4040 });
4041 _ = editor.update(cx, |editor, window, cx| {
4042 editor.fold_creases(
4043 vec![
4044 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4045 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4046 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4047 ],
4048 true,
4049 window,
4050 cx,
4051 );
4052 editor.change_selections(None, window, cx, |s| {
4053 s.select_display_ranges([
4054 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4055 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4056 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4057 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4058 ])
4059 });
4060 assert_eq!(
4061 editor.display_text(cx),
4062 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4063 );
4064
4065 editor.move_line_up(&MoveLineUp, window, cx);
4066 assert_eq!(
4067 editor.display_text(cx),
4068 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4069 );
4070 assert_eq!(
4071 editor.selections.display_ranges(cx),
4072 vec![
4073 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4074 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4075 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4076 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4077 ]
4078 );
4079 });
4080
4081 _ = editor.update(cx, |editor, window, cx| {
4082 editor.move_line_down(&MoveLineDown, window, cx);
4083 assert_eq!(
4084 editor.display_text(cx),
4085 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4086 );
4087 assert_eq!(
4088 editor.selections.display_ranges(cx),
4089 vec![
4090 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4091 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4092 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4093 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4094 ]
4095 );
4096 });
4097
4098 _ = editor.update(cx, |editor, window, cx| {
4099 editor.move_line_down(&MoveLineDown, window, cx);
4100 assert_eq!(
4101 editor.display_text(cx),
4102 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4103 );
4104 assert_eq!(
4105 editor.selections.display_ranges(cx),
4106 vec![
4107 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4108 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4109 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4110 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4111 ]
4112 );
4113 });
4114
4115 _ = editor.update(cx, |editor, window, cx| {
4116 editor.move_line_up(&MoveLineUp, window, cx);
4117 assert_eq!(
4118 editor.display_text(cx),
4119 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4120 );
4121 assert_eq!(
4122 editor.selections.display_ranges(cx),
4123 vec![
4124 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4125 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4126 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4127 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4128 ]
4129 );
4130 });
4131}
4132
4133#[gpui::test]
4134fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4135 init_test(cx, |_| {});
4136
4137 let editor = cx.add_window(|window, cx| {
4138 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4139 build_editor(buffer, window, cx)
4140 });
4141 _ = editor.update(cx, |editor, window, cx| {
4142 let snapshot = editor.buffer.read(cx).snapshot(cx);
4143 editor.insert_blocks(
4144 [BlockProperties {
4145 style: BlockStyle::Fixed,
4146 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4147 height: 1,
4148 render: Arc::new(|_| div().into_any()),
4149 priority: 0,
4150 }],
4151 Some(Autoscroll::fit()),
4152 cx,
4153 );
4154 editor.change_selections(None, window, cx, |s| {
4155 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4156 });
4157 editor.move_line_down(&MoveLineDown, window, cx);
4158 });
4159}
4160
4161#[gpui::test]
4162async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4163 init_test(cx, |_| {});
4164
4165 let mut cx = EditorTestContext::new(cx).await;
4166 cx.set_state(
4167 &"
4168 ˇzero
4169 one
4170 two
4171 three
4172 four
4173 five
4174 "
4175 .unindent(),
4176 );
4177
4178 // Create a four-line block that replaces three lines of text.
4179 cx.update_editor(|editor, window, cx| {
4180 let snapshot = editor.snapshot(window, cx);
4181 let snapshot = &snapshot.buffer_snapshot;
4182 let placement = BlockPlacement::Replace(
4183 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4184 );
4185 editor.insert_blocks(
4186 [BlockProperties {
4187 placement,
4188 height: 4,
4189 style: BlockStyle::Sticky,
4190 render: Arc::new(|_| gpui::div().into_any_element()),
4191 priority: 0,
4192 }],
4193 None,
4194 cx,
4195 );
4196 });
4197
4198 // Move down so that the cursor touches the block.
4199 cx.update_editor(|editor, window, cx| {
4200 editor.move_down(&Default::default(), window, cx);
4201 });
4202 cx.assert_editor_state(
4203 &"
4204 zero
4205 «one
4206 two
4207 threeˇ»
4208 four
4209 five
4210 "
4211 .unindent(),
4212 );
4213
4214 // Move down past the block.
4215 cx.update_editor(|editor, window, cx| {
4216 editor.move_down(&Default::default(), window, cx);
4217 });
4218 cx.assert_editor_state(
4219 &"
4220 zero
4221 one
4222 two
4223 three
4224 ˇfour
4225 five
4226 "
4227 .unindent(),
4228 );
4229}
4230
4231#[gpui::test]
4232fn test_transpose(cx: &mut TestAppContext) {
4233 init_test(cx, |_| {});
4234
4235 _ = cx.add_window(|window, cx| {
4236 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4237 editor.set_style(EditorStyle::default(), window, cx);
4238 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4239 editor.transpose(&Default::default(), window, cx);
4240 assert_eq!(editor.text(cx), "bac");
4241 assert_eq!(editor.selections.ranges(cx), [2..2]);
4242
4243 editor.transpose(&Default::default(), window, cx);
4244 assert_eq!(editor.text(cx), "bca");
4245 assert_eq!(editor.selections.ranges(cx), [3..3]);
4246
4247 editor.transpose(&Default::default(), window, cx);
4248 assert_eq!(editor.text(cx), "bac");
4249 assert_eq!(editor.selections.ranges(cx), [3..3]);
4250
4251 editor
4252 });
4253
4254 _ = cx.add_window(|window, cx| {
4255 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4256 editor.set_style(EditorStyle::default(), window, cx);
4257 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4258 editor.transpose(&Default::default(), window, cx);
4259 assert_eq!(editor.text(cx), "acb\nde");
4260 assert_eq!(editor.selections.ranges(cx), [3..3]);
4261
4262 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4263 editor.transpose(&Default::default(), window, cx);
4264 assert_eq!(editor.text(cx), "acbd\ne");
4265 assert_eq!(editor.selections.ranges(cx), [5..5]);
4266
4267 editor.transpose(&Default::default(), window, cx);
4268 assert_eq!(editor.text(cx), "acbde\n");
4269 assert_eq!(editor.selections.ranges(cx), [6..6]);
4270
4271 editor.transpose(&Default::default(), window, cx);
4272 assert_eq!(editor.text(cx), "acbd\ne");
4273 assert_eq!(editor.selections.ranges(cx), [6..6]);
4274
4275 editor
4276 });
4277
4278 _ = cx.add_window(|window, cx| {
4279 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4280 editor.set_style(EditorStyle::default(), window, cx);
4281 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4282 editor.transpose(&Default::default(), window, cx);
4283 assert_eq!(editor.text(cx), "bacd\ne");
4284 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4285
4286 editor.transpose(&Default::default(), window, cx);
4287 assert_eq!(editor.text(cx), "bcade\n");
4288 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4289
4290 editor.transpose(&Default::default(), window, cx);
4291 assert_eq!(editor.text(cx), "bcda\ne");
4292 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4293
4294 editor.transpose(&Default::default(), window, cx);
4295 assert_eq!(editor.text(cx), "bcade\n");
4296 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4297
4298 editor.transpose(&Default::default(), window, cx);
4299 assert_eq!(editor.text(cx), "bcaed\n");
4300 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4301
4302 editor
4303 });
4304
4305 _ = cx.add_window(|window, cx| {
4306 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4307 editor.set_style(EditorStyle::default(), window, cx);
4308 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4309 editor.transpose(&Default::default(), window, cx);
4310 assert_eq!(editor.text(cx), "🏀🍐✋");
4311 assert_eq!(editor.selections.ranges(cx), [8..8]);
4312
4313 editor.transpose(&Default::default(), window, cx);
4314 assert_eq!(editor.text(cx), "🏀✋🍐");
4315 assert_eq!(editor.selections.ranges(cx), [11..11]);
4316
4317 editor.transpose(&Default::default(), window, cx);
4318 assert_eq!(editor.text(cx), "🏀🍐✋");
4319 assert_eq!(editor.selections.ranges(cx), [11..11]);
4320
4321 editor
4322 });
4323}
4324
4325#[gpui::test]
4326async fn test_rewrap(cx: &mut TestAppContext) {
4327 init_test(cx, |_| {});
4328
4329 let mut cx = EditorTestContext::new(cx).await;
4330
4331 let language_with_c_comments = Arc::new(Language::new(
4332 LanguageConfig {
4333 line_comments: vec!["// ".into()],
4334 ..LanguageConfig::default()
4335 },
4336 None,
4337 ));
4338 let language_with_pound_comments = Arc::new(Language::new(
4339 LanguageConfig {
4340 line_comments: vec!["# ".into()],
4341 ..LanguageConfig::default()
4342 },
4343 None,
4344 ));
4345 let markdown_language = Arc::new(Language::new(
4346 LanguageConfig {
4347 name: "Markdown".into(),
4348 ..LanguageConfig::default()
4349 },
4350 None,
4351 ));
4352 let language_with_doc_comments = Arc::new(Language::new(
4353 LanguageConfig {
4354 line_comments: vec!["// ".into(), "/// ".into()],
4355 ..LanguageConfig::default()
4356 },
4357 Some(tree_sitter_rust::LANGUAGE.into()),
4358 ));
4359
4360 let plaintext_language = Arc::new(Language::new(
4361 LanguageConfig {
4362 name: "Plain Text".into(),
4363 ..LanguageConfig::default()
4364 },
4365 None,
4366 ));
4367
4368 assert_rewrap(
4369 indoc! {"
4370 // ˇ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.
4371 "},
4372 indoc! {"
4373 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4374 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4375 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4376 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4377 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4378 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4379 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4380 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4381 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4382 // porttitor id. Aliquam id accumsan eros.
4383 "},
4384 language_with_c_comments.clone(),
4385 &mut cx,
4386 );
4387
4388 // Test that rewrapping works inside of a selection
4389 assert_rewrap(
4390 indoc! {"
4391 «// 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.ˇ»
4392 "},
4393 indoc! {"
4394 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4395 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4396 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4397 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4398 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4399 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4400 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4401 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4402 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4403 // porttitor id. Aliquam id accumsan eros.ˇ»
4404 "},
4405 language_with_c_comments.clone(),
4406 &mut cx,
4407 );
4408
4409 // Test that cursors that expand to the same region are collapsed.
4410 assert_rewrap(
4411 indoc! {"
4412 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4413 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4414 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4415 // ˇ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.
4416 "},
4417 indoc! {"
4418 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4419 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4420 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4421 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4422 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4423 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4424 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4425 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4426 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4427 // porttitor id. Aliquam id accumsan eros.
4428 "},
4429 language_with_c_comments.clone(),
4430 &mut cx,
4431 );
4432
4433 // Test that non-contiguous selections are treated separately.
4434 assert_rewrap(
4435 indoc! {"
4436 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4437 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4438 //
4439 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4440 // ˇ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.
4441 "},
4442 indoc! {"
4443 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4444 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4445 // auctor, eu lacinia sapien scelerisque.
4446 //
4447 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4448 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4449 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4450 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4451 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4452 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4453 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4454 "},
4455 language_with_c_comments.clone(),
4456 &mut cx,
4457 );
4458
4459 // Test that different comment prefixes are supported.
4460 assert_rewrap(
4461 indoc! {"
4462 # ˇ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.
4463 "},
4464 indoc! {"
4465 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4466 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4467 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4468 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4469 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4470 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4471 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4472 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4473 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4474 # accumsan eros.
4475 "},
4476 language_with_pound_comments.clone(),
4477 &mut cx,
4478 );
4479
4480 // Test that rewrapping is ignored outside of comments in most languages.
4481 assert_rewrap(
4482 indoc! {"
4483 /// Adds two numbers.
4484 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4485 fn add(a: u32, b: u32) -> u32 {
4486 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ˇ
4487 }
4488 "},
4489 indoc! {"
4490 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4491 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4492 fn add(a: u32, b: u32) -> u32 {
4493 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ˇ
4494 }
4495 "},
4496 language_with_doc_comments.clone(),
4497 &mut cx,
4498 );
4499
4500 // Test that rewrapping works in Markdown and Plain Text languages.
4501 assert_rewrap(
4502 indoc! {"
4503 # Hello
4504
4505 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.
4506 "},
4507 indoc! {"
4508 # Hello
4509
4510 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4511 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4512 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4513 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4514 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4515 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4516 Integer sit amet scelerisque nisi.
4517 "},
4518 markdown_language,
4519 &mut cx,
4520 );
4521
4522 assert_rewrap(
4523 indoc! {"
4524 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.
4525 "},
4526 indoc! {"
4527 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4528 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4529 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4530 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4531 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4532 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4533 Integer sit amet scelerisque nisi.
4534 "},
4535 plaintext_language,
4536 &mut cx,
4537 );
4538
4539 // Test rewrapping unaligned comments in a selection.
4540 assert_rewrap(
4541 indoc! {"
4542 fn foo() {
4543 if true {
4544 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4545 // Praesent semper egestas tellus id dignissim.ˇ»
4546 do_something();
4547 } else {
4548 //
4549 }
4550 }
4551 "},
4552 indoc! {"
4553 fn foo() {
4554 if true {
4555 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4556 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4557 // egestas tellus id dignissim.ˇ»
4558 do_something();
4559 } else {
4560 //
4561 }
4562 }
4563 "},
4564 language_with_doc_comments.clone(),
4565 &mut cx,
4566 );
4567
4568 assert_rewrap(
4569 indoc! {"
4570 fn foo() {
4571 if true {
4572 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4573 // Praesent semper egestas tellus id dignissim.»
4574 do_something();
4575 } else {
4576 //
4577 }
4578
4579 }
4580 "},
4581 indoc! {"
4582 fn foo() {
4583 if true {
4584 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4585 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4586 // egestas tellus id dignissim.»
4587 do_something();
4588 } else {
4589 //
4590 }
4591
4592 }
4593 "},
4594 language_with_doc_comments.clone(),
4595 &mut cx,
4596 );
4597
4598 #[track_caller]
4599 fn assert_rewrap(
4600 unwrapped_text: &str,
4601 wrapped_text: &str,
4602 language: Arc<Language>,
4603 cx: &mut EditorTestContext,
4604 ) {
4605 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4606 cx.set_state(unwrapped_text);
4607 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4608 cx.assert_editor_state(wrapped_text);
4609 }
4610}
4611
4612#[gpui::test]
4613async fn test_clipboard(cx: &mut gpui::TestAppContext) {
4614 init_test(cx, |_| {});
4615
4616 let mut cx = EditorTestContext::new(cx).await;
4617
4618 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4619 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4620 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4621
4622 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4623 cx.set_state("two ˇfour ˇsix ˇ");
4624 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4625 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4626
4627 // Paste again but with only two cursors. Since the number of cursors doesn't
4628 // match the number of slices in the clipboard, the entire clipboard text
4629 // is pasted at each cursor.
4630 cx.set_state("ˇtwo one✅ four three six five ˇ");
4631 cx.update_editor(|e, window, cx| {
4632 e.handle_input("( ", window, cx);
4633 e.paste(&Paste, window, cx);
4634 e.handle_input(") ", window, cx);
4635 });
4636 cx.assert_editor_state(
4637 &([
4638 "( one✅ ",
4639 "three ",
4640 "five ) ˇtwo one✅ four three six five ( one✅ ",
4641 "three ",
4642 "five ) ˇ",
4643 ]
4644 .join("\n")),
4645 );
4646
4647 // Cut with three selections, one of which is full-line.
4648 cx.set_state(indoc! {"
4649 1«2ˇ»3
4650 4ˇ567
4651 «8ˇ»9"});
4652 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4653 cx.assert_editor_state(indoc! {"
4654 1ˇ3
4655 ˇ9"});
4656
4657 // Paste with three selections, noticing how the copied selection that was full-line
4658 // gets inserted before the second cursor.
4659 cx.set_state(indoc! {"
4660 1ˇ3
4661 9ˇ
4662 «oˇ»ne"});
4663 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4664 cx.assert_editor_state(indoc! {"
4665 12ˇ3
4666 4567
4667 9ˇ
4668 8ˇne"});
4669
4670 // Copy with a single cursor only, which writes the whole line into the clipboard.
4671 cx.set_state(indoc! {"
4672 The quick brown
4673 fox juˇmps over
4674 the lazy dog"});
4675 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4676 assert_eq!(
4677 cx.read_from_clipboard()
4678 .and_then(|item| item.text().as_deref().map(str::to_string)),
4679 Some("fox jumps over\n".to_string())
4680 );
4681
4682 // Paste with three selections, noticing how the copied full-line selection is inserted
4683 // before the empty selections but replaces the selection that is non-empty.
4684 cx.set_state(indoc! {"
4685 Tˇhe quick brown
4686 «foˇ»x jumps over
4687 tˇhe lazy dog"});
4688 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4689 cx.assert_editor_state(indoc! {"
4690 fox jumps over
4691 Tˇhe quick brown
4692 fox jumps over
4693 ˇx jumps over
4694 fox jumps over
4695 tˇhe lazy dog"});
4696}
4697
4698#[gpui::test]
4699async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
4700 init_test(cx, |_| {});
4701
4702 let mut cx = EditorTestContext::new(cx).await;
4703 let language = Arc::new(Language::new(
4704 LanguageConfig::default(),
4705 Some(tree_sitter_rust::LANGUAGE.into()),
4706 ));
4707 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4708
4709 // Cut an indented block, without the leading whitespace.
4710 cx.set_state(indoc! {"
4711 const a: B = (
4712 c(),
4713 «d(
4714 e,
4715 f
4716 )ˇ»
4717 );
4718 "});
4719 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4720 cx.assert_editor_state(indoc! {"
4721 const a: B = (
4722 c(),
4723 ˇ
4724 );
4725 "});
4726
4727 // Paste it at the same position.
4728 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4729 cx.assert_editor_state(indoc! {"
4730 const a: B = (
4731 c(),
4732 d(
4733 e,
4734 f
4735 )ˇ
4736 );
4737 "});
4738
4739 // Paste it at a line with a lower indent level.
4740 cx.set_state(indoc! {"
4741 ˇ
4742 const a: B = (
4743 c(),
4744 );
4745 "});
4746 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4747 cx.assert_editor_state(indoc! {"
4748 d(
4749 e,
4750 f
4751 )ˇ
4752 const a: B = (
4753 c(),
4754 );
4755 "});
4756
4757 // Cut an indented block, with the leading whitespace.
4758 cx.set_state(indoc! {"
4759 const a: B = (
4760 c(),
4761 « d(
4762 e,
4763 f
4764 )
4765 ˇ»);
4766 "});
4767 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4768 cx.assert_editor_state(indoc! {"
4769 const a: B = (
4770 c(),
4771 ˇ);
4772 "});
4773
4774 // Paste it at the same position.
4775 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4776 cx.assert_editor_state(indoc! {"
4777 const a: B = (
4778 c(),
4779 d(
4780 e,
4781 f
4782 )
4783 ˇ);
4784 "});
4785
4786 // Paste it at a line with a higher indent level.
4787 cx.set_state(indoc! {"
4788 const a: B = (
4789 c(),
4790 d(
4791 e,
4792 fˇ
4793 )
4794 );
4795 "});
4796 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4797 cx.assert_editor_state(indoc! {"
4798 const a: B = (
4799 c(),
4800 d(
4801 e,
4802 f d(
4803 e,
4804 f
4805 )
4806 ˇ
4807 )
4808 );
4809 "});
4810}
4811
4812#[gpui::test]
4813fn test_select_all(cx: &mut TestAppContext) {
4814 init_test(cx, |_| {});
4815
4816 let editor = cx.add_window(|window, cx| {
4817 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
4818 build_editor(buffer, window, cx)
4819 });
4820 _ = editor.update(cx, |editor, window, cx| {
4821 editor.select_all(&SelectAll, window, cx);
4822 assert_eq!(
4823 editor.selections.display_ranges(cx),
4824 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
4825 );
4826 });
4827}
4828
4829#[gpui::test]
4830fn test_select_line(cx: &mut TestAppContext) {
4831 init_test(cx, |_| {});
4832
4833 let editor = cx.add_window(|window, cx| {
4834 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
4835 build_editor(buffer, window, cx)
4836 });
4837 _ = editor.update(cx, |editor, window, cx| {
4838 editor.change_selections(None, window, cx, |s| {
4839 s.select_display_ranges([
4840 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4841 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4842 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4843 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
4844 ])
4845 });
4846 editor.select_line(&SelectLine, window, cx);
4847 assert_eq!(
4848 editor.selections.display_ranges(cx),
4849 vec![
4850 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
4851 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
4852 ]
4853 );
4854 });
4855
4856 _ = editor.update(cx, |editor, window, cx| {
4857 editor.select_line(&SelectLine, window, cx);
4858 assert_eq!(
4859 editor.selections.display_ranges(cx),
4860 vec![
4861 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
4862 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
4863 ]
4864 );
4865 });
4866
4867 _ = editor.update(cx, |editor, window, cx| {
4868 editor.select_line(&SelectLine, window, cx);
4869 assert_eq!(
4870 editor.selections.display_ranges(cx),
4871 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
4872 );
4873 });
4874}
4875
4876#[gpui::test]
4877fn test_split_selection_into_lines(cx: &mut TestAppContext) {
4878 init_test(cx, |_| {});
4879
4880 let editor = cx.add_window(|window, cx| {
4881 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
4882 build_editor(buffer, window, cx)
4883 });
4884 _ = editor.update(cx, |editor, window, cx| {
4885 editor.fold_creases(
4886 vec![
4887 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4888 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4889 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4890 ],
4891 true,
4892 window,
4893 cx,
4894 );
4895 editor.change_selections(None, window, cx, |s| {
4896 s.select_display_ranges([
4897 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4898 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4899 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4900 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
4901 ])
4902 });
4903 assert_eq!(
4904 editor.display_text(cx),
4905 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
4906 );
4907 });
4908
4909 _ = editor.update(cx, |editor, window, cx| {
4910 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
4911 assert_eq!(
4912 editor.display_text(cx),
4913 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
4914 );
4915 assert_eq!(
4916 editor.selections.display_ranges(cx),
4917 [
4918 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4919 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4920 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4921 DisplayPoint::new(DisplayRow(5), 4)..DisplayPoint::new(DisplayRow(5), 4)
4922 ]
4923 );
4924 });
4925
4926 _ = editor.update(cx, |editor, window, cx| {
4927 editor.change_selections(None, window, cx, |s| {
4928 s.select_display_ranges([
4929 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
4930 ])
4931 });
4932 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
4933 assert_eq!(
4934 editor.display_text(cx),
4935 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
4936 );
4937 assert_eq!(
4938 editor.selections.display_ranges(cx),
4939 [
4940 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
4941 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
4942 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
4943 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
4944 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
4945 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
4946 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5),
4947 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(7), 0)
4948 ]
4949 );
4950 });
4951}
4952
4953#[gpui::test]
4954async fn test_add_selection_above_below(cx: &mut TestAppContext) {
4955 init_test(cx, |_| {});
4956
4957 let mut cx = EditorTestContext::new(cx).await;
4958
4959 // let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx);
4960 cx.set_state(indoc!(
4961 r#"abc
4962 defˇghi
4963
4964 jk
4965 nlmo
4966 "#
4967 ));
4968
4969 cx.update_editor(|editor, window, cx| {
4970 editor.add_selection_above(&Default::default(), window, cx);
4971 });
4972
4973 cx.assert_editor_state(indoc!(
4974 r#"abcˇ
4975 defˇghi
4976
4977 jk
4978 nlmo
4979 "#
4980 ));
4981
4982 cx.update_editor(|editor, window, cx| {
4983 editor.add_selection_above(&Default::default(), window, cx);
4984 });
4985
4986 cx.assert_editor_state(indoc!(
4987 r#"abcˇ
4988 defˇghi
4989
4990 jk
4991 nlmo
4992 "#
4993 ));
4994
4995 cx.update_editor(|editor, window, cx| {
4996 editor.add_selection_below(&Default::default(), window, cx);
4997 });
4998
4999 cx.assert_editor_state(indoc!(
5000 r#"abc
5001 defˇghi
5002
5003 jk
5004 nlmo
5005 "#
5006 ));
5007
5008 cx.update_editor(|editor, window, cx| {
5009 editor.undo_selection(&Default::default(), window, cx);
5010 });
5011
5012 cx.assert_editor_state(indoc!(
5013 r#"abcˇ
5014 defˇghi
5015
5016 jk
5017 nlmo
5018 "#
5019 ));
5020
5021 cx.update_editor(|editor, window, cx| {
5022 editor.redo_selection(&Default::default(), window, cx);
5023 });
5024
5025 cx.assert_editor_state(indoc!(
5026 r#"abc
5027 defˇghi
5028
5029 jk
5030 nlmo
5031 "#
5032 ));
5033
5034 cx.update_editor(|editor, window, cx| {
5035 editor.add_selection_below(&Default::default(), window, cx);
5036 });
5037
5038 cx.assert_editor_state(indoc!(
5039 r#"abc
5040 defˇghi
5041
5042 jk
5043 nlmˇo
5044 "#
5045 ));
5046
5047 cx.update_editor(|editor, window, cx| {
5048 editor.add_selection_below(&Default::default(), window, cx);
5049 });
5050
5051 cx.assert_editor_state(indoc!(
5052 r#"abc
5053 defˇghi
5054
5055 jk
5056 nlmˇo
5057 "#
5058 ));
5059
5060 // change selections
5061 cx.set_state(indoc!(
5062 r#"abc
5063 def«ˇg»hi
5064
5065 jk
5066 nlmo
5067 "#
5068 ));
5069
5070 cx.update_editor(|editor, window, cx| {
5071 editor.add_selection_below(&Default::default(), window, cx);
5072 });
5073
5074 cx.assert_editor_state(indoc!(
5075 r#"abc
5076 def«ˇg»hi
5077
5078 jk
5079 nlm«ˇo»
5080 "#
5081 ));
5082
5083 cx.update_editor(|editor, window, cx| {
5084 editor.add_selection_below(&Default::default(), window, cx);
5085 });
5086
5087 cx.assert_editor_state(indoc!(
5088 r#"abc
5089 def«ˇg»hi
5090
5091 jk
5092 nlm«ˇo»
5093 "#
5094 ));
5095
5096 cx.update_editor(|editor, window, cx| {
5097 editor.add_selection_above(&Default::default(), window, cx);
5098 });
5099
5100 cx.assert_editor_state(indoc!(
5101 r#"abc
5102 def«ˇg»hi
5103
5104 jk
5105 nlmo
5106 "#
5107 ));
5108
5109 cx.update_editor(|editor, window, cx| {
5110 editor.add_selection_above(&Default::default(), window, cx);
5111 });
5112
5113 cx.assert_editor_state(indoc!(
5114 r#"abc
5115 def«ˇg»hi
5116
5117 jk
5118 nlmo
5119 "#
5120 ));
5121
5122 // Change selections again
5123 cx.set_state(indoc!(
5124 r#"a«bc
5125 defgˇ»hi
5126
5127 jk
5128 nlmo
5129 "#
5130 ));
5131
5132 cx.update_editor(|editor, window, cx| {
5133 editor.add_selection_below(&Default::default(), window, cx);
5134 });
5135
5136 cx.assert_editor_state(indoc!(
5137 r#"a«bcˇ»
5138 d«efgˇ»hi
5139
5140 j«kˇ»
5141 nlmo
5142 "#
5143 ));
5144
5145 cx.update_editor(|editor, window, cx| {
5146 editor.add_selection_below(&Default::default(), window, cx);
5147 });
5148 cx.assert_editor_state(indoc!(
5149 r#"a«bcˇ»
5150 d«efgˇ»hi
5151
5152 j«kˇ»
5153 n«lmoˇ»
5154 "#
5155 ));
5156 cx.update_editor(|editor, window, cx| {
5157 editor.add_selection_above(&Default::default(), window, cx);
5158 });
5159
5160 cx.assert_editor_state(indoc!(
5161 r#"a«bcˇ»
5162 d«efgˇ»hi
5163
5164 j«kˇ»
5165 nlmo
5166 "#
5167 ));
5168
5169 // Change selections again
5170 cx.set_state(indoc!(
5171 r#"abc
5172 d«ˇefghi
5173
5174 jk
5175 nlm»o
5176 "#
5177 ));
5178
5179 cx.update_editor(|editor, window, cx| {
5180 editor.add_selection_above(&Default::default(), window, cx);
5181 });
5182
5183 cx.assert_editor_state(indoc!(
5184 r#"a«ˇbc»
5185 d«ˇef»ghi
5186
5187 j«ˇk»
5188 n«ˇlm»o
5189 "#
5190 ));
5191
5192 cx.update_editor(|editor, window, cx| {
5193 editor.add_selection_below(&Default::default(), window, cx);
5194 });
5195
5196 cx.assert_editor_state(indoc!(
5197 r#"abc
5198 d«ˇef»ghi
5199
5200 j«ˇk»
5201 n«ˇlm»o
5202 "#
5203 ));
5204}
5205
5206#[gpui::test]
5207async fn test_select_next(cx: &mut gpui::TestAppContext) {
5208 init_test(cx, |_| {});
5209
5210 let mut cx = EditorTestContext::new(cx).await;
5211 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5212
5213 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5214 .unwrap();
5215 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5216
5217 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5218 .unwrap();
5219 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5220
5221 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5222 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5223
5224 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5225 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5226
5227 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5228 .unwrap();
5229 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5230
5231 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5232 .unwrap();
5233 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5234}
5235
5236#[gpui::test]
5237async fn test_select_all_matches(cx: &mut gpui::TestAppContext) {
5238 init_test(cx, |_| {});
5239
5240 let mut cx = EditorTestContext::new(cx).await;
5241
5242 // Test caret-only selections
5243 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5244
5245 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5246 .unwrap();
5247 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5248
5249 // Test left-to-right selections
5250 cx.set_state("abc\n«abcˇ»\nabc");
5251
5252 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5253 .unwrap();
5254 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5255
5256 // Test right-to-left selections
5257 cx.set_state("abc\n«ˇabc»\nabc");
5258
5259 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5260 .unwrap();
5261 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5262}
5263
5264#[gpui::test]
5265async fn test_select_next_with_multiple_carets(cx: &mut gpui::TestAppContext) {
5266 init_test(cx, |_| {});
5267
5268 let mut cx = EditorTestContext::new(cx).await;
5269 cx.set_state(
5270 r#"let foo = 2;
5271lˇet foo = 2;
5272let fooˇ = 2;
5273let foo = 2;
5274let foo = ˇ2;"#,
5275 );
5276
5277 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5278 .unwrap();
5279 cx.assert_editor_state(
5280 r#"let foo = 2;
5281«letˇ» foo = 2;
5282let «fooˇ» = 2;
5283let foo = 2;
5284let foo = «2ˇ»;"#,
5285 );
5286
5287 // noop for multiple selections with different contents
5288 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5289 .unwrap();
5290 cx.assert_editor_state(
5291 r#"let foo = 2;
5292«letˇ» foo = 2;
5293let «fooˇ» = 2;
5294let foo = 2;
5295let foo = «2ˇ»;"#,
5296 );
5297}
5298
5299#[gpui::test]
5300async fn test_select_previous_multibuffer(cx: &mut gpui::TestAppContext) {
5301 init_test(cx, |_| {});
5302
5303 let mut cx =
5304 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
5305
5306 cx.assert_editor_state(indoc! {"
5307 ˇbbb
5308 ccc
5309
5310 bbb
5311 ccc
5312 "});
5313 cx.dispatch_action(SelectPrevious::default());
5314 cx.assert_editor_state(indoc! {"
5315 «bbbˇ»
5316 ccc
5317
5318 bbb
5319 ccc
5320 "});
5321 cx.dispatch_action(SelectPrevious::default());
5322 cx.assert_editor_state(indoc! {"
5323 «bbbˇ»
5324 ccc
5325
5326 «bbbˇ»
5327 ccc
5328 "});
5329}
5330
5331#[gpui::test]
5332async fn test_select_previous_with_single_caret(cx: &mut gpui::TestAppContext) {
5333 init_test(cx, |_| {});
5334
5335 let mut cx = EditorTestContext::new(cx).await;
5336 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5337
5338 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5339 .unwrap();
5340 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5341
5342 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5343 .unwrap();
5344 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5345
5346 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5347 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5348
5349 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5350 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5351
5352 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5353 .unwrap();
5354 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
5355
5356 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5357 .unwrap();
5358 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
5359
5360 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5361 .unwrap();
5362 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5363}
5364
5365#[gpui::test]
5366async fn test_select_previous_empty_buffer(cx: &mut gpui::TestAppContext) {
5367 init_test(cx, |_| {});
5368
5369 let mut cx = EditorTestContext::new(cx).await;
5370 cx.set_state("aˇ");
5371
5372 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5373 .unwrap();
5374 cx.assert_editor_state("«aˇ»");
5375 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5376 .unwrap();
5377 cx.assert_editor_state("«aˇ»");
5378}
5379
5380#[gpui::test]
5381async fn test_select_previous_with_multiple_carets(cx: &mut gpui::TestAppContext) {
5382 init_test(cx, |_| {});
5383
5384 let mut cx = EditorTestContext::new(cx).await;
5385 cx.set_state(
5386 r#"let foo = 2;
5387lˇet foo = 2;
5388let fooˇ = 2;
5389let foo = 2;
5390let foo = ˇ2;"#,
5391 );
5392
5393 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5394 .unwrap();
5395 cx.assert_editor_state(
5396 r#"let foo = 2;
5397«letˇ» foo = 2;
5398let «fooˇ» = 2;
5399let foo = 2;
5400let foo = «2ˇ»;"#,
5401 );
5402
5403 // noop for multiple selections with different contents
5404 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5405 .unwrap();
5406 cx.assert_editor_state(
5407 r#"let foo = 2;
5408«letˇ» foo = 2;
5409let «fooˇ» = 2;
5410let foo = 2;
5411let foo = «2ˇ»;"#,
5412 );
5413}
5414
5415#[gpui::test]
5416async fn test_select_previous_with_single_selection(cx: &mut gpui::TestAppContext) {
5417 init_test(cx, |_| {});
5418
5419 let mut cx = EditorTestContext::new(cx).await;
5420 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5421
5422 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5423 .unwrap();
5424 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5425
5426 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5427 .unwrap();
5428 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5429
5430 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5431 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5432
5433 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5434 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5435
5436 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5437 .unwrap();
5438 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
5439
5440 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5441 .unwrap();
5442 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5443}
5444
5445#[gpui::test]
5446async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
5447 init_test(cx, |_| {});
5448
5449 let language = Arc::new(Language::new(
5450 LanguageConfig::default(),
5451 Some(tree_sitter_rust::LANGUAGE.into()),
5452 ));
5453
5454 let text = r#"
5455 use mod1::mod2::{mod3, mod4};
5456
5457 fn fn_1(param1: bool, param2: &str) {
5458 let var1 = "text";
5459 }
5460 "#
5461 .unindent();
5462
5463 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5464 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5465 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5466
5467 editor
5468 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5469 .await;
5470
5471 editor.update_in(cx, |editor, window, cx| {
5472 editor.change_selections(None, window, cx, |s| {
5473 s.select_display_ranges([
5474 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
5475 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
5476 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
5477 ]);
5478 });
5479 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5480 });
5481 editor.update(cx, |editor, cx| {
5482 assert_text_with_selections(
5483 editor,
5484 indoc! {r#"
5485 use mod1::mod2::{mod3, «mod4ˇ»};
5486
5487 fn fn_1«ˇ(param1: bool, param2: &str)» {
5488 let var1 = "«textˇ»";
5489 }
5490 "#},
5491 cx,
5492 );
5493 });
5494
5495 editor.update_in(cx, |editor, window, cx| {
5496 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5497 });
5498 editor.update(cx, |editor, cx| {
5499 assert_text_with_selections(
5500 editor,
5501 indoc! {r#"
5502 use mod1::mod2::«{mod3, mod4}ˇ»;
5503
5504 «ˇfn fn_1(param1: bool, param2: &str) {
5505 let var1 = "text";
5506 }»
5507 "#},
5508 cx,
5509 );
5510 });
5511
5512 editor.update_in(cx, |editor, window, cx| {
5513 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5514 });
5515 assert_eq!(
5516 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
5517 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
5518 );
5519
5520 // Trying to expand the selected syntax node one more time has no effect.
5521 editor.update_in(cx, |editor, window, cx| {
5522 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5523 });
5524 assert_eq!(
5525 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
5526 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
5527 );
5528
5529 editor.update_in(cx, |editor, window, cx| {
5530 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5531 });
5532 editor.update(cx, |editor, cx| {
5533 assert_text_with_selections(
5534 editor,
5535 indoc! {r#"
5536 use mod1::mod2::«{mod3, mod4}ˇ»;
5537
5538 «ˇfn fn_1(param1: bool, param2: &str) {
5539 let var1 = "text";
5540 }»
5541 "#},
5542 cx,
5543 );
5544 });
5545
5546 editor.update_in(cx, |editor, window, cx| {
5547 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5548 });
5549 editor.update(cx, |editor, cx| {
5550 assert_text_with_selections(
5551 editor,
5552 indoc! {r#"
5553 use mod1::mod2::{mod3, «mod4ˇ»};
5554
5555 fn fn_1«ˇ(param1: bool, param2: &str)» {
5556 let var1 = "«textˇ»";
5557 }
5558 "#},
5559 cx,
5560 );
5561 });
5562
5563 editor.update_in(cx, |editor, window, cx| {
5564 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5565 });
5566 editor.update(cx, |editor, cx| {
5567 assert_text_with_selections(
5568 editor,
5569 indoc! {r#"
5570 use mod1::mod2::{mod3, mo«ˇ»d4};
5571
5572 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
5573 let var1 = "te«ˇ»xt";
5574 }
5575 "#},
5576 cx,
5577 );
5578 });
5579
5580 // Trying to shrink the selected syntax node one more time has no effect.
5581 editor.update_in(cx, |editor, window, cx| {
5582 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5583 });
5584 editor.update_in(cx, |editor, _, cx| {
5585 assert_text_with_selections(
5586 editor,
5587 indoc! {r#"
5588 use mod1::mod2::{mod3, mo«ˇ»d4};
5589
5590 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
5591 let var1 = "te«ˇ»xt";
5592 }
5593 "#},
5594 cx,
5595 );
5596 });
5597
5598 // Ensure that we keep expanding the selection if the larger selection starts or ends within
5599 // a fold.
5600 editor.update_in(cx, |editor, window, cx| {
5601 editor.fold_creases(
5602 vec![
5603 Crease::simple(
5604 Point::new(0, 21)..Point::new(0, 24),
5605 FoldPlaceholder::test(),
5606 ),
5607 Crease::simple(
5608 Point::new(3, 20)..Point::new(3, 22),
5609 FoldPlaceholder::test(),
5610 ),
5611 ],
5612 true,
5613 window,
5614 cx,
5615 );
5616 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5617 });
5618 editor.update(cx, |editor, cx| {
5619 assert_text_with_selections(
5620 editor,
5621 indoc! {r#"
5622 use mod1::mod2::«{mod3, mod4}ˇ»;
5623
5624 fn fn_1«ˇ(param1: bool, param2: &str)» {
5625 «let var1 = "text";ˇ»
5626 }
5627 "#},
5628 cx,
5629 );
5630 });
5631}
5632
5633#[gpui::test]
5634async fn test_fold_function_bodies(cx: &mut gpui::TestAppContext) {
5635 init_test(cx, |_| {});
5636
5637 let base_text = r#"
5638 impl A {
5639 // this is an uncommitted comment
5640
5641 fn b() {
5642 c();
5643 }
5644
5645 // this is another uncommitted comment
5646
5647 fn d() {
5648 // e
5649 // f
5650 }
5651 }
5652
5653 fn g() {
5654 // h
5655 }
5656 "#
5657 .unindent();
5658
5659 let text = r#"
5660 ˇimpl A {
5661
5662 fn b() {
5663 c();
5664 }
5665
5666 fn d() {
5667 // e
5668 // f
5669 }
5670 }
5671
5672 fn g() {
5673 // h
5674 }
5675 "#
5676 .unindent();
5677
5678 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
5679 cx.set_state(&text);
5680 cx.set_diff_base(&base_text);
5681 cx.update_editor(|editor, window, cx| {
5682 editor.expand_all_diff_hunks(&Default::default(), window, cx);
5683 });
5684
5685 cx.assert_state_with_diff(
5686 "
5687 ˇimpl A {
5688 - // this is an uncommitted comment
5689
5690 fn b() {
5691 c();
5692 }
5693
5694 - // this is another uncommitted comment
5695 -
5696 fn d() {
5697 // e
5698 // f
5699 }
5700 }
5701
5702 fn g() {
5703 // h
5704 }
5705 "
5706 .unindent(),
5707 );
5708
5709 let expected_display_text = "
5710 impl A {
5711 // this is an uncommitted comment
5712
5713 fn b() {
5714 ⋯
5715 }
5716
5717 // this is another uncommitted comment
5718
5719 fn d() {
5720 ⋯
5721 }
5722 }
5723
5724 fn g() {
5725 ⋯
5726 }
5727 "
5728 .unindent();
5729
5730 cx.update_editor(|editor, window, cx| {
5731 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
5732 assert_eq!(editor.display_text(cx), expected_display_text);
5733 });
5734}
5735
5736#[gpui::test]
5737async fn test_autoindent(cx: &mut gpui::TestAppContext) {
5738 init_test(cx, |_| {});
5739
5740 let language = Arc::new(
5741 Language::new(
5742 LanguageConfig {
5743 brackets: BracketPairConfig {
5744 pairs: vec![
5745 BracketPair {
5746 start: "{".to_string(),
5747 end: "}".to_string(),
5748 close: false,
5749 surround: false,
5750 newline: true,
5751 },
5752 BracketPair {
5753 start: "(".to_string(),
5754 end: ")".to_string(),
5755 close: false,
5756 surround: false,
5757 newline: true,
5758 },
5759 ],
5760 ..Default::default()
5761 },
5762 ..Default::default()
5763 },
5764 Some(tree_sitter_rust::LANGUAGE.into()),
5765 )
5766 .with_indents_query(
5767 r#"
5768 (_ "(" ")" @end) @indent
5769 (_ "{" "}" @end) @indent
5770 "#,
5771 )
5772 .unwrap(),
5773 );
5774
5775 let text = "fn a() {}";
5776
5777 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5778 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5779 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5780 editor
5781 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5782 .await;
5783
5784 editor.update_in(cx, |editor, window, cx| {
5785 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
5786 editor.newline(&Newline, window, cx);
5787 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
5788 assert_eq!(
5789 editor.selections.ranges(cx),
5790 &[
5791 Point::new(1, 4)..Point::new(1, 4),
5792 Point::new(3, 4)..Point::new(3, 4),
5793 Point::new(5, 0)..Point::new(5, 0)
5794 ]
5795 );
5796 });
5797}
5798
5799#[gpui::test]
5800async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
5801 init_test(cx, |_| {});
5802
5803 {
5804 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
5805 cx.set_state(indoc! {"
5806 impl A {
5807
5808 fn b() {}
5809
5810 «fn c() {
5811
5812 }ˇ»
5813 }
5814 "});
5815
5816 cx.update_editor(|editor, window, cx| {
5817 editor.autoindent(&Default::default(), window, cx);
5818 });
5819
5820 cx.assert_editor_state(indoc! {"
5821 impl A {
5822
5823 fn b() {}
5824
5825 «fn c() {
5826
5827 }ˇ»
5828 }
5829 "});
5830 }
5831
5832 {
5833 let mut cx = EditorTestContext::new_multibuffer(
5834 cx,
5835 [indoc! { "
5836 impl A {
5837 «
5838 // a
5839 fn b(){}
5840 »
5841 «
5842 }
5843 fn c(){}
5844 »
5845 "}],
5846 );
5847
5848 let buffer = cx.update_editor(|editor, _, cx| {
5849 let buffer = editor.buffer().update(cx, |buffer, _| {
5850 buffer.all_buffers().iter().next().unwrap().clone()
5851 });
5852 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5853 buffer
5854 });
5855
5856 cx.run_until_parked();
5857 cx.update_editor(|editor, window, cx| {
5858 editor.select_all(&Default::default(), window, cx);
5859 editor.autoindent(&Default::default(), window, cx)
5860 });
5861 cx.run_until_parked();
5862
5863 cx.update(|_, cx| {
5864 pretty_assertions::assert_eq!(
5865 buffer.read(cx).text(),
5866 indoc! { "
5867 impl A {
5868
5869 // a
5870 fn b(){}
5871
5872
5873 }
5874 fn c(){}
5875
5876 " }
5877 )
5878 });
5879 }
5880}
5881
5882#[gpui::test]
5883async fn test_autoclose_and_auto_surround_pairs(cx: &mut gpui::TestAppContext) {
5884 init_test(cx, |_| {});
5885
5886 let mut cx = EditorTestContext::new(cx).await;
5887
5888 let language = Arc::new(Language::new(
5889 LanguageConfig {
5890 brackets: BracketPairConfig {
5891 pairs: vec![
5892 BracketPair {
5893 start: "{".to_string(),
5894 end: "}".to_string(),
5895 close: true,
5896 surround: true,
5897 newline: true,
5898 },
5899 BracketPair {
5900 start: "(".to_string(),
5901 end: ")".to_string(),
5902 close: true,
5903 surround: true,
5904 newline: true,
5905 },
5906 BracketPair {
5907 start: "/*".to_string(),
5908 end: " */".to_string(),
5909 close: true,
5910 surround: true,
5911 newline: true,
5912 },
5913 BracketPair {
5914 start: "[".to_string(),
5915 end: "]".to_string(),
5916 close: false,
5917 surround: false,
5918 newline: true,
5919 },
5920 BracketPair {
5921 start: "\"".to_string(),
5922 end: "\"".to_string(),
5923 close: true,
5924 surround: true,
5925 newline: false,
5926 },
5927 BracketPair {
5928 start: "<".to_string(),
5929 end: ">".to_string(),
5930 close: false,
5931 surround: true,
5932 newline: true,
5933 },
5934 ],
5935 ..Default::default()
5936 },
5937 autoclose_before: "})]".to_string(),
5938 ..Default::default()
5939 },
5940 Some(tree_sitter_rust::LANGUAGE.into()),
5941 ));
5942
5943 cx.language_registry().add(language.clone());
5944 cx.update_buffer(|buffer, cx| {
5945 buffer.set_language(Some(language), cx);
5946 });
5947
5948 cx.set_state(
5949 &r#"
5950 🏀ˇ
5951 εˇ
5952 ❤️ˇ
5953 "#
5954 .unindent(),
5955 );
5956
5957 // autoclose multiple nested brackets at multiple cursors
5958 cx.update_editor(|editor, window, cx| {
5959 editor.handle_input("{", window, cx);
5960 editor.handle_input("{", window, cx);
5961 editor.handle_input("{", window, cx);
5962 });
5963 cx.assert_editor_state(
5964 &"
5965 🏀{{{ˇ}}}
5966 ε{{{ˇ}}}
5967 ❤️{{{ˇ}}}
5968 "
5969 .unindent(),
5970 );
5971
5972 // insert a different closing bracket
5973 cx.update_editor(|editor, window, cx| {
5974 editor.handle_input(")", window, cx);
5975 });
5976 cx.assert_editor_state(
5977 &"
5978 🏀{{{)ˇ}}}
5979 ε{{{)ˇ}}}
5980 ❤️{{{)ˇ}}}
5981 "
5982 .unindent(),
5983 );
5984
5985 // skip over the auto-closed brackets when typing a closing bracket
5986 cx.update_editor(|editor, window, cx| {
5987 editor.move_right(&MoveRight, window, cx);
5988 editor.handle_input("}", window, cx);
5989 editor.handle_input("}", window, cx);
5990 editor.handle_input("}", window, cx);
5991 });
5992 cx.assert_editor_state(
5993 &"
5994 🏀{{{)}}}}ˇ
5995 ε{{{)}}}}ˇ
5996 ❤️{{{)}}}}ˇ
5997 "
5998 .unindent(),
5999 );
6000
6001 // autoclose multi-character pairs
6002 cx.set_state(
6003 &"
6004 ˇ
6005 ˇ
6006 "
6007 .unindent(),
6008 );
6009 cx.update_editor(|editor, window, cx| {
6010 editor.handle_input("/", window, cx);
6011 editor.handle_input("*", window, cx);
6012 });
6013 cx.assert_editor_state(
6014 &"
6015 /*ˇ */
6016 /*ˇ */
6017 "
6018 .unindent(),
6019 );
6020
6021 // one cursor autocloses a multi-character pair, one cursor
6022 // does not autoclose.
6023 cx.set_state(
6024 &"
6025 /ˇ
6026 ˇ
6027 "
6028 .unindent(),
6029 );
6030 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6031 cx.assert_editor_state(
6032 &"
6033 /*ˇ */
6034 *ˇ
6035 "
6036 .unindent(),
6037 );
6038
6039 // Don't autoclose if the next character isn't whitespace and isn't
6040 // listed in the language's "autoclose_before" section.
6041 cx.set_state("ˇa b");
6042 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6043 cx.assert_editor_state("{ˇa b");
6044
6045 // Don't autoclose if `close` is false for the bracket pair
6046 cx.set_state("ˇ");
6047 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6048 cx.assert_editor_state("[ˇ");
6049
6050 // Surround with brackets if text is selected
6051 cx.set_state("«aˇ» b");
6052 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6053 cx.assert_editor_state("{«aˇ»} b");
6054
6055 // Autclose pair where the start and end characters are the same
6056 cx.set_state("aˇ");
6057 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6058 cx.assert_editor_state("a\"ˇ\"");
6059 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6060 cx.assert_editor_state("a\"\"ˇ");
6061
6062 // Don't autoclose pair if autoclose is disabled
6063 cx.set_state("ˇ");
6064 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6065 cx.assert_editor_state("<ˇ");
6066
6067 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6068 cx.set_state("«aˇ» b");
6069 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6070 cx.assert_editor_state("<«aˇ»> b");
6071}
6072
6073#[gpui::test]
6074async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut gpui::TestAppContext) {
6075 init_test(cx, |settings| {
6076 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6077 });
6078
6079 let mut cx = EditorTestContext::new(cx).await;
6080
6081 let language = Arc::new(Language::new(
6082 LanguageConfig {
6083 brackets: BracketPairConfig {
6084 pairs: vec![
6085 BracketPair {
6086 start: "{".to_string(),
6087 end: "}".to_string(),
6088 close: true,
6089 surround: true,
6090 newline: true,
6091 },
6092 BracketPair {
6093 start: "(".to_string(),
6094 end: ")".to_string(),
6095 close: true,
6096 surround: true,
6097 newline: true,
6098 },
6099 BracketPair {
6100 start: "[".to_string(),
6101 end: "]".to_string(),
6102 close: false,
6103 surround: false,
6104 newline: true,
6105 },
6106 ],
6107 ..Default::default()
6108 },
6109 autoclose_before: "})]".to_string(),
6110 ..Default::default()
6111 },
6112 Some(tree_sitter_rust::LANGUAGE.into()),
6113 ));
6114
6115 cx.language_registry().add(language.clone());
6116 cx.update_buffer(|buffer, cx| {
6117 buffer.set_language(Some(language), cx);
6118 });
6119
6120 cx.set_state(
6121 &"
6122 ˇ
6123 ˇ
6124 ˇ
6125 "
6126 .unindent(),
6127 );
6128
6129 // ensure only matching closing brackets are skipped over
6130 cx.update_editor(|editor, window, cx| {
6131 editor.handle_input("}", window, cx);
6132 editor.move_left(&MoveLeft, window, cx);
6133 editor.handle_input(")", window, cx);
6134 editor.move_left(&MoveLeft, window, cx);
6135 });
6136 cx.assert_editor_state(
6137 &"
6138 ˇ)}
6139 ˇ)}
6140 ˇ)}
6141 "
6142 .unindent(),
6143 );
6144
6145 // skip-over closing brackets at multiple cursors
6146 cx.update_editor(|editor, window, cx| {
6147 editor.handle_input(")", window, cx);
6148 editor.handle_input("}", window, cx);
6149 });
6150 cx.assert_editor_state(
6151 &"
6152 )}ˇ
6153 )}ˇ
6154 )}ˇ
6155 "
6156 .unindent(),
6157 );
6158
6159 // ignore non-close brackets
6160 cx.update_editor(|editor, window, cx| {
6161 editor.handle_input("]", window, cx);
6162 editor.move_left(&MoveLeft, window, cx);
6163 editor.handle_input("]", window, cx);
6164 });
6165 cx.assert_editor_state(
6166 &"
6167 )}]ˇ]
6168 )}]ˇ]
6169 )}]ˇ]
6170 "
6171 .unindent(),
6172 );
6173}
6174
6175#[gpui::test]
6176async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
6177 init_test(cx, |_| {});
6178
6179 let mut cx = EditorTestContext::new(cx).await;
6180
6181 let html_language = Arc::new(
6182 Language::new(
6183 LanguageConfig {
6184 name: "HTML".into(),
6185 brackets: BracketPairConfig {
6186 pairs: vec![
6187 BracketPair {
6188 start: "<".into(),
6189 end: ">".into(),
6190 close: true,
6191 ..Default::default()
6192 },
6193 BracketPair {
6194 start: "{".into(),
6195 end: "}".into(),
6196 close: true,
6197 ..Default::default()
6198 },
6199 BracketPair {
6200 start: "(".into(),
6201 end: ")".into(),
6202 close: true,
6203 ..Default::default()
6204 },
6205 ],
6206 ..Default::default()
6207 },
6208 autoclose_before: "})]>".into(),
6209 ..Default::default()
6210 },
6211 Some(tree_sitter_html::language()),
6212 )
6213 .with_injection_query(
6214 r#"
6215 (script_element
6216 (raw_text) @injection.content
6217 (#set! injection.language "javascript"))
6218 "#,
6219 )
6220 .unwrap(),
6221 );
6222
6223 let javascript_language = Arc::new(Language::new(
6224 LanguageConfig {
6225 name: "JavaScript".into(),
6226 brackets: BracketPairConfig {
6227 pairs: vec![
6228 BracketPair {
6229 start: "/*".into(),
6230 end: " */".into(),
6231 close: true,
6232 ..Default::default()
6233 },
6234 BracketPair {
6235 start: "{".into(),
6236 end: "}".into(),
6237 close: true,
6238 ..Default::default()
6239 },
6240 BracketPair {
6241 start: "(".into(),
6242 end: ")".into(),
6243 close: true,
6244 ..Default::default()
6245 },
6246 ],
6247 ..Default::default()
6248 },
6249 autoclose_before: "})]>".into(),
6250 ..Default::default()
6251 },
6252 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6253 ));
6254
6255 cx.language_registry().add(html_language.clone());
6256 cx.language_registry().add(javascript_language.clone());
6257
6258 cx.update_buffer(|buffer, cx| {
6259 buffer.set_language(Some(html_language), cx);
6260 });
6261
6262 cx.set_state(
6263 &r#"
6264 <body>ˇ
6265 <script>
6266 var x = 1;ˇ
6267 </script>
6268 </body>ˇ
6269 "#
6270 .unindent(),
6271 );
6272
6273 // Precondition: different languages are active at different locations.
6274 cx.update_editor(|editor, window, cx| {
6275 let snapshot = editor.snapshot(window, cx);
6276 let cursors = editor.selections.ranges::<usize>(cx);
6277 let languages = cursors
6278 .iter()
6279 .map(|c| snapshot.language_at(c.start).unwrap().name())
6280 .collect::<Vec<_>>();
6281 assert_eq!(
6282 languages,
6283 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6284 );
6285 });
6286
6287 // Angle brackets autoclose in HTML, but not JavaScript.
6288 cx.update_editor(|editor, window, cx| {
6289 editor.handle_input("<", window, cx);
6290 editor.handle_input("a", window, cx);
6291 });
6292 cx.assert_editor_state(
6293 &r#"
6294 <body><aˇ>
6295 <script>
6296 var x = 1;<aˇ
6297 </script>
6298 </body><aˇ>
6299 "#
6300 .unindent(),
6301 );
6302
6303 // Curly braces and parens autoclose in both HTML and JavaScript.
6304 cx.update_editor(|editor, window, cx| {
6305 editor.handle_input(" b=", window, cx);
6306 editor.handle_input("{", window, cx);
6307 editor.handle_input("c", window, cx);
6308 editor.handle_input("(", window, cx);
6309 });
6310 cx.assert_editor_state(
6311 &r#"
6312 <body><a b={c(ˇ)}>
6313 <script>
6314 var x = 1;<a b={c(ˇ)}
6315 </script>
6316 </body><a b={c(ˇ)}>
6317 "#
6318 .unindent(),
6319 );
6320
6321 // Brackets that were already autoclosed are skipped.
6322 cx.update_editor(|editor, window, cx| {
6323 editor.handle_input(")", window, cx);
6324 editor.handle_input("d", window, cx);
6325 editor.handle_input("}", window, cx);
6326 });
6327 cx.assert_editor_state(
6328 &r#"
6329 <body><a b={c()d}ˇ>
6330 <script>
6331 var x = 1;<a b={c()d}ˇ
6332 </script>
6333 </body><a b={c()d}ˇ>
6334 "#
6335 .unindent(),
6336 );
6337 cx.update_editor(|editor, window, cx| {
6338 editor.handle_input(">", window, cx);
6339 });
6340 cx.assert_editor_state(
6341 &r#"
6342 <body><a b={c()d}>ˇ
6343 <script>
6344 var x = 1;<a b={c()d}>ˇ
6345 </script>
6346 </body><a b={c()d}>ˇ
6347 "#
6348 .unindent(),
6349 );
6350
6351 // Reset
6352 cx.set_state(
6353 &r#"
6354 <body>ˇ
6355 <script>
6356 var x = 1;ˇ
6357 </script>
6358 </body>ˇ
6359 "#
6360 .unindent(),
6361 );
6362
6363 cx.update_editor(|editor, window, cx| {
6364 editor.handle_input("<", window, cx);
6365 });
6366 cx.assert_editor_state(
6367 &r#"
6368 <body><ˇ>
6369 <script>
6370 var x = 1;<ˇ
6371 </script>
6372 </body><ˇ>
6373 "#
6374 .unindent(),
6375 );
6376
6377 // When backspacing, the closing angle brackets are removed.
6378 cx.update_editor(|editor, window, cx| {
6379 editor.backspace(&Backspace, window, cx);
6380 });
6381 cx.assert_editor_state(
6382 &r#"
6383 <body>ˇ
6384 <script>
6385 var x = 1;ˇ
6386 </script>
6387 </body>ˇ
6388 "#
6389 .unindent(),
6390 );
6391
6392 // Block comments autoclose in JavaScript, but not HTML.
6393 cx.update_editor(|editor, window, cx| {
6394 editor.handle_input("/", window, cx);
6395 editor.handle_input("*", window, cx);
6396 });
6397 cx.assert_editor_state(
6398 &r#"
6399 <body>/*ˇ
6400 <script>
6401 var x = 1;/*ˇ */
6402 </script>
6403 </body>/*ˇ
6404 "#
6405 .unindent(),
6406 );
6407}
6408
6409#[gpui::test]
6410async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
6411 init_test(cx, |_| {});
6412
6413 let mut cx = EditorTestContext::new(cx).await;
6414
6415 let rust_language = Arc::new(
6416 Language::new(
6417 LanguageConfig {
6418 name: "Rust".into(),
6419 brackets: serde_json::from_value(json!([
6420 { "start": "{", "end": "}", "close": true, "newline": true },
6421 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
6422 ]))
6423 .unwrap(),
6424 autoclose_before: "})]>".into(),
6425 ..Default::default()
6426 },
6427 Some(tree_sitter_rust::LANGUAGE.into()),
6428 )
6429 .with_override_query("(string_literal) @string")
6430 .unwrap(),
6431 );
6432
6433 cx.language_registry().add(rust_language.clone());
6434 cx.update_buffer(|buffer, cx| {
6435 buffer.set_language(Some(rust_language), cx);
6436 });
6437
6438 cx.set_state(
6439 &r#"
6440 let x = ˇ
6441 "#
6442 .unindent(),
6443 );
6444
6445 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
6446 cx.update_editor(|editor, window, cx| {
6447 editor.handle_input("\"", window, cx);
6448 });
6449 cx.assert_editor_state(
6450 &r#"
6451 let x = "ˇ"
6452 "#
6453 .unindent(),
6454 );
6455
6456 // Inserting another quotation mark. The cursor moves across the existing
6457 // automatically-inserted quotation mark.
6458 cx.update_editor(|editor, window, cx| {
6459 editor.handle_input("\"", window, cx);
6460 });
6461 cx.assert_editor_state(
6462 &r#"
6463 let x = ""ˇ
6464 "#
6465 .unindent(),
6466 );
6467
6468 // Reset
6469 cx.set_state(
6470 &r#"
6471 let x = ˇ
6472 "#
6473 .unindent(),
6474 );
6475
6476 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
6477 cx.update_editor(|editor, window, cx| {
6478 editor.handle_input("\"", window, cx);
6479 editor.handle_input(" ", window, cx);
6480 editor.move_left(&Default::default(), window, cx);
6481 editor.handle_input("\\", window, cx);
6482 editor.handle_input("\"", window, cx);
6483 });
6484 cx.assert_editor_state(
6485 &r#"
6486 let x = "\"ˇ "
6487 "#
6488 .unindent(),
6489 );
6490
6491 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
6492 // mark. Nothing is inserted.
6493 cx.update_editor(|editor, window, cx| {
6494 editor.move_right(&Default::default(), window, cx);
6495 editor.handle_input("\"", window, cx);
6496 });
6497 cx.assert_editor_state(
6498 &r#"
6499 let x = "\" "ˇ
6500 "#
6501 .unindent(),
6502 );
6503}
6504
6505#[gpui::test]
6506async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
6507 init_test(cx, |_| {});
6508
6509 let language = Arc::new(Language::new(
6510 LanguageConfig {
6511 brackets: BracketPairConfig {
6512 pairs: vec![
6513 BracketPair {
6514 start: "{".to_string(),
6515 end: "}".to_string(),
6516 close: true,
6517 surround: true,
6518 newline: true,
6519 },
6520 BracketPair {
6521 start: "/* ".to_string(),
6522 end: "*/".to_string(),
6523 close: true,
6524 surround: true,
6525 ..Default::default()
6526 },
6527 ],
6528 ..Default::default()
6529 },
6530 ..Default::default()
6531 },
6532 Some(tree_sitter_rust::LANGUAGE.into()),
6533 ));
6534
6535 let text = r#"
6536 a
6537 b
6538 c
6539 "#
6540 .unindent();
6541
6542 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6543 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6544 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6545 editor
6546 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6547 .await;
6548
6549 editor.update_in(cx, |editor, window, cx| {
6550 editor.change_selections(None, window, cx, |s| {
6551 s.select_display_ranges([
6552 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6553 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6554 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
6555 ])
6556 });
6557
6558 editor.handle_input("{", window, cx);
6559 editor.handle_input("{", window, cx);
6560 editor.handle_input("{", window, cx);
6561 assert_eq!(
6562 editor.text(cx),
6563 "
6564 {{{a}}}
6565 {{{b}}}
6566 {{{c}}}
6567 "
6568 .unindent()
6569 );
6570 assert_eq!(
6571 editor.selections.display_ranges(cx),
6572 [
6573 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
6574 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
6575 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
6576 ]
6577 );
6578
6579 editor.undo(&Undo, window, cx);
6580 editor.undo(&Undo, window, cx);
6581 editor.undo(&Undo, window, cx);
6582 assert_eq!(
6583 editor.text(cx),
6584 "
6585 a
6586 b
6587 c
6588 "
6589 .unindent()
6590 );
6591 assert_eq!(
6592 editor.selections.display_ranges(cx),
6593 [
6594 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6595 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6596 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
6597 ]
6598 );
6599
6600 // Ensure inserting the first character of a multi-byte bracket pair
6601 // doesn't surround the selections with the bracket.
6602 editor.handle_input("/", window, cx);
6603 assert_eq!(
6604 editor.text(cx),
6605 "
6606 /
6607 /
6608 /
6609 "
6610 .unindent()
6611 );
6612 assert_eq!(
6613 editor.selections.display_ranges(cx),
6614 [
6615 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6616 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6617 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
6618 ]
6619 );
6620
6621 editor.undo(&Undo, window, cx);
6622 assert_eq!(
6623 editor.text(cx),
6624 "
6625 a
6626 b
6627 c
6628 "
6629 .unindent()
6630 );
6631 assert_eq!(
6632 editor.selections.display_ranges(cx),
6633 [
6634 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6635 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6636 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
6637 ]
6638 );
6639
6640 // Ensure inserting the last character of a multi-byte bracket pair
6641 // doesn't surround the selections with the bracket.
6642 editor.handle_input("*", window, cx);
6643 assert_eq!(
6644 editor.text(cx),
6645 "
6646 *
6647 *
6648 *
6649 "
6650 .unindent()
6651 );
6652 assert_eq!(
6653 editor.selections.display_ranges(cx),
6654 [
6655 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6656 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6657 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
6658 ]
6659 );
6660 });
6661}
6662
6663#[gpui::test]
6664async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
6665 init_test(cx, |_| {});
6666
6667 let language = Arc::new(Language::new(
6668 LanguageConfig {
6669 brackets: BracketPairConfig {
6670 pairs: vec![BracketPair {
6671 start: "{".to_string(),
6672 end: "}".to_string(),
6673 close: true,
6674 surround: true,
6675 newline: true,
6676 }],
6677 ..Default::default()
6678 },
6679 autoclose_before: "}".to_string(),
6680 ..Default::default()
6681 },
6682 Some(tree_sitter_rust::LANGUAGE.into()),
6683 ));
6684
6685 let text = r#"
6686 a
6687 b
6688 c
6689 "#
6690 .unindent();
6691
6692 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6693 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6694 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6695 editor
6696 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6697 .await;
6698
6699 editor.update_in(cx, |editor, window, cx| {
6700 editor.change_selections(None, window, cx, |s| {
6701 s.select_ranges([
6702 Point::new(0, 1)..Point::new(0, 1),
6703 Point::new(1, 1)..Point::new(1, 1),
6704 Point::new(2, 1)..Point::new(2, 1),
6705 ])
6706 });
6707
6708 editor.handle_input("{", window, cx);
6709 editor.handle_input("{", window, cx);
6710 editor.handle_input("_", window, cx);
6711 assert_eq!(
6712 editor.text(cx),
6713 "
6714 a{{_}}
6715 b{{_}}
6716 c{{_}}
6717 "
6718 .unindent()
6719 );
6720 assert_eq!(
6721 editor.selections.ranges::<Point>(cx),
6722 [
6723 Point::new(0, 4)..Point::new(0, 4),
6724 Point::new(1, 4)..Point::new(1, 4),
6725 Point::new(2, 4)..Point::new(2, 4)
6726 ]
6727 );
6728
6729 editor.backspace(&Default::default(), window, cx);
6730 editor.backspace(&Default::default(), window, cx);
6731 assert_eq!(
6732 editor.text(cx),
6733 "
6734 a{}
6735 b{}
6736 c{}
6737 "
6738 .unindent()
6739 );
6740 assert_eq!(
6741 editor.selections.ranges::<Point>(cx),
6742 [
6743 Point::new(0, 2)..Point::new(0, 2),
6744 Point::new(1, 2)..Point::new(1, 2),
6745 Point::new(2, 2)..Point::new(2, 2)
6746 ]
6747 );
6748
6749 editor.delete_to_previous_word_start(&Default::default(), window, cx);
6750 assert_eq!(
6751 editor.text(cx),
6752 "
6753 a
6754 b
6755 c
6756 "
6757 .unindent()
6758 );
6759 assert_eq!(
6760 editor.selections.ranges::<Point>(cx),
6761 [
6762 Point::new(0, 1)..Point::new(0, 1),
6763 Point::new(1, 1)..Point::new(1, 1),
6764 Point::new(2, 1)..Point::new(2, 1)
6765 ]
6766 );
6767 });
6768}
6769
6770#[gpui::test]
6771async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut gpui::TestAppContext) {
6772 init_test(cx, |settings| {
6773 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6774 });
6775
6776 let mut cx = EditorTestContext::new(cx).await;
6777
6778 let language = Arc::new(Language::new(
6779 LanguageConfig {
6780 brackets: BracketPairConfig {
6781 pairs: vec![
6782 BracketPair {
6783 start: "{".to_string(),
6784 end: "}".to_string(),
6785 close: true,
6786 surround: true,
6787 newline: true,
6788 },
6789 BracketPair {
6790 start: "(".to_string(),
6791 end: ")".to_string(),
6792 close: true,
6793 surround: true,
6794 newline: true,
6795 },
6796 BracketPair {
6797 start: "[".to_string(),
6798 end: "]".to_string(),
6799 close: false,
6800 surround: true,
6801 newline: true,
6802 },
6803 ],
6804 ..Default::default()
6805 },
6806 autoclose_before: "})]".to_string(),
6807 ..Default::default()
6808 },
6809 Some(tree_sitter_rust::LANGUAGE.into()),
6810 ));
6811
6812 cx.language_registry().add(language.clone());
6813 cx.update_buffer(|buffer, cx| {
6814 buffer.set_language(Some(language), cx);
6815 });
6816
6817 cx.set_state(
6818 &"
6819 {(ˇ)}
6820 [[ˇ]]
6821 {(ˇ)}
6822 "
6823 .unindent(),
6824 );
6825
6826 cx.update_editor(|editor, window, cx| {
6827 editor.backspace(&Default::default(), window, cx);
6828 editor.backspace(&Default::default(), window, cx);
6829 });
6830
6831 cx.assert_editor_state(
6832 &"
6833 ˇ
6834 ˇ]]
6835 ˇ
6836 "
6837 .unindent(),
6838 );
6839
6840 cx.update_editor(|editor, window, cx| {
6841 editor.handle_input("{", window, cx);
6842 editor.handle_input("{", window, cx);
6843 editor.move_right(&MoveRight, window, cx);
6844 editor.move_right(&MoveRight, window, cx);
6845 editor.move_left(&MoveLeft, window, cx);
6846 editor.move_left(&MoveLeft, window, cx);
6847 editor.backspace(&Default::default(), window, cx);
6848 });
6849
6850 cx.assert_editor_state(
6851 &"
6852 {ˇ}
6853 {ˇ}]]
6854 {ˇ}
6855 "
6856 .unindent(),
6857 );
6858
6859 cx.update_editor(|editor, window, cx| {
6860 editor.backspace(&Default::default(), window, cx);
6861 });
6862
6863 cx.assert_editor_state(
6864 &"
6865 ˇ
6866 ˇ]]
6867 ˇ
6868 "
6869 .unindent(),
6870 );
6871}
6872
6873#[gpui::test]
6874async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) {
6875 init_test(cx, |_| {});
6876
6877 let language = Arc::new(Language::new(
6878 LanguageConfig::default(),
6879 Some(tree_sitter_rust::LANGUAGE.into()),
6880 ));
6881
6882 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
6883 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6884 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6885 editor
6886 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6887 .await;
6888
6889 editor.update_in(cx, |editor, window, cx| {
6890 editor.set_auto_replace_emoji_shortcode(true);
6891
6892 editor.handle_input("Hello ", window, cx);
6893 editor.handle_input(":wave", window, cx);
6894 assert_eq!(editor.text(cx), "Hello :wave".unindent());
6895
6896 editor.handle_input(":", window, cx);
6897 assert_eq!(editor.text(cx), "Hello 👋".unindent());
6898
6899 editor.handle_input(" :smile", window, cx);
6900 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
6901
6902 editor.handle_input(":", window, cx);
6903 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
6904
6905 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
6906 editor.handle_input(":wave", window, cx);
6907 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
6908
6909 editor.handle_input(":", window, cx);
6910 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
6911
6912 editor.handle_input(":1", window, cx);
6913 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
6914
6915 editor.handle_input(":", window, cx);
6916 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
6917
6918 // Ensure shortcode does not get replaced when it is part of a word
6919 editor.handle_input(" Test:wave", window, cx);
6920 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
6921
6922 editor.handle_input(":", window, cx);
6923 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
6924
6925 editor.set_auto_replace_emoji_shortcode(false);
6926
6927 // Ensure shortcode does not get replaced when auto replace is off
6928 editor.handle_input(" :wave", window, cx);
6929 assert_eq!(
6930 editor.text(cx),
6931 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
6932 );
6933
6934 editor.handle_input(":", window, cx);
6935 assert_eq!(
6936 editor.text(cx),
6937 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
6938 );
6939 });
6940}
6941
6942#[gpui::test]
6943async fn test_snippet_placeholder_choices(cx: &mut gpui::TestAppContext) {
6944 init_test(cx, |_| {});
6945
6946 let (text, insertion_ranges) = marked_text_ranges(
6947 indoc! {"
6948 ˇ
6949 "},
6950 false,
6951 );
6952
6953 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
6954 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6955
6956 _ = editor.update_in(cx, |editor, window, cx| {
6957 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
6958
6959 editor
6960 .insert_snippet(&insertion_ranges, snippet, window, cx)
6961 .unwrap();
6962
6963 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
6964 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
6965 assert_eq!(editor.text(cx), expected_text);
6966 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
6967 }
6968
6969 assert(
6970 editor,
6971 cx,
6972 indoc! {"
6973 type «» =•
6974 "},
6975 );
6976
6977 assert!(editor.context_menu_visible(), "There should be a matches");
6978 });
6979}
6980
6981#[gpui::test]
6982async fn test_snippets(cx: &mut gpui::TestAppContext) {
6983 init_test(cx, |_| {});
6984
6985 let (text, insertion_ranges) = marked_text_ranges(
6986 indoc! {"
6987 a.ˇ b
6988 a.ˇ b
6989 a.ˇ b
6990 "},
6991 false,
6992 );
6993
6994 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
6995 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6996
6997 editor.update_in(cx, |editor, window, cx| {
6998 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
6999
7000 editor
7001 .insert_snippet(&insertion_ranges, snippet, window, cx)
7002 .unwrap();
7003
7004 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7005 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7006 assert_eq!(editor.text(cx), expected_text);
7007 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7008 }
7009
7010 assert(
7011 editor,
7012 cx,
7013 indoc! {"
7014 a.f(«one», two, «three») b
7015 a.f(«one», two, «three») b
7016 a.f(«one», two, «three») b
7017 "},
7018 );
7019
7020 // Can't move earlier than the first tab stop
7021 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7022 assert(
7023 editor,
7024 cx,
7025 indoc! {"
7026 a.f(«one», two, «three») b
7027 a.f(«one», two, «three») b
7028 a.f(«one», two, «three») b
7029 "},
7030 );
7031
7032 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7033 assert(
7034 editor,
7035 cx,
7036 indoc! {"
7037 a.f(one, «two», three) b
7038 a.f(one, «two», three) b
7039 a.f(one, «two», three) b
7040 "},
7041 );
7042
7043 editor.move_to_prev_snippet_tabstop(window, cx);
7044 assert(
7045 editor,
7046 cx,
7047 indoc! {"
7048 a.f(«one», two, «three») b
7049 a.f(«one», two, «three») b
7050 a.f(«one», two, «three») b
7051 "},
7052 );
7053
7054 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7055 assert(
7056 editor,
7057 cx,
7058 indoc! {"
7059 a.f(one, «two», three) b
7060 a.f(one, «two», three) b
7061 a.f(one, «two», three) b
7062 "},
7063 );
7064 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7065 assert(
7066 editor,
7067 cx,
7068 indoc! {"
7069 a.f(one, two, three)ˇ b
7070 a.f(one, two, three)ˇ b
7071 a.f(one, two, three)ˇ b
7072 "},
7073 );
7074
7075 // As soon as the last tab stop is reached, snippet state is gone
7076 editor.move_to_prev_snippet_tabstop(window, cx);
7077 assert(
7078 editor,
7079 cx,
7080 indoc! {"
7081 a.f(one, two, three)ˇ b
7082 a.f(one, two, three)ˇ b
7083 a.f(one, two, three)ˇ b
7084 "},
7085 );
7086 });
7087}
7088
7089#[gpui::test]
7090async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
7091 init_test(cx, |_| {});
7092
7093 let fs = FakeFs::new(cx.executor());
7094 fs.insert_file(path!("/file.rs"), Default::default()).await;
7095
7096 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
7097
7098 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7099 language_registry.add(rust_lang());
7100 let mut fake_servers = language_registry.register_fake_lsp(
7101 "Rust",
7102 FakeLspAdapter {
7103 capabilities: lsp::ServerCapabilities {
7104 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7105 ..Default::default()
7106 },
7107 ..Default::default()
7108 },
7109 );
7110
7111 let buffer = project
7112 .update(cx, |project, cx| {
7113 project.open_local_buffer(path!("/file.rs"), cx)
7114 })
7115 .await
7116 .unwrap();
7117
7118 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7119 let (editor, cx) = cx.add_window_view(|window, cx| {
7120 build_editor_with_project(project.clone(), buffer, window, cx)
7121 });
7122 editor.update_in(cx, |editor, window, cx| {
7123 editor.set_text("one\ntwo\nthree\n", window, cx)
7124 });
7125 assert!(cx.read(|cx| editor.is_dirty(cx)));
7126
7127 cx.executor().start_waiting();
7128 let fake_server = fake_servers.next().await.unwrap();
7129
7130 let save = editor
7131 .update_in(cx, |editor, window, cx| {
7132 editor.save(true, project.clone(), window, cx)
7133 })
7134 .unwrap();
7135 fake_server
7136 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7137 assert_eq!(
7138 params.text_document.uri,
7139 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7140 );
7141 assert_eq!(params.options.tab_size, 4);
7142 Ok(Some(vec![lsp::TextEdit::new(
7143 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7144 ", ".to_string(),
7145 )]))
7146 })
7147 .next()
7148 .await;
7149 cx.executor().start_waiting();
7150 save.await;
7151
7152 assert_eq!(
7153 editor.update(cx, |editor, cx| editor.text(cx)),
7154 "one, two\nthree\n"
7155 );
7156 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7157
7158 editor.update_in(cx, |editor, window, cx| {
7159 editor.set_text("one\ntwo\nthree\n", window, cx)
7160 });
7161 assert!(cx.read(|cx| editor.is_dirty(cx)));
7162
7163 // Ensure we can still save even if formatting hangs.
7164 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7165 assert_eq!(
7166 params.text_document.uri,
7167 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7168 );
7169 futures::future::pending::<()>().await;
7170 unreachable!()
7171 });
7172 let save = editor
7173 .update_in(cx, |editor, window, cx| {
7174 editor.save(true, project.clone(), window, cx)
7175 })
7176 .unwrap();
7177 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7178 cx.executor().start_waiting();
7179 save.await;
7180 assert_eq!(
7181 editor.update(cx, |editor, cx| editor.text(cx)),
7182 "one\ntwo\nthree\n"
7183 );
7184 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7185
7186 // For non-dirty buffer, no formatting request should be sent
7187 let save = editor
7188 .update_in(cx, |editor, window, cx| {
7189 editor.save(true, project.clone(), window, cx)
7190 })
7191 .unwrap();
7192 let _pending_format_request = fake_server
7193 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7194 panic!("Should not be invoked on non-dirty buffer");
7195 })
7196 .next();
7197 cx.executor().start_waiting();
7198 save.await;
7199
7200 // Set rust language override and assert overridden tabsize is sent to language server
7201 update_test_language_settings(cx, |settings| {
7202 settings.languages.insert(
7203 "Rust".into(),
7204 LanguageSettingsContent {
7205 tab_size: NonZeroU32::new(8),
7206 ..Default::default()
7207 },
7208 );
7209 });
7210
7211 editor.update_in(cx, |editor, window, cx| {
7212 editor.set_text("somehting_new\n", window, cx)
7213 });
7214 assert!(cx.read(|cx| editor.is_dirty(cx)));
7215 let save = editor
7216 .update_in(cx, |editor, window, cx| {
7217 editor.save(true, project.clone(), window, cx)
7218 })
7219 .unwrap();
7220 fake_server
7221 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7222 assert_eq!(
7223 params.text_document.uri,
7224 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7225 );
7226 assert_eq!(params.options.tab_size, 8);
7227 Ok(Some(vec![]))
7228 })
7229 .next()
7230 .await;
7231 cx.executor().start_waiting();
7232 save.await;
7233}
7234
7235#[gpui::test]
7236async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) {
7237 init_test(cx, |_| {});
7238
7239 let cols = 4;
7240 let rows = 10;
7241 let sample_text_1 = sample_text(rows, cols, 'a');
7242 assert_eq!(
7243 sample_text_1,
7244 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7245 );
7246 let sample_text_2 = sample_text(rows, cols, 'l');
7247 assert_eq!(
7248 sample_text_2,
7249 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7250 );
7251 let sample_text_3 = sample_text(rows, cols, 'v');
7252 assert_eq!(
7253 sample_text_3,
7254 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7255 );
7256
7257 let fs = FakeFs::new(cx.executor());
7258 fs.insert_tree(
7259 path!("/a"),
7260 json!({
7261 "main.rs": sample_text_1,
7262 "other.rs": sample_text_2,
7263 "lib.rs": sample_text_3,
7264 }),
7265 )
7266 .await;
7267
7268 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
7269 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7270 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7271
7272 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7273 language_registry.add(rust_lang());
7274 let mut fake_servers = language_registry.register_fake_lsp(
7275 "Rust",
7276 FakeLspAdapter {
7277 capabilities: lsp::ServerCapabilities {
7278 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7279 ..Default::default()
7280 },
7281 ..Default::default()
7282 },
7283 );
7284
7285 let worktree = project.update(cx, |project, cx| {
7286 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7287 assert_eq!(worktrees.len(), 1);
7288 worktrees.pop().unwrap()
7289 });
7290 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
7291
7292 let buffer_1 = project
7293 .update(cx, |project, cx| {
7294 project.open_buffer((worktree_id, "main.rs"), cx)
7295 })
7296 .await
7297 .unwrap();
7298 let buffer_2 = project
7299 .update(cx, |project, cx| {
7300 project.open_buffer((worktree_id, "other.rs"), cx)
7301 })
7302 .await
7303 .unwrap();
7304 let buffer_3 = project
7305 .update(cx, |project, cx| {
7306 project.open_buffer((worktree_id, "lib.rs"), cx)
7307 })
7308 .await
7309 .unwrap();
7310
7311 let multi_buffer = cx.new(|cx| {
7312 let mut multi_buffer = MultiBuffer::new(ReadWrite);
7313 multi_buffer.push_excerpts(
7314 buffer_1.clone(),
7315 [
7316 ExcerptRange {
7317 context: Point::new(0, 0)..Point::new(3, 0),
7318 primary: None,
7319 },
7320 ExcerptRange {
7321 context: Point::new(5, 0)..Point::new(7, 0),
7322 primary: None,
7323 },
7324 ExcerptRange {
7325 context: Point::new(9, 0)..Point::new(10, 4),
7326 primary: None,
7327 },
7328 ],
7329 cx,
7330 );
7331 multi_buffer.push_excerpts(
7332 buffer_2.clone(),
7333 [
7334 ExcerptRange {
7335 context: Point::new(0, 0)..Point::new(3, 0),
7336 primary: None,
7337 },
7338 ExcerptRange {
7339 context: Point::new(5, 0)..Point::new(7, 0),
7340 primary: None,
7341 },
7342 ExcerptRange {
7343 context: Point::new(9, 0)..Point::new(10, 4),
7344 primary: None,
7345 },
7346 ],
7347 cx,
7348 );
7349 multi_buffer.push_excerpts(
7350 buffer_3.clone(),
7351 [
7352 ExcerptRange {
7353 context: Point::new(0, 0)..Point::new(3, 0),
7354 primary: None,
7355 },
7356 ExcerptRange {
7357 context: Point::new(5, 0)..Point::new(7, 0),
7358 primary: None,
7359 },
7360 ExcerptRange {
7361 context: Point::new(9, 0)..Point::new(10, 4),
7362 primary: None,
7363 },
7364 ],
7365 cx,
7366 );
7367 multi_buffer
7368 });
7369 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
7370 Editor::new(
7371 EditorMode::Full,
7372 multi_buffer,
7373 Some(project.clone()),
7374 true,
7375 window,
7376 cx,
7377 )
7378 });
7379
7380 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7381 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7382 s.select_ranges(Some(1..2))
7383 });
7384 editor.insert("|one|two|three|", window, cx);
7385 });
7386 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7387 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7388 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7389 s.select_ranges(Some(60..70))
7390 });
7391 editor.insert("|four|five|six|", window, cx);
7392 });
7393 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7394
7395 // First two buffers should be edited, but not the third one.
7396 assert_eq!(
7397 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7398 "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}",
7399 );
7400 buffer_1.update(cx, |buffer, _| {
7401 assert!(buffer.is_dirty());
7402 assert_eq!(
7403 buffer.text(),
7404 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
7405 )
7406 });
7407 buffer_2.update(cx, |buffer, _| {
7408 assert!(buffer.is_dirty());
7409 assert_eq!(
7410 buffer.text(),
7411 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
7412 )
7413 });
7414 buffer_3.update(cx, |buffer, _| {
7415 assert!(!buffer.is_dirty());
7416 assert_eq!(buffer.text(), sample_text_3,)
7417 });
7418 cx.executor().run_until_parked();
7419
7420 cx.executor().start_waiting();
7421 let save = multi_buffer_editor
7422 .update_in(cx, |editor, window, cx| {
7423 editor.save(true, project.clone(), window, cx)
7424 })
7425 .unwrap();
7426
7427 let fake_server = fake_servers.next().await.unwrap();
7428 fake_server
7429 .server
7430 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7431 Ok(Some(vec![lsp::TextEdit::new(
7432 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7433 format!("[{} formatted]", params.text_document.uri),
7434 )]))
7435 })
7436 .detach();
7437 save.await;
7438
7439 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
7440 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
7441 assert_eq!(
7442 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7443 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}"),
7444 );
7445 buffer_1.update(cx, |buffer, _| {
7446 assert!(!buffer.is_dirty());
7447 assert_eq!(
7448 buffer.text(),
7449 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
7450 )
7451 });
7452 buffer_2.update(cx, |buffer, _| {
7453 assert!(!buffer.is_dirty());
7454 assert_eq!(
7455 buffer.text(),
7456 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
7457 )
7458 });
7459 buffer_3.update(cx, |buffer, _| {
7460 assert!(!buffer.is_dirty());
7461 assert_eq!(buffer.text(), sample_text_3,)
7462 });
7463}
7464
7465#[gpui::test]
7466async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
7467 init_test(cx, |_| {});
7468
7469 let fs = FakeFs::new(cx.executor());
7470 fs.insert_file(path!("/file.rs"), Default::default()).await;
7471
7472 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
7473
7474 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7475 language_registry.add(rust_lang());
7476 let mut fake_servers = language_registry.register_fake_lsp(
7477 "Rust",
7478 FakeLspAdapter {
7479 capabilities: lsp::ServerCapabilities {
7480 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
7481 ..Default::default()
7482 },
7483 ..Default::default()
7484 },
7485 );
7486
7487 let buffer = project
7488 .update(cx, |project, cx| {
7489 project.open_local_buffer(path!("/file.rs"), cx)
7490 })
7491 .await
7492 .unwrap();
7493
7494 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7495 let (editor, cx) = cx.add_window_view(|window, cx| {
7496 build_editor_with_project(project.clone(), buffer, window, cx)
7497 });
7498 editor.update_in(cx, |editor, window, cx| {
7499 editor.set_text("one\ntwo\nthree\n", window, cx)
7500 });
7501 assert!(cx.read(|cx| editor.is_dirty(cx)));
7502
7503 cx.executor().start_waiting();
7504 let fake_server = fake_servers.next().await.unwrap();
7505
7506 let save = editor
7507 .update_in(cx, |editor, window, cx| {
7508 editor.save(true, project.clone(), window, cx)
7509 })
7510 .unwrap();
7511 fake_server
7512 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
7513 assert_eq!(
7514 params.text_document.uri,
7515 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7516 );
7517 assert_eq!(params.options.tab_size, 4);
7518 Ok(Some(vec![lsp::TextEdit::new(
7519 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7520 ", ".to_string(),
7521 )]))
7522 })
7523 .next()
7524 .await;
7525 cx.executor().start_waiting();
7526 save.await;
7527 assert_eq!(
7528 editor.update(cx, |editor, cx| editor.text(cx)),
7529 "one, two\nthree\n"
7530 );
7531 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7532
7533 editor.update_in(cx, |editor, window, cx| {
7534 editor.set_text("one\ntwo\nthree\n", window, cx)
7535 });
7536 assert!(cx.read(|cx| editor.is_dirty(cx)));
7537
7538 // Ensure we can still save even if formatting hangs.
7539 fake_server.handle_request::<lsp::request::RangeFormatting, _, _>(
7540 move |params, _| async move {
7541 assert_eq!(
7542 params.text_document.uri,
7543 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7544 );
7545 futures::future::pending::<()>().await;
7546 unreachable!()
7547 },
7548 );
7549 let save = editor
7550 .update_in(cx, |editor, window, cx| {
7551 editor.save(true, project.clone(), window, cx)
7552 })
7553 .unwrap();
7554 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7555 cx.executor().start_waiting();
7556 save.await;
7557 assert_eq!(
7558 editor.update(cx, |editor, cx| editor.text(cx)),
7559 "one\ntwo\nthree\n"
7560 );
7561 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7562
7563 // For non-dirty buffer, no formatting request should be sent
7564 let save = editor
7565 .update_in(cx, |editor, window, cx| {
7566 editor.save(true, project.clone(), window, cx)
7567 })
7568 .unwrap();
7569 let _pending_format_request = fake_server
7570 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7571 panic!("Should not be invoked on non-dirty buffer");
7572 })
7573 .next();
7574 cx.executor().start_waiting();
7575 save.await;
7576
7577 // Set Rust language override and assert overridden tabsize is sent to language server
7578 update_test_language_settings(cx, |settings| {
7579 settings.languages.insert(
7580 "Rust".into(),
7581 LanguageSettingsContent {
7582 tab_size: NonZeroU32::new(8),
7583 ..Default::default()
7584 },
7585 );
7586 });
7587
7588 editor.update_in(cx, |editor, window, cx| {
7589 editor.set_text("somehting_new\n", window, cx)
7590 });
7591 assert!(cx.read(|cx| editor.is_dirty(cx)));
7592 let save = editor
7593 .update_in(cx, |editor, window, cx| {
7594 editor.save(true, project.clone(), window, cx)
7595 })
7596 .unwrap();
7597 fake_server
7598 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
7599 assert_eq!(
7600 params.text_document.uri,
7601 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7602 );
7603 assert_eq!(params.options.tab_size, 8);
7604 Ok(Some(vec![]))
7605 })
7606 .next()
7607 .await;
7608 cx.executor().start_waiting();
7609 save.await;
7610}
7611
7612#[gpui::test]
7613async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
7614 init_test(cx, |settings| {
7615 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
7616 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
7617 ))
7618 });
7619
7620 let fs = FakeFs::new(cx.executor());
7621 fs.insert_file(path!("/file.rs"), Default::default()).await;
7622
7623 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
7624
7625 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7626 language_registry.add(Arc::new(Language::new(
7627 LanguageConfig {
7628 name: "Rust".into(),
7629 matcher: LanguageMatcher {
7630 path_suffixes: vec!["rs".to_string()],
7631 ..Default::default()
7632 },
7633 ..LanguageConfig::default()
7634 },
7635 Some(tree_sitter_rust::LANGUAGE.into()),
7636 )));
7637 update_test_language_settings(cx, |settings| {
7638 // Enable Prettier formatting for the same buffer, and ensure
7639 // LSP is called instead of Prettier.
7640 settings.defaults.prettier = Some(PrettierSettings {
7641 allowed: true,
7642 ..PrettierSettings::default()
7643 });
7644 });
7645 let mut fake_servers = language_registry.register_fake_lsp(
7646 "Rust",
7647 FakeLspAdapter {
7648 capabilities: lsp::ServerCapabilities {
7649 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7650 ..Default::default()
7651 },
7652 ..Default::default()
7653 },
7654 );
7655
7656 let buffer = project
7657 .update(cx, |project, cx| {
7658 project.open_local_buffer(path!("/file.rs"), cx)
7659 })
7660 .await
7661 .unwrap();
7662
7663 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7664 let (editor, cx) = cx.add_window_view(|window, cx| {
7665 build_editor_with_project(project.clone(), buffer, window, cx)
7666 });
7667 editor.update_in(cx, |editor, window, cx| {
7668 editor.set_text("one\ntwo\nthree\n", window, cx)
7669 });
7670
7671 cx.executor().start_waiting();
7672 let fake_server = fake_servers.next().await.unwrap();
7673
7674 let format = editor
7675 .update_in(cx, |editor, window, cx| {
7676 editor.perform_format(
7677 project.clone(),
7678 FormatTrigger::Manual,
7679 FormatTarget::Buffers,
7680 window,
7681 cx,
7682 )
7683 })
7684 .unwrap();
7685 fake_server
7686 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7687 assert_eq!(
7688 params.text_document.uri,
7689 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7690 );
7691 assert_eq!(params.options.tab_size, 4);
7692 Ok(Some(vec![lsp::TextEdit::new(
7693 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7694 ", ".to_string(),
7695 )]))
7696 })
7697 .next()
7698 .await;
7699 cx.executor().start_waiting();
7700 format.await;
7701 assert_eq!(
7702 editor.update(cx, |editor, cx| editor.text(cx)),
7703 "one, two\nthree\n"
7704 );
7705
7706 editor.update_in(cx, |editor, window, cx| {
7707 editor.set_text("one\ntwo\nthree\n", window, cx)
7708 });
7709 // Ensure we don't lock if formatting hangs.
7710 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7711 assert_eq!(
7712 params.text_document.uri,
7713 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7714 );
7715 futures::future::pending::<()>().await;
7716 unreachable!()
7717 });
7718 let format = editor
7719 .update_in(cx, |editor, window, cx| {
7720 editor.perform_format(
7721 project,
7722 FormatTrigger::Manual,
7723 FormatTarget::Buffers,
7724 window,
7725 cx,
7726 )
7727 })
7728 .unwrap();
7729 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7730 cx.executor().start_waiting();
7731 format.await;
7732 assert_eq!(
7733 editor.update(cx, |editor, cx| editor.text(cx)),
7734 "one\ntwo\nthree\n"
7735 );
7736}
7737
7738#[gpui::test]
7739async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
7740 init_test(cx, |_| {});
7741
7742 let mut cx = EditorLspTestContext::new_rust(
7743 lsp::ServerCapabilities {
7744 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7745 ..Default::default()
7746 },
7747 cx,
7748 )
7749 .await;
7750
7751 cx.set_state(indoc! {"
7752 one.twoˇ
7753 "});
7754
7755 // The format request takes a long time. When it completes, it inserts
7756 // a newline and an indent before the `.`
7757 cx.lsp
7758 .handle_request::<lsp::request::Formatting, _, _>(move |_, cx| {
7759 let executor = cx.background_executor().clone();
7760 async move {
7761 executor.timer(Duration::from_millis(100)).await;
7762 Ok(Some(vec![lsp::TextEdit {
7763 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
7764 new_text: "\n ".into(),
7765 }]))
7766 }
7767 });
7768
7769 // Submit a format request.
7770 let format_1 = cx
7771 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7772 .unwrap();
7773 cx.executor().run_until_parked();
7774
7775 // Submit a second format request.
7776 let format_2 = cx
7777 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7778 .unwrap();
7779 cx.executor().run_until_parked();
7780
7781 // Wait for both format requests to complete
7782 cx.executor().advance_clock(Duration::from_millis(200));
7783 cx.executor().start_waiting();
7784 format_1.await.unwrap();
7785 cx.executor().start_waiting();
7786 format_2.await.unwrap();
7787
7788 // The formatting edits only happens once.
7789 cx.assert_editor_state(indoc! {"
7790 one
7791 .twoˇ
7792 "});
7793}
7794
7795#[gpui::test]
7796async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
7797 init_test(cx, |settings| {
7798 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
7799 });
7800
7801 let mut cx = EditorLspTestContext::new_rust(
7802 lsp::ServerCapabilities {
7803 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7804 ..Default::default()
7805 },
7806 cx,
7807 )
7808 .await;
7809
7810 // Set up a buffer white some trailing whitespace and no trailing newline.
7811 cx.set_state(
7812 &[
7813 "one ", //
7814 "twoˇ", //
7815 "three ", //
7816 "four", //
7817 ]
7818 .join("\n"),
7819 );
7820
7821 // Submit a format request.
7822 let format = cx
7823 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7824 .unwrap();
7825
7826 // Record which buffer changes have been sent to the language server
7827 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
7828 cx.lsp
7829 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
7830 let buffer_changes = buffer_changes.clone();
7831 move |params, _| {
7832 buffer_changes.lock().extend(
7833 params
7834 .content_changes
7835 .into_iter()
7836 .map(|e| (e.range.unwrap(), e.text)),
7837 );
7838 }
7839 });
7840
7841 // Handle formatting requests to the language server.
7842 cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
7843 let buffer_changes = buffer_changes.clone();
7844 move |_, _| {
7845 // When formatting is requested, trailing whitespace has already been stripped,
7846 // and the trailing newline has already been added.
7847 assert_eq!(
7848 &buffer_changes.lock()[1..],
7849 &[
7850 (
7851 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
7852 "".into()
7853 ),
7854 (
7855 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
7856 "".into()
7857 ),
7858 (
7859 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
7860 "\n".into()
7861 ),
7862 ]
7863 );
7864
7865 // Insert blank lines between each line of the buffer.
7866 async move {
7867 Ok(Some(vec![
7868 lsp::TextEdit {
7869 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
7870 new_text: "\n".into(),
7871 },
7872 lsp::TextEdit {
7873 range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
7874 new_text: "\n".into(),
7875 },
7876 ]))
7877 }
7878 }
7879 });
7880
7881 // After formatting the buffer, the trailing whitespace is stripped,
7882 // a newline is appended, and the edits provided by the language server
7883 // have been applied.
7884 format.await.unwrap();
7885 cx.assert_editor_state(
7886 &[
7887 "one", //
7888 "", //
7889 "twoˇ", //
7890 "", //
7891 "three", //
7892 "four", //
7893 "", //
7894 ]
7895 .join("\n"),
7896 );
7897
7898 // Undoing the formatting undoes the trailing whitespace removal, the
7899 // trailing newline, and the LSP edits.
7900 cx.update_buffer(|buffer, cx| buffer.undo(cx));
7901 cx.assert_editor_state(
7902 &[
7903 "one ", //
7904 "twoˇ", //
7905 "three ", //
7906 "four", //
7907 ]
7908 .join("\n"),
7909 );
7910}
7911
7912#[gpui::test]
7913async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
7914 cx: &mut gpui::TestAppContext,
7915) {
7916 init_test(cx, |_| {});
7917
7918 cx.update(|cx| {
7919 cx.update_global::<SettingsStore, _>(|settings, cx| {
7920 settings.update_user_settings::<EditorSettings>(cx, |settings| {
7921 settings.auto_signature_help = Some(true);
7922 });
7923 });
7924 });
7925
7926 let mut cx = EditorLspTestContext::new_rust(
7927 lsp::ServerCapabilities {
7928 signature_help_provider: Some(lsp::SignatureHelpOptions {
7929 ..Default::default()
7930 }),
7931 ..Default::default()
7932 },
7933 cx,
7934 )
7935 .await;
7936
7937 let language = Language::new(
7938 LanguageConfig {
7939 name: "Rust".into(),
7940 brackets: BracketPairConfig {
7941 pairs: vec![
7942 BracketPair {
7943 start: "{".to_string(),
7944 end: "}".to_string(),
7945 close: true,
7946 surround: true,
7947 newline: true,
7948 },
7949 BracketPair {
7950 start: "(".to_string(),
7951 end: ")".to_string(),
7952 close: true,
7953 surround: true,
7954 newline: true,
7955 },
7956 BracketPair {
7957 start: "/*".to_string(),
7958 end: " */".to_string(),
7959 close: true,
7960 surround: true,
7961 newline: true,
7962 },
7963 BracketPair {
7964 start: "[".to_string(),
7965 end: "]".to_string(),
7966 close: false,
7967 surround: false,
7968 newline: true,
7969 },
7970 BracketPair {
7971 start: "\"".to_string(),
7972 end: "\"".to_string(),
7973 close: true,
7974 surround: true,
7975 newline: false,
7976 },
7977 BracketPair {
7978 start: "<".to_string(),
7979 end: ">".to_string(),
7980 close: false,
7981 surround: true,
7982 newline: true,
7983 },
7984 ],
7985 ..Default::default()
7986 },
7987 autoclose_before: "})]".to_string(),
7988 ..Default::default()
7989 },
7990 Some(tree_sitter_rust::LANGUAGE.into()),
7991 );
7992 let language = Arc::new(language);
7993
7994 cx.language_registry().add(language.clone());
7995 cx.update_buffer(|buffer, cx| {
7996 buffer.set_language(Some(language), cx);
7997 });
7998
7999 cx.set_state(
8000 &r#"
8001 fn main() {
8002 sampleˇ
8003 }
8004 "#
8005 .unindent(),
8006 );
8007
8008 cx.update_editor(|editor, window, cx| {
8009 editor.handle_input("(", window, cx);
8010 });
8011 cx.assert_editor_state(
8012 &"
8013 fn main() {
8014 sample(ˇ)
8015 }
8016 "
8017 .unindent(),
8018 );
8019
8020 let mocked_response = lsp::SignatureHelp {
8021 signatures: vec![lsp::SignatureInformation {
8022 label: "fn sample(param1: u8, param2: u8)".to_string(),
8023 documentation: None,
8024 parameters: Some(vec![
8025 lsp::ParameterInformation {
8026 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8027 documentation: None,
8028 },
8029 lsp::ParameterInformation {
8030 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8031 documentation: None,
8032 },
8033 ]),
8034 active_parameter: None,
8035 }],
8036 active_signature: Some(0),
8037 active_parameter: Some(0),
8038 };
8039 handle_signature_help_request(&mut cx, mocked_response).await;
8040
8041 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8042 .await;
8043
8044 cx.editor(|editor, _, _| {
8045 let signature_help_state = editor.signature_help_state.popover().cloned();
8046 assert!(signature_help_state.is_some());
8047 let ParsedMarkdown {
8048 text, highlights, ..
8049 } = signature_help_state.unwrap().parsed_content;
8050 assert_eq!(text, "param1: u8, param2: u8");
8051 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8052 });
8053}
8054
8055#[gpui::test]
8056async fn test_handle_input_with_different_show_signature_settings(cx: &mut gpui::TestAppContext) {
8057 init_test(cx, |_| {});
8058
8059 cx.update(|cx| {
8060 cx.update_global::<SettingsStore, _>(|settings, cx| {
8061 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8062 settings.auto_signature_help = Some(false);
8063 settings.show_signature_help_after_edits = Some(false);
8064 });
8065 });
8066 });
8067
8068 let mut cx = EditorLspTestContext::new_rust(
8069 lsp::ServerCapabilities {
8070 signature_help_provider: Some(lsp::SignatureHelpOptions {
8071 ..Default::default()
8072 }),
8073 ..Default::default()
8074 },
8075 cx,
8076 )
8077 .await;
8078
8079 let language = Language::new(
8080 LanguageConfig {
8081 name: "Rust".into(),
8082 brackets: BracketPairConfig {
8083 pairs: vec![
8084 BracketPair {
8085 start: "{".to_string(),
8086 end: "}".to_string(),
8087 close: true,
8088 surround: true,
8089 newline: true,
8090 },
8091 BracketPair {
8092 start: "(".to_string(),
8093 end: ")".to_string(),
8094 close: true,
8095 surround: true,
8096 newline: true,
8097 },
8098 BracketPair {
8099 start: "/*".to_string(),
8100 end: " */".to_string(),
8101 close: true,
8102 surround: true,
8103 newline: true,
8104 },
8105 BracketPair {
8106 start: "[".to_string(),
8107 end: "]".to_string(),
8108 close: false,
8109 surround: false,
8110 newline: true,
8111 },
8112 BracketPair {
8113 start: "\"".to_string(),
8114 end: "\"".to_string(),
8115 close: true,
8116 surround: true,
8117 newline: false,
8118 },
8119 BracketPair {
8120 start: "<".to_string(),
8121 end: ">".to_string(),
8122 close: false,
8123 surround: true,
8124 newline: true,
8125 },
8126 ],
8127 ..Default::default()
8128 },
8129 autoclose_before: "})]".to_string(),
8130 ..Default::default()
8131 },
8132 Some(tree_sitter_rust::LANGUAGE.into()),
8133 );
8134 let language = Arc::new(language);
8135
8136 cx.language_registry().add(language.clone());
8137 cx.update_buffer(|buffer, cx| {
8138 buffer.set_language(Some(language), cx);
8139 });
8140
8141 // Ensure that signature_help is not called when no signature help is enabled.
8142 cx.set_state(
8143 &r#"
8144 fn main() {
8145 sampleˇ
8146 }
8147 "#
8148 .unindent(),
8149 );
8150 cx.update_editor(|editor, window, cx| {
8151 editor.handle_input("(", window, cx);
8152 });
8153 cx.assert_editor_state(
8154 &"
8155 fn main() {
8156 sample(ˇ)
8157 }
8158 "
8159 .unindent(),
8160 );
8161 cx.editor(|editor, _, _| {
8162 assert!(editor.signature_help_state.task().is_none());
8163 });
8164
8165 let mocked_response = lsp::SignatureHelp {
8166 signatures: vec![lsp::SignatureInformation {
8167 label: "fn sample(param1: u8, param2: u8)".to_string(),
8168 documentation: None,
8169 parameters: Some(vec![
8170 lsp::ParameterInformation {
8171 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8172 documentation: None,
8173 },
8174 lsp::ParameterInformation {
8175 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8176 documentation: None,
8177 },
8178 ]),
8179 active_parameter: None,
8180 }],
8181 active_signature: Some(0),
8182 active_parameter: Some(0),
8183 };
8184
8185 // Ensure that signature_help is called when enabled afte edits
8186 cx.update(|_, cx| {
8187 cx.update_global::<SettingsStore, _>(|settings, cx| {
8188 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8189 settings.auto_signature_help = Some(false);
8190 settings.show_signature_help_after_edits = Some(true);
8191 });
8192 });
8193 });
8194 cx.set_state(
8195 &r#"
8196 fn main() {
8197 sampleˇ
8198 }
8199 "#
8200 .unindent(),
8201 );
8202 cx.update_editor(|editor, window, cx| {
8203 editor.handle_input("(", window, cx);
8204 });
8205 cx.assert_editor_state(
8206 &"
8207 fn main() {
8208 sample(ˇ)
8209 }
8210 "
8211 .unindent(),
8212 );
8213 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8214 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8215 .await;
8216 cx.update_editor(|editor, _, _| {
8217 let signature_help_state = editor.signature_help_state.popover().cloned();
8218 assert!(signature_help_state.is_some());
8219 let ParsedMarkdown {
8220 text, highlights, ..
8221 } = signature_help_state.unwrap().parsed_content;
8222 assert_eq!(text, "param1: u8, param2: u8");
8223 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8224 editor.signature_help_state = SignatureHelpState::default();
8225 });
8226
8227 // Ensure that signature_help is called when auto signature help override is enabled
8228 cx.update(|_, cx| {
8229 cx.update_global::<SettingsStore, _>(|settings, cx| {
8230 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8231 settings.auto_signature_help = Some(true);
8232 settings.show_signature_help_after_edits = Some(false);
8233 });
8234 });
8235 });
8236 cx.set_state(
8237 &r#"
8238 fn main() {
8239 sampleˇ
8240 }
8241 "#
8242 .unindent(),
8243 );
8244 cx.update_editor(|editor, window, cx| {
8245 editor.handle_input("(", window, cx);
8246 });
8247 cx.assert_editor_state(
8248 &"
8249 fn main() {
8250 sample(ˇ)
8251 }
8252 "
8253 .unindent(),
8254 );
8255 handle_signature_help_request(&mut cx, mocked_response).await;
8256 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8257 .await;
8258 cx.editor(|editor, _, _| {
8259 let signature_help_state = editor.signature_help_state.popover().cloned();
8260 assert!(signature_help_state.is_some());
8261 let ParsedMarkdown {
8262 text, highlights, ..
8263 } = signature_help_state.unwrap().parsed_content;
8264 assert_eq!(text, "param1: u8, param2: u8");
8265 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8266 });
8267}
8268
8269#[gpui::test]
8270async fn test_signature_help(cx: &mut gpui::TestAppContext) {
8271 init_test(cx, |_| {});
8272 cx.update(|cx| {
8273 cx.update_global::<SettingsStore, _>(|settings, cx| {
8274 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8275 settings.auto_signature_help = Some(true);
8276 });
8277 });
8278 });
8279
8280 let mut cx = EditorLspTestContext::new_rust(
8281 lsp::ServerCapabilities {
8282 signature_help_provider: Some(lsp::SignatureHelpOptions {
8283 ..Default::default()
8284 }),
8285 ..Default::default()
8286 },
8287 cx,
8288 )
8289 .await;
8290
8291 // A test that directly calls `show_signature_help`
8292 cx.update_editor(|editor, window, cx| {
8293 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8294 });
8295
8296 let mocked_response = lsp::SignatureHelp {
8297 signatures: vec![lsp::SignatureInformation {
8298 label: "fn sample(param1: u8, param2: u8)".to_string(),
8299 documentation: None,
8300 parameters: Some(vec![
8301 lsp::ParameterInformation {
8302 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8303 documentation: None,
8304 },
8305 lsp::ParameterInformation {
8306 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8307 documentation: None,
8308 },
8309 ]),
8310 active_parameter: None,
8311 }],
8312 active_signature: Some(0),
8313 active_parameter: Some(0),
8314 };
8315 handle_signature_help_request(&mut cx, mocked_response).await;
8316
8317 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8318 .await;
8319
8320 cx.editor(|editor, _, _| {
8321 let signature_help_state = editor.signature_help_state.popover().cloned();
8322 assert!(signature_help_state.is_some());
8323 let ParsedMarkdown {
8324 text, highlights, ..
8325 } = signature_help_state.unwrap().parsed_content;
8326 assert_eq!(text, "param1: u8, param2: u8");
8327 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8328 });
8329
8330 // When exiting outside from inside the brackets, `signature_help` is closed.
8331 cx.set_state(indoc! {"
8332 fn main() {
8333 sample(ˇ);
8334 }
8335
8336 fn sample(param1: u8, param2: u8) {}
8337 "});
8338
8339 cx.update_editor(|editor, window, cx| {
8340 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
8341 });
8342
8343 let mocked_response = lsp::SignatureHelp {
8344 signatures: Vec::new(),
8345 active_signature: None,
8346 active_parameter: None,
8347 };
8348 handle_signature_help_request(&mut cx, mocked_response).await;
8349
8350 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
8351 .await;
8352
8353 cx.editor(|editor, _, _| {
8354 assert!(!editor.signature_help_state.is_shown());
8355 });
8356
8357 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
8358 cx.set_state(indoc! {"
8359 fn main() {
8360 sample(ˇ);
8361 }
8362
8363 fn sample(param1: u8, param2: u8) {}
8364 "});
8365
8366 let mocked_response = lsp::SignatureHelp {
8367 signatures: vec![lsp::SignatureInformation {
8368 label: "fn sample(param1: u8, param2: u8)".to_string(),
8369 documentation: None,
8370 parameters: Some(vec![
8371 lsp::ParameterInformation {
8372 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8373 documentation: None,
8374 },
8375 lsp::ParameterInformation {
8376 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8377 documentation: None,
8378 },
8379 ]),
8380 active_parameter: None,
8381 }],
8382 active_signature: Some(0),
8383 active_parameter: Some(0),
8384 };
8385 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8386 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8387 .await;
8388 cx.editor(|editor, _, _| {
8389 assert!(editor.signature_help_state.is_shown());
8390 });
8391
8392 // Restore the popover with more parameter input
8393 cx.set_state(indoc! {"
8394 fn main() {
8395 sample(param1, param2ˇ);
8396 }
8397
8398 fn sample(param1: u8, param2: u8) {}
8399 "});
8400
8401 let mocked_response = lsp::SignatureHelp {
8402 signatures: vec![lsp::SignatureInformation {
8403 label: "fn sample(param1: u8, param2: u8)".to_string(),
8404 documentation: None,
8405 parameters: Some(vec![
8406 lsp::ParameterInformation {
8407 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8408 documentation: None,
8409 },
8410 lsp::ParameterInformation {
8411 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8412 documentation: None,
8413 },
8414 ]),
8415 active_parameter: None,
8416 }],
8417 active_signature: Some(0),
8418 active_parameter: Some(1),
8419 };
8420 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8421 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8422 .await;
8423
8424 // When selecting a range, the popover is gone.
8425 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
8426 cx.update_editor(|editor, window, cx| {
8427 editor.change_selections(None, window, cx, |s| {
8428 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
8429 })
8430 });
8431 cx.assert_editor_state(indoc! {"
8432 fn main() {
8433 sample(param1, «ˇparam2»);
8434 }
8435
8436 fn sample(param1: u8, param2: u8) {}
8437 "});
8438 cx.editor(|editor, _, _| {
8439 assert!(!editor.signature_help_state.is_shown());
8440 });
8441
8442 // When unselecting again, the popover is back if within the brackets.
8443 cx.update_editor(|editor, window, cx| {
8444 editor.change_selections(None, window, cx, |s| {
8445 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8446 })
8447 });
8448 cx.assert_editor_state(indoc! {"
8449 fn main() {
8450 sample(param1, ˇparam2);
8451 }
8452
8453 fn sample(param1: u8, param2: u8) {}
8454 "});
8455 handle_signature_help_request(&mut cx, mocked_response).await;
8456 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8457 .await;
8458 cx.editor(|editor, _, _| {
8459 assert!(editor.signature_help_state.is_shown());
8460 });
8461
8462 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
8463 cx.update_editor(|editor, window, cx| {
8464 editor.change_selections(None, window, cx, |s| {
8465 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
8466 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8467 })
8468 });
8469 cx.assert_editor_state(indoc! {"
8470 fn main() {
8471 sample(param1, ˇparam2);
8472 }
8473
8474 fn sample(param1: u8, param2: u8) {}
8475 "});
8476
8477 let mocked_response = lsp::SignatureHelp {
8478 signatures: vec![lsp::SignatureInformation {
8479 label: "fn sample(param1: u8, param2: u8)".to_string(),
8480 documentation: None,
8481 parameters: Some(vec![
8482 lsp::ParameterInformation {
8483 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8484 documentation: None,
8485 },
8486 lsp::ParameterInformation {
8487 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8488 documentation: None,
8489 },
8490 ]),
8491 active_parameter: None,
8492 }],
8493 active_signature: Some(0),
8494 active_parameter: Some(1),
8495 };
8496 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8497 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8498 .await;
8499 cx.update_editor(|editor, _, cx| {
8500 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
8501 });
8502 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
8503 .await;
8504 cx.update_editor(|editor, window, cx| {
8505 editor.change_selections(None, window, cx, |s| {
8506 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
8507 })
8508 });
8509 cx.assert_editor_state(indoc! {"
8510 fn main() {
8511 sample(param1, «ˇparam2»);
8512 }
8513
8514 fn sample(param1: u8, param2: u8) {}
8515 "});
8516 cx.update_editor(|editor, window, cx| {
8517 editor.change_selections(None, window, cx, |s| {
8518 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8519 })
8520 });
8521 cx.assert_editor_state(indoc! {"
8522 fn main() {
8523 sample(param1, ˇparam2);
8524 }
8525
8526 fn sample(param1: u8, param2: u8) {}
8527 "});
8528 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
8529 .await;
8530}
8531
8532#[gpui::test]
8533async fn test_completion(cx: &mut gpui::TestAppContext) {
8534 init_test(cx, |_| {});
8535
8536 let mut cx = EditorLspTestContext::new_rust(
8537 lsp::ServerCapabilities {
8538 completion_provider: Some(lsp::CompletionOptions {
8539 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
8540 resolve_provider: Some(true),
8541 ..Default::default()
8542 }),
8543 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
8544 ..Default::default()
8545 },
8546 cx,
8547 )
8548 .await;
8549 let counter = Arc::new(AtomicUsize::new(0));
8550
8551 cx.set_state(indoc! {"
8552 oneˇ
8553 two
8554 three
8555 "});
8556 cx.simulate_keystroke(".");
8557 handle_completion_request(
8558 &mut cx,
8559 indoc! {"
8560 one.|<>
8561 two
8562 three
8563 "},
8564 vec!["first_completion", "second_completion"],
8565 counter.clone(),
8566 )
8567 .await;
8568 cx.condition(|editor, _| editor.context_menu_visible())
8569 .await;
8570 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
8571
8572 let _handler = handle_signature_help_request(
8573 &mut cx,
8574 lsp::SignatureHelp {
8575 signatures: vec![lsp::SignatureInformation {
8576 label: "test signature".to_string(),
8577 documentation: None,
8578 parameters: Some(vec![lsp::ParameterInformation {
8579 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
8580 documentation: None,
8581 }]),
8582 active_parameter: None,
8583 }],
8584 active_signature: None,
8585 active_parameter: None,
8586 },
8587 );
8588 cx.update_editor(|editor, window, cx| {
8589 assert!(
8590 !editor.signature_help_state.is_shown(),
8591 "No signature help was called for"
8592 );
8593 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8594 });
8595 cx.run_until_parked();
8596 cx.update_editor(|editor, _, _| {
8597 assert!(
8598 !editor.signature_help_state.is_shown(),
8599 "No signature help should be shown when completions menu is open"
8600 );
8601 });
8602
8603 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8604 editor.context_menu_next(&Default::default(), window, cx);
8605 editor
8606 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8607 .unwrap()
8608 });
8609 cx.assert_editor_state(indoc! {"
8610 one.second_completionˇ
8611 two
8612 three
8613 "});
8614
8615 handle_resolve_completion_request(
8616 &mut cx,
8617 Some(vec![
8618 (
8619 //This overlaps with the primary completion edit which is
8620 //misbehavior from the LSP spec, test that we filter it out
8621 indoc! {"
8622 one.second_ˇcompletion
8623 two
8624 threeˇ
8625 "},
8626 "overlapping additional edit",
8627 ),
8628 (
8629 indoc! {"
8630 one.second_completion
8631 two
8632 threeˇ
8633 "},
8634 "\nadditional edit",
8635 ),
8636 ]),
8637 )
8638 .await;
8639 apply_additional_edits.await.unwrap();
8640 cx.assert_editor_state(indoc! {"
8641 one.second_completionˇ
8642 two
8643 three
8644 additional edit
8645 "});
8646
8647 cx.set_state(indoc! {"
8648 one.second_completion
8649 twoˇ
8650 threeˇ
8651 additional edit
8652 "});
8653 cx.simulate_keystroke(" ");
8654 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8655 cx.simulate_keystroke("s");
8656 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8657
8658 cx.assert_editor_state(indoc! {"
8659 one.second_completion
8660 two sˇ
8661 three sˇ
8662 additional edit
8663 "});
8664 handle_completion_request(
8665 &mut cx,
8666 indoc! {"
8667 one.second_completion
8668 two s
8669 three <s|>
8670 additional edit
8671 "},
8672 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
8673 counter.clone(),
8674 )
8675 .await;
8676 cx.condition(|editor, _| editor.context_menu_visible())
8677 .await;
8678 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
8679
8680 cx.simulate_keystroke("i");
8681
8682 handle_completion_request(
8683 &mut cx,
8684 indoc! {"
8685 one.second_completion
8686 two si
8687 three <si|>
8688 additional edit
8689 "},
8690 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
8691 counter.clone(),
8692 )
8693 .await;
8694 cx.condition(|editor, _| editor.context_menu_visible())
8695 .await;
8696 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
8697
8698 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8699 editor
8700 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8701 .unwrap()
8702 });
8703 cx.assert_editor_state(indoc! {"
8704 one.second_completion
8705 two sixth_completionˇ
8706 three sixth_completionˇ
8707 additional edit
8708 "});
8709
8710 apply_additional_edits.await.unwrap();
8711
8712 update_test_language_settings(&mut cx, |settings| {
8713 settings.defaults.show_completions_on_input = Some(false);
8714 });
8715 cx.set_state("editorˇ");
8716 cx.simulate_keystroke(".");
8717 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8718 cx.simulate_keystroke("c");
8719 cx.simulate_keystroke("l");
8720 cx.simulate_keystroke("o");
8721 cx.assert_editor_state("editor.cloˇ");
8722 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8723 cx.update_editor(|editor, window, cx| {
8724 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
8725 });
8726 handle_completion_request(
8727 &mut cx,
8728 "editor.<clo|>",
8729 vec!["close", "clobber"],
8730 counter.clone(),
8731 )
8732 .await;
8733 cx.condition(|editor, _| editor.context_menu_visible())
8734 .await;
8735 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
8736
8737 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8738 editor
8739 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8740 .unwrap()
8741 });
8742 cx.assert_editor_state("editor.closeˇ");
8743 handle_resolve_completion_request(&mut cx, None).await;
8744 apply_additional_edits.await.unwrap();
8745}
8746
8747#[gpui::test]
8748async fn test_multiline_completion(cx: &mut gpui::TestAppContext) {
8749 init_test(cx, |_| {});
8750
8751 let fs = FakeFs::new(cx.executor());
8752 fs.insert_tree(
8753 path!("/a"),
8754 json!({
8755 "main.ts": "a",
8756 }),
8757 )
8758 .await;
8759
8760 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8761 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8762 let typescript_language = Arc::new(Language::new(
8763 LanguageConfig {
8764 name: "TypeScript".into(),
8765 matcher: LanguageMatcher {
8766 path_suffixes: vec!["ts".to_string()],
8767 ..LanguageMatcher::default()
8768 },
8769 line_comments: vec!["// ".into()],
8770 ..LanguageConfig::default()
8771 },
8772 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8773 ));
8774 language_registry.add(typescript_language.clone());
8775 let mut fake_servers = language_registry.register_fake_lsp(
8776 "TypeScript",
8777 FakeLspAdapter {
8778 capabilities: lsp::ServerCapabilities {
8779 completion_provider: Some(lsp::CompletionOptions {
8780 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
8781 ..lsp::CompletionOptions::default()
8782 }),
8783 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
8784 ..lsp::ServerCapabilities::default()
8785 },
8786 // Emulate vtsls label generation
8787 label_for_completion: Some(Box::new(|item, _| {
8788 let text = if let Some(description) = item
8789 .label_details
8790 .as_ref()
8791 .and_then(|label_details| label_details.description.as_ref())
8792 {
8793 format!("{} {}", item.label, description)
8794 } else if let Some(detail) = &item.detail {
8795 format!("{} {}", item.label, detail)
8796 } else {
8797 item.label.clone()
8798 };
8799 let len = text.len();
8800 Some(language::CodeLabel {
8801 text,
8802 runs: Vec::new(),
8803 filter_range: 0..len,
8804 })
8805 })),
8806 ..FakeLspAdapter::default()
8807 },
8808 );
8809 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8810 let cx = &mut VisualTestContext::from_window(*workspace, cx);
8811 let worktree_id = workspace
8812 .update(cx, |workspace, _window, cx| {
8813 workspace.project().update(cx, |project, cx| {
8814 project.worktrees(cx).next().unwrap().read(cx).id()
8815 })
8816 })
8817 .unwrap();
8818 let _buffer = project
8819 .update(cx, |project, cx| {
8820 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
8821 })
8822 .await
8823 .unwrap();
8824 let editor = workspace
8825 .update(cx, |workspace, window, cx| {
8826 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
8827 })
8828 .unwrap()
8829 .await
8830 .unwrap()
8831 .downcast::<Editor>()
8832 .unwrap();
8833 let fake_server = fake_servers.next().await.unwrap();
8834
8835 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
8836 let multiline_label_2 = "a\nb\nc\n";
8837 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
8838 let multiline_description = "d\ne\nf\n";
8839 let multiline_detail_2 = "g\nh\ni\n";
8840
8841 let mut completion_handle =
8842 fake_server.handle_request::<lsp::request::Completion, _, _>(move |params, _| async move {
8843 Ok(Some(lsp::CompletionResponse::Array(vec![
8844 lsp::CompletionItem {
8845 label: multiline_label.to_string(),
8846 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8847 range: lsp::Range {
8848 start: lsp::Position {
8849 line: params.text_document_position.position.line,
8850 character: params.text_document_position.position.character,
8851 },
8852 end: lsp::Position {
8853 line: params.text_document_position.position.line,
8854 character: params.text_document_position.position.character,
8855 },
8856 },
8857 new_text: "new_text_1".to_string(),
8858 })),
8859 ..lsp::CompletionItem::default()
8860 },
8861 lsp::CompletionItem {
8862 label: "single line label 1".to_string(),
8863 detail: Some(multiline_detail.to_string()),
8864 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8865 range: lsp::Range {
8866 start: lsp::Position {
8867 line: params.text_document_position.position.line,
8868 character: params.text_document_position.position.character,
8869 },
8870 end: lsp::Position {
8871 line: params.text_document_position.position.line,
8872 character: params.text_document_position.position.character,
8873 },
8874 },
8875 new_text: "new_text_2".to_string(),
8876 })),
8877 ..lsp::CompletionItem::default()
8878 },
8879 lsp::CompletionItem {
8880 label: "single line label 2".to_string(),
8881 label_details: Some(lsp::CompletionItemLabelDetails {
8882 description: Some(multiline_description.to_string()),
8883 detail: None,
8884 }),
8885 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8886 range: lsp::Range {
8887 start: lsp::Position {
8888 line: params.text_document_position.position.line,
8889 character: params.text_document_position.position.character,
8890 },
8891 end: lsp::Position {
8892 line: params.text_document_position.position.line,
8893 character: params.text_document_position.position.character,
8894 },
8895 },
8896 new_text: "new_text_2".to_string(),
8897 })),
8898 ..lsp::CompletionItem::default()
8899 },
8900 lsp::CompletionItem {
8901 label: multiline_label_2.to_string(),
8902 detail: Some(multiline_detail_2.to_string()),
8903 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8904 range: lsp::Range {
8905 start: lsp::Position {
8906 line: params.text_document_position.position.line,
8907 character: params.text_document_position.position.character,
8908 },
8909 end: lsp::Position {
8910 line: params.text_document_position.position.line,
8911 character: params.text_document_position.position.character,
8912 },
8913 },
8914 new_text: "new_text_3".to_string(),
8915 })),
8916 ..lsp::CompletionItem::default()
8917 },
8918 lsp::CompletionItem {
8919 label: "Label with many spaces and \t but without newlines".to_string(),
8920 detail: Some(
8921 "Details with many spaces and \t but without newlines".to_string(),
8922 ),
8923 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8924 range: lsp::Range {
8925 start: lsp::Position {
8926 line: params.text_document_position.position.line,
8927 character: params.text_document_position.position.character,
8928 },
8929 end: lsp::Position {
8930 line: params.text_document_position.position.line,
8931 character: params.text_document_position.position.character,
8932 },
8933 },
8934 new_text: "new_text_4".to_string(),
8935 })),
8936 ..lsp::CompletionItem::default()
8937 },
8938 ])))
8939 });
8940
8941 editor.update_in(cx, |editor, window, cx| {
8942 cx.focus_self(window);
8943 editor.move_to_end(&MoveToEnd, window, cx);
8944 editor.handle_input(".", window, cx);
8945 });
8946 cx.run_until_parked();
8947 completion_handle.next().await.unwrap();
8948
8949 editor.update(cx, |editor, _| {
8950 assert!(editor.context_menu_visible());
8951 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
8952 {
8953 let completion_labels = menu
8954 .completions
8955 .borrow()
8956 .iter()
8957 .map(|c| c.label.text.clone())
8958 .collect::<Vec<_>>();
8959 assert_eq!(
8960 completion_labels,
8961 &[
8962 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
8963 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
8964 "single line label 2 d e f ",
8965 "a b c g h i ",
8966 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
8967 ],
8968 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
8969 );
8970
8971 for completion in menu
8972 .completions
8973 .borrow()
8974 .iter() {
8975 assert_eq!(
8976 completion.label.filter_range,
8977 0..completion.label.text.len(),
8978 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
8979 );
8980 }
8981
8982 } else {
8983 panic!("expected completion menu to be open");
8984 }
8985 });
8986}
8987
8988#[gpui::test]
8989async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
8990 init_test(cx, |_| {});
8991 let mut cx = EditorLspTestContext::new_rust(
8992 lsp::ServerCapabilities {
8993 completion_provider: Some(lsp::CompletionOptions {
8994 trigger_characters: Some(vec![".".to_string()]),
8995 ..Default::default()
8996 }),
8997 ..Default::default()
8998 },
8999 cx,
9000 )
9001 .await;
9002 cx.lsp
9003 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9004 Ok(Some(lsp::CompletionResponse::Array(vec![
9005 lsp::CompletionItem {
9006 label: "first".into(),
9007 ..Default::default()
9008 },
9009 lsp::CompletionItem {
9010 label: "last".into(),
9011 ..Default::default()
9012 },
9013 ])))
9014 });
9015 cx.set_state("variableˇ");
9016 cx.simulate_keystroke(".");
9017 cx.executor().run_until_parked();
9018
9019 cx.update_editor(|editor, _, _| {
9020 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9021 {
9022 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
9023 } else {
9024 panic!("expected completion menu to be open");
9025 }
9026 });
9027
9028 cx.update_editor(|editor, window, cx| {
9029 editor.move_page_down(&MovePageDown::default(), window, cx);
9030 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9031 {
9032 assert!(
9033 menu.selected_item == 1,
9034 "expected PageDown to select the last item from the context menu"
9035 );
9036 } else {
9037 panic!("expected completion menu to stay open after PageDown");
9038 }
9039 });
9040
9041 cx.update_editor(|editor, window, cx| {
9042 editor.move_page_up(&MovePageUp::default(), window, cx);
9043 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9044 {
9045 assert!(
9046 menu.selected_item == 0,
9047 "expected PageUp to select the first item from the context menu"
9048 );
9049 } else {
9050 panic!("expected completion menu to stay open after PageUp");
9051 }
9052 });
9053}
9054
9055#[gpui::test]
9056async fn test_completion_sort(cx: &mut gpui::TestAppContext) {
9057 init_test(cx, |_| {});
9058 let mut cx = EditorLspTestContext::new_rust(
9059 lsp::ServerCapabilities {
9060 completion_provider: Some(lsp::CompletionOptions {
9061 trigger_characters: Some(vec![".".to_string()]),
9062 ..Default::default()
9063 }),
9064 ..Default::default()
9065 },
9066 cx,
9067 )
9068 .await;
9069 cx.lsp
9070 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9071 Ok(Some(lsp::CompletionResponse::Array(vec![
9072 lsp::CompletionItem {
9073 label: "Range".into(),
9074 sort_text: Some("a".into()),
9075 ..Default::default()
9076 },
9077 lsp::CompletionItem {
9078 label: "r".into(),
9079 sort_text: Some("b".into()),
9080 ..Default::default()
9081 },
9082 lsp::CompletionItem {
9083 label: "ret".into(),
9084 sort_text: Some("c".into()),
9085 ..Default::default()
9086 },
9087 lsp::CompletionItem {
9088 label: "return".into(),
9089 sort_text: Some("d".into()),
9090 ..Default::default()
9091 },
9092 lsp::CompletionItem {
9093 label: "slice".into(),
9094 sort_text: Some("d".into()),
9095 ..Default::default()
9096 },
9097 ])))
9098 });
9099 cx.set_state("rˇ");
9100 cx.executor().run_until_parked();
9101 cx.update_editor(|editor, window, cx| {
9102 editor.show_completions(
9103 &ShowCompletions {
9104 trigger: Some("r".into()),
9105 },
9106 window,
9107 cx,
9108 );
9109 });
9110 cx.executor().run_until_parked();
9111
9112 cx.update_editor(|editor, _, _| {
9113 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9114 {
9115 assert_eq!(
9116 completion_menu_entries(&menu),
9117 &["r", "ret", "Range", "return"]
9118 );
9119 } else {
9120 panic!("expected completion menu to be open");
9121 }
9122 });
9123}
9124
9125#[gpui::test]
9126async fn test_no_duplicated_completion_requests(cx: &mut gpui::TestAppContext) {
9127 init_test(cx, |_| {});
9128
9129 let mut cx = EditorLspTestContext::new_rust(
9130 lsp::ServerCapabilities {
9131 completion_provider: Some(lsp::CompletionOptions {
9132 trigger_characters: Some(vec![".".to_string()]),
9133 resolve_provider: Some(true),
9134 ..Default::default()
9135 }),
9136 ..Default::default()
9137 },
9138 cx,
9139 )
9140 .await;
9141
9142 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
9143 cx.simulate_keystroke(".");
9144 let completion_item = lsp::CompletionItem {
9145 label: "Some".into(),
9146 kind: Some(lsp::CompletionItemKind::SNIPPET),
9147 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
9148 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
9149 kind: lsp::MarkupKind::Markdown,
9150 value: "```rust\nSome(2)\n```".to_string(),
9151 })),
9152 deprecated: Some(false),
9153 sort_text: Some("Some".to_string()),
9154 filter_text: Some("Some".to_string()),
9155 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
9156 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9157 range: lsp::Range {
9158 start: lsp::Position {
9159 line: 0,
9160 character: 22,
9161 },
9162 end: lsp::Position {
9163 line: 0,
9164 character: 22,
9165 },
9166 },
9167 new_text: "Some(2)".to_string(),
9168 })),
9169 additional_text_edits: Some(vec![lsp::TextEdit {
9170 range: lsp::Range {
9171 start: lsp::Position {
9172 line: 0,
9173 character: 20,
9174 },
9175 end: lsp::Position {
9176 line: 0,
9177 character: 22,
9178 },
9179 },
9180 new_text: "".to_string(),
9181 }]),
9182 ..Default::default()
9183 };
9184
9185 let closure_completion_item = completion_item.clone();
9186 let counter = Arc::new(AtomicUsize::new(0));
9187 let counter_clone = counter.clone();
9188 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
9189 let task_completion_item = closure_completion_item.clone();
9190 counter_clone.fetch_add(1, atomic::Ordering::Release);
9191 async move {
9192 Ok(Some(lsp::CompletionResponse::Array(vec![
9193 task_completion_item,
9194 ])))
9195 }
9196 });
9197
9198 cx.condition(|editor, _| editor.context_menu_visible())
9199 .await;
9200 cx.assert_editor_state(indoc! {"fn main() { let a = 2.ˇ; }"});
9201 assert!(request.next().await.is_some());
9202 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9203
9204 cx.simulate_keystroke("S");
9205 cx.simulate_keystroke("o");
9206 cx.simulate_keystroke("m");
9207 cx.condition(|editor, _| editor.context_menu_visible())
9208 .await;
9209 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Somˇ; }"});
9210 assert!(request.next().await.is_some());
9211 assert!(request.next().await.is_some());
9212 assert!(request.next().await.is_some());
9213 request.close();
9214 assert!(request.next().await.is_none());
9215 assert_eq!(
9216 counter.load(atomic::Ordering::Acquire),
9217 4,
9218 "With the completions menu open, only one LSP request should happen per input"
9219 );
9220}
9221
9222#[gpui::test]
9223async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
9224 init_test(cx, |_| {});
9225 let mut cx = EditorTestContext::new(cx).await;
9226 let language = Arc::new(Language::new(
9227 LanguageConfig {
9228 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
9229 ..Default::default()
9230 },
9231 Some(tree_sitter_rust::LANGUAGE.into()),
9232 ));
9233 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
9234
9235 // If multiple selections intersect a line, the line is only toggled once.
9236 cx.set_state(indoc! {"
9237 fn a() {
9238 «//b();
9239 ˇ»// «c();
9240 //ˇ» d();
9241 }
9242 "});
9243
9244 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9245
9246 cx.assert_editor_state(indoc! {"
9247 fn a() {
9248 «b();
9249 c();
9250 ˇ» d();
9251 }
9252 "});
9253
9254 // The comment prefix is inserted at the same column for every line in a
9255 // selection.
9256 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9257
9258 cx.assert_editor_state(indoc! {"
9259 fn a() {
9260 // «b();
9261 // c();
9262 ˇ»// d();
9263 }
9264 "});
9265
9266 // If a selection ends at the beginning of a line, that line is not toggled.
9267 cx.set_selections_state(indoc! {"
9268 fn a() {
9269 // b();
9270 «// c();
9271 ˇ» // d();
9272 }
9273 "});
9274
9275 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9276
9277 cx.assert_editor_state(indoc! {"
9278 fn a() {
9279 // b();
9280 «c();
9281 ˇ» // d();
9282 }
9283 "});
9284
9285 // If a selection span a single line and is empty, the line is toggled.
9286 cx.set_state(indoc! {"
9287 fn a() {
9288 a();
9289 b();
9290 ˇ
9291 }
9292 "});
9293
9294 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9295
9296 cx.assert_editor_state(indoc! {"
9297 fn a() {
9298 a();
9299 b();
9300 //•ˇ
9301 }
9302 "});
9303
9304 // If a selection span multiple lines, empty lines are not toggled.
9305 cx.set_state(indoc! {"
9306 fn a() {
9307 «a();
9308
9309 c();ˇ»
9310 }
9311 "});
9312
9313 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9314
9315 cx.assert_editor_state(indoc! {"
9316 fn a() {
9317 // «a();
9318
9319 // c();ˇ»
9320 }
9321 "});
9322
9323 // If a selection includes multiple comment prefixes, all lines are uncommented.
9324 cx.set_state(indoc! {"
9325 fn a() {
9326 «// a();
9327 /// b();
9328 //! c();ˇ»
9329 }
9330 "});
9331
9332 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9333
9334 cx.assert_editor_state(indoc! {"
9335 fn a() {
9336 «a();
9337 b();
9338 c();ˇ»
9339 }
9340 "});
9341}
9342
9343#[gpui::test]
9344async fn test_toggle_comment_ignore_indent(cx: &mut gpui::TestAppContext) {
9345 init_test(cx, |_| {});
9346 let mut cx = EditorTestContext::new(cx).await;
9347 let language = Arc::new(Language::new(
9348 LanguageConfig {
9349 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
9350 ..Default::default()
9351 },
9352 Some(tree_sitter_rust::LANGUAGE.into()),
9353 ));
9354 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
9355
9356 let toggle_comments = &ToggleComments {
9357 advance_downwards: false,
9358 ignore_indent: true,
9359 };
9360
9361 // If multiple selections intersect a line, the line is only toggled once.
9362 cx.set_state(indoc! {"
9363 fn a() {
9364 // «b();
9365 // c();
9366 // ˇ» d();
9367 }
9368 "});
9369
9370 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9371
9372 cx.assert_editor_state(indoc! {"
9373 fn a() {
9374 «b();
9375 c();
9376 ˇ» d();
9377 }
9378 "});
9379
9380 // The comment prefix is inserted at the beginning of each line
9381 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9382
9383 cx.assert_editor_state(indoc! {"
9384 fn a() {
9385 // «b();
9386 // c();
9387 // ˇ» d();
9388 }
9389 "});
9390
9391 // If a selection ends at the beginning of a line, that line is not toggled.
9392 cx.set_selections_state(indoc! {"
9393 fn a() {
9394 // b();
9395 // «c();
9396 ˇ»// d();
9397 }
9398 "});
9399
9400 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9401
9402 cx.assert_editor_state(indoc! {"
9403 fn a() {
9404 // b();
9405 «c();
9406 ˇ»// d();
9407 }
9408 "});
9409
9410 // If a selection span a single line and is empty, the line is toggled.
9411 cx.set_state(indoc! {"
9412 fn a() {
9413 a();
9414 b();
9415 ˇ
9416 }
9417 "});
9418
9419 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9420
9421 cx.assert_editor_state(indoc! {"
9422 fn a() {
9423 a();
9424 b();
9425 //ˇ
9426 }
9427 "});
9428
9429 // If a selection span multiple lines, empty lines are not toggled.
9430 cx.set_state(indoc! {"
9431 fn a() {
9432 «a();
9433
9434 c();ˇ»
9435 }
9436 "});
9437
9438 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9439
9440 cx.assert_editor_state(indoc! {"
9441 fn a() {
9442 // «a();
9443
9444 // c();ˇ»
9445 }
9446 "});
9447
9448 // If a selection includes multiple comment prefixes, all lines are uncommented.
9449 cx.set_state(indoc! {"
9450 fn a() {
9451 // «a();
9452 /// b();
9453 //! c();ˇ»
9454 }
9455 "});
9456
9457 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9458
9459 cx.assert_editor_state(indoc! {"
9460 fn a() {
9461 «a();
9462 b();
9463 c();ˇ»
9464 }
9465 "});
9466}
9467
9468#[gpui::test]
9469async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
9470 init_test(cx, |_| {});
9471
9472 let language = Arc::new(Language::new(
9473 LanguageConfig {
9474 line_comments: vec!["// ".into()],
9475 ..Default::default()
9476 },
9477 Some(tree_sitter_rust::LANGUAGE.into()),
9478 ));
9479
9480 let mut cx = EditorTestContext::new(cx).await;
9481
9482 cx.language_registry().add(language.clone());
9483 cx.update_buffer(|buffer, cx| {
9484 buffer.set_language(Some(language), cx);
9485 });
9486
9487 let toggle_comments = &ToggleComments {
9488 advance_downwards: true,
9489 ignore_indent: false,
9490 };
9491
9492 // Single cursor on one line -> advance
9493 // Cursor moves horizontally 3 characters as well on non-blank line
9494 cx.set_state(indoc!(
9495 "fn a() {
9496 ˇdog();
9497 cat();
9498 }"
9499 ));
9500 cx.update_editor(|editor, window, cx| {
9501 editor.toggle_comments(toggle_comments, window, cx);
9502 });
9503 cx.assert_editor_state(indoc!(
9504 "fn a() {
9505 // dog();
9506 catˇ();
9507 }"
9508 ));
9509
9510 // Single selection on one line -> don't advance
9511 cx.set_state(indoc!(
9512 "fn a() {
9513 «dog()ˇ»;
9514 cat();
9515 }"
9516 ));
9517 cx.update_editor(|editor, window, cx| {
9518 editor.toggle_comments(toggle_comments, window, cx);
9519 });
9520 cx.assert_editor_state(indoc!(
9521 "fn a() {
9522 // «dog()ˇ»;
9523 cat();
9524 }"
9525 ));
9526
9527 // Multiple cursors on one line -> advance
9528 cx.set_state(indoc!(
9529 "fn a() {
9530 ˇdˇog();
9531 cat();
9532 }"
9533 ));
9534 cx.update_editor(|editor, window, cx| {
9535 editor.toggle_comments(toggle_comments, window, cx);
9536 });
9537 cx.assert_editor_state(indoc!(
9538 "fn a() {
9539 // dog();
9540 catˇ(ˇ);
9541 }"
9542 ));
9543
9544 // Multiple cursors on one line, with selection -> don't advance
9545 cx.set_state(indoc!(
9546 "fn a() {
9547 ˇdˇog«()ˇ»;
9548 cat();
9549 }"
9550 ));
9551 cx.update_editor(|editor, window, cx| {
9552 editor.toggle_comments(toggle_comments, window, cx);
9553 });
9554 cx.assert_editor_state(indoc!(
9555 "fn a() {
9556 // ˇdˇog«()ˇ»;
9557 cat();
9558 }"
9559 ));
9560
9561 // Single cursor on one line -> advance
9562 // Cursor moves to column 0 on blank line
9563 cx.set_state(indoc!(
9564 "fn a() {
9565 ˇdog();
9566
9567 cat();
9568 }"
9569 ));
9570 cx.update_editor(|editor, window, cx| {
9571 editor.toggle_comments(toggle_comments, window, cx);
9572 });
9573 cx.assert_editor_state(indoc!(
9574 "fn a() {
9575 // dog();
9576 ˇ
9577 cat();
9578 }"
9579 ));
9580
9581 // Single cursor on one line -> advance
9582 // Cursor starts and ends at column 0
9583 cx.set_state(indoc!(
9584 "fn a() {
9585 ˇ dog();
9586 cat();
9587 }"
9588 ));
9589 cx.update_editor(|editor, window, cx| {
9590 editor.toggle_comments(toggle_comments, window, cx);
9591 });
9592 cx.assert_editor_state(indoc!(
9593 "fn a() {
9594 // dog();
9595 ˇ cat();
9596 }"
9597 ));
9598}
9599
9600#[gpui::test]
9601async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
9602 init_test(cx, |_| {});
9603
9604 let mut cx = EditorTestContext::new(cx).await;
9605
9606 let html_language = Arc::new(
9607 Language::new(
9608 LanguageConfig {
9609 name: "HTML".into(),
9610 block_comment: Some(("<!-- ".into(), " -->".into())),
9611 ..Default::default()
9612 },
9613 Some(tree_sitter_html::language()),
9614 )
9615 .with_injection_query(
9616 r#"
9617 (script_element
9618 (raw_text) @injection.content
9619 (#set! injection.language "javascript"))
9620 "#,
9621 )
9622 .unwrap(),
9623 );
9624
9625 let javascript_language = Arc::new(Language::new(
9626 LanguageConfig {
9627 name: "JavaScript".into(),
9628 line_comments: vec!["// ".into()],
9629 ..Default::default()
9630 },
9631 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9632 ));
9633
9634 cx.language_registry().add(html_language.clone());
9635 cx.language_registry().add(javascript_language.clone());
9636 cx.update_buffer(|buffer, cx| {
9637 buffer.set_language(Some(html_language), cx);
9638 });
9639
9640 // Toggle comments for empty selections
9641 cx.set_state(
9642 &r#"
9643 <p>A</p>ˇ
9644 <p>B</p>ˇ
9645 <p>C</p>ˇ
9646 "#
9647 .unindent(),
9648 );
9649 cx.update_editor(|editor, window, cx| {
9650 editor.toggle_comments(&ToggleComments::default(), window, cx)
9651 });
9652 cx.assert_editor_state(
9653 &r#"
9654 <!-- <p>A</p>ˇ -->
9655 <!-- <p>B</p>ˇ -->
9656 <!-- <p>C</p>ˇ -->
9657 "#
9658 .unindent(),
9659 );
9660 cx.update_editor(|editor, window, cx| {
9661 editor.toggle_comments(&ToggleComments::default(), window, cx)
9662 });
9663 cx.assert_editor_state(
9664 &r#"
9665 <p>A</p>ˇ
9666 <p>B</p>ˇ
9667 <p>C</p>ˇ
9668 "#
9669 .unindent(),
9670 );
9671
9672 // Toggle comments for mixture of empty and non-empty selections, where
9673 // multiple selections occupy a given line.
9674 cx.set_state(
9675 &r#"
9676 <p>A«</p>
9677 <p>ˇ»B</p>ˇ
9678 <p>C«</p>
9679 <p>ˇ»D</p>ˇ
9680 "#
9681 .unindent(),
9682 );
9683
9684 cx.update_editor(|editor, window, cx| {
9685 editor.toggle_comments(&ToggleComments::default(), window, cx)
9686 });
9687 cx.assert_editor_state(
9688 &r#"
9689 <!-- <p>A«</p>
9690 <p>ˇ»B</p>ˇ -->
9691 <!-- <p>C«</p>
9692 <p>ˇ»D</p>ˇ -->
9693 "#
9694 .unindent(),
9695 );
9696 cx.update_editor(|editor, window, cx| {
9697 editor.toggle_comments(&ToggleComments::default(), window, cx)
9698 });
9699 cx.assert_editor_state(
9700 &r#"
9701 <p>A«</p>
9702 <p>ˇ»B</p>ˇ
9703 <p>C«</p>
9704 <p>ˇ»D</p>ˇ
9705 "#
9706 .unindent(),
9707 );
9708
9709 // Toggle comments when different languages are active for different
9710 // selections.
9711 cx.set_state(
9712 &r#"
9713 ˇ<script>
9714 ˇvar x = new Y();
9715 ˇ</script>
9716 "#
9717 .unindent(),
9718 );
9719 cx.executor().run_until_parked();
9720 cx.update_editor(|editor, window, cx| {
9721 editor.toggle_comments(&ToggleComments::default(), window, cx)
9722 });
9723 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
9724 // Uncommenting and commenting from this position brings in even more wrong artifacts.
9725 cx.assert_editor_state(
9726 &r#"
9727 <!-- ˇ<script> -->
9728 // ˇvar x = new Y();
9729 <!-- ˇ</script> -->
9730 "#
9731 .unindent(),
9732 );
9733}
9734
9735#[gpui::test]
9736fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
9737 init_test(cx, |_| {});
9738
9739 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9740 let multibuffer = cx.new(|cx| {
9741 let mut multibuffer = MultiBuffer::new(ReadWrite);
9742 multibuffer.push_excerpts(
9743 buffer.clone(),
9744 [
9745 ExcerptRange {
9746 context: Point::new(0, 0)..Point::new(0, 4),
9747 primary: None,
9748 },
9749 ExcerptRange {
9750 context: Point::new(1, 0)..Point::new(1, 4),
9751 primary: None,
9752 },
9753 ],
9754 cx,
9755 );
9756 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
9757 multibuffer
9758 });
9759
9760 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
9761 editor.update_in(cx, |editor, window, cx| {
9762 assert_eq!(editor.text(cx), "aaaa\nbbbb");
9763 editor.change_selections(None, window, cx, |s| {
9764 s.select_ranges([
9765 Point::new(0, 0)..Point::new(0, 0),
9766 Point::new(1, 0)..Point::new(1, 0),
9767 ])
9768 });
9769
9770 editor.handle_input("X", window, cx);
9771 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
9772 assert_eq!(
9773 editor.selections.ranges(cx),
9774 [
9775 Point::new(0, 1)..Point::new(0, 1),
9776 Point::new(1, 1)..Point::new(1, 1),
9777 ]
9778 );
9779
9780 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
9781 editor.change_selections(None, window, cx, |s| {
9782 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
9783 });
9784 editor.backspace(&Default::default(), window, cx);
9785 assert_eq!(editor.text(cx), "Xa\nbbb");
9786 assert_eq!(
9787 editor.selections.ranges(cx),
9788 [Point::new(1, 0)..Point::new(1, 0)]
9789 );
9790
9791 editor.change_selections(None, window, cx, |s| {
9792 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
9793 });
9794 editor.backspace(&Default::default(), window, cx);
9795 assert_eq!(editor.text(cx), "X\nbb");
9796 assert_eq!(
9797 editor.selections.ranges(cx),
9798 [Point::new(0, 1)..Point::new(0, 1)]
9799 );
9800 });
9801}
9802
9803#[gpui::test]
9804fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
9805 init_test(cx, |_| {});
9806
9807 let markers = vec![('[', ']').into(), ('(', ')').into()];
9808 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
9809 indoc! {"
9810 [aaaa
9811 (bbbb]
9812 cccc)",
9813 },
9814 markers.clone(),
9815 );
9816 let excerpt_ranges = markers.into_iter().map(|marker| {
9817 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
9818 ExcerptRange {
9819 context,
9820 primary: None,
9821 }
9822 });
9823 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
9824 let multibuffer = cx.new(|cx| {
9825 let mut multibuffer = MultiBuffer::new(ReadWrite);
9826 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
9827 multibuffer
9828 });
9829
9830 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
9831 editor.update_in(cx, |editor, window, cx| {
9832 let (expected_text, selection_ranges) = marked_text_ranges(
9833 indoc! {"
9834 aaaa
9835 bˇbbb
9836 bˇbbˇb
9837 cccc"
9838 },
9839 true,
9840 );
9841 assert_eq!(editor.text(cx), expected_text);
9842 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
9843
9844 editor.handle_input("X", window, cx);
9845
9846 let (expected_text, expected_selections) = marked_text_ranges(
9847 indoc! {"
9848 aaaa
9849 bXˇbbXb
9850 bXˇbbXˇb
9851 cccc"
9852 },
9853 false,
9854 );
9855 assert_eq!(editor.text(cx), expected_text);
9856 assert_eq!(editor.selections.ranges(cx), expected_selections);
9857
9858 editor.newline(&Newline, window, cx);
9859 let (expected_text, expected_selections) = marked_text_ranges(
9860 indoc! {"
9861 aaaa
9862 bX
9863 ˇbbX
9864 b
9865 bX
9866 ˇbbX
9867 ˇb
9868 cccc"
9869 },
9870 false,
9871 );
9872 assert_eq!(editor.text(cx), expected_text);
9873 assert_eq!(editor.selections.ranges(cx), expected_selections);
9874 });
9875}
9876
9877#[gpui::test]
9878fn test_refresh_selections(cx: &mut TestAppContext) {
9879 init_test(cx, |_| {});
9880
9881 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9882 let mut excerpt1_id = None;
9883 let multibuffer = cx.new(|cx| {
9884 let mut multibuffer = MultiBuffer::new(ReadWrite);
9885 excerpt1_id = multibuffer
9886 .push_excerpts(
9887 buffer.clone(),
9888 [
9889 ExcerptRange {
9890 context: Point::new(0, 0)..Point::new(1, 4),
9891 primary: None,
9892 },
9893 ExcerptRange {
9894 context: Point::new(1, 0)..Point::new(2, 4),
9895 primary: None,
9896 },
9897 ],
9898 cx,
9899 )
9900 .into_iter()
9901 .next();
9902 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
9903 multibuffer
9904 });
9905
9906 let editor = cx.add_window(|window, cx| {
9907 let mut editor = build_editor(multibuffer.clone(), window, cx);
9908 let snapshot = editor.snapshot(window, cx);
9909 editor.change_selections(None, window, cx, |s| {
9910 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
9911 });
9912 editor.begin_selection(
9913 Point::new(2, 1).to_display_point(&snapshot),
9914 true,
9915 1,
9916 window,
9917 cx,
9918 );
9919 assert_eq!(
9920 editor.selections.ranges(cx),
9921 [
9922 Point::new(1, 3)..Point::new(1, 3),
9923 Point::new(2, 1)..Point::new(2, 1),
9924 ]
9925 );
9926 editor
9927 });
9928
9929 // Refreshing selections is a no-op when excerpts haven't changed.
9930 _ = editor.update(cx, |editor, window, cx| {
9931 editor.change_selections(None, window, cx, |s| s.refresh());
9932 assert_eq!(
9933 editor.selections.ranges(cx),
9934 [
9935 Point::new(1, 3)..Point::new(1, 3),
9936 Point::new(2, 1)..Point::new(2, 1),
9937 ]
9938 );
9939 });
9940
9941 multibuffer.update(cx, |multibuffer, cx| {
9942 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
9943 });
9944 _ = editor.update(cx, |editor, window, cx| {
9945 // Removing an excerpt causes the first selection to become degenerate.
9946 assert_eq!(
9947 editor.selections.ranges(cx),
9948 [
9949 Point::new(0, 0)..Point::new(0, 0),
9950 Point::new(0, 1)..Point::new(0, 1)
9951 ]
9952 );
9953
9954 // Refreshing selections will relocate the first selection to the original buffer
9955 // location.
9956 editor.change_selections(None, window, cx, |s| s.refresh());
9957 assert_eq!(
9958 editor.selections.ranges(cx),
9959 [
9960 Point::new(0, 1)..Point::new(0, 1),
9961 Point::new(0, 3)..Point::new(0, 3)
9962 ]
9963 );
9964 assert!(editor.selections.pending_anchor().is_some());
9965 });
9966}
9967
9968#[gpui::test]
9969fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
9970 init_test(cx, |_| {});
9971
9972 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9973 let mut excerpt1_id = None;
9974 let multibuffer = cx.new(|cx| {
9975 let mut multibuffer = MultiBuffer::new(ReadWrite);
9976 excerpt1_id = multibuffer
9977 .push_excerpts(
9978 buffer.clone(),
9979 [
9980 ExcerptRange {
9981 context: Point::new(0, 0)..Point::new(1, 4),
9982 primary: None,
9983 },
9984 ExcerptRange {
9985 context: Point::new(1, 0)..Point::new(2, 4),
9986 primary: None,
9987 },
9988 ],
9989 cx,
9990 )
9991 .into_iter()
9992 .next();
9993 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
9994 multibuffer
9995 });
9996
9997 let editor = cx.add_window(|window, cx| {
9998 let mut editor = build_editor(multibuffer.clone(), window, cx);
9999 let snapshot = editor.snapshot(window, cx);
10000 editor.begin_selection(
10001 Point::new(1, 3).to_display_point(&snapshot),
10002 false,
10003 1,
10004 window,
10005 cx,
10006 );
10007 assert_eq!(
10008 editor.selections.ranges(cx),
10009 [Point::new(1, 3)..Point::new(1, 3)]
10010 );
10011 editor
10012 });
10013
10014 multibuffer.update(cx, |multibuffer, cx| {
10015 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
10016 });
10017 _ = editor.update(cx, |editor, window, cx| {
10018 assert_eq!(
10019 editor.selections.ranges(cx),
10020 [Point::new(0, 0)..Point::new(0, 0)]
10021 );
10022
10023 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
10024 editor.change_selections(None, window, cx, |s| s.refresh());
10025 assert_eq!(
10026 editor.selections.ranges(cx),
10027 [Point::new(0, 3)..Point::new(0, 3)]
10028 );
10029 assert!(editor.selections.pending_anchor().is_some());
10030 });
10031}
10032
10033#[gpui::test]
10034async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
10035 init_test(cx, |_| {});
10036
10037 let language = Arc::new(
10038 Language::new(
10039 LanguageConfig {
10040 brackets: BracketPairConfig {
10041 pairs: vec![
10042 BracketPair {
10043 start: "{".to_string(),
10044 end: "}".to_string(),
10045 close: true,
10046 surround: true,
10047 newline: true,
10048 },
10049 BracketPair {
10050 start: "/* ".to_string(),
10051 end: " */".to_string(),
10052 close: true,
10053 surround: true,
10054 newline: true,
10055 },
10056 ],
10057 ..Default::default()
10058 },
10059 ..Default::default()
10060 },
10061 Some(tree_sitter_rust::LANGUAGE.into()),
10062 )
10063 .with_indents_query("")
10064 .unwrap(),
10065 );
10066
10067 let text = concat!(
10068 "{ }\n", //
10069 " x\n", //
10070 " /* */\n", //
10071 "x\n", //
10072 "{{} }\n", //
10073 );
10074
10075 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10076 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10077 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10078 editor
10079 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10080 .await;
10081
10082 editor.update_in(cx, |editor, window, cx| {
10083 editor.change_selections(None, window, cx, |s| {
10084 s.select_display_ranges([
10085 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
10086 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
10087 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
10088 ])
10089 });
10090 editor.newline(&Newline, window, cx);
10091
10092 assert_eq!(
10093 editor.buffer().read(cx).read(cx).text(),
10094 concat!(
10095 "{ \n", // Suppress rustfmt
10096 "\n", //
10097 "}\n", //
10098 " x\n", //
10099 " /* \n", //
10100 " \n", //
10101 " */\n", //
10102 "x\n", //
10103 "{{} \n", //
10104 "}\n", //
10105 )
10106 );
10107 });
10108}
10109
10110#[gpui::test]
10111fn test_highlighted_ranges(cx: &mut TestAppContext) {
10112 init_test(cx, |_| {});
10113
10114 let editor = cx.add_window(|window, cx| {
10115 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
10116 build_editor(buffer.clone(), window, cx)
10117 });
10118
10119 _ = editor.update(cx, |editor, window, cx| {
10120 struct Type1;
10121 struct Type2;
10122
10123 let buffer = editor.buffer.read(cx).snapshot(cx);
10124
10125 let anchor_range =
10126 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
10127
10128 editor.highlight_background::<Type1>(
10129 &[
10130 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
10131 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
10132 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
10133 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
10134 ],
10135 |_| Hsla::red(),
10136 cx,
10137 );
10138 editor.highlight_background::<Type2>(
10139 &[
10140 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
10141 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
10142 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
10143 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
10144 ],
10145 |_| Hsla::green(),
10146 cx,
10147 );
10148
10149 let snapshot = editor.snapshot(window, cx);
10150 let mut highlighted_ranges = editor.background_highlights_in_range(
10151 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
10152 &snapshot,
10153 cx.theme().colors(),
10154 );
10155 // Enforce a consistent ordering based on color without relying on the ordering of the
10156 // highlight's `TypeId` which is non-executor.
10157 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
10158 assert_eq!(
10159 highlighted_ranges,
10160 &[
10161 (
10162 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
10163 Hsla::red(),
10164 ),
10165 (
10166 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
10167 Hsla::red(),
10168 ),
10169 (
10170 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
10171 Hsla::green(),
10172 ),
10173 (
10174 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
10175 Hsla::green(),
10176 ),
10177 ]
10178 );
10179 assert_eq!(
10180 editor.background_highlights_in_range(
10181 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
10182 &snapshot,
10183 cx.theme().colors(),
10184 ),
10185 &[(
10186 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
10187 Hsla::red(),
10188 )]
10189 );
10190 });
10191}
10192
10193#[gpui::test]
10194async fn test_following(cx: &mut gpui::TestAppContext) {
10195 init_test(cx, |_| {});
10196
10197 let fs = FakeFs::new(cx.executor());
10198 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
10199
10200 let buffer = project.update(cx, |project, cx| {
10201 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
10202 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
10203 });
10204 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
10205 let follower = cx.update(|cx| {
10206 cx.open_window(
10207 WindowOptions {
10208 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
10209 gpui::Point::new(px(0.), px(0.)),
10210 gpui::Point::new(px(10.), px(80.)),
10211 ))),
10212 ..Default::default()
10213 },
10214 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
10215 )
10216 .unwrap()
10217 });
10218
10219 let is_still_following = Rc::new(RefCell::new(true));
10220 let follower_edit_event_count = Rc::new(RefCell::new(0));
10221 let pending_update = Rc::new(RefCell::new(None));
10222 let leader_entity = leader.root(cx).unwrap();
10223 let follower_entity = follower.root(cx).unwrap();
10224 _ = follower.update(cx, {
10225 let update = pending_update.clone();
10226 let is_still_following = is_still_following.clone();
10227 let follower_edit_event_count = follower_edit_event_count.clone();
10228 |_, window, cx| {
10229 cx.subscribe_in(
10230 &leader_entity,
10231 window,
10232 move |_, leader, event, window, cx| {
10233 leader.read(cx).add_event_to_update_proto(
10234 event,
10235 &mut update.borrow_mut(),
10236 window,
10237 cx,
10238 );
10239 },
10240 )
10241 .detach();
10242
10243 cx.subscribe_in(
10244 &follower_entity,
10245 window,
10246 move |_, _, event: &EditorEvent, _window, _cx| {
10247 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
10248 *is_still_following.borrow_mut() = false;
10249 }
10250
10251 if let EditorEvent::BufferEdited = event {
10252 *follower_edit_event_count.borrow_mut() += 1;
10253 }
10254 },
10255 )
10256 .detach();
10257 }
10258 });
10259
10260 // Update the selections only
10261 _ = leader.update(cx, |leader, window, cx| {
10262 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
10263 });
10264 follower
10265 .update(cx, |follower, window, cx| {
10266 follower.apply_update_proto(
10267 &project,
10268 pending_update.borrow_mut().take().unwrap(),
10269 window,
10270 cx,
10271 )
10272 })
10273 .unwrap()
10274 .await
10275 .unwrap();
10276 _ = follower.update(cx, |follower, _, cx| {
10277 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
10278 });
10279 assert!(*is_still_following.borrow());
10280 assert_eq!(*follower_edit_event_count.borrow(), 0);
10281
10282 // Update the scroll position only
10283 _ = leader.update(cx, |leader, window, cx| {
10284 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
10285 });
10286 follower
10287 .update(cx, |follower, window, cx| {
10288 follower.apply_update_proto(
10289 &project,
10290 pending_update.borrow_mut().take().unwrap(),
10291 window,
10292 cx,
10293 )
10294 })
10295 .unwrap()
10296 .await
10297 .unwrap();
10298 assert_eq!(
10299 follower
10300 .update(cx, |follower, _, cx| follower.scroll_position(cx))
10301 .unwrap(),
10302 gpui::Point::new(1.5, 3.5)
10303 );
10304 assert!(*is_still_following.borrow());
10305 assert_eq!(*follower_edit_event_count.borrow(), 0);
10306
10307 // Update the selections and scroll position. The follower's scroll position is updated
10308 // via autoscroll, not via the leader's exact scroll position.
10309 _ = leader.update(cx, |leader, window, cx| {
10310 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10311 leader.request_autoscroll(Autoscroll::newest(), cx);
10312 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
10313 });
10314 follower
10315 .update(cx, |follower, window, cx| {
10316 follower.apply_update_proto(
10317 &project,
10318 pending_update.borrow_mut().take().unwrap(),
10319 window,
10320 cx,
10321 )
10322 })
10323 .unwrap()
10324 .await
10325 .unwrap();
10326 _ = follower.update(cx, |follower, _, cx| {
10327 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
10328 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
10329 });
10330 assert!(*is_still_following.borrow());
10331
10332 // Creating a pending selection that precedes another selection
10333 _ = leader.update(cx, |leader, window, cx| {
10334 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
10335 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
10336 });
10337 follower
10338 .update(cx, |follower, window, cx| {
10339 follower.apply_update_proto(
10340 &project,
10341 pending_update.borrow_mut().take().unwrap(),
10342 window,
10343 cx,
10344 )
10345 })
10346 .unwrap()
10347 .await
10348 .unwrap();
10349 _ = follower.update(cx, |follower, _, cx| {
10350 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
10351 });
10352 assert!(*is_still_following.borrow());
10353
10354 // Extend the pending selection so that it surrounds another selection
10355 _ = leader.update(cx, |leader, window, cx| {
10356 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
10357 });
10358 follower
10359 .update(cx, |follower, window, cx| {
10360 follower.apply_update_proto(
10361 &project,
10362 pending_update.borrow_mut().take().unwrap(),
10363 window,
10364 cx,
10365 )
10366 })
10367 .unwrap()
10368 .await
10369 .unwrap();
10370 _ = follower.update(cx, |follower, _, cx| {
10371 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
10372 });
10373
10374 // Scrolling locally breaks the follow
10375 _ = follower.update(cx, |follower, window, cx| {
10376 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
10377 follower.set_scroll_anchor(
10378 ScrollAnchor {
10379 anchor: top_anchor,
10380 offset: gpui::Point::new(0.0, 0.5),
10381 },
10382 window,
10383 cx,
10384 );
10385 });
10386 assert!(!(*is_still_following.borrow()));
10387}
10388
10389#[gpui::test]
10390async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
10391 init_test(cx, |_| {});
10392
10393 let fs = FakeFs::new(cx.executor());
10394 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
10395 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10396 let pane = workspace
10397 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10398 .unwrap();
10399
10400 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10401
10402 let leader = pane.update_in(cx, |_, window, cx| {
10403 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
10404 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
10405 });
10406
10407 // Start following the editor when it has no excerpts.
10408 let mut state_message =
10409 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
10410 let workspace_entity = workspace.root(cx).unwrap();
10411 let follower_1 = cx
10412 .update_window(*workspace.deref(), |_, window, cx| {
10413 Editor::from_state_proto(
10414 workspace_entity,
10415 ViewId {
10416 creator: Default::default(),
10417 id: 0,
10418 },
10419 &mut state_message,
10420 window,
10421 cx,
10422 )
10423 })
10424 .unwrap()
10425 .unwrap()
10426 .await
10427 .unwrap();
10428
10429 let update_message = Rc::new(RefCell::new(None));
10430 follower_1.update_in(cx, {
10431 let update = update_message.clone();
10432 |_, window, cx| {
10433 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
10434 leader.read(cx).add_event_to_update_proto(
10435 event,
10436 &mut update.borrow_mut(),
10437 window,
10438 cx,
10439 );
10440 })
10441 .detach();
10442 }
10443 });
10444
10445 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
10446 (
10447 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
10448 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
10449 )
10450 });
10451
10452 // Insert some excerpts.
10453 leader.update(cx, |leader, cx| {
10454 leader.buffer.update(cx, |multibuffer, cx| {
10455 let excerpt_ids = multibuffer.push_excerpts(
10456 buffer_1.clone(),
10457 [
10458 ExcerptRange {
10459 context: 1..6,
10460 primary: None,
10461 },
10462 ExcerptRange {
10463 context: 12..15,
10464 primary: None,
10465 },
10466 ExcerptRange {
10467 context: 0..3,
10468 primary: None,
10469 },
10470 ],
10471 cx,
10472 );
10473 multibuffer.insert_excerpts_after(
10474 excerpt_ids[0],
10475 buffer_2.clone(),
10476 [
10477 ExcerptRange {
10478 context: 8..12,
10479 primary: None,
10480 },
10481 ExcerptRange {
10482 context: 0..6,
10483 primary: None,
10484 },
10485 ],
10486 cx,
10487 );
10488 });
10489 });
10490
10491 // Apply the update of adding the excerpts.
10492 follower_1
10493 .update_in(cx, |follower, window, cx| {
10494 follower.apply_update_proto(
10495 &project,
10496 update_message.borrow().clone().unwrap(),
10497 window,
10498 cx,
10499 )
10500 })
10501 .await
10502 .unwrap();
10503 assert_eq!(
10504 follower_1.update(cx, |editor, cx| editor.text(cx)),
10505 leader.update(cx, |editor, cx| editor.text(cx))
10506 );
10507 update_message.borrow_mut().take();
10508
10509 // Start following separately after it already has excerpts.
10510 let mut state_message =
10511 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
10512 let workspace_entity = workspace.root(cx).unwrap();
10513 let follower_2 = cx
10514 .update_window(*workspace.deref(), |_, window, cx| {
10515 Editor::from_state_proto(
10516 workspace_entity,
10517 ViewId {
10518 creator: Default::default(),
10519 id: 0,
10520 },
10521 &mut state_message,
10522 window,
10523 cx,
10524 )
10525 })
10526 .unwrap()
10527 .unwrap()
10528 .await
10529 .unwrap();
10530 assert_eq!(
10531 follower_2.update(cx, |editor, cx| editor.text(cx)),
10532 leader.update(cx, |editor, cx| editor.text(cx))
10533 );
10534
10535 // Remove some excerpts.
10536 leader.update(cx, |leader, cx| {
10537 leader.buffer.update(cx, |multibuffer, cx| {
10538 let excerpt_ids = multibuffer.excerpt_ids();
10539 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
10540 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
10541 });
10542 });
10543
10544 // Apply the update of removing the excerpts.
10545 follower_1
10546 .update_in(cx, |follower, window, cx| {
10547 follower.apply_update_proto(
10548 &project,
10549 update_message.borrow().clone().unwrap(),
10550 window,
10551 cx,
10552 )
10553 })
10554 .await
10555 .unwrap();
10556 follower_2
10557 .update_in(cx, |follower, window, cx| {
10558 follower.apply_update_proto(
10559 &project,
10560 update_message.borrow().clone().unwrap(),
10561 window,
10562 cx,
10563 )
10564 })
10565 .await
10566 .unwrap();
10567 update_message.borrow_mut().take();
10568 assert_eq!(
10569 follower_1.update(cx, |editor, cx| editor.text(cx)),
10570 leader.update(cx, |editor, cx| editor.text(cx))
10571 );
10572}
10573
10574#[gpui::test]
10575async fn go_to_prev_overlapping_diagnostic(
10576 executor: BackgroundExecutor,
10577 cx: &mut gpui::TestAppContext,
10578) {
10579 init_test(cx, |_| {});
10580
10581 let mut cx = EditorTestContext::new(cx).await;
10582 let lsp_store =
10583 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10584
10585 cx.set_state(indoc! {"
10586 ˇfn func(abc def: i32) -> u32 {
10587 }
10588 "});
10589
10590 cx.update(|_, cx| {
10591 lsp_store.update(cx, |lsp_store, cx| {
10592 lsp_store
10593 .update_diagnostics(
10594 LanguageServerId(0),
10595 lsp::PublishDiagnosticsParams {
10596 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
10597 version: None,
10598 diagnostics: vec![
10599 lsp::Diagnostic {
10600 range: lsp::Range::new(
10601 lsp::Position::new(0, 11),
10602 lsp::Position::new(0, 12),
10603 ),
10604 severity: Some(lsp::DiagnosticSeverity::ERROR),
10605 ..Default::default()
10606 },
10607 lsp::Diagnostic {
10608 range: lsp::Range::new(
10609 lsp::Position::new(0, 12),
10610 lsp::Position::new(0, 15),
10611 ),
10612 severity: Some(lsp::DiagnosticSeverity::ERROR),
10613 ..Default::default()
10614 },
10615 lsp::Diagnostic {
10616 range: lsp::Range::new(
10617 lsp::Position::new(0, 25),
10618 lsp::Position::new(0, 28),
10619 ),
10620 severity: Some(lsp::DiagnosticSeverity::ERROR),
10621 ..Default::default()
10622 },
10623 ],
10624 },
10625 &[],
10626 cx,
10627 )
10628 .unwrap()
10629 });
10630 });
10631
10632 executor.run_until_parked();
10633
10634 cx.update_editor(|editor, window, cx| {
10635 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10636 });
10637
10638 cx.assert_editor_state(indoc! {"
10639 fn func(abc def: i32) -> ˇu32 {
10640 }
10641 "});
10642
10643 cx.update_editor(|editor, window, cx| {
10644 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10645 });
10646
10647 cx.assert_editor_state(indoc! {"
10648 fn func(abc ˇdef: i32) -> u32 {
10649 }
10650 "});
10651
10652 cx.update_editor(|editor, window, cx| {
10653 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10654 });
10655
10656 cx.assert_editor_state(indoc! {"
10657 fn func(abcˇ def: i32) -> u32 {
10658 }
10659 "});
10660
10661 cx.update_editor(|editor, window, cx| {
10662 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10663 });
10664
10665 cx.assert_editor_state(indoc! {"
10666 fn func(abc def: i32) -> ˇu32 {
10667 }
10668 "});
10669}
10670
10671#[gpui::test]
10672async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
10673 init_test(cx, |_| {});
10674
10675 let mut cx = EditorTestContext::new(cx).await;
10676
10677 cx.set_state(indoc! {"
10678 fn func(abˇc def: i32) -> u32 {
10679 }
10680 "});
10681 let lsp_store =
10682 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10683
10684 cx.update(|_, cx| {
10685 lsp_store.update(cx, |lsp_store, cx| {
10686 lsp_store.update_diagnostics(
10687 LanguageServerId(0),
10688 lsp::PublishDiagnosticsParams {
10689 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
10690 version: None,
10691 diagnostics: vec![lsp::Diagnostic {
10692 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
10693 severity: Some(lsp::DiagnosticSeverity::ERROR),
10694 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
10695 ..Default::default()
10696 }],
10697 },
10698 &[],
10699 cx,
10700 )
10701 })
10702 }).unwrap();
10703 cx.run_until_parked();
10704 cx.update_editor(|editor, window, cx| {
10705 hover_popover::hover(editor, &Default::default(), window, cx)
10706 });
10707 cx.run_until_parked();
10708 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
10709}
10710
10711#[gpui::test]
10712async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
10713 init_test(cx, |_| {});
10714
10715 let mut cx = EditorTestContext::new(cx).await;
10716
10717 let diff_base = r#"
10718 use some::mod;
10719
10720 const A: u32 = 42;
10721
10722 fn main() {
10723 println!("hello");
10724
10725 println!("world");
10726 }
10727 "#
10728 .unindent();
10729
10730 // Edits are modified, removed, modified, added
10731 cx.set_state(
10732 &r#"
10733 use some::modified;
10734
10735 ˇ
10736 fn main() {
10737 println!("hello there");
10738
10739 println!("around the");
10740 println!("world");
10741 }
10742 "#
10743 .unindent(),
10744 );
10745
10746 cx.set_diff_base(&diff_base);
10747 executor.run_until_parked();
10748
10749 cx.update_editor(|editor, window, cx| {
10750 //Wrap around the bottom of the buffer
10751 for _ in 0..3 {
10752 editor.go_to_next_hunk(&GoToHunk, window, cx);
10753 }
10754 });
10755
10756 cx.assert_editor_state(
10757 &r#"
10758 ˇuse some::modified;
10759
10760
10761 fn main() {
10762 println!("hello there");
10763
10764 println!("around the");
10765 println!("world");
10766 }
10767 "#
10768 .unindent(),
10769 );
10770
10771 cx.update_editor(|editor, window, cx| {
10772 //Wrap around the top of the buffer
10773 for _ in 0..2 {
10774 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10775 }
10776 });
10777
10778 cx.assert_editor_state(
10779 &r#"
10780 use some::modified;
10781
10782
10783 fn main() {
10784 ˇ println!("hello there");
10785
10786 println!("around the");
10787 println!("world");
10788 }
10789 "#
10790 .unindent(),
10791 );
10792
10793 cx.update_editor(|editor, window, cx| {
10794 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10795 });
10796
10797 cx.assert_editor_state(
10798 &r#"
10799 use some::modified;
10800
10801 ˇ
10802 fn main() {
10803 println!("hello there");
10804
10805 println!("around the");
10806 println!("world");
10807 }
10808 "#
10809 .unindent(),
10810 );
10811
10812 cx.update_editor(|editor, window, cx| {
10813 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10814 });
10815
10816 cx.assert_editor_state(
10817 &r#"
10818 ˇuse some::modified;
10819
10820
10821 fn main() {
10822 println!("hello there");
10823
10824 println!("around the");
10825 println!("world");
10826 }
10827 "#
10828 .unindent(),
10829 );
10830
10831 cx.update_editor(|editor, window, cx| {
10832 for _ in 0..2 {
10833 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10834 }
10835 });
10836
10837 cx.assert_editor_state(
10838 &r#"
10839 use some::modified;
10840
10841
10842 fn main() {
10843 ˇ println!("hello there");
10844
10845 println!("around the");
10846 println!("world");
10847 }
10848 "#
10849 .unindent(),
10850 );
10851
10852 cx.update_editor(|editor, window, cx| {
10853 editor.fold(&Fold, window, cx);
10854 });
10855
10856 cx.update_editor(|editor, window, cx| {
10857 editor.go_to_next_hunk(&GoToHunk, window, cx);
10858 });
10859
10860 cx.assert_editor_state(
10861 &r#"
10862 ˇuse some::modified;
10863
10864
10865 fn main() {
10866 println!("hello there");
10867
10868 println!("around the");
10869 println!("world");
10870 }
10871 "#
10872 .unindent(),
10873 );
10874}
10875
10876#[test]
10877fn test_split_words() {
10878 fn split(text: &str) -> Vec<&str> {
10879 split_words(text).collect()
10880 }
10881
10882 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
10883 assert_eq!(split("hello_world"), &["hello_", "world"]);
10884 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
10885 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
10886 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
10887 assert_eq!(split("helloworld"), &["helloworld"]);
10888
10889 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
10890}
10891
10892#[gpui::test]
10893async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
10894 init_test(cx, |_| {});
10895
10896 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
10897 let mut assert = |before, after| {
10898 let _state_context = cx.set_state(before);
10899 cx.run_until_parked();
10900 cx.update_editor(|editor, window, cx| {
10901 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
10902 });
10903 cx.assert_editor_state(after);
10904 };
10905
10906 // Outside bracket jumps to outside of matching bracket
10907 assert("console.logˇ(var);", "console.log(var)ˇ;");
10908 assert("console.log(var)ˇ;", "console.logˇ(var);");
10909
10910 // Inside bracket jumps to inside of matching bracket
10911 assert("console.log(ˇvar);", "console.log(varˇ);");
10912 assert("console.log(varˇ);", "console.log(ˇvar);");
10913
10914 // When outside a bracket and inside, favor jumping to the inside bracket
10915 assert(
10916 "console.log('foo', [1, 2, 3]ˇ);",
10917 "console.log(ˇ'foo', [1, 2, 3]);",
10918 );
10919 assert(
10920 "console.log(ˇ'foo', [1, 2, 3]);",
10921 "console.log('foo', [1, 2, 3]ˇ);",
10922 );
10923
10924 // Bias forward if two options are equally likely
10925 assert(
10926 "let result = curried_fun()ˇ();",
10927 "let result = curried_fun()()ˇ;",
10928 );
10929
10930 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
10931 assert(
10932 indoc! {"
10933 function test() {
10934 console.log('test')ˇ
10935 }"},
10936 indoc! {"
10937 function test() {
10938 console.logˇ('test')
10939 }"},
10940 );
10941}
10942
10943#[gpui::test]
10944async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
10945 init_test(cx, |_| {});
10946
10947 let fs = FakeFs::new(cx.executor());
10948 fs.insert_tree(
10949 path!("/a"),
10950 json!({
10951 "main.rs": "fn main() { let a = 5; }",
10952 "other.rs": "// Test file",
10953 }),
10954 )
10955 .await;
10956 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10957
10958 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10959 language_registry.add(Arc::new(Language::new(
10960 LanguageConfig {
10961 name: "Rust".into(),
10962 matcher: LanguageMatcher {
10963 path_suffixes: vec!["rs".to_string()],
10964 ..Default::default()
10965 },
10966 brackets: BracketPairConfig {
10967 pairs: vec![BracketPair {
10968 start: "{".to_string(),
10969 end: "}".to_string(),
10970 close: true,
10971 surround: true,
10972 newline: true,
10973 }],
10974 disabled_scopes_by_bracket_ix: Vec::new(),
10975 },
10976 ..Default::default()
10977 },
10978 Some(tree_sitter_rust::LANGUAGE.into()),
10979 )));
10980 let mut fake_servers = language_registry.register_fake_lsp(
10981 "Rust",
10982 FakeLspAdapter {
10983 capabilities: lsp::ServerCapabilities {
10984 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
10985 first_trigger_character: "{".to_string(),
10986 more_trigger_character: None,
10987 }),
10988 ..Default::default()
10989 },
10990 ..Default::default()
10991 },
10992 );
10993
10994 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10995
10996 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10997
10998 let worktree_id = workspace
10999 .update(cx, |workspace, _, cx| {
11000 workspace.project().update(cx, |project, cx| {
11001 project.worktrees(cx).next().unwrap().read(cx).id()
11002 })
11003 })
11004 .unwrap();
11005
11006 let buffer = project
11007 .update(cx, |project, cx| {
11008 project.open_local_buffer(path!("/a/main.rs"), cx)
11009 })
11010 .await
11011 .unwrap();
11012 let editor_handle = workspace
11013 .update(cx, |workspace, window, cx| {
11014 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
11015 })
11016 .unwrap()
11017 .await
11018 .unwrap()
11019 .downcast::<Editor>()
11020 .unwrap();
11021
11022 cx.executor().start_waiting();
11023 let fake_server = fake_servers.next().await.unwrap();
11024
11025 fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
11026 assert_eq!(
11027 params.text_document_position.text_document.uri,
11028 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
11029 );
11030 assert_eq!(
11031 params.text_document_position.position,
11032 lsp::Position::new(0, 21),
11033 );
11034
11035 Ok(Some(vec![lsp::TextEdit {
11036 new_text: "]".to_string(),
11037 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11038 }]))
11039 });
11040
11041 editor_handle.update_in(cx, |editor, window, cx| {
11042 window.focus(&editor.focus_handle(cx));
11043 editor.change_selections(None, window, cx, |s| {
11044 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
11045 });
11046 editor.handle_input("{", window, cx);
11047 });
11048
11049 cx.executor().run_until_parked();
11050
11051 buffer.update(cx, |buffer, _| {
11052 assert_eq!(
11053 buffer.text(),
11054 "fn main() { let a = {5}; }",
11055 "No extra braces from on type formatting should appear in the buffer"
11056 )
11057 });
11058}
11059
11060#[gpui::test]
11061async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) {
11062 init_test(cx, |_| {});
11063
11064 let fs = FakeFs::new(cx.executor());
11065 fs.insert_tree(
11066 path!("/a"),
11067 json!({
11068 "main.rs": "fn main() { let a = 5; }",
11069 "other.rs": "// Test file",
11070 }),
11071 )
11072 .await;
11073
11074 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11075
11076 let server_restarts = Arc::new(AtomicUsize::new(0));
11077 let closure_restarts = Arc::clone(&server_restarts);
11078 let language_server_name = "test language server";
11079 let language_name: LanguageName = "Rust".into();
11080
11081 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11082 language_registry.add(Arc::new(Language::new(
11083 LanguageConfig {
11084 name: language_name.clone(),
11085 matcher: LanguageMatcher {
11086 path_suffixes: vec!["rs".to_string()],
11087 ..Default::default()
11088 },
11089 ..Default::default()
11090 },
11091 Some(tree_sitter_rust::LANGUAGE.into()),
11092 )));
11093 let mut fake_servers = language_registry.register_fake_lsp(
11094 "Rust",
11095 FakeLspAdapter {
11096 name: language_server_name,
11097 initialization_options: Some(json!({
11098 "testOptionValue": true
11099 })),
11100 initializer: Some(Box::new(move |fake_server| {
11101 let task_restarts = Arc::clone(&closure_restarts);
11102 fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
11103 task_restarts.fetch_add(1, atomic::Ordering::Release);
11104 futures::future::ready(Ok(()))
11105 });
11106 })),
11107 ..Default::default()
11108 },
11109 );
11110
11111 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11112 let _buffer = project
11113 .update(cx, |project, cx| {
11114 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
11115 })
11116 .await
11117 .unwrap();
11118 let _fake_server = fake_servers.next().await.unwrap();
11119 update_test_language_settings(cx, |language_settings| {
11120 language_settings.languages.insert(
11121 language_name.clone(),
11122 LanguageSettingsContent {
11123 tab_size: NonZeroU32::new(8),
11124 ..Default::default()
11125 },
11126 );
11127 });
11128 cx.executor().run_until_parked();
11129 assert_eq!(
11130 server_restarts.load(atomic::Ordering::Acquire),
11131 0,
11132 "Should not restart LSP server on an unrelated change"
11133 );
11134
11135 update_test_project_settings(cx, |project_settings| {
11136 project_settings.lsp.insert(
11137 "Some other server name".into(),
11138 LspSettings {
11139 binary: None,
11140 settings: None,
11141 initialization_options: Some(json!({
11142 "some other init value": false
11143 })),
11144 },
11145 );
11146 });
11147 cx.executor().run_until_parked();
11148 assert_eq!(
11149 server_restarts.load(atomic::Ordering::Acquire),
11150 0,
11151 "Should not restart LSP server on an unrelated LSP settings change"
11152 );
11153
11154 update_test_project_settings(cx, |project_settings| {
11155 project_settings.lsp.insert(
11156 language_server_name.into(),
11157 LspSettings {
11158 binary: None,
11159 settings: None,
11160 initialization_options: Some(json!({
11161 "anotherInitValue": false
11162 })),
11163 },
11164 );
11165 });
11166 cx.executor().run_until_parked();
11167 assert_eq!(
11168 server_restarts.load(atomic::Ordering::Acquire),
11169 1,
11170 "Should restart LSP server on a related LSP settings change"
11171 );
11172
11173 update_test_project_settings(cx, |project_settings| {
11174 project_settings.lsp.insert(
11175 language_server_name.into(),
11176 LspSettings {
11177 binary: None,
11178 settings: None,
11179 initialization_options: Some(json!({
11180 "anotherInitValue": false
11181 })),
11182 },
11183 );
11184 });
11185 cx.executor().run_until_parked();
11186 assert_eq!(
11187 server_restarts.load(atomic::Ordering::Acquire),
11188 1,
11189 "Should not restart LSP server on a related LSP settings change that is the same"
11190 );
11191
11192 update_test_project_settings(cx, |project_settings| {
11193 project_settings.lsp.insert(
11194 language_server_name.into(),
11195 LspSettings {
11196 binary: None,
11197 settings: None,
11198 initialization_options: None,
11199 },
11200 );
11201 });
11202 cx.executor().run_until_parked();
11203 assert_eq!(
11204 server_restarts.load(atomic::Ordering::Acquire),
11205 2,
11206 "Should restart LSP server on another related LSP settings change"
11207 );
11208}
11209
11210#[gpui::test]
11211async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
11212 init_test(cx, |_| {});
11213
11214 let mut cx = EditorLspTestContext::new_rust(
11215 lsp::ServerCapabilities {
11216 completion_provider: Some(lsp::CompletionOptions {
11217 trigger_characters: Some(vec![".".to_string()]),
11218 resolve_provider: Some(true),
11219 ..Default::default()
11220 }),
11221 ..Default::default()
11222 },
11223 cx,
11224 )
11225 .await;
11226
11227 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11228 cx.simulate_keystroke(".");
11229 let completion_item = lsp::CompletionItem {
11230 label: "some".into(),
11231 kind: Some(lsp::CompletionItemKind::SNIPPET),
11232 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11233 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11234 kind: lsp::MarkupKind::Markdown,
11235 value: "```rust\nSome(2)\n```".to_string(),
11236 })),
11237 deprecated: Some(false),
11238 sort_text: Some("fffffff2".to_string()),
11239 filter_text: Some("some".to_string()),
11240 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11241 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11242 range: lsp::Range {
11243 start: lsp::Position {
11244 line: 0,
11245 character: 22,
11246 },
11247 end: lsp::Position {
11248 line: 0,
11249 character: 22,
11250 },
11251 },
11252 new_text: "Some(2)".to_string(),
11253 })),
11254 additional_text_edits: Some(vec![lsp::TextEdit {
11255 range: lsp::Range {
11256 start: lsp::Position {
11257 line: 0,
11258 character: 20,
11259 },
11260 end: lsp::Position {
11261 line: 0,
11262 character: 22,
11263 },
11264 },
11265 new_text: "".to_string(),
11266 }]),
11267 ..Default::default()
11268 };
11269
11270 let closure_completion_item = completion_item.clone();
11271 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11272 let task_completion_item = closure_completion_item.clone();
11273 async move {
11274 Ok(Some(lsp::CompletionResponse::Array(vec![
11275 task_completion_item,
11276 ])))
11277 }
11278 });
11279
11280 request.next().await;
11281
11282 cx.condition(|editor, _| editor.context_menu_visible())
11283 .await;
11284 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11285 editor
11286 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11287 .unwrap()
11288 });
11289 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
11290
11291 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
11292 let task_completion_item = completion_item.clone();
11293 async move { Ok(task_completion_item) }
11294 })
11295 .next()
11296 .await
11297 .unwrap();
11298 apply_additional_edits.await.unwrap();
11299 cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
11300}
11301
11302#[gpui::test]
11303async fn test_completions_resolve_updates_labels_if_filter_text_matches(
11304 cx: &mut gpui::TestAppContext,
11305) {
11306 init_test(cx, |_| {});
11307
11308 let mut cx = EditorLspTestContext::new_rust(
11309 lsp::ServerCapabilities {
11310 completion_provider: Some(lsp::CompletionOptions {
11311 trigger_characters: Some(vec![".".to_string()]),
11312 resolve_provider: Some(true),
11313 ..Default::default()
11314 }),
11315 ..Default::default()
11316 },
11317 cx,
11318 )
11319 .await;
11320
11321 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11322 cx.simulate_keystroke(".");
11323
11324 let item1 = lsp::CompletionItem {
11325 label: "method id()".to_string(),
11326 filter_text: Some("id".to_string()),
11327 detail: None,
11328 documentation: None,
11329 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11330 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11331 new_text: ".id".to_string(),
11332 })),
11333 ..lsp::CompletionItem::default()
11334 };
11335
11336 let item2 = lsp::CompletionItem {
11337 label: "other".to_string(),
11338 filter_text: Some("other".to_string()),
11339 detail: None,
11340 documentation: None,
11341 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11342 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11343 new_text: ".other".to_string(),
11344 })),
11345 ..lsp::CompletionItem::default()
11346 };
11347
11348 let item1 = item1.clone();
11349 cx.handle_request::<lsp::request::Completion, _, _>({
11350 let item1 = item1.clone();
11351 move |_, _, _| {
11352 let item1 = item1.clone();
11353 let item2 = item2.clone();
11354 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
11355 }
11356 })
11357 .next()
11358 .await;
11359
11360 cx.condition(|editor, _| editor.context_menu_visible())
11361 .await;
11362 cx.update_editor(|editor, _, _| {
11363 let context_menu = editor.context_menu.borrow_mut();
11364 let context_menu = context_menu
11365 .as_ref()
11366 .expect("Should have the context menu deployed");
11367 match context_menu {
11368 CodeContextMenu::Completions(completions_menu) => {
11369 let completions = completions_menu.completions.borrow_mut();
11370 assert_eq!(
11371 completions
11372 .iter()
11373 .map(|completion| &completion.label.text)
11374 .collect::<Vec<_>>(),
11375 vec!["method id()", "other"]
11376 )
11377 }
11378 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11379 }
11380 });
11381
11382 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>({
11383 let item1 = item1.clone();
11384 move |_, item_to_resolve, _| {
11385 let item1 = item1.clone();
11386 async move {
11387 if item1 == item_to_resolve {
11388 Ok(lsp::CompletionItem {
11389 label: "method id()".to_string(),
11390 filter_text: Some("id".to_string()),
11391 detail: Some("Now resolved!".to_string()),
11392 documentation: Some(lsp::Documentation::String("Docs".to_string())),
11393 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11394 range: lsp::Range::new(
11395 lsp::Position::new(0, 22),
11396 lsp::Position::new(0, 22),
11397 ),
11398 new_text: ".id".to_string(),
11399 })),
11400 ..lsp::CompletionItem::default()
11401 })
11402 } else {
11403 Ok(item_to_resolve)
11404 }
11405 }
11406 }
11407 })
11408 .next()
11409 .await
11410 .unwrap();
11411 cx.run_until_parked();
11412
11413 cx.update_editor(|editor, window, cx| {
11414 editor.context_menu_next(&Default::default(), window, cx);
11415 });
11416
11417 cx.update_editor(|editor, _, _| {
11418 let context_menu = editor.context_menu.borrow_mut();
11419 let context_menu = context_menu
11420 .as_ref()
11421 .expect("Should have the context menu deployed");
11422 match context_menu {
11423 CodeContextMenu::Completions(completions_menu) => {
11424 let completions = completions_menu.completions.borrow_mut();
11425 assert_eq!(
11426 completions
11427 .iter()
11428 .map(|completion| &completion.label.text)
11429 .collect::<Vec<_>>(),
11430 vec!["method id() Now resolved!", "other"],
11431 "Should update first completion label, but not second as the filter text did not match."
11432 );
11433 }
11434 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11435 }
11436 });
11437}
11438
11439#[gpui::test]
11440async fn test_completions_resolve_happens_once(cx: &mut gpui::TestAppContext) {
11441 init_test(cx, |_| {});
11442
11443 let mut cx = EditorLspTestContext::new_rust(
11444 lsp::ServerCapabilities {
11445 completion_provider: Some(lsp::CompletionOptions {
11446 trigger_characters: Some(vec![".".to_string()]),
11447 resolve_provider: Some(true),
11448 ..Default::default()
11449 }),
11450 ..Default::default()
11451 },
11452 cx,
11453 )
11454 .await;
11455
11456 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11457 cx.simulate_keystroke(".");
11458
11459 let unresolved_item_1 = lsp::CompletionItem {
11460 label: "id".to_string(),
11461 filter_text: Some("id".to_string()),
11462 detail: None,
11463 documentation: None,
11464 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11465 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11466 new_text: ".id".to_string(),
11467 })),
11468 ..lsp::CompletionItem::default()
11469 };
11470 let resolved_item_1 = lsp::CompletionItem {
11471 additional_text_edits: Some(vec![lsp::TextEdit {
11472 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
11473 new_text: "!!".to_string(),
11474 }]),
11475 ..unresolved_item_1.clone()
11476 };
11477 let unresolved_item_2 = lsp::CompletionItem {
11478 label: "other".to_string(),
11479 filter_text: Some("other".to_string()),
11480 detail: None,
11481 documentation: None,
11482 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11483 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11484 new_text: ".other".to_string(),
11485 })),
11486 ..lsp::CompletionItem::default()
11487 };
11488 let resolved_item_2 = lsp::CompletionItem {
11489 additional_text_edits: Some(vec![lsp::TextEdit {
11490 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
11491 new_text: "??".to_string(),
11492 }]),
11493 ..unresolved_item_2.clone()
11494 };
11495
11496 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
11497 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
11498 cx.lsp
11499 .server
11500 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
11501 let unresolved_item_1 = unresolved_item_1.clone();
11502 let resolved_item_1 = resolved_item_1.clone();
11503 let unresolved_item_2 = unresolved_item_2.clone();
11504 let resolved_item_2 = resolved_item_2.clone();
11505 let resolve_requests_1 = resolve_requests_1.clone();
11506 let resolve_requests_2 = resolve_requests_2.clone();
11507 move |unresolved_request, _| {
11508 let unresolved_item_1 = unresolved_item_1.clone();
11509 let resolved_item_1 = resolved_item_1.clone();
11510 let unresolved_item_2 = unresolved_item_2.clone();
11511 let resolved_item_2 = resolved_item_2.clone();
11512 let resolve_requests_1 = resolve_requests_1.clone();
11513 let resolve_requests_2 = resolve_requests_2.clone();
11514 async move {
11515 if unresolved_request == unresolved_item_1 {
11516 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
11517 Ok(resolved_item_1.clone())
11518 } else if unresolved_request == unresolved_item_2 {
11519 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
11520 Ok(resolved_item_2.clone())
11521 } else {
11522 panic!("Unexpected completion item {unresolved_request:?}")
11523 }
11524 }
11525 }
11526 })
11527 .detach();
11528
11529 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11530 let unresolved_item_1 = unresolved_item_1.clone();
11531 let unresolved_item_2 = unresolved_item_2.clone();
11532 async move {
11533 Ok(Some(lsp::CompletionResponse::Array(vec![
11534 unresolved_item_1,
11535 unresolved_item_2,
11536 ])))
11537 }
11538 })
11539 .next()
11540 .await;
11541
11542 cx.condition(|editor, _| editor.context_menu_visible())
11543 .await;
11544 cx.update_editor(|editor, _, _| {
11545 let context_menu = editor.context_menu.borrow_mut();
11546 let context_menu = context_menu
11547 .as_ref()
11548 .expect("Should have the context menu deployed");
11549 match context_menu {
11550 CodeContextMenu::Completions(completions_menu) => {
11551 let completions = completions_menu.completions.borrow_mut();
11552 assert_eq!(
11553 completions
11554 .iter()
11555 .map(|completion| &completion.label.text)
11556 .collect::<Vec<_>>(),
11557 vec!["id", "other"]
11558 )
11559 }
11560 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11561 }
11562 });
11563 cx.run_until_parked();
11564
11565 cx.update_editor(|editor, window, cx| {
11566 editor.context_menu_next(&ContextMenuNext, window, cx);
11567 });
11568 cx.run_until_parked();
11569 cx.update_editor(|editor, window, cx| {
11570 editor.context_menu_prev(&ContextMenuPrev, window, cx);
11571 });
11572 cx.run_until_parked();
11573 cx.update_editor(|editor, window, cx| {
11574 editor.context_menu_next(&ContextMenuNext, window, cx);
11575 });
11576 cx.run_until_parked();
11577 cx.update_editor(|editor, window, cx| {
11578 editor
11579 .compose_completion(&ComposeCompletion::default(), window, cx)
11580 .expect("No task returned")
11581 })
11582 .await
11583 .expect("Completion failed");
11584 cx.run_until_parked();
11585
11586 cx.update_editor(|editor, _, cx| {
11587 assert_eq!(
11588 resolve_requests_1.load(atomic::Ordering::Acquire),
11589 1,
11590 "Should always resolve once despite multiple selections"
11591 );
11592 assert_eq!(
11593 resolve_requests_2.load(atomic::Ordering::Acquire),
11594 1,
11595 "Should always resolve once after multiple selections and applying the completion"
11596 );
11597 assert_eq!(
11598 editor.text(cx),
11599 "fn main() { let a = ??.other; }",
11600 "Should use resolved data when applying the completion"
11601 );
11602 });
11603}
11604
11605#[gpui::test]
11606async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
11607 init_test(cx, |_| {});
11608
11609 let item_0 = lsp::CompletionItem {
11610 label: "abs".into(),
11611 insert_text: Some("abs".into()),
11612 data: Some(json!({ "very": "special"})),
11613 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
11614 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11615 lsp::InsertReplaceEdit {
11616 new_text: "abs".to_string(),
11617 insert: lsp::Range::default(),
11618 replace: lsp::Range::default(),
11619 },
11620 )),
11621 ..lsp::CompletionItem::default()
11622 };
11623 let items = iter::once(item_0.clone())
11624 .chain((11..51).map(|i| lsp::CompletionItem {
11625 label: format!("item_{}", i),
11626 insert_text: Some(format!("item_{}", i)),
11627 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
11628 ..lsp::CompletionItem::default()
11629 }))
11630 .collect::<Vec<_>>();
11631
11632 let default_commit_characters = vec!["?".to_string()];
11633 let default_data = json!({ "default": "data"});
11634 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
11635 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
11636 let default_edit_range = lsp::Range {
11637 start: lsp::Position {
11638 line: 0,
11639 character: 5,
11640 },
11641 end: lsp::Position {
11642 line: 0,
11643 character: 5,
11644 },
11645 };
11646
11647 let item_0_out = lsp::CompletionItem {
11648 commit_characters: Some(default_commit_characters.clone()),
11649 insert_text_format: Some(default_insert_text_format),
11650 ..item_0
11651 };
11652 let items_out = iter::once(item_0_out)
11653 .chain(items[1..].iter().map(|item| lsp::CompletionItem {
11654 commit_characters: Some(default_commit_characters.clone()),
11655 data: Some(default_data.clone()),
11656 insert_text_mode: Some(default_insert_text_mode),
11657 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11658 range: default_edit_range,
11659 new_text: item.label.clone(),
11660 })),
11661 ..item.clone()
11662 }))
11663 .collect::<Vec<lsp::CompletionItem>>();
11664
11665 let mut cx = EditorLspTestContext::new_rust(
11666 lsp::ServerCapabilities {
11667 completion_provider: Some(lsp::CompletionOptions {
11668 trigger_characters: Some(vec![".".to_string()]),
11669 resolve_provider: Some(true),
11670 ..Default::default()
11671 }),
11672 ..Default::default()
11673 },
11674 cx,
11675 )
11676 .await;
11677
11678 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11679 cx.simulate_keystroke(".");
11680
11681 let completion_data = default_data.clone();
11682 let completion_characters = default_commit_characters.clone();
11683 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11684 let default_data = completion_data.clone();
11685 let default_commit_characters = completion_characters.clone();
11686 let items = items.clone();
11687 async move {
11688 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
11689 items,
11690 item_defaults: Some(lsp::CompletionListItemDefaults {
11691 data: Some(default_data.clone()),
11692 commit_characters: Some(default_commit_characters.clone()),
11693 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
11694 default_edit_range,
11695 )),
11696 insert_text_format: Some(default_insert_text_format),
11697 insert_text_mode: Some(default_insert_text_mode),
11698 }),
11699 ..lsp::CompletionList::default()
11700 })))
11701 }
11702 })
11703 .next()
11704 .await;
11705
11706 let resolved_items = Arc::new(Mutex::new(Vec::new()));
11707 cx.lsp
11708 .server
11709 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
11710 let closure_resolved_items = resolved_items.clone();
11711 move |item_to_resolve, _| {
11712 let closure_resolved_items = closure_resolved_items.clone();
11713 async move {
11714 closure_resolved_items.lock().push(item_to_resolve.clone());
11715 Ok(item_to_resolve)
11716 }
11717 }
11718 })
11719 .detach();
11720
11721 cx.condition(|editor, _| editor.context_menu_visible())
11722 .await;
11723 cx.run_until_parked();
11724 cx.update_editor(|editor, _, _| {
11725 let menu = editor.context_menu.borrow_mut();
11726 match menu.as_ref().expect("should have the completions menu") {
11727 CodeContextMenu::Completions(completions_menu) => {
11728 assert_eq!(
11729 completions_menu
11730 .entries
11731 .borrow()
11732 .iter()
11733 .map(|mat| mat.string.clone())
11734 .collect::<Vec<String>>(),
11735 items_out
11736 .iter()
11737 .map(|completion| completion.label.clone())
11738 .collect::<Vec<String>>()
11739 );
11740 }
11741 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
11742 }
11743 });
11744 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
11745 // with 4 from the end.
11746 assert_eq!(
11747 *resolved_items.lock(),
11748 [
11749 &items_out[0..16],
11750 &items_out[items_out.len() - 4..items_out.len()]
11751 ]
11752 .concat()
11753 .iter()
11754 .cloned()
11755 .collect::<Vec<lsp::CompletionItem>>()
11756 );
11757 resolved_items.lock().clear();
11758
11759 cx.update_editor(|editor, window, cx| {
11760 editor.context_menu_prev(&ContextMenuPrev, window, cx);
11761 });
11762 cx.run_until_parked();
11763 // Completions that have already been resolved are skipped.
11764 assert_eq!(
11765 *resolved_items.lock(),
11766 items_out[items_out.len() - 16..items_out.len() - 4]
11767 .iter()
11768 .cloned()
11769 .collect::<Vec<lsp::CompletionItem>>()
11770 );
11771 resolved_items.lock().clear();
11772}
11773
11774#[gpui::test]
11775async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
11776 init_test(cx, |_| {});
11777
11778 let mut cx = EditorLspTestContext::new(
11779 Language::new(
11780 LanguageConfig {
11781 matcher: LanguageMatcher {
11782 path_suffixes: vec!["jsx".into()],
11783 ..Default::default()
11784 },
11785 overrides: [(
11786 "element".into(),
11787 LanguageConfigOverride {
11788 word_characters: Override::Set(['-'].into_iter().collect()),
11789 ..Default::default()
11790 },
11791 )]
11792 .into_iter()
11793 .collect(),
11794 ..Default::default()
11795 },
11796 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
11797 )
11798 .with_override_query("(jsx_self_closing_element) @element")
11799 .unwrap(),
11800 lsp::ServerCapabilities {
11801 completion_provider: Some(lsp::CompletionOptions {
11802 trigger_characters: Some(vec![":".to_string()]),
11803 ..Default::default()
11804 }),
11805 ..Default::default()
11806 },
11807 cx,
11808 )
11809 .await;
11810
11811 cx.lsp
11812 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11813 Ok(Some(lsp::CompletionResponse::Array(vec![
11814 lsp::CompletionItem {
11815 label: "bg-blue".into(),
11816 ..Default::default()
11817 },
11818 lsp::CompletionItem {
11819 label: "bg-red".into(),
11820 ..Default::default()
11821 },
11822 lsp::CompletionItem {
11823 label: "bg-yellow".into(),
11824 ..Default::default()
11825 },
11826 ])))
11827 });
11828
11829 cx.set_state(r#"<p class="bgˇ" />"#);
11830
11831 // Trigger completion when typing a dash, because the dash is an extra
11832 // word character in the 'element' scope, which contains the cursor.
11833 cx.simulate_keystroke("-");
11834 cx.executor().run_until_parked();
11835 cx.update_editor(|editor, _, _| {
11836 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11837 {
11838 assert_eq!(
11839 completion_menu_entries(&menu),
11840 &["bg-red", "bg-blue", "bg-yellow"]
11841 );
11842 } else {
11843 panic!("expected completion menu to be open");
11844 }
11845 });
11846
11847 cx.simulate_keystroke("l");
11848 cx.executor().run_until_parked();
11849 cx.update_editor(|editor, _, _| {
11850 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11851 {
11852 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
11853 } else {
11854 panic!("expected completion menu to be open");
11855 }
11856 });
11857
11858 // When filtering completions, consider the character after the '-' to
11859 // be the start of a subword.
11860 cx.set_state(r#"<p class="yelˇ" />"#);
11861 cx.simulate_keystroke("l");
11862 cx.executor().run_until_parked();
11863 cx.update_editor(|editor, _, _| {
11864 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11865 {
11866 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
11867 } else {
11868 panic!("expected completion menu to be open");
11869 }
11870 });
11871}
11872
11873fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
11874 let entries = menu.entries.borrow();
11875 entries.iter().map(|mat| mat.string.clone()).collect()
11876}
11877
11878#[gpui::test]
11879async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
11880 init_test(cx, |settings| {
11881 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
11882 FormatterList(vec![Formatter::Prettier].into()),
11883 ))
11884 });
11885
11886 let fs = FakeFs::new(cx.executor());
11887 fs.insert_file(path!("/file.ts"), Default::default()).await;
11888
11889 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
11890 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11891
11892 language_registry.add(Arc::new(Language::new(
11893 LanguageConfig {
11894 name: "TypeScript".into(),
11895 matcher: LanguageMatcher {
11896 path_suffixes: vec!["ts".to_string()],
11897 ..Default::default()
11898 },
11899 ..Default::default()
11900 },
11901 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11902 )));
11903 update_test_language_settings(cx, |settings| {
11904 settings.defaults.prettier = Some(PrettierSettings {
11905 allowed: true,
11906 ..PrettierSettings::default()
11907 });
11908 });
11909
11910 let test_plugin = "test_plugin";
11911 let _ = language_registry.register_fake_lsp(
11912 "TypeScript",
11913 FakeLspAdapter {
11914 prettier_plugins: vec![test_plugin],
11915 ..Default::default()
11916 },
11917 );
11918
11919 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
11920 let buffer = project
11921 .update(cx, |project, cx| {
11922 project.open_local_buffer(path!("/file.ts"), cx)
11923 })
11924 .await
11925 .unwrap();
11926
11927 let buffer_text = "one\ntwo\nthree\n";
11928 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11929 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11930 editor.update_in(cx, |editor, window, cx| {
11931 editor.set_text(buffer_text, window, cx)
11932 });
11933
11934 editor
11935 .update_in(cx, |editor, window, cx| {
11936 editor.perform_format(
11937 project.clone(),
11938 FormatTrigger::Manual,
11939 FormatTarget::Buffers,
11940 window,
11941 cx,
11942 )
11943 })
11944 .unwrap()
11945 .await;
11946 assert_eq!(
11947 editor.update(cx, |editor, cx| editor.text(cx)),
11948 buffer_text.to_string() + prettier_format_suffix,
11949 "Test prettier formatting was not applied to the original buffer text",
11950 );
11951
11952 update_test_language_settings(cx, |settings| {
11953 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
11954 });
11955 let format = editor.update_in(cx, |editor, window, cx| {
11956 editor.perform_format(
11957 project.clone(),
11958 FormatTrigger::Manual,
11959 FormatTarget::Buffers,
11960 window,
11961 cx,
11962 )
11963 });
11964 format.await.unwrap();
11965 assert_eq!(
11966 editor.update(cx, |editor, cx| editor.text(cx)),
11967 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
11968 "Autoformatting (via test prettier) was not applied to the original buffer text",
11969 );
11970}
11971
11972#[gpui::test]
11973async fn test_addition_reverts(cx: &mut gpui::TestAppContext) {
11974 init_test(cx, |_| {});
11975 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11976 let base_text = indoc! {r#"
11977 struct Row;
11978 struct Row1;
11979 struct Row2;
11980
11981 struct Row4;
11982 struct Row5;
11983 struct Row6;
11984
11985 struct Row8;
11986 struct Row9;
11987 struct Row10;"#};
11988
11989 // When addition hunks are not adjacent to carets, no hunk revert is performed
11990 assert_hunk_revert(
11991 indoc! {r#"struct Row;
11992 struct Row1;
11993 struct Row1.1;
11994 struct Row1.2;
11995 struct Row2;ˇ
11996
11997 struct Row4;
11998 struct Row5;
11999 struct Row6;
12000
12001 struct Row8;
12002 ˇstruct Row9;
12003 struct Row9.1;
12004 struct Row9.2;
12005 struct Row9.3;
12006 struct Row10;"#},
12007 vec![DiffHunkStatus::added(), DiffHunkStatus::added()],
12008 indoc! {r#"struct Row;
12009 struct Row1;
12010 struct Row1.1;
12011 struct Row1.2;
12012 struct Row2;ˇ
12013
12014 struct Row4;
12015 struct Row5;
12016 struct Row6;
12017
12018 struct Row8;
12019 ˇstruct Row9;
12020 struct Row9.1;
12021 struct Row9.2;
12022 struct Row9.3;
12023 struct Row10;"#},
12024 base_text,
12025 &mut cx,
12026 );
12027 // Same for selections
12028 assert_hunk_revert(
12029 indoc! {r#"struct Row;
12030 struct Row1;
12031 struct Row2;
12032 struct Row2.1;
12033 struct Row2.2;
12034 «ˇ
12035 struct Row4;
12036 struct» Row5;
12037 «struct Row6;
12038 ˇ»
12039 struct Row9.1;
12040 struct Row9.2;
12041 struct Row9.3;
12042 struct Row8;
12043 struct Row9;
12044 struct Row10;"#},
12045 vec![DiffHunkStatus::added(), DiffHunkStatus::added()],
12046 indoc! {r#"struct Row;
12047 struct Row1;
12048 struct Row2;
12049 struct Row2.1;
12050 struct Row2.2;
12051 «ˇ
12052 struct Row4;
12053 struct» Row5;
12054 «struct Row6;
12055 ˇ»
12056 struct Row9.1;
12057 struct Row9.2;
12058 struct Row9.3;
12059 struct Row8;
12060 struct Row9;
12061 struct Row10;"#},
12062 base_text,
12063 &mut cx,
12064 );
12065
12066 // When carets and selections intersect the addition hunks, those are reverted.
12067 // Adjacent carets got merged.
12068 assert_hunk_revert(
12069 indoc! {r#"struct Row;
12070 ˇ// something on the top
12071 struct Row1;
12072 struct Row2;
12073 struct Roˇw3.1;
12074 struct Row2.2;
12075 struct Row2.3;ˇ
12076
12077 struct Row4;
12078 struct ˇRow5.1;
12079 struct Row5.2;
12080 struct «Rowˇ»5.3;
12081 struct Row5;
12082 struct Row6;
12083 ˇ
12084 struct Row9.1;
12085 struct «Rowˇ»9.2;
12086 struct «ˇRow»9.3;
12087 struct Row8;
12088 struct Row9;
12089 «ˇ// something on bottom»
12090 struct Row10;"#},
12091 vec![
12092 DiffHunkStatus::added(),
12093 DiffHunkStatus::added(),
12094 DiffHunkStatus::added(),
12095 DiffHunkStatus::added(),
12096 DiffHunkStatus::added(),
12097 ],
12098 indoc! {r#"struct Row;
12099 ˇstruct Row1;
12100 struct Row2;
12101 ˇ
12102 struct Row4;
12103 ˇstruct Row5;
12104 struct Row6;
12105 ˇ
12106 ˇstruct Row8;
12107 struct Row9;
12108 ˇstruct Row10;"#},
12109 base_text,
12110 &mut cx,
12111 );
12112}
12113
12114#[gpui::test]
12115async fn test_modification_reverts(cx: &mut gpui::TestAppContext) {
12116 init_test(cx, |_| {});
12117 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12118 let base_text = indoc! {r#"
12119 struct Row;
12120 struct Row1;
12121 struct Row2;
12122
12123 struct Row4;
12124 struct Row5;
12125 struct Row6;
12126
12127 struct Row8;
12128 struct Row9;
12129 struct Row10;"#};
12130
12131 // Modification hunks behave the same as the addition ones.
12132 assert_hunk_revert(
12133 indoc! {r#"struct Row;
12134 struct Row1;
12135 struct Row33;
12136 ˇ
12137 struct Row4;
12138 struct Row5;
12139 struct Row6;
12140 ˇ
12141 struct Row99;
12142 struct Row9;
12143 struct Row10;"#},
12144 vec![DiffHunkStatus::modified(), DiffHunkStatus::modified()],
12145 indoc! {r#"struct Row;
12146 struct Row1;
12147 struct Row33;
12148 ˇ
12149 struct Row4;
12150 struct Row5;
12151 struct Row6;
12152 ˇ
12153 struct Row99;
12154 struct Row9;
12155 struct Row10;"#},
12156 base_text,
12157 &mut cx,
12158 );
12159 assert_hunk_revert(
12160 indoc! {r#"struct Row;
12161 struct Row1;
12162 struct Row33;
12163 «ˇ
12164 struct Row4;
12165 struct» Row5;
12166 «struct Row6;
12167 ˇ»
12168 struct Row99;
12169 struct Row9;
12170 struct Row10;"#},
12171 vec![DiffHunkStatus::modified(), DiffHunkStatus::modified()],
12172 indoc! {r#"struct Row;
12173 struct Row1;
12174 struct Row33;
12175 «ˇ
12176 struct Row4;
12177 struct» Row5;
12178 «struct Row6;
12179 ˇ»
12180 struct Row99;
12181 struct Row9;
12182 struct Row10;"#},
12183 base_text,
12184 &mut cx,
12185 );
12186
12187 assert_hunk_revert(
12188 indoc! {r#"ˇstruct Row1.1;
12189 struct Row1;
12190 «ˇstr»uct Row22;
12191
12192 struct ˇRow44;
12193 struct Row5;
12194 struct «Rˇ»ow66;ˇ
12195
12196 «struˇ»ct Row88;
12197 struct Row9;
12198 struct Row1011;ˇ"#},
12199 vec![
12200 DiffHunkStatus::modified(),
12201 DiffHunkStatus::modified(),
12202 DiffHunkStatus::modified(),
12203 DiffHunkStatus::modified(),
12204 DiffHunkStatus::modified(),
12205 DiffHunkStatus::modified(),
12206 ],
12207 indoc! {r#"struct Row;
12208 ˇstruct Row1;
12209 struct Row2;
12210 ˇ
12211 struct Row4;
12212 ˇstruct Row5;
12213 struct Row6;
12214 ˇ
12215 struct Row8;
12216 ˇstruct Row9;
12217 struct Row10;ˇ"#},
12218 base_text,
12219 &mut cx,
12220 );
12221}
12222
12223#[gpui::test]
12224async fn test_deleting_over_diff_hunk(cx: &mut gpui::TestAppContext) {
12225 init_test(cx, |_| {});
12226 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12227 let base_text = indoc! {r#"
12228 one
12229
12230 two
12231 three
12232 "#};
12233
12234 cx.set_diff_base(base_text);
12235 cx.set_state("\nˇ\n");
12236 cx.executor().run_until_parked();
12237 cx.update_editor(|editor, _window, cx| {
12238 editor.expand_selected_diff_hunks(cx);
12239 });
12240 cx.executor().run_until_parked();
12241 cx.update_editor(|editor, window, cx| {
12242 editor.backspace(&Default::default(), window, cx);
12243 });
12244 cx.run_until_parked();
12245 cx.assert_state_with_diff(
12246 indoc! {r#"
12247
12248 - two
12249 - threeˇ
12250 +
12251 "#}
12252 .to_string(),
12253 );
12254}
12255
12256#[gpui::test]
12257async fn test_deletion_reverts(cx: &mut gpui::TestAppContext) {
12258 init_test(cx, |_| {});
12259 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12260 let base_text = indoc! {r#"struct Row;
12261struct Row1;
12262struct Row2;
12263
12264struct Row4;
12265struct Row5;
12266struct Row6;
12267
12268struct Row8;
12269struct Row9;
12270struct Row10;"#};
12271
12272 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
12273 assert_hunk_revert(
12274 indoc! {r#"struct Row;
12275 struct Row2;
12276
12277 ˇstruct Row4;
12278 struct Row5;
12279 struct Row6;
12280 ˇ
12281 struct Row8;
12282 struct Row10;"#},
12283 vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()],
12284 indoc! {r#"struct Row;
12285 struct Row2;
12286
12287 ˇstruct Row4;
12288 struct Row5;
12289 struct Row6;
12290 ˇ
12291 struct Row8;
12292 struct Row10;"#},
12293 base_text,
12294 &mut cx,
12295 );
12296 assert_hunk_revert(
12297 indoc! {r#"struct Row;
12298 struct Row2;
12299
12300 «ˇstruct Row4;
12301 struct» Row5;
12302 «struct Row6;
12303 ˇ»
12304 struct Row8;
12305 struct Row10;"#},
12306 vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()],
12307 indoc! {r#"struct Row;
12308 struct Row2;
12309
12310 «ˇstruct Row4;
12311 struct» Row5;
12312 «struct Row6;
12313 ˇ»
12314 struct Row8;
12315 struct Row10;"#},
12316 base_text,
12317 &mut cx,
12318 );
12319
12320 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
12321 assert_hunk_revert(
12322 indoc! {r#"struct Row;
12323 ˇstruct Row2;
12324
12325 struct Row4;
12326 struct Row5;
12327 struct Row6;
12328
12329 struct Row8;ˇ
12330 struct Row10;"#},
12331 vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()],
12332 indoc! {r#"struct Row;
12333 struct Row1;
12334 ˇstruct Row2;
12335
12336 struct Row4;
12337 struct Row5;
12338 struct Row6;
12339
12340 struct Row8;ˇ
12341 struct Row9;
12342 struct Row10;"#},
12343 base_text,
12344 &mut cx,
12345 );
12346 assert_hunk_revert(
12347 indoc! {r#"struct Row;
12348 struct Row2«ˇ;
12349 struct Row4;
12350 struct» Row5;
12351 «struct Row6;
12352
12353 struct Row8;ˇ»
12354 struct Row10;"#},
12355 vec![
12356 DiffHunkStatus::removed(),
12357 DiffHunkStatus::removed(),
12358 DiffHunkStatus::removed(),
12359 ],
12360 indoc! {r#"struct Row;
12361 struct Row1;
12362 struct Row2«ˇ;
12363
12364 struct Row4;
12365 struct» Row5;
12366 «struct Row6;
12367
12368 struct Row8;ˇ»
12369 struct Row9;
12370 struct Row10;"#},
12371 base_text,
12372 &mut cx,
12373 );
12374}
12375
12376#[gpui::test]
12377async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
12378 init_test(cx, |_| {});
12379
12380 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
12381 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
12382 let base_text_3 =
12383 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
12384
12385 let text_1 = edit_first_char_of_every_line(base_text_1);
12386 let text_2 = edit_first_char_of_every_line(base_text_2);
12387 let text_3 = edit_first_char_of_every_line(base_text_3);
12388
12389 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
12390 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
12391 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
12392
12393 let multibuffer = cx.new(|cx| {
12394 let mut multibuffer = MultiBuffer::new(ReadWrite);
12395 multibuffer.push_excerpts(
12396 buffer_1.clone(),
12397 [
12398 ExcerptRange {
12399 context: Point::new(0, 0)..Point::new(3, 0),
12400 primary: None,
12401 },
12402 ExcerptRange {
12403 context: Point::new(5, 0)..Point::new(7, 0),
12404 primary: None,
12405 },
12406 ExcerptRange {
12407 context: Point::new(9, 0)..Point::new(10, 4),
12408 primary: None,
12409 },
12410 ],
12411 cx,
12412 );
12413 multibuffer.push_excerpts(
12414 buffer_2.clone(),
12415 [
12416 ExcerptRange {
12417 context: Point::new(0, 0)..Point::new(3, 0),
12418 primary: None,
12419 },
12420 ExcerptRange {
12421 context: Point::new(5, 0)..Point::new(7, 0),
12422 primary: None,
12423 },
12424 ExcerptRange {
12425 context: Point::new(9, 0)..Point::new(10, 4),
12426 primary: None,
12427 },
12428 ],
12429 cx,
12430 );
12431 multibuffer.push_excerpts(
12432 buffer_3.clone(),
12433 [
12434 ExcerptRange {
12435 context: Point::new(0, 0)..Point::new(3, 0),
12436 primary: None,
12437 },
12438 ExcerptRange {
12439 context: Point::new(5, 0)..Point::new(7, 0),
12440 primary: None,
12441 },
12442 ExcerptRange {
12443 context: Point::new(9, 0)..Point::new(10, 4),
12444 primary: None,
12445 },
12446 ],
12447 cx,
12448 );
12449 multibuffer
12450 });
12451
12452 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12453 editor.update_in(cx, |editor, _window, cx| {
12454 for (buffer, diff_base) in [
12455 (buffer_1.clone(), base_text_1),
12456 (buffer_2.clone(), base_text_2),
12457 (buffer_3.clone(), base_text_3),
12458 ] {
12459 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
12460 editor
12461 .buffer
12462 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
12463 }
12464 });
12465 cx.executor().run_until_parked();
12466
12467 editor.update_in(cx, |editor, window, cx| {
12468 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}");
12469 editor.select_all(&SelectAll, window, cx);
12470 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
12471 });
12472 cx.executor().run_until_parked();
12473
12474 // When all ranges are selected, all buffer hunks are reverted.
12475 editor.update(cx, |editor, cx| {
12476 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");
12477 });
12478 buffer_1.update(cx, |buffer, _| {
12479 assert_eq!(buffer.text(), base_text_1);
12480 });
12481 buffer_2.update(cx, |buffer, _| {
12482 assert_eq!(buffer.text(), base_text_2);
12483 });
12484 buffer_3.update(cx, |buffer, _| {
12485 assert_eq!(buffer.text(), base_text_3);
12486 });
12487
12488 editor.update_in(cx, |editor, window, cx| {
12489 editor.undo(&Default::default(), window, cx);
12490 });
12491
12492 editor.update_in(cx, |editor, window, cx| {
12493 editor.change_selections(None, window, cx, |s| {
12494 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
12495 });
12496 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
12497 });
12498
12499 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
12500 // but not affect buffer_2 and its related excerpts.
12501 editor.update(cx, |editor, cx| {
12502 assert_eq!(
12503 editor.text(cx),
12504 "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}"
12505 );
12506 });
12507 buffer_1.update(cx, |buffer, _| {
12508 assert_eq!(buffer.text(), base_text_1);
12509 });
12510 buffer_2.update(cx, |buffer, _| {
12511 assert_eq!(
12512 buffer.text(),
12513 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
12514 );
12515 });
12516 buffer_3.update(cx, |buffer, _| {
12517 assert_eq!(
12518 buffer.text(),
12519 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
12520 );
12521 });
12522
12523 fn edit_first_char_of_every_line(text: &str) -> String {
12524 text.split('\n')
12525 .map(|line| format!("X{}", &line[1..]))
12526 .collect::<Vec<_>>()
12527 .join("\n")
12528 }
12529}
12530
12531#[gpui::test]
12532async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
12533 init_test(cx, |_| {});
12534
12535 let cols = 4;
12536 let rows = 10;
12537 let sample_text_1 = sample_text(rows, cols, 'a');
12538 assert_eq!(
12539 sample_text_1,
12540 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
12541 );
12542 let sample_text_2 = sample_text(rows, cols, 'l');
12543 assert_eq!(
12544 sample_text_2,
12545 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
12546 );
12547 let sample_text_3 = sample_text(rows, cols, 'v');
12548 assert_eq!(
12549 sample_text_3,
12550 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
12551 );
12552
12553 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
12554 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
12555 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
12556
12557 let multi_buffer = cx.new(|cx| {
12558 let mut multibuffer = MultiBuffer::new(ReadWrite);
12559 multibuffer.push_excerpts(
12560 buffer_1.clone(),
12561 [
12562 ExcerptRange {
12563 context: Point::new(0, 0)..Point::new(3, 0),
12564 primary: None,
12565 },
12566 ExcerptRange {
12567 context: Point::new(5, 0)..Point::new(7, 0),
12568 primary: None,
12569 },
12570 ExcerptRange {
12571 context: Point::new(9, 0)..Point::new(10, 4),
12572 primary: None,
12573 },
12574 ],
12575 cx,
12576 );
12577 multibuffer.push_excerpts(
12578 buffer_2.clone(),
12579 [
12580 ExcerptRange {
12581 context: Point::new(0, 0)..Point::new(3, 0),
12582 primary: None,
12583 },
12584 ExcerptRange {
12585 context: Point::new(5, 0)..Point::new(7, 0),
12586 primary: None,
12587 },
12588 ExcerptRange {
12589 context: Point::new(9, 0)..Point::new(10, 4),
12590 primary: None,
12591 },
12592 ],
12593 cx,
12594 );
12595 multibuffer.push_excerpts(
12596 buffer_3.clone(),
12597 [
12598 ExcerptRange {
12599 context: Point::new(0, 0)..Point::new(3, 0),
12600 primary: None,
12601 },
12602 ExcerptRange {
12603 context: Point::new(5, 0)..Point::new(7, 0),
12604 primary: None,
12605 },
12606 ExcerptRange {
12607 context: Point::new(9, 0)..Point::new(10, 4),
12608 primary: None,
12609 },
12610 ],
12611 cx,
12612 );
12613 multibuffer
12614 });
12615
12616 let fs = FakeFs::new(cx.executor());
12617 fs.insert_tree(
12618 "/a",
12619 json!({
12620 "main.rs": sample_text_1,
12621 "other.rs": sample_text_2,
12622 "lib.rs": sample_text_3,
12623 }),
12624 )
12625 .await;
12626 let project = Project::test(fs, ["/a".as_ref()], cx).await;
12627 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12628 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12629 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
12630 Editor::new(
12631 EditorMode::Full,
12632 multi_buffer,
12633 Some(project.clone()),
12634 true,
12635 window,
12636 cx,
12637 )
12638 });
12639 let multibuffer_item_id = workspace
12640 .update(cx, |workspace, window, cx| {
12641 assert!(
12642 workspace.active_item(cx).is_none(),
12643 "active item should be None before the first item is added"
12644 );
12645 workspace.add_item_to_active_pane(
12646 Box::new(multi_buffer_editor.clone()),
12647 None,
12648 true,
12649 window,
12650 cx,
12651 );
12652 let active_item = workspace
12653 .active_item(cx)
12654 .expect("should have an active item after adding the multi buffer");
12655 assert!(
12656 !active_item.is_singleton(cx),
12657 "A multi buffer was expected to active after adding"
12658 );
12659 active_item.item_id()
12660 })
12661 .unwrap();
12662 cx.executor().run_until_parked();
12663
12664 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12665 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12666 s.select_ranges(Some(1..2))
12667 });
12668 editor.open_excerpts(&OpenExcerpts, window, cx);
12669 });
12670 cx.executor().run_until_parked();
12671 let first_item_id = workspace
12672 .update(cx, |workspace, window, cx| {
12673 let active_item = workspace
12674 .active_item(cx)
12675 .expect("should have an active item after navigating into the 1st buffer");
12676 let first_item_id = active_item.item_id();
12677 assert_ne!(
12678 first_item_id, multibuffer_item_id,
12679 "Should navigate into the 1st buffer and activate it"
12680 );
12681 assert!(
12682 active_item.is_singleton(cx),
12683 "New active item should be a singleton buffer"
12684 );
12685 assert_eq!(
12686 active_item
12687 .act_as::<Editor>(cx)
12688 .expect("should have navigated into an editor for the 1st buffer")
12689 .read(cx)
12690 .text(cx),
12691 sample_text_1
12692 );
12693
12694 workspace
12695 .go_back(workspace.active_pane().downgrade(), window, cx)
12696 .detach_and_log_err(cx);
12697
12698 first_item_id
12699 })
12700 .unwrap();
12701 cx.executor().run_until_parked();
12702 workspace
12703 .update(cx, |workspace, _, cx| {
12704 let active_item = workspace
12705 .active_item(cx)
12706 .expect("should have an active item after navigating back");
12707 assert_eq!(
12708 active_item.item_id(),
12709 multibuffer_item_id,
12710 "Should navigate back to the multi buffer"
12711 );
12712 assert!(!active_item.is_singleton(cx));
12713 })
12714 .unwrap();
12715
12716 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12717 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12718 s.select_ranges(Some(39..40))
12719 });
12720 editor.open_excerpts(&OpenExcerpts, window, cx);
12721 });
12722 cx.executor().run_until_parked();
12723 let second_item_id = workspace
12724 .update(cx, |workspace, window, cx| {
12725 let active_item = workspace
12726 .active_item(cx)
12727 .expect("should have an active item after navigating into the 2nd buffer");
12728 let second_item_id = active_item.item_id();
12729 assert_ne!(
12730 second_item_id, multibuffer_item_id,
12731 "Should navigate away from the multibuffer"
12732 );
12733 assert_ne!(
12734 second_item_id, first_item_id,
12735 "Should navigate into the 2nd buffer and activate it"
12736 );
12737 assert!(
12738 active_item.is_singleton(cx),
12739 "New active item should be a singleton buffer"
12740 );
12741 assert_eq!(
12742 active_item
12743 .act_as::<Editor>(cx)
12744 .expect("should have navigated into an editor")
12745 .read(cx)
12746 .text(cx),
12747 sample_text_2
12748 );
12749
12750 workspace
12751 .go_back(workspace.active_pane().downgrade(), window, cx)
12752 .detach_and_log_err(cx);
12753
12754 second_item_id
12755 })
12756 .unwrap();
12757 cx.executor().run_until_parked();
12758 workspace
12759 .update(cx, |workspace, _, cx| {
12760 let active_item = workspace
12761 .active_item(cx)
12762 .expect("should have an active item after navigating back from the 2nd buffer");
12763 assert_eq!(
12764 active_item.item_id(),
12765 multibuffer_item_id,
12766 "Should navigate back from the 2nd buffer to the multi buffer"
12767 );
12768 assert!(!active_item.is_singleton(cx));
12769 })
12770 .unwrap();
12771
12772 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12773 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12774 s.select_ranges(Some(70..70))
12775 });
12776 editor.open_excerpts(&OpenExcerpts, window, cx);
12777 });
12778 cx.executor().run_until_parked();
12779 workspace
12780 .update(cx, |workspace, window, cx| {
12781 let active_item = workspace
12782 .active_item(cx)
12783 .expect("should have an active item after navigating into the 3rd buffer");
12784 let third_item_id = active_item.item_id();
12785 assert_ne!(
12786 third_item_id, multibuffer_item_id,
12787 "Should navigate into the 3rd buffer and activate it"
12788 );
12789 assert_ne!(third_item_id, first_item_id);
12790 assert_ne!(third_item_id, second_item_id);
12791 assert!(
12792 active_item.is_singleton(cx),
12793 "New active item should be a singleton buffer"
12794 );
12795 assert_eq!(
12796 active_item
12797 .act_as::<Editor>(cx)
12798 .expect("should have navigated into an editor")
12799 .read(cx)
12800 .text(cx),
12801 sample_text_3
12802 );
12803
12804 workspace
12805 .go_back(workspace.active_pane().downgrade(), window, cx)
12806 .detach_and_log_err(cx);
12807 })
12808 .unwrap();
12809 cx.executor().run_until_parked();
12810 workspace
12811 .update(cx, |workspace, _, cx| {
12812 let active_item = workspace
12813 .active_item(cx)
12814 .expect("should have an active item after navigating back from the 3rd buffer");
12815 assert_eq!(
12816 active_item.item_id(),
12817 multibuffer_item_id,
12818 "Should navigate back from the 3rd buffer to the multi buffer"
12819 );
12820 assert!(!active_item.is_singleton(cx));
12821 })
12822 .unwrap();
12823}
12824
12825#[gpui::test]
12826async fn test_toggle_selected_diff_hunks(
12827 executor: BackgroundExecutor,
12828 cx: &mut gpui::TestAppContext,
12829) {
12830 init_test(cx, |_| {});
12831
12832 let mut cx = EditorTestContext::new(cx).await;
12833
12834 let diff_base = r#"
12835 use some::mod;
12836
12837 const A: u32 = 42;
12838
12839 fn main() {
12840 println!("hello");
12841
12842 println!("world");
12843 }
12844 "#
12845 .unindent();
12846
12847 cx.set_state(
12848 &r#"
12849 use some::modified;
12850
12851 ˇ
12852 fn main() {
12853 println!("hello there");
12854
12855 println!("around the");
12856 println!("world");
12857 }
12858 "#
12859 .unindent(),
12860 );
12861
12862 cx.set_diff_base(&diff_base);
12863 executor.run_until_parked();
12864
12865 cx.update_editor(|editor, window, cx| {
12866 editor.go_to_next_hunk(&GoToHunk, window, cx);
12867 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
12868 });
12869 executor.run_until_parked();
12870 cx.assert_state_with_diff(
12871 r#"
12872 use some::modified;
12873
12874
12875 fn main() {
12876 - println!("hello");
12877 + ˇ println!("hello there");
12878
12879 println!("around the");
12880 println!("world");
12881 }
12882 "#
12883 .unindent(),
12884 );
12885
12886 cx.update_editor(|editor, window, cx| {
12887 for _ in 0..2 {
12888 editor.go_to_next_hunk(&GoToHunk, window, cx);
12889 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
12890 }
12891 });
12892 executor.run_until_parked();
12893 cx.assert_state_with_diff(
12894 r#"
12895 - use some::mod;
12896 + ˇuse some::modified;
12897
12898
12899 fn main() {
12900 - println!("hello");
12901 + println!("hello there");
12902
12903 + println!("around the");
12904 println!("world");
12905 }
12906 "#
12907 .unindent(),
12908 );
12909
12910 cx.update_editor(|editor, window, cx| {
12911 editor.go_to_next_hunk(&GoToHunk, window, cx);
12912 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
12913 });
12914 executor.run_until_parked();
12915 cx.assert_state_with_diff(
12916 r#"
12917 - use some::mod;
12918 + use some::modified;
12919
12920 - const A: u32 = 42;
12921 ˇ
12922 fn main() {
12923 - println!("hello");
12924 + println!("hello there");
12925
12926 + println!("around the");
12927 println!("world");
12928 }
12929 "#
12930 .unindent(),
12931 );
12932
12933 cx.update_editor(|editor, window, cx| {
12934 editor.cancel(&Cancel, window, cx);
12935 });
12936
12937 cx.assert_state_with_diff(
12938 r#"
12939 use some::modified;
12940
12941 ˇ
12942 fn main() {
12943 println!("hello there");
12944
12945 println!("around the");
12946 println!("world");
12947 }
12948 "#
12949 .unindent(),
12950 );
12951}
12952
12953#[gpui::test]
12954async fn test_diff_base_change_with_expanded_diff_hunks(
12955 executor: BackgroundExecutor,
12956 cx: &mut gpui::TestAppContext,
12957) {
12958 init_test(cx, |_| {});
12959
12960 let mut cx = EditorTestContext::new(cx).await;
12961
12962 let diff_base = r#"
12963 use some::mod1;
12964 use some::mod2;
12965
12966 const A: u32 = 42;
12967 const B: u32 = 42;
12968 const C: u32 = 42;
12969
12970 fn main() {
12971 println!("hello");
12972
12973 println!("world");
12974 }
12975 "#
12976 .unindent();
12977
12978 cx.set_state(
12979 &r#"
12980 use some::mod2;
12981
12982 const A: u32 = 42;
12983 const C: u32 = 42;
12984
12985 fn main(ˇ) {
12986 //println!("hello");
12987
12988 println!("world");
12989 //
12990 //
12991 }
12992 "#
12993 .unindent(),
12994 );
12995
12996 cx.set_diff_base(&diff_base);
12997 executor.run_until_parked();
12998
12999 cx.update_editor(|editor, window, cx| {
13000 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13001 });
13002 executor.run_until_parked();
13003 cx.assert_state_with_diff(
13004 r#"
13005 - use some::mod1;
13006 use some::mod2;
13007
13008 const A: u32 = 42;
13009 - const B: u32 = 42;
13010 const C: u32 = 42;
13011
13012 fn main(ˇ) {
13013 - println!("hello");
13014 + //println!("hello");
13015
13016 println!("world");
13017 + //
13018 + //
13019 }
13020 "#
13021 .unindent(),
13022 );
13023
13024 cx.set_diff_base("new diff base!");
13025 executor.run_until_parked();
13026 cx.assert_state_with_diff(
13027 r#"
13028 use some::mod2;
13029
13030 const A: u32 = 42;
13031 const C: u32 = 42;
13032
13033 fn main(ˇ) {
13034 //println!("hello");
13035
13036 println!("world");
13037 //
13038 //
13039 }
13040 "#
13041 .unindent(),
13042 );
13043
13044 cx.update_editor(|editor, window, cx| {
13045 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13046 });
13047 executor.run_until_parked();
13048 cx.assert_state_with_diff(
13049 r#"
13050 - new diff base!
13051 + use some::mod2;
13052 +
13053 + const A: u32 = 42;
13054 + const C: u32 = 42;
13055 +
13056 + fn main(ˇ) {
13057 + //println!("hello");
13058 +
13059 + println!("world");
13060 + //
13061 + //
13062 + }
13063 "#
13064 .unindent(),
13065 );
13066}
13067
13068#[gpui::test]
13069async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) {
13070 init_test(cx, |_| {});
13071
13072 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
13073 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
13074 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
13075 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
13076 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
13077 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
13078
13079 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
13080 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
13081 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
13082
13083 let multi_buffer = cx.new(|cx| {
13084 let mut multibuffer = MultiBuffer::new(ReadWrite);
13085 multibuffer.push_excerpts(
13086 buffer_1.clone(),
13087 [
13088 ExcerptRange {
13089 context: Point::new(0, 0)..Point::new(3, 0),
13090 primary: None,
13091 },
13092 ExcerptRange {
13093 context: Point::new(5, 0)..Point::new(7, 0),
13094 primary: None,
13095 },
13096 ExcerptRange {
13097 context: Point::new(9, 0)..Point::new(10, 3),
13098 primary: None,
13099 },
13100 ],
13101 cx,
13102 );
13103 multibuffer.push_excerpts(
13104 buffer_2.clone(),
13105 [
13106 ExcerptRange {
13107 context: Point::new(0, 0)..Point::new(3, 0),
13108 primary: None,
13109 },
13110 ExcerptRange {
13111 context: Point::new(5, 0)..Point::new(7, 0),
13112 primary: None,
13113 },
13114 ExcerptRange {
13115 context: Point::new(9, 0)..Point::new(10, 3),
13116 primary: None,
13117 },
13118 ],
13119 cx,
13120 );
13121 multibuffer.push_excerpts(
13122 buffer_3.clone(),
13123 [
13124 ExcerptRange {
13125 context: Point::new(0, 0)..Point::new(3, 0),
13126 primary: None,
13127 },
13128 ExcerptRange {
13129 context: Point::new(5, 0)..Point::new(7, 0),
13130 primary: None,
13131 },
13132 ExcerptRange {
13133 context: Point::new(9, 0)..Point::new(10, 3),
13134 primary: None,
13135 },
13136 ],
13137 cx,
13138 );
13139 multibuffer
13140 });
13141
13142 let editor = cx.add_window(|window, cx| {
13143 Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
13144 });
13145 editor
13146 .update(cx, |editor, _window, cx| {
13147 for (buffer, diff_base) in [
13148 (buffer_1.clone(), file_1_old),
13149 (buffer_2.clone(), file_2_old),
13150 (buffer_3.clone(), file_3_old),
13151 ] {
13152 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
13153 editor
13154 .buffer
13155 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
13156 }
13157 })
13158 .unwrap();
13159
13160 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13161 cx.run_until_parked();
13162
13163 cx.assert_editor_state(
13164 &"
13165 ˇaaa
13166 ccc
13167 ddd
13168
13169 ggg
13170 hhh
13171
13172
13173 lll
13174 mmm
13175 NNN
13176
13177 qqq
13178 rrr
13179
13180 uuu
13181 111
13182 222
13183 333
13184
13185 666
13186 777
13187
13188 000
13189 !!!"
13190 .unindent(),
13191 );
13192
13193 cx.update_editor(|editor, window, cx| {
13194 editor.select_all(&SelectAll, window, cx);
13195 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13196 });
13197 cx.executor().run_until_parked();
13198
13199 cx.assert_state_with_diff(
13200 "
13201 «aaa
13202 - bbb
13203 ccc
13204 ddd
13205
13206 ggg
13207 hhh
13208
13209
13210 lll
13211 mmm
13212 - nnn
13213 + NNN
13214
13215 qqq
13216 rrr
13217
13218 uuu
13219 111
13220 222
13221 333
13222
13223 + 666
13224 777
13225
13226 000
13227 !!!ˇ»"
13228 .unindent(),
13229 );
13230}
13231
13232#[gpui::test]
13233async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext) {
13234 init_test(cx, |_| {});
13235
13236 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
13237 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
13238
13239 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
13240 let multi_buffer = cx.new(|cx| {
13241 let mut multibuffer = MultiBuffer::new(ReadWrite);
13242 multibuffer.push_excerpts(
13243 buffer.clone(),
13244 [
13245 ExcerptRange {
13246 context: Point::new(0, 0)..Point::new(2, 0),
13247 primary: None,
13248 },
13249 ExcerptRange {
13250 context: Point::new(4, 0)..Point::new(7, 0),
13251 primary: None,
13252 },
13253 ExcerptRange {
13254 context: Point::new(9, 0)..Point::new(10, 0),
13255 primary: None,
13256 },
13257 ],
13258 cx,
13259 );
13260 multibuffer
13261 });
13262
13263 let editor = cx.add_window(|window, cx| {
13264 Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
13265 });
13266 editor
13267 .update(cx, |editor, _window, cx| {
13268 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
13269 editor
13270 .buffer
13271 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
13272 })
13273 .unwrap();
13274
13275 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13276 cx.run_until_parked();
13277
13278 cx.update_editor(|editor, window, cx| {
13279 editor.expand_all_diff_hunks(&Default::default(), window, cx)
13280 });
13281 cx.executor().run_until_parked();
13282
13283 // When the start of a hunk coincides with the start of its excerpt,
13284 // the hunk is expanded. When the start of a a hunk is earlier than
13285 // the start of its excerpt, the hunk is not expanded.
13286 cx.assert_state_with_diff(
13287 "
13288 ˇaaa
13289 - bbb
13290 + BBB
13291
13292 - ddd
13293 - eee
13294 + DDD
13295 + EEE
13296 fff
13297
13298 iii
13299 "
13300 .unindent(),
13301 );
13302}
13303
13304#[gpui::test]
13305async fn test_edits_around_expanded_insertion_hunks(
13306 executor: BackgroundExecutor,
13307 cx: &mut gpui::TestAppContext,
13308) {
13309 init_test(cx, |_| {});
13310
13311 let mut cx = EditorTestContext::new(cx).await;
13312
13313 let diff_base = r#"
13314 use some::mod1;
13315 use some::mod2;
13316
13317 const A: u32 = 42;
13318
13319 fn main() {
13320 println!("hello");
13321
13322 println!("world");
13323 }
13324 "#
13325 .unindent();
13326 executor.run_until_parked();
13327 cx.set_state(
13328 &r#"
13329 use some::mod1;
13330 use some::mod2;
13331
13332 const A: u32 = 42;
13333 const B: u32 = 42;
13334 const C: u32 = 42;
13335 ˇ
13336
13337 fn main() {
13338 println!("hello");
13339
13340 println!("world");
13341 }
13342 "#
13343 .unindent(),
13344 );
13345
13346 cx.set_diff_base(&diff_base);
13347 executor.run_until_parked();
13348
13349 cx.update_editor(|editor, window, cx| {
13350 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13351 });
13352 executor.run_until_parked();
13353
13354 cx.assert_state_with_diff(
13355 r#"
13356 use some::mod1;
13357 use some::mod2;
13358
13359 const A: u32 = 42;
13360 + const B: u32 = 42;
13361 + const C: u32 = 42;
13362 + ˇ
13363
13364 fn main() {
13365 println!("hello");
13366
13367 println!("world");
13368 }
13369 "#
13370 .unindent(),
13371 );
13372
13373 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
13374 executor.run_until_parked();
13375
13376 cx.assert_state_with_diff(
13377 r#"
13378 use some::mod1;
13379 use some::mod2;
13380
13381 const A: u32 = 42;
13382 + const B: u32 = 42;
13383 + const C: u32 = 42;
13384 + const D: u32 = 42;
13385 + ˇ
13386
13387 fn main() {
13388 println!("hello");
13389
13390 println!("world");
13391 }
13392 "#
13393 .unindent(),
13394 );
13395
13396 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
13397 executor.run_until_parked();
13398
13399 cx.assert_state_with_diff(
13400 r#"
13401 use some::mod1;
13402 use some::mod2;
13403
13404 const A: u32 = 42;
13405 + const B: u32 = 42;
13406 + const C: u32 = 42;
13407 + const D: u32 = 42;
13408 + const E: u32 = 42;
13409 + ˇ
13410
13411 fn main() {
13412 println!("hello");
13413
13414 println!("world");
13415 }
13416 "#
13417 .unindent(),
13418 );
13419
13420 cx.update_editor(|editor, window, cx| {
13421 editor.delete_line(&DeleteLine, window, cx);
13422 });
13423 executor.run_until_parked();
13424
13425 cx.assert_state_with_diff(
13426 r#"
13427 use some::mod1;
13428 use some::mod2;
13429
13430 const A: u32 = 42;
13431 + const B: u32 = 42;
13432 + const C: u32 = 42;
13433 + const D: u32 = 42;
13434 + const E: u32 = 42;
13435 ˇ
13436 fn main() {
13437 println!("hello");
13438
13439 println!("world");
13440 }
13441 "#
13442 .unindent(),
13443 );
13444
13445 cx.update_editor(|editor, window, cx| {
13446 editor.move_up(&MoveUp, window, cx);
13447 editor.delete_line(&DeleteLine, window, cx);
13448 editor.move_up(&MoveUp, window, cx);
13449 editor.delete_line(&DeleteLine, window, cx);
13450 editor.move_up(&MoveUp, window, cx);
13451 editor.delete_line(&DeleteLine, window, cx);
13452 });
13453 executor.run_until_parked();
13454 cx.assert_state_with_diff(
13455 r#"
13456 use some::mod1;
13457 use some::mod2;
13458
13459 const A: u32 = 42;
13460 + const B: u32 = 42;
13461 ˇ
13462 fn main() {
13463 println!("hello");
13464
13465 println!("world");
13466 }
13467 "#
13468 .unindent(),
13469 );
13470
13471 cx.update_editor(|editor, window, cx| {
13472 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
13473 editor.delete_line(&DeleteLine, window, cx);
13474 });
13475 executor.run_until_parked();
13476 cx.assert_state_with_diff(
13477 r#"
13478 ˇ
13479 fn main() {
13480 println!("hello");
13481
13482 println!("world");
13483 }
13484 "#
13485 .unindent(),
13486 );
13487}
13488
13489#[gpui::test]
13490async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
13491 init_test(cx, |_| {});
13492
13493 let mut cx = EditorTestContext::new(cx).await;
13494 cx.set_diff_base(indoc! { "
13495 one
13496 two
13497 three
13498 four
13499 five
13500 "
13501 });
13502 cx.set_state(indoc! { "
13503 one
13504 ˇthree
13505 five
13506 "});
13507 cx.run_until_parked();
13508 cx.update_editor(|editor, window, cx| {
13509 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13510 });
13511 cx.assert_state_with_diff(
13512 indoc! { "
13513 one
13514 - two
13515 ˇthree
13516 - four
13517 five
13518 "}
13519 .to_string(),
13520 );
13521 cx.update_editor(|editor, window, cx| {
13522 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13523 });
13524
13525 cx.assert_state_with_diff(
13526 indoc! { "
13527 one
13528 ˇthree
13529 five
13530 "}
13531 .to_string(),
13532 );
13533
13534 cx.set_state(indoc! { "
13535 one
13536 ˇTWO
13537 three
13538 four
13539 five
13540 "});
13541 cx.run_until_parked();
13542 cx.update_editor(|editor, window, cx| {
13543 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13544 });
13545
13546 cx.assert_state_with_diff(
13547 indoc! { "
13548 one
13549 - two
13550 + ˇTWO
13551 three
13552 four
13553 five
13554 "}
13555 .to_string(),
13556 );
13557 cx.update_editor(|editor, window, cx| {
13558 editor.move_up(&Default::default(), window, cx);
13559 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13560 });
13561 cx.assert_state_with_diff(
13562 indoc! { "
13563 one
13564 ˇTWO
13565 three
13566 four
13567 five
13568 "}
13569 .to_string(),
13570 );
13571}
13572
13573#[gpui::test]
13574async fn test_edits_around_expanded_deletion_hunks(
13575 executor: BackgroundExecutor,
13576 cx: &mut gpui::TestAppContext,
13577) {
13578 init_test(cx, |_| {});
13579
13580 let mut cx = EditorTestContext::new(cx).await;
13581
13582 let diff_base = r#"
13583 use some::mod1;
13584 use some::mod2;
13585
13586 const A: u32 = 42;
13587 const B: u32 = 42;
13588 const C: u32 = 42;
13589
13590
13591 fn main() {
13592 println!("hello");
13593
13594 println!("world");
13595 }
13596 "#
13597 .unindent();
13598 executor.run_until_parked();
13599 cx.set_state(
13600 &r#"
13601 use some::mod1;
13602 use some::mod2;
13603
13604 ˇconst B: u32 = 42;
13605 const C: u32 = 42;
13606
13607
13608 fn main() {
13609 println!("hello");
13610
13611 println!("world");
13612 }
13613 "#
13614 .unindent(),
13615 );
13616
13617 cx.set_diff_base(&diff_base);
13618 executor.run_until_parked();
13619
13620 cx.update_editor(|editor, window, cx| {
13621 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13622 });
13623 executor.run_until_parked();
13624
13625 cx.assert_state_with_diff(
13626 r#"
13627 use some::mod1;
13628 use some::mod2;
13629
13630 - const A: u32 = 42;
13631 ˇconst B: u32 = 42;
13632 const C: u32 = 42;
13633
13634
13635 fn main() {
13636 println!("hello");
13637
13638 println!("world");
13639 }
13640 "#
13641 .unindent(),
13642 );
13643
13644 cx.update_editor(|editor, window, cx| {
13645 editor.delete_line(&DeleteLine, window, cx);
13646 });
13647 executor.run_until_parked();
13648 cx.assert_state_with_diff(
13649 r#"
13650 use some::mod1;
13651 use some::mod2;
13652
13653 - const A: u32 = 42;
13654 - const B: u32 = 42;
13655 ˇconst C: u32 = 42;
13656
13657
13658 fn main() {
13659 println!("hello");
13660
13661 println!("world");
13662 }
13663 "#
13664 .unindent(),
13665 );
13666
13667 cx.update_editor(|editor, window, cx| {
13668 editor.delete_line(&DeleteLine, window, cx);
13669 });
13670 executor.run_until_parked();
13671 cx.assert_state_with_diff(
13672 r#"
13673 use some::mod1;
13674 use some::mod2;
13675
13676 - const A: u32 = 42;
13677 - const B: u32 = 42;
13678 - const C: u32 = 42;
13679 ˇ
13680
13681 fn main() {
13682 println!("hello");
13683
13684 println!("world");
13685 }
13686 "#
13687 .unindent(),
13688 );
13689
13690 cx.update_editor(|editor, window, cx| {
13691 editor.handle_input("replacement", window, cx);
13692 });
13693 executor.run_until_parked();
13694 cx.assert_state_with_diff(
13695 r#"
13696 use some::mod1;
13697 use some::mod2;
13698
13699 - const A: u32 = 42;
13700 - const B: u32 = 42;
13701 - const C: u32 = 42;
13702 -
13703 + replacementˇ
13704
13705 fn main() {
13706 println!("hello");
13707
13708 println!("world");
13709 }
13710 "#
13711 .unindent(),
13712 );
13713}
13714
13715#[gpui::test]
13716async fn test_backspace_after_deletion_hunk(
13717 executor: BackgroundExecutor,
13718 cx: &mut gpui::TestAppContext,
13719) {
13720 init_test(cx, |_| {});
13721
13722 let mut cx = EditorTestContext::new(cx).await;
13723
13724 let base_text = r#"
13725 one
13726 two
13727 three
13728 four
13729 five
13730 "#
13731 .unindent();
13732 executor.run_until_parked();
13733 cx.set_state(
13734 &r#"
13735 one
13736 two
13737 fˇour
13738 five
13739 "#
13740 .unindent(),
13741 );
13742
13743 cx.set_diff_base(&base_text);
13744 executor.run_until_parked();
13745
13746 cx.update_editor(|editor, window, cx| {
13747 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13748 });
13749 executor.run_until_parked();
13750
13751 cx.assert_state_with_diff(
13752 r#"
13753 one
13754 two
13755 - three
13756 fˇour
13757 five
13758 "#
13759 .unindent(),
13760 );
13761
13762 cx.update_editor(|editor, window, cx| {
13763 editor.backspace(&Backspace, window, cx);
13764 editor.backspace(&Backspace, window, cx);
13765 });
13766 executor.run_until_parked();
13767 cx.assert_state_with_diff(
13768 r#"
13769 one
13770 two
13771 - threeˇ
13772 - four
13773 + our
13774 five
13775 "#
13776 .unindent(),
13777 );
13778}
13779
13780#[gpui::test]
13781async fn test_edit_after_expanded_modification_hunk(
13782 executor: BackgroundExecutor,
13783 cx: &mut gpui::TestAppContext,
13784) {
13785 init_test(cx, |_| {});
13786
13787 let mut cx = EditorTestContext::new(cx).await;
13788
13789 let diff_base = r#"
13790 use some::mod1;
13791 use some::mod2;
13792
13793 const A: u32 = 42;
13794 const B: u32 = 42;
13795 const C: u32 = 42;
13796 const D: u32 = 42;
13797
13798
13799 fn main() {
13800 println!("hello");
13801
13802 println!("world");
13803 }"#
13804 .unindent();
13805
13806 cx.set_state(
13807 &r#"
13808 use some::mod1;
13809 use some::mod2;
13810
13811 const A: u32 = 42;
13812 const B: u32 = 42;
13813 const C: u32 = 43ˇ
13814 const D: u32 = 42;
13815
13816
13817 fn main() {
13818 println!("hello");
13819
13820 println!("world");
13821 }"#
13822 .unindent(),
13823 );
13824
13825 cx.set_diff_base(&diff_base);
13826 executor.run_until_parked();
13827 cx.update_editor(|editor, window, cx| {
13828 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13829 });
13830 executor.run_until_parked();
13831
13832 cx.assert_state_with_diff(
13833 r#"
13834 use some::mod1;
13835 use some::mod2;
13836
13837 const A: u32 = 42;
13838 const B: u32 = 42;
13839 - const C: u32 = 42;
13840 + const C: u32 = 43ˇ
13841 const D: u32 = 42;
13842
13843
13844 fn main() {
13845 println!("hello");
13846
13847 println!("world");
13848 }"#
13849 .unindent(),
13850 );
13851
13852 cx.update_editor(|editor, window, cx| {
13853 editor.handle_input("\nnew_line\n", window, cx);
13854 });
13855 executor.run_until_parked();
13856
13857 cx.assert_state_with_diff(
13858 r#"
13859 use some::mod1;
13860 use some::mod2;
13861
13862 const A: u32 = 42;
13863 const B: u32 = 42;
13864 - const C: u32 = 42;
13865 + const C: u32 = 43
13866 + new_line
13867 + ˇ
13868 const D: u32 = 42;
13869
13870
13871 fn main() {
13872 println!("hello");
13873
13874 println!("world");
13875 }"#
13876 .unindent(),
13877 );
13878}
13879
13880async fn setup_indent_guides_editor(
13881 text: &str,
13882 cx: &mut gpui::TestAppContext,
13883) -> (BufferId, EditorTestContext) {
13884 init_test(cx, |_| {});
13885
13886 let mut cx = EditorTestContext::new(cx).await;
13887
13888 let buffer_id = cx.update_editor(|editor, window, cx| {
13889 editor.set_text(text, window, cx);
13890 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
13891
13892 buffer_ids[0]
13893 });
13894
13895 (buffer_id, cx)
13896}
13897
13898fn assert_indent_guides(
13899 range: Range<u32>,
13900 expected: Vec<IndentGuide>,
13901 active_indices: Option<Vec<usize>>,
13902 cx: &mut EditorTestContext,
13903) {
13904 let indent_guides = cx.update_editor(|editor, window, cx| {
13905 let snapshot = editor.snapshot(window, cx).display_snapshot;
13906 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
13907 editor,
13908 MultiBufferRow(range.start)..MultiBufferRow(range.end),
13909 true,
13910 &snapshot,
13911 cx,
13912 );
13913
13914 indent_guides.sort_by(|a, b| {
13915 a.depth.cmp(&b.depth).then(
13916 a.start_row
13917 .cmp(&b.start_row)
13918 .then(a.end_row.cmp(&b.end_row)),
13919 )
13920 });
13921 indent_guides
13922 });
13923
13924 if let Some(expected) = active_indices {
13925 let active_indices = cx.update_editor(|editor, window, cx| {
13926 let snapshot = editor.snapshot(window, cx).display_snapshot;
13927 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
13928 });
13929
13930 assert_eq!(
13931 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
13932 expected,
13933 "Active indent guide indices do not match"
13934 );
13935 }
13936
13937 assert_eq!(indent_guides, expected, "Indent guides do not match");
13938}
13939
13940fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
13941 IndentGuide {
13942 buffer_id,
13943 start_row: MultiBufferRow(start_row),
13944 end_row: MultiBufferRow(end_row),
13945 depth,
13946 tab_size: 4,
13947 settings: IndentGuideSettings {
13948 enabled: true,
13949 line_width: 1,
13950 active_line_width: 1,
13951 ..Default::default()
13952 },
13953 }
13954}
13955
13956#[gpui::test]
13957async fn test_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
13958 let (buffer_id, mut cx) = setup_indent_guides_editor(
13959 &"
13960 fn main() {
13961 let a = 1;
13962 }"
13963 .unindent(),
13964 cx,
13965 )
13966 .await;
13967
13968 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
13969}
13970
13971#[gpui::test]
13972async fn test_indent_guide_simple_block(cx: &mut gpui::TestAppContext) {
13973 let (buffer_id, mut cx) = setup_indent_guides_editor(
13974 &"
13975 fn main() {
13976 let a = 1;
13977 let b = 2;
13978 }"
13979 .unindent(),
13980 cx,
13981 )
13982 .await;
13983
13984 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
13985}
13986
13987#[gpui::test]
13988async fn test_indent_guide_nested(cx: &mut gpui::TestAppContext) {
13989 let (buffer_id, mut cx) = setup_indent_guides_editor(
13990 &"
13991 fn main() {
13992 let a = 1;
13993 if a == 3 {
13994 let b = 2;
13995 } else {
13996 let c = 3;
13997 }
13998 }"
13999 .unindent(),
14000 cx,
14001 )
14002 .await;
14003
14004 assert_indent_guides(
14005 0..8,
14006 vec![
14007 indent_guide(buffer_id, 1, 6, 0),
14008 indent_guide(buffer_id, 3, 3, 1),
14009 indent_guide(buffer_id, 5, 5, 1),
14010 ],
14011 None,
14012 &mut cx,
14013 );
14014}
14015
14016#[gpui::test]
14017async fn test_indent_guide_tab(cx: &mut gpui::TestAppContext) {
14018 let (buffer_id, mut cx) = setup_indent_guides_editor(
14019 &"
14020 fn main() {
14021 let a = 1;
14022 let b = 2;
14023 let c = 3;
14024 }"
14025 .unindent(),
14026 cx,
14027 )
14028 .await;
14029
14030 assert_indent_guides(
14031 0..5,
14032 vec![
14033 indent_guide(buffer_id, 1, 3, 0),
14034 indent_guide(buffer_id, 2, 2, 1),
14035 ],
14036 None,
14037 &mut cx,
14038 );
14039}
14040
14041#[gpui::test]
14042async fn test_indent_guide_continues_on_empty_line(cx: &mut gpui::TestAppContext) {
14043 let (buffer_id, mut cx) = setup_indent_guides_editor(
14044 &"
14045 fn main() {
14046 let a = 1;
14047
14048 let c = 3;
14049 }"
14050 .unindent(),
14051 cx,
14052 )
14053 .await;
14054
14055 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
14056}
14057
14058#[gpui::test]
14059async fn test_indent_guide_complex(cx: &mut gpui::TestAppContext) {
14060 let (buffer_id, mut cx) = setup_indent_guides_editor(
14061 &"
14062 fn main() {
14063 let a = 1;
14064
14065 let c = 3;
14066
14067 if a == 3 {
14068 let b = 2;
14069 } else {
14070 let c = 3;
14071 }
14072 }"
14073 .unindent(),
14074 cx,
14075 )
14076 .await;
14077
14078 assert_indent_guides(
14079 0..11,
14080 vec![
14081 indent_guide(buffer_id, 1, 9, 0),
14082 indent_guide(buffer_id, 6, 6, 1),
14083 indent_guide(buffer_id, 8, 8, 1),
14084 ],
14085 None,
14086 &mut cx,
14087 );
14088}
14089
14090#[gpui::test]
14091async fn test_indent_guide_starts_off_screen(cx: &mut gpui::TestAppContext) {
14092 let (buffer_id, mut cx) = setup_indent_guides_editor(
14093 &"
14094 fn main() {
14095 let a = 1;
14096
14097 let c = 3;
14098
14099 if a == 3 {
14100 let b = 2;
14101 } else {
14102 let c = 3;
14103 }
14104 }"
14105 .unindent(),
14106 cx,
14107 )
14108 .await;
14109
14110 assert_indent_guides(
14111 1..11,
14112 vec![
14113 indent_guide(buffer_id, 1, 9, 0),
14114 indent_guide(buffer_id, 6, 6, 1),
14115 indent_guide(buffer_id, 8, 8, 1),
14116 ],
14117 None,
14118 &mut cx,
14119 );
14120}
14121
14122#[gpui::test]
14123async fn test_indent_guide_ends_off_screen(cx: &mut gpui::TestAppContext) {
14124 let (buffer_id, mut cx) = setup_indent_guides_editor(
14125 &"
14126 fn main() {
14127 let a = 1;
14128
14129 let c = 3;
14130
14131 if a == 3 {
14132 let b = 2;
14133 } else {
14134 let c = 3;
14135 }
14136 }"
14137 .unindent(),
14138 cx,
14139 )
14140 .await;
14141
14142 assert_indent_guides(
14143 1..10,
14144 vec![
14145 indent_guide(buffer_id, 1, 9, 0),
14146 indent_guide(buffer_id, 6, 6, 1),
14147 indent_guide(buffer_id, 8, 8, 1),
14148 ],
14149 None,
14150 &mut cx,
14151 );
14152}
14153
14154#[gpui::test]
14155async fn test_indent_guide_without_brackets(cx: &mut gpui::TestAppContext) {
14156 let (buffer_id, mut cx) = setup_indent_guides_editor(
14157 &"
14158 block1
14159 block2
14160 block3
14161 block4
14162 block2
14163 block1
14164 block1"
14165 .unindent(),
14166 cx,
14167 )
14168 .await;
14169
14170 assert_indent_guides(
14171 1..10,
14172 vec![
14173 indent_guide(buffer_id, 1, 4, 0),
14174 indent_guide(buffer_id, 2, 3, 1),
14175 indent_guide(buffer_id, 3, 3, 2),
14176 ],
14177 None,
14178 &mut cx,
14179 );
14180}
14181
14182#[gpui::test]
14183async fn test_indent_guide_ends_before_empty_line(cx: &mut gpui::TestAppContext) {
14184 let (buffer_id, mut cx) = setup_indent_guides_editor(
14185 &"
14186 block1
14187 block2
14188 block3
14189
14190 block1
14191 block1"
14192 .unindent(),
14193 cx,
14194 )
14195 .await;
14196
14197 assert_indent_guides(
14198 0..6,
14199 vec![
14200 indent_guide(buffer_id, 1, 2, 0),
14201 indent_guide(buffer_id, 2, 2, 1),
14202 ],
14203 None,
14204 &mut cx,
14205 );
14206}
14207
14208#[gpui::test]
14209async fn test_indent_guide_continuing_off_screen(cx: &mut gpui::TestAppContext) {
14210 let (buffer_id, mut cx) = setup_indent_guides_editor(
14211 &"
14212 block1
14213
14214
14215
14216 block2
14217 "
14218 .unindent(),
14219 cx,
14220 )
14221 .await;
14222
14223 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
14224}
14225
14226#[gpui::test]
14227async fn test_indent_guide_tabs(cx: &mut gpui::TestAppContext) {
14228 let (buffer_id, mut cx) = setup_indent_guides_editor(
14229 &"
14230 def a:
14231 \tb = 3
14232 \tif True:
14233 \t\tc = 4
14234 \t\td = 5
14235 \tprint(b)
14236 "
14237 .unindent(),
14238 cx,
14239 )
14240 .await;
14241
14242 assert_indent_guides(
14243 0..6,
14244 vec![
14245 indent_guide(buffer_id, 1, 6, 0),
14246 indent_guide(buffer_id, 3, 4, 1),
14247 ],
14248 None,
14249 &mut cx,
14250 );
14251}
14252
14253#[gpui::test]
14254async fn test_active_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
14255 let (buffer_id, mut cx) = setup_indent_guides_editor(
14256 &"
14257 fn main() {
14258 let a = 1;
14259 }"
14260 .unindent(),
14261 cx,
14262 )
14263 .await;
14264
14265 cx.update_editor(|editor, window, cx| {
14266 editor.change_selections(None, window, cx, |s| {
14267 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14268 });
14269 });
14270
14271 assert_indent_guides(
14272 0..3,
14273 vec![indent_guide(buffer_id, 1, 1, 0)],
14274 Some(vec![0]),
14275 &mut cx,
14276 );
14277}
14278
14279#[gpui::test]
14280async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppContext) {
14281 let (buffer_id, mut cx) = setup_indent_guides_editor(
14282 &"
14283 fn main() {
14284 if 1 == 2 {
14285 let a = 1;
14286 }
14287 }"
14288 .unindent(),
14289 cx,
14290 )
14291 .await;
14292
14293 cx.update_editor(|editor, window, cx| {
14294 editor.change_selections(None, window, cx, |s| {
14295 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14296 });
14297 });
14298
14299 assert_indent_guides(
14300 0..4,
14301 vec![
14302 indent_guide(buffer_id, 1, 3, 0),
14303 indent_guide(buffer_id, 2, 2, 1),
14304 ],
14305 Some(vec![1]),
14306 &mut cx,
14307 );
14308
14309 cx.update_editor(|editor, window, cx| {
14310 editor.change_selections(None, window, cx, |s| {
14311 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
14312 });
14313 });
14314
14315 assert_indent_guides(
14316 0..4,
14317 vec![
14318 indent_guide(buffer_id, 1, 3, 0),
14319 indent_guide(buffer_id, 2, 2, 1),
14320 ],
14321 Some(vec![1]),
14322 &mut cx,
14323 );
14324
14325 cx.update_editor(|editor, window, cx| {
14326 editor.change_selections(None, window, cx, |s| {
14327 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
14328 });
14329 });
14330
14331 assert_indent_guides(
14332 0..4,
14333 vec![
14334 indent_guide(buffer_id, 1, 3, 0),
14335 indent_guide(buffer_id, 2, 2, 1),
14336 ],
14337 Some(vec![0]),
14338 &mut cx,
14339 );
14340}
14341
14342#[gpui::test]
14343async fn test_active_indent_guide_empty_line(cx: &mut gpui::TestAppContext) {
14344 let (buffer_id, mut cx) = setup_indent_guides_editor(
14345 &"
14346 fn main() {
14347 let a = 1;
14348
14349 let b = 2;
14350 }"
14351 .unindent(),
14352 cx,
14353 )
14354 .await;
14355
14356 cx.update_editor(|editor, window, cx| {
14357 editor.change_selections(None, window, cx, |s| {
14358 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
14359 });
14360 });
14361
14362 assert_indent_guides(
14363 0..5,
14364 vec![indent_guide(buffer_id, 1, 3, 0)],
14365 Some(vec![0]),
14366 &mut cx,
14367 );
14368}
14369
14370#[gpui::test]
14371async fn test_active_indent_guide_non_matching_indent(cx: &mut gpui::TestAppContext) {
14372 let (buffer_id, mut cx) = setup_indent_guides_editor(
14373 &"
14374 def m:
14375 a = 1
14376 pass"
14377 .unindent(),
14378 cx,
14379 )
14380 .await;
14381
14382 cx.update_editor(|editor, window, cx| {
14383 editor.change_selections(None, window, cx, |s| {
14384 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14385 });
14386 });
14387
14388 assert_indent_guides(
14389 0..3,
14390 vec![indent_guide(buffer_id, 1, 2, 0)],
14391 Some(vec![0]),
14392 &mut cx,
14393 );
14394}
14395
14396#[gpui::test]
14397async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut gpui::TestAppContext) {
14398 init_test(cx, |_| {});
14399 let mut cx = EditorTestContext::new(cx).await;
14400 let text = indoc! {
14401 "
14402 impl A {
14403 fn b() {
14404 0;
14405 3;
14406 5;
14407 6;
14408 7;
14409 }
14410 }
14411 "
14412 };
14413 let base_text = indoc! {
14414 "
14415 impl A {
14416 fn b() {
14417 0;
14418 1;
14419 2;
14420 3;
14421 4;
14422 }
14423 fn c() {
14424 5;
14425 6;
14426 7;
14427 }
14428 }
14429 "
14430 };
14431
14432 cx.update_editor(|editor, window, cx| {
14433 editor.set_text(text, window, cx);
14434
14435 editor.buffer().update(cx, |multibuffer, cx| {
14436 let buffer = multibuffer.as_singleton().unwrap();
14437 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
14438
14439 multibuffer.set_all_diff_hunks_expanded(cx);
14440 multibuffer.add_diff(diff, cx);
14441
14442 buffer.read(cx).remote_id()
14443 })
14444 });
14445 cx.run_until_parked();
14446
14447 cx.assert_state_with_diff(
14448 indoc! { "
14449 impl A {
14450 fn b() {
14451 0;
14452 - 1;
14453 - 2;
14454 3;
14455 - 4;
14456 - }
14457 - fn c() {
14458 5;
14459 6;
14460 7;
14461 }
14462 }
14463 ˇ"
14464 }
14465 .to_string(),
14466 );
14467
14468 let mut actual_guides = cx.update_editor(|editor, window, cx| {
14469 editor
14470 .snapshot(window, cx)
14471 .buffer_snapshot
14472 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
14473 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
14474 .collect::<Vec<_>>()
14475 });
14476 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
14477 assert_eq!(
14478 actual_guides,
14479 vec![
14480 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
14481 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
14482 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
14483 ]
14484 );
14485}
14486
14487#[gpui::test]
14488fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
14489 init_test(cx, |_| {});
14490
14491 let editor = cx.add_window(|window, cx| {
14492 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
14493 build_editor(buffer, window, cx)
14494 });
14495
14496 let render_args = Arc::new(Mutex::new(None));
14497 let snapshot = editor
14498 .update(cx, |editor, window, cx| {
14499 let snapshot = editor.buffer().read(cx).snapshot(cx);
14500 let range =
14501 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
14502
14503 struct RenderArgs {
14504 row: MultiBufferRow,
14505 folded: bool,
14506 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
14507 }
14508
14509 let crease = Crease::inline(
14510 range,
14511 FoldPlaceholder::test(),
14512 {
14513 let toggle_callback = render_args.clone();
14514 move |row, folded, callback, _window, _cx| {
14515 *toggle_callback.lock() = Some(RenderArgs {
14516 row,
14517 folded,
14518 callback,
14519 });
14520 div()
14521 }
14522 },
14523 |_row, _folded, _window, _cx| div(),
14524 );
14525
14526 editor.insert_creases(Some(crease), cx);
14527 let snapshot = editor.snapshot(window, cx);
14528 let _div = snapshot.render_crease_toggle(
14529 MultiBufferRow(1),
14530 false,
14531 cx.entity().clone(),
14532 window,
14533 cx,
14534 );
14535 snapshot
14536 })
14537 .unwrap();
14538
14539 let render_args = render_args.lock().take().unwrap();
14540 assert_eq!(render_args.row, MultiBufferRow(1));
14541 assert!(!render_args.folded);
14542 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
14543
14544 cx.update_window(*editor, |_, window, cx| {
14545 (render_args.callback)(true, window, cx)
14546 })
14547 .unwrap();
14548 let snapshot = editor
14549 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
14550 .unwrap();
14551 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
14552
14553 cx.update_window(*editor, |_, window, cx| {
14554 (render_args.callback)(false, window, cx)
14555 })
14556 .unwrap();
14557 let snapshot = editor
14558 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
14559 .unwrap();
14560 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
14561}
14562
14563#[gpui::test]
14564async fn test_input_text(cx: &mut gpui::TestAppContext) {
14565 init_test(cx, |_| {});
14566 let mut cx = EditorTestContext::new(cx).await;
14567
14568 cx.set_state(
14569 &r#"ˇone
14570 two
14571
14572 three
14573 fourˇ
14574 five
14575
14576 siˇx"#
14577 .unindent(),
14578 );
14579
14580 cx.dispatch_action(HandleInput(String::new()));
14581 cx.assert_editor_state(
14582 &r#"ˇone
14583 two
14584
14585 three
14586 fourˇ
14587 five
14588
14589 siˇx"#
14590 .unindent(),
14591 );
14592
14593 cx.dispatch_action(HandleInput("AAAA".to_string()));
14594 cx.assert_editor_state(
14595 &r#"AAAAˇone
14596 two
14597
14598 three
14599 fourAAAAˇ
14600 five
14601
14602 siAAAAˇx"#
14603 .unindent(),
14604 );
14605}
14606
14607#[gpui::test]
14608async fn test_scroll_cursor_center_top_bottom(cx: &mut gpui::TestAppContext) {
14609 init_test(cx, |_| {});
14610
14611 let mut cx = EditorTestContext::new(cx).await;
14612 cx.set_state(
14613 r#"let foo = 1;
14614let foo = 2;
14615let foo = 3;
14616let fooˇ = 4;
14617let foo = 5;
14618let foo = 6;
14619let foo = 7;
14620let foo = 8;
14621let foo = 9;
14622let foo = 10;
14623let foo = 11;
14624let foo = 12;
14625let foo = 13;
14626let foo = 14;
14627let foo = 15;"#,
14628 );
14629
14630 cx.update_editor(|e, window, cx| {
14631 assert_eq!(
14632 e.next_scroll_position,
14633 NextScrollCursorCenterTopBottom::Center,
14634 "Default next scroll direction is center",
14635 );
14636
14637 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14638 assert_eq!(
14639 e.next_scroll_position,
14640 NextScrollCursorCenterTopBottom::Top,
14641 "After center, next scroll direction should be top",
14642 );
14643
14644 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14645 assert_eq!(
14646 e.next_scroll_position,
14647 NextScrollCursorCenterTopBottom::Bottom,
14648 "After top, next scroll direction should be bottom",
14649 );
14650
14651 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14652 assert_eq!(
14653 e.next_scroll_position,
14654 NextScrollCursorCenterTopBottom::Center,
14655 "After bottom, scrolling should start over",
14656 );
14657
14658 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14659 assert_eq!(
14660 e.next_scroll_position,
14661 NextScrollCursorCenterTopBottom::Top,
14662 "Scrolling continues if retriggered fast enough"
14663 );
14664 });
14665
14666 cx.executor()
14667 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
14668 cx.executor().run_until_parked();
14669 cx.update_editor(|e, _, _| {
14670 assert_eq!(
14671 e.next_scroll_position,
14672 NextScrollCursorCenterTopBottom::Center,
14673 "If scrolling is not triggered fast enough, it should reset"
14674 );
14675 });
14676}
14677
14678#[gpui::test]
14679async fn test_goto_definition_with_find_all_references_fallback(cx: &mut gpui::TestAppContext) {
14680 init_test(cx, |_| {});
14681 let mut cx = EditorLspTestContext::new_rust(
14682 lsp::ServerCapabilities {
14683 definition_provider: Some(lsp::OneOf::Left(true)),
14684 references_provider: Some(lsp::OneOf::Left(true)),
14685 ..lsp::ServerCapabilities::default()
14686 },
14687 cx,
14688 )
14689 .await;
14690
14691 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
14692 let go_to_definition = cx.lsp.handle_request::<lsp::request::GotoDefinition, _, _>(
14693 move |params, _| async move {
14694 if empty_go_to_definition {
14695 Ok(None)
14696 } else {
14697 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
14698 uri: params.text_document_position_params.text_document.uri,
14699 range: lsp::Range::new(lsp::Position::new(4, 3), lsp::Position::new(4, 6)),
14700 })))
14701 }
14702 },
14703 );
14704 let references =
14705 cx.lsp
14706 .handle_request::<lsp::request::References, _, _>(move |params, _| async move {
14707 Ok(Some(vec![lsp::Location {
14708 uri: params.text_document_position.text_document.uri,
14709 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
14710 }]))
14711 });
14712 (go_to_definition, references)
14713 };
14714
14715 cx.set_state(
14716 &r#"fn one() {
14717 let mut a = ˇtwo();
14718 }
14719
14720 fn two() {}"#
14721 .unindent(),
14722 );
14723 set_up_lsp_handlers(false, &mut cx);
14724 let navigated = cx
14725 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
14726 .await
14727 .expect("Failed to navigate to definition");
14728 assert_eq!(
14729 navigated,
14730 Navigated::Yes,
14731 "Should have navigated to definition from the GetDefinition response"
14732 );
14733 cx.assert_editor_state(
14734 &r#"fn one() {
14735 let mut a = two();
14736 }
14737
14738 fn «twoˇ»() {}"#
14739 .unindent(),
14740 );
14741
14742 let editors = cx.update_workspace(|workspace, _, cx| {
14743 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
14744 });
14745 cx.update_editor(|_, _, test_editor_cx| {
14746 assert_eq!(
14747 editors.len(),
14748 1,
14749 "Initially, only one, test, editor should be open in the workspace"
14750 );
14751 assert_eq!(
14752 test_editor_cx.entity(),
14753 editors.last().expect("Asserted len is 1").clone()
14754 );
14755 });
14756
14757 set_up_lsp_handlers(true, &mut cx);
14758 let navigated = cx
14759 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
14760 .await
14761 .expect("Failed to navigate to lookup references");
14762 assert_eq!(
14763 navigated,
14764 Navigated::Yes,
14765 "Should have navigated to references as a fallback after empty GoToDefinition response"
14766 );
14767 // We should not change the selections in the existing file,
14768 // if opening another milti buffer with the references
14769 cx.assert_editor_state(
14770 &r#"fn one() {
14771 let mut a = two();
14772 }
14773
14774 fn «twoˇ»() {}"#
14775 .unindent(),
14776 );
14777 let editors = cx.update_workspace(|workspace, _, cx| {
14778 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
14779 });
14780 cx.update_editor(|_, _, test_editor_cx| {
14781 assert_eq!(
14782 editors.len(),
14783 2,
14784 "After falling back to references search, we open a new editor with the results"
14785 );
14786 let references_fallback_text = editors
14787 .into_iter()
14788 .find(|new_editor| *new_editor != test_editor_cx.entity())
14789 .expect("Should have one non-test editor now")
14790 .read(test_editor_cx)
14791 .text(test_editor_cx);
14792 assert_eq!(
14793 references_fallback_text, "fn one() {\n let mut a = two();\n}",
14794 "Should use the range from the references response and not the GoToDefinition one"
14795 );
14796 });
14797}
14798
14799#[gpui::test]
14800async fn test_find_enclosing_node_with_task(cx: &mut gpui::TestAppContext) {
14801 init_test(cx, |_| {});
14802
14803 let language = Arc::new(Language::new(
14804 LanguageConfig::default(),
14805 Some(tree_sitter_rust::LANGUAGE.into()),
14806 ));
14807
14808 let text = r#"
14809 #[cfg(test)]
14810 mod tests() {
14811 #[test]
14812 fn runnable_1() {
14813 let a = 1;
14814 }
14815
14816 #[test]
14817 fn runnable_2() {
14818 let a = 1;
14819 let b = 2;
14820 }
14821 }
14822 "#
14823 .unindent();
14824
14825 let fs = FakeFs::new(cx.executor());
14826 fs.insert_file("/file.rs", Default::default()).await;
14827
14828 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14829 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14830 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14831 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14832 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
14833
14834 let editor = cx.new_window_entity(|window, cx| {
14835 Editor::new(
14836 EditorMode::Full,
14837 multi_buffer,
14838 Some(project.clone()),
14839 true,
14840 window,
14841 cx,
14842 )
14843 });
14844
14845 editor.update_in(cx, |editor, window, cx| {
14846 editor.tasks.insert(
14847 (buffer.read(cx).remote_id(), 3),
14848 RunnableTasks {
14849 templates: vec![],
14850 offset: MultiBufferOffset(43),
14851 column: 0,
14852 extra_variables: HashMap::default(),
14853 context_range: BufferOffset(43)..BufferOffset(85),
14854 },
14855 );
14856 editor.tasks.insert(
14857 (buffer.read(cx).remote_id(), 8),
14858 RunnableTasks {
14859 templates: vec![],
14860 offset: MultiBufferOffset(86),
14861 column: 0,
14862 extra_variables: HashMap::default(),
14863 context_range: BufferOffset(86)..BufferOffset(191),
14864 },
14865 );
14866
14867 // Test finding task when cursor is inside function body
14868 editor.change_selections(None, window, cx, |s| {
14869 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
14870 });
14871 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
14872 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
14873
14874 // Test finding task when cursor is on function name
14875 editor.change_selections(None, window, cx, |s| {
14876 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
14877 });
14878 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
14879 assert_eq!(row, 8, "Should find task when cursor is on function name");
14880 });
14881}
14882
14883#[gpui::test]
14884async fn test_multi_buffer_folding(cx: &mut gpui::TestAppContext) {
14885 init_test(cx, |_| {});
14886
14887 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
14888 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
14889 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
14890
14891 let fs = FakeFs::new(cx.executor());
14892 fs.insert_tree(
14893 path!("/a"),
14894 json!({
14895 "first.rs": sample_text_1,
14896 "second.rs": sample_text_2,
14897 "third.rs": sample_text_3,
14898 }),
14899 )
14900 .await;
14901 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14902 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14903 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14904 let worktree = project.update(cx, |project, cx| {
14905 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
14906 assert_eq!(worktrees.len(), 1);
14907 worktrees.pop().unwrap()
14908 });
14909 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
14910
14911 let buffer_1 = project
14912 .update(cx, |project, cx| {
14913 project.open_buffer((worktree_id, "first.rs"), cx)
14914 })
14915 .await
14916 .unwrap();
14917 let buffer_2 = project
14918 .update(cx, |project, cx| {
14919 project.open_buffer((worktree_id, "second.rs"), cx)
14920 })
14921 .await
14922 .unwrap();
14923 let buffer_3 = project
14924 .update(cx, |project, cx| {
14925 project.open_buffer((worktree_id, "third.rs"), cx)
14926 })
14927 .await
14928 .unwrap();
14929
14930 let multi_buffer = cx.new(|cx| {
14931 let mut multi_buffer = MultiBuffer::new(ReadWrite);
14932 multi_buffer.push_excerpts(
14933 buffer_1.clone(),
14934 [
14935 ExcerptRange {
14936 context: Point::new(0, 0)..Point::new(3, 0),
14937 primary: None,
14938 },
14939 ExcerptRange {
14940 context: Point::new(5, 0)..Point::new(7, 0),
14941 primary: None,
14942 },
14943 ExcerptRange {
14944 context: Point::new(9, 0)..Point::new(10, 4),
14945 primary: None,
14946 },
14947 ],
14948 cx,
14949 );
14950 multi_buffer.push_excerpts(
14951 buffer_2.clone(),
14952 [
14953 ExcerptRange {
14954 context: Point::new(0, 0)..Point::new(3, 0),
14955 primary: None,
14956 },
14957 ExcerptRange {
14958 context: Point::new(5, 0)..Point::new(7, 0),
14959 primary: None,
14960 },
14961 ExcerptRange {
14962 context: Point::new(9, 0)..Point::new(10, 4),
14963 primary: None,
14964 },
14965 ],
14966 cx,
14967 );
14968 multi_buffer.push_excerpts(
14969 buffer_3.clone(),
14970 [
14971 ExcerptRange {
14972 context: Point::new(0, 0)..Point::new(3, 0),
14973 primary: None,
14974 },
14975 ExcerptRange {
14976 context: Point::new(5, 0)..Point::new(7, 0),
14977 primary: None,
14978 },
14979 ExcerptRange {
14980 context: Point::new(9, 0)..Point::new(10, 4),
14981 primary: None,
14982 },
14983 ],
14984 cx,
14985 );
14986 multi_buffer
14987 });
14988 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
14989 Editor::new(
14990 EditorMode::Full,
14991 multi_buffer,
14992 Some(project.clone()),
14993 true,
14994 window,
14995 cx,
14996 )
14997 });
14998
14999 let full_text = "\n\n\naaaa\nbbbb\ncccc\n\n\n\nffff\ngggg\n\n\n\njjjj\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n";
15000 assert_eq!(
15001 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15002 full_text,
15003 );
15004
15005 multi_buffer_editor.update(cx, |editor, cx| {
15006 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
15007 });
15008 assert_eq!(
15009 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15010 "\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
15011 "After folding the first buffer, its text should not be displayed"
15012 );
15013
15014 multi_buffer_editor.update(cx, |editor, cx| {
15015 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
15016 });
15017 assert_eq!(
15018 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15019 "\n\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
15020 "After folding the second buffer, its text should not be displayed"
15021 );
15022
15023 multi_buffer_editor.update(cx, |editor, cx| {
15024 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
15025 });
15026 assert_eq!(
15027 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15028 "\n\n\n\n\n",
15029 "After folding the third buffer, its text should not be displayed"
15030 );
15031
15032 // Emulate selection inside the fold logic, that should work
15033 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15034 editor
15035 .snapshot(window, cx)
15036 .next_line_boundary(Point::new(0, 4));
15037 });
15038
15039 multi_buffer_editor.update(cx, |editor, cx| {
15040 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
15041 });
15042 assert_eq!(
15043 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15044 "\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
15045 "After unfolding the second buffer, its text should be displayed"
15046 );
15047
15048 multi_buffer_editor.update(cx, |editor, cx| {
15049 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
15050 });
15051 assert_eq!(
15052 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15053 "\n\n\naaaa\nbbbb\ncccc\n\n\n\nffff\ngggg\n\n\n\njjjj\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
15054 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
15055 );
15056
15057 multi_buffer_editor.update(cx, |editor, cx| {
15058 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
15059 });
15060 assert_eq!(
15061 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15062 full_text,
15063 "After unfolding the all buffers, all original text should be displayed"
15064 );
15065}
15066
15067#[gpui::test]
15068async fn test_multi_buffer_single_excerpts_folding(cx: &mut gpui::TestAppContext) {
15069 init_test(cx, |_| {});
15070
15071 let sample_text_1 = "1111\n2222\n3333".to_string();
15072 let sample_text_2 = "4444\n5555\n6666".to_string();
15073 let sample_text_3 = "7777\n8888\n9999".to_string();
15074
15075 let fs = FakeFs::new(cx.executor());
15076 fs.insert_tree(
15077 path!("/a"),
15078 json!({
15079 "first.rs": sample_text_1,
15080 "second.rs": sample_text_2,
15081 "third.rs": sample_text_3,
15082 }),
15083 )
15084 .await;
15085 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15086 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15087 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15088 let worktree = project.update(cx, |project, cx| {
15089 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15090 assert_eq!(worktrees.len(), 1);
15091 worktrees.pop().unwrap()
15092 });
15093 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15094
15095 let buffer_1 = project
15096 .update(cx, |project, cx| {
15097 project.open_buffer((worktree_id, "first.rs"), cx)
15098 })
15099 .await
15100 .unwrap();
15101 let buffer_2 = project
15102 .update(cx, |project, cx| {
15103 project.open_buffer((worktree_id, "second.rs"), cx)
15104 })
15105 .await
15106 .unwrap();
15107 let buffer_3 = project
15108 .update(cx, |project, cx| {
15109 project.open_buffer((worktree_id, "third.rs"), cx)
15110 })
15111 .await
15112 .unwrap();
15113
15114 let multi_buffer = cx.new(|cx| {
15115 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15116 multi_buffer.push_excerpts(
15117 buffer_1.clone(),
15118 [ExcerptRange {
15119 context: Point::new(0, 0)..Point::new(3, 0),
15120 primary: None,
15121 }],
15122 cx,
15123 );
15124 multi_buffer.push_excerpts(
15125 buffer_2.clone(),
15126 [ExcerptRange {
15127 context: Point::new(0, 0)..Point::new(3, 0),
15128 primary: None,
15129 }],
15130 cx,
15131 );
15132 multi_buffer.push_excerpts(
15133 buffer_3.clone(),
15134 [ExcerptRange {
15135 context: Point::new(0, 0)..Point::new(3, 0),
15136 primary: None,
15137 }],
15138 cx,
15139 );
15140 multi_buffer
15141 });
15142
15143 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15144 Editor::new(
15145 EditorMode::Full,
15146 multi_buffer,
15147 Some(project.clone()),
15148 true,
15149 window,
15150 cx,
15151 )
15152 });
15153
15154 let full_text = "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n";
15155 assert_eq!(
15156 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15157 full_text,
15158 );
15159
15160 multi_buffer_editor.update(cx, |editor, cx| {
15161 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
15162 });
15163 assert_eq!(
15164 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15165 "\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n",
15166 "After folding the first buffer, its text should not be displayed"
15167 );
15168
15169 multi_buffer_editor.update(cx, |editor, cx| {
15170 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
15171 });
15172
15173 assert_eq!(
15174 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15175 "\n\n\n\n\n\n\n7777\n8888\n9999\n",
15176 "After folding the second buffer, its text should not be displayed"
15177 );
15178
15179 multi_buffer_editor.update(cx, |editor, cx| {
15180 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
15181 });
15182 assert_eq!(
15183 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15184 "\n\n\n\n\n",
15185 "After folding the third buffer, its text should not be displayed"
15186 );
15187
15188 multi_buffer_editor.update(cx, |editor, cx| {
15189 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
15190 });
15191 assert_eq!(
15192 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15193 "\n\n\n\n\n4444\n5555\n6666\n\n\n",
15194 "After unfolding the second buffer, its text should be displayed"
15195 );
15196
15197 multi_buffer_editor.update(cx, |editor, cx| {
15198 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
15199 });
15200 assert_eq!(
15201 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15202 "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n",
15203 "After unfolding the first buffer, its text should be displayed"
15204 );
15205
15206 multi_buffer_editor.update(cx, |editor, cx| {
15207 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
15208 });
15209 assert_eq!(
15210 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15211 full_text,
15212 "After unfolding all buffers, all original text should be displayed"
15213 );
15214}
15215
15216#[gpui::test]
15217async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppContext) {
15218 init_test(cx, |_| {});
15219
15220 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
15221
15222 let fs = FakeFs::new(cx.executor());
15223 fs.insert_tree(
15224 path!("/a"),
15225 json!({
15226 "main.rs": sample_text,
15227 }),
15228 )
15229 .await;
15230 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15231 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15232 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15233 let worktree = project.update(cx, |project, cx| {
15234 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15235 assert_eq!(worktrees.len(), 1);
15236 worktrees.pop().unwrap()
15237 });
15238 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15239
15240 let buffer_1 = project
15241 .update(cx, |project, cx| {
15242 project.open_buffer((worktree_id, "main.rs"), cx)
15243 })
15244 .await
15245 .unwrap();
15246
15247 let multi_buffer = cx.new(|cx| {
15248 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15249 multi_buffer.push_excerpts(
15250 buffer_1.clone(),
15251 [ExcerptRange {
15252 context: Point::new(0, 0)
15253 ..Point::new(
15254 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
15255 0,
15256 ),
15257 primary: None,
15258 }],
15259 cx,
15260 );
15261 multi_buffer
15262 });
15263 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15264 Editor::new(
15265 EditorMode::Full,
15266 multi_buffer,
15267 Some(project.clone()),
15268 true,
15269 window,
15270 cx,
15271 )
15272 });
15273
15274 let selection_range = Point::new(1, 0)..Point::new(2, 0);
15275 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15276 enum TestHighlight {}
15277 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
15278 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
15279 editor.highlight_text::<TestHighlight>(
15280 vec![highlight_range.clone()],
15281 HighlightStyle::color(Hsla::green()),
15282 cx,
15283 );
15284 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
15285 });
15286
15287 let full_text = format!("\n\n\n{sample_text}\n");
15288 assert_eq!(
15289 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15290 full_text,
15291 );
15292}
15293
15294#[gpui::test]
15295async fn test_inline_completion_text(cx: &mut TestAppContext) {
15296 init_test(cx, |_| {});
15297
15298 // Simple insertion
15299 assert_highlighted_edits(
15300 "Hello, world!",
15301 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
15302 true,
15303 cx,
15304 |highlighted_edits, cx| {
15305 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
15306 assert_eq!(highlighted_edits.highlights.len(), 1);
15307 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
15308 assert_eq!(
15309 highlighted_edits.highlights[0].1.background_color,
15310 Some(cx.theme().status().created_background)
15311 );
15312 },
15313 )
15314 .await;
15315
15316 // Replacement
15317 assert_highlighted_edits(
15318 "This is a test.",
15319 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
15320 false,
15321 cx,
15322 |highlighted_edits, cx| {
15323 assert_eq!(highlighted_edits.text, "That is a test.");
15324 assert_eq!(highlighted_edits.highlights.len(), 1);
15325 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
15326 assert_eq!(
15327 highlighted_edits.highlights[0].1.background_color,
15328 Some(cx.theme().status().created_background)
15329 );
15330 },
15331 )
15332 .await;
15333
15334 // Multiple edits
15335 assert_highlighted_edits(
15336 "Hello, world!",
15337 vec![
15338 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
15339 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
15340 ],
15341 false,
15342 cx,
15343 |highlighted_edits, cx| {
15344 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
15345 assert_eq!(highlighted_edits.highlights.len(), 2);
15346 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
15347 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
15348 assert_eq!(
15349 highlighted_edits.highlights[0].1.background_color,
15350 Some(cx.theme().status().created_background)
15351 );
15352 assert_eq!(
15353 highlighted_edits.highlights[1].1.background_color,
15354 Some(cx.theme().status().created_background)
15355 );
15356 },
15357 )
15358 .await;
15359
15360 // Multiple lines with edits
15361 assert_highlighted_edits(
15362 "First line\nSecond line\nThird line\nFourth line",
15363 vec![
15364 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
15365 (
15366 Point::new(2, 0)..Point::new(2, 10),
15367 "New third line".to_string(),
15368 ),
15369 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
15370 ],
15371 false,
15372 cx,
15373 |highlighted_edits, cx| {
15374 assert_eq!(
15375 highlighted_edits.text,
15376 "Second modified\nNew third line\nFourth updated line"
15377 );
15378 assert_eq!(highlighted_edits.highlights.len(), 3);
15379 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
15380 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
15381 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
15382 for highlight in &highlighted_edits.highlights {
15383 assert_eq!(
15384 highlight.1.background_color,
15385 Some(cx.theme().status().created_background)
15386 );
15387 }
15388 },
15389 )
15390 .await;
15391}
15392
15393#[gpui::test]
15394async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
15395 init_test(cx, |_| {});
15396
15397 // Deletion
15398 assert_highlighted_edits(
15399 "Hello, world!",
15400 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
15401 true,
15402 cx,
15403 |highlighted_edits, cx| {
15404 assert_eq!(highlighted_edits.text, "Hello, world!");
15405 assert_eq!(highlighted_edits.highlights.len(), 1);
15406 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
15407 assert_eq!(
15408 highlighted_edits.highlights[0].1.background_color,
15409 Some(cx.theme().status().deleted_background)
15410 );
15411 },
15412 )
15413 .await;
15414
15415 // Insertion
15416 assert_highlighted_edits(
15417 "Hello, world!",
15418 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
15419 true,
15420 cx,
15421 |highlighted_edits, cx| {
15422 assert_eq!(highlighted_edits.highlights.len(), 1);
15423 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
15424 assert_eq!(
15425 highlighted_edits.highlights[0].1.background_color,
15426 Some(cx.theme().status().created_background)
15427 );
15428 },
15429 )
15430 .await;
15431}
15432
15433async fn assert_highlighted_edits(
15434 text: &str,
15435 edits: Vec<(Range<Point>, String)>,
15436 include_deletions: bool,
15437 cx: &mut TestAppContext,
15438 assertion_fn: impl Fn(HighlightedText, &App),
15439) {
15440 let window = cx.add_window(|window, cx| {
15441 let buffer = MultiBuffer::build_simple(text, cx);
15442 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
15443 });
15444 let cx = &mut VisualTestContext::from_window(*window, cx);
15445
15446 let (buffer, snapshot) = window
15447 .update(cx, |editor, _window, cx| {
15448 (
15449 editor.buffer().clone(),
15450 editor.buffer().read(cx).snapshot(cx),
15451 )
15452 })
15453 .unwrap();
15454
15455 let edits = edits
15456 .into_iter()
15457 .map(|(range, edit)| {
15458 (
15459 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
15460 edit,
15461 )
15462 })
15463 .collect::<Vec<_>>();
15464
15465 let text_anchor_edits = edits
15466 .clone()
15467 .into_iter()
15468 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
15469 .collect::<Vec<_>>();
15470
15471 let edit_preview = window
15472 .update(cx, |_, _window, cx| {
15473 buffer
15474 .read(cx)
15475 .as_singleton()
15476 .unwrap()
15477 .read(cx)
15478 .preview_edits(text_anchor_edits.into(), cx)
15479 })
15480 .unwrap()
15481 .await;
15482
15483 cx.update(|_window, cx| {
15484 let highlighted_edits = inline_completion_edit_text(
15485 &snapshot.as_singleton().unwrap().2,
15486 &edits,
15487 &edit_preview,
15488 include_deletions,
15489 cx,
15490 );
15491 assertion_fn(highlighted_edits, cx)
15492 });
15493}
15494
15495#[gpui::test]
15496async fn test_rename_with_duplicate_edits(cx: &mut gpui::TestAppContext) {
15497 init_test(cx, |_| {});
15498 let capabilities = lsp::ServerCapabilities {
15499 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
15500 prepare_provider: Some(true),
15501 work_done_progress_options: Default::default(),
15502 })),
15503 ..Default::default()
15504 };
15505 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
15506
15507 cx.set_state(indoc! {"
15508 struct Fˇoo {}
15509 "});
15510
15511 cx.update_editor(|editor, _, cx| {
15512 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
15513 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
15514 editor.highlight_background::<DocumentHighlightRead>(
15515 &[highlight_range],
15516 |c| c.editor_document_highlight_read_background,
15517 cx,
15518 );
15519 });
15520
15521 let mut prepare_rename_handler =
15522 cx.handle_request::<lsp::request::PrepareRenameRequest, _, _>(move |_, _, _| async move {
15523 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
15524 start: lsp::Position {
15525 line: 0,
15526 character: 7,
15527 },
15528 end: lsp::Position {
15529 line: 0,
15530 character: 10,
15531 },
15532 })))
15533 });
15534 let prepare_rename_task = cx
15535 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
15536 .expect("Prepare rename was not started");
15537 prepare_rename_handler.next().await.unwrap();
15538 prepare_rename_task.await.expect("Prepare rename failed");
15539
15540 let mut rename_handler =
15541 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
15542 let edit = lsp::TextEdit {
15543 range: lsp::Range {
15544 start: lsp::Position {
15545 line: 0,
15546 character: 7,
15547 },
15548 end: lsp::Position {
15549 line: 0,
15550 character: 10,
15551 },
15552 },
15553 new_text: "FooRenamed".to_string(),
15554 };
15555 Ok(Some(lsp::WorkspaceEdit::new(
15556 // Specify the same edit twice
15557 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
15558 )))
15559 });
15560 let rename_task = cx
15561 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
15562 .expect("Confirm rename was not started");
15563 rename_handler.next().await.unwrap();
15564 rename_task.await.expect("Confirm rename failed");
15565 cx.run_until_parked();
15566
15567 // Despite two edits, only one is actually applied as those are identical
15568 cx.assert_editor_state(indoc! {"
15569 struct FooRenamedˇ {}
15570 "});
15571}
15572
15573#[gpui::test]
15574async fn test_rename_without_prepare(cx: &mut gpui::TestAppContext) {
15575 init_test(cx, |_| {});
15576 // These capabilities indicate that the server does not support prepare rename.
15577 let capabilities = lsp::ServerCapabilities {
15578 rename_provider: Some(lsp::OneOf::Left(true)),
15579 ..Default::default()
15580 };
15581 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
15582
15583 cx.set_state(indoc! {"
15584 struct Fˇoo {}
15585 "});
15586
15587 cx.update_editor(|editor, _window, cx| {
15588 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
15589 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
15590 editor.highlight_background::<DocumentHighlightRead>(
15591 &[highlight_range],
15592 |c| c.editor_document_highlight_read_background,
15593 cx,
15594 );
15595 });
15596
15597 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
15598 .expect("Prepare rename was not started")
15599 .await
15600 .expect("Prepare rename failed");
15601
15602 let mut rename_handler =
15603 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
15604 let edit = lsp::TextEdit {
15605 range: lsp::Range {
15606 start: lsp::Position {
15607 line: 0,
15608 character: 7,
15609 },
15610 end: lsp::Position {
15611 line: 0,
15612 character: 10,
15613 },
15614 },
15615 new_text: "FooRenamed".to_string(),
15616 };
15617 Ok(Some(lsp::WorkspaceEdit::new(
15618 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
15619 )))
15620 });
15621 let rename_task = cx
15622 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
15623 .expect("Confirm rename was not started");
15624 rename_handler.next().await.unwrap();
15625 rename_task.await.expect("Confirm rename failed");
15626 cx.run_until_parked();
15627
15628 // Correct range is renamed, as `surrounding_word` is used to find it.
15629 cx.assert_editor_state(indoc! {"
15630 struct FooRenamedˇ {}
15631 "});
15632}
15633
15634fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
15635 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
15636 point..point
15637}
15638
15639fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
15640 let (text, ranges) = marked_text_ranges(marked_text, true);
15641 assert_eq!(editor.text(cx), text);
15642 assert_eq!(
15643 editor.selections.ranges(cx),
15644 ranges,
15645 "Assert selections are {}",
15646 marked_text
15647 );
15648}
15649
15650pub fn handle_signature_help_request(
15651 cx: &mut EditorLspTestContext,
15652 mocked_response: lsp::SignatureHelp,
15653) -> impl Future<Output = ()> {
15654 let mut request =
15655 cx.handle_request::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
15656 let mocked_response = mocked_response.clone();
15657 async move { Ok(Some(mocked_response)) }
15658 });
15659
15660 async move {
15661 request.next().await;
15662 }
15663}
15664
15665/// Handle completion request passing a marked string specifying where the completion
15666/// should be triggered from using '|' character, what range should be replaced, and what completions
15667/// should be returned using '<' and '>' to delimit the range
15668pub fn handle_completion_request(
15669 cx: &mut EditorLspTestContext,
15670 marked_string: &str,
15671 completions: Vec<&'static str>,
15672 counter: Arc<AtomicUsize>,
15673) -> impl Future<Output = ()> {
15674 let complete_from_marker: TextRangeMarker = '|'.into();
15675 let replace_range_marker: TextRangeMarker = ('<', '>').into();
15676 let (_, mut marked_ranges) = marked_text_ranges_by(
15677 marked_string,
15678 vec![complete_from_marker.clone(), replace_range_marker.clone()],
15679 );
15680
15681 let complete_from_position =
15682 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
15683 let replace_range =
15684 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
15685
15686 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
15687 let completions = completions.clone();
15688 counter.fetch_add(1, atomic::Ordering::Release);
15689 async move {
15690 assert_eq!(params.text_document_position.text_document.uri, url.clone());
15691 assert_eq!(
15692 params.text_document_position.position,
15693 complete_from_position
15694 );
15695 Ok(Some(lsp::CompletionResponse::Array(
15696 completions
15697 .iter()
15698 .map(|completion_text| lsp::CompletionItem {
15699 label: completion_text.to_string(),
15700 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15701 range: replace_range,
15702 new_text: completion_text.to_string(),
15703 })),
15704 ..Default::default()
15705 })
15706 .collect(),
15707 )))
15708 }
15709 });
15710
15711 async move {
15712 request.next().await;
15713 }
15714}
15715
15716fn handle_resolve_completion_request(
15717 cx: &mut EditorLspTestContext,
15718 edits: Option<Vec<(&'static str, &'static str)>>,
15719) -> impl Future<Output = ()> {
15720 let edits = edits.map(|edits| {
15721 edits
15722 .iter()
15723 .map(|(marked_string, new_text)| {
15724 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
15725 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
15726 lsp::TextEdit::new(replace_range, new_text.to_string())
15727 })
15728 .collect::<Vec<_>>()
15729 });
15730
15731 let mut request =
15732 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15733 let edits = edits.clone();
15734 async move {
15735 Ok(lsp::CompletionItem {
15736 additional_text_edits: edits,
15737 ..Default::default()
15738 })
15739 }
15740 });
15741
15742 async move {
15743 request.next().await;
15744 }
15745}
15746
15747pub(crate) fn update_test_language_settings(
15748 cx: &mut TestAppContext,
15749 f: impl Fn(&mut AllLanguageSettingsContent),
15750) {
15751 cx.update(|cx| {
15752 SettingsStore::update_global(cx, |store, cx| {
15753 store.update_user_settings::<AllLanguageSettings>(cx, f);
15754 });
15755 });
15756}
15757
15758pub(crate) fn update_test_project_settings(
15759 cx: &mut TestAppContext,
15760 f: impl Fn(&mut ProjectSettings),
15761) {
15762 cx.update(|cx| {
15763 SettingsStore::update_global(cx, |store, cx| {
15764 store.update_user_settings::<ProjectSettings>(cx, f);
15765 });
15766 });
15767}
15768
15769pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
15770 cx.update(|cx| {
15771 assets::Assets.load_test_fonts(cx);
15772 let store = SettingsStore::test(cx);
15773 cx.set_global(store);
15774 theme::init(theme::LoadThemes::JustBase, cx);
15775 release_channel::init(SemanticVersion::default(), cx);
15776 client::init_settings(cx);
15777 language::init(cx);
15778 Project::init_settings(cx);
15779 workspace::init_settings(cx);
15780 crate::init(cx);
15781 });
15782
15783 update_test_language_settings(cx, f);
15784}
15785
15786#[track_caller]
15787fn assert_hunk_revert(
15788 not_reverted_text_with_selections: &str,
15789 expected_hunk_statuses_before: Vec<DiffHunkStatus>,
15790 expected_reverted_text_with_selections: &str,
15791 base_text: &str,
15792 cx: &mut EditorLspTestContext,
15793) {
15794 cx.set_state(not_reverted_text_with_selections);
15795 cx.set_diff_base(base_text);
15796 cx.executor().run_until_parked();
15797
15798 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
15799 let snapshot = editor.snapshot(window, cx);
15800 let reverted_hunk_statuses = snapshot
15801 .buffer_snapshot
15802 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
15803 .map(|hunk| hunk.status())
15804 .collect::<Vec<_>>();
15805
15806 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
15807 reverted_hunk_statuses
15808 });
15809 cx.executor().run_until_parked();
15810 cx.assert_editor_state(expected_reverted_text_with_selections);
15811 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
15812}