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_with_multiple_carets(cx: &mut gpui::TestAppContext) {
5367 init_test(cx, |_| {});
5368
5369 let mut cx = EditorTestContext::new(cx).await;
5370 cx.set_state(
5371 r#"let foo = 2;
5372lˇet foo = 2;
5373let fooˇ = 2;
5374let foo = 2;
5375let foo = ˇ2;"#,
5376 );
5377
5378 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5379 .unwrap();
5380 cx.assert_editor_state(
5381 r#"let foo = 2;
5382«letˇ» foo = 2;
5383let «fooˇ» = 2;
5384let foo = 2;
5385let foo = «2ˇ»;"#,
5386 );
5387
5388 // noop for multiple selections with different contents
5389 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5390 .unwrap();
5391 cx.assert_editor_state(
5392 r#"let foo = 2;
5393«letˇ» foo = 2;
5394let «fooˇ» = 2;
5395let foo = 2;
5396let foo = «2ˇ»;"#,
5397 );
5398}
5399
5400#[gpui::test]
5401async fn test_select_previous_with_single_selection(cx: &mut gpui::TestAppContext) {
5402 init_test(cx, |_| {});
5403
5404 let mut cx = EditorTestContext::new(cx).await;
5405 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5406
5407 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5408 .unwrap();
5409 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5410
5411 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5412 .unwrap();
5413 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5414
5415 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5416 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5417
5418 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5419 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5420
5421 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5422 .unwrap();
5423 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
5424
5425 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5426 .unwrap();
5427 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5428}
5429
5430#[gpui::test]
5431async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
5432 init_test(cx, |_| {});
5433
5434 let language = Arc::new(Language::new(
5435 LanguageConfig::default(),
5436 Some(tree_sitter_rust::LANGUAGE.into()),
5437 ));
5438
5439 let text = r#"
5440 use mod1::mod2::{mod3, mod4};
5441
5442 fn fn_1(param1: bool, param2: &str) {
5443 let var1 = "text";
5444 }
5445 "#
5446 .unindent();
5447
5448 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5449 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5450 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5451
5452 editor
5453 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5454 .await;
5455
5456 editor.update_in(cx, |editor, window, cx| {
5457 editor.change_selections(None, window, cx, |s| {
5458 s.select_display_ranges([
5459 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
5460 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
5461 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
5462 ]);
5463 });
5464 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5465 });
5466 editor.update(cx, |editor, cx| {
5467 assert_text_with_selections(
5468 editor,
5469 indoc! {r#"
5470 use mod1::mod2::{mod3, «mod4ˇ»};
5471
5472 fn fn_1«ˇ(param1: bool, param2: &str)» {
5473 let var1 = "«textˇ»";
5474 }
5475 "#},
5476 cx,
5477 );
5478 });
5479
5480 editor.update_in(cx, |editor, window, cx| {
5481 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5482 });
5483 editor.update(cx, |editor, cx| {
5484 assert_text_with_selections(
5485 editor,
5486 indoc! {r#"
5487 use mod1::mod2::«{mod3, mod4}ˇ»;
5488
5489 «ˇfn fn_1(param1: bool, param2: &str) {
5490 let var1 = "text";
5491 }»
5492 "#},
5493 cx,
5494 );
5495 });
5496
5497 editor.update_in(cx, |editor, window, cx| {
5498 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5499 });
5500 assert_eq!(
5501 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
5502 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
5503 );
5504
5505 // Trying to expand the selected syntax node one more time has no effect.
5506 editor.update_in(cx, |editor, window, cx| {
5507 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5508 });
5509 assert_eq!(
5510 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
5511 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
5512 );
5513
5514 editor.update_in(cx, |editor, window, cx| {
5515 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5516 });
5517 editor.update(cx, |editor, cx| {
5518 assert_text_with_selections(
5519 editor,
5520 indoc! {r#"
5521 use mod1::mod2::«{mod3, mod4}ˇ»;
5522
5523 «ˇfn fn_1(param1: bool, param2: &str) {
5524 let var1 = "text";
5525 }»
5526 "#},
5527 cx,
5528 );
5529 });
5530
5531 editor.update_in(cx, |editor, window, cx| {
5532 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5533 });
5534 editor.update(cx, |editor, cx| {
5535 assert_text_with_selections(
5536 editor,
5537 indoc! {r#"
5538 use mod1::mod2::{mod3, «mod4ˇ»};
5539
5540 fn fn_1«ˇ(param1: bool, param2: &str)» {
5541 let var1 = "«textˇ»";
5542 }
5543 "#},
5544 cx,
5545 );
5546 });
5547
5548 editor.update_in(cx, |editor, window, cx| {
5549 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5550 });
5551 editor.update(cx, |editor, cx| {
5552 assert_text_with_selections(
5553 editor,
5554 indoc! {r#"
5555 use mod1::mod2::{mod3, mo«ˇ»d4};
5556
5557 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
5558 let var1 = "te«ˇ»xt";
5559 }
5560 "#},
5561 cx,
5562 );
5563 });
5564
5565 // Trying to shrink the selected syntax node one more time has no effect.
5566 editor.update_in(cx, |editor, window, cx| {
5567 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5568 });
5569 editor.update_in(cx, |editor, _, cx| {
5570 assert_text_with_selections(
5571 editor,
5572 indoc! {r#"
5573 use mod1::mod2::{mod3, mo«ˇ»d4};
5574
5575 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
5576 let var1 = "te«ˇ»xt";
5577 }
5578 "#},
5579 cx,
5580 );
5581 });
5582
5583 // Ensure that we keep expanding the selection if the larger selection starts or ends within
5584 // a fold.
5585 editor.update_in(cx, |editor, window, cx| {
5586 editor.fold_creases(
5587 vec![
5588 Crease::simple(
5589 Point::new(0, 21)..Point::new(0, 24),
5590 FoldPlaceholder::test(),
5591 ),
5592 Crease::simple(
5593 Point::new(3, 20)..Point::new(3, 22),
5594 FoldPlaceholder::test(),
5595 ),
5596 ],
5597 true,
5598 window,
5599 cx,
5600 );
5601 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5602 });
5603 editor.update(cx, |editor, cx| {
5604 assert_text_with_selections(
5605 editor,
5606 indoc! {r#"
5607 use mod1::mod2::«{mod3, mod4}ˇ»;
5608
5609 fn fn_1«ˇ(param1: bool, param2: &str)» {
5610 «let var1 = "text";ˇ»
5611 }
5612 "#},
5613 cx,
5614 );
5615 });
5616}
5617
5618#[gpui::test]
5619async fn test_fold_function_bodies(cx: &mut gpui::TestAppContext) {
5620 init_test(cx, |_| {});
5621
5622 let base_text = r#"
5623 impl A {
5624 // this is an uncommitted comment
5625
5626 fn b() {
5627 c();
5628 }
5629
5630 // this is another uncommitted comment
5631
5632 fn d() {
5633 // e
5634 // f
5635 }
5636 }
5637
5638 fn g() {
5639 // h
5640 }
5641 "#
5642 .unindent();
5643
5644 let text = r#"
5645 ˇimpl A {
5646
5647 fn b() {
5648 c();
5649 }
5650
5651 fn d() {
5652 // e
5653 // f
5654 }
5655 }
5656
5657 fn g() {
5658 // h
5659 }
5660 "#
5661 .unindent();
5662
5663 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
5664 cx.set_state(&text);
5665 cx.set_diff_base(&base_text);
5666 cx.update_editor(|editor, window, cx| {
5667 editor.expand_all_diff_hunks(&Default::default(), window, cx);
5668 });
5669
5670 cx.assert_state_with_diff(
5671 "
5672 ˇimpl A {
5673 - // this is an uncommitted comment
5674
5675 fn b() {
5676 c();
5677 }
5678
5679 - // this is another uncommitted comment
5680 -
5681 fn d() {
5682 // e
5683 // f
5684 }
5685 }
5686
5687 fn g() {
5688 // h
5689 }
5690 "
5691 .unindent(),
5692 );
5693
5694 let expected_display_text = "
5695 impl A {
5696 // this is an uncommitted comment
5697
5698 fn b() {
5699 ⋯
5700 }
5701
5702 // this is another uncommitted comment
5703
5704 fn d() {
5705 ⋯
5706 }
5707 }
5708
5709 fn g() {
5710 ⋯
5711 }
5712 "
5713 .unindent();
5714
5715 cx.update_editor(|editor, window, cx| {
5716 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
5717 assert_eq!(editor.display_text(cx), expected_display_text);
5718 });
5719}
5720
5721#[gpui::test]
5722async fn test_autoindent(cx: &mut gpui::TestAppContext) {
5723 init_test(cx, |_| {});
5724
5725 let language = Arc::new(
5726 Language::new(
5727 LanguageConfig {
5728 brackets: BracketPairConfig {
5729 pairs: vec![
5730 BracketPair {
5731 start: "{".to_string(),
5732 end: "}".to_string(),
5733 close: false,
5734 surround: false,
5735 newline: true,
5736 },
5737 BracketPair {
5738 start: "(".to_string(),
5739 end: ")".to_string(),
5740 close: false,
5741 surround: false,
5742 newline: true,
5743 },
5744 ],
5745 ..Default::default()
5746 },
5747 ..Default::default()
5748 },
5749 Some(tree_sitter_rust::LANGUAGE.into()),
5750 )
5751 .with_indents_query(
5752 r#"
5753 (_ "(" ")" @end) @indent
5754 (_ "{" "}" @end) @indent
5755 "#,
5756 )
5757 .unwrap(),
5758 );
5759
5760 let text = "fn a() {}";
5761
5762 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5763 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5764 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5765 editor
5766 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5767 .await;
5768
5769 editor.update_in(cx, |editor, window, cx| {
5770 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
5771 editor.newline(&Newline, window, cx);
5772 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
5773 assert_eq!(
5774 editor.selections.ranges(cx),
5775 &[
5776 Point::new(1, 4)..Point::new(1, 4),
5777 Point::new(3, 4)..Point::new(3, 4),
5778 Point::new(5, 0)..Point::new(5, 0)
5779 ]
5780 );
5781 });
5782}
5783
5784#[gpui::test]
5785async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
5786 init_test(cx, |_| {});
5787
5788 {
5789 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
5790 cx.set_state(indoc! {"
5791 impl A {
5792
5793 fn b() {}
5794
5795 «fn c() {
5796
5797 }ˇ»
5798 }
5799 "});
5800
5801 cx.update_editor(|editor, window, cx| {
5802 editor.autoindent(&Default::default(), window, cx);
5803 });
5804
5805 cx.assert_editor_state(indoc! {"
5806 impl A {
5807
5808 fn b() {}
5809
5810 «fn c() {
5811
5812 }ˇ»
5813 }
5814 "});
5815 }
5816
5817 {
5818 let mut cx = EditorTestContext::new_multibuffer(
5819 cx,
5820 [indoc! { "
5821 impl A {
5822 «
5823 // a
5824 fn b(){}
5825 »
5826 «
5827 }
5828 fn c(){}
5829 »
5830 "}],
5831 );
5832
5833 let buffer = cx.update_editor(|editor, _, cx| {
5834 let buffer = editor.buffer().update(cx, |buffer, _| {
5835 buffer.all_buffers().iter().next().unwrap().clone()
5836 });
5837 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5838 buffer
5839 });
5840
5841 cx.run_until_parked();
5842 cx.update_editor(|editor, window, cx| {
5843 editor.select_all(&Default::default(), window, cx);
5844 editor.autoindent(&Default::default(), window, cx)
5845 });
5846 cx.run_until_parked();
5847
5848 cx.update(|_, cx| {
5849 pretty_assertions::assert_eq!(
5850 buffer.read(cx).text(),
5851 indoc! { "
5852 impl A {
5853
5854 // a
5855 fn b(){}
5856
5857
5858 }
5859 fn c(){}
5860
5861 " }
5862 )
5863 });
5864 }
5865}
5866
5867#[gpui::test]
5868async fn test_autoclose_and_auto_surround_pairs(cx: &mut gpui::TestAppContext) {
5869 init_test(cx, |_| {});
5870
5871 let mut cx = EditorTestContext::new(cx).await;
5872
5873 let language = Arc::new(Language::new(
5874 LanguageConfig {
5875 brackets: BracketPairConfig {
5876 pairs: vec![
5877 BracketPair {
5878 start: "{".to_string(),
5879 end: "}".to_string(),
5880 close: true,
5881 surround: true,
5882 newline: true,
5883 },
5884 BracketPair {
5885 start: "(".to_string(),
5886 end: ")".to_string(),
5887 close: true,
5888 surround: true,
5889 newline: true,
5890 },
5891 BracketPair {
5892 start: "/*".to_string(),
5893 end: " */".to_string(),
5894 close: true,
5895 surround: true,
5896 newline: true,
5897 },
5898 BracketPair {
5899 start: "[".to_string(),
5900 end: "]".to_string(),
5901 close: false,
5902 surround: false,
5903 newline: true,
5904 },
5905 BracketPair {
5906 start: "\"".to_string(),
5907 end: "\"".to_string(),
5908 close: true,
5909 surround: true,
5910 newline: false,
5911 },
5912 BracketPair {
5913 start: "<".to_string(),
5914 end: ">".to_string(),
5915 close: false,
5916 surround: true,
5917 newline: true,
5918 },
5919 ],
5920 ..Default::default()
5921 },
5922 autoclose_before: "})]".to_string(),
5923 ..Default::default()
5924 },
5925 Some(tree_sitter_rust::LANGUAGE.into()),
5926 ));
5927
5928 cx.language_registry().add(language.clone());
5929 cx.update_buffer(|buffer, cx| {
5930 buffer.set_language(Some(language), cx);
5931 });
5932
5933 cx.set_state(
5934 &r#"
5935 🏀ˇ
5936 εˇ
5937 ❤️ˇ
5938 "#
5939 .unindent(),
5940 );
5941
5942 // autoclose multiple nested brackets at multiple cursors
5943 cx.update_editor(|editor, window, cx| {
5944 editor.handle_input("{", window, cx);
5945 editor.handle_input("{", window, cx);
5946 editor.handle_input("{", window, cx);
5947 });
5948 cx.assert_editor_state(
5949 &"
5950 🏀{{{ˇ}}}
5951 ε{{{ˇ}}}
5952 ❤️{{{ˇ}}}
5953 "
5954 .unindent(),
5955 );
5956
5957 // insert a different closing bracket
5958 cx.update_editor(|editor, window, cx| {
5959 editor.handle_input(")", window, cx);
5960 });
5961 cx.assert_editor_state(
5962 &"
5963 🏀{{{)ˇ}}}
5964 ε{{{)ˇ}}}
5965 ❤️{{{)ˇ}}}
5966 "
5967 .unindent(),
5968 );
5969
5970 // skip over the auto-closed brackets when typing a closing bracket
5971 cx.update_editor(|editor, window, cx| {
5972 editor.move_right(&MoveRight, window, cx);
5973 editor.handle_input("}", window, cx);
5974 editor.handle_input("}", window, cx);
5975 editor.handle_input("}", window, cx);
5976 });
5977 cx.assert_editor_state(
5978 &"
5979 🏀{{{)}}}}ˇ
5980 ε{{{)}}}}ˇ
5981 ❤️{{{)}}}}ˇ
5982 "
5983 .unindent(),
5984 );
5985
5986 // autoclose multi-character pairs
5987 cx.set_state(
5988 &"
5989 ˇ
5990 ˇ
5991 "
5992 .unindent(),
5993 );
5994 cx.update_editor(|editor, window, cx| {
5995 editor.handle_input("/", window, cx);
5996 editor.handle_input("*", window, cx);
5997 });
5998 cx.assert_editor_state(
5999 &"
6000 /*ˇ */
6001 /*ˇ */
6002 "
6003 .unindent(),
6004 );
6005
6006 // one cursor autocloses a multi-character pair, one cursor
6007 // does not autoclose.
6008 cx.set_state(
6009 &"
6010 /ˇ
6011 ˇ
6012 "
6013 .unindent(),
6014 );
6015 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6016 cx.assert_editor_state(
6017 &"
6018 /*ˇ */
6019 *ˇ
6020 "
6021 .unindent(),
6022 );
6023
6024 // Don't autoclose if the next character isn't whitespace and isn't
6025 // listed in the language's "autoclose_before" section.
6026 cx.set_state("ˇa b");
6027 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6028 cx.assert_editor_state("{ˇa b");
6029
6030 // Don't autoclose if `close` is false for the bracket pair
6031 cx.set_state("ˇ");
6032 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6033 cx.assert_editor_state("[ˇ");
6034
6035 // Surround with brackets if text is selected
6036 cx.set_state("«aˇ» b");
6037 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6038 cx.assert_editor_state("{«aˇ»} b");
6039
6040 // Autclose pair where the start and end characters are the same
6041 cx.set_state("aˇ");
6042 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6043 cx.assert_editor_state("a\"ˇ\"");
6044 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6045 cx.assert_editor_state("a\"\"ˇ");
6046
6047 // Don't autoclose pair if autoclose is disabled
6048 cx.set_state("ˇ");
6049 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6050 cx.assert_editor_state("<ˇ");
6051
6052 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6053 cx.set_state("«aˇ» b");
6054 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6055 cx.assert_editor_state("<«aˇ»> b");
6056}
6057
6058#[gpui::test]
6059async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut gpui::TestAppContext) {
6060 init_test(cx, |settings| {
6061 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6062 });
6063
6064 let mut cx = EditorTestContext::new(cx).await;
6065
6066 let language = Arc::new(Language::new(
6067 LanguageConfig {
6068 brackets: BracketPairConfig {
6069 pairs: vec![
6070 BracketPair {
6071 start: "{".to_string(),
6072 end: "}".to_string(),
6073 close: true,
6074 surround: true,
6075 newline: true,
6076 },
6077 BracketPair {
6078 start: "(".to_string(),
6079 end: ")".to_string(),
6080 close: true,
6081 surround: true,
6082 newline: true,
6083 },
6084 BracketPair {
6085 start: "[".to_string(),
6086 end: "]".to_string(),
6087 close: false,
6088 surround: false,
6089 newline: true,
6090 },
6091 ],
6092 ..Default::default()
6093 },
6094 autoclose_before: "})]".to_string(),
6095 ..Default::default()
6096 },
6097 Some(tree_sitter_rust::LANGUAGE.into()),
6098 ));
6099
6100 cx.language_registry().add(language.clone());
6101 cx.update_buffer(|buffer, cx| {
6102 buffer.set_language(Some(language), cx);
6103 });
6104
6105 cx.set_state(
6106 &"
6107 ˇ
6108 ˇ
6109 ˇ
6110 "
6111 .unindent(),
6112 );
6113
6114 // ensure only matching closing brackets are skipped over
6115 cx.update_editor(|editor, window, cx| {
6116 editor.handle_input("}", window, cx);
6117 editor.move_left(&MoveLeft, window, cx);
6118 editor.handle_input(")", window, cx);
6119 editor.move_left(&MoveLeft, window, cx);
6120 });
6121 cx.assert_editor_state(
6122 &"
6123 ˇ)}
6124 ˇ)}
6125 ˇ)}
6126 "
6127 .unindent(),
6128 );
6129
6130 // skip-over closing brackets at multiple cursors
6131 cx.update_editor(|editor, window, cx| {
6132 editor.handle_input(")", window, cx);
6133 editor.handle_input("}", window, cx);
6134 });
6135 cx.assert_editor_state(
6136 &"
6137 )}ˇ
6138 )}ˇ
6139 )}ˇ
6140 "
6141 .unindent(),
6142 );
6143
6144 // ignore non-close brackets
6145 cx.update_editor(|editor, window, cx| {
6146 editor.handle_input("]", window, cx);
6147 editor.move_left(&MoveLeft, window, cx);
6148 editor.handle_input("]", window, cx);
6149 });
6150 cx.assert_editor_state(
6151 &"
6152 )}]ˇ]
6153 )}]ˇ]
6154 )}]ˇ]
6155 "
6156 .unindent(),
6157 );
6158}
6159
6160#[gpui::test]
6161async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
6162 init_test(cx, |_| {});
6163
6164 let mut cx = EditorTestContext::new(cx).await;
6165
6166 let html_language = Arc::new(
6167 Language::new(
6168 LanguageConfig {
6169 name: "HTML".into(),
6170 brackets: BracketPairConfig {
6171 pairs: vec![
6172 BracketPair {
6173 start: "<".into(),
6174 end: ">".into(),
6175 close: true,
6176 ..Default::default()
6177 },
6178 BracketPair {
6179 start: "{".into(),
6180 end: "}".into(),
6181 close: true,
6182 ..Default::default()
6183 },
6184 BracketPair {
6185 start: "(".into(),
6186 end: ")".into(),
6187 close: true,
6188 ..Default::default()
6189 },
6190 ],
6191 ..Default::default()
6192 },
6193 autoclose_before: "})]>".into(),
6194 ..Default::default()
6195 },
6196 Some(tree_sitter_html::language()),
6197 )
6198 .with_injection_query(
6199 r#"
6200 (script_element
6201 (raw_text) @injection.content
6202 (#set! injection.language "javascript"))
6203 "#,
6204 )
6205 .unwrap(),
6206 );
6207
6208 let javascript_language = Arc::new(Language::new(
6209 LanguageConfig {
6210 name: "JavaScript".into(),
6211 brackets: BracketPairConfig {
6212 pairs: vec![
6213 BracketPair {
6214 start: "/*".into(),
6215 end: " */".into(),
6216 close: true,
6217 ..Default::default()
6218 },
6219 BracketPair {
6220 start: "{".into(),
6221 end: "}".into(),
6222 close: true,
6223 ..Default::default()
6224 },
6225 BracketPair {
6226 start: "(".into(),
6227 end: ")".into(),
6228 close: true,
6229 ..Default::default()
6230 },
6231 ],
6232 ..Default::default()
6233 },
6234 autoclose_before: "})]>".into(),
6235 ..Default::default()
6236 },
6237 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6238 ));
6239
6240 cx.language_registry().add(html_language.clone());
6241 cx.language_registry().add(javascript_language.clone());
6242
6243 cx.update_buffer(|buffer, cx| {
6244 buffer.set_language(Some(html_language), cx);
6245 });
6246
6247 cx.set_state(
6248 &r#"
6249 <body>ˇ
6250 <script>
6251 var x = 1;ˇ
6252 </script>
6253 </body>ˇ
6254 "#
6255 .unindent(),
6256 );
6257
6258 // Precondition: different languages are active at different locations.
6259 cx.update_editor(|editor, window, cx| {
6260 let snapshot = editor.snapshot(window, cx);
6261 let cursors = editor.selections.ranges::<usize>(cx);
6262 let languages = cursors
6263 .iter()
6264 .map(|c| snapshot.language_at(c.start).unwrap().name())
6265 .collect::<Vec<_>>();
6266 assert_eq!(
6267 languages,
6268 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6269 );
6270 });
6271
6272 // Angle brackets autoclose in HTML, but not JavaScript.
6273 cx.update_editor(|editor, window, cx| {
6274 editor.handle_input("<", window, cx);
6275 editor.handle_input("a", window, cx);
6276 });
6277 cx.assert_editor_state(
6278 &r#"
6279 <body><aˇ>
6280 <script>
6281 var x = 1;<aˇ
6282 </script>
6283 </body><aˇ>
6284 "#
6285 .unindent(),
6286 );
6287
6288 // Curly braces and parens autoclose in both HTML and JavaScript.
6289 cx.update_editor(|editor, window, cx| {
6290 editor.handle_input(" b=", window, cx);
6291 editor.handle_input("{", window, cx);
6292 editor.handle_input("c", window, cx);
6293 editor.handle_input("(", window, cx);
6294 });
6295 cx.assert_editor_state(
6296 &r#"
6297 <body><a b={c(ˇ)}>
6298 <script>
6299 var x = 1;<a b={c(ˇ)}
6300 </script>
6301 </body><a b={c(ˇ)}>
6302 "#
6303 .unindent(),
6304 );
6305
6306 // Brackets that were already autoclosed are skipped.
6307 cx.update_editor(|editor, window, cx| {
6308 editor.handle_input(")", window, cx);
6309 editor.handle_input("d", window, cx);
6310 editor.handle_input("}", window, cx);
6311 });
6312 cx.assert_editor_state(
6313 &r#"
6314 <body><a b={c()d}ˇ>
6315 <script>
6316 var x = 1;<a b={c()d}ˇ
6317 </script>
6318 </body><a b={c()d}ˇ>
6319 "#
6320 .unindent(),
6321 );
6322 cx.update_editor(|editor, window, cx| {
6323 editor.handle_input(">", window, cx);
6324 });
6325 cx.assert_editor_state(
6326 &r#"
6327 <body><a b={c()d}>ˇ
6328 <script>
6329 var x = 1;<a b={c()d}>ˇ
6330 </script>
6331 </body><a b={c()d}>ˇ
6332 "#
6333 .unindent(),
6334 );
6335
6336 // Reset
6337 cx.set_state(
6338 &r#"
6339 <body>ˇ
6340 <script>
6341 var x = 1;ˇ
6342 </script>
6343 </body>ˇ
6344 "#
6345 .unindent(),
6346 );
6347
6348 cx.update_editor(|editor, window, cx| {
6349 editor.handle_input("<", window, cx);
6350 });
6351 cx.assert_editor_state(
6352 &r#"
6353 <body><ˇ>
6354 <script>
6355 var x = 1;<ˇ
6356 </script>
6357 </body><ˇ>
6358 "#
6359 .unindent(),
6360 );
6361
6362 // When backspacing, the closing angle brackets are removed.
6363 cx.update_editor(|editor, window, cx| {
6364 editor.backspace(&Backspace, 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 // Block comments autoclose in JavaScript, but not HTML.
6378 cx.update_editor(|editor, window, cx| {
6379 editor.handle_input("/", window, cx);
6380 editor.handle_input("*", window, cx);
6381 });
6382 cx.assert_editor_state(
6383 &r#"
6384 <body>/*ˇ
6385 <script>
6386 var x = 1;/*ˇ */
6387 </script>
6388 </body>/*ˇ
6389 "#
6390 .unindent(),
6391 );
6392}
6393
6394#[gpui::test]
6395async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
6396 init_test(cx, |_| {});
6397
6398 let mut cx = EditorTestContext::new(cx).await;
6399
6400 let rust_language = Arc::new(
6401 Language::new(
6402 LanguageConfig {
6403 name: "Rust".into(),
6404 brackets: serde_json::from_value(json!([
6405 { "start": "{", "end": "}", "close": true, "newline": true },
6406 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
6407 ]))
6408 .unwrap(),
6409 autoclose_before: "})]>".into(),
6410 ..Default::default()
6411 },
6412 Some(tree_sitter_rust::LANGUAGE.into()),
6413 )
6414 .with_override_query("(string_literal) @string")
6415 .unwrap(),
6416 );
6417
6418 cx.language_registry().add(rust_language.clone());
6419 cx.update_buffer(|buffer, cx| {
6420 buffer.set_language(Some(rust_language), cx);
6421 });
6422
6423 cx.set_state(
6424 &r#"
6425 let x = ˇ
6426 "#
6427 .unindent(),
6428 );
6429
6430 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
6431 cx.update_editor(|editor, window, cx| {
6432 editor.handle_input("\"", window, cx);
6433 });
6434 cx.assert_editor_state(
6435 &r#"
6436 let x = "ˇ"
6437 "#
6438 .unindent(),
6439 );
6440
6441 // Inserting another quotation mark. The cursor moves across the existing
6442 // automatically-inserted quotation mark.
6443 cx.update_editor(|editor, window, cx| {
6444 editor.handle_input("\"", window, cx);
6445 });
6446 cx.assert_editor_state(
6447 &r#"
6448 let x = ""ˇ
6449 "#
6450 .unindent(),
6451 );
6452
6453 // Reset
6454 cx.set_state(
6455 &r#"
6456 let x = ˇ
6457 "#
6458 .unindent(),
6459 );
6460
6461 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
6462 cx.update_editor(|editor, window, cx| {
6463 editor.handle_input("\"", window, cx);
6464 editor.handle_input(" ", window, cx);
6465 editor.move_left(&Default::default(), window, cx);
6466 editor.handle_input("\\", window, cx);
6467 editor.handle_input("\"", window, cx);
6468 });
6469 cx.assert_editor_state(
6470 &r#"
6471 let x = "\"ˇ "
6472 "#
6473 .unindent(),
6474 );
6475
6476 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
6477 // mark. Nothing is inserted.
6478 cx.update_editor(|editor, window, cx| {
6479 editor.move_right(&Default::default(), window, cx);
6480 editor.handle_input("\"", window, cx);
6481 });
6482 cx.assert_editor_state(
6483 &r#"
6484 let x = "\" "ˇ
6485 "#
6486 .unindent(),
6487 );
6488}
6489
6490#[gpui::test]
6491async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
6492 init_test(cx, |_| {});
6493
6494 let language = Arc::new(Language::new(
6495 LanguageConfig {
6496 brackets: BracketPairConfig {
6497 pairs: vec![
6498 BracketPair {
6499 start: "{".to_string(),
6500 end: "}".to_string(),
6501 close: true,
6502 surround: true,
6503 newline: true,
6504 },
6505 BracketPair {
6506 start: "/* ".to_string(),
6507 end: "*/".to_string(),
6508 close: true,
6509 surround: true,
6510 ..Default::default()
6511 },
6512 ],
6513 ..Default::default()
6514 },
6515 ..Default::default()
6516 },
6517 Some(tree_sitter_rust::LANGUAGE.into()),
6518 ));
6519
6520 let text = r#"
6521 a
6522 b
6523 c
6524 "#
6525 .unindent();
6526
6527 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6528 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6529 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6530 editor
6531 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6532 .await;
6533
6534 editor.update_in(cx, |editor, window, cx| {
6535 editor.change_selections(None, window, cx, |s| {
6536 s.select_display_ranges([
6537 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6538 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6539 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
6540 ])
6541 });
6542
6543 editor.handle_input("{", window, cx);
6544 editor.handle_input("{", window, cx);
6545 editor.handle_input("{", window, cx);
6546 assert_eq!(
6547 editor.text(cx),
6548 "
6549 {{{a}}}
6550 {{{b}}}
6551 {{{c}}}
6552 "
6553 .unindent()
6554 );
6555 assert_eq!(
6556 editor.selections.display_ranges(cx),
6557 [
6558 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
6559 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
6560 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
6561 ]
6562 );
6563
6564 editor.undo(&Undo, window, cx);
6565 editor.undo(&Undo, window, cx);
6566 editor.undo(&Undo, window, cx);
6567 assert_eq!(
6568 editor.text(cx),
6569 "
6570 a
6571 b
6572 c
6573 "
6574 .unindent()
6575 );
6576 assert_eq!(
6577 editor.selections.display_ranges(cx),
6578 [
6579 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6580 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6581 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
6582 ]
6583 );
6584
6585 // Ensure inserting the first character of a multi-byte bracket pair
6586 // doesn't surround the selections with the bracket.
6587 editor.handle_input("/", window, cx);
6588 assert_eq!(
6589 editor.text(cx),
6590 "
6591 /
6592 /
6593 /
6594 "
6595 .unindent()
6596 );
6597 assert_eq!(
6598 editor.selections.display_ranges(cx),
6599 [
6600 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6601 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6602 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
6603 ]
6604 );
6605
6606 editor.undo(&Undo, window, cx);
6607 assert_eq!(
6608 editor.text(cx),
6609 "
6610 a
6611 b
6612 c
6613 "
6614 .unindent()
6615 );
6616 assert_eq!(
6617 editor.selections.display_ranges(cx),
6618 [
6619 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6620 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6621 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
6622 ]
6623 );
6624
6625 // Ensure inserting the last character of a multi-byte bracket pair
6626 // doesn't surround the selections with the bracket.
6627 editor.handle_input("*", window, cx);
6628 assert_eq!(
6629 editor.text(cx),
6630 "
6631 *
6632 *
6633 *
6634 "
6635 .unindent()
6636 );
6637 assert_eq!(
6638 editor.selections.display_ranges(cx),
6639 [
6640 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6641 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6642 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
6643 ]
6644 );
6645 });
6646}
6647
6648#[gpui::test]
6649async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
6650 init_test(cx, |_| {});
6651
6652 let language = Arc::new(Language::new(
6653 LanguageConfig {
6654 brackets: BracketPairConfig {
6655 pairs: vec![BracketPair {
6656 start: "{".to_string(),
6657 end: "}".to_string(),
6658 close: true,
6659 surround: true,
6660 newline: true,
6661 }],
6662 ..Default::default()
6663 },
6664 autoclose_before: "}".to_string(),
6665 ..Default::default()
6666 },
6667 Some(tree_sitter_rust::LANGUAGE.into()),
6668 ));
6669
6670 let text = r#"
6671 a
6672 b
6673 c
6674 "#
6675 .unindent();
6676
6677 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6678 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6679 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6680 editor
6681 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6682 .await;
6683
6684 editor.update_in(cx, |editor, window, cx| {
6685 editor.change_selections(None, window, cx, |s| {
6686 s.select_ranges([
6687 Point::new(0, 1)..Point::new(0, 1),
6688 Point::new(1, 1)..Point::new(1, 1),
6689 Point::new(2, 1)..Point::new(2, 1),
6690 ])
6691 });
6692
6693 editor.handle_input("{", window, cx);
6694 editor.handle_input("{", window, cx);
6695 editor.handle_input("_", window, cx);
6696 assert_eq!(
6697 editor.text(cx),
6698 "
6699 a{{_}}
6700 b{{_}}
6701 c{{_}}
6702 "
6703 .unindent()
6704 );
6705 assert_eq!(
6706 editor.selections.ranges::<Point>(cx),
6707 [
6708 Point::new(0, 4)..Point::new(0, 4),
6709 Point::new(1, 4)..Point::new(1, 4),
6710 Point::new(2, 4)..Point::new(2, 4)
6711 ]
6712 );
6713
6714 editor.backspace(&Default::default(), window, cx);
6715 editor.backspace(&Default::default(), window, cx);
6716 assert_eq!(
6717 editor.text(cx),
6718 "
6719 a{}
6720 b{}
6721 c{}
6722 "
6723 .unindent()
6724 );
6725 assert_eq!(
6726 editor.selections.ranges::<Point>(cx),
6727 [
6728 Point::new(0, 2)..Point::new(0, 2),
6729 Point::new(1, 2)..Point::new(1, 2),
6730 Point::new(2, 2)..Point::new(2, 2)
6731 ]
6732 );
6733
6734 editor.delete_to_previous_word_start(&Default::default(), window, cx);
6735 assert_eq!(
6736 editor.text(cx),
6737 "
6738 a
6739 b
6740 c
6741 "
6742 .unindent()
6743 );
6744 assert_eq!(
6745 editor.selections.ranges::<Point>(cx),
6746 [
6747 Point::new(0, 1)..Point::new(0, 1),
6748 Point::new(1, 1)..Point::new(1, 1),
6749 Point::new(2, 1)..Point::new(2, 1)
6750 ]
6751 );
6752 });
6753}
6754
6755#[gpui::test]
6756async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut gpui::TestAppContext) {
6757 init_test(cx, |settings| {
6758 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6759 });
6760
6761 let mut cx = EditorTestContext::new(cx).await;
6762
6763 let language = Arc::new(Language::new(
6764 LanguageConfig {
6765 brackets: BracketPairConfig {
6766 pairs: vec![
6767 BracketPair {
6768 start: "{".to_string(),
6769 end: "}".to_string(),
6770 close: true,
6771 surround: true,
6772 newline: true,
6773 },
6774 BracketPair {
6775 start: "(".to_string(),
6776 end: ")".to_string(),
6777 close: true,
6778 surround: true,
6779 newline: true,
6780 },
6781 BracketPair {
6782 start: "[".to_string(),
6783 end: "]".to_string(),
6784 close: false,
6785 surround: true,
6786 newline: true,
6787 },
6788 ],
6789 ..Default::default()
6790 },
6791 autoclose_before: "})]".to_string(),
6792 ..Default::default()
6793 },
6794 Some(tree_sitter_rust::LANGUAGE.into()),
6795 ));
6796
6797 cx.language_registry().add(language.clone());
6798 cx.update_buffer(|buffer, cx| {
6799 buffer.set_language(Some(language), cx);
6800 });
6801
6802 cx.set_state(
6803 &"
6804 {(ˇ)}
6805 [[ˇ]]
6806 {(ˇ)}
6807 "
6808 .unindent(),
6809 );
6810
6811 cx.update_editor(|editor, window, cx| {
6812 editor.backspace(&Default::default(), window, cx);
6813 editor.backspace(&Default::default(), window, cx);
6814 });
6815
6816 cx.assert_editor_state(
6817 &"
6818 ˇ
6819 ˇ]]
6820 ˇ
6821 "
6822 .unindent(),
6823 );
6824
6825 cx.update_editor(|editor, window, cx| {
6826 editor.handle_input("{", window, cx);
6827 editor.handle_input("{", window, cx);
6828 editor.move_right(&MoveRight, window, cx);
6829 editor.move_right(&MoveRight, window, cx);
6830 editor.move_left(&MoveLeft, window, cx);
6831 editor.move_left(&MoveLeft, window, cx);
6832 editor.backspace(&Default::default(), window, cx);
6833 });
6834
6835 cx.assert_editor_state(
6836 &"
6837 {ˇ}
6838 {ˇ}]]
6839 {ˇ}
6840 "
6841 .unindent(),
6842 );
6843
6844 cx.update_editor(|editor, window, cx| {
6845 editor.backspace(&Default::default(), window, cx);
6846 });
6847
6848 cx.assert_editor_state(
6849 &"
6850 ˇ
6851 ˇ]]
6852 ˇ
6853 "
6854 .unindent(),
6855 );
6856}
6857
6858#[gpui::test]
6859async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) {
6860 init_test(cx, |_| {});
6861
6862 let language = Arc::new(Language::new(
6863 LanguageConfig::default(),
6864 Some(tree_sitter_rust::LANGUAGE.into()),
6865 ));
6866
6867 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
6868 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6869 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6870 editor
6871 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6872 .await;
6873
6874 editor.update_in(cx, |editor, window, cx| {
6875 editor.set_auto_replace_emoji_shortcode(true);
6876
6877 editor.handle_input("Hello ", window, cx);
6878 editor.handle_input(":wave", window, cx);
6879 assert_eq!(editor.text(cx), "Hello :wave".unindent());
6880
6881 editor.handle_input(":", window, cx);
6882 assert_eq!(editor.text(cx), "Hello 👋".unindent());
6883
6884 editor.handle_input(" :smile", window, cx);
6885 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
6886
6887 editor.handle_input(":", window, cx);
6888 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
6889
6890 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
6891 editor.handle_input(":wave", window, cx);
6892 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
6893
6894 editor.handle_input(":", window, cx);
6895 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
6896
6897 editor.handle_input(":1", window, cx);
6898 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
6899
6900 editor.handle_input(":", window, cx);
6901 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
6902
6903 // Ensure shortcode does not get replaced when it is part of a word
6904 editor.handle_input(" Test:wave", window, cx);
6905 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
6906
6907 editor.handle_input(":", window, cx);
6908 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
6909
6910 editor.set_auto_replace_emoji_shortcode(false);
6911
6912 // Ensure shortcode does not get replaced when auto replace is off
6913 editor.handle_input(" :wave", window, cx);
6914 assert_eq!(
6915 editor.text(cx),
6916 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
6917 );
6918
6919 editor.handle_input(":", window, cx);
6920 assert_eq!(
6921 editor.text(cx),
6922 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
6923 );
6924 });
6925}
6926
6927#[gpui::test]
6928async fn test_snippet_placeholder_choices(cx: &mut gpui::TestAppContext) {
6929 init_test(cx, |_| {});
6930
6931 let (text, insertion_ranges) = marked_text_ranges(
6932 indoc! {"
6933 ˇ
6934 "},
6935 false,
6936 );
6937
6938 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
6939 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6940
6941 _ = editor.update_in(cx, |editor, window, cx| {
6942 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
6943
6944 editor
6945 .insert_snippet(&insertion_ranges, snippet, window, cx)
6946 .unwrap();
6947
6948 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
6949 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
6950 assert_eq!(editor.text(cx), expected_text);
6951 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
6952 }
6953
6954 assert(
6955 editor,
6956 cx,
6957 indoc! {"
6958 type «» =•
6959 "},
6960 );
6961
6962 assert!(editor.context_menu_visible(), "There should be a matches");
6963 });
6964}
6965
6966#[gpui::test]
6967async fn test_snippets(cx: &mut gpui::TestAppContext) {
6968 init_test(cx, |_| {});
6969
6970 let (text, insertion_ranges) = marked_text_ranges(
6971 indoc! {"
6972 a.ˇ b
6973 a.ˇ b
6974 a.ˇ b
6975 "},
6976 false,
6977 );
6978
6979 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
6980 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6981
6982 editor.update_in(cx, |editor, window, cx| {
6983 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
6984
6985 editor
6986 .insert_snippet(&insertion_ranges, snippet, window, cx)
6987 .unwrap();
6988
6989 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
6990 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
6991 assert_eq!(editor.text(cx), expected_text);
6992 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
6993 }
6994
6995 assert(
6996 editor,
6997 cx,
6998 indoc! {"
6999 a.f(«one», two, «three») b
7000 a.f(«one», two, «three») b
7001 a.f(«one», two, «three») b
7002 "},
7003 );
7004
7005 // Can't move earlier than the first tab stop
7006 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7007 assert(
7008 editor,
7009 cx,
7010 indoc! {"
7011 a.f(«one», two, «three») b
7012 a.f(«one», two, «three») b
7013 a.f(«one», two, «three») b
7014 "},
7015 );
7016
7017 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7018 assert(
7019 editor,
7020 cx,
7021 indoc! {"
7022 a.f(one, «two», three) b
7023 a.f(one, «two», three) b
7024 a.f(one, «two», three) b
7025 "},
7026 );
7027
7028 editor.move_to_prev_snippet_tabstop(window, cx);
7029 assert(
7030 editor,
7031 cx,
7032 indoc! {"
7033 a.f(«one», two, «three») b
7034 a.f(«one», two, «three») b
7035 a.f(«one», two, «three») b
7036 "},
7037 );
7038
7039 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7040 assert(
7041 editor,
7042 cx,
7043 indoc! {"
7044 a.f(one, «two», three) b
7045 a.f(one, «two», three) b
7046 a.f(one, «two», three) b
7047 "},
7048 );
7049 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7050 assert(
7051 editor,
7052 cx,
7053 indoc! {"
7054 a.f(one, two, three)ˇ b
7055 a.f(one, two, three)ˇ b
7056 a.f(one, two, three)ˇ b
7057 "},
7058 );
7059
7060 // As soon as the last tab stop is reached, snippet state is gone
7061 editor.move_to_prev_snippet_tabstop(window, cx);
7062 assert(
7063 editor,
7064 cx,
7065 indoc! {"
7066 a.f(one, two, three)ˇ b
7067 a.f(one, two, three)ˇ b
7068 a.f(one, two, three)ˇ b
7069 "},
7070 );
7071 });
7072}
7073
7074#[gpui::test]
7075async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
7076 init_test(cx, |_| {});
7077
7078 let fs = FakeFs::new(cx.executor());
7079 fs.insert_file(path!("/file.rs"), Default::default()).await;
7080
7081 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
7082
7083 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7084 language_registry.add(rust_lang());
7085 let mut fake_servers = language_registry.register_fake_lsp(
7086 "Rust",
7087 FakeLspAdapter {
7088 capabilities: lsp::ServerCapabilities {
7089 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7090 ..Default::default()
7091 },
7092 ..Default::default()
7093 },
7094 );
7095
7096 let buffer = project
7097 .update(cx, |project, cx| {
7098 project.open_local_buffer(path!("/file.rs"), cx)
7099 })
7100 .await
7101 .unwrap();
7102
7103 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7104 let (editor, cx) = cx.add_window_view(|window, cx| {
7105 build_editor_with_project(project.clone(), buffer, window, cx)
7106 });
7107 editor.update_in(cx, |editor, window, cx| {
7108 editor.set_text("one\ntwo\nthree\n", window, cx)
7109 });
7110 assert!(cx.read(|cx| editor.is_dirty(cx)));
7111
7112 cx.executor().start_waiting();
7113 let fake_server = fake_servers.next().await.unwrap();
7114
7115 let save = editor
7116 .update_in(cx, |editor, window, cx| {
7117 editor.save(true, project.clone(), window, cx)
7118 })
7119 .unwrap();
7120 fake_server
7121 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7122 assert_eq!(
7123 params.text_document.uri,
7124 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7125 );
7126 assert_eq!(params.options.tab_size, 4);
7127 Ok(Some(vec![lsp::TextEdit::new(
7128 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7129 ", ".to_string(),
7130 )]))
7131 })
7132 .next()
7133 .await;
7134 cx.executor().start_waiting();
7135 save.await;
7136
7137 assert_eq!(
7138 editor.update(cx, |editor, cx| editor.text(cx)),
7139 "one, two\nthree\n"
7140 );
7141 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7142
7143 editor.update_in(cx, |editor, window, cx| {
7144 editor.set_text("one\ntwo\nthree\n", window, cx)
7145 });
7146 assert!(cx.read(|cx| editor.is_dirty(cx)));
7147
7148 // Ensure we can still save even if formatting hangs.
7149 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7150 assert_eq!(
7151 params.text_document.uri,
7152 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7153 );
7154 futures::future::pending::<()>().await;
7155 unreachable!()
7156 });
7157 let save = editor
7158 .update_in(cx, |editor, window, cx| {
7159 editor.save(true, project.clone(), window, cx)
7160 })
7161 .unwrap();
7162 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7163 cx.executor().start_waiting();
7164 save.await;
7165 assert_eq!(
7166 editor.update(cx, |editor, cx| editor.text(cx)),
7167 "one\ntwo\nthree\n"
7168 );
7169 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7170
7171 // For non-dirty buffer, no formatting request should be sent
7172 let save = editor
7173 .update_in(cx, |editor, window, cx| {
7174 editor.save(true, project.clone(), window, cx)
7175 })
7176 .unwrap();
7177 let _pending_format_request = fake_server
7178 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7179 panic!("Should not be invoked on non-dirty buffer");
7180 })
7181 .next();
7182 cx.executor().start_waiting();
7183 save.await;
7184
7185 // Set rust language override and assert overridden tabsize is sent to language server
7186 update_test_language_settings(cx, |settings| {
7187 settings.languages.insert(
7188 "Rust".into(),
7189 LanguageSettingsContent {
7190 tab_size: NonZeroU32::new(8),
7191 ..Default::default()
7192 },
7193 );
7194 });
7195
7196 editor.update_in(cx, |editor, window, cx| {
7197 editor.set_text("somehting_new\n", window, cx)
7198 });
7199 assert!(cx.read(|cx| editor.is_dirty(cx)));
7200 let save = editor
7201 .update_in(cx, |editor, window, cx| {
7202 editor.save(true, project.clone(), window, cx)
7203 })
7204 .unwrap();
7205 fake_server
7206 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7207 assert_eq!(
7208 params.text_document.uri,
7209 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7210 );
7211 assert_eq!(params.options.tab_size, 8);
7212 Ok(Some(vec![]))
7213 })
7214 .next()
7215 .await;
7216 cx.executor().start_waiting();
7217 save.await;
7218}
7219
7220#[gpui::test]
7221async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) {
7222 init_test(cx, |_| {});
7223
7224 let cols = 4;
7225 let rows = 10;
7226 let sample_text_1 = sample_text(rows, cols, 'a');
7227 assert_eq!(
7228 sample_text_1,
7229 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7230 );
7231 let sample_text_2 = sample_text(rows, cols, 'l');
7232 assert_eq!(
7233 sample_text_2,
7234 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7235 );
7236 let sample_text_3 = sample_text(rows, cols, 'v');
7237 assert_eq!(
7238 sample_text_3,
7239 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7240 );
7241
7242 let fs = FakeFs::new(cx.executor());
7243 fs.insert_tree(
7244 path!("/a"),
7245 json!({
7246 "main.rs": sample_text_1,
7247 "other.rs": sample_text_2,
7248 "lib.rs": sample_text_3,
7249 }),
7250 )
7251 .await;
7252
7253 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
7254 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7255 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7256
7257 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7258 language_registry.add(rust_lang());
7259 let mut fake_servers = language_registry.register_fake_lsp(
7260 "Rust",
7261 FakeLspAdapter {
7262 capabilities: lsp::ServerCapabilities {
7263 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7264 ..Default::default()
7265 },
7266 ..Default::default()
7267 },
7268 );
7269
7270 let worktree = project.update(cx, |project, cx| {
7271 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7272 assert_eq!(worktrees.len(), 1);
7273 worktrees.pop().unwrap()
7274 });
7275 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
7276
7277 let buffer_1 = project
7278 .update(cx, |project, cx| {
7279 project.open_buffer((worktree_id, "main.rs"), cx)
7280 })
7281 .await
7282 .unwrap();
7283 let buffer_2 = project
7284 .update(cx, |project, cx| {
7285 project.open_buffer((worktree_id, "other.rs"), cx)
7286 })
7287 .await
7288 .unwrap();
7289 let buffer_3 = project
7290 .update(cx, |project, cx| {
7291 project.open_buffer((worktree_id, "lib.rs"), cx)
7292 })
7293 .await
7294 .unwrap();
7295
7296 let multi_buffer = cx.new(|cx| {
7297 let mut multi_buffer = MultiBuffer::new(ReadWrite);
7298 multi_buffer.push_excerpts(
7299 buffer_1.clone(),
7300 [
7301 ExcerptRange {
7302 context: Point::new(0, 0)..Point::new(3, 0),
7303 primary: None,
7304 },
7305 ExcerptRange {
7306 context: Point::new(5, 0)..Point::new(7, 0),
7307 primary: None,
7308 },
7309 ExcerptRange {
7310 context: Point::new(9, 0)..Point::new(10, 4),
7311 primary: None,
7312 },
7313 ],
7314 cx,
7315 );
7316 multi_buffer.push_excerpts(
7317 buffer_2.clone(),
7318 [
7319 ExcerptRange {
7320 context: Point::new(0, 0)..Point::new(3, 0),
7321 primary: None,
7322 },
7323 ExcerptRange {
7324 context: Point::new(5, 0)..Point::new(7, 0),
7325 primary: None,
7326 },
7327 ExcerptRange {
7328 context: Point::new(9, 0)..Point::new(10, 4),
7329 primary: None,
7330 },
7331 ],
7332 cx,
7333 );
7334 multi_buffer.push_excerpts(
7335 buffer_3.clone(),
7336 [
7337 ExcerptRange {
7338 context: Point::new(0, 0)..Point::new(3, 0),
7339 primary: None,
7340 },
7341 ExcerptRange {
7342 context: Point::new(5, 0)..Point::new(7, 0),
7343 primary: None,
7344 },
7345 ExcerptRange {
7346 context: Point::new(9, 0)..Point::new(10, 4),
7347 primary: None,
7348 },
7349 ],
7350 cx,
7351 );
7352 multi_buffer
7353 });
7354 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
7355 Editor::new(
7356 EditorMode::Full,
7357 multi_buffer,
7358 Some(project.clone()),
7359 true,
7360 window,
7361 cx,
7362 )
7363 });
7364
7365 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7366 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7367 s.select_ranges(Some(1..2))
7368 });
7369 editor.insert("|one|two|three|", window, cx);
7370 });
7371 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7372 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7373 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7374 s.select_ranges(Some(60..70))
7375 });
7376 editor.insert("|four|five|six|", window, cx);
7377 });
7378 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7379
7380 // First two buffers should be edited, but not the third one.
7381 assert_eq!(
7382 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7383 "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}",
7384 );
7385 buffer_1.update(cx, |buffer, _| {
7386 assert!(buffer.is_dirty());
7387 assert_eq!(
7388 buffer.text(),
7389 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
7390 )
7391 });
7392 buffer_2.update(cx, |buffer, _| {
7393 assert!(buffer.is_dirty());
7394 assert_eq!(
7395 buffer.text(),
7396 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
7397 )
7398 });
7399 buffer_3.update(cx, |buffer, _| {
7400 assert!(!buffer.is_dirty());
7401 assert_eq!(buffer.text(), sample_text_3,)
7402 });
7403 cx.executor().run_until_parked();
7404
7405 cx.executor().start_waiting();
7406 let save = multi_buffer_editor
7407 .update_in(cx, |editor, window, cx| {
7408 editor.save(true, project.clone(), window, cx)
7409 })
7410 .unwrap();
7411
7412 let fake_server = fake_servers.next().await.unwrap();
7413 fake_server
7414 .server
7415 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7416 Ok(Some(vec![lsp::TextEdit::new(
7417 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7418 format!("[{} formatted]", params.text_document.uri),
7419 )]))
7420 })
7421 .detach();
7422 save.await;
7423
7424 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
7425 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
7426 assert_eq!(
7427 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7428 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}"),
7429 );
7430 buffer_1.update(cx, |buffer, _| {
7431 assert!(!buffer.is_dirty());
7432 assert_eq!(
7433 buffer.text(),
7434 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
7435 )
7436 });
7437 buffer_2.update(cx, |buffer, _| {
7438 assert!(!buffer.is_dirty());
7439 assert_eq!(
7440 buffer.text(),
7441 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
7442 )
7443 });
7444 buffer_3.update(cx, |buffer, _| {
7445 assert!(!buffer.is_dirty());
7446 assert_eq!(buffer.text(), sample_text_3,)
7447 });
7448}
7449
7450#[gpui::test]
7451async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
7452 init_test(cx, |_| {});
7453
7454 let fs = FakeFs::new(cx.executor());
7455 fs.insert_file(path!("/file.rs"), Default::default()).await;
7456
7457 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
7458
7459 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7460 language_registry.add(rust_lang());
7461 let mut fake_servers = language_registry.register_fake_lsp(
7462 "Rust",
7463 FakeLspAdapter {
7464 capabilities: lsp::ServerCapabilities {
7465 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
7466 ..Default::default()
7467 },
7468 ..Default::default()
7469 },
7470 );
7471
7472 let buffer = project
7473 .update(cx, |project, cx| {
7474 project.open_local_buffer(path!("/file.rs"), cx)
7475 })
7476 .await
7477 .unwrap();
7478
7479 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7480 let (editor, cx) = cx.add_window_view(|window, cx| {
7481 build_editor_with_project(project.clone(), buffer, window, cx)
7482 });
7483 editor.update_in(cx, |editor, window, cx| {
7484 editor.set_text("one\ntwo\nthree\n", window, cx)
7485 });
7486 assert!(cx.read(|cx| editor.is_dirty(cx)));
7487
7488 cx.executor().start_waiting();
7489 let fake_server = fake_servers.next().await.unwrap();
7490
7491 let save = editor
7492 .update_in(cx, |editor, window, cx| {
7493 editor.save(true, project.clone(), window, cx)
7494 })
7495 .unwrap();
7496 fake_server
7497 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
7498 assert_eq!(
7499 params.text_document.uri,
7500 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7501 );
7502 assert_eq!(params.options.tab_size, 4);
7503 Ok(Some(vec![lsp::TextEdit::new(
7504 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7505 ", ".to_string(),
7506 )]))
7507 })
7508 .next()
7509 .await;
7510 cx.executor().start_waiting();
7511 save.await;
7512 assert_eq!(
7513 editor.update(cx, |editor, cx| editor.text(cx)),
7514 "one, two\nthree\n"
7515 );
7516 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7517
7518 editor.update_in(cx, |editor, window, cx| {
7519 editor.set_text("one\ntwo\nthree\n", window, cx)
7520 });
7521 assert!(cx.read(|cx| editor.is_dirty(cx)));
7522
7523 // Ensure we can still save even if formatting hangs.
7524 fake_server.handle_request::<lsp::request::RangeFormatting, _, _>(
7525 move |params, _| async move {
7526 assert_eq!(
7527 params.text_document.uri,
7528 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7529 );
7530 futures::future::pending::<()>().await;
7531 unreachable!()
7532 },
7533 );
7534 let save = editor
7535 .update_in(cx, |editor, window, cx| {
7536 editor.save(true, project.clone(), window, cx)
7537 })
7538 .unwrap();
7539 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7540 cx.executor().start_waiting();
7541 save.await;
7542 assert_eq!(
7543 editor.update(cx, |editor, cx| editor.text(cx)),
7544 "one\ntwo\nthree\n"
7545 );
7546 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7547
7548 // For non-dirty buffer, no formatting request should be sent
7549 let save = editor
7550 .update_in(cx, |editor, window, cx| {
7551 editor.save(true, project.clone(), window, cx)
7552 })
7553 .unwrap();
7554 let _pending_format_request = fake_server
7555 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7556 panic!("Should not be invoked on non-dirty buffer");
7557 })
7558 .next();
7559 cx.executor().start_waiting();
7560 save.await;
7561
7562 // Set Rust language override and assert overridden tabsize is sent to language server
7563 update_test_language_settings(cx, |settings| {
7564 settings.languages.insert(
7565 "Rust".into(),
7566 LanguageSettingsContent {
7567 tab_size: NonZeroU32::new(8),
7568 ..Default::default()
7569 },
7570 );
7571 });
7572
7573 editor.update_in(cx, |editor, window, cx| {
7574 editor.set_text("somehting_new\n", window, cx)
7575 });
7576 assert!(cx.read(|cx| editor.is_dirty(cx)));
7577 let save = editor
7578 .update_in(cx, |editor, window, cx| {
7579 editor.save(true, project.clone(), window, cx)
7580 })
7581 .unwrap();
7582 fake_server
7583 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
7584 assert_eq!(
7585 params.text_document.uri,
7586 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7587 );
7588 assert_eq!(params.options.tab_size, 8);
7589 Ok(Some(vec![]))
7590 })
7591 .next()
7592 .await;
7593 cx.executor().start_waiting();
7594 save.await;
7595}
7596
7597#[gpui::test]
7598async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
7599 init_test(cx, |settings| {
7600 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
7601 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
7602 ))
7603 });
7604
7605 let fs = FakeFs::new(cx.executor());
7606 fs.insert_file(path!("/file.rs"), Default::default()).await;
7607
7608 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
7609
7610 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7611 language_registry.add(Arc::new(Language::new(
7612 LanguageConfig {
7613 name: "Rust".into(),
7614 matcher: LanguageMatcher {
7615 path_suffixes: vec!["rs".to_string()],
7616 ..Default::default()
7617 },
7618 ..LanguageConfig::default()
7619 },
7620 Some(tree_sitter_rust::LANGUAGE.into()),
7621 )));
7622 update_test_language_settings(cx, |settings| {
7623 // Enable Prettier formatting for the same buffer, and ensure
7624 // LSP is called instead of Prettier.
7625 settings.defaults.prettier = Some(PrettierSettings {
7626 allowed: true,
7627 ..PrettierSettings::default()
7628 });
7629 });
7630 let mut fake_servers = language_registry.register_fake_lsp(
7631 "Rust",
7632 FakeLspAdapter {
7633 capabilities: lsp::ServerCapabilities {
7634 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7635 ..Default::default()
7636 },
7637 ..Default::default()
7638 },
7639 );
7640
7641 let buffer = project
7642 .update(cx, |project, cx| {
7643 project.open_local_buffer(path!("/file.rs"), cx)
7644 })
7645 .await
7646 .unwrap();
7647
7648 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7649 let (editor, cx) = cx.add_window_view(|window, cx| {
7650 build_editor_with_project(project.clone(), buffer, window, cx)
7651 });
7652 editor.update_in(cx, |editor, window, cx| {
7653 editor.set_text("one\ntwo\nthree\n", window, cx)
7654 });
7655
7656 cx.executor().start_waiting();
7657 let fake_server = fake_servers.next().await.unwrap();
7658
7659 let format = editor
7660 .update_in(cx, |editor, window, cx| {
7661 editor.perform_format(
7662 project.clone(),
7663 FormatTrigger::Manual,
7664 FormatTarget::Buffers,
7665 window,
7666 cx,
7667 )
7668 })
7669 .unwrap();
7670 fake_server
7671 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7672 assert_eq!(
7673 params.text_document.uri,
7674 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7675 );
7676 assert_eq!(params.options.tab_size, 4);
7677 Ok(Some(vec![lsp::TextEdit::new(
7678 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7679 ", ".to_string(),
7680 )]))
7681 })
7682 .next()
7683 .await;
7684 cx.executor().start_waiting();
7685 format.await;
7686 assert_eq!(
7687 editor.update(cx, |editor, cx| editor.text(cx)),
7688 "one, two\nthree\n"
7689 );
7690
7691 editor.update_in(cx, |editor, window, cx| {
7692 editor.set_text("one\ntwo\nthree\n", window, cx)
7693 });
7694 // Ensure we don't lock if formatting hangs.
7695 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7696 assert_eq!(
7697 params.text_document.uri,
7698 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7699 );
7700 futures::future::pending::<()>().await;
7701 unreachable!()
7702 });
7703 let format = editor
7704 .update_in(cx, |editor, window, cx| {
7705 editor.perform_format(
7706 project,
7707 FormatTrigger::Manual,
7708 FormatTarget::Buffers,
7709 window,
7710 cx,
7711 )
7712 })
7713 .unwrap();
7714 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7715 cx.executor().start_waiting();
7716 format.await;
7717 assert_eq!(
7718 editor.update(cx, |editor, cx| editor.text(cx)),
7719 "one\ntwo\nthree\n"
7720 );
7721}
7722
7723#[gpui::test]
7724async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
7725 init_test(cx, |_| {});
7726
7727 let mut cx = EditorLspTestContext::new_rust(
7728 lsp::ServerCapabilities {
7729 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7730 ..Default::default()
7731 },
7732 cx,
7733 )
7734 .await;
7735
7736 cx.set_state(indoc! {"
7737 one.twoˇ
7738 "});
7739
7740 // The format request takes a long time. When it completes, it inserts
7741 // a newline and an indent before the `.`
7742 cx.lsp
7743 .handle_request::<lsp::request::Formatting, _, _>(move |_, cx| {
7744 let executor = cx.background_executor().clone();
7745 async move {
7746 executor.timer(Duration::from_millis(100)).await;
7747 Ok(Some(vec![lsp::TextEdit {
7748 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
7749 new_text: "\n ".into(),
7750 }]))
7751 }
7752 });
7753
7754 // Submit a format request.
7755 let format_1 = cx
7756 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7757 .unwrap();
7758 cx.executor().run_until_parked();
7759
7760 // Submit a second format request.
7761 let format_2 = cx
7762 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7763 .unwrap();
7764 cx.executor().run_until_parked();
7765
7766 // Wait for both format requests to complete
7767 cx.executor().advance_clock(Duration::from_millis(200));
7768 cx.executor().start_waiting();
7769 format_1.await.unwrap();
7770 cx.executor().start_waiting();
7771 format_2.await.unwrap();
7772
7773 // The formatting edits only happens once.
7774 cx.assert_editor_state(indoc! {"
7775 one
7776 .twoˇ
7777 "});
7778}
7779
7780#[gpui::test]
7781async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
7782 init_test(cx, |settings| {
7783 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
7784 });
7785
7786 let mut cx = EditorLspTestContext::new_rust(
7787 lsp::ServerCapabilities {
7788 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7789 ..Default::default()
7790 },
7791 cx,
7792 )
7793 .await;
7794
7795 // Set up a buffer white some trailing whitespace and no trailing newline.
7796 cx.set_state(
7797 &[
7798 "one ", //
7799 "twoˇ", //
7800 "three ", //
7801 "four", //
7802 ]
7803 .join("\n"),
7804 );
7805
7806 // Submit a format request.
7807 let format = cx
7808 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7809 .unwrap();
7810
7811 // Record which buffer changes have been sent to the language server
7812 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
7813 cx.lsp
7814 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
7815 let buffer_changes = buffer_changes.clone();
7816 move |params, _| {
7817 buffer_changes.lock().extend(
7818 params
7819 .content_changes
7820 .into_iter()
7821 .map(|e| (e.range.unwrap(), e.text)),
7822 );
7823 }
7824 });
7825
7826 // Handle formatting requests to the language server.
7827 cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
7828 let buffer_changes = buffer_changes.clone();
7829 move |_, _| {
7830 // When formatting is requested, trailing whitespace has already been stripped,
7831 // and the trailing newline has already been added.
7832 assert_eq!(
7833 &buffer_changes.lock()[1..],
7834 &[
7835 (
7836 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
7837 "".into()
7838 ),
7839 (
7840 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
7841 "".into()
7842 ),
7843 (
7844 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
7845 "\n".into()
7846 ),
7847 ]
7848 );
7849
7850 // Insert blank lines between each line of the buffer.
7851 async move {
7852 Ok(Some(vec![
7853 lsp::TextEdit {
7854 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
7855 new_text: "\n".into(),
7856 },
7857 lsp::TextEdit {
7858 range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
7859 new_text: "\n".into(),
7860 },
7861 ]))
7862 }
7863 }
7864 });
7865
7866 // After formatting the buffer, the trailing whitespace is stripped,
7867 // a newline is appended, and the edits provided by the language server
7868 // have been applied.
7869 format.await.unwrap();
7870 cx.assert_editor_state(
7871 &[
7872 "one", //
7873 "", //
7874 "twoˇ", //
7875 "", //
7876 "three", //
7877 "four", //
7878 "", //
7879 ]
7880 .join("\n"),
7881 );
7882
7883 // Undoing the formatting undoes the trailing whitespace removal, the
7884 // trailing newline, and the LSP edits.
7885 cx.update_buffer(|buffer, cx| buffer.undo(cx));
7886 cx.assert_editor_state(
7887 &[
7888 "one ", //
7889 "twoˇ", //
7890 "three ", //
7891 "four", //
7892 ]
7893 .join("\n"),
7894 );
7895}
7896
7897#[gpui::test]
7898async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
7899 cx: &mut gpui::TestAppContext,
7900) {
7901 init_test(cx, |_| {});
7902
7903 cx.update(|cx| {
7904 cx.update_global::<SettingsStore, _>(|settings, cx| {
7905 settings.update_user_settings::<EditorSettings>(cx, |settings| {
7906 settings.auto_signature_help = Some(true);
7907 });
7908 });
7909 });
7910
7911 let mut cx = EditorLspTestContext::new_rust(
7912 lsp::ServerCapabilities {
7913 signature_help_provider: Some(lsp::SignatureHelpOptions {
7914 ..Default::default()
7915 }),
7916 ..Default::default()
7917 },
7918 cx,
7919 )
7920 .await;
7921
7922 let language = Language::new(
7923 LanguageConfig {
7924 name: "Rust".into(),
7925 brackets: BracketPairConfig {
7926 pairs: vec![
7927 BracketPair {
7928 start: "{".to_string(),
7929 end: "}".to_string(),
7930 close: true,
7931 surround: true,
7932 newline: true,
7933 },
7934 BracketPair {
7935 start: "(".to_string(),
7936 end: ")".to_string(),
7937 close: true,
7938 surround: true,
7939 newline: true,
7940 },
7941 BracketPair {
7942 start: "/*".to_string(),
7943 end: " */".to_string(),
7944 close: true,
7945 surround: true,
7946 newline: true,
7947 },
7948 BracketPair {
7949 start: "[".to_string(),
7950 end: "]".to_string(),
7951 close: false,
7952 surround: false,
7953 newline: true,
7954 },
7955 BracketPair {
7956 start: "\"".to_string(),
7957 end: "\"".to_string(),
7958 close: true,
7959 surround: true,
7960 newline: false,
7961 },
7962 BracketPair {
7963 start: "<".to_string(),
7964 end: ">".to_string(),
7965 close: false,
7966 surround: true,
7967 newline: true,
7968 },
7969 ],
7970 ..Default::default()
7971 },
7972 autoclose_before: "})]".to_string(),
7973 ..Default::default()
7974 },
7975 Some(tree_sitter_rust::LANGUAGE.into()),
7976 );
7977 let language = Arc::new(language);
7978
7979 cx.language_registry().add(language.clone());
7980 cx.update_buffer(|buffer, cx| {
7981 buffer.set_language(Some(language), cx);
7982 });
7983
7984 cx.set_state(
7985 &r#"
7986 fn main() {
7987 sampleˇ
7988 }
7989 "#
7990 .unindent(),
7991 );
7992
7993 cx.update_editor(|editor, window, cx| {
7994 editor.handle_input("(", window, cx);
7995 });
7996 cx.assert_editor_state(
7997 &"
7998 fn main() {
7999 sample(ˇ)
8000 }
8001 "
8002 .unindent(),
8003 );
8004
8005 let mocked_response = lsp::SignatureHelp {
8006 signatures: vec![lsp::SignatureInformation {
8007 label: "fn sample(param1: u8, param2: u8)".to_string(),
8008 documentation: None,
8009 parameters: Some(vec![
8010 lsp::ParameterInformation {
8011 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8012 documentation: None,
8013 },
8014 lsp::ParameterInformation {
8015 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8016 documentation: None,
8017 },
8018 ]),
8019 active_parameter: None,
8020 }],
8021 active_signature: Some(0),
8022 active_parameter: Some(0),
8023 };
8024 handle_signature_help_request(&mut cx, mocked_response).await;
8025
8026 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8027 .await;
8028
8029 cx.editor(|editor, _, _| {
8030 let signature_help_state = editor.signature_help_state.popover().cloned();
8031 assert!(signature_help_state.is_some());
8032 let ParsedMarkdown {
8033 text, highlights, ..
8034 } = signature_help_state.unwrap().parsed_content;
8035 assert_eq!(text, "param1: u8, param2: u8");
8036 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8037 });
8038}
8039
8040#[gpui::test]
8041async fn test_handle_input_with_different_show_signature_settings(cx: &mut gpui::TestAppContext) {
8042 init_test(cx, |_| {});
8043
8044 cx.update(|cx| {
8045 cx.update_global::<SettingsStore, _>(|settings, cx| {
8046 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8047 settings.auto_signature_help = Some(false);
8048 settings.show_signature_help_after_edits = Some(false);
8049 });
8050 });
8051 });
8052
8053 let mut cx = EditorLspTestContext::new_rust(
8054 lsp::ServerCapabilities {
8055 signature_help_provider: Some(lsp::SignatureHelpOptions {
8056 ..Default::default()
8057 }),
8058 ..Default::default()
8059 },
8060 cx,
8061 )
8062 .await;
8063
8064 let language = Language::new(
8065 LanguageConfig {
8066 name: "Rust".into(),
8067 brackets: BracketPairConfig {
8068 pairs: vec![
8069 BracketPair {
8070 start: "{".to_string(),
8071 end: "}".to_string(),
8072 close: true,
8073 surround: true,
8074 newline: true,
8075 },
8076 BracketPair {
8077 start: "(".to_string(),
8078 end: ")".to_string(),
8079 close: true,
8080 surround: true,
8081 newline: true,
8082 },
8083 BracketPair {
8084 start: "/*".to_string(),
8085 end: " */".to_string(),
8086 close: true,
8087 surround: true,
8088 newline: true,
8089 },
8090 BracketPair {
8091 start: "[".to_string(),
8092 end: "]".to_string(),
8093 close: false,
8094 surround: false,
8095 newline: true,
8096 },
8097 BracketPair {
8098 start: "\"".to_string(),
8099 end: "\"".to_string(),
8100 close: true,
8101 surround: true,
8102 newline: false,
8103 },
8104 BracketPair {
8105 start: "<".to_string(),
8106 end: ">".to_string(),
8107 close: false,
8108 surround: true,
8109 newline: true,
8110 },
8111 ],
8112 ..Default::default()
8113 },
8114 autoclose_before: "})]".to_string(),
8115 ..Default::default()
8116 },
8117 Some(tree_sitter_rust::LANGUAGE.into()),
8118 );
8119 let language = Arc::new(language);
8120
8121 cx.language_registry().add(language.clone());
8122 cx.update_buffer(|buffer, cx| {
8123 buffer.set_language(Some(language), cx);
8124 });
8125
8126 // Ensure that signature_help is not called when no signature help is enabled.
8127 cx.set_state(
8128 &r#"
8129 fn main() {
8130 sampleˇ
8131 }
8132 "#
8133 .unindent(),
8134 );
8135 cx.update_editor(|editor, window, cx| {
8136 editor.handle_input("(", window, cx);
8137 });
8138 cx.assert_editor_state(
8139 &"
8140 fn main() {
8141 sample(ˇ)
8142 }
8143 "
8144 .unindent(),
8145 );
8146 cx.editor(|editor, _, _| {
8147 assert!(editor.signature_help_state.task().is_none());
8148 });
8149
8150 let mocked_response = lsp::SignatureHelp {
8151 signatures: vec![lsp::SignatureInformation {
8152 label: "fn sample(param1: u8, param2: u8)".to_string(),
8153 documentation: None,
8154 parameters: Some(vec![
8155 lsp::ParameterInformation {
8156 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8157 documentation: None,
8158 },
8159 lsp::ParameterInformation {
8160 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8161 documentation: None,
8162 },
8163 ]),
8164 active_parameter: None,
8165 }],
8166 active_signature: Some(0),
8167 active_parameter: Some(0),
8168 };
8169
8170 // Ensure that signature_help is called when enabled afte edits
8171 cx.update(|_, cx| {
8172 cx.update_global::<SettingsStore, _>(|settings, cx| {
8173 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8174 settings.auto_signature_help = Some(false);
8175 settings.show_signature_help_after_edits = Some(true);
8176 });
8177 });
8178 });
8179 cx.set_state(
8180 &r#"
8181 fn main() {
8182 sampleˇ
8183 }
8184 "#
8185 .unindent(),
8186 );
8187 cx.update_editor(|editor, window, cx| {
8188 editor.handle_input("(", window, cx);
8189 });
8190 cx.assert_editor_state(
8191 &"
8192 fn main() {
8193 sample(ˇ)
8194 }
8195 "
8196 .unindent(),
8197 );
8198 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8199 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8200 .await;
8201 cx.update_editor(|editor, _, _| {
8202 let signature_help_state = editor.signature_help_state.popover().cloned();
8203 assert!(signature_help_state.is_some());
8204 let ParsedMarkdown {
8205 text, highlights, ..
8206 } = signature_help_state.unwrap().parsed_content;
8207 assert_eq!(text, "param1: u8, param2: u8");
8208 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8209 editor.signature_help_state = SignatureHelpState::default();
8210 });
8211
8212 // Ensure that signature_help is called when auto signature help override is enabled
8213 cx.update(|_, cx| {
8214 cx.update_global::<SettingsStore, _>(|settings, cx| {
8215 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8216 settings.auto_signature_help = Some(true);
8217 settings.show_signature_help_after_edits = Some(false);
8218 });
8219 });
8220 });
8221 cx.set_state(
8222 &r#"
8223 fn main() {
8224 sampleˇ
8225 }
8226 "#
8227 .unindent(),
8228 );
8229 cx.update_editor(|editor, window, cx| {
8230 editor.handle_input("(", window, cx);
8231 });
8232 cx.assert_editor_state(
8233 &"
8234 fn main() {
8235 sample(ˇ)
8236 }
8237 "
8238 .unindent(),
8239 );
8240 handle_signature_help_request(&mut cx, mocked_response).await;
8241 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8242 .await;
8243 cx.editor(|editor, _, _| {
8244 let signature_help_state = editor.signature_help_state.popover().cloned();
8245 assert!(signature_help_state.is_some());
8246 let ParsedMarkdown {
8247 text, highlights, ..
8248 } = signature_help_state.unwrap().parsed_content;
8249 assert_eq!(text, "param1: u8, param2: u8");
8250 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8251 });
8252}
8253
8254#[gpui::test]
8255async fn test_signature_help(cx: &mut gpui::TestAppContext) {
8256 init_test(cx, |_| {});
8257 cx.update(|cx| {
8258 cx.update_global::<SettingsStore, _>(|settings, cx| {
8259 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8260 settings.auto_signature_help = Some(true);
8261 });
8262 });
8263 });
8264
8265 let mut cx = EditorLspTestContext::new_rust(
8266 lsp::ServerCapabilities {
8267 signature_help_provider: Some(lsp::SignatureHelpOptions {
8268 ..Default::default()
8269 }),
8270 ..Default::default()
8271 },
8272 cx,
8273 )
8274 .await;
8275
8276 // A test that directly calls `show_signature_help`
8277 cx.update_editor(|editor, window, cx| {
8278 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8279 });
8280
8281 let mocked_response = lsp::SignatureHelp {
8282 signatures: vec![lsp::SignatureInformation {
8283 label: "fn sample(param1: u8, param2: u8)".to_string(),
8284 documentation: None,
8285 parameters: Some(vec![
8286 lsp::ParameterInformation {
8287 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8288 documentation: None,
8289 },
8290 lsp::ParameterInformation {
8291 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8292 documentation: None,
8293 },
8294 ]),
8295 active_parameter: None,
8296 }],
8297 active_signature: Some(0),
8298 active_parameter: Some(0),
8299 };
8300 handle_signature_help_request(&mut cx, mocked_response).await;
8301
8302 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8303 .await;
8304
8305 cx.editor(|editor, _, _| {
8306 let signature_help_state = editor.signature_help_state.popover().cloned();
8307 assert!(signature_help_state.is_some());
8308 let ParsedMarkdown {
8309 text, highlights, ..
8310 } = signature_help_state.unwrap().parsed_content;
8311 assert_eq!(text, "param1: u8, param2: u8");
8312 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8313 });
8314
8315 // When exiting outside from inside the brackets, `signature_help` is closed.
8316 cx.set_state(indoc! {"
8317 fn main() {
8318 sample(ˇ);
8319 }
8320
8321 fn sample(param1: u8, param2: u8) {}
8322 "});
8323
8324 cx.update_editor(|editor, window, cx| {
8325 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
8326 });
8327
8328 let mocked_response = lsp::SignatureHelp {
8329 signatures: Vec::new(),
8330 active_signature: None,
8331 active_parameter: None,
8332 };
8333 handle_signature_help_request(&mut cx, mocked_response).await;
8334
8335 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
8336 .await;
8337
8338 cx.editor(|editor, _, _| {
8339 assert!(!editor.signature_help_state.is_shown());
8340 });
8341
8342 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
8343 cx.set_state(indoc! {"
8344 fn main() {
8345 sample(ˇ);
8346 }
8347
8348 fn sample(param1: u8, param2: u8) {}
8349 "});
8350
8351 let mocked_response = lsp::SignatureHelp {
8352 signatures: vec![lsp::SignatureInformation {
8353 label: "fn sample(param1: u8, param2: u8)".to_string(),
8354 documentation: None,
8355 parameters: Some(vec![
8356 lsp::ParameterInformation {
8357 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8358 documentation: None,
8359 },
8360 lsp::ParameterInformation {
8361 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8362 documentation: None,
8363 },
8364 ]),
8365 active_parameter: None,
8366 }],
8367 active_signature: Some(0),
8368 active_parameter: Some(0),
8369 };
8370 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8371 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8372 .await;
8373 cx.editor(|editor, _, _| {
8374 assert!(editor.signature_help_state.is_shown());
8375 });
8376
8377 // Restore the popover with more parameter input
8378 cx.set_state(indoc! {"
8379 fn main() {
8380 sample(param1, param2ˇ);
8381 }
8382
8383 fn sample(param1: u8, param2: u8) {}
8384 "});
8385
8386 let mocked_response = lsp::SignatureHelp {
8387 signatures: vec![lsp::SignatureInformation {
8388 label: "fn sample(param1: u8, param2: u8)".to_string(),
8389 documentation: None,
8390 parameters: Some(vec![
8391 lsp::ParameterInformation {
8392 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8393 documentation: None,
8394 },
8395 lsp::ParameterInformation {
8396 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8397 documentation: None,
8398 },
8399 ]),
8400 active_parameter: None,
8401 }],
8402 active_signature: Some(0),
8403 active_parameter: Some(1),
8404 };
8405 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8406 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8407 .await;
8408
8409 // When selecting a range, the popover is gone.
8410 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
8411 cx.update_editor(|editor, window, cx| {
8412 editor.change_selections(None, window, cx, |s| {
8413 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
8414 })
8415 });
8416 cx.assert_editor_state(indoc! {"
8417 fn main() {
8418 sample(param1, «ˇparam2»);
8419 }
8420
8421 fn sample(param1: u8, param2: u8) {}
8422 "});
8423 cx.editor(|editor, _, _| {
8424 assert!(!editor.signature_help_state.is_shown());
8425 });
8426
8427 // When unselecting again, the popover is back if within the brackets.
8428 cx.update_editor(|editor, window, cx| {
8429 editor.change_selections(None, window, cx, |s| {
8430 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8431 })
8432 });
8433 cx.assert_editor_state(indoc! {"
8434 fn main() {
8435 sample(param1, ˇparam2);
8436 }
8437
8438 fn sample(param1: u8, param2: u8) {}
8439 "});
8440 handle_signature_help_request(&mut cx, mocked_response).await;
8441 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8442 .await;
8443 cx.editor(|editor, _, _| {
8444 assert!(editor.signature_help_state.is_shown());
8445 });
8446
8447 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
8448 cx.update_editor(|editor, window, cx| {
8449 editor.change_selections(None, window, cx, |s| {
8450 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
8451 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8452 })
8453 });
8454 cx.assert_editor_state(indoc! {"
8455 fn main() {
8456 sample(param1, ˇparam2);
8457 }
8458
8459 fn sample(param1: u8, param2: u8) {}
8460 "});
8461
8462 let mocked_response = lsp::SignatureHelp {
8463 signatures: vec![lsp::SignatureInformation {
8464 label: "fn sample(param1: u8, param2: u8)".to_string(),
8465 documentation: None,
8466 parameters: Some(vec![
8467 lsp::ParameterInformation {
8468 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8469 documentation: None,
8470 },
8471 lsp::ParameterInformation {
8472 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8473 documentation: None,
8474 },
8475 ]),
8476 active_parameter: None,
8477 }],
8478 active_signature: Some(0),
8479 active_parameter: Some(1),
8480 };
8481 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8482 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8483 .await;
8484 cx.update_editor(|editor, _, cx| {
8485 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
8486 });
8487 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
8488 .await;
8489 cx.update_editor(|editor, window, cx| {
8490 editor.change_selections(None, window, cx, |s| {
8491 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
8492 })
8493 });
8494 cx.assert_editor_state(indoc! {"
8495 fn main() {
8496 sample(param1, «ˇparam2»);
8497 }
8498
8499 fn sample(param1: u8, param2: u8) {}
8500 "});
8501 cx.update_editor(|editor, window, cx| {
8502 editor.change_selections(None, window, cx, |s| {
8503 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8504 })
8505 });
8506 cx.assert_editor_state(indoc! {"
8507 fn main() {
8508 sample(param1, ˇparam2);
8509 }
8510
8511 fn sample(param1: u8, param2: u8) {}
8512 "});
8513 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
8514 .await;
8515}
8516
8517#[gpui::test]
8518async fn test_completion(cx: &mut gpui::TestAppContext) {
8519 init_test(cx, |_| {});
8520
8521 let mut cx = EditorLspTestContext::new_rust(
8522 lsp::ServerCapabilities {
8523 completion_provider: Some(lsp::CompletionOptions {
8524 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
8525 resolve_provider: Some(true),
8526 ..Default::default()
8527 }),
8528 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
8529 ..Default::default()
8530 },
8531 cx,
8532 )
8533 .await;
8534 let counter = Arc::new(AtomicUsize::new(0));
8535
8536 cx.set_state(indoc! {"
8537 oneˇ
8538 two
8539 three
8540 "});
8541 cx.simulate_keystroke(".");
8542 handle_completion_request(
8543 &mut cx,
8544 indoc! {"
8545 one.|<>
8546 two
8547 three
8548 "},
8549 vec!["first_completion", "second_completion"],
8550 counter.clone(),
8551 )
8552 .await;
8553 cx.condition(|editor, _| editor.context_menu_visible())
8554 .await;
8555 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
8556
8557 let _handler = handle_signature_help_request(
8558 &mut cx,
8559 lsp::SignatureHelp {
8560 signatures: vec![lsp::SignatureInformation {
8561 label: "test signature".to_string(),
8562 documentation: None,
8563 parameters: Some(vec![lsp::ParameterInformation {
8564 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
8565 documentation: None,
8566 }]),
8567 active_parameter: None,
8568 }],
8569 active_signature: None,
8570 active_parameter: None,
8571 },
8572 );
8573 cx.update_editor(|editor, window, cx| {
8574 assert!(
8575 !editor.signature_help_state.is_shown(),
8576 "No signature help was called for"
8577 );
8578 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8579 });
8580 cx.run_until_parked();
8581 cx.update_editor(|editor, _, _| {
8582 assert!(
8583 !editor.signature_help_state.is_shown(),
8584 "No signature help should be shown when completions menu is open"
8585 );
8586 });
8587
8588 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8589 editor.context_menu_next(&Default::default(), window, cx);
8590 editor
8591 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8592 .unwrap()
8593 });
8594 cx.assert_editor_state(indoc! {"
8595 one.second_completionˇ
8596 two
8597 three
8598 "});
8599
8600 handle_resolve_completion_request(
8601 &mut cx,
8602 Some(vec![
8603 (
8604 //This overlaps with the primary completion edit which is
8605 //misbehavior from the LSP spec, test that we filter it out
8606 indoc! {"
8607 one.second_ˇcompletion
8608 two
8609 threeˇ
8610 "},
8611 "overlapping additional edit",
8612 ),
8613 (
8614 indoc! {"
8615 one.second_completion
8616 two
8617 threeˇ
8618 "},
8619 "\nadditional edit",
8620 ),
8621 ]),
8622 )
8623 .await;
8624 apply_additional_edits.await.unwrap();
8625 cx.assert_editor_state(indoc! {"
8626 one.second_completionˇ
8627 two
8628 three
8629 additional edit
8630 "});
8631
8632 cx.set_state(indoc! {"
8633 one.second_completion
8634 twoˇ
8635 threeˇ
8636 additional edit
8637 "});
8638 cx.simulate_keystroke(" ");
8639 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8640 cx.simulate_keystroke("s");
8641 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8642
8643 cx.assert_editor_state(indoc! {"
8644 one.second_completion
8645 two sˇ
8646 three sˇ
8647 additional edit
8648 "});
8649 handle_completion_request(
8650 &mut cx,
8651 indoc! {"
8652 one.second_completion
8653 two s
8654 three <s|>
8655 additional edit
8656 "},
8657 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
8658 counter.clone(),
8659 )
8660 .await;
8661 cx.condition(|editor, _| editor.context_menu_visible())
8662 .await;
8663 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
8664
8665 cx.simulate_keystroke("i");
8666
8667 handle_completion_request(
8668 &mut cx,
8669 indoc! {"
8670 one.second_completion
8671 two si
8672 three <si|>
8673 additional edit
8674 "},
8675 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
8676 counter.clone(),
8677 )
8678 .await;
8679 cx.condition(|editor, _| editor.context_menu_visible())
8680 .await;
8681 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
8682
8683 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8684 editor
8685 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8686 .unwrap()
8687 });
8688 cx.assert_editor_state(indoc! {"
8689 one.second_completion
8690 two sixth_completionˇ
8691 three sixth_completionˇ
8692 additional edit
8693 "});
8694
8695 apply_additional_edits.await.unwrap();
8696
8697 update_test_language_settings(&mut cx, |settings| {
8698 settings.defaults.show_completions_on_input = Some(false);
8699 });
8700 cx.set_state("editorˇ");
8701 cx.simulate_keystroke(".");
8702 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8703 cx.simulate_keystroke("c");
8704 cx.simulate_keystroke("l");
8705 cx.simulate_keystroke("o");
8706 cx.assert_editor_state("editor.cloˇ");
8707 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8708 cx.update_editor(|editor, window, cx| {
8709 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
8710 });
8711 handle_completion_request(
8712 &mut cx,
8713 "editor.<clo|>",
8714 vec!["close", "clobber"],
8715 counter.clone(),
8716 )
8717 .await;
8718 cx.condition(|editor, _| editor.context_menu_visible())
8719 .await;
8720 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
8721
8722 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8723 editor
8724 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8725 .unwrap()
8726 });
8727 cx.assert_editor_state("editor.closeˇ");
8728 handle_resolve_completion_request(&mut cx, None).await;
8729 apply_additional_edits.await.unwrap();
8730}
8731
8732#[gpui::test]
8733async fn test_multiline_completion(cx: &mut gpui::TestAppContext) {
8734 init_test(cx, |_| {});
8735
8736 let fs = FakeFs::new(cx.executor());
8737 fs.insert_tree(
8738 path!("/a"),
8739 json!({
8740 "main.ts": "a",
8741 }),
8742 )
8743 .await;
8744
8745 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8746 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8747 let typescript_language = Arc::new(Language::new(
8748 LanguageConfig {
8749 name: "TypeScript".into(),
8750 matcher: LanguageMatcher {
8751 path_suffixes: vec!["ts".to_string()],
8752 ..LanguageMatcher::default()
8753 },
8754 line_comments: vec!["// ".into()],
8755 ..LanguageConfig::default()
8756 },
8757 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8758 ));
8759 language_registry.add(typescript_language.clone());
8760 let mut fake_servers = language_registry.register_fake_lsp(
8761 "TypeScript",
8762 FakeLspAdapter {
8763 capabilities: lsp::ServerCapabilities {
8764 completion_provider: Some(lsp::CompletionOptions {
8765 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
8766 ..lsp::CompletionOptions::default()
8767 }),
8768 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
8769 ..lsp::ServerCapabilities::default()
8770 },
8771 // Emulate vtsls label generation
8772 label_for_completion: Some(Box::new(|item, _| {
8773 let text = if let Some(description) = item
8774 .label_details
8775 .as_ref()
8776 .and_then(|label_details| label_details.description.as_ref())
8777 {
8778 format!("{} {}", item.label, description)
8779 } else if let Some(detail) = &item.detail {
8780 format!("{} {}", item.label, detail)
8781 } else {
8782 item.label.clone()
8783 };
8784 let len = text.len();
8785 Some(language::CodeLabel {
8786 text,
8787 runs: Vec::new(),
8788 filter_range: 0..len,
8789 })
8790 })),
8791 ..FakeLspAdapter::default()
8792 },
8793 );
8794 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8795 let cx = &mut VisualTestContext::from_window(*workspace, cx);
8796 let worktree_id = workspace
8797 .update(cx, |workspace, _window, cx| {
8798 workspace.project().update(cx, |project, cx| {
8799 project.worktrees(cx).next().unwrap().read(cx).id()
8800 })
8801 })
8802 .unwrap();
8803 let _buffer = project
8804 .update(cx, |project, cx| {
8805 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
8806 })
8807 .await
8808 .unwrap();
8809 let editor = workspace
8810 .update(cx, |workspace, window, cx| {
8811 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
8812 })
8813 .unwrap()
8814 .await
8815 .unwrap()
8816 .downcast::<Editor>()
8817 .unwrap();
8818 let fake_server = fake_servers.next().await.unwrap();
8819
8820 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
8821 let multiline_label_2 = "a\nb\nc\n";
8822 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
8823 let multiline_description = "d\ne\nf\n";
8824 let multiline_detail_2 = "g\nh\ni\n";
8825
8826 let mut completion_handle =
8827 fake_server.handle_request::<lsp::request::Completion, _, _>(move |params, _| async move {
8828 Ok(Some(lsp::CompletionResponse::Array(vec![
8829 lsp::CompletionItem {
8830 label: multiline_label.to_string(),
8831 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8832 range: lsp::Range {
8833 start: lsp::Position {
8834 line: params.text_document_position.position.line,
8835 character: params.text_document_position.position.character,
8836 },
8837 end: lsp::Position {
8838 line: params.text_document_position.position.line,
8839 character: params.text_document_position.position.character,
8840 },
8841 },
8842 new_text: "new_text_1".to_string(),
8843 })),
8844 ..lsp::CompletionItem::default()
8845 },
8846 lsp::CompletionItem {
8847 label: "single line label 1".to_string(),
8848 detail: Some(multiline_detail.to_string()),
8849 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8850 range: lsp::Range {
8851 start: lsp::Position {
8852 line: params.text_document_position.position.line,
8853 character: params.text_document_position.position.character,
8854 },
8855 end: lsp::Position {
8856 line: params.text_document_position.position.line,
8857 character: params.text_document_position.position.character,
8858 },
8859 },
8860 new_text: "new_text_2".to_string(),
8861 })),
8862 ..lsp::CompletionItem::default()
8863 },
8864 lsp::CompletionItem {
8865 label: "single line label 2".to_string(),
8866 label_details: Some(lsp::CompletionItemLabelDetails {
8867 description: Some(multiline_description.to_string()),
8868 detail: None,
8869 }),
8870 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8871 range: lsp::Range {
8872 start: lsp::Position {
8873 line: params.text_document_position.position.line,
8874 character: params.text_document_position.position.character,
8875 },
8876 end: lsp::Position {
8877 line: params.text_document_position.position.line,
8878 character: params.text_document_position.position.character,
8879 },
8880 },
8881 new_text: "new_text_2".to_string(),
8882 })),
8883 ..lsp::CompletionItem::default()
8884 },
8885 lsp::CompletionItem {
8886 label: multiline_label_2.to_string(),
8887 detail: Some(multiline_detail_2.to_string()),
8888 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8889 range: lsp::Range {
8890 start: lsp::Position {
8891 line: params.text_document_position.position.line,
8892 character: params.text_document_position.position.character,
8893 },
8894 end: lsp::Position {
8895 line: params.text_document_position.position.line,
8896 character: params.text_document_position.position.character,
8897 },
8898 },
8899 new_text: "new_text_3".to_string(),
8900 })),
8901 ..lsp::CompletionItem::default()
8902 },
8903 lsp::CompletionItem {
8904 label: "Label with many spaces and \t but without newlines".to_string(),
8905 detail: Some(
8906 "Details with many spaces and \t but without newlines".to_string(),
8907 ),
8908 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8909 range: lsp::Range {
8910 start: lsp::Position {
8911 line: params.text_document_position.position.line,
8912 character: params.text_document_position.position.character,
8913 },
8914 end: lsp::Position {
8915 line: params.text_document_position.position.line,
8916 character: params.text_document_position.position.character,
8917 },
8918 },
8919 new_text: "new_text_4".to_string(),
8920 })),
8921 ..lsp::CompletionItem::default()
8922 },
8923 ])))
8924 });
8925
8926 editor.update_in(cx, |editor, window, cx| {
8927 cx.focus_self(window);
8928 editor.move_to_end(&MoveToEnd, window, cx);
8929 editor.handle_input(".", window, cx);
8930 });
8931 cx.run_until_parked();
8932 completion_handle.next().await.unwrap();
8933
8934 editor.update(cx, |editor, _| {
8935 assert!(editor.context_menu_visible());
8936 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
8937 {
8938 let completion_labels = menu
8939 .completions
8940 .borrow()
8941 .iter()
8942 .map(|c| c.label.text.clone())
8943 .collect::<Vec<_>>();
8944 assert_eq!(
8945 completion_labels,
8946 &[
8947 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
8948 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
8949 "single line label 2 d e f ",
8950 "a b c g h i ",
8951 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
8952 ],
8953 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
8954 );
8955
8956 for completion in menu
8957 .completions
8958 .borrow()
8959 .iter() {
8960 assert_eq!(
8961 completion.label.filter_range,
8962 0..completion.label.text.len(),
8963 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
8964 );
8965 }
8966
8967 } else {
8968 panic!("expected completion menu to be open");
8969 }
8970 });
8971}
8972
8973#[gpui::test]
8974async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
8975 init_test(cx, |_| {});
8976 let mut cx = EditorLspTestContext::new_rust(
8977 lsp::ServerCapabilities {
8978 completion_provider: Some(lsp::CompletionOptions {
8979 trigger_characters: Some(vec![".".to_string()]),
8980 ..Default::default()
8981 }),
8982 ..Default::default()
8983 },
8984 cx,
8985 )
8986 .await;
8987 cx.lsp
8988 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
8989 Ok(Some(lsp::CompletionResponse::Array(vec![
8990 lsp::CompletionItem {
8991 label: "first".into(),
8992 ..Default::default()
8993 },
8994 lsp::CompletionItem {
8995 label: "last".into(),
8996 ..Default::default()
8997 },
8998 ])))
8999 });
9000 cx.set_state("variableˇ");
9001 cx.simulate_keystroke(".");
9002 cx.executor().run_until_parked();
9003
9004 cx.update_editor(|editor, _, _| {
9005 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9006 {
9007 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
9008 } else {
9009 panic!("expected completion menu to be open");
9010 }
9011 });
9012
9013 cx.update_editor(|editor, window, cx| {
9014 editor.move_page_down(&MovePageDown::default(), window, cx);
9015 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9016 {
9017 assert!(
9018 menu.selected_item == 1,
9019 "expected PageDown to select the last item from the context menu"
9020 );
9021 } else {
9022 panic!("expected completion menu to stay open after PageDown");
9023 }
9024 });
9025
9026 cx.update_editor(|editor, window, cx| {
9027 editor.move_page_up(&MovePageUp::default(), window, cx);
9028 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9029 {
9030 assert!(
9031 menu.selected_item == 0,
9032 "expected PageUp to select the first item from the context menu"
9033 );
9034 } else {
9035 panic!("expected completion menu to stay open after PageUp");
9036 }
9037 });
9038}
9039
9040#[gpui::test]
9041async fn test_completion_sort(cx: &mut gpui::TestAppContext) {
9042 init_test(cx, |_| {});
9043 let mut cx = EditorLspTestContext::new_rust(
9044 lsp::ServerCapabilities {
9045 completion_provider: Some(lsp::CompletionOptions {
9046 trigger_characters: Some(vec![".".to_string()]),
9047 ..Default::default()
9048 }),
9049 ..Default::default()
9050 },
9051 cx,
9052 )
9053 .await;
9054 cx.lsp
9055 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9056 Ok(Some(lsp::CompletionResponse::Array(vec![
9057 lsp::CompletionItem {
9058 label: "Range".into(),
9059 sort_text: Some("a".into()),
9060 ..Default::default()
9061 },
9062 lsp::CompletionItem {
9063 label: "r".into(),
9064 sort_text: Some("b".into()),
9065 ..Default::default()
9066 },
9067 lsp::CompletionItem {
9068 label: "ret".into(),
9069 sort_text: Some("c".into()),
9070 ..Default::default()
9071 },
9072 lsp::CompletionItem {
9073 label: "return".into(),
9074 sort_text: Some("d".into()),
9075 ..Default::default()
9076 },
9077 lsp::CompletionItem {
9078 label: "slice".into(),
9079 sort_text: Some("d".into()),
9080 ..Default::default()
9081 },
9082 ])))
9083 });
9084 cx.set_state("rˇ");
9085 cx.executor().run_until_parked();
9086 cx.update_editor(|editor, window, cx| {
9087 editor.show_completions(
9088 &ShowCompletions {
9089 trigger: Some("r".into()),
9090 },
9091 window,
9092 cx,
9093 );
9094 });
9095 cx.executor().run_until_parked();
9096
9097 cx.update_editor(|editor, _, _| {
9098 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9099 {
9100 assert_eq!(
9101 completion_menu_entries(&menu),
9102 &["r", "ret", "Range", "return"]
9103 );
9104 } else {
9105 panic!("expected completion menu to be open");
9106 }
9107 });
9108}
9109
9110#[gpui::test]
9111async fn test_no_duplicated_completion_requests(cx: &mut gpui::TestAppContext) {
9112 init_test(cx, |_| {});
9113
9114 let mut cx = EditorLspTestContext::new_rust(
9115 lsp::ServerCapabilities {
9116 completion_provider: Some(lsp::CompletionOptions {
9117 trigger_characters: Some(vec![".".to_string()]),
9118 resolve_provider: Some(true),
9119 ..Default::default()
9120 }),
9121 ..Default::default()
9122 },
9123 cx,
9124 )
9125 .await;
9126
9127 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
9128 cx.simulate_keystroke(".");
9129 let completion_item = lsp::CompletionItem {
9130 label: "Some".into(),
9131 kind: Some(lsp::CompletionItemKind::SNIPPET),
9132 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
9133 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
9134 kind: lsp::MarkupKind::Markdown,
9135 value: "```rust\nSome(2)\n```".to_string(),
9136 })),
9137 deprecated: Some(false),
9138 sort_text: Some("Some".to_string()),
9139 filter_text: Some("Some".to_string()),
9140 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
9141 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9142 range: lsp::Range {
9143 start: lsp::Position {
9144 line: 0,
9145 character: 22,
9146 },
9147 end: lsp::Position {
9148 line: 0,
9149 character: 22,
9150 },
9151 },
9152 new_text: "Some(2)".to_string(),
9153 })),
9154 additional_text_edits: Some(vec![lsp::TextEdit {
9155 range: lsp::Range {
9156 start: lsp::Position {
9157 line: 0,
9158 character: 20,
9159 },
9160 end: lsp::Position {
9161 line: 0,
9162 character: 22,
9163 },
9164 },
9165 new_text: "".to_string(),
9166 }]),
9167 ..Default::default()
9168 };
9169
9170 let closure_completion_item = completion_item.clone();
9171 let counter = Arc::new(AtomicUsize::new(0));
9172 let counter_clone = counter.clone();
9173 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
9174 let task_completion_item = closure_completion_item.clone();
9175 counter_clone.fetch_add(1, atomic::Ordering::Release);
9176 async move {
9177 Ok(Some(lsp::CompletionResponse::Array(vec![
9178 task_completion_item,
9179 ])))
9180 }
9181 });
9182
9183 cx.condition(|editor, _| editor.context_menu_visible())
9184 .await;
9185 cx.assert_editor_state(indoc! {"fn main() { let a = 2.ˇ; }"});
9186 assert!(request.next().await.is_some());
9187 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9188
9189 cx.simulate_keystroke("S");
9190 cx.simulate_keystroke("o");
9191 cx.simulate_keystroke("m");
9192 cx.condition(|editor, _| editor.context_menu_visible())
9193 .await;
9194 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Somˇ; }"});
9195 assert!(request.next().await.is_some());
9196 assert!(request.next().await.is_some());
9197 assert!(request.next().await.is_some());
9198 request.close();
9199 assert!(request.next().await.is_none());
9200 assert_eq!(
9201 counter.load(atomic::Ordering::Acquire),
9202 4,
9203 "With the completions menu open, only one LSP request should happen per input"
9204 );
9205}
9206
9207#[gpui::test]
9208async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
9209 init_test(cx, |_| {});
9210 let mut cx = EditorTestContext::new(cx).await;
9211 let language = Arc::new(Language::new(
9212 LanguageConfig {
9213 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
9214 ..Default::default()
9215 },
9216 Some(tree_sitter_rust::LANGUAGE.into()),
9217 ));
9218 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
9219
9220 // If multiple selections intersect a line, the line is only toggled once.
9221 cx.set_state(indoc! {"
9222 fn a() {
9223 «//b();
9224 ˇ»// «c();
9225 //ˇ» d();
9226 }
9227 "});
9228
9229 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9230
9231 cx.assert_editor_state(indoc! {"
9232 fn a() {
9233 «b();
9234 c();
9235 ˇ» d();
9236 }
9237 "});
9238
9239 // The comment prefix is inserted at the same column for every line in a
9240 // selection.
9241 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9242
9243 cx.assert_editor_state(indoc! {"
9244 fn a() {
9245 // «b();
9246 // c();
9247 ˇ»// d();
9248 }
9249 "});
9250
9251 // If a selection ends at the beginning of a line, that line is not toggled.
9252 cx.set_selections_state(indoc! {"
9253 fn a() {
9254 // b();
9255 «// c();
9256 ˇ» // d();
9257 }
9258 "});
9259
9260 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9261
9262 cx.assert_editor_state(indoc! {"
9263 fn a() {
9264 // b();
9265 «c();
9266 ˇ» // d();
9267 }
9268 "});
9269
9270 // If a selection span a single line and is empty, the line is toggled.
9271 cx.set_state(indoc! {"
9272 fn a() {
9273 a();
9274 b();
9275 ˇ
9276 }
9277 "});
9278
9279 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9280
9281 cx.assert_editor_state(indoc! {"
9282 fn a() {
9283 a();
9284 b();
9285 //•ˇ
9286 }
9287 "});
9288
9289 // If a selection span multiple lines, empty lines are not toggled.
9290 cx.set_state(indoc! {"
9291 fn a() {
9292 «a();
9293
9294 c();ˇ»
9295 }
9296 "});
9297
9298 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9299
9300 cx.assert_editor_state(indoc! {"
9301 fn a() {
9302 // «a();
9303
9304 // c();ˇ»
9305 }
9306 "});
9307
9308 // If a selection includes multiple comment prefixes, all lines are uncommented.
9309 cx.set_state(indoc! {"
9310 fn a() {
9311 «// a();
9312 /// b();
9313 //! c();ˇ»
9314 }
9315 "});
9316
9317 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9318
9319 cx.assert_editor_state(indoc! {"
9320 fn a() {
9321 «a();
9322 b();
9323 c();ˇ»
9324 }
9325 "});
9326}
9327
9328#[gpui::test]
9329async fn test_toggle_comment_ignore_indent(cx: &mut gpui::TestAppContext) {
9330 init_test(cx, |_| {});
9331 let mut cx = EditorTestContext::new(cx).await;
9332 let language = Arc::new(Language::new(
9333 LanguageConfig {
9334 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
9335 ..Default::default()
9336 },
9337 Some(tree_sitter_rust::LANGUAGE.into()),
9338 ));
9339 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
9340
9341 let toggle_comments = &ToggleComments {
9342 advance_downwards: false,
9343 ignore_indent: true,
9344 };
9345
9346 // If multiple selections intersect a line, the line is only toggled once.
9347 cx.set_state(indoc! {"
9348 fn a() {
9349 // «b();
9350 // c();
9351 // ˇ» d();
9352 }
9353 "});
9354
9355 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9356
9357 cx.assert_editor_state(indoc! {"
9358 fn a() {
9359 «b();
9360 c();
9361 ˇ» d();
9362 }
9363 "});
9364
9365 // The comment prefix is inserted at the beginning of each line
9366 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9367
9368 cx.assert_editor_state(indoc! {"
9369 fn a() {
9370 // «b();
9371 // c();
9372 // ˇ» d();
9373 }
9374 "});
9375
9376 // If a selection ends at the beginning of a line, that line is not toggled.
9377 cx.set_selections_state(indoc! {"
9378 fn a() {
9379 // b();
9380 // «c();
9381 ˇ»// d();
9382 }
9383 "});
9384
9385 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9386
9387 cx.assert_editor_state(indoc! {"
9388 fn a() {
9389 // b();
9390 «c();
9391 ˇ»// d();
9392 }
9393 "});
9394
9395 // If a selection span a single line and is empty, the line is toggled.
9396 cx.set_state(indoc! {"
9397 fn a() {
9398 a();
9399 b();
9400 ˇ
9401 }
9402 "});
9403
9404 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9405
9406 cx.assert_editor_state(indoc! {"
9407 fn a() {
9408 a();
9409 b();
9410 //ˇ
9411 }
9412 "});
9413
9414 // If a selection span multiple lines, empty lines are not toggled.
9415 cx.set_state(indoc! {"
9416 fn a() {
9417 «a();
9418
9419 c();ˇ»
9420 }
9421 "});
9422
9423 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9424
9425 cx.assert_editor_state(indoc! {"
9426 fn a() {
9427 // «a();
9428
9429 // c();ˇ»
9430 }
9431 "});
9432
9433 // If a selection includes multiple comment prefixes, all lines are uncommented.
9434 cx.set_state(indoc! {"
9435 fn a() {
9436 // «a();
9437 /// b();
9438 //! c();ˇ»
9439 }
9440 "});
9441
9442 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9443
9444 cx.assert_editor_state(indoc! {"
9445 fn a() {
9446 «a();
9447 b();
9448 c();ˇ»
9449 }
9450 "});
9451}
9452
9453#[gpui::test]
9454async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
9455 init_test(cx, |_| {});
9456
9457 let language = Arc::new(Language::new(
9458 LanguageConfig {
9459 line_comments: vec!["// ".into()],
9460 ..Default::default()
9461 },
9462 Some(tree_sitter_rust::LANGUAGE.into()),
9463 ));
9464
9465 let mut cx = EditorTestContext::new(cx).await;
9466
9467 cx.language_registry().add(language.clone());
9468 cx.update_buffer(|buffer, cx| {
9469 buffer.set_language(Some(language), cx);
9470 });
9471
9472 let toggle_comments = &ToggleComments {
9473 advance_downwards: true,
9474 ignore_indent: false,
9475 };
9476
9477 // Single cursor on one line -> advance
9478 // Cursor moves horizontally 3 characters as well on non-blank line
9479 cx.set_state(indoc!(
9480 "fn a() {
9481 ˇdog();
9482 cat();
9483 }"
9484 ));
9485 cx.update_editor(|editor, window, cx| {
9486 editor.toggle_comments(toggle_comments, window, cx);
9487 });
9488 cx.assert_editor_state(indoc!(
9489 "fn a() {
9490 // dog();
9491 catˇ();
9492 }"
9493 ));
9494
9495 // Single selection on one line -> don't advance
9496 cx.set_state(indoc!(
9497 "fn a() {
9498 «dog()ˇ»;
9499 cat();
9500 }"
9501 ));
9502 cx.update_editor(|editor, window, cx| {
9503 editor.toggle_comments(toggle_comments, window, cx);
9504 });
9505 cx.assert_editor_state(indoc!(
9506 "fn a() {
9507 // «dog()ˇ»;
9508 cat();
9509 }"
9510 ));
9511
9512 // Multiple cursors on one line -> advance
9513 cx.set_state(indoc!(
9514 "fn a() {
9515 ˇdˇog();
9516 cat();
9517 }"
9518 ));
9519 cx.update_editor(|editor, window, cx| {
9520 editor.toggle_comments(toggle_comments, window, cx);
9521 });
9522 cx.assert_editor_state(indoc!(
9523 "fn a() {
9524 // dog();
9525 catˇ(ˇ);
9526 }"
9527 ));
9528
9529 // Multiple cursors on one line, with selection -> don't advance
9530 cx.set_state(indoc!(
9531 "fn a() {
9532 ˇdˇog«()ˇ»;
9533 cat();
9534 }"
9535 ));
9536 cx.update_editor(|editor, window, cx| {
9537 editor.toggle_comments(toggle_comments, window, cx);
9538 });
9539 cx.assert_editor_state(indoc!(
9540 "fn a() {
9541 // ˇdˇog«()ˇ»;
9542 cat();
9543 }"
9544 ));
9545
9546 // Single cursor on one line -> advance
9547 // Cursor moves to column 0 on blank line
9548 cx.set_state(indoc!(
9549 "fn a() {
9550 ˇdog();
9551
9552 cat();
9553 }"
9554 ));
9555 cx.update_editor(|editor, window, cx| {
9556 editor.toggle_comments(toggle_comments, window, cx);
9557 });
9558 cx.assert_editor_state(indoc!(
9559 "fn a() {
9560 // dog();
9561 ˇ
9562 cat();
9563 }"
9564 ));
9565
9566 // Single cursor on one line -> advance
9567 // Cursor starts and ends at column 0
9568 cx.set_state(indoc!(
9569 "fn a() {
9570 ˇ dog();
9571 cat();
9572 }"
9573 ));
9574 cx.update_editor(|editor, window, cx| {
9575 editor.toggle_comments(toggle_comments, window, cx);
9576 });
9577 cx.assert_editor_state(indoc!(
9578 "fn a() {
9579 // dog();
9580 ˇ cat();
9581 }"
9582 ));
9583}
9584
9585#[gpui::test]
9586async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
9587 init_test(cx, |_| {});
9588
9589 let mut cx = EditorTestContext::new(cx).await;
9590
9591 let html_language = Arc::new(
9592 Language::new(
9593 LanguageConfig {
9594 name: "HTML".into(),
9595 block_comment: Some(("<!-- ".into(), " -->".into())),
9596 ..Default::default()
9597 },
9598 Some(tree_sitter_html::language()),
9599 )
9600 .with_injection_query(
9601 r#"
9602 (script_element
9603 (raw_text) @injection.content
9604 (#set! injection.language "javascript"))
9605 "#,
9606 )
9607 .unwrap(),
9608 );
9609
9610 let javascript_language = Arc::new(Language::new(
9611 LanguageConfig {
9612 name: "JavaScript".into(),
9613 line_comments: vec!["// ".into()],
9614 ..Default::default()
9615 },
9616 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9617 ));
9618
9619 cx.language_registry().add(html_language.clone());
9620 cx.language_registry().add(javascript_language.clone());
9621 cx.update_buffer(|buffer, cx| {
9622 buffer.set_language(Some(html_language), cx);
9623 });
9624
9625 // Toggle comments for empty selections
9626 cx.set_state(
9627 &r#"
9628 <p>A</p>ˇ
9629 <p>B</p>ˇ
9630 <p>C</p>ˇ
9631 "#
9632 .unindent(),
9633 );
9634 cx.update_editor(|editor, window, cx| {
9635 editor.toggle_comments(&ToggleComments::default(), window, cx)
9636 });
9637 cx.assert_editor_state(
9638 &r#"
9639 <!-- <p>A</p>ˇ -->
9640 <!-- <p>B</p>ˇ -->
9641 <!-- <p>C</p>ˇ -->
9642 "#
9643 .unindent(),
9644 );
9645 cx.update_editor(|editor, window, cx| {
9646 editor.toggle_comments(&ToggleComments::default(), window, cx)
9647 });
9648 cx.assert_editor_state(
9649 &r#"
9650 <p>A</p>ˇ
9651 <p>B</p>ˇ
9652 <p>C</p>ˇ
9653 "#
9654 .unindent(),
9655 );
9656
9657 // Toggle comments for mixture of empty and non-empty selections, where
9658 // multiple selections occupy a given line.
9659 cx.set_state(
9660 &r#"
9661 <p>A«</p>
9662 <p>ˇ»B</p>ˇ
9663 <p>C«</p>
9664 <p>ˇ»D</p>ˇ
9665 "#
9666 .unindent(),
9667 );
9668
9669 cx.update_editor(|editor, window, cx| {
9670 editor.toggle_comments(&ToggleComments::default(), window, cx)
9671 });
9672 cx.assert_editor_state(
9673 &r#"
9674 <!-- <p>A«</p>
9675 <p>ˇ»B</p>ˇ -->
9676 <!-- <p>C«</p>
9677 <p>ˇ»D</p>ˇ -->
9678 "#
9679 .unindent(),
9680 );
9681 cx.update_editor(|editor, window, cx| {
9682 editor.toggle_comments(&ToggleComments::default(), window, cx)
9683 });
9684 cx.assert_editor_state(
9685 &r#"
9686 <p>A«</p>
9687 <p>ˇ»B</p>ˇ
9688 <p>C«</p>
9689 <p>ˇ»D</p>ˇ
9690 "#
9691 .unindent(),
9692 );
9693
9694 // Toggle comments when different languages are active for different
9695 // selections.
9696 cx.set_state(
9697 &r#"
9698 ˇ<script>
9699 ˇvar x = new Y();
9700 ˇ</script>
9701 "#
9702 .unindent(),
9703 );
9704 cx.executor().run_until_parked();
9705 cx.update_editor(|editor, window, cx| {
9706 editor.toggle_comments(&ToggleComments::default(), window, cx)
9707 });
9708 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
9709 // Uncommenting and commenting from this position brings in even more wrong artifacts.
9710 cx.assert_editor_state(
9711 &r#"
9712 <!-- ˇ<script> -->
9713 // ˇvar x = new Y();
9714 <!-- ˇ</script> -->
9715 "#
9716 .unindent(),
9717 );
9718}
9719
9720#[gpui::test]
9721fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
9722 init_test(cx, |_| {});
9723
9724 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9725 let multibuffer = cx.new(|cx| {
9726 let mut multibuffer = MultiBuffer::new(ReadWrite);
9727 multibuffer.push_excerpts(
9728 buffer.clone(),
9729 [
9730 ExcerptRange {
9731 context: Point::new(0, 0)..Point::new(0, 4),
9732 primary: None,
9733 },
9734 ExcerptRange {
9735 context: Point::new(1, 0)..Point::new(1, 4),
9736 primary: None,
9737 },
9738 ],
9739 cx,
9740 );
9741 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
9742 multibuffer
9743 });
9744
9745 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
9746 editor.update_in(cx, |editor, window, cx| {
9747 assert_eq!(editor.text(cx), "aaaa\nbbbb");
9748 editor.change_selections(None, window, cx, |s| {
9749 s.select_ranges([
9750 Point::new(0, 0)..Point::new(0, 0),
9751 Point::new(1, 0)..Point::new(1, 0),
9752 ])
9753 });
9754
9755 editor.handle_input("X", window, cx);
9756 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
9757 assert_eq!(
9758 editor.selections.ranges(cx),
9759 [
9760 Point::new(0, 1)..Point::new(0, 1),
9761 Point::new(1, 1)..Point::new(1, 1),
9762 ]
9763 );
9764
9765 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
9766 editor.change_selections(None, window, cx, |s| {
9767 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
9768 });
9769 editor.backspace(&Default::default(), window, cx);
9770 assert_eq!(editor.text(cx), "Xa\nbbb");
9771 assert_eq!(
9772 editor.selections.ranges(cx),
9773 [Point::new(1, 0)..Point::new(1, 0)]
9774 );
9775
9776 editor.change_selections(None, window, cx, |s| {
9777 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
9778 });
9779 editor.backspace(&Default::default(), window, cx);
9780 assert_eq!(editor.text(cx), "X\nbb");
9781 assert_eq!(
9782 editor.selections.ranges(cx),
9783 [Point::new(0, 1)..Point::new(0, 1)]
9784 );
9785 });
9786}
9787
9788#[gpui::test]
9789fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
9790 init_test(cx, |_| {});
9791
9792 let markers = vec![('[', ']').into(), ('(', ')').into()];
9793 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
9794 indoc! {"
9795 [aaaa
9796 (bbbb]
9797 cccc)",
9798 },
9799 markers.clone(),
9800 );
9801 let excerpt_ranges = markers.into_iter().map(|marker| {
9802 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
9803 ExcerptRange {
9804 context,
9805 primary: None,
9806 }
9807 });
9808 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
9809 let multibuffer = cx.new(|cx| {
9810 let mut multibuffer = MultiBuffer::new(ReadWrite);
9811 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
9812 multibuffer
9813 });
9814
9815 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
9816 editor.update_in(cx, |editor, window, cx| {
9817 let (expected_text, selection_ranges) = marked_text_ranges(
9818 indoc! {"
9819 aaaa
9820 bˇbbb
9821 bˇbbˇb
9822 cccc"
9823 },
9824 true,
9825 );
9826 assert_eq!(editor.text(cx), expected_text);
9827 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
9828
9829 editor.handle_input("X", window, cx);
9830
9831 let (expected_text, expected_selections) = marked_text_ranges(
9832 indoc! {"
9833 aaaa
9834 bXˇbbXb
9835 bXˇbbXˇb
9836 cccc"
9837 },
9838 false,
9839 );
9840 assert_eq!(editor.text(cx), expected_text);
9841 assert_eq!(editor.selections.ranges(cx), expected_selections);
9842
9843 editor.newline(&Newline, window, cx);
9844 let (expected_text, expected_selections) = marked_text_ranges(
9845 indoc! {"
9846 aaaa
9847 bX
9848 ˇbbX
9849 b
9850 bX
9851 ˇbbX
9852 ˇb
9853 cccc"
9854 },
9855 false,
9856 );
9857 assert_eq!(editor.text(cx), expected_text);
9858 assert_eq!(editor.selections.ranges(cx), expected_selections);
9859 });
9860}
9861
9862#[gpui::test]
9863fn test_refresh_selections(cx: &mut TestAppContext) {
9864 init_test(cx, |_| {});
9865
9866 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9867 let mut excerpt1_id = None;
9868 let multibuffer = cx.new(|cx| {
9869 let mut multibuffer = MultiBuffer::new(ReadWrite);
9870 excerpt1_id = multibuffer
9871 .push_excerpts(
9872 buffer.clone(),
9873 [
9874 ExcerptRange {
9875 context: Point::new(0, 0)..Point::new(1, 4),
9876 primary: None,
9877 },
9878 ExcerptRange {
9879 context: Point::new(1, 0)..Point::new(2, 4),
9880 primary: None,
9881 },
9882 ],
9883 cx,
9884 )
9885 .into_iter()
9886 .next();
9887 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
9888 multibuffer
9889 });
9890
9891 let editor = cx.add_window(|window, cx| {
9892 let mut editor = build_editor(multibuffer.clone(), window, cx);
9893 let snapshot = editor.snapshot(window, cx);
9894 editor.change_selections(None, window, cx, |s| {
9895 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
9896 });
9897 editor.begin_selection(
9898 Point::new(2, 1).to_display_point(&snapshot),
9899 true,
9900 1,
9901 window,
9902 cx,
9903 );
9904 assert_eq!(
9905 editor.selections.ranges(cx),
9906 [
9907 Point::new(1, 3)..Point::new(1, 3),
9908 Point::new(2, 1)..Point::new(2, 1),
9909 ]
9910 );
9911 editor
9912 });
9913
9914 // Refreshing selections is a no-op when excerpts haven't changed.
9915 _ = editor.update(cx, |editor, window, cx| {
9916 editor.change_selections(None, window, cx, |s| s.refresh());
9917 assert_eq!(
9918 editor.selections.ranges(cx),
9919 [
9920 Point::new(1, 3)..Point::new(1, 3),
9921 Point::new(2, 1)..Point::new(2, 1),
9922 ]
9923 );
9924 });
9925
9926 multibuffer.update(cx, |multibuffer, cx| {
9927 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
9928 });
9929 _ = editor.update(cx, |editor, window, cx| {
9930 // Removing an excerpt causes the first selection to become degenerate.
9931 assert_eq!(
9932 editor.selections.ranges(cx),
9933 [
9934 Point::new(0, 0)..Point::new(0, 0),
9935 Point::new(0, 1)..Point::new(0, 1)
9936 ]
9937 );
9938
9939 // Refreshing selections will relocate the first selection to the original buffer
9940 // location.
9941 editor.change_selections(None, window, cx, |s| s.refresh());
9942 assert_eq!(
9943 editor.selections.ranges(cx),
9944 [
9945 Point::new(0, 1)..Point::new(0, 1),
9946 Point::new(0, 3)..Point::new(0, 3)
9947 ]
9948 );
9949 assert!(editor.selections.pending_anchor().is_some());
9950 });
9951}
9952
9953#[gpui::test]
9954fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
9955 init_test(cx, |_| {});
9956
9957 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9958 let mut excerpt1_id = None;
9959 let multibuffer = cx.new(|cx| {
9960 let mut multibuffer = MultiBuffer::new(ReadWrite);
9961 excerpt1_id = multibuffer
9962 .push_excerpts(
9963 buffer.clone(),
9964 [
9965 ExcerptRange {
9966 context: Point::new(0, 0)..Point::new(1, 4),
9967 primary: None,
9968 },
9969 ExcerptRange {
9970 context: Point::new(1, 0)..Point::new(2, 4),
9971 primary: None,
9972 },
9973 ],
9974 cx,
9975 )
9976 .into_iter()
9977 .next();
9978 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
9979 multibuffer
9980 });
9981
9982 let editor = cx.add_window(|window, cx| {
9983 let mut editor = build_editor(multibuffer.clone(), window, cx);
9984 let snapshot = editor.snapshot(window, cx);
9985 editor.begin_selection(
9986 Point::new(1, 3).to_display_point(&snapshot),
9987 false,
9988 1,
9989 window,
9990 cx,
9991 );
9992 assert_eq!(
9993 editor.selections.ranges(cx),
9994 [Point::new(1, 3)..Point::new(1, 3)]
9995 );
9996 editor
9997 });
9998
9999 multibuffer.update(cx, |multibuffer, cx| {
10000 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
10001 });
10002 _ = editor.update(cx, |editor, window, cx| {
10003 assert_eq!(
10004 editor.selections.ranges(cx),
10005 [Point::new(0, 0)..Point::new(0, 0)]
10006 );
10007
10008 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
10009 editor.change_selections(None, window, cx, |s| s.refresh());
10010 assert_eq!(
10011 editor.selections.ranges(cx),
10012 [Point::new(0, 3)..Point::new(0, 3)]
10013 );
10014 assert!(editor.selections.pending_anchor().is_some());
10015 });
10016}
10017
10018#[gpui::test]
10019async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
10020 init_test(cx, |_| {});
10021
10022 let language = Arc::new(
10023 Language::new(
10024 LanguageConfig {
10025 brackets: BracketPairConfig {
10026 pairs: vec![
10027 BracketPair {
10028 start: "{".to_string(),
10029 end: "}".to_string(),
10030 close: true,
10031 surround: true,
10032 newline: true,
10033 },
10034 BracketPair {
10035 start: "/* ".to_string(),
10036 end: " */".to_string(),
10037 close: true,
10038 surround: true,
10039 newline: true,
10040 },
10041 ],
10042 ..Default::default()
10043 },
10044 ..Default::default()
10045 },
10046 Some(tree_sitter_rust::LANGUAGE.into()),
10047 )
10048 .with_indents_query("")
10049 .unwrap(),
10050 );
10051
10052 let text = concat!(
10053 "{ }\n", //
10054 " x\n", //
10055 " /* */\n", //
10056 "x\n", //
10057 "{{} }\n", //
10058 );
10059
10060 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10061 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10062 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10063 editor
10064 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10065 .await;
10066
10067 editor.update_in(cx, |editor, window, cx| {
10068 editor.change_selections(None, window, cx, |s| {
10069 s.select_display_ranges([
10070 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
10071 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
10072 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
10073 ])
10074 });
10075 editor.newline(&Newline, window, cx);
10076
10077 assert_eq!(
10078 editor.buffer().read(cx).read(cx).text(),
10079 concat!(
10080 "{ \n", // Suppress rustfmt
10081 "\n", //
10082 "}\n", //
10083 " x\n", //
10084 " /* \n", //
10085 " \n", //
10086 " */\n", //
10087 "x\n", //
10088 "{{} \n", //
10089 "}\n", //
10090 )
10091 );
10092 });
10093}
10094
10095#[gpui::test]
10096fn test_highlighted_ranges(cx: &mut TestAppContext) {
10097 init_test(cx, |_| {});
10098
10099 let editor = cx.add_window(|window, cx| {
10100 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
10101 build_editor(buffer.clone(), window, cx)
10102 });
10103
10104 _ = editor.update(cx, |editor, window, cx| {
10105 struct Type1;
10106 struct Type2;
10107
10108 let buffer = editor.buffer.read(cx).snapshot(cx);
10109
10110 let anchor_range =
10111 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
10112
10113 editor.highlight_background::<Type1>(
10114 &[
10115 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
10116 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
10117 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
10118 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
10119 ],
10120 |_| Hsla::red(),
10121 cx,
10122 );
10123 editor.highlight_background::<Type2>(
10124 &[
10125 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
10126 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
10127 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
10128 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
10129 ],
10130 |_| Hsla::green(),
10131 cx,
10132 );
10133
10134 let snapshot = editor.snapshot(window, cx);
10135 let mut highlighted_ranges = editor.background_highlights_in_range(
10136 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
10137 &snapshot,
10138 cx.theme().colors(),
10139 );
10140 // Enforce a consistent ordering based on color without relying on the ordering of the
10141 // highlight's `TypeId` which is non-executor.
10142 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
10143 assert_eq!(
10144 highlighted_ranges,
10145 &[
10146 (
10147 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
10148 Hsla::red(),
10149 ),
10150 (
10151 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
10152 Hsla::red(),
10153 ),
10154 (
10155 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
10156 Hsla::green(),
10157 ),
10158 (
10159 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
10160 Hsla::green(),
10161 ),
10162 ]
10163 );
10164 assert_eq!(
10165 editor.background_highlights_in_range(
10166 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
10167 &snapshot,
10168 cx.theme().colors(),
10169 ),
10170 &[(
10171 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
10172 Hsla::red(),
10173 )]
10174 );
10175 });
10176}
10177
10178#[gpui::test]
10179async fn test_following(cx: &mut gpui::TestAppContext) {
10180 init_test(cx, |_| {});
10181
10182 let fs = FakeFs::new(cx.executor());
10183 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
10184
10185 let buffer = project.update(cx, |project, cx| {
10186 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
10187 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
10188 });
10189 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
10190 let follower = cx.update(|cx| {
10191 cx.open_window(
10192 WindowOptions {
10193 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
10194 gpui::Point::new(px(0.), px(0.)),
10195 gpui::Point::new(px(10.), px(80.)),
10196 ))),
10197 ..Default::default()
10198 },
10199 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
10200 )
10201 .unwrap()
10202 });
10203
10204 let is_still_following = Rc::new(RefCell::new(true));
10205 let follower_edit_event_count = Rc::new(RefCell::new(0));
10206 let pending_update = Rc::new(RefCell::new(None));
10207 let leader_entity = leader.root(cx).unwrap();
10208 let follower_entity = follower.root(cx).unwrap();
10209 _ = follower.update(cx, {
10210 let update = pending_update.clone();
10211 let is_still_following = is_still_following.clone();
10212 let follower_edit_event_count = follower_edit_event_count.clone();
10213 |_, window, cx| {
10214 cx.subscribe_in(
10215 &leader_entity,
10216 window,
10217 move |_, leader, event, window, cx| {
10218 leader.read(cx).add_event_to_update_proto(
10219 event,
10220 &mut update.borrow_mut(),
10221 window,
10222 cx,
10223 );
10224 },
10225 )
10226 .detach();
10227
10228 cx.subscribe_in(
10229 &follower_entity,
10230 window,
10231 move |_, _, event: &EditorEvent, _window, _cx| {
10232 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
10233 *is_still_following.borrow_mut() = false;
10234 }
10235
10236 if let EditorEvent::BufferEdited = event {
10237 *follower_edit_event_count.borrow_mut() += 1;
10238 }
10239 },
10240 )
10241 .detach();
10242 }
10243 });
10244
10245 // Update the selections only
10246 _ = leader.update(cx, |leader, window, cx| {
10247 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
10248 });
10249 follower
10250 .update(cx, |follower, window, cx| {
10251 follower.apply_update_proto(
10252 &project,
10253 pending_update.borrow_mut().take().unwrap(),
10254 window,
10255 cx,
10256 )
10257 })
10258 .unwrap()
10259 .await
10260 .unwrap();
10261 _ = follower.update(cx, |follower, _, cx| {
10262 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
10263 });
10264 assert!(*is_still_following.borrow());
10265 assert_eq!(*follower_edit_event_count.borrow(), 0);
10266
10267 // Update the scroll position only
10268 _ = leader.update(cx, |leader, window, cx| {
10269 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
10270 });
10271 follower
10272 .update(cx, |follower, window, cx| {
10273 follower.apply_update_proto(
10274 &project,
10275 pending_update.borrow_mut().take().unwrap(),
10276 window,
10277 cx,
10278 )
10279 })
10280 .unwrap()
10281 .await
10282 .unwrap();
10283 assert_eq!(
10284 follower
10285 .update(cx, |follower, _, cx| follower.scroll_position(cx))
10286 .unwrap(),
10287 gpui::Point::new(1.5, 3.5)
10288 );
10289 assert!(*is_still_following.borrow());
10290 assert_eq!(*follower_edit_event_count.borrow(), 0);
10291
10292 // Update the selections and scroll position. The follower's scroll position is updated
10293 // via autoscroll, not via the leader's exact scroll position.
10294 _ = leader.update(cx, |leader, window, cx| {
10295 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10296 leader.request_autoscroll(Autoscroll::newest(), cx);
10297 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
10298 });
10299 follower
10300 .update(cx, |follower, window, cx| {
10301 follower.apply_update_proto(
10302 &project,
10303 pending_update.borrow_mut().take().unwrap(),
10304 window,
10305 cx,
10306 )
10307 })
10308 .unwrap()
10309 .await
10310 .unwrap();
10311 _ = follower.update(cx, |follower, _, cx| {
10312 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
10313 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
10314 });
10315 assert!(*is_still_following.borrow());
10316
10317 // Creating a pending selection that precedes another selection
10318 _ = leader.update(cx, |leader, window, cx| {
10319 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
10320 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
10321 });
10322 follower
10323 .update(cx, |follower, window, cx| {
10324 follower.apply_update_proto(
10325 &project,
10326 pending_update.borrow_mut().take().unwrap(),
10327 window,
10328 cx,
10329 )
10330 })
10331 .unwrap()
10332 .await
10333 .unwrap();
10334 _ = follower.update(cx, |follower, _, cx| {
10335 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
10336 });
10337 assert!(*is_still_following.borrow());
10338
10339 // Extend the pending selection so that it surrounds another selection
10340 _ = leader.update(cx, |leader, window, cx| {
10341 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
10342 });
10343 follower
10344 .update(cx, |follower, window, cx| {
10345 follower.apply_update_proto(
10346 &project,
10347 pending_update.borrow_mut().take().unwrap(),
10348 window,
10349 cx,
10350 )
10351 })
10352 .unwrap()
10353 .await
10354 .unwrap();
10355 _ = follower.update(cx, |follower, _, cx| {
10356 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
10357 });
10358
10359 // Scrolling locally breaks the follow
10360 _ = follower.update(cx, |follower, window, cx| {
10361 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
10362 follower.set_scroll_anchor(
10363 ScrollAnchor {
10364 anchor: top_anchor,
10365 offset: gpui::Point::new(0.0, 0.5),
10366 },
10367 window,
10368 cx,
10369 );
10370 });
10371 assert!(!(*is_still_following.borrow()));
10372}
10373
10374#[gpui::test]
10375async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
10376 init_test(cx, |_| {});
10377
10378 let fs = FakeFs::new(cx.executor());
10379 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
10380 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10381 let pane = workspace
10382 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10383 .unwrap();
10384
10385 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10386
10387 let leader = pane.update_in(cx, |_, window, cx| {
10388 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
10389 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
10390 });
10391
10392 // Start following the editor when it has no excerpts.
10393 let mut state_message =
10394 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
10395 let workspace_entity = workspace.root(cx).unwrap();
10396 let follower_1 = cx
10397 .update_window(*workspace.deref(), |_, window, cx| {
10398 Editor::from_state_proto(
10399 workspace_entity,
10400 ViewId {
10401 creator: Default::default(),
10402 id: 0,
10403 },
10404 &mut state_message,
10405 window,
10406 cx,
10407 )
10408 })
10409 .unwrap()
10410 .unwrap()
10411 .await
10412 .unwrap();
10413
10414 let update_message = Rc::new(RefCell::new(None));
10415 follower_1.update_in(cx, {
10416 let update = update_message.clone();
10417 |_, window, cx| {
10418 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
10419 leader.read(cx).add_event_to_update_proto(
10420 event,
10421 &mut update.borrow_mut(),
10422 window,
10423 cx,
10424 );
10425 })
10426 .detach();
10427 }
10428 });
10429
10430 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
10431 (
10432 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
10433 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
10434 )
10435 });
10436
10437 // Insert some excerpts.
10438 leader.update(cx, |leader, cx| {
10439 leader.buffer.update(cx, |multibuffer, cx| {
10440 let excerpt_ids = multibuffer.push_excerpts(
10441 buffer_1.clone(),
10442 [
10443 ExcerptRange {
10444 context: 1..6,
10445 primary: None,
10446 },
10447 ExcerptRange {
10448 context: 12..15,
10449 primary: None,
10450 },
10451 ExcerptRange {
10452 context: 0..3,
10453 primary: None,
10454 },
10455 ],
10456 cx,
10457 );
10458 multibuffer.insert_excerpts_after(
10459 excerpt_ids[0],
10460 buffer_2.clone(),
10461 [
10462 ExcerptRange {
10463 context: 8..12,
10464 primary: None,
10465 },
10466 ExcerptRange {
10467 context: 0..6,
10468 primary: None,
10469 },
10470 ],
10471 cx,
10472 );
10473 });
10474 });
10475
10476 // Apply the update of adding the excerpts.
10477 follower_1
10478 .update_in(cx, |follower, window, cx| {
10479 follower.apply_update_proto(
10480 &project,
10481 update_message.borrow().clone().unwrap(),
10482 window,
10483 cx,
10484 )
10485 })
10486 .await
10487 .unwrap();
10488 assert_eq!(
10489 follower_1.update(cx, |editor, cx| editor.text(cx)),
10490 leader.update(cx, |editor, cx| editor.text(cx))
10491 );
10492 update_message.borrow_mut().take();
10493
10494 // Start following separately after it already has excerpts.
10495 let mut state_message =
10496 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
10497 let workspace_entity = workspace.root(cx).unwrap();
10498 let follower_2 = cx
10499 .update_window(*workspace.deref(), |_, window, cx| {
10500 Editor::from_state_proto(
10501 workspace_entity,
10502 ViewId {
10503 creator: Default::default(),
10504 id: 0,
10505 },
10506 &mut state_message,
10507 window,
10508 cx,
10509 )
10510 })
10511 .unwrap()
10512 .unwrap()
10513 .await
10514 .unwrap();
10515 assert_eq!(
10516 follower_2.update(cx, |editor, cx| editor.text(cx)),
10517 leader.update(cx, |editor, cx| editor.text(cx))
10518 );
10519
10520 // Remove some excerpts.
10521 leader.update(cx, |leader, cx| {
10522 leader.buffer.update(cx, |multibuffer, cx| {
10523 let excerpt_ids = multibuffer.excerpt_ids();
10524 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
10525 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
10526 });
10527 });
10528
10529 // Apply the update of removing the excerpts.
10530 follower_1
10531 .update_in(cx, |follower, window, cx| {
10532 follower.apply_update_proto(
10533 &project,
10534 update_message.borrow().clone().unwrap(),
10535 window,
10536 cx,
10537 )
10538 })
10539 .await
10540 .unwrap();
10541 follower_2
10542 .update_in(cx, |follower, window, cx| {
10543 follower.apply_update_proto(
10544 &project,
10545 update_message.borrow().clone().unwrap(),
10546 window,
10547 cx,
10548 )
10549 })
10550 .await
10551 .unwrap();
10552 update_message.borrow_mut().take();
10553 assert_eq!(
10554 follower_1.update(cx, |editor, cx| editor.text(cx)),
10555 leader.update(cx, |editor, cx| editor.text(cx))
10556 );
10557}
10558
10559#[gpui::test]
10560async fn go_to_prev_overlapping_diagnostic(
10561 executor: BackgroundExecutor,
10562 cx: &mut gpui::TestAppContext,
10563) {
10564 init_test(cx, |_| {});
10565
10566 let mut cx = EditorTestContext::new(cx).await;
10567 let lsp_store =
10568 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10569
10570 cx.set_state(indoc! {"
10571 ˇfn func(abc def: i32) -> u32 {
10572 }
10573 "});
10574
10575 cx.update(|_, cx| {
10576 lsp_store.update(cx, |lsp_store, cx| {
10577 lsp_store
10578 .update_diagnostics(
10579 LanguageServerId(0),
10580 lsp::PublishDiagnosticsParams {
10581 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
10582 version: None,
10583 diagnostics: vec![
10584 lsp::Diagnostic {
10585 range: lsp::Range::new(
10586 lsp::Position::new(0, 11),
10587 lsp::Position::new(0, 12),
10588 ),
10589 severity: Some(lsp::DiagnosticSeverity::ERROR),
10590 ..Default::default()
10591 },
10592 lsp::Diagnostic {
10593 range: lsp::Range::new(
10594 lsp::Position::new(0, 12),
10595 lsp::Position::new(0, 15),
10596 ),
10597 severity: Some(lsp::DiagnosticSeverity::ERROR),
10598 ..Default::default()
10599 },
10600 lsp::Diagnostic {
10601 range: lsp::Range::new(
10602 lsp::Position::new(0, 25),
10603 lsp::Position::new(0, 28),
10604 ),
10605 severity: Some(lsp::DiagnosticSeverity::ERROR),
10606 ..Default::default()
10607 },
10608 ],
10609 },
10610 &[],
10611 cx,
10612 )
10613 .unwrap()
10614 });
10615 });
10616
10617 executor.run_until_parked();
10618
10619 cx.update_editor(|editor, window, cx| {
10620 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10621 });
10622
10623 cx.assert_editor_state(indoc! {"
10624 fn func(abc def: i32) -> ˇu32 {
10625 }
10626 "});
10627
10628 cx.update_editor(|editor, window, cx| {
10629 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10630 });
10631
10632 cx.assert_editor_state(indoc! {"
10633 fn func(abc ˇdef: i32) -> u32 {
10634 }
10635 "});
10636
10637 cx.update_editor(|editor, window, cx| {
10638 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10639 });
10640
10641 cx.assert_editor_state(indoc! {"
10642 fn func(abcˇ def: i32) -> u32 {
10643 }
10644 "});
10645
10646 cx.update_editor(|editor, window, cx| {
10647 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10648 });
10649
10650 cx.assert_editor_state(indoc! {"
10651 fn func(abc def: i32) -> ˇu32 {
10652 }
10653 "});
10654}
10655
10656#[gpui::test]
10657async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
10658 init_test(cx, |_| {});
10659
10660 let mut cx = EditorTestContext::new(cx).await;
10661
10662 cx.set_state(indoc! {"
10663 fn func(abˇc def: i32) -> u32 {
10664 }
10665 "});
10666 let lsp_store =
10667 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10668
10669 cx.update(|_, cx| {
10670 lsp_store.update(cx, |lsp_store, cx| {
10671 lsp_store.update_diagnostics(
10672 LanguageServerId(0),
10673 lsp::PublishDiagnosticsParams {
10674 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
10675 version: None,
10676 diagnostics: vec![lsp::Diagnostic {
10677 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
10678 severity: Some(lsp::DiagnosticSeverity::ERROR),
10679 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
10680 ..Default::default()
10681 }],
10682 },
10683 &[],
10684 cx,
10685 )
10686 })
10687 }).unwrap();
10688 cx.run_until_parked();
10689 cx.update_editor(|editor, window, cx| {
10690 hover_popover::hover(editor, &Default::default(), window, cx)
10691 });
10692 cx.run_until_parked();
10693 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
10694}
10695
10696#[gpui::test]
10697async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
10698 init_test(cx, |_| {});
10699
10700 let mut cx = EditorTestContext::new(cx).await;
10701
10702 let diff_base = r#"
10703 use some::mod;
10704
10705 const A: u32 = 42;
10706
10707 fn main() {
10708 println!("hello");
10709
10710 println!("world");
10711 }
10712 "#
10713 .unindent();
10714
10715 // Edits are modified, removed, modified, added
10716 cx.set_state(
10717 &r#"
10718 use some::modified;
10719
10720 ˇ
10721 fn main() {
10722 println!("hello there");
10723
10724 println!("around the");
10725 println!("world");
10726 }
10727 "#
10728 .unindent(),
10729 );
10730
10731 cx.set_diff_base(&diff_base);
10732 executor.run_until_parked();
10733
10734 cx.update_editor(|editor, window, cx| {
10735 //Wrap around the bottom of the buffer
10736 for _ in 0..3 {
10737 editor.go_to_next_hunk(&GoToHunk, window, cx);
10738 }
10739 });
10740
10741 cx.assert_editor_state(
10742 &r#"
10743 ˇuse some::modified;
10744
10745
10746 fn main() {
10747 println!("hello there");
10748
10749 println!("around the");
10750 println!("world");
10751 }
10752 "#
10753 .unindent(),
10754 );
10755
10756 cx.update_editor(|editor, window, cx| {
10757 //Wrap around the top of the buffer
10758 for _ in 0..2 {
10759 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10760 }
10761 });
10762
10763 cx.assert_editor_state(
10764 &r#"
10765 use some::modified;
10766
10767
10768 fn main() {
10769 ˇ println!("hello there");
10770
10771 println!("around the");
10772 println!("world");
10773 }
10774 "#
10775 .unindent(),
10776 );
10777
10778 cx.update_editor(|editor, window, cx| {
10779 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10780 });
10781
10782 cx.assert_editor_state(
10783 &r#"
10784 use some::modified;
10785
10786 ˇ
10787 fn main() {
10788 println!("hello there");
10789
10790 println!("around the");
10791 println!("world");
10792 }
10793 "#
10794 .unindent(),
10795 );
10796
10797 cx.update_editor(|editor, window, cx| {
10798 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10799 });
10800
10801 cx.assert_editor_state(
10802 &r#"
10803 ˇuse some::modified;
10804
10805
10806 fn main() {
10807 println!("hello there");
10808
10809 println!("around the");
10810 println!("world");
10811 }
10812 "#
10813 .unindent(),
10814 );
10815
10816 cx.update_editor(|editor, window, cx| {
10817 for _ in 0..2 {
10818 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10819 }
10820 });
10821
10822 cx.assert_editor_state(
10823 &r#"
10824 use some::modified;
10825
10826
10827 fn main() {
10828 ˇ println!("hello there");
10829
10830 println!("around the");
10831 println!("world");
10832 }
10833 "#
10834 .unindent(),
10835 );
10836
10837 cx.update_editor(|editor, window, cx| {
10838 editor.fold(&Fold, window, cx);
10839 });
10840
10841 cx.update_editor(|editor, window, cx| {
10842 editor.go_to_next_hunk(&GoToHunk, window, cx);
10843 });
10844
10845 cx.assert_editor_state(
10846 &r#"
10847 ˇuse some::modified;
10848
10849
10850 fn main() {
10851 println!("hello there");
10852
10853 println!("around the");
10854 println!("world");
10855 }
10856 "#
10857 .unindent(),
10858 );
10859}
10860
10861#[test]
10862fn test_split_words() {
10863 fn split(text: &str) -> Vec<&str> {
10864 split_words(text).collect()
10865 }
10866
10867 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
10868 assert_eq!(split("hello_world"), &["hello_", "world"]);
10869 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
10870 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
10871 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
10872 assert_eq!(split("helloworld"), &["helloworld"]);
10873
10874 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
10875}
10876
10877#[gpui::test]
10878async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
10879 init_test(cx, |_| {});
10880
10881 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
10882 let mut assert = |before, after| {
10883 let _state_context = cx.set_state(before);
10884 cx.run_until_parked();
10885 cx.update_editor(|editor, window, cx| {
10886 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
10887 });
10888 cx.assert_editor_state(after);
10889 };
10890
10891 // Outside bracket jumps to outside of matching bracket
10892 assert("console.logˇ(var);", "console.log(var)ˇ;");
10893 assert("console.log(var)ˇ;", "console.logˇ(var);");
10894
10895 // Inside bracket jumps to inside of matching bracket
10896 assert("console.log(ˇvar);", "console.log(varˇ);");
10897 assert("console.log(varˇ);", "console.log(ˇvar);");
10898
10899 // When outside a bracket and inside, favor jumping to the inside bracket
10900 assert(
10901 "console.log('foo', [1, 2, 3]ˇ);",
10902 "console.log(ˇ'foo', [1, 2, 3]);",
10903 );
10904 assert(
10905 "console.log(ˇ'foo', [1, 2, 3]);",
10906 "console.log('foo', [1, 2, 3]ˇ);",
10907 );
10908
10909 // Bias forward if two options are equally likely
10910 assert(
10911 "let result = curried_fun()ˇ();",
10912 "let result = curried_fun()()ˇ;",
10913 );
10914
10915 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
10916 assert(
10917 indoc! {"
10918 function test() {
10919 console.log('test')ˇ
10920 }"},
10921 indoc! {"
10922 function test() {
10923 console.logˇ('test')
10924 }"},
10925 );
10926}
10927
10928#[gpui::test]
10929async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
10930 init_test(cx, |_| {});
10931
10932 let fs = FakeFs::new(cx.executor());
10933 fs.insert_tree(
10934 path!("/a"),
10935 json!({
10936 "main.rs": "fn main() { let a = 5; }",
10937 "other.rs": "// Test file",
10938 }),
10939 )
10940 .await;
10941 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10942
10943 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10944 language_registry.add(Arc::new(Language::new(
10945 LanguageConfig {
10946 name: "Rust".into(),
10947 matcher: LanguageMatcher {
10948 path_suffixes: vec!["rs".to_string()],
10949 ..Default::default()
10950 },
10951 brackets: BracketPairConfig {
10952 pairs: vec![BracketPair {
10953 start: "{".to_string(),
10954 end: "}".to_string(),
10955 close: true,
10956 surround: true,
10957 newline: true,
10958 }],
10959 disabled_scopes_by_bracket_ix: Vec::new(),
10960 },
10961 ..Default::default()
10962 },
10963 Some(tree_sitter_rust::LANGUAGE.into()),
10964 )));
10965 let mut fake_servers = language_registry.register_fake_lsp(
10966 "Rust",
10967 FakeLspAdapter {
10968 capabilities: lsp::ServerCapabilities {
10969 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
10970 first_trigger_character: "{".to_string(),
10971 more_trigger_character: None,
10972 }),
10973 ..Default::default()
10974 },
10975 ..Default::default()
10976 },
10977 );
10978
10979 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10980
10981 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10982
10983 let worktree_id = workspace
10984 .update(cx, |workspace, _, cx| {
10985 workspace.project().update(cx, |project, cx| {
10986 project.worktrees(cx).next().unwrap().read(cx).id()
10987 })
10988 })
10989 .unwrap();
10990
10991 let buffer = project
10992 .update(cx, |project, cx| {
10993 project.open_local_buffer(path!("/a/main.rs"), cx)
10994 })
10995 .await
10996 .unwrap();
10997 let editor_handle = workspace
10998 .update(cx, |workspace, window, cx| {
10999 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
11000 })
11001 .unwrap()
11002 .await
11003 .unwrap()
11004 .downcast::<Editor>()
11005 .unwrap();
11006
11007 cx.executor().start_waiting();
11008 let fake_server = fake_servers.next().await.unwrap();
11009
11010 fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
11011 assert_eq!(
11012 params.text_document_position.text_document.uri,
11013 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
11014 );
11015 assert_eq!(
11016 params.text_document_position.position,
11017 lsp::Position::new(0, 21),
11018 );
11019
11020 Ok(Some(vec![lsp::TextEdit {
11021 new_text: "]".to_string(),
11022 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11023 }]))
11024 });
11025
11026 editor_handle.update_in(cx, |editor, window, cx| {
11027 window.focus(&editor.focus_handle(cx));
11028 editor.change_selections(None, window, cx, |s| {
11029 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
11030 });
11031 editor.handle_input("{", window, cx);
11032 });
11033
11034 cx.executor().run_until_parked();
11035
11036 buffer.update(cx, |buffer, _| {
11037 assert_eq!(
11038 buffer.text(),
11039 "fn main() { let a = {5}; }",
11040 "No extra braces from on type formatting should appear in the buffer"
11041 )
11042 });
11043}
11044
11045#[gpui::test]
11046async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) {
11047 init_test(cx, |_| {});
11048
11049 let fs = FakeFs::new(cx.executor());
11050 fs.insert_tree(
11051 path!("/a"),
11052 json!({
11053 "main.rs": "fn main() { let a = 5; }",
11054 "other.rs": "// Test file",
11055 }),
11056 )
11057 .await;
11058
11059 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11060
11061 let server_restarts = Arc::new(AtomicUsize::new(0));
11062 let closure_restarts = Arc::clone(&server_restarts);
11063 let language_server_name = "test language server";
11064 let language_name: LanguageName = "Rust".into();
11065
11066 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11067 language_registry.add(Arc::new(Language::new(
11068 LanguageConfig {
11069 name: language_name.clone(),
11070 matcher: LanguageMatcher {
11071 path_suffixes: vec!["rs".to_string()],
11072 ..Default::default()
11073 },
11074 ..Default::default()
11075 },
11076 Some(tree_sitter_rust::LANGUAGE.into()),
11077 )));
11078 let mut fake_servers = language_registry.register_fake_lsp(
11079 "Rust",
11080 FakeLspAdapter {
11081 name: language_server_name,
11082 initialization_options: Some(json!({
11083 "testOptionValue": true
11084 })),
11085 initializer: Some(Box::new(move |fake_server| {
11086 let task_restarts = Arc::clone(&closure_restarts);
11087 fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
11088 task_restarts.fetch_add(1, atomic::Ordering::Release);
11089 futures::future::ready(Ok(()))
11090 });
11091 })),
11092 ..Default::default()
11093 },
11094 );
11095
11096 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11097 let _buffer = project
11098 .update(cx, |project, cx| {
11099 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
11100 })
11101 .await
11102 .unwrap();
11103 let _fake_server = fake_servers.next().await.unwrap();
11104 update_test_language_settings(cx, |language_settings| {
11105 language_settings.languages.insert(
11106 language_name.clone(),
11107 LanguageSettingsContent {
11108 tab_size: NonZeroU32::new(8),
11109 ..Default::default()
11110 },
11111 );
11112 });
11113 cx.executor().run_until_parked();
11114 assert_eq!(
11115 server_restarts.load(atomic::Ordering::Acquire),
11116 0,
11117 "Should not restart LSP server on an unrelated change"
11118 );
11119
11120 update_test_project_settings(cx, |project_settings| {
11121 project_settings.lsp.insert(
11122 "Some other server name".into(),
11123 LspSettings {
11124 binary: None,
11125 settings: None,
11126 initialization_options: Some(json!({
11127 "some other init value": false
11128 })),
11129 },
11130 );
11131 });
11132 cx.executor().run_until_parked();
11133 assert_eq!(
11134 server_restarts.load(atomic::Ordering::Acquire),
11135 0,
11136 "Should not restart LSP server on an unrelated LSP settings change"
11137 );
11138
11139 update_test_project_settings(cx, |project_settings| {
11140 project_settings.lsp.insert(
11141 language_server_name.into(),
11142 LspSettings {
11143 binary: None,
11144 settings: None,
11145 initialization_options: Some(json!({
11146 "anotherInitValue": false
11147 })),
11148 },
11149 );
11150 });
11151 cx.executor().run_until_parked();
11152 assert_eq!(
11153 server_restarts.load(atomic::Ordering::Acquire),
11154 1,
11155 "Should restart LSP server on a related LSP settings change"
11156 );
11157
11158 update_test_project_settings(cx, |project_settings| {
11159 project_settings.lsp.insert(
11160 language_server_name.into(),
11161 LspSettings {
11162 binary: None,
11163 settings: None,
11164 initialization_options: Some(json!({
11165 "anotherInitValue": false
11166 })),
11167 },
11168 );
11169 });
11170 cx.executor().run_until_parked();
11171 assert_eq!(
11172 server_restarts.load(atomic::Ordering::Acquire),
11173 1,
11174 "Should not restart LSP server on a related LSP settings change that is the same"
11175 );
11176
11177 update_test_project_settings(cx, |project_settings| {
11178 project_settings.lsp.insert(
11179 language_server_name.into(),
11180 LspSettings {
11181 binary: None,
11182 settings: None,
11183 initialization_options: None,
11184 },
11185 );
11186 });
11187 cx.executor().run_until_parked();
11188 assert_eq!(
11189 server_restarts.load(atomic::Ordering::Acquire),
11190 2,
11191 "Should restart LSP server on another related LSP settings change"
11192 );
11193}
11194
11195#[gpui::test]
11196async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
11197 init_test(cx, |_| {});
11198
11199 let mut cx = EditorLspTestContext::new_rust(
11200 lsp::ServerCapabilities {
11201 completion_provider: Some(lsp::CompletionOptions {
11202 trigger_characters: Some(vec![".".to_string()]),
11203 resolve_provider: Some(true),
11204 ..Default::default()
11205 }),
11206 ..Default::default()
11207 },
11208 cx,
11209 )
11210 .await;
11211
11212 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11213 cx.simulate_keystroke(".");
11214 let completion_item = lsp::CompletionItem {
11215 label: "some".into(),
11216 kind: Some(lsp::CompletionItemKind::SNIPPET),
11217 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11218 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11219 kind: lsp::MarkupKind::Markdown,
11220 value: "```rust\nSome(2)\n```".to_string(),
11221 })),
11222 deprecated: Some(false),
11223 sort_text: Some("fffffff2".to_string()),
11224 filter_text: Some("some".to_string()),
11225 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11226 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11227 range: lsp::Range {
11228 start: lsp::Position {
11229 line: 0,
11230 character: 22,
11231 },
11232 end: lsp::Position {
11233 line: 0,
11234 character: 22,
11235 },
11236 },
11237 new_text: "Some(2)".to_string(),
11238 })),
11239 additional_text_edits: Some(vec![lsp::TextEdit {
11240 range: lsp::Range {
11241 start: lsp::Position {
11242 line: 0,
11243 character: 20,
11244 },
11245 end: lsp::Position {
11246 line: 0,
11247 character: 22,
11248 },
11249 },
11250 new_text: "".to_string(),
11251 }]),
11252 ..Default::default()
11253 };
11254
11255 let closure_completion_item = completion_item.clone();
11256 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11257 let task_completion_item = closure_completion_item.clone();
11258 async move {
11259 Ok(Some(lsp::CompletionResponse::Array(vec![
11260 task_completion_item,
11261 ])))
11262 }
11263 });
11264
11265 request.next().await;
11266
11267 cx.condition(|editor, _| editor.context_menu_visible())
11268 .await;
11269 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11270 editor
11271 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11272 .unwrap()
11273 });
11274 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
11275
11276 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
11277 let task_completion_item = completion_item.clone();
11278 async move { Ok(task_completion_item) }
11279 })
11280 .next()
11281 .await
11282 .unwrap();
11283 apply_additional_edits.await.unwrap();
11284 cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
11285}
11286
11287#[gpui::test]
11288async fn test_completions_resolve_updates_labels_if_filter_text_matches(
11289 cx: &mut gpui::TestAppContext,
11290) {
11291 init_test(cx, |_| {});
11292
11293 let mut cx = EditorLspTestContext::new_rust(
11294 lsp::ServerCapabilities {
11295 completion_provider: Some(lsp::CompletionOptions {
11296 trigger_characters: Some(vec![".".to_string()]),
11297 resolve_provider: Some(true),
11298 ..Default::default()
11299 }),
11300 ..Default::default()
11301 },
11302 cx,
11303 )
11304 .await;
11305
11306 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11307 cx.simulate_keystroke(".");
11308
11309 let item1 = lsp::CompletionItem {
11310 label: "method id()".to_string(),
11311 filter_text: Some("id".to_string()),
11312 detail: None,
11313 documentation: None,
11314 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11315 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11316 new_text: ".id".to_string(),
11317 })),
11318 ..lsp::CompletionItem::default()
11319 };
11320
11321 let item2 = lsp::CompletionItem {
11322 label: "other".to_string(),
11323 filter_text: Some("other".to_string()),
11324 detail: None,
11325 documentation: None,
11326 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11327 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11328 new_text: ".other".to_string(),
11329 })),
11330 ..lsp::CompletionItem::default()
11331 };
11332
11333 let item1 = item1.clone();
11334 cx.handle_request::<lsp::request::Completion, _, _>({
11335 let item1 = item1.clone();
11336 move |_, _, _| {
11337 let item1 = item1.clone();
11338 let item2 = item2.clone();
11339 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
11340 }
11341 })
11342 .next()
11343 .await;
11344
11345 cx.condition(|editor, _| editor.context_menu_visible())
11346 .await;
11347 cx.update_editor(|editor, _, _| {
11348 let context_menu = editor.context_menu.borrow_mut();
11349 let context_menu = context_menu
11350 .as_ref()
11351 .expect("Should have the context menu deployed");
11352 match context_menu {
11353 CodeContextMenu::Completions(completions_menu) => {
11354 let completions = completions_menu.completions.borrow_mut();
11355 assert_eq!(
11356 completions
11357 .iter()
11358 .map(|completion| &completion.label.text)
11359 .collect::<Vec<_>>(),
11360 vec!["method id()", "other"]
11361 )
11362 }
11363 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11364 }
11365 });
11366
11367 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>({
11368 let item1 = item1.clone();
11369 move |_, item_to_resolve, _| {
11370 let item1 = item1.clone();
11371 async move {
11372 if item1 == item_to_resolve {
11373 Ok(lsp::CompletionItem {
11374 label: "method id()".to_string(),
11375 filter_text: Some("id".to_string()),
11376 detail: Some("Now resolved!".to_string()),
11377 documentation: Some(lsp::Documentation::String("Docs".to_string())),
11378 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11379 range: lsp::Range::new(
11380 lsp::Position::new(0, 22),
11381 lsp::Position::new(0, 22),
11382 ),
11383 new_text: ".id".to_string(),
11384 })),
11385 ..lsp::CompletionItem::default()
11386 })
11387 } else {
11388 Ok(item_to_resolve)
11389 }
11390 }
11391 }
11392 })
11393 .next()
11394 .await
11395 .unwrap();
11396 cx.run_until_parked();
11397
11398 cx.update_editor(|editor, window, cx| {
11399 editor.context_menu_next(&Default::default(), window, cx);
11400 });
11401
11402 cx.update_editor(|editor, _, _| {
11403 let context_menu = editor.context_menu.borrow_mut();
11404 let context_menu = context_menu
11405 .as_ref()
11406 .expect("Should have the context menu deployed");
11407 match context_menu {
11408 CodeContextMenu::Completions(completions_menu) => {
11409 let completions = completions_menu.completions.borrow_mut();
11410 assert_eq!(
11411 completions
11412 .iter()
11413 .map(|completion| &completion.label.text)
11414 .collect::<Vec<_>>(),
11415 vec!["method id() Now resolved!", "other"],
11416 "Should update first completion label, but not second as the filter text did not match."
11417 );
11418 }
11419 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11420 }
11421 });
11422}
11423
11424#[gpui::test]
11425async fn test_completions_resolve_happens_once(cx: &mut gpui::TestAppContext) {
11426 init_test(cx, |_| {});
11427
11428 let mut cx = EditorLspTestContext::new_rust(
11429 lsp::ServerCapabilities {
11430 completion_provider: Some(lsp::CompletionOptions {
11431 trigger_characters: Some(vec![".".to_string()]),
11432 resolve_provider: Some(true),
11433 ..Default::default()
11434 }),
11435 ..Default::default()
11436 },
11437 cx,
11438 )
11439 .await;
11440
11441 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11442 cx.simulate_keystroke(".");
11443
11444 let unresolved_item_1 = lsp::CompletionItem {
11445 label: "id".to_string(),
11446 filter_text: Some("id".to_string()),
11447 detail: None,
11448 documentation: None,
11449 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11450 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11451 new_text: ".id".to_string(),
11452 })),
11453 ..lsp::CompletionItem::default()
11454 };
11455 let resolved_item_1 = lsp::CompletionItem {
11456 additional_text_edits: Some(vec![lsp::TextEdit {
11457 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
11458 new_text: "!!".to_string(),
11459 }]),
11460 ..unresolved_item_1.clone()
11461 };
11462 let unresolved_item_2 = lsp::CompletionItem {
11463 label: "other".to_string(),
11464 filter_text: Some("other".to_string()),
11465 detail: None,
11466 documentation: None,
11467 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11468 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11469 new_text: ".other".to_string(),
11470 })),
11471 ..lsp::CompletionItem::default()
11472 };
11473 let resolved_item_2 = lsp::CompletionItem {
11474 additional_text_edits: Some(vec![lsp::TextEdit {
11475 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
11476 new_text: "??".to_string(),
11477 }]),
11478 ..unresolved_item_2.clone()
11479 };
11480
11481 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
11482 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
11483 cx.lsp
11484 .server
11485 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
11486 let unresolved_item_1 = unresolved_item_1.clone();
11487 let resolved_item_1 = resolved_item_1.clone();
11488 let unresolved_item_2 = unresolved_item_2.clone();
11489 let resolved_item_2 = resolved_item_2.clone();
11490 let resolve_requests_1 = resolve_requests_1.clone();
11491 let resolve_requests_2 = resolve_requests_2.clone();
11492 move |unresolved_request, _| {
11493 let unresolved_item_1 = unresolved_item_1.clone();
11494 let resolved_item_1 = resolved_item_1.clone();
11495 let unresolved_item_2 = unresolved_item_2.clone();
11496 let resolved_item_2 = resolved_item_2.clone();
11497 let resolve_requests_1 = resolve_requests_1.clone();
11498 let resolve_requests_2 = resolve_requests_2.clone();
11499 async move {
11500 if unresolved_request == unresolved_item_1 {
11501 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
11502 Ok(resolved_item_1.clone())
11503 } else if unresolved_request == unresolved_item_2 {
11504 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
11505 Ok(resolved_item_2.clone())
11506 } else {
11507 panic!("Unexpected completion item {unresolved_request:?}")
11508 }
11509 }
11510 }
11511 })
11512 .detach();
11513
11514 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11515 let unresolved_item_1 = unresolved_item_1.clone();
11516 let unresolved_item_2 = unresolved_item_2.clone();
11517 async move {
11518 Ok(Some(lsp::CompletionResponse::Array(vec![
11519 unresolved_item_1,
11520 unresolved_item_2,
11521 ])))
11522 }
11523 })
11524 .next()
11525 .await;
11526
11527 cx.condition(|editor, _| editor.context_menu_visible())
11528 .await;
11529 cx.update_editor(|editor, _, _| {
11530 let context_menu = editor.context_menu.borrow_mut();
11531 let context_menu = context_menu
11532 .as_ref()
11533 .expect("Should have the context menu deployed");
11534 match context_menu {
11535 CodeContextMenu::Completions(completions_menu) => {
11536 let completions = completions_menu.completions.borrow_mut();
11537 assert_eq!(
11538 completions
11539 .iter()
11540 .map(|completion| &completion.label.text)
11541 .collect::<Vec<_>>(),
11542 vec!["id", "other"]
11543 )
11544 }
11545 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11546 }
11547 });
11548 cx.run_until_parked();
11549
11550 cx.update_editor(|editor, window, cx| {
11551 editor.context_menu_next(&ContextMenuNext, window, cx);
11552 });
11553 cx.run_until_parked();
11554 cx.update_editor(|editor, window, cx| {
11555 editor.context_menu_prev(&ContextMenuPrev, window, cx);
11556 });
11557 cx.run_until_parked();
11558 cx.update_editor(|editor, window, cx| {
11559 editor.context_menu_next(&ContextMenuNext, window, cx);
11560 });
11561 cx.run_until_parked();
11562 cx.update_editor(|editor, window, cx| {
11563 editor
11564 .compose_completion(&ComposeCompletion::default(), window, cx)
11565 .expect("No task returned")
11566 })
11567 .await
11568 .expect("Completion failed");
11569 cx.run_until_parked();
11570
11571 cx.update_editor(|editor, _, cx| {
11572 assert_eq!(
11573 resolve_requests_1.load(atomic::Ordering::Acquire),
11574 1,
11575 "Should always resolve once despite multiple selections"
11576 );
11577 assert_eq!(
11578 resolve_requests_2.load(atomic::Ordering::Acquire),
11579 1,
11580 "Should always resolve once after multiple selections and applying the completion"
11581 );
11582 assert_eq!(
11583 editor.text(cx),
11584 "fn main() { let a = ??.other; }",
11585 "Should use resolved data when applying the completion"
11586 );
11587 });
11588}
11589
11590#[gpui::test]
11591async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
11592 init_test(cx, |_| {});
11593
11594 let item_0 = lsp::CompletionItem {
11595 label: "abs".into(),
11596 insert_text: Some("abs".into()),
11597 data: Some(json!({ "very": "special"})),
11598 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
11599 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11600 lsp::InsertReplaceEdit {
11601 new_text: "abs".to_string(),
11602 insert: lsp::Range::default(),
11603 replace: lsp::Range::default(),
11604 },
11605 )),
11606 ..lsp::CompletionItem::default()
11607 };
11608 let items = iter::once(item_0.clone())
11609 .chain((11..51).map(|i| lsp::CompletionItem {
11610 label: format!("item_{}", i),
11611 insert_text: Some(format!("item_{}", i)),
11612 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
11613 ..lsp::CompletionItem::default()
11614 }))
11615 .collect::<Vec<_>>();
11616
11617 let default_commit_characters = vec!["?".to_string()];
11618 let default_data = json!({ "default": "data"});
11619 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
11620 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
11621 let default_edit_range = lsp::Range {
11622 start: lsp::Position {
11623 line: 0,
11624 character: 5,
11625 },
11626 end: lsp::Position {
11627 line: 0,
11628 character: 5,
11629 },
11630 };
11631
11632 let item_0_out = lsp::CompletionItem {
11633 commit_characters: Some(default_commit_characters.clone()),
11634 insert_text_format: Some(default_insert_text_format),
11635 ..item_0
11636 };
11637 let items_out = iter::once(item_0_out)
11638 .chain(items[1..].iter().map(|item| lsp::CompletionItem {
11639 commit_characters: Some(default_commit_characters.clone()),
11640 data: Some(default_data.clone()),
11641 insert_text_mode: Some(default_insert_text_mode),
11642 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11643 range: default_edit_range,
11644 new_text: item.label.clone(),
11645 })),
11646 ..item.clone()
11647 }))
11648 .collect::<Vec<lsp::CompletionItem>>();
11649
11650 let mut cx = EditorLspTestContext::new_rust(
11651 lsp::ServerCapabilities {
11652 completion_provider: Some(lsp::CompletionOptions {
11653 trigger_characters: Some(vec![".".to_string()]),
11654 resolve_provider: Some(true),
11655 ..Default::default()
11656 }),
11657 ..Default::default()
11658 },
11659 cx,
11660 )
11661 .await;
11662
11663 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11664 cx.simulate_keystroke(".");
11665
11666 let completion_data = default_data.clone();
11667 let completion_characters = default_commit_characters.clone();
11668 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11669 let default_data = completion_data.clone();
11670 let default_commit_characters = completion_characters.clone();
11671 let items = items.clone();
11672 async move {
11673 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
11674 items,
11675 item_defaults: Some(lsp::CompletionListItemDefaults {
11676 data: Some(default_data.clone()),
11677 commit_characters: Some(default_commit_characters.clone()),
11678 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
11679 default_edit_range,
11680 )),
11681 insert_text_format: Some(default_insert_text_format),
11682 insert_text_mode: Some(default_insert_text_mode),
11683 }),
11684 ..lsp::CompletionList::default()
11685 })))
11686 }
11687 })
11688 .next()
11689 .await;
11690
11691 let resolved_items = Arc::new(Mutex::new(Vec::new()));
11692 cx.lsp
11693 .server
11694 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
11695 let closure_resolved_items = resolved_items.clone();
11696 move |item_to_resolve, _| {
11697 let closure_resolved_items = closure_resolved_items.clone();
11698 async move {
11699 closure_resolved_items.lock().push(item_to_resolve.clone());
11700 Ok(item_to_resolve)
11701 }
11702 }
11703 })
11704 .detach();
11705
11706 cx.condition(|editor, _| editor.context_menu_visible())
11707 .await;
11708 cx.run_until_parked();
11709 cx.update_editor(|editor, _, _| {
11710 let menu = editor.context_menu.borrow_mut();
11711 match menu.as_ref().expect("should have the completions menu") {
11712 CodeContextMenu::Completions(completions_menu) => {
11713 assert_eq!(
11714 completions_menu
11715 .entries
11716 .borrow()
11717 .iter()
11718 .map(|mat| mat.string.clone())
11719 .collect::<Vec<String>>(),
11720 items_out
11721 .iter()
11722 .map(|completion| completion.label.clone())
11723 .collect::<Vec<String>>()
11724 );
11725 }
11726 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
11727 }
11728 });
11729 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
11730 // with 4 from the end.
11731 assert_eq!(
11732 *resolved_items.lock(),
11733 [
11734 &items_out[0..16],
11735 &items_out[items_out.len() - 4..items_out.len()]
11736 ]
11737 .concat()
11738 .iter()
11739 .cloned()
11740 .collect::<Vec<lsp::CompletionItem>>()
11741 );
11742 resolved_items.lock().clear();
11743
11744 cx.update_editor(|editor, window, cx| {
11745 editor.context_menu_prev(&ContextMenuPrev, window, cx);
11746 });
11747 cx.run_until_parked();
11748 // Completions that have already been resolved are skipped.
11749 assert_eq!(
11750 *resolved_items.lock(),
11751 items_out[items_out.len() - 16..items_out.len() - 4]
11752 .iter()
11753 .cloned()
11754 .collect::<Vec<lsp::CompletionItem>>()
11755 );
11756 resolved_items.lock().clear();
11757}
11758
11759#[gpui::test]
11760async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
11761 init_test(cx, |_| {});
11762
11763 let mut cx = EditorLspTestContext::new(
11764 Language::new(
11765 LanguageConfig {
11766 matcher: LanguageMatcher {
11767 path_suffixes: vec!["jsx".into()],
11768 ..Default::default()
11769 },
11770 overrides: [(
11771 "element".into(),
11772 LanguageConfigOverride {
11773 word_characters: Override::Set(['-'].into_iter().collect()),
11774 ..Default::default()
11775 },
11776 )]
11777 .into_iter()
11778 .collect(),
11779 ..Default::default()
11780 },
11781 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
11782 )
11783 .with_override_query("(jsx_self_closing_element) @element")
11784 .unwrap(),
11785 lsp::ServerCapabilities {
11786 completion_provider: Some(lsp::CompletionOptions {
11787 trigger_characters: Some(vec![":".to_string()]),
11788 ..Default::default()
11789 }),
11790 ..Default::default()
11791 },
11792 cx,
11793 )
11794 .await;
11795
11796 cx.lsp
11797 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11798 Ok(Some(lsp::CompletionResponse::Array(vec![
11799 lsp::CompletionItem {
11800 label: "bg-blue".into(),
11801 ..Default::default()
11802 },
11803 lsp::CompletionItem {
11804 label: "bg-red".into(),
11805 ..Default::default()
11806 },
11807 lsp::CompletionItem {
11808 label: "bg-yellow".into(),
11809 ..Default::default()
11810 },
11811 ])))
11812 });
11813
11814 cx.set_state(r#"<p class="bgˇ" />"#);
11815
11816 // Trigger completion when typing a dash, because the dash is an extra
11817 // word character in the 'element' scope, which contains the cursor.
11818 cx.simulate_keystroke("-");
11819 cx.executor().run_until_parked();
11820 cx.update_editor(|editor, _, _| {
11821 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11822 {
11823 assert_eq!(
11824 completion_menu_entries(&menu),
11825 &["bg-red", "bg-blue", "bg-yellow"]
11826 );
11827 } else {
11828 panic!("expected completion menu to be open");
11829 }
11830 });
11831
11832 cx.simulate_keystroke("l");
11833 cx.executor().run_until_parked();
11834 cx.update_editor(|editor, _, _| {
11835 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11836 {
11837 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
11838 } else {
11839 panic!("expected completion menu to be open");
11840 }
11841 });
11842
11843 // When filtering completions, consider the character after the '-' to
11844 // be the start of a subword.
11845 cx.set_state(r#"<p class="yelˇ" />"#);
11846 cx.simulate_keystroke("l");
11847 cx.executor().run_until_parked();
11848 cx.update_editor(|editor, _, _| {
11849 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11850 {
11851 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
11852 } else {
11853 panic!("expected completion menu to be open");
11854 }
11855 });
11856}
11857
11858fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
11859 let entries = menu.entries.borrow();
11860 entries.iter().map(|mat| mat.string.clone()).collect()
11861}
11862
11863#[gpui::test]
11864async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
11865 init_test(cx, |settings| {
11866 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
11867 FormatterList(vec![Formatter::Prettier].into()),
11868 ))
11869 });
11870
11871 let fs = FakeFs::new(cx.executor());
11872 fs.insert_file(path!("/file.ts"), Default::default()).await;
11873
11874 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
11875 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11876
11877 language_registry.add(Arc::new(Language::new(
11878 LanguageConfig {
11879 name: "TypeScript".into(),
11880 matcher: LanguageMatcher {
11881 path_suffixes: vec!["ts".to_string()],
11882 ..Default::default()
11883 },
11884 ..Default::default()
11885 },
11886 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11887 )));
11888 update_test_language_settings(cx, |settings| {
11889 settings.defaults.prettier = Some(PrettierSettings {
11890 allowed: true,
11891 ..PrettierSettings::default()
11892 });
11893 });
11894
11895 let test_plugin = "test_plugin";
11896 let _ = language_registry.register_fake_lsp(
11897 "TypeScript",
11898 FakeLspAdapter {
11899 prettier_plugins: vec![test_plugin],
11900 ..Default::default()
11901 },
11902 );
11903
11904 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
11905 let buffer = project
11906 .update(cx, |project, cx| {
11907 project.open_local_buffer(path!("/file.ts"), cx)
11908 })
11909 .await
11910 .unwrap();
11911
11912 let buffer_text = "one\ntwo\nthree\n";
11913 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11914 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11915 editor.update_in(cx, |editor, window, cx| {
11916 editor.set_text(buffer_text, window, cx)
11917 });
11918
11919 editor
11920 .update_in(cx, |editor, window, cx| {
11921 editor.perform_format(
11922 project.clone(),
11923 FormatTrigger::Manual,
11924 FormatTarget::Buffers,
11925 window,
11926 cx,
11927 )
11928 })
11929 .unwrap()
11930 .await;
11931 assert_eq!(
11932 editor.update(cx, |editor, cx| editor.text(cx)),
11933 buffer_text.to_string() + prettier_format_suffix,
11934 "Test prettier formatting was not applied to the original buffer text",
11935 );
11936
11937 update_test_language_settings(cx, |settings| {
11938 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
11939 });
11940 let format = editor.update_in(cx, |editor, window, cx| {
11941 editor.perform_format(
11942 project.clone(),
11943 FormatTrigger::Manual,
11944 FormatTarget::Buffers,
11945 window,
11946 cx,
11947 )
11948 });
11949 format.await.unwrap();
11950 assert_eq!(
11951 editor.update(cx, |editor, cx| editor.text(cx)),
11952 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
11953 "Autoformatting (via test prettier) was not applied to the original buffer text",
11954 );
11955}
11956
11957#[gpui::test]
11958async fn test_addition_reverts(cx: &mut gpui::TestAppContext) {
11959 init_test(cx, |_| {});
11960 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11961 let base_text = indoc! {r#"
11962 struct Row;
11963 struct Row1;
11964 struct Row2;
11965
11966 struct Row4;
11967 struct Row5;
11968 struct Row6;
11969
11970 struct Row8;
11971 struct Row9;
11972 struct Row10;"#};
11973
11974 // When addition hunks are not adjacent to carets, no hunk revert is performed
11975 assert_hunk_revert(
11976 indoc! {r#"struct Row;
11977 struct Row1;
11978 struct Row1.1;
11979 struct Row1.2;
11980 struct Row2;ˇ
11981
11982 struct Row4;
11983 struct Row5;
11984 struct Row6;
11985
11986 struct Row8;
11987 ˇstruct Row9;
11988 struct Row9.1;
11989 struct Row9.2;
11990 struct Row9.3;
11991 struct Row10;"#},
11992 vec![DiffHunkStatus::added(), DiffHunkStatus::added()],
11993 indoc! {r#"struct Row;
11994 struct Row1;
11995 struct Row1.1;
11996 struct Row1.2;
11997 struct Row2;ˇ
11998
11999 struct Row4;
12000 struct Row5;
12001 struct Row6;
12002
12003 struct Row8;
12004 ˇstruct Row9;
12005 struct Row9.1;
12006 struct Row9.2;
12007 struct Row9.3;
12008 struct Row10;"#},
12009 base_text,
12010 &mut cx,
12011 );
12012 // Same for selections
12013 assert_hunk_revert(
12014 indoc! {r#"struct Row;
12015 struct Row1;
12016 struct Row2;
12017 struct Row2.1;
12018 struct Row2.2;
12019 «ˇ
12020 struct Row4;
12021 struct» Row5;
12022 «struct Row6;
12023 ˇ»
12024 struct Row9.1;
12025 struct Row9.2;
12026 struct Row9.3;
12027 struct Row8;
12028 struct Row9;
12029 struct Row10;"#},
12030 vec![DiffHunkStatus::added(), DiffHunkStatus::added()],
12031 indoc! {r#"struct Row;
12032 struct Row1;
12033 struct Row2;
12034 struct Row2.1;
12035 struct Row2.2;
12036 «ˇ
12037 struct Row4;
12038 struct» Row5;
12039 «struct Row6;
12040 ˇ»
12041 struct Row9.1;
12042 struct Row9.2;
12043 struct Row9.3;
12044 struct Row8;
12045 struct Row9;
12046 struct Row10;"#},
12047 base_text,
12048 &mut cx,
12049 );
12050
12051 // When carets and selections intersect the addition hunks, those are reverted.
12052 // Adjacent carets got merged.
12053 assert_hunk_revert(
12054 indoc! {r#"struct Row;
12055 ˇ// something on the top
12056 struct Row1;
12057 struct Row2;
12058 struct Roˇw3.1;
12059 struct Row2.2;
12060 struct Row2.3;ˇ
12061
12062 struct Row4;
12063 struct ˇRow5.1;
12064 struct Row5.2;
12065 struct «Rowˇ»5.3;
12066 struct Row5;
12067 struct Row6;
12068 ˇ
12069 struct Row9.1;
12070 struct «Rowˇ»9.2;
12071 struct «ˇRow»9.3;
12072 struct Row8;
12073 struct Row9;
12074 «ˇ// something on bottom»
12075 struct Row10;"#},
12076 vec![
12077 DiffHunkStatus::added(),
12078 DiffHunkStatus::added(),
12079 DiffHunkStatus::added(),
12080 DiffHunkStatus::added(),
12081 DiffHunkStatus::added(),
12082 ],
12083 indoc! {r#"struct Row;
12084 ˇstruct Row1;
12085 struct Row2;
12086 ˇ
12087 struct Row4;
12088 ˇstruct Row5;
12089 struct Row6;
12090 ˇ
12091 ˇstruct Row8;
12092 struct Row9;
12093 ˇstruct Row10;"#},
12094 base_text,
12095 &mut cx,
12096 );
12097}
12098
12099#[gpui::test]
12100async fn test_modification_reverts(cx: &mut gpui::TestAppContext) {
12101 init_test(cx, |_| {});
12102 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12103 let base_text = indoc! {r#"
12104 struct Row;
12105 struct Row1;
12106 struct Row2;
12107
12108 struct Row4;
12109 struct Row5;
12110 struct Row6;
12111
12112 struct Row8;
12113 struct Row9;
12114 struct Row10;"#};
12115
12116 // Modification hunks behave the same as the addition ones.
12117 assert_hunk_revert(
12118 indoc! {r#"struct Row;
12119 struct Row1;
12120 struct Row33;
12121 ˇ
12122 struct Row4;
12123 struct Row5;
12124 struct Row6;
12125 ˇ
12126 struct Row99;
12127 struct Row9;
12128 struct Row10;"#},
12129 vec![DiffHunkStatus::modified(), DiffHunkStatus::modified()],
12130 indoc! {r#"struct Row;
12131 struct Row1;
12132 struct Row33;
12133 ˇ
12134 struct Row4;
12135 struct Row5;
12136 struct Row6;
12137 ˇ
12138 struct Row99;
12139 struct Row9;
12140 struct Row10;"#},
12141 base_text,
12142 &mut cx,
12143 );
12144 assert_hunk_revert(
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 vec![DiffHunkStatus::modified(), DiffHunkStatus::modified()],
12157 indoc! {r#"struct Row;
12158 struct Row1;
12159 struct Row33;
12160 «ˇ
12161 struct Row4;
12162 struct» Row5;
12163 «struct Row6;
12164 ˇ»
12165 struct Row99;
12166 struct Row9;
12167 struct Row10;"#},
12168 base_text,
12169 &mut cx,
12170 );
12171
12172 assert_hunk_revert(
12173 indoc! {r#"ˇstruct Row1.1;
12174 struct Row1;
12175 «ˇstr»uct Row22;
12176
12177 struct ˇRow44;
12178 struct Row5;
12179 struct «Rˇ»ow66;ˇ
12180
12181 «struˇ»ct Row88;
12182 struct Row9;
12183 struct Row1011;ˇ"#},
12184 vec![
12185 DiffHunkStatus::modified(),
12186 DiffHunkStatus::modified(),
12187 DiffHunkStatus::modified(),
12188 DiffHunkStatus::modified(),
12189 DiffHunkStatus::modified(),
12190 DiffHunkStatus::modified(),
12191 ],
12192 indoc! {r#"struct Row;
12193 ˇstruct Row1;
12194 struct Row2;
12195 ˇ
12196 struct Row4;
12197 ˇstruct Row5;
12198 struct Row6;
12199 ˇ
12200 struct Row8;
12201 ˇstruct Row9;
12202 struct Row10;ˇ"#},
12203 base_text,
12204 &mut cx,
12205 );
12206}
12207
12208#[gpui::test]
12209async fn test_deleting_over_diff_hunk(cx: &mut gpui::TestAppContext) {
12210 init_test(cx, |_| {});
12211 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12212 let base_text = indoc! {r#"
12213 one
12214
12215 two
12216 three
12217 "#};
12218
12219 cx.set_diff_base(base_text);
12220 cx.set_state("\nˇ\n");
12221 cx.executor().run_until_parked();
12222 cx.update_editor(|editor, _window, cx| {
12223 editor.expand_selected_diff_hunks(cx);
12224 });
12225 cx.executor().run_until_parked();
12226 cx.update_editor(|editor, window, cx| {
12227 editor.backspace(&Default::default(), window, cx);
12228 });
12229 cx.run_until_parked();
12230 cx.assert_state_with_diff(
12231 indoc! {r#"
12232
12233 - two
12234 - threeˇ
12235 +
12236 "#}
12237 .to_string(),
12238 );
12239}
12240
12241#[gpui::test]
12242async fn test_deletion_reverts(cx: &mut gpui::TestAppContext) {
12243 init_test(cx, |_| {});
12244 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12245 let base_text = indoc! {r#"struct Row;
12246struct Row1;
12247struct Row2;
12248
12249struct Row4;
12250struct Row5;
12251struct Row6;
12252
12253struct Row8;
12254struct Row9;
12255struct Row10;"#};
12256
12257 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
12258 assert_hunk_revert(
12259 indoc! {r#"struct Row;
12260 struct Row2;
12261
12262 ˇstruct Row4;
12263 struct Row5;
12264 struct Row6;
12265 ˇ
12266 struct Row8;
12267 struct Row10;"#},
12268 vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()],
12269 indoc! {r#"struct Row;
12270 struct Row2;
12271
12272 ˇstruct Row4;
12273 struct Row5;
12274 struct Row6;
12275 ˇ
12276 struct Row8;
12277 struct Row10;"#},
12278 base_text,
12279 &mut cx,
12280 );
12281 assert_hunk_revert(
12282 indoc! {r#"struct Row;
12283 struct Row2;
12284
12285 «ˇstruct Row4;
12286 struct» Row5;
12287 «struct Row6;
12288 ˇ»
12289 struct Row8;
12290 struct Row10;"#},
12291 vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()],
12292 indoc! {r#"struct Row;
12293 struct Row2;
12294
12295 «ˇstruct Row4;
12296 struct» Row5;
12297 «struct Row6;
12298 ˇ»
12299 struct Row8;
12300 struct Row10;"#},
12301 base_text,
12302 &mut cx,
12303 );
12304
12305 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
12306 assert_hunk_revert(
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 vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()],
12317 indoc! {r#"struct Row;
12318 struct Row1;
12319 ˇstruct Row2;
12320
12321 struct Row4;
12322 struct Row5;
12323 struct Row6;
12324
12325 struct Row8;ˇ
12326 struct Row9;
12327 struct Row10;"#},
12328 base_text,
12329 &mut cx,
12330 );
12331 assert_hunk_revert(
12332 indoc! {r#"struct Row;
12333 struct Row2«ˇ;
12334 struct Row4;
12335 struct» Row5;
12336 «struct Row6;
12337
12338 struct Row8;ˇ»
12339 struct Row10;"#},
12340 vec![
12341 DiffHunkStatus::removed(),
12342 DiffHunkStatus::removed(),
12343 DiffHunkStatus::removed(),
12344 ],
12345 indoc! {r#"struct Row;
12346 struct Row1;
12347 struct Row2«ˇ;
12348
12349 struct Row4;
12350 struct» Row5;
12351 «struct Row6;
12352
12353 struct Row8;ˇ»
12354 struct Row9;
12355 struct Row10;"#},
12356 base_text,
12357 &mut cx,
12358 );
12359}
12360
12361#[gpui::test]
12362async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
12363 init_test(cx, |_| {});
12364
12365 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
12366 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
12367 let base_text_3 =
12368 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
12369
12370 let text_1 = edit_first_char_of_every_line(base_text_1);
12371 let text_2 = edit_first_char_of_every_line(base_text_2);
12372 let text_3 = edit_first_char_of_every_line(base_text_3);
12373
12374 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
12375 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
12376 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
12377
12378 let multibuffer = cx.new(|cx| {
12379 let mut multibuffer = MultiBuffer::new(ReadWrite);
12380 multibuffer.push_excerpts(
12381 buffer_1.clone(),
12382 [
12383 ExcerptRange {
12384 context: Point::new(0, 0)..Point::new(3, 0),
12385 primary: None,
12386 },
12387 ExcerptRange {
12388 context: Point::new(5, 0)..Point::new(7, 0),
12389 primary: None,
12390 },
12391 ExcerptRange {
12392 context: Point::new(9, 0)..Point::new(10, 4),
12393 primary: None,
12394 },
12395 ],
12396 cx,
12397 );
12398 multibuffer.push_excerpts(
12399 buffer_2.clone(),
12400 [
12401 ExcerptRange {
12402 context: Point::new(0, 0)..Point::new(3, 0),
12403 primary: None,
12404 },
12405 ExcerptRange {
12406 context: Point::new(5, 0)..Point::new(7, 0),
12407 primary: None,
12408 },
12409 ExcerptRange {
12410 context: Point::new(9, 0)..Point::new(10, 4),
12411 primary: None,
12412 },
12413 ],
12414 cx,
12415 );
12416 multibuffer.push_excerpts(
12417 buffer_3.clone(),
12418 [
12419 ExcerptRange {
12420 context: Point::new(0, 0)..Point::new(3, 0),
12421 primary: None,
12422 },
12423 ExcerptRange {
12424 context: Point::new(5, 0)..Point::new(7, 0),
12425 primary: None,
12426 },
12427 ExcerptRange {
12428 context: Point::new(9, 0)..Point::new(10, 4),
12429 primary: None,
12430 },
12431 ],
12432 cx,
12433 );
12434 multibuffer
12435 });
12436
12437 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12438 editor.update_in(cx, |editor, _window, cx| {
12439 for (buffer, diff_base) in [
12440 (buffer_1.clone(), base_text_1),
12441 (buffer_2.clone(), base_text_2),
12442 (buffer_3.clone(), base_text_3),
12443 ] {
12444 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
12445 editor
12446 .buffer
12447 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
12448 }
12449 });
12450 cx.executor().run_until_parked();
12451
12452 editor.update_in(cx, |editor, window, cx| {
12453 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}");
12454 editor.select_all(&SelectAll, window, cx);
12455 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
12456 });
12457 cx.executor().run_until_parked();
12458
12459 // When all ranges are selected, all buffer hunks are reverted.
12460 editor.update(cx, |editor, cx| {
12461 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");
12462 });
12463 buffer_1.update(cx, |buffer, _| {
12464 assert_eq!(buffer.text(), base_text_1);
12465 });
12466 buffer_2.update(cx, |buffer, _| {
12467 assert_eq!(buffer.text(), base_text_2);
12468 });
12469 buffer_3.update(cx, |buffer, _| {
12470 assert_eq!(buffer.text(), base_text_3);
12471 });
12472
12473 editor.update_in(cx, |editor, window, cx| {
12474 editor.undo(&Default::default(), window, cx);
12475 });
12476
12477 editor.update_in(cx, |editor, window, cx| {
12478 editor.change_selections(None, window, cx, |s| {
12479 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
12480 });
12481 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
12482 });
12483
12484 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
12485 // but not affect buffer_2 and its related excerpts.
12486 editor.update(cx, |editor, cx| {
12487 assert_eq!(
12488 editor.text(cx),
12489 "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}"
12490 );
12491 });
12492 buffer_1.update(cx, |buffer, _| {
12493 assert_eq!(buffer.text(), base_text_1);
12494 });
12495 buffer_2.update(cx, |buffer, _| {
12496 assert_eq!(
12497 buffer.text(),
12498 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
12499 );
12500 });
12501 buffer_3.update(cx, |buffer, _| {
12502 assert_eq!(
12503 buffer.text(),
12504 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
12505 );
12506 });
12507
12508 fn edit_first_char_of_every_line(text: &str) -> String {
12509 text.split('\n')
12510 .map(|line| format!("X{}", &line[1..]))
12511 .collect::<Vec<_>>()
12512 .join("\n")
12513 }
12514}
12515
12516#[gpui::test]
12517async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
12518 init_test(cx, |_| {});
12519
12520 let cols = 4;
12521 let rows = 10;
12522 let sample_text_1 = sample_text(rows, cols, 'a');
12523 assert_eq!(
12524 sample_text_1,
12525 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
12526 );
12527 let sample_text_2 = sample_text(rows, cols, 'l');
12528 assert_eq!(
12529 sample_text_2,
12530 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
12531 );
12532 let sample_text_3 = sample_text(rows, cols, 'v');
12533 assert_eq!(
12534 sample_text_3,
12535 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
12536 );
12537
12538 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
12539 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
12540 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
12541
12542 let multi_buffer = cx.new(|cx| {
12543 let mut multibuffer = MultiBuffer::new(ReadWrite);
12544 multibuffer.push_excerpts(
12545 buffer_1.clone(),
12546 [
12547 ExcerptRange {
12548 context: Point::new(0, 0)..Point::new(3, 0),
12549 primary: None,
12550 },
12551 ExcerptRange {
12552 context: Point::new(5, 0)..Point::new(7, 0),
12553 primary: None,
12554 },
12555 ExcerptRange {
12556 context: Point::new(9, 0)..Point::new(10, 4),
12557 primary: None,
12558 },
12559 ],
12560 cx,
12561 );
12562 multibuffer.push_excerpts(
12563 buffer_2.clone(),
12564 [
12565 ExcerptRange {
12566 context: Point::new(0, 0)..Point::new(3, 0),
12567 primary: None,
12568 },
12569 ExcerptRange {
12570 context: Point::new(5, 0)..Point::new(7, 0),
12571 primary: None,
12572 },
12573 ExcerptRange {
12574 context: Point::new(9, 0)..Point::new(10, 4),
12575 primary: None,
12576 },
12577 ],
12578 cx,
12579 );
12580 multibuffer.push_excerpts(
12581 buffer_3.clone(),
12582 [
12583 ExcerptRange {
12584 context: Point::new(0, 0)..Point::new(3, 0),
12585 primary: None,
12586 },
12587 ExcerptRange {
12588 context: Point::new(5, 0)..Point::new(7, 0),
12589 primary: None,
12590 },
12591 ExcerptRange {
12592 context: Point::new(9, 0)..Point::new(10, 4),
12593 primary: None,
12594 },
12595 ],
12596 cx,
12597 );
12598 multibuffer
12599 });
12600
12601 let fs = FakeFs::new(cx.executor());
12602 fs.insert_tree(
12603 "/a",
12604 json!({
12605 "main.rs": sample_text_1,
12606 "other.rs": sample_text_2,
12607 "lib.rs": sample_text_3,
12608 }),
12609 )
12610 .await;
12611 let project = Project::test(fs, ["/a".as_ref()], cx).await;
12612 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12613 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12614 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
12615 Editor::new(
12616 EditorMode::Full,
12617 multi_buffer,
12618 Some(project.clone()),
12619 true,
12620 window,
12621 cx,
12622 )
12623 });
12624 let multibuffer_item_id = workspace
12625 .update(cx, |workspace, window, cx| {
12626 assert!(
12627 workspace.active_item(cx).is_none(),
12628 "active item should be None before the first item is added"
12629 );
12630 workspace.add_item_to_active_pane(
12631 Box::new(multi_buffer_editor.clone()),
12632 None,
12633 true,
12634 window,
12635 cx,
12636 );
12637 let active_item = workspace
12638 .active_item(cx)
12639 .expect("should have an active item after adding the multi buffer");
12640 assert!(
12641 !active_item.is_singleton(cx),
12642 "A multi buffer was expected to active after adding"
12643 );
12644 active_item.item_id()
12645 })
12646 .unwrap();
12647 cx.executor().run_until_parked();
12648
12649 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12650 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12651 s.select_ranges(Some(1..2))
12652 });
12653 editor.open_excerpts(&OpenExcerpts, window, cx);
12654 });
12655 cx.executor().run_until_parked();
12656 let first_item_id = workspace
12657 .update(cx, |workspace, window, cx| {
12658 let active_item = workspace
12659 .active_item(cx)
12660 .expect("should have an active item after navigating into the 1st buffer");
12661 let first_item_id = active_item.item_id();
12662 assert_ne!(
12663 first_item_id, multibuffer_item_id,
12664 "Should navigate into the 1st buffer and activate it"
12665 );
12666 assert!(
12667 active_item.is_singleton(cx),
12668 "New active item should be a singleton buffer"
12669 );
12670 assert_eq!(
12671 active_item
12672 .act_as::<Editor>(cx)
12673 .expect("should have navigated into an editor for the 1st buffer")
12674 .read(cx)
12675 .text(cx),
12676 sample_text_1
12677 );
12678
12679 workspace
12680 .go_back(workspace.active_pane().downgrade(), window, cx)
12681 .detach_and_log_err(cx);
12682
12683 first_item_id
12684 })
12685 .unwrap();
12686 cx.executor().run_until_parked();
12687 workspace
12688 .update(cx, |workspace, _, cx| {
12689 let active_item = workspace
12690 .active_item(cx)
12691 .expect("should have an active item after navigating back");
12692 assert_eq!(
12693 active_item.item_id(),
12694 multibuffer_item_id,
12695 "Should navigate back to the multi buffer"
12696 );
12697 assert!(!active_item.is_singleton(cx));
12698 })
12699 .unwrap();
12700
12701 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12702 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12703 s.select_ranges(Some(39..40))
12704 });
12705 editor.open_excerpts(&OpenExcerpts, window, cx);
12706 });
12707 cx.executor().run_until_parked();
12708 let second_item_id = workspace
12709 .update(cx, |workspace, window, cx| {
12710 let active_item = workspace
12711 .active_item(cx)
12712 .expect("should have an active item after navigating into the 2nd buffer");
12713 let second_item_id = active_item.item_id();
12714 assert_ne!(
12715 second_item_id, multibuffer_item_id,
12716 "Should navigate away from the multibuffer"
12717 );
12718 assert_ne!(
12719 second_item_id, first_item_id,
12720 "Should navigate into the 2nd buffer and activate it"
12721 );
12722 assert!(
12723 active_item.is_singleton(cx),
12724 "New active item should be a singleton buffer"
12725 );
12726 assert_eq!(
12727 active_item
12728 .act_as::<Editor>(cx)
12729 .expect("should have navigated into an editor")
12730 .read(cx)
12731 .text(cx),
12732 sample_text_2
12733 );
12734
12735 workspace
12736 .go_back(workspace.active_pane().downgrade(), window, cx)
12737 .detach_and_log_err(cx);
12738
12739 second_item_id
12740 })
12741 .unwrap();
12742 cx.executor().run_until_parked();
12743 workspace
12744 .update(cx, |workspace, _, cx| {
12745 let active_item = workspace
12746 .active_item(cx)
12747 .expect("should have an active item after navigating back from the 2nd buffer");
12748 assert_eq!(
12749 active_item.item_id(),
12750 multibuffer_item_id,
12751 "Should navigate back from the 2nd buffer to the multi buffer"
12752 );
12753 assert!(!active_item.is_singleton(cx));
12754 })
12755 .unwrap();
12756
12757 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12758 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12759 s.select_ranges(Some(70..70))
12760 });
12761 editor.open_excerpts(&OpenExcerpts, window, cx);
12762 });
12763 cx.executor().run_until_parked();
12764 workspace
12765 .update(cx, |workspace, window, cx| {
12766 let active_item = workspace
12767 .active_item(cx)
12768 .expect("should have an active item after navigating into the 3rd buffer");
12769 let third_item_id = active_item.item_id();
12770 assert_ne!(
12771 third_item_id, multibuffer_item_id,
12772 "Should navigate into the 3rd buffer and activate it"
12773 );
12774 assert_ne!(third_item_id, first_item_id);
12775 assert_ne!(third_item_id, second_item_id);
12776 assert!(
12777 active_item.is_singleton(cx),
12778 "New active item should be a singleton buffer"
12779 );
12780 assert_eq!(
12781 active_item
12782 .act_as::<Editor>(cx)
12783 .expect("should have navigated into an editor")
12784 .read(cx)
12785 .text(cx),
12786 sample_text_3
12787 );
12788
12789 workspace
12790 .go_back(workspace.active_pane().downgrade(), window, cx)
12791 .detach_and_log_err(cx);
12792 })
12793 .unwrap();
12794 cx.executor().run_until_parked();
12795 workspace
12796 .update(cx, |workspace, _, cx| {
12797 let active_item = workspace
12798 .active_item(cx)
12799 .expect("should have an active item after navigating back from the 3rd buffer");
12800 assert_eq!(
12801 active_item.item_id(),
12802 multibuffer_item_id,
12803 "Should navigate back from the 3rd buffer to the multi buffer"
12804 );
12805 assert!(!active_item.is_singleton(cx));
12806 })
12807 .unwrap();
12808}
12809
12810#[gpui::test]
12811async fn test_toggle_selected_diff_hunks(
12812 executor: BackgroundExecutor,
12813 cx: &mut gpui::TestAppContext,
12814) {
12815 init_test(cx, |_| {});
12816
12817 let mut cx = EditorTestContext::new(cx).await;
12818
12819 let diff_base = r#"
12820 use some::mod;
12821
12822 const A: u32 = 42;
12823
12824 fn main() {
12825 println!("hello");
12826
12827 println!("world");
12828 }
12829 "#
12830 .unindent();
12831
12832 cx.set_state(
12833 &r#"
12834 use some::modified;
12835
12836 ˇ
12837 fn main() {
12838 println!("hello there");
12839
12840 println!("around the");
12841 println!("world");
12842 }
12843 "#
12844 .unindent(),
12845 );
12846
12847 cx.set_diff_base(&diff_base);
12848 executor.run_until_parked();
12849
12850 cx.update_editor(|editor, window, cx| {
12851 editor.go_to_next_hunk(&GoToHunk, window, cx);
12852 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
12853 });
12854 executor.run_until_parked();
12855 cx.assert_state_with_diff(
12856 r#"
12857 use some::modified;
12858
12859
12860 fn main() {
12861 - println!("hello");
12862 + ˇ println!("hello there");
12863
12864 println!("around the");
12865 println!("world");
12866 }
12867 "#
12868 .unindent(),
12869 );
12870
12871 cx.update_editor(|editor, window, cx| {
12872 for _ in 0..2 {
12873 editor.go_to_next_hunk(&GoToHunk, window, cx);
12874 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
12875 }
12876 });
12877 executor.run_until_parked();
12878 cx.assert_state_with_diff(
12879 r#"
12880 - use some::mod;
12881 + ˇuse some::modified;
12882
12883
12884 fn main() {
12885 - println!("hello");
12886 + println!("hello there");
12887
12888 + println!("around the");
12889 println!("world");
12890 }
12891 "#
12892 .unindent(),
12893 );
12894
12895 cx.update_editor(|editor, window, cx| {
12896 editor.go_to_next_hunk(&GoToHunk, window, cx);
12897 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
12898 });
12899 executor.run_until_parked();
12900 cx.assert_state_with_diff(
12901 r#"
12902 - use some::mod;
12903 + use some::modified;
12904
12905 - const A: u32 = 42;
12906 ˇ
12907 fn main() {
12908 - println!("hello");
12909 + println!("hello there");
12910
12911 + println!("around the");
12912 println!("world");
12913 }
12914 "#
12915 .unindent(),
12916 );
12917
12918 cx.update_editor(|editor, window, cx| {
12919 editor.cancel(&Cancel, window, cx);
12920 });
12921
12922 cx.assert_state_with_diff(
12923 r#"
12924 use some::modified;
12925
12926 ˇ
12927 fn main() {
12928 println!("hello there");
12929
12930 println!("around the");
12931 println!("world");
12932 }
12933 "#
12934 .unindent(),
12935 );
12936}
12937
12938#[gpui::test]
12939async fn test_diff_base_change_with_expanded_diff_hunks(
12940 executor: BackgroundExecutor,
12941 cx: &mut gpui::TestAppContext,
12942) {
12943 init_test(cx, |_| {});
12944
12945 let mut cx = EditorTestContext::new(cx).await;
12946
12947 let diff_base = r#"
12948 use some::mod1;
12949 use some::mod2;
12950
12951 const A: u32 = 42;
12952 const B: u32 = 42;
12953 const C: u32 = 42;
12954
12955 fn main() {
12956 println!("hello");
12957
12958 println!("world");
12959 }
12960 "#
12961 .unindent();
12962
12963 cx.set_state(
12964 &r#"
12965 use some::mod2;
12966
12967 const A: u32 = 42;
12968 const C: u32 = 42;
12969
12970 fn main(ˇ) {
12971 //println!("hello");
12972
12973 println!("world");
12974 //
12975 //
12976 }
12977 "#
12978 .unindent(),
12979 );
12980
12981 cx.set_diff_base(&diff_base);
12982 executor.run_until_parked();
12983
12984 cx.update_editor(|editor, window, cx| {
12985 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
12986 });
12987 executor.run_until_parked();
12988 cx.assert_state_with_diff(
12989 r#"
12990 - use some::mod1;
12991 use some::mod2;
12992
12993 const A: u32 = 42;
12994 - const B: u32 = 42;
12995 const C: u32 = 42;
12996
12997 fn main(ˇ) {
12998 - println!("hello");
12999 + //println!("hello");
13000
13001 println!("world");
13002 + //
13003 + //
13004 }
13005 "#
13006 .unindent(),
13007 );
13008
13009 cx.set_diff_base("new diff base!");
13010 executor.run_until_parked();
13011 cx.assert_state_with_diff(
13012 r#"
13013 use some::mod2;
13014
13015 const A: u32 = 42;
13016 const C: u32 = 42;
13017
13018 fn main(ˇ) {
13019 //println!("hello");
13020
13021 println!("world");
13022 //
13023 //
13024 }
13025 "#
13026 .unindent(),
13027 );
13028
13029 cx.update_editor(|editor, window, cx| {
13030 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13031 });
13032 executor.run_until_parked();
13033 cx.assert_state_with_diff(
13034 r#"
13035 - new diff base!
13036 + use some::mod2;
13037 +
13038 + const A: u32 = 42;
13039 + const C: u32 = 42;
13040 +
13041 + fn main(ˇ) {
13042 + //println!("hello");
13043 +
13044 + println!("world");
13045 + //
13046 + //
13047 + }
13048 "#
13049 .unindent(),
13050 );
13051}
13052
13053#[gpui::test]
13054async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) {
13055 init_test(cx, |_| {});
13056
13057 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
13058 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
13059 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
13060 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
13061 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
13062 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
13063
13064 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
13065 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
13066 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
13067
13068 let multi_buffer = cx.new(|cx| {
13069 let mut multibuffer = MultiBuffer::new(ReadWrite);
13070 multibuffer.push_excerpts(
13071 buffer_1.clone(),
13072 [
13073 ExcerptRange {
13074 context: Point::new(0, 0)..Point::new(3, 0),
13075 primary: None,
13076 },
13077 ExcerptRange {
13078 context: Point::new(5, 0)..Point::new(7, 0),
13079 primary: None,
13080 },
13081 ExcerptRange {
13082 context: Point::new(9, 0)..Point::new(10, 3),
13083 primary: None,
13084 },
13085 ],
13086 cx,
13087 );
13088 multibuffer.push_excerpts(
13089 buffer_2.clone(),
13090 [
13091 ExcerptRange {
13092 context: Point::new(0, 0)..Point::new(3, 0),
13093 primary: None,
13094 },
13095 ExcerptRange {
13096 context: Point::new(5, 0)..Point::new(7, 0),
13097 primary: None,
13098 },
13099 ExcerptRange {
13100 context: Point::new(9, 0)..Point::new(10, 3),
13101 primary: None,
13102 },
13103 ],
13104 cx,
13105 );
13106 multibuffer.push_excerpts(
13107 buffer_3.clone(),
13108 [
13109 ExcerptRange {
13110 context: Point::new(0, 0)..Point::new(3, 0),
13111 primary: None,
13112 },
13113 ExcerptRange {
13114 context: Point::new(5, 0)..Point::new(7, 0),
13115 primary: None,
13116 },
13117 ExcerptRange {
13118 context: Point::new(9, 0)..Point::new(10, 3),
13119 primary: None,
13120 },
13121 ],
13122 cx,
13123 );
13124 multibuffer
13125 });
13126
13127 let editor = cx.add_window(|window, cx| {
13128 Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
13129 });
13130 editor
13131 .update(cx, |editor, _window, cx| {
13132 for (buffer, diff_base) in [
13133 (buffer_1.clone(), file_1_old),
13134 (buffer_2.clone(), file_2_old),
13135 (buffer_3.clone(), file_3_old),
13136 ] {
13137 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
13138 editor
13139 .buffer
13140 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
13141 }
13142 })
13143 .unwrap();
13144
13145 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13146 cx.run_until_parked();
13147
13148 cx.assert_editor_state(
13149 &"
13150 ˇaaa
13151 ccc
13152 ddd
13153
13154 ggg
13155 hhh
13156
13157
13158 lll
13159 mmm
13160 NNN
13161
13162 qqq
13163 rrr
13164
13165 uuu
13166 111
13167 222
13168 333
13169
13170 666
13171 777
13172
13173 000
13174 !!!"
13175 .unindent(),
13176 );
13177
13178 cx.update_editor(|editor, window, cx| {
13179 editor.select_all(&SelectAll, window, cx);
13180 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13181 });
13182 cx.executor().run_until_parked();
13183
13184 cx.assert_state_with_diff(
13185 "
13186 «aaa
13187 - bbb
13188 ccc
13189 ddd
13190
13191 ggg
13192 hhh
13193
13194
13195 lll
13196 mmm
13197 - nnn
13198 + NNN
13199
13200 qqq
13201 rrr
13202
13203 uuu
13204 111
13205 222
13206 333
13207
13208 + 666
13209 777
13210
13211 000
13212 !!!ˇ»"
13213 .unindent(),
13214 );
13215}
13216
13217#[gpui::test]
13218async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext) {
13219 init_test(cx, |_| {});
13220
13221 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
13222 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
13223
13224 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
13225 let multi_buffer = cx.new(|cx| {
13226 let mut multibuffer = MultiBuffer::new(ReadWrite);
13227 multibuffer.push_excerpts(
13228 buffer.clone(),
13229 [
13230 ExcerptRange {
13231 context: Point::new(0, 0)..Point::new(2, 0),
13232 primary: None,
13233 },
13234 ExcerptRange {
13235 context: Point::new(4, 0)..Point::new(7, 0),
13236 primary: None,
13237 },
13238 ExcerptRange {
13239 context: Point::new(9, 0)..Point::new(10, 0),
13240 primary: None,
13241 },
13242 ],
13243 cx,
13244 );
13245 multibuffer
13246 });
13247
13248 let editor = cx.add_window(|window, cx| {
13249 Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
13250 });
13251 editor
13252 .update(cx, |editor, _window, cx| {
13253 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
13254 editor
13255 .buffer
13256 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
13257 })
13258 .unwrap();
13259
13260 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13261 cx.run_until_parked();
13262
13263 cx.update_editor(|editor, window, cx| {
13264 editor.expand_all_diff_hunks(&Default::default(), window, cx)
13265 });
13266 cx.executor().run_until_parked();
13267
13268 // When the start of a hunk coincides with the start of its excerpt,
13269 // the hunk is expanded. When the start of a a hunk is earlier than
13270 // the start of its excerpt, the hunk is not expanded.
13271 cx.assert_state_with_diff(
13272 "
13273 ˇaaa
13274 - bbb
13275 + BBB
13276
13277 - ddd
13278 - eee
13279 + DDD
13280 + EEE
13281 fff
13282
13283 iii
13284 "
13285 .unindent(),
13286 );
13287}
13288
13289#[gpui::test]
13290async fn test_edits_around_expanded_insertion_hunks(
13291 executor: BackgroundExecutor,
13292 cx: &mut gpui::TestAppContext,
13293) {
13294 init_test(cx, |_| {});
13295
13296 let mut cx = EditorTestContext::new(cx).await;
13297
13298 let diff_base = r#"
13299 use some::mod1;
13300 use some::mod2;
13301
13302 const A: u32 = 42;
13303
13304 fn main() {
13305 println!("hello");
13306
13307 println!("world");
13308 }
13309 "#
13310 .unindent();
13311 executor.run_until_parked();
13312 cx.set_state(
13313 &r#"
13314 use some::mod1;
13315 use some::mod2;
13316
13317 const A: u32 = 42;
13318 const B: u32 = 42;
13319 const C: u32 = 42;
13320 ˇ
13321
13322 fn main() {
13323 println!("hello");
13324
13325 println!("world");
13326 }
13327 "#
13328 .unindent(),
13329 );
13330
13331 cx.set_diff_base(&diff_base);
13332 executor.run_until_parked();
13333
13334 cx.update_editor(|editor, window, cx| {
13335 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13336 });
13337 executor.run_until_parked();
13338
13339 cx.assert_state_with_diff(
13340 r#"
13341 use some::mod1;
13342 use some::mod2;
13343
13344 const A: u32 = 42;
13345 + const B: u32 = 42;
13346 + const C: u32 = 42;
13347 + ˇ
13348
13349 fn main() {
13350 println!("hello");
13351
13352 println!("world");
13353 }
13354 "#
13355 .unindent(),
13356 );
13357
13358 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
13359 executor.run_until_parked();
13360
13361 cx.assert_state_with_diff(
13362 r#"
13363 use some::mod1;
13364 use some::mod2;
13365
13366 const A: u32 = 42;
13367 + const B: u32 = 42;
13368 + const C: u32 = 42;
13369 + const D: u32 = 42;
13370 + ˇ
13371
13372 fn main() {
13373 println!("hello");
13374
13375 println!("world");
13376 }
13377 "#
13378 .unindent(),
13379 );
13380
13381 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
13382 executor.run_until_parked();
13383
13384 cx.assert_state_with_diff(
13385 r#"
13386 use some::mod1;
13387 use some::mod2;
13388
13389 const A: u32 = 42;
13390 + const B: u32 = 42;
13391 + const C: u32 = 42;
13392 + const D: u32 = 42;
13393 + const E: u32 = 42;
13394 + ˇ
13395
13396 fn main() {
13397 println!("hello");
13398
13399 println!("world");
13400 }
13401 "#
13402 .unindent(),
13403 );
13404
13405 cx.update_editor(|editor, window, cx| {
13406 editor.delete_line(&DeleteLine, window, cx);
13407 });
13408 executor.run_until_parked();
13409
13410 cx.assert_state_with_diff(
13411 r#"
13412 use some::mod1;
13413 use some::mod2;
13414
13415 const A: u32 = 42;
13416 + const B: u32 = 42;
13417 + const C: u32 = 42;
13418 + const D: u32 = 42;
13419 + const E: u32 = 42;
13420 ˇ
13421 fn main() {
13422 println!("hello");
13423
13424 println!("world");
13425 }
13426 "#
13427 .unindent(),
13428 );
13429
13430 cx.update_editor(|editor, window, cx| {
13431 editor.move_up(&MoveUp, window, cx);
13432 editor.delete_line(&DeleteLine, window, cx);
13433 editor.move_up(&MoveUp, window, cx);
13434 editor.delete_line(&DeleteLine, window, cx);
13435 editor.move_up(&MoveUp, window, cx);
13436 editor.delete_line(&DeleteLine, window, cx);
13437 });
13438 executor.run_until_parked();
13439 cx.assert_state_with_diff(
13440 r#"
13441 use some::mod1;
13442 use some::mod2;
13443
13444 const A: u32 = 42;
13445 + const B: u32 = 42;
13446 ˇ
13447 fn main() {
13448 println!("hello");
13449
13450 println!("world");
13451 }
13452 "#
13453 .unindent(),
13454 );
13455
13456 cx.update_editor(|editor, window, cx| {
13457 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
13458 editor.delete_line(&DeleteLine, window, cx);
13459 });
13460 executor.run_until_parked();
13461 cx.assert_state_with_diff(
13462 r#"
13463 ˇ
13464 fn main() {
13465 println!("hello");
13466
13467 println!("world");
13468 }
13469 "#
13470 .unindent(),
13471 );
13472}
13473
13474#[gpui::test]
13475async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
13476 init_test(cx, |_| {});
13477
13478 let mut cx = EditorTestContext::new(cx).await;
13479 cx.set_diff_base(indoc! { "
13480 one
13481 two
13482 three
13483 four
13484 five
13485 "
13486 });
13487 cx.set_state(indoc! { "
13488 one
13489 ˇthree
13490 five
13491 "});
13492 cx.run_until_parked();
13493 cx.update_editor(|editor, window, cx| {
13494 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13495 });
13496 cx.assert_state_with_diff(
13497 indoc! { "
13498 one
13499 - two
13500 ˇthree
13501 - four
13502 five
13503 "}
13504 .to_string(),
13505 );
13506 cx.update_editor(|editor, window, cx| {
13507 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13508 });
13509
13510 cx.assert_state_with_diff(
13511 indoc! { "
13512 one
13513 ˇthree
13514 five
13515 "}
13516 .to_string(),
13517 );
13518
13519 cx.set_state(indoc! { "
13520 one
13521 ˇTWO
13522 three
13523 four
13524 five
13525 "});
13526 cx.run_until_parked();
13527 cx.update_editor(|editor, window, cx| {
13528 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13529 });
13530
13531 cx.assert_state_with_diff(
13532 indoc! { "
13533 one
13534 - two
13535 + ˇTWO
13536 three
13537 four
13538 five
13539 "}
13540 .to_string(),
13541 );
13542 cx.update_editor(|editor, window, cx| {
13543 editor.move_up(&Default::default(), window, cx);
13544 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13545 });
13546 cx.assert_state_with_diff(
13547 indoc! { "
13548 one
13549 ˇTWO
13550 three
13551 four
13552 five
13553 "}
13554 .to_string(),
13555 );
13556}
13557
13558#[gpui::test]
13559async fn test_edits_around_expanded_deletion_hunks(
13560 executor: BackgroundExecutor,
13561 cx: &mut gpui::TestAppContext,
13562) {
13563 init_test(cx, |_| {});
13564
13565 let mut cx = EditorTestContext::new(cx).await;
13566
13567 let diff_base = r#"
13568 use some::mod1;
13569 use some::mod2;
13570
13571 const A: u32 = 42;
13572 const B: u32 = 42;
13573 const C: u32 = 42;
13574
13575
13576 fn main() {
13577 println!("hello");
13578
13579 println!("world");
13580 }
13581 "#
13582 .unindent();
13583 executor.run_until_parked();
13584 cx.set_state(
13585 &r#"
13586 use some::mod1;
13587 use some::mod2;
13588
13589 ˇconst B: u32 = 42;
13590 const C: u32 = 42;
13591
13592
13593 fn main() {
13594 println!("hello");
13595
13596 println!("world");
13597 }
13598 "#
13599 .unindent(),
13600 );
13601
13602 cx.set_diff_base(&diff_base);
13603 executor.run_until_parked();
13604
13605 cx.update_editor(|editor, window, cx| {
13606 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13607 });
13608 executor.run_until_parked();
13609
13610 cx.assert_state_with_diff(
13611 r#"
13612 use some::mod1;
13613 use some::mod2;
13614
13615 - const A: u32 = 42;
13616 ˇconst B: u32 = 42;
13617 const C: u32 = 42;
13618
13619
13620 fn main() {
13621 println!("hello");
13622
13623 println!("world");
13624 }
13625 "#
13626 .unindent(),
13627 );
13628
13629 cx.update_editor(|editor, window, cx| {
13630 editor.delete_line(&DeleteLine, window, cx);
13631 });
13632 executor.run_until_parked();
13633 cx.assert_state_with_diff(
13634 r#"
13635 use some::mod1;
13636 use some::mod2;
13637
13638 - const A: u32 = 42;
13639 - const B: u32 = 42;
13640 ˇconst C: u32 = 42;
13641
13642
13643 fn main() {
13644 println!("hello");
13645
13646 println!("world");
13647 }
13648 "#
13649 .unindent(),
13650 );
13651
13652 cx.update_editor(|editor, window, cx| {
13653 editor.delete_line(&DeleteLine, window, cx);
13654 });
13655 executor.run_until_parked();
13656 cx.assert_state_with_diff(
13657 r#"
13658 use some::mod1;
13659 use some::mod2;
13660
13661 - const A: u32 = 42;
13662 - const B: u32 = 42;
13663 - const C: u32 = 42;
13664 ˇ
13665
13666 fn main() {
13667 println!("hello");
13668
13669 println!("world");
13670 }
13671 "#
13672 .unindent(),
13673 );
13674
13675 cx.update_editor(|editor, window, cx| {
13676 editor.handle_input("replacement", window, cx);
13677 });
13678 executor.run_until_parked();
13679 cx.assert_state_with_diff(
13680 r#"
13681 use some::mod1;
13682 use some::mod2;
13683
13684 - const A: u32 = 42;
13685 - const B: u32 = 42;
13686 - const C: u32 = 42;
13687 -
13688 + replacementˇ
13689
13690 fn main() {
13691 println!("hello");
13692
13693 println!("world");
13694 }
13695 "#
13696 .unindent(),
13697 );
13698}
13699
13700#[gpui::test]
13701async fn test_backspace_after_deletion_hunk(
13702 executor: BackgroundExecutor,
13703 cx: &mut gpui::TestAppContext,
13704) {
13705 init_test(cx, |_| {});
13706
13707 let mut cx = EditorTestContext::new(cx).await;
13708
13709 let base_text = r#"
13710 one
13711 two
13712 three
13713 four
13714 five
13715 "#
13716 .unindent();
13717 executor.run_until_parked();
13718 cx.set_state(
13719 &r#"
13720 one
13721 two
13722 fˇour
13723 five
13724 "#
13725 .unindent(),
13726 );
13727
13728 cx.set_diff_base(&base_text);
13729 executor.run_until_parked();
13730
13731 cx.update_editor(|editor, window, cx| {
13732 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13733 });
13734 executor.run_until_parked();
13735
13736 cx.assert_state_with_diff(
13737 r#"
13738 one
13739 two
13740 - three
13741 fˇour
13742 five
13743 "#
13744 .unindent(),
13745 );
13746
13747 cx.update_editor(|editor, window, cx| {
13748 editor.backspace(&Backspace, window, cx);
13749 editor.backspace(&Backspace, window, cx);
13750 });
13751 executor.run_until_parked();
13752 cx.assert_state_with_diff(
13753 r#"
13754 one
13755 two
13756 - threeˇ
13757 - four
13758 + our
13759 five
13760 "#
13761 .unindent(),
13762 );
13763}
13764
13765#[gpui::test]
13766async fn test_edit_after_expanded_modification_hunk(
13767 executor: BackgroundExecutor,
13768 cx: &mut gpui::TestAppContext,
13769) {
13770 init_test(cx, |_| {});
13771
13772 let mut cx = EditorTestContext::new(cx).await;
13773
13774 let diff_base = r#"
13775 use some::mod1;
13776 use some::mod2;
13777
13778 const A: u32 = 42;
13779 const B: u32 = 42;
13780 const C: u32 = 42;
13781 const D: u32 = 42;
13782
13783
13784 fn main() {
13785 println!("hello");
13786
13787 println!("world");
13788 }"#
13789 .unindent();
13790
13791 cx.set_state(
13792 &r#"
13793 use some::mod1;
13794 use some::mod2;
13795
13796 const A: u32 = 42;
13797 const B: u32 = 42;
13798 const C: u32 = 43ˇ
13799 const D: u32 = 42;
13800
13801
13802 fn main() {
13803 println!("hello");
13804
13805 println!("world");
13806 }"#
13807 .unindent(),
13808 );
13809
13810 cx.set_diff_base(&diff_base);
13811 executor.run_until_parked();
13812 cx.update_editor(|editor, window, cx| {
13813 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13814 });
13815 executor.run_until_parked();
13816
13817 cx.assert_state_with_diff(
13818 r#"
13819 use some::mod1;
13820 use some::mod2;
13821
13822 const A: u32 = 42;
13823 const B: u32 = 42;
13824 - const C: u32 = 42;
13825 + const C: u32 = 43ˇ
13826 const D: u32 = 42;
13827
13828
13829 fn main() {
13830 println!("hello");
13831
13832 println!("world");
13833 }"#
13834 .unindent(),
13835 );
13836
13837 cx.update_editor(|editor, window, cx| {
13838 editor.handle_input("\nnew_line\n", window, cx);
13839 });
13840 executor.run_until_parked();
13841
13842 cx.assert_state_with_diff(
13843 r#"
13844 use some::mod1;
13845 use some::mod2;
13846
13847 const A: u32 = 42;
13848 const B: u32 = 42;
13849 - const C: u32 = 42;
13850 + const C: u32 = 43
13851 + new_line
13852 + ˇ
13853 const D: u32 = 42;
13854
13855
13856 fn main() {
13857 println!("hello");
13858
13859 println!("world");
13860 }"#
13861 .unindent(),
13862 );
13863}
13864
13865async fn setup_indent_guides_editor(
13866 text: &str,
13867 cx: &mut gpui::TestAppContext,
13868) -> (BufferId, EditorTestContext) {
13869 init_test(cx, |_| {});
13870
13871 let mut cx = EditorTestContext::new(cx).await;
13872
13873 let buffer_id = cx.update_editor(|editor, window, cx| {
13874 editor.set_text(text, window, cx);
13875 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
13876
13877 buffer_ids[0]
13878 });
13879
13880 (buffer_id, cx)
13881}
13882
13883fn assert_indent_guides(
13884 range: Range<u32>,
13885 expected: Vec<IndentGuide>,
13886 active_indices: Option<Vec<usize>>,
13887 cx: &mut EditorTestContext,
13888) {
13889 let indent_guides = cx.update_editor(|editor, window, cx| {
13890 let snapshot = editor.snapshot(window, cx).display_snapshot;
13891 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
13892 editor,
13893 MultiBufferRow(range.start)..MultiBufferRow(range.end),
13894 true,
13895 &snapshot,
13896 cx,
13897 );
13898
13899 indent_guides.sort_by(|a, b| {
13900 a.depth.cmp(&b.depth).then(
13901 a.start_row
13902 .cmp(&b.start_row)
13903 .then(a.end_row.cmp(&b.end_row)),
13904 )
13905 });
13906 indent_guides
13907 });
13908
13909 if let Some(expected) = active_indices {
13910 let active_indices = cx.update_editor(|editor, window, cx| {
13911 let snapshot = editor.snapshot(window, cx).display_snapshot;
13912 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
13913 });
13914
13915 assert_eq!(
13916 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
13917 expected,
13918 "Active indent guide indices do not match"
13919 );
13920 }
13921
13922 assert_eq!(indent_guides, expected, "Indent guides do not match");
13923}
13924
13925fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
13926 IndentGuide {
13927 buffer_id,
13928 start_row: MultiBufferRow(start_row),
13929 end_row: MultiBufferRow(end_row),
13930 depth,
13931 tab_size: 4,
13932 settings: IndentGuideSettings {
13933 enabled: true,
13934 line_width: 1,
13935 active_line_width: 1,
13936 ..Default::default()
13937 },
13938 }
13939}
13940
13941#[gpui::test]
13942async fn test_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
13943 let (buffer_id, mut cx) = setup_indent_guides_editor(
13944 &"
13945 fn main() {
13946 let a = 1;
13947 }"
13948 .unindent(),
13949 cx,
13950 )
13951 .await;
13952
13953 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
13954}
13955
13956#[gpui::test]
13957async fn test_indent_guide_simple_block(cx: &mut gpui::TestAppContext) {
13958 let (buffer_id, mut cx) = setup_indent_guides_editor(
13959 &"
13960 fn main() {
13961 let a = 1;
13962 let b = 2;
13963 }"
13964 .unindent(),
13965 cx,
13966 )
13967 .await;
13968
13969 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
13970}
13971
13972#[gpui::test]
13973async fn test_indent_guide_nested(cx: &mut gpui::TestAppContext) {
13974 let (buffer_id, mut cx) = setup_indent_guides_editor(
13975 &"
13976 fn main() {
13977 let a = 1;
13978 if a == 3 {
13979 let b = 2;
13980 } else {
13981 let c = 3;
13982 }
13983 }"
13984 .unindent(),
13985 cx,
13986 )
13987 .await;
13988
13989 assert_indent_guides(
13990 0..8,
13991 vec![
13992 indent_guide(buffer_id, 1, 6, 0),
13993 indent_guide(buffer_id, 3, 3, 1),
13994 indent_guide(buffer_id, 5, 5, 1),
13995 ],
13996 None,
13997 &mut cx,
13998 );
13999}
14000
14001#[gpui::test]
14002async fn test_indent_guide_tab(cx: &mut gpui::TestAppContext) {
14003 let (buffer_id, mut cx) = setup_indent_guides_editor(
14004 &"
14005 fn main() {
14006 let a = 1;
14007 let b = 2;
14008 let c = 3;
14009 }"
14010 .unindent(),
14011 cx,
14012 )
14013 .await;
14014
14015 assert_indent_guides(
14016 0..5,
14017 vec![
14018 indent_guide(buffer_id, 1, 3, 0),
14019 indent_guide(buffer_id, 2, 2, 1),
14020 ],
14021 None,
14022 &mut cx,
14023 );
14024}
14025
14026#[gpui::test]
14027async fn test_indent_guide_continues_on_empty_line(cx: &mut gpui::TestAppContext) {
14028 let (buffer_id, mut cx) = setup_indent_guides_editor(
14029 &"
14030 fn main() {
14031 let a = 1;
14032
14033 let c = 3;
14034 }"
14035 .unindent(),
14036 cx,
14037 )
14038 .await;
14039
14040 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
14041}
14042
14043#[gpui::test]
14044async fn test_indent_guide_complex(cx: &mut gpui::TestAppContext) {
14045 let (buffer_id, mut cx) = setup_indent_guides_editor(
14046 &"
14047 fn main() {
14048 let a = 1;
14049
14050 let c = 3;
14051
14052 if a == 3 {
14053 let b = 2;
14054 } else {
14055 let c = 3;
14056 }
14057 }"
14058 .unindent(),
14059 cx,
14060 )
14061 .await;
14062
14063 assert_indent_guides(
14064 0..11,
14065 vec![
14066 indent_guide(buffer_id, 1, 9, 0),
14067 indent_guide(buffer_id, 6, 6, 1),
14068 indent_guide(buffer_id, 8, 8, 1),
14069 ],
14070 None,
14071 &mut cx,
14072 );
14073}
14074
14075#[gpui::test]
14076async fn test_indent_guide_starts_off_screen(cx: &mut gpui::TestAppContext) {
14077 let (buffer_id, mut cx) = setup_indent_guides_editor(
14078 &"
14079 fn main() {
14080 let a = 1;
14081
14082 let c = 3;
14083
14084 if a == 3 {
14085 let b = 2;
14086 } else {
14087 let c = 3;
14088 }
14089 }"
14090 .unindent(),
14091 cx,
14092 )
14093 .await;
14094
14095 assert_indent_guides(
14096 1..11,
14097 vec![
14098 indent_guide(buffer_id, 1, 9, 0),
14099 indent_guide(buffer_id, 6, 6, 1),
14100 indent_guide(buffer_id, 8, 8, 1),
14101 ],
14102 None,
14103 &mut cx,
14104 );
14105}
14106
14107#[gpui::test]
14108async fn test_indent_guide_ends_off_screen(cx: &mut gpui::TestAppContext) {
14109 let (buffer_id, mut cx) = setup_indent_guides_editor(
14110 &"
14111 fn main() {
14112 let a = 1;
14113
14114 let c = 3;
14115
14116 if a == 3 {
14117 let b = 2;
14118 } else {
14119 let c = 3;
14120 }
14121 }"
14122 .unindent(),
14123 cx,
14124 )
14125 .await;
14126
14127 assert_indent_guides(
14128 1..10,
14129 vec![
14130 indent_guide(buffer_id, 1, 9, 0),
14131 indent_guide(buffer_id, 6, 6, 1),
14132 indent_guide(buffer_id, 8, 8, 1),
14133 ],
14134 None,
14135 &mut cx,
14136 );
14137}
14138
14139#[gpui::test]
14140async fn test_indent_guide_without_brackets(cx: &mut gpui::TestAppContext) {
14141 let (buffer_id, mut cx) = setup_indent_guides_editor(
14142 &"
14143 block1
14144 block2
14145 block3
14146 block4
14147 block2
14148 block1
14149 block1"
14150 .unindent(),
14151 cx,
14152 )
14153 .await;
14154
14155 assert_indent_guides(
14156 1..10,
14157 vec![
14158 indent_guide(buffer_id, 1, 4, 0),
14159 indent_guide(buffer_id, 2, 3, 1),
14160 indent_guide(buffer_id, 3, 3, 2),
14161 ],
14162 None,
14163 &mut cx,
14164 );
14165}
14166
14167#[gpui::test]
14168async fn test_indent_guide_ends_before_empty_line(cx: &mut gpui::TestAppContext) {
14169 let (buffer_id, mut cx) = setup_indent_guides_editor(
14170 &"
14171 block1
14172 block2
14173 block3
14174
14175 block1
14176 block1"
14177 .unindent(),
14178 cx,
14179 )
14180 .await;
14181
14182 assert_indent_guides(
14183 0..6,
14184 vec![
14185 indent_guide(buffer_id, 1, 2, 0),
14186 indent_guide(buffer_id, 2, 2, 1),
14187 ],
14188 None,
14189 &mut cx,
14190 );
14191}
14192
14193#[gpui::test]
14194async fn test_indent_guide_continuing_off_screen(cx: &mut gpui::TestAppContext) {
14195 let (buffer_id, mut cx) = setup_indent_guides_editor(
14196 &"
14197 block1
14198
14199
14200
14201 block2
14202 "
14203 .unindent(),
14204 cx,
14205 )
14206 .await;
14207
14208 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
14209}
14210
14211#[gpui::test]
14212async fn test_indent_guide_tabs(cx: &mut gpui::TestAppContext) {
14213 let (buffer_id, mut cx) = setup_indent_guides_editor(
14214 &"
14215 def a:
14216 \tb = 3
14217 \tif True:
14218 \t\tc = 4
14219 \t\td = 5
14220 \tprint(b)
14221 "
14222 .unindent(),
14223 cx,
14224 )
14225 .await;
14226
14227 assert_indent_guides(
14228 0..6,
14229 vec![
14230 indent_guide(buffer_id, 1, 6, 0),
14231 indent_guide(buffer_id, 3, 4, 1),
14232 ],
14233 None,
14234 &mut cx,
14235 );
14236}
14237
14238#[gpui::test]
14239async fn test_active_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
14240 let (buffer_id, mut cx) = setup_indent_guides_editor(
14241 &"
14242 fn main() {
14243 let a = 1;
14244 }"
14245 .unindent(),
14246 cx,
14247 )
14248 .await;
14249
14250 cx.update_editor(|editor, window, cx| {
14251 editor.change_selections(None, window, cx, |s| {
14252 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14253 });
14254 });
14255
14256 assert_indent_guides(
14257 0..3,
14258 vec![indent_guide(buffer_id, 1, 1, 0)],
14259 Some(vec![0]),
14260 &mut cx,
14261 );
14262}
14263
14264#[gpui::test]
14265async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppContext) {
14266 let (buffer_id, mut cx) = setup_indent_guides_editor(
14267 &"
14268 fn main() {
14269 if 1 == 2 {
14270 let a = 1;
14271 }
14272 }"
14273 .unindent(),
14274 cx,
14275 )
14276 .await;
14277
14278 cx.update_editor(|editor, window, cx| {
14279 editor.change_selections(None, window, cx, |s| {
14280 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14281 });
14282 });
14283
14284 assert_indent_guides(
14285 0..4,
14286 vec![
14287 indent_guide(buffer_id, 1, 3, 0),
14288 indent_guide(buffer_id, 2, 2, 1),
14289 ],
14290 Some(vec![1]),
14291 &mut cx,
14292 );
14293
14294 cx.update_editor(|editor, window, cx| {
14295 editor.change_selections(None, window, cx, |s| {
14296 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
14297 });
14298 });
14299
14300 assert_indent_guides(
14301 0..4,
14302 vec![
14303 indent_guide(buffer_id, 1, 3, 0),
14304 indent_guide(buffer_id, 2, 2, 1),
14305 ],
14306 Some(vec![1]),
14307 &mut cx,
14308 );
14309
14310 cx.update_editor(|editor, window, cx| {
14311 editor.change_selections(None, window, cx, |s| {
14312 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
14313 });
14314 });
14315
14316 assert_indent_guides(
14317 0..4,
14318 vec![
14319 indent_guide(buffer_id, 1, 3, 0),
14320 indent_guide(buffer_id, 2, 2, 1),
14321 ],
14322 Some(vec![0]),
14323 &mut cx,
14324 );
14325}
14326
14327#[gpui::test]
14328async fn test_active_indent_guide_empty_line(cx: &mut gpui::TestAppContext) {
14329 let (buffer_id, mut cx) = setup_indent_guides_editor(
14330 &"
14331 fn main() {
14332 let a = 1;
14333
14334 let b = 2;
14335 }"
14336 .unindent(),
14337 cx,
14338 )
14339 .await;
14340
14341 cx.update_editor(|editor, window, cx| {
14342 editor.change_selections(None, window, cx, |s| {
14343 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
14344 });
14345 });
14346
14347 assert_indent_guides(
14348 0..5,
14349 vec![indent_guide(buffer_id, 1, 3, 0)],
14350 Some(vec![0]),
14351 &mut cx,
14352 );
14353}
14354
14355#[gpui::test]
14356async fn test_active_indent_guide_non_matching_indent(cx: &mut gpui::TestAppContext) {
14357 let (buffer_id, mut cx) = setup_indent_guides_editor(
14358 &"
14359 def m:
14360 a = 1
14361 pass"
14362 .unindent(),
14363 cx,
14364 )
14365 .await;
14366
14367 cx.update_editor(|editor, window, cx| {
14368 editor.change_selections(None, window, cx, |s| {
14369 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14370 });
14371 });
14372
14373 assert_indent_guides(
14374 0..3,
14375 vec![indent_guide(buffer_id, 1, 2, 0)],
14376 Some(vec![0]),
14377 &mut cx,
14378 );
14379}
14380
14381#[gpui::test]
14382async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut gpui::TestAppContext) {
14383 init_test(cx, |_| {});
14384 let mut cx = EditorTestContext::new(cx).await;
14385 let text = indoc! {
14386 "
14387 impl A {
14388 fn b() {
14389 0;
14390 3;
14391 5;
14392 6;
14393 7;
14394 }
14395 }
14396 "
14397 };
14398 let base_text = indoc! {
14399 "
14400 impl A {
14401 fn b() {
14402 0;
14403 1;
14404 2;
14405 3;
14406 4;
14407 }
14408 fn c() {
14409 5;
14410 6;
14411 7;
14412 }
14413 }
14414 "
14415 };
14416
14417 cx.update_editor(|editor, window, cx| {
14418 editor.set_text(text, window, cx);
14419
14420 editor.buffer().update(cx, |multibuffer, cx| {
14421 let buffer = multibuffer.as_singleton().unwrap();
14422 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
14423
14424 multibuffer.set_all_diff_hunks_expanded(cx);
14425 multibuffer.add_diff(diff, cx);
14426
14427 buffer.read(cx).remote_id()
14428 })
14429 });
14430 cx.run_until_parked();
14431
14432 cx.assert_state_with_diff(
14433 indoc! { "
14434 impl A {
14435 fn b() {
14436 0;
14437 - 1;
14438 - 2;
14439 3;
14440 - 4;
14441 - }
14442 - fn c() {
14443 5;
14444 6;
14445 7;
14446 }
14447 }
14448 ˇ"
14449 }
14450 .to_string(),
14451 );
14452
14453 let mut actual_guides = cx.update_editor(|editor, window, cx| {
14454 editor
14455 .snapshot(window, cx)
14456 .buffer_snapshot
14457 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
14458 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
14459 .collect::<Vec<_>>()
14460 });
14461 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
14462 assert_eq!(
14463 actual_guides,
14464 vec![
14465 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
14466 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
14467 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
14468 ]
14469 );
14470}
14471
14472#[gpui::test]
14473fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
14474 init_test(cx, |_| {});
14475
14476 let editor = cx.add_window(|window, cx| {
14477 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
14478 build_editor(buffer, window, cx)
14479 });
14480
14481 let render_args = Arc::new(Mutex::new(None));
14482 let snapshot = editor
14483 .update(cx, |editor, window, cx| {
14484 let snapshot = editor.buffer().read(cx).snapshot(cx);
14485 let range =
14486 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
14487
14488 struct RenderArgs {
14489 row: MultiBufferRow,
14490 folded: bool,
14491 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
14492 }
14493
14494 let crease = Crease::inline(
14495 range,
14496 FoldPlaceholder::test(),
14497 {
14498 let toggle_callback = render_args.clone();
14499 move |row, folded, callback, _window, _cx| {
14500 *toggle_callback.lock() = Some(RenderArgs {
14501 row,
14502 folded,
14503 callback,
14504 });
14505 div()
14506 }
14507 },
14508 |_row, _folded, _window, _cx| div(),
14509 );
14510
14511 editor.insert_creases(Some(crease), cx);
14512 let snapshot = editor.snapshot(window, cx);
14513 let _div = snapshot.render_crease_toggle(
14514 MultiBufferRow(1),
14515 false,
14516 cx.entity().clone(),
14517 window,
14518 cx,
14519 );
14520 snapshot
14521 })
14522 .unwrap();
14523
14524 let render_args = render_args.lock().take().unwrap();
14525 assert_eq!(render_args.row, MultiBufferRow(1));
14526 assert!(!render_args.folded);
14527 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
14528
14529 cx.update_window(*editor, |_, window, cx| {
14530 (render_args.callback)(true, window, cx)
14531 })
14532 .unwrap();
14533 let snapshot = editor
14534 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
14535 .unwrap();
14536 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
14537
14538 cx.update_window(*editor, |_, window, cx| {
14539 (render_args.callback)(false, window, cx)
14540 })
14541 .unwrap();
14542 let snapshot = editor
14543 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
14544 .unwrap();
14545 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
14546}
14547
14548#[gpui::test]
14549async fn test_input_text(cx: &mut gpui::TestAppContext) {
14550 init_test(cx, |_| {});
14551 let mut cx = EditorTestContext::new(cx).await;
14552
14553 cx.set_state(
14554 &r#"ˇone
14555 two
14556
14557 three
14558 fourˇ
14559 five
14560
14561 siˇx"#
14562 .unindent(),
14563 );
14564
14565 cx.dispatch_action(HandleInput(String::new()));
14566 cx.assert_editor_state(
14567 &r#"ˇone
14568 two
14569
14570 three
14571 fourˇ
14572 five
14573
14574 siˇx"#
14575 .unindent(),
14576 );
14577
14578 cx.dispatch_action(HandleInput("AAAA".to_string()));
14579 cx.assert_editor_state(
14580 &r#"AAAAˇone
14581 two
14582
14583 three
14584 fourAAAAˇ
14585 five
14586
14587 siAAAAˇx"#
14588 .unindent(),
14589 );
14590}
14591
14592#[gpui::test]
14593async fn test_scroll_cursor_center_top_bottom(cx: &mut gpui::TestAppContext) {
14594 init_test(cx, |_| {});
14595
14596 let mut cx = EditorTestContext::new(cx).await;
14597 cx.set_state(
14598 r#"let foo = 1;
14599let foo = 2;
14600let foo = 3;
14601let fooˇ = 4;
14602let foo = 5;
14603let foo = 6;
14604let foo = 7;
14605let foo = 8;
14606let foo = 9;
14607let foo = 10;
14608let foo = 11;
14609let foo = 12;
14610let foo = 13;
14611let foo = 14;
14612let foo = 15;"#,
14613 );
14614
14615 cx.update_editor(|e, window, cx| {
14616 assert_eq!(
14617 e.next_scroll_position,
14618 NextScrollCursorCenterTopBottom::Center,
14619 "Default next scroll direction is center",
14620 );
14621
14622 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14623 assert_eq!(
14624 e.next_scroll_position,
14625 NextScrollCursorCenterTopBottom::Top,
14626 "After center, next scroll direction should be top",
14627 );
14628
14629 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14630 assert_eq!(
14631 e.next_scroll_position,
14632 NextScrollCursorCenterTopBottom::Bottom,
14633 "After top, next scroll direction should be bottom",
14634 );
14635
14636 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14637 assert_eq!(
14638 e.next_scroll_position,
14639 NextScrollCursorCenterTopBottom::Center,
14640 "After bottom, scrolling should start over",
14641 );
14642
14643 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14644 assert_eq!(
14645 e.next_scroll_position,
14646 NextScrollCursorCenterTopBottom::Top,
14647 "Scrolling continues if retriggered fast enough"
14648 );
14649 });
14650
14651 cx.executor()
14652 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
14653 cx.executor().run_until_parked();
14654 cx.update_editor(|e, _, _| {
14655 assert_eq!(
14656 e.next_scroll_position,
14657 NextScrollCursorCenterTopBottom::Center,
14658 "If scrolling is not triggered fast enough, it should reset"
14659 );
14660 });
14661}
14662
14663#[gpui::test]
14664async fn test_goto_definition_with_find_all_references_fallback(cx: &mut gpui::TestAppContext) {
14665 init_test(cx, |_| {});
14666 let mut cx = EditorLspTestContext::new_rust(
14667 lsp::ServerCapabilities {
14668 definition_provider: Some(lsp::OneOf::Left(true)),
14669 references_provider: Some(lsp::OneOf::Left(true)),
14670 ..lsp::ServerCapabilities::default()
14671 },
14672 cx,
14673 )
14674 .await;
14675
14676 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
14677 let go_to_definition = cx.lsp.handle_request::<lsp::request::GotoDefinition, _, _>(
14678 move |params, _| async move {
14679 if empty_go_to_definition {
14680 Ok(None)
14681 } else {
14682 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
14683 uri: params.text_document_position_params.text_document.uri,
14684 range: lsp::Range::new(lsp::Position::new(4, 3), lsp::Position::new(4, 6)),
14685 })))
14686 }
14687 },
14688 );
14689 let references =
14690 cx.lsp
14691 .handle_request::<lsp::request::References, _, _>(move |params, _| async move {
14692 Ok(Some(vec![lsp::Location {
14693 uri: params.text_document_position.text_document.uri,
14694 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
14695 }]))
14696 });
14697 (go_to_definition, references)
14698 };
14699
14700 cx.set_state(
14701 &r#"fn one() {
14702 let mut a = ˇtwo();
14703 }
14704
14705 fn two() {}"#
14706 .unindent(),
14707 );
14708 set_up_lsp_handlers(false, &mut cx);
14709 let navigated = cx
14710 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
14711 .await
14712 .expect("Failed to navigate to definition");
14713 assert_eq!(
14714 navigated,
14715 Navigated::Yes,
14716 "Should have navigated to definition from the GetDefinition response"
14717 );
14718 cx.assert_editor_state(
14719 &r#"fn one() {
14720 let mut a = two();
14721 }
14722
14723 fn «twoˇ»() {}"#
14724 .unindent(),
14725 );
14726
14727 let editors = cx.update_workspace(|workspace, _, cx| {
14728 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
14729 });
14730 cx.update_editor(|_, _, test_editor_cx| {
14731 assert_eq!(
14732 editors.len(),
14733 1,
14734 "Initially, only one, test, editor should be open in the workspace"
14735 );
14736 assert_eq!(
14737 test_editor_cx.entity(),
14738 editors.last().expect("Asserted len is 1").clone()
14739 );
14740 });
14741
14742 set_up_lsp_handlers(true, &mut cx);
14743 let navigated = cx
14744 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
14745 .await
14746 .expect("Failed to navigate to lookup references");
14747 assert_eq!(
14748 navigated,
14749 Navigated::Yes,
14750 "Should have navigated to references as a fallback after empty GoToDefinition response"
14751 );
14752 // We should not change the selections in the existing file,
14753 // if opening another milti buffer with the references
14754 cx.assert_editor_state(
14755 &r#"fn one() {
14756 let mut a = two();
14757 }
14758
14759 fn «twoˇ»() {}"#
14760 .unindent(),
14761 );
14762 let editors = cx.update_workspace(|workspace, _, cx| {
14763 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
14764 });
14765 cx.update_editor(|_, _, test_editor_cx| {
14766 assert_eq!(
14767 editors.len(),
14768 2,
14769 "After falling back to references search, we open a new editor with the results"
14770 );
14771 let references_fallback_text = editors
14772 .into_iter()
14773 .find(|new_editor| *new_editor != test_editor_cx.entity())
14774 .expect("Should have one non-test editor now")
14775 .read(test_editor_cx)
14776 .text(test_editor_cx);
14777 assert_eq!(
14778 references_fallback_text, "fn one() {\n let mut a = two();\n}",
14779 "Should use the range from the references response and not the GoToDefinition one"
14780 );
14781 });
14782}
14783
14784#[gpui::test]
14785async fn test_find_enclosing_node_with_task(cx: &mut gpui::TestAppContext) {
14786 init_test(cx, |_| {});
14787
14788 let language = Arc::new(Language::new(
14789 LanguageConfig::default(),
14790 Some(tree_sitter_rust::LANGUAGE.into()),
14791 ));
14792
14793 let text = r#"
14794 #[cfg(test)]
14795 mod tests() {
14796 #[test]
14797 fn runnable_1() {
14798 let a = 1;
14799 }
14800
14801 #[test]
14802 fn runnable_2() {
14803 let a = 1;
14804 let b = 2;
14805 }
14806 }
14807 "#
14808 .unindent();
14809
14810 let fs = FakeFs::new(cx.executor());
14811 fs.insert_file("/file.rs", Default::default()).await;
14812
14813 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14814 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14815 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14816 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14817 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
14818
14819 let editor = cx.new_window_entity(|window, cx| {
14820 Editor::new(
14821 EditorMode::Full,
14822 multi_buffer,
14823 Some(project.clone()),
14824 true,
14825 window,
14826 cx,
14827 )
14828 });
14829
14830 editor.update_in(cx, |editor, window, cx| {
14831 editor.tasks.insert(
14832 (buffer.read(cx).remote_id(), 3),
14833 RunnableTasks {
14834 templates: vec![],
14835 offset: MultiBufferOffset(43),
14836 column: 0,
14837 extra_variables: HashMap::default(),
14838 context_range: BufferOffset(43)..BufferOffset(85),
14839 },
14840 );
14841 editor.tasks.insert(
14842 (buffer.read(cx).remote_id(), 8),
14843 RunnableTasks {
14844 templates: vec![],
14845 offset: MultiBufferOffset(86),
14846 column: 0,
14847 extra_variables: HashMap::default(),
14848 context_range: BufferOffset(86)..BufferOffset(191),
14849 },
14850 );
14851
14852 // Test finding task when cursor is inside function body
14853 editor.change_selections(None, window, cx, |s| {
14854 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
14855 });
14856 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
14857 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
14858
14859 // Test finding task when cursor is on function name
14860 editor.change_selections(None, window, cx, |s| {
14861 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
14862 });
14863 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
14864 assert_eq!(row, 8, "Should find task when cursor is on function name");
14865 });
14866}
14867
14868#[gpui::test]
14869async fn test_multi_buffer_folding(cx: &mut gpui::TestAppContext) {
14870 init_test(cx, |_| {});
14871
14872 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
14873 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
14874 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
14875
14876 let fs = FakeFs::new(cx.executor());
14877 fs.insert_tree(
14878 path!("/a"),
14879 json!({
14880 "first.rs": sample_text_1,
14881 "second.rs": sample_text_2,
14882 "third.rs": sample_text_3,
14883 }),
14884 )
14885 .await;
14886 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14887 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14888 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14889 let worktree = project.update(cx, |project, cx| {
14890 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
14891 assert_eq!(worktrees.len(), 1);
14892 worktrees.pop().unwrap()
14893 });
14894 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
14895
14896 let buffer_1 = project
14897 .update(cx, |project, cx| {
14898 project.open_buffer((worktree_id, "first.rs"), cx)
14899 })
14900 .await
14901 .unwrap();
14902 let buffer_2 = project
14903 .update(cx, |project, cx| {
14904 project.open_buffer((worktree_id, "second.rs"), cx)
14905 })
14906 .await
14907 .unwrap();
14908 let buffer_3 = project
14909 .update(cx, |project, cx| {
14910 project.open_buffer((worktree_id, "third.rs"), cx)
14911 })
14912 .await
14913 .unwrap();
14914
14915 let multi_buffer = cx.new(|cx| {
14916 let mut multi_buffer = MultiBuffer::new(ReadWrite);
14917 multi_buffer.push_excerpts(
14918 buffer_1.clone(),
14919 [
14920 ExcerptRange {
14921 context: Point::new(0, 0)..Point::new(3, 0),
14922 primary: None,
14923 },
14924 ExcerptRange {
14925 context: Point::new(5, 0)..Point::new(7, 0),
14926 primary: None,
14927 },
14928 ExcerptRange {
14929 context: Point::new(9, 0)..Point::new(10, 4),
14930 primary: None,
14931 },
14932 ],
14933 cx,
14934 );
14935 multi_buffer.push_excerpts(
14936 buffer_2.clone(),
14937 [
14938 ExcerptRange {
14939 context: Point::new(0, 0)..Point::new(3, 0),
14940 primary: None,
14941 },
14942 ExcerptRange {
14943 context: Point::new(5, 0)..Point::new(7, 0),
14944 primary: None,
14945 },
14946 ExcerptRange {
14947 context: Point::new(9, 0)..Point::new(10, 4),
14948 primary: None,
14949 },
14950 ],
14951 cx,
14952 );
14953 multi_buffer.push_excerpts(
14954 buffer_3.clone(),
14955 [
14956 ExcerptRange {
14957 context: Point::new(0, 0)..Point::new(3, 0),
14958 primary: None,
14959 },
14960 ExcerptRange {
14961 context: Point::new(5, 0)..Point::new(7, 0),
14962 primary: None,
14963 },
14964 ExcerptRange {
14965 context: Point::new(9, 0)..Point::new(10, 4),
14966 primary: None,
14967 },
14968 ],
14969 cx,
14970 );
14971 multi_buffer
14972 });
14973 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
14974 Editor::new(
14975 EditorMode::Full,
14976 multi_buffer,
14977 Some(project.clone()),
14978 true,
14979 window,
14980 cx,
14981 )
14982 });
14983
14984 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";
14985 assert_eq!(
14986 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
14987 full_text,
14988 );
14989
14990 multi_buffer_editor.update(cx, |editor, cx| {
14991 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
14992 });
14993 assert_eq!(
14994 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
14995 "\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",
14996 "After folding the first buffer, its text should not be displayed"
14997 );
14998
14999 multi_buffer_editor.update(cx, |editor, cx| {
15000 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
15001 });
15002 assert_eq!(
15003 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15004 "\n\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
15005 "After folding the second buffer, its text should not be displayed"
15006 );
15007
15008 multi_buffer_editor.update(cx, |editor, cx| {
15009 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
15010 });
15011 assert_eq!(
15012 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15013 "\n\n\n\n\n",
15014 "After folding the third buffer, its text should not be displayed"
15015 );
15016
15017 // Emulate selection inside the fold logic, that should work
15018 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15019 editor
15020 .snapshot(window, cx)
15021 .next_line_boundary(Point::new(0, 4));
15022 });
15023
15024 multi_buffer_editor.update(cx, |editor, cx| {
15025 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
15026 });
15027 assert_eq!(
15028 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15029 "\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
15030 "After unfolding the second buffer, its text should be displayed"
15031 );
15032
15033 multi_buffer_editor.update(cx, |editor, cx| {
15034 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
15035 });
15036 assert_eq!(
15037 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15038 "\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",
15039 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
15040 );
15041
15042 multi_buffer_editor.update(cx, |editor, cx| {
15043 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
15044 });
15045 assert_eq!(
15046 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15047 full_text,
15048 "After unfolding the all buffers, all original text should be displayed"
15049 );
15050}
15051
15052#[gpui::test]
15053async fn test_multi_buffer_single_excerpts_folding(cx: &mut gpui::TestAppContext) {
15054 init_test(cx, |_| {});
15055
15056 let sample_text_1 = "1111\n2222\n3333".to_string();
15057 let sample_text_2 = "4444\n5555\n6666".to_string();
15058 let sample_text_3 = "7777\n8888\n9999".to_string();
15059
15060 let fs = FakeFs::new(cx.executor());
15061 fs.insert_tree(
15062 path!("/a"),
15063 json!({
15064 "first.rs": sample_text_1,
15065 "second.rs": sample_text_2,
15066 "third.rs": sample_text_3,
15067 }),
15068 )
15069 .await;
15070 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15071 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15072 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15073 let worktree = project.update(cx, |project, cx| {
15074 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15075 assert_eq!(worktrees.len(), 1);
15076 worktrees.pop().unwrap()
15077 });
15078 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15079
15080 let buffer_1 = project
15081 .update(cx, |project, cx| {
15082 project.open_buffer((worktree_id, "first.rs"), cx)
15083 })
15084 .await
15085 .unwrap();
15086 let buffer_2 = project
15087 .update(cx, |project, cx| {
15088 project.open_buffer((worktree_id, "second.rs"), cx)
15089 })
15090 .await
15091 .unwrap();
15092 let buffer_3 = project
15093 .update(cx, |project, cx| {
15094 project.open_buffer((worktree_id, "third.rs"), cx)
15095 })
15096 .await
15097 .unwrap();
15098
15099 let multi_buffer = cx.new(|cx| {
15100 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15101 multi_buffer.push_excerpts(
15102 buffer_1.clone(),
15103 [ExcerptRange {
15104 context: Point::new(0, 0)..Point::new(3, 0),
15105 primary: None,
15106 }],
15107 cx,
15108 );
15109 multi_buffer.push_excerpts(
15110 buffer_2.clone(),
15111 [ExcerptRange {
15112 context: Point::new(0, 0)..Point::new(3, 0),
15113 primary: None,
15114 }],
15115 cx,
15116 );
15117 multi_buffer.push_excerpts(
15118 buffer_3.clone(),
15119 [ExcerptRange {
15120 context: Point::new(0, 0)..Point::new(3, 0),
15121 primary: None,
15122 }],
15123 cx,
15124 );
15125 multi_buffer
15126 });
15127
15128 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15129 Editor::new(
15130 EditorMode::Full,
15131 multi_buffer,
15132 Some(project.clone()),
15133 true,
15134 window,
15135 cx,
15136 )
15137 });
15138
15139 let full_text = "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n";
15140 assert_eq!(
15141 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15142 full_text,
15143 );
15144
15145 multi_buffer_editor.update(cx, |editor, cx| {
15146 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
15147 });
15148 assert_eq!(
15149 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15150 "\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n",
15151 "After folding the first buffer, its text should not be displayed"
15152 );
15153
15154 multi_buffer_editor.update(cx, |editor, cx| {
15155 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
15156 });
15157
15158 assert_eq!(
15159 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15160 "\n\n\n\n\n\n\n7777\n8888\n9999\n",
15161 "After folding the second buffer, its text should not be displayed"
15162 );
15163
15164 multi_buffer_editor.update(cx, |editor, cx| {
15165 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
15166 });
15167 assert_eq!(
15168 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15169 "\n\n\n\n\n",
15170 "After folding the third buffer, its text should not be displayed"
15171 );
15172
15173 multi_buffer_editor.update(cx, |editor, cx| {
15174 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
15175 });
15176 assert_eq!(
15177 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15178 "\n\n\n\n\n4444\n5555\n6666\n\n\n",
15179 "After unfolding the second buffer, its text should be displayed"
15180 );
15181
15182 multi_buffer_editor.update(cx, |editor, cx| {
15183 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
15184 });
15185 assert_eq!(
15186 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15187 "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n",
15188 "After unfolding the first buffer, its text should be displayed"
15189 );
15190
15191 multi_buffer_editor.update(cx, |editor, cx| {
15192 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
15193 });
15194 assert_eq!(
15195 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15196 full_text,
15197 "After unfolding all buffers, all original text should be displayed"
15198 );
15199}
15200
15201#[gpui::test]
15202async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppContext) {
15203 init_test(cx, |_| {});
15204
15205 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
15206
15207 let fs = FakeFs::new(cx.executor());
15208 fs.insert_tree(
15209 path!("/a"),
15210 json!({
15211 "main.rs": sample_text,
15212 }),
15213 )
15214 .await;
15215 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15216 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15217 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15218 let worktree = project.update(cx, |project, cx| {
15219 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15220 assert_eq!(worktrees.len(), 1);
15221 worktrees.pop().unwrap()
15222 });
15223 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15224
15225 let buffer_1 = project
15226 .update(cx, |project, cx| {
15227 project.open_buffer((worktree_id, "main.rs"), cx)
15228 })
15229 .await
15230 .unwrap();
15231
15232 let multi_buffer = cx.new(|cx| {
15233 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15234 multi_buffer.push_excerpts(
15235 buffer_1.clone(),
15236 [ExcerptRange {
15237 context: Point::new(0, 0)
15238 ..Point::new(
15239 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
15240 0,
15241 ),
15242 primary: None,
15243 }],
15244 cx,
15245 );
15246 multi_buffer
15247 });
15248 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15249 Editor::new(
15250 EditorMode::Full,
15251 multi_buffer,
15252 Some(project.clone()),
15253 true,
15254 window,
15255 cx,
15256 )
15257 });
15258
15259 let selection_range = Point::new(1, 0)..Point::new(2, 0);
15260 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15261 enum TestHighlight {}
15262 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
15263 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
15264 editor.highlight_text::<TestHighlight>(
15265 vec![highlight_range.clone()],
15266 HighlightStyle::color(Hsla::green()),
15267 cx,
15268 );
15269 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
15270 });
15271
15272 let full_text = format!("\n\n\n{sample_text}\n");
15273 assert_eq!(
15274 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15275 full_text,
15276 );
15277}
15278
15279#[gpui::test]
15280async fn test_inline_completion_text(cx: &mut TestAppContext) {
15281 init_test(cx, |_| {});
15282
15283 // Simple insertion
15284 assert_highlighted_edits(
15285 "Hello, world!",
15286 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
15287 true,
15288 cx,
15289 |highlighted_edits, cx| {
15290 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
15291 assert_eq!(highlighted_edits.highlights.len(), 1);
15292 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
15293 assert_eq!(
15294 highlighted_edits.highlights[0].1.background_color,
15295 Some(cx.theme().status().created_background)
15296 );
15297 },
15298 )
15299 .await;
15300
15301 // Replacement
15302 assert_highlighted_edits(
15303 "This is a test.",
15304 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
15305 false,
15306 cx,
15307 |highlighted_edits, cx| {
15308 assert_eq!(highlighted_edits.text, "That is a test.");
15309 assert_eq!(highlighted_edits.highlights.len(), 1);
15310 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
15311 assert_eq!(
15312 highlighted_edits.highlights[0].1.background_color,
15313 Some(cx.theme().status().created_background)
15314 );
15315 },
15316 )
15317 .await;
15318
15319 // Multiple edits
15320 assert_highlighted_edits(
15321 "Hello, world!",
15322 vec![
15323 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
15324 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
15325 ],
15326 false,
15327 cx,
15328 |highlighted_edits, cx| {
15329 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
15330 assert_eq!(highlighted_edits.highlights.len(), 2);
15331 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
15332 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
15333 assert_eq!(
15334 highlighted_edits.highlights[0].1.background_color,
15335 Some(cx.theme().status().created_background)
15336 );
15337 assert_eq!(
15338 highlighted_edits.highlights[1].1.background_color,
15339 Some(cx.theme().status().created_background)
15340 );
15341 },
15342 )
15343 .await;
15344
15345 // Multiple lines with edits
15346 assert_highlighted_edits(
15347 "First line\nSecond line\nThird line\nFourth line",
15348 vec![
15349 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
15350 (
15351 Point::new(2, 0)..Point::new(2, 10),
15352 "New third line".to_string(),
15353 ),
15354 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
15355 ],
15356 false,
15357 cx,
15358 |highlighted_edits, cx| {
15359 assert_eq!(
15360 highlighted_edits.text,
15361 "Second modified\nNew third line\nFourth updated line"
15362 );
15363 assert_eq!(highlighted_edits.highlights.len(), 3);
15364 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
15365 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
15366 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
15367 for highlight in &highlighted_edits.highlights {
15368 assert_eq!(
15369 highlight.1.background_color,
15370 Some(cx.theme().status().created_background)
15371 );
15372 }
15373 },
15374 )
15375 .await;
15376}
15377
15378#[gpui::test]
15379async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
15380 init_test(cx, |_| {});
15381
15382 // Deletion
15383 assert_highlighted_edits(
15384 "Hello, world!",
15385 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
15386 true,
15387 cx,
15388 |highlighted_edits, cx| {
15389 assert_eq!(highlighted_edits.text, "Hello, world!");
15390 assert_eq!(highlighted_edits.highlights.len(), 1);
15391 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
15392 assert_eq!(
15393 highlighted_edits.highlights[0].1.background_color,
15394 Some(cx.theme().status().deleted_background)
15395 );
15396 },
15397 )
15398 .await;
15399
15400 // Insertion
15401 assert_highlighted_edits(
15402 "Hello, world!",
15403 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
15404 true,
15405 cx,
15406 |highlighted_edits, cx| {
15407 assert_eq!(highlighted_edits.highlights.len(), 1);
15408 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
15409 assert_eq!(
15410 highlighted_edits.highlights[0].1.background_color,
15411 Some(cx.theme().status().created_background)
15412 );
15413 },
15414 )
15415 .await;
15416}
15417
15418async fn assert_highlighted_edits(
15419 text: &str,
15420 edits: Vec<(Range<Point>, String)>,
15421 include_deletions: bool,
15422 cx: &mut TestAppContext,
15423 assertion_fn: impl Fn(HighlightedText, &App),
15424) {
15425 let window = cx.add_window(|window, cx| {
15426 let buffer = MultiBuffer::build_simple(text, cx);
15427 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
15428 });
15429 let cx = &mut VisualTestContext::from_window(*window, cx);
15430
15431 let (buffer, snapshot) = window
15432 .update(cx, |editor, _window, cx| {
15433 (
15434 editor.buffer().clone(),
15435 editor.buffer().read(cx).snapshot(cx),
15436 )
15437 })
15438 .unwrap();
15439
15440 let edits = edits
15441 .into_iter()
15442 .map(|(range, edit)| {
15443 (
15444 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
15445 edit,
15446 )
15447 })
15448 .collect::<Vec<_>>();
15449
15450 let text_anchor_edits = edits
15451 .clone()
15452 .into_iter()
15453 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
15454 .collect::<Vec<_>>();
15455
15456 let edit_preview = window
15457 .update(cx, |_, _window, cx| {
15458 buffer
15459 .read(cx)
15460 .as_singleton()
15461 .unwrap()
15462 .read(cx)
15463 .preview_edits(text_anchor_edits.into(), cx)
15464 })
15465 .unwrap()
15466 .await;
15467
15468 cx.update(|_window, cx| {
15469 let highlighted_edits = inline_completion_edit_text(
15470 &snapshot.as_singleton().unwrap().2,
15471 &edits,
15472 &edit_preview,
15473 include_deletions,
15474 cx,
15475 );
15476 assertion_fn(highlighted_edits, cx)
15477 });
15478}
15479
15480#[gpui::test]
15481async fn test_rename_with_duplicate_edits(cx: &mut gpui::TestAppContext) {
15482 init_test(cx, |_| {});
15483 let capabilities = lsp::ServerCapabilities {
15484 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
15485 prepare_provider: Some(true),
15486 work_done_progress_options: Default::default(),
15487 })),
15488 ..Default::default()
15489 };
15490 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
15491
15492 cx.set_state(indoc! {"
15493 struct Fˇoo {}
15494 "});
15495
15496 cx.update_editor(|editor, _, cx| {
15497 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
15498 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
15499 editor.highlight_background::<DocumentHighlightRead>(
15500 &[highlight_range],
15501 |c| c.editor_document_highlight_read_background,
15502 cx,
15503 );
15504 });
15505
15506 let mut prepare_rename_handler =
15507 cx.handle_request::<lsp::request::PrepareRenameRequest, _, _>(move |_, _, _| async move {
15508 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
15509 start: lsp::Position {
15510 line: 0,
15511 character: 7,
15512 },
15513 end: lsp::Position {
15514 line: 0,
15515 character: 10,
15516 },
15517 })))
15518 });
15519 let prepare_rename_task = cx
15520 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
15521 .expect("Prepare rename was not started");
15522 prepare_rename_handler.next().await.unwrap();
15523 prepare_rename_task.await.expect("Prepare rename failed");
15524
15525 let mut rename_handler =
15526 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
15527 let edit = lsp::TextEdit {
15528 range: lsp::Range {
15529 start: lsp::Position {
15530 line: 0,
15531 character: 7,
15532 },
15533 end: lsp::Position {
15534 line: 0,
15535 character: 10,
15536 },
15537 },
15538 new_text: "FooRenamed".to_string(),
15539 };
15540 Ok(Some(lsp::WorkspaceEdit::new(
15541 // Specify the same edit twice
15542 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
15543 )))
15544 });
15545 let rename_task = cx
15546 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
15547 .expect("Confirm rename was not started");
15548 rename_handler.next().await.unwrap();
15549 rename_task.await.expect("Confirm rename failed");
15550 cx.run_until_parked();
15551
15552 // Despite two edits, only one is actually applied as those are identical
15553 cx.assert_editor_state(indoc! {"
15554 struct FooRenamedˇ {}
15555 "});
15556}
15557
15558#[gpui::test]
15559async fn test_rename_without_prepare(cx: &mut gpui::TestAppContext) {
15560 init_test(cx, |_| {});
15561 // These capabilities indicate that the server does not support prepare rename.
15562 let capabilities = lsp::ServerCapabilities {
15563 rename_provider: Some(lsp::OneOf::Left(true)),
15564 ..Default::default()
15565 };
15566 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
15567
15568 cx.set_state(indoc! {"
15569 struct Fˇoo {}
15570 "});
15571
15572 cx.update_editor(|editor, _window, cx| {
15573 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
15574 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
15575 editor.highlight_background::<DocumentHighlightRead>(
15576 &[highlight_range],
15577 |c| c.editor_document_highlight_read_background,
15578 cx,
15579 );
15580 });
15581
15582 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
15583 .expect("Prepare rename was not started")
15584 .await
15585 .expect("Prepare rename failed");
15586
15587 let mut rename_handler =
15588 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
15589 let edit = lsp::TextEdit {
15590 range: lsp::Range {
15591 start: lsp::Position {
15592 line: 0,
15593 character: 7,
15594 },
15595 end: lsp::Position {
15596 line: 0,
15597 character: 10,
15598 },
15599 },
15600 new_text: "FooRenamed".to_string(),
15601 };
15602 Ok(Some(lsp::WorkspaceEdit::new(
15603 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
15604 )))
15605 });
15606 let rename_task = cx
15607 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
15608 .expect("Confirm rename was not started");
15609 rename_handler.next().await.unwrap();
15610 rename_task.await.expect("Confirm rename failed");
15611 cx.run_until_parked();
15612
15613 // Correct range is renamed, as `surrounding_word` is used to find it.
15614 cx.assert_editor_state(indoc! {"
15615 struct FooRenamedˇ {}
15616 "});
15617}
15618
15619fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
15620 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
15621 point..point
15622}
15623
15624fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
15625 let (text, ranges) = marked_text_ranges(marked_text, true);
15626 assert_eq!(editor.text(cx), text);
15627 assert_eq!(
15628 editor.selections.ranges(cx),
15629 ranges,
15630 "Assert selections are {}",
15631 marked_text
15632 );
15633}
15634
15635pub fn handle_signature_help_request(
15636 cx: &mut EditorLspTestContext,
15637 mocked_response: lsp::SignatureHelp,
15638) -> impl Future<Output = ()> {
15639 let mut request =
15640 cx.handle_request::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
15641 let mocked_response = mocked_response.clone();
15642 async move { Ok(Some(mocked_response)) }
15643 });
15644
15645 async move {
15646 request.next().await;
15647 }
15648}
15649
15650/// Handle completion request passing a marked string specifying where the completion
15651/// should be triggered from using '|' character, what range should be replaced, and what completions
15652/// should be returned using '<' and '>' to delimit the range
15653pub fn handle_completion_request(
15654 cx: &mut EditorLspTestContext,
15655 marked_string: &str,
15656 completions: Vec<&'static str>,
15657 counter: Arc<AtomicUsize>,
15658) -> impl Future<Output = ()> {
15659 let complete_from_marker: TextRangeMarker = '|'.into();
15660 let replace_range_marker: TextRangeMarker = ('<', '>').into();
15661 let (_, mut marked_ranges) = marked_text_ranges_by(
15662 marked_string,
15663 vec![complete_from_marker.clone(), replace_range_marker.clone()],
15664 );
15665
15666 let complete_from_position =
15667 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
15668 let replace_range =
15669 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
15670
15671 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
15672 let completions = completions.clone();
15673 counter.fetch_add(1, atomic::Ordering::Release);
15674 async move {
15675 assert_eq!(params.text_document_position.text_document.uri, url.clone());
15676 assert_eq!(
15677 params.text_document_position.position,
15678 complete_from_position
15679 );
15680 Ok(Some(lsp::CompletionResponse::Array(
15681 completions
15682 .iter()
15683 .map(|completion_text| lsp::CompletionItem {
15684 label: completion_text.to_string(),
15685 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15686 range: replace_range,
15687 new_text: completion_text.to_string(),
15688 })),
15689 ..Default::default()
15690 })
15691 .collect(),
15692 )))
15693 }
15694 });
15695
15696 async move {
15697 request.next().await;
15698 }
15699}
15700
15701fn handle_resolve_completion_request(
15702 cx: &mut EditorLspTestContext,
15703 edits: Option<Vec<(&'static str, &'static str)>>,
15704) -> impl Future<Output = ()> {
15705 let edits = edits.map(|edits| {
15706 edits
15707 .iter()
15708 .map(|(marked_string, new_text)| {
15709 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
15710 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
15711 lsp::TextEdit::new(replace_range, new_text.to_string())
15712 })
15713 .collect::<Vec<_>>()
15714 });
15715
15716 let mut request =
15717 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15718 let edits = edits.clone();
15719 async move {
15720 Ok(lsp::CompletionItem {
15721 additional_text_edits: edits,
15722 ..Default::default()
15723 })
15724 }
15725 });
15726
15727 async move {
15728 request.next().await;
15729 }
15730}
15731
15732pub(crate) fn update_test_language_settings(
15733 cx: &mut TestAppContext,
15734 f: impl Fn(&mut AllLanguageSettingsContent),
15735) {
15736 cx.update(|cx| {
15737 SettingsStore::update_global(cx, |store, cx| {
15738 store.update_user_settings::<AllLanguageSettings>(cx, f);
15739 });
15740 });
15741}
15742
15743pub(crate) fn update_test_project_settings(
15744 cx: &mut TestAppContext,
15745 f: impl Fn(&mut ProjectSettings),
15746) {
15747 cx.update(|cx| {
15748 SettingsStore::update_global(cx, |store, cx| {
15749 store.update_user_settings::<ProjectSettings>(cx, f);
15750 });
15751 });
15752}
15753
15754pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
15755 cx.update(|cx| {
15756 assets::Assets.load_test_fonts(cx);
15757 let store = SettingsStore::test(cx);
15758 cx.set_global(store);
15759 theme::init(theme::LoadThemes::JustBase, cx);
15760 release_channel::init(SemanticVersion::default(), cx);
15761 client::init_settings(cx);
15762 language::init(cx);
15763 Project::init_settings(cx);
15764 workspace::init_settings(cx);
15765 crate::init(cx);
15766 });
15767
15768 update_test_language_settings(cx, f);
15769}
15770
15771#[track_caller]
15772fn assert_hunk_revert(
15773 not_reverted_text_with_selections: &str,
15774 expected_hunk_statuses_before: Vec<DiffHunkStatus>,
15775 expected_reverted_text_with_selections: &str,
15776 base_text: &str,
15777 cx: &mut EditorLspTestContext,
15778) {
15779 cx.set_state(not_reverted_text_with_selections);
15780 cx.set_diff_base(base_text);
15781 cx.executor().run_until_parked();
15782
15783 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
15784 let snapshot = editor.snapshot(window, cx);
15785 let reverted_hunk_statuses = snapshot
15786 .buffer_snapshot
15787 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
15788 .map(|hunk| hunk.status())
15789 .collect::<Vec<_>>();
15790
15791 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
15792 reverted_hunk_statuses
15793 });
15794 cx.executor().run_until_parked();
15795 cx.assert_editor_state(expected_reverted_text_with_selections);
15796 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
15797}