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 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 cycle_through_same_place_diagnostics(
10658 executor: BackgroundExecutor,
10659 cx: &mut gpui::TestAppContext,
10660) {
10661 init_test(cx, |_| {});
10662
10663 let mut cx = EditorTestContext::new(cx).await;
10664 let lsp_store =
10665 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10666
10667 cx.set_state(indoc! {"
10668 ˇfn func(abc def: i32) -> u32 {
10669 }
10670 "});
10671
10672 cx.update(|_, cx| {
10673 lsp_store.update(cx, |lsp_store, cx| {
10674 lsp_store
10675 .update_diagnostics(
10676 LanguageServerId(0),
10677 lsp::PublishDiagnosticsParams {
10678 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
10679 version: None,
10680 diagnostics: vec![
10681 lsp::Diagnostic {
10682 range: lsp::Range::new(
10683 lsp::Position::new(0, 11),
10684 lsp::Position::new(0, 12),
10685 ),
10686 severity: Some(lsp::DiagnosticSeverity::ERROR),
10687 ..Default::default()
10688 },
10689 lsp::Diagnostic {
10690 range: lsp::Range::new(
10691 lsp::Position::new(0, 12),
10692 lsp::Position::new(0, 15),
10693 ),
10694 severity: Some(lsp::DiagnosticSeverity::ERROR),
10695 ..Default::default()
10696 },
10697 lsp::Diagnostic {
10698 range: lsp::Range::new(
10699 lsp::Position::new(0, 12),
10700 lsp::Position::new(0, 15),
10701 ),
10702 severity: Some(lsp::DiagnosticSeverity::ERROR),
10703 ..Default::default()
10704 },
10705 lsp::Diagnostic {
10706 range: lsp::Range::new(
10707 lsp::Position::new(0, 25),
10708 lsp::Position::new(0, 28),
10709 ),
10710 severity: Some(lsp::DiagnosticSeverity::ERROR),
10711 ..Default::default()
10712 },
10713 ],
10714 },
10715 &[],
10716 cx,
10717 )
10718 .unwrap()
10719 });
10720 });
10721 executor.run_until_parked();
10722
10723 //// Backward
10724
10725 // Fourth diagnostic
10726 cx.update_editor(|editor, window, cx| {
10727 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10728 });
10729 cx.assert_editor_state(indoc! {"
10730 fn func(abc def: i32) -> ˇu32 {
10731 }
10732 "});
10733
10734 // Third diagnostic
10735 cx.update_editor(|editor, window, cx| {
10736 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10737 });
10738 cx.assert_editor_state(indoc! {"
10739 fn func(abc ˇdef: i32) -> u32 {
10740 }
10741 "});
10742
10743 // Second diagnostic, same place
10744 cx.update_editor(|editor, window, cx| {
10745 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10746 });
10747 cx.assert_editor_state(indoc! {"
10748 fn func(abc ˇdef: i32) -> u32 {
10749 }
10750 "});
10751
10752 // First diagnostic
10753 cx.update_editor(|editor, window, cx| {
10754 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10755 });
10756 cx.assert_editor_state(indoc! {"
10757 fn func(abcˇ def: i32) -> u32 {
10758 }
10759 "});
10760
10761 // Wrapped over, fourth diagnostic
10762 cx.update_editor(|editor, window, cx| {
10763 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10764 });
10765 cx.assert_editor_state(indoc! {"
10766 fn func(abc def: i32) -> ˇu32 {
10767 }
10768 "});
10769
10770 cx.update_editor(|editor, window, cx| {
10771 editor.move_to_beginning(&MoveToBeginning, window, cx);
10772 });
10773 cx.assert_editor_state(indoc! {"
10774 ˇfn func(abc def: i32) -> u32 {
10775 }
10776 "});
10777
10778 //// Forward
10779
10780 // First diagnostic
10781 cx.update_editor(|editor, window, cx| {
10782 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10783 });
10784 cx.assert_editor_state(indoc! {"
10785 fn func(abcˇ def: i32) -> u32 {
10786 }
10787 "});
10788
10789 // Second diagnostic
10790 cx.update_editor(|editor, window, cx| {
10791 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10792 });
10793 cx.assert_editor_state(indoc! {"
10794 fn func(abc ˇdef: i32) -> u32 {
10795 }
10796 "});
10797
10798 // Third diagnostic, same place
10799 cx.update_editor(|editor, window, cx| {
10800 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10801 });
10802 cx.assert_editor_state(indoc! {"
10803 fn func(abc ˇdef: i32) -> u32 {
10804 }
10805 "});
10806
10807 // Fourth diagnostic
10808 cx.update_editor(|editor, window, cx| {
10809 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10810 });
10811 cx.assert_editor_state(indoc! {"
10812 fn func(abc def: i32) -> ˇu32 {
10813 }
10814 "});
10815
10816 // Wrapped around, first diagnostic
10817 cx.update_editor(|editor, window, cx| {
10818 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10819 });
10820 cx.assert_editor_state(indoc! {"
10821 fn func(abcˇ def: i32) -> u32 {
10822 }
10823 "});
10824}
10825
10826#[gpui::test]
10827async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
10828 init_test(cx, |_| {});
10829
10830 let mut cx = EditorTestContext::new(cx).await;
10831
10832 cx.set_state(indoc! {"
10833 fn func(abˇc def: i32) -> u32 {
10834 }
10835 "});
10836 let lsp_store =
10837 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10838
10839 cx.update(|_, cx| {
10840 lsp_store.update(cx, |lsp_store, cx| {
10841 lsp_store.update_diagnostics(
10842 LanguageServerId(0),
10843 lsp::PublishDiagnosticsParams {
10844 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
10845 version: None,
10846 diagnostics: vec![lsp::Diagnostic {
10847 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
10848 severity: Some(lsp::DiagnosticSeverity::ERROR),
10849 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
10850 ..Default::default()
10851 }],
10852 },
10853 &[],
10854 cx,
10855 )
10856 })
10857 }).unwrap();
10858 cx.run_until_parked();
10859 cx.update_editor(|editor, window, cx| {
10860 hover_popover::hover(editor, &Default::default(), window, cx)
10861 });
10862 cx.run_until_parked();
10863 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
10864}
10865
10866#[gpui::test]
10867async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
10868 init_test(cx, |_| {});
10869
10870 let mut cx = EditorTestContext::new(cx).await;
10871
10872 let diff_base = r#"
10873 use some::mod;
10874
10875 const A: u32 = 42;
10876
10877 fn main() {
10878 println!("hello");
10879
10880 println!("world");
10881 }
10882 "#
10883 .unindent();
10884
10885 // Edits are modified, removed, modified, added
10886 cx.set_state(
10887 &r#"
10888 use some::modified;
10889
10890 ˇ
10891 fn main() {
10892 println!("hello there");
10893
10894 println!("around the");
10895 println!("world");
10896 }
10897 "#
10898 .unindent(),
10899 );
10900
10901 cx.set_diff_base(&diff_base);
10902 executor.run_until_parked();
10903
10904 cx.update_editor(|editor, window, cx| {
10905 //Wrap around the bottom of the buffer
10906 for _ in 0..3 {
10907 editor.go_to_next_hunk(&GoToHunk, window, cx);
10908 }
10909 });
10910
10911 cx.assert_editor_state(
10912 &r#"
10913 ˇuse some::modified;
10914
10915
10916 fn main() {
10917 println!("hello there");
10918
10919 println!("around the");
10920 println!("world");
10921 }
10922 "#
10923 .unindent(),
10924 );
10925
10926 cx.update_editor(|editor, window, cx| {
10927 //Wrap around the top of the buffer
10928 for _ in 0..2 {
10929 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10930 }
10931 });
10932
10933 cx.assert_editor_state(
10934 &r#"
10935 use some::modified;
10936
10937
10938 fn main() {
10939 ˇ println!("hello there");
10940
10941 println!("around the");
10942 println!("world");
10943 }
10944 "#
10945 .unindent(),
10946 );
10947
10948 cx.update_editor(|editor, window, cx| {
10949 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10950 });
10951
10952 cx.assert_editor_state(
10953 &r#"
10954 use some::modified;
10955
10956 ˇ
10957 fn main() {
10958 println!("hello there");
10959
10960 println!("around the");
10961 println!("world");
10962 }
10963 "#
10964 .unindent(),
10965 );
10966
10967 cx.update_editor(|editor, window, cx| {
10968 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10969 });
10970
10971 cx.assert_editor_state(
10972 &r#"
10973 ˇuse some::modified;
10974
10975
10976 fn main() {
10977 println!("hello there");
10978
10979 println!("around the");
10980 println!("world");
10981 }
10982 "#
10983 .unindent(),
10984 );
10985
10986 cx.update_editor(|editor, window, cx| {
10987 for _ in 0..2 {
10988 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10989 }
10990 });
10991
10992 cx.assert_editor_state(
10993 &r#"
10994 use some::modified;
10995
10996
10997 fn main() {
10998 ˇ println!("hello there");
10999
11000 println!("around the");
11001 println!("world");
11002 }
11003 "#
11004 .unindent(),
11005 );
11006
11007 cx.update_editor(|editor, window, cx| {
11008 editor.fold(&Fold, window, cx);
11009 });
11010
11011 cx.update_editor(|editor, window, cx| {
11012 editor.go_to_next_hunk(&GoToHunk, window, cx);
11013 });
11014
11015 cx.assert_editor_state(
11016 &r#"
11017 ˇuse some::modified;
11018
11019
11020 fn main() {
11021 println!("hello there");
11022
11023 println!("around the");
11024 println!("world");
11025 }
11026 "#
11027 .unindent(),
11028 );
11029}
11030
11031#[test]
11032fn test_split_words() {
11033 fn split(text: &str) -> Vec<&str> {
11034 split_words(text).collect()
11035 }
11036
11037 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
11038 assert_eq!(split("hello_world"), &["hello_", "world"]);
11039 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
11040 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
11041 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
11042 assert_eq!(split("helloworld"), &["helloworld"]);
11043
11044 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
11045}
11046
11047#[gpui::test]
11048async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
11049 init_test(cx, |_| {});
11050
11051 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
11052 let mut assert = |before, after| {
11053 let _state_context = cx.set_state(before);
11054 cx.run_until_parked();
11055 cx.update_editor(|editor, window, cx| {
11056 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
11057 });
11058 cx.assert_editor_state(after);
11059 };
11060
11061 // Outside bracket jumps to outside of matching bracket
11062 assert("console.logˇ(var);", "console.log(var)ˇ;");
11063 assert("console.log(var)ˇ;", "console.logˇ(var);");
11064
11065 // Inside bracket jumps to inside of matching bracket
11066 assert("console.log(ˇvar);", "console.log(varˇ);");
11067 assert("console.log(varˇ);", "console.log(ˇvar);");
11068
11069 // When outside a bracket and inside, favor jumping to the inside bracket
11070 assert(
11071 "console.log('foo', [1, 2, 3]ˇ);",
11072 "console.log(ˇ'foo', [1, 2, 3]);",
11073 );
11074 assert(
11075 "console.log(ˇ'foo', [1, 2, 3]);",
11076 "console.log('foo', [1, 2, 3]ˇ);",
11077 );
11078
11079 // Bias forward if two options are equally likely
11080 assert(
11081 "let result = curried_fun()ˇ();",
11082 "let result = curried_fun()()ˇ;",
11083 );
11084
11085 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
11086 assert(
11087 indoc! {"
11088 function test() {
11089 console.log('test')ˇ
11090 }"},
11091 indoc! {"
11092 function test() {
11093 console.logˇ('test')
11094 }"},
11095 );
11096}
11097
11098#[gpui::test]
11099async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
11100 init_test(cx, |_| {});
11101
11102 let fs = FakeFs::new(cx.executor());
11103 fs.insert_tree(
11104 path!("/a"),
11105 json!({
11106 "main.rs": "fn main() { let a = 5; }",
11107 "other.rs": "// Test file",
11108 }),
11109 )
11110 .await;
11111 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11112
11113 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11114 language_registry.add(Arc::new(Language::new(
11115 LanguageConfig {
11116 name: "Rust".into(),
11117 matcher: LanguageMatcher {
11118 path_suffixes: vec!["rs".to_string()],
11119 ..Default::default()
11120 },
11121 brackets: BracketPairConfig {
11122 pairs: vec![BracketPair {
11123 start: "{".to_string(),
11124 end: "}".to_string(),
11125 close: true,
11126 surround: true,
11127 newline: true,
11128 }],
11129 disabled_scopes_by_bracket_ix: Vec::new(),
11130 },
11131 ..Default::default()
11132 },
11133 Some(tree_sitter_rust::LANGUAGE.into()),
11134 )));
11135 let mut fake_servers = language_registry.register_fake_lsp(
11136 "Rust",
11137 FakeLspAdapter {
11138 capabilities: lsp::ServerCapabilities {
11139 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
11140 first_trigger_character: "{".to_string(),
11141 more_trigger_character: None,
11142 }),
11143 ..Default::default()
11144 },
11145 ..Default::default()
11146 },
11147 );
11148
11149 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11150
11151 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11152
11153 let worktree_id = workspace
11154 .update(cx, |workspace, _, cx| {
11155 workspace.project().update(cx, |project, cx| {
11156 project.worktrees(cx).next().unwrap().read(cx).id()
11157 })
11158 })
11159 .unwrap();
11160
11161 let buffer = project
11162 .update(cx, |project, cx| {
11163 project.open_local_buffer(path!("/a/main.rs"), cx)
11164 })
11165 .await
11166 .unwrap();
11167 let editor_handle = workspace
11168 .update(cx, |workspace, window, cx| {
11169 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
11170 })
11171 .unwrap()
11172 .await
11173 .unwrap()
11174 .downcast::<Editor>()
11175 .unwrap();
11176
11177 cx.executor().start_waiting();
11178 let fake_server = fake_servers.next().await.unwrap();
11179
11180 fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
11181 assert_eq!(
11182 params.text_document_position.text_document.uri,
11183 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
11184 );
11185 assert_eq!(
11186 params.text_document_position.position,
11187 lsp::Position::new(0, 21),
11188 );
11189
11190 Ok(Some(vec![lsp::TextEdit {
11191 new_text: "]".to_string(),
11192 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11193 }]))
11194 });
11195
11196 editor_handle.update_in(cx, |editor, window, cx| {
11197 window.focus(&editor.focus_handle(cx));
11198 editor.change_selections(None, window, cx, |s| {
11199 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
11200 });
11201 editor.handle_input("{", window, cx);
11202 });
11203
11204 cx.executor().run_until_parked();
11205
11206 buffer.update(cx, |buffer, _| {
11207 assert_eq!(
11208 buffer.text(),
11209 "fn main() { let a = {5}; }",
11210 "No extra braces from on type formatting should appear in the buffer"
11211 )
11212 });
11213}
11214
11215#[gpui::test]
11216async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) {
11217 init_test(cx, |_| {});
11218
11219 let fs = FakeFs::new(cx.executor());
11220 fs.insert_tree(
11221 path!("/a"),
11222 json!({
11223 "main.rs": "fn main() { let a = 5; }",
11224 "other.rs": "// Test file",
11225 }),
11226 )
11227 .await;
11228
11229 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11230
11231 let server_restarts = Arc::new(AtomicUsize::new(0));
11232 let closure_restarts = Arc::clone(&server_restarts);
11233 let language_server_name = "test language server";
11234 let language_name: LanguageName = "Rust".into();
11235
11236 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11237 language_registry.add(Arc::new(Language::new(
11238 LanguageConfig {
11239 name: language_name.clone(),
11240 matcher: LanguageMatcher {
11241 path_suffixes: vec!["rs".to_string()],
11242 ..Default::default()
11243 },
11244 ..Default::default()
11245 },
11246 Some(tree_sitter_rust::LANGUAGE.into()),
11247 )));
11248 let mut fake_servers = language_registry.register_fake_lsp(
11249 "Rust",
11250 FakeLspAdapter {
11251 name: language_server_name,
11252 initialization_options: Some(json!({
11253 "testOptionValue": true
11254 })),
11255 initializer: Some(Box::new(move |fake_server| {
11256 let task_restarts = Arc::clone(&closure_restarts);
11257 fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
11258 task_restarts.fetch_add(1, atomic::Ordering::Release);
11259 futures::future::ready(Ok(()))
11260 });
11261 })),
11262 ..Default::default()
11263 },
11264 );
11265
11266 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11267 let _buffer = project
11268 .update(cx, |project, cx| {
11269 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
11270 })
11271 .await
11272 .unwrap();
11273 let _fake_server = fake_servers.next().await.unwrap();
11274 update_test_language_settings(cx, |language_settings| {
11275 language_settings.languages.insert(
11276 language_name.clone(),
11277 LanguageSettingsContent {
11278 tab_size: NonZeroU32::new(8),
11279 ..Default::default()
11280 },
11281 );
11282 });
11283 cx.executor().run_until_parked();
11284 assert_eq!(
11285 server_restarts.load(atomic::Ordering::Acquire),
11286 0,
11287 "Should not restart LSP server on an unrelated change"
11288 );
11289
11290 update_test_project_settings(cx, |project_settings| {
11291 project_settings.lsp.insert(
11292 "Some other server name".into(),
11293 LspSettings {
11294 binary: None,
11295 settings: None,
11296 initialization_options: Some(json!({
11297 "some other init value": false
11298 })),
11299 },
11300 );
11301 });
11302 cx.executor().run_until_parked();
11303 assert_eq!(
11304 server_restarts.load(atomic::Ordering::Acquire),
11305 0,
11306 "Should not restart LSP server on an unrelated LSP settings change"
11307 );
11308
11309 update_test_project_settings(cx, |project_settings| {
11310 project_settings.lsp.insert(
11311 language_server_name.into(),
11312 LspSettings {
11313 binary: None,
11314 settings: None,
11315 initialization_options: Some(json!({
11316 "anotherInitValue": false
11317 })),
11318 },
11319 );
11320 });
11321 cx.executor().run_until_parked();
11322 assert_eq!(
11323 server_restarts.load(atomic::Ordering::Acquire),
11324 1,
11325 "Should restart LSP server on a related LSP settings change"
11326 );
11327
11328 update_test_project_settings(cx, |project_settings| {
11329 project_settings.lsp.insert(
11330 language_server_name.into(),
11331 LspSettings {
11332 binary: None,
11333 settings: None,
11334 initialization_options: Some(json!({
11335 "anotherInitValue": false
11336 })),
11337 },
11338 );
11339 });
11340 cx.executor().run_until_parked();
11341 assert_eq!(
11342 server_restarts.load(atomic::Ordering::Acquire),
11343 1,
11344 "Should not restart LSP server on a related LSP settings change that is the same"
11345 );
11346
11347 update_test_project_settings(cx, |project_settings| {
11348 project_settings.lsp.insert(
11349 language_server_name.into(),
11350 LspSettings {
11351 binary: None,
11352 settings: None,
11353 initialization_options: None,
11354 },
11355 );
11356 });
11357 cx.executor().run_until_parked();
11358 assert_eq!(
11359 server_restarts.load(atomic::Ordering::Acquire),
11360 2,
11361 "Should restart LSP server on another related LSP settings change"
11362 );
11363}
11364
11365#[gpui::test]
11366async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
11367 init_test(cx, |_| {});
11368
11369 let mut cx = EditorLspTestContext::new_rust(
11370 lsp::ServerCapabilities {
11371 completion_provider: Some(lsp::CompletionOptions {
11372 trigger_characters: Some(vec![".".to_string()]),
11373 resolve_provider: Some(true),
11374 ..Default::default()
11375 }),
11376 ..Default::default()
11377 },
11378 cx,
11379 )
11380 .await;
11381
11382 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11383 cx.simulate_keystroke(".");
11384 let completion_item = lsp::CompletionItem {
11385 label: "some".into(),
11386 kind: Some(lsp::CompletionItemKind::SNIPPET),
11387 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11388 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11389 kind: lsp::MarkupKind::Markdown,
11390 value: "```rust\nSome(2)\n```".to_string(),
11391 })),
11392 deprecated: Some(false),
11393 sort_text: Some("fffffff2".to_string()),
11394 filter_text: Some("some".to_string()),
11395 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11396 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11397 range: lsp::Range {
11398 start: lsp::Position {
11399 line: 0,
11400 character: 22,
11401 },
11402 end: lsp::Position {
11403 line: 0,
11404 character: 22,
11405 },
11406 },
11407 new_text: "Some(2)".to_string(),
11408 })),
11409 additional_text_edits: Some(vec![lsp::TextEdit {
11410 range: lsp::Range {
11411 start: lsp::Position {
11412 line: 0,
11413 character: 20,
11414 },
11415 end: lsp::Position {
11416 line: 0,
11417 character: 22,
11418 },
11419 },
11420 new_text: "".to_string(),
11421 }]),
11422 ..Default::default()
11423 };
11424
11425 let closure_completion_item = completion_item.clone();
11426 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11427 let task_completion_item = closure_completion_item.clone();
11428 async move {
11429 Ok(Some(lsp::CompletionResponse::Array(vec![
11430 task_completion_item,
11431 ])))
11432 }
11433 });
11434
11435 request.next().await;
11436
11437 cx.condition(|editor, _| editor.context_menu_visible())
11438 .await;
11439 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11440 editor
11441 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11442 .unwrap()
11443 });
11444 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
11445
11446 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
11447 let task_completion_item = completion_item.clone();
11448 async move { Ok(task_completion_item) }
11449 })
11450 .next()
11451 .await
11452 .unwrap();
11453 apply_additional_edits.await.unwrap();
11454 cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
11455}
11456
11457#[gpui::test]
11458async fn test_completions_resolve_updates_labels_if_filter_text_matches(
11459 cx: &mut gpui::TestAppContext,
11460) {
11461 init_test(cx, |_| {});
11462
11463 let mut cx = EditorLspTestContext::new_rust(
11464 lsp::ServerCapabilities {
11465 completion_provider: Some(lsp::CompletionOptions {
11466 trigger_characters: Some(vec![".".to_string()]),
11467 resolve_provider: Some(true),
11468 ..Default::default()
11469 }),
11470 ..Default::default()
11471 },
11472 cx,
11473 )
11474 .await;
11475
11476 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11477 cx.simulate_keystroke(".");
11478
11479 let item1 = lsp::CompletionItem {
11480 label: "method id()".to_string(),
11481 filter_text: Some("id".to_string()),
11482 detail: None,
11483 documentation: None,
11484 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11485 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11486 new_text: ".id".to_string(),
11487 })),
11488 ..lsp::CompletionItem::default()
11489 };
11490
11491 let item2 = lsp::CompletionItem {
11492 label: "other".to_string(),
11493 filter_text: Some("other".to_string()),
11494 detail: None,
11495 documentation: None,
11496 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11497 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11498 new_text: ".other".to_string(),
11499 })),
11500 ..lsp::CompletionItem::default()
11501 };
11502
11503 let item1 = item1.clone();
11504 cx.handle_request::<lsp::request::Completion, _, _>({
11505 let item1 = item1.clone();
11506 move |_, _, _| {
11507 let item1 = item1.clone();
11508 let item2 = item2.clone();
11509 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
11510 }
11511 })
11512 .next()
11513 .await;
11514
11515 cx.condition(|editor, _| editor.context_menu_visible())
11516 .await;
11517 cx.update_editor(|editor, _, _| {
11518 let context_menu = editor.context_menu.borrow_mut();
11519 let context_menu = context_menu
11520 .as_ref()
11521 .expect("Should have the context menu deployed");
11522 match context_menu {
11523 CodeContextMenu::Completions(completions_menu) => {
11524 let completions = completions_menu.completions.borrow_mut();
11525 assert_eq!(
11526 completions
11527 .iter()
11528 .map(|completion| &completion.label.text)
11529 .collect::<Vec<_>>(),
11530 vec!["method id()", "other"]
11531 )
11532 }
11533 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11534 }
11535 });
11536
11537 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>({
11538 let item1 = item1.clone();
11539 move |_, item_to_resolve, _| {
11540 let item1 = item1.clone();
11541 async move {
11542 if item1 == item_to_resolve {
11543 Ok(lsp::CompletionItem {
11544 label: "method id()".to_string(),
11545 filter_text: Some("id".to_string()),
11546 detail: Some("Now resolved!".to_string()),
11547 documentation: Some(lsp::Documentation::String("Docs".to_string())),
11548 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11549 range: lsp::Range::new(
11550 lsp::Position::new(0, 22),
11551 lsp::Position::new(0, 22),
11552 ),
11553 new_text: ".id".to_string(),
11554 })),
11555 ..lsp::CompletionItem::default()
11556 })
11557 } else {
11558 Ok(item_to_resolve)
11559 }
11560 }
11561 }
11562 })
11563 .next()
11564 .await
11565 .unwrap();
11566 cx.run_until_parked();
11567
11568 cx.update_editor(|editor, window, cx| {
11569 editor.context_menu_next(&Default::default(), window, cx);
11570 });
11571
11572 cx.update_editor(|editor, _, _| {
11573 let context_menu = editor.context_menu.borrow_mut();
11574 let context_menu = context_menu
11575 .as_ref()
11576 .expect("Should have the context menu deployed");
11577 match context_menu {
11578 CodeContextMenu::Completions(completions_menu) => {
11579 let completions = completions_menu.completions.borrow_mut();
11580 assert_eq!(
11581 completions
11582 .iter()
11583 .map(|completion| &completion.label.text)
11584 .collect::<Vec<_>>(),
11585 vec!["method id() Now resolved!", "other"],
11586 "Should update first completion label, but not second as the filter text did not match."
11587 );
11588 }
11589 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11590 }
11591 });
11592}
11593
11594#[gpui::test]
11595async fn test_completions_resolve_happens_once(cx: &mut gpui::TestAppContext) {
11596 init_test(cx, |_| {});
11597
11598 let mut cx = EditorLspTestContext::new_rust(
11599 lsp::ServerCapabilities {
11600 completion_provider: Some(lsp::CompletionOptions {
11601 trigger_characters: Some(vec![".".to_string()]),
11602 resolve_provider: Some(true),
11603 ..Default::default()
11604 }),
11605 ..Default::default()
11606 },
11607 cx,
11608 )
11609 .await;
11610
11611 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11612 cx.simulate_keystroke(".");
11613
11614 let unresolved_item_1 = lsp::CompletionItem {
11615 label: "id".to_string(),
11616 filter_text: Some("id".to_string()),
11617 detail: None,
11618 documentation: None,
11619 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11620 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11621 new_text: ".id".to_string(),
11622 })),
11623 ..lsp::CompletionItem::default()
11624 };
11625 let resolved_item_1 = lsp::CompletionItem {
11626 additional_text_edits: Some(vec![lsp::TextEdit {
11627 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
11628 new_text: "!!".to_string(),
11629 }]),
11630 ..unresolved_item_1.clone()
11631 };
11632 let unresolved_item_2 = lsp::CompletionItem {
11633 label: "other".to_string(),
11634 filter_text: Some("other".to_string()),
11635 detail: None,
11636 documentation: None,
11637 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11638 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11639 new_text: ".other".to_string(),
11640 })),
11641 ..lsp::CompletionItem::default()
11642 };
11643 let resolved_item_2 = lsp::CompletionItem {
11644 additional_text_edits: Some(vec![lsp::TextEdit {
11645 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
11646 new_text: "??".to_string(),
11647 }]),
11648 ..unresolved_item_2.clone()
11649 };
11650
11651 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
11652 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
11653 cx.lsp
11654 .server
11655 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
11656 let unresolved_item_1 = unresolved_item_1.clone();
11657 let resolved_item_1 = resolved_item_1.clone();
11658 let unresolved_item_2 = unresolved_item_2.clone();
11659 let resolved_item_2 = resolved_item_2.clone();
11660 let resolve_requests_1 = resolve_requests_1.clone();
11661 let resolve_requests_2 = resolve_requests_2.clone();
11662 move |unresolved_request, _| {
11663 let unresolved_item_1 = unresolved_item_1.clone();
11664 let resolved_item_1 = resolved_item_1.clone();
11665 let unresolved_item_2 = unresolved_item_2.clone();
11666 let resolved_item_2 = resolved_item_2.clone();
11667 let resolve_requests_1 = resolve_requests_1.clone();
11668 let resolve_requests_2 = resolve_requests_2.clone();
11669 async move {
11670 if unresolved_request == unresolved_item_1 {
11671 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
11672 Ok(resolved_item_1.clone())
11673 } else if unresolved_request == unresolved_item_2 {
11674 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
11675 Ok(resolved_item_2.clone())
11676 } else {
11677 panic!("Unexpected completion item {unresolved_request:?}")
11678 }
11679 }
11680 }
11681 })
11682 .detach();
11683
11684 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11685 let unresolved_item_1 = unresolved_item_1.clone();
11686 let unresolved_item_2 = unresolved_item_2.clone();
11687 async move {
11688 Ok(Some(lsp::CompletionResponse::Array(vec![
11689 unresolved_item_1,
11690 unresolved_item_2,
11691 ])))
11692 }
11693 })
11694 .next()
11695 .await;
11696
11697 cx.condition(|editor, _| editor.context_menu_visible())
11698 .await;
11699 cx.update_editor(|editor, _, _| {
11700 let context_menu = editor.context_menu.borrow_mut();
11701 let context_menu = context_menu
11702 .as_ref()
11703 .expect("Should have the context menu deployed");
11704 match context_menu {
11705 CodeContextMenu::Completions(completions_menu) => {
11706 let completions = completions_menu.completions.borrow_mut();
11707 assert_eq!(
11708 completions
11709 .iter()
11710 .map(|completion| &completion.label.text)
11711 .collect::<Vec<_>>(),
11712 vec!["id", "other"]
11713 )
11714 }
11715 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11716 }
11717 });
11718 cx.run_until_parked();
11719
11720 cx.update_editor(|editor, window, cx| {
11721 editor.context_menu_next(&ContextMenuNext, window, cx);
11722 });
11723 cx.run_until_parked();
11724 cx.update_editor(|editor, window, cx| {
11725 editor.context_menu_prev(&ContextMenuPrev, window, cx);
11726 });
11727 cx.run_until_parked();
11728 cx.update_editor(|editor, window, cx| {
11729 editor.context_menu_next(&ContextMenuNext, window, cx);
11730 });
11731 cx.run_until_parked();
11732 cx.update_editor(|editor, window, cx| {
11733 editor
11734 .compose_completion(&ComposeCompletion::default(), window, cx)
11735 .expect("No task returned")
11736 })
11737 .await
11738 .expect("Completion failed");
11739 cx.run_until_parked();
11740
11741 cx.update_editor(|editor, _, cx| {
11742 assert_eq!(
11743 resolve_requests_1.load(atomic::Ordering::Acquire),
11744 1,
11745 "Should always resolve once despite multiple selections"
11746 );
11747 assert_eq!(
11748 resolve_requests_2.load(atomic::Ordering::Acquire),
11749 1,
11750 "Should always resolve once after multiple selections and applying the completion"
11751 );
11752 assert_eq!(
11753 editor.text(cx),
11754 "fn main() { let a = ??.other; }",
11755 "Should use resolved data when applying the completion"
11756 );
11757 });
11758}
11759
11760#[gpui::test]
11761async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
11762 init_test(cx, |_| {});
11763
11764 let item_0 = lsp::CompletionItem {
11765 label: "abs".into(),
11766 insert_text: Some("abs".into()),
11767 data: Some(json!({ "very": "special"})),
11768 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
11769 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11770 lsp::InsertReplaceEdit {
11771 new_text: "abs".to_string(),
11772 insert: lsp::Range::default(),
11773 replace: lsp::Range::default(),
11774 },
11775 )),
11776 ..lsp::CompletionItem::default()
11777 };
11778 let items = iter::once(item_0.clone())
11779 .chain((11..51).map(|i| lsp::CompletionItem {
11780 label: format!("item_{}", i),
11781 insert_text: Some(format!("item_{}", i)),
11782 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
11783 ..lsp::CompletionItem::default()
11784 }))
11785 .collect::<Vec<_>>();
11786
11787 let default_commit_characters = vec!["?".to_string()];
11788 let default_data = json!({ "default": "data"});
11789 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
11790 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
11791 let default_edit_range = lsp::Range {
11792 start: lsp::Position {
11793 line: 0,
11794 character: 5,
11795 },
11796 end: lsp::Position {
11797 line: 0,
11798 character: 5,
11799 },
11800 };
11801
11802 let item_0_out = lsp::CompletionItem {
11803 commit_characters: Some(default_commit_characters.clone()),
11804 insert_text_format: Some(default_insert_text_format),
11805 ..item_0
11806 };
11807 let items_out = iter::once(item_0_out)
11808 .chain(items[1..].iter().map(|item| lsp::CompletionItem {
11809 commit_characters: Some(default_commit_characters.clone()),
11810 data: Some(default_data.clone()),
11811 insert_text_mode: Some(default_insert_text_mode),
11812 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11813 range: default_edit_range,
11814 new_text: item.label.clone(),
11815 })),
11816 ..item.clone()
11817 }))
11818 .collect::<Vec<lsp::CompletionItem>>();
11819
11820 let mut cx = EditorLspTestContext::new_rust(
11821 lsp::ServerCapabilities {
11822 completion_provider: Some(lsp::CompletionOptions {
11823 trigger_characters: Some(vec![".".to_string()]),
11824 resolve_provider: Some(true),
11825 ..Default::default()
11826 }),
11827 ..Default::default()
11828 },
11829 cx,
11830 )
11831 .await;
11832
11833 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11834 cx.simulate_keystroke(".");
11835
11836 let completion_data = default_data.clone();
11837 let completion_characters = default_commit_characters.clone();
11838 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11839 let default_data = completion_data.clone();
11840 let default_commit_characters = completion_characters.clone();
11841 let items = items.clone();
11842 async move {
11843 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
11844 items,
11845 item_defaults: Some(lsp::CompletionListItemDefaults {
11846 data: Some(default_data.clone()),
11847 commit_characters: Some(default_commit_characters.clone()),
11848 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
11849 default_edit_range,
11850 )),
11851 insert_text_format: Some(default_insert_text_format),
11852 insert_text_mode: Some(default_insert_text_mode),
11853 }),
11854 ..lsp::CompletionList::default()
11855 })))
11856 }
11857 })
11858 .next()
11859 .await;
11860
11861 let resolved_items = Arc::new(Mutex::new(Vec::new()));
11862 cx.lsp
11863 .server
11864 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
11865 let closure_resolved_items = resolved_items.clone();
11866 move |item_to_resolve, _| {
11867 let closure_resolved_items = closure_resolved_items.clone();
11868 async move {
11869 closure_resolved_items.lock().push(item_to_resolve.clone());
11870 Ok(item_to_resolve)
11871 }
11872 }
11873 })
11874 .detach();
11875
11876 cx.condition(|editor, _| editor.context_menu_visible())
11877 .await;
11878 cx.run_until_parked();
11879 cx.update_editor(|editor, _, _| {
11880 let menu = editor.context_menu.borrow_mut();
11881 match menu.as_ref().expect("should have the completions menu") {
11882 CodeContextMenu::Completions(completions_menu) => {
11883 assert_eq!(
11884 completions_menu
11885 .entries
11886 .borrow()
11887 .iter()
11888 .map(|mat| mat.string.clone())
11889 .collect::<Vec<String>>(),
11890 items_out
11891 .iter()
11892 .map(|completion| completion.label.clone())
11893 .collect::<Vec<String>>()
11894 );
11895 }
11896 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
11897 }
11898 });
11899 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
11900 // with 4 from the end.
11901 assert_eq!(
11902 *resolved_items.lock(),
11903 [
11904 &items_out[0..16],
11905 &items_out[items_out.len() - 4..items_out.len()]
11906 ]
11907 .concat()
11908 .iter()
11909 .cloned()
11910 .collect::<Vec<lsp::CompletionItem>>()
11911 );
11912 resolved_items.lock().clear();
11913
11914 cx.update_editor(|editor, window, cx| {
11915 editor.context_menu_prev(&ContextMenuPrev, window, cx);
11916 });
11917 cx.run_until_parked();
11918 // Completions that have already been resolved are skipped.
11919 assert_eq!(
11920 *resolved_items.lock(),
11921 items_out[items_out.len() - 16..items_out.len() - 4]
11922 .iter()
11923 .cloned()
11924 .collect::<Vec<lsp::CompletionItem>>()
11925 );
11926 resolved_items.lock().clear();
11927}
11928
11929#[gpui::test]
11930async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
11931 init_test(cx, |_| {});
11932
11933 let mut cx = EditorLspTestContext::new(
11934 Language::new(
11935 LanguageConfig {
11936 matcher: LanguageMatcher {
11937 path_suffixes: vec!["jsx".into()],
11938 ..Default::default()
11939 },
11940 overrides: [(
11941 "element".into(),
11942 LanguageConfigOverride {
11943 word_characters: Override::Set(['-'].into_iter().collect()),
11944 ..Default::default()
11945 },
11946 )]
11947 .into_iter()
11948 .collect(),
11949 ..Default::default()
11950 },
11951 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
11952 )
11953 .with_override_query("(jsx_self_closing_element) @element")
11954 .unwrap(),
11955 lsp::ServerCapabilities {
11956 completion_provider: Some(lsp::CompletionOptions {
11957 trigger_characters: Some(vec![":".to_string()]),
11958 ..Default::default()
11959 }),
11960 ..Default::default()
11961 },
11962 cx,
11963 )
11964 .await;
11965
11966 cx.lsp
11967 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11968 Ok(Some(lsp::CompletionResponse::Array(vec![
11969 lsp::CompletionItem {
11970 label: "bg-blue".into(),
11971 ..Default::default()
11972 },
11973 lsp::CompletionItem {
11974 label: "bg-red".into(),
11975 ..Default::default()
11976 },
11977 lsp::CompletionItem {
11978 label: "bg-yellow".into(),
11979 ..Default::default()
11980 },
11981 ])))
11982 });
11983
11984 cx.set_state(r#"<p class="bgˇ" />"#);
11985
11986 // Trigger completion when typing a dash, because the dash is an extra
11987 // word character in the 'element' scope, which contains the cursor.
11988 cx.simulate_keystroke("-");
11989 cx.executor().run_until_parked();
11990 cx.update_editor(|editor, _, _| {
11991 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11992 {
11993 assert_eq!(
11994 completion_menu_entries(&menu),
11995 &["bg-red", "bg-blue", "bg-yellow"]
11996 );
11997 } else {
11998 panic!("expected completion menu to be open");
11999 }
12000 });
12001
12002 cx.simulate_keystroke("l");
12003 cx.executor().run_until_parked();
12004 cx.update_editor(|editor, _, _| {
12005 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12006 {
12007 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
12008 } else {
12009 panic!("expected completion menu to be open");
12010 }
12011 });
12012
12013 // When filtering completions, consider the character after the '-' to
12014 // be the start of a subword.
12015 cx.set_state(r#"<p class="yelˇ" />"#);
12016 cx.simulate_keystroke("l");
12017 cx.executor().run_until_parked();
12018 cx.update_editor(|editor, _, _| {
12019 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12020 {
12021 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
12022 } else {
12023 panic!("expected completion menu to be open");
12024 }
12025 });
12026}
12027
12028fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
12029 let entries = menu.entries.borrow();
12030 entries.iter().map(|mat| mat.string.clone()).collect()
12031}
12032
12033#[gpui::test]
12034async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
12035 init_test(cx, |settings| {
12036 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
12037 FormatterList(vec![Formatter::Prettier].into()),
12038 ))
12039 });
12040
12041 let fs = FakeFs::new(cx.executor());
12042 fs.insert_file(path!("/file.ts"), Default::default()).await;
12043
12044 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
12045 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12046
12047 language_registry.add(Arc::new(Language::new(
12048 LanguageConfig {
12049 name: "TypeScript".into(),
12050 matcher: LanguageMatcher {
12051 path_suffixes: vec!["ts".to_string()],
12052 ..Default::default()
12053 },
12054 ..Default::default()
12055 },
12056 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12057 )));
12058 update_test_language_settings(cx, |settings| {
12059 settings.defaults.prettier = Some(PrettierSettings {
12060 allowed: true,
12061 ..PrettierSettings::default()
12062 });
12063 });
12064
12065 let test_plugin = "test_plugin";
12066 let _ = language_registry.register_fake_lsp(
12067 "TypeScript",
12068 FakeLspAdapter {
12069 prettier_plugins: vec![test_plugin],
12070 ..Default::default()
12071 },
12072 );
12073
12074 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
12075 let buffer = project
12076 .update(cx, |project, cx| {
12077 project.open_local_buffer(path!("/file.ts"), cx)
12078 })
12079 .await
12080 .unwrap();
12081
12082 let buffer_text = "one\ntwo\nthree\n";
12083 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12084 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12085 editor.update_in(cx, |editor, window, cx| {
12086 editor.set_text(buffer_text, window, cx)
12087 });
12088
12089 editor
12090 .update_in(cx, |editor, window, cx| {
12091 editor.perform_format(
12092 project.clone(),
12093 FormatTrigger::Manual,
12094 FormatTarget::Buffers,
12095 window,
12096 cx,
12097 )
12098 })
12099 .unwrap()
12100 .await;
12101 assert_eq!(
12102 editor.update(cx, |editor, cx| editor.text(cx)),
12103 buffer_text.to_string() + prettier_format_suffix,
12104 "Test prettier formatting was not applied to the original buffer text",
12105 );
12106
12107 update_test_language_settings(cx, |settings| {
12108 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
12109 });
12110 let format = editor.update_in(cx, |editor, window, cx| {
12111 editor.perform_format(
12112 project.clone(),
12113 FormatTrigger::Manual,
12114 FormatTarget::Buffers,
12115 window,
12116 cx,
12117 )
12118 });
12119 format.await.unwrap();
12120 assert_eq!(
12121 editor.update(cx, |editor, cx| editor.text(cx)),
12122 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
12123 "Autoformatting (via test prettier) was not applied to the original buffer text",
12124 );
12125}
12126
12127#[gpui::test]
12128async fn test_addition_reverts(cx: &mut gpui::TestAppContext) {
12129 init_test(cx, |_| {});
12130 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12131 let base_text = indoc! {r#"
12132 struct Row;
12133 struct Row1;
12134 struct Row2;
12135
12136 struct Row4;
12137 struct Row5;
12138 struct Row6;
12139
12140 struct Row8;
12141 struct Row9;
12142 struct Row10;"#};
12143
12144 // When addition hunks are not adjacent to carets, no hunk revert is performed
12145 assert_hunk_revert(
12146 indoc! {r#"struct Row;
12147 struct Row1;
12148 struct Row1.1;
12149 struct Row1.2;
12150 struct Row2;ˇ
12151
12152 struct Row4;
12153 struct Row5;
12154 struct Row6;
12155
12156 struct Row8;
12157 ˇstruct Row9;
12158 struct Row9.1;
12159 struct Row9.2;
12160 struct Row9.3;
12161 struct Row10;"#},
12162 vec![DiffHunkStatus::Added, DiffHunkStatus::Added],
12163 indoc! {r#"struct Row;
12164 struct Row1;
12165 struct Row1.1;
12166 struct Row1.2;
12167 struct Row2;ˇ
12168
12169 struct Row4;
12170 struct Row5;
12171 struct Row6;
12172
12173 struct Row8;
12174 ˇstruct Row9;
12175 struct Row9.1;
12176 struct Row9.2;
12177 struct Row9.3;
12178 struct Row10;"#},
12179 base_text,
12180 &mut cx,
12181 );
12182 // Same for selections
12183 assert_hunk_revert(
12184 indoc! {r#"struct Row;
12185 struct Row1;
12186 struct Row2;
12187 struct Row2.1;
12188 struct Row2.2;
12189 «ˇ
12190 struct Row4;
12191 struct» Row5;
12192 «struct Row6;
12193 ˇ»
12194 struct Row9.1;
12195 struct Row9.2;
12196 struct Row9.3;
12197 struct Row8;
12198 struct Row9;
12199 struct Row10;"#},
12200 vec![DiffHunkStatus::Added, DiffHunkStatus::Added],
12201 indoc! {r#"struct Row;
12202 struct Row1;
12203 struct Row2;
12204 struct Row2.1;
12205 struct Row2.2;
12206 «ˇ
12207 struct Row4;
12208 struct» Row5;
12209 «struct Row6;
12210 ˇ»
12211 struct Row9.1;
12212 struct Row9.2;
12213 struct Row9.3;
12214 struct Row8;
12215 struct Row9;
12216 struct Row10;"#},
12217 base_text,
12218 &mut cx,
12219 );
12220
12221 // When carets and selections intersect the addition hunks, those are reverted.
12222 // Adjacent carets got merged.
12223 assert_hunk_revert(
12224 indoc! {r#"struct Row;
12225 ˇ// something on the top
12226 struct Row1;
12227 struct Row2;
12228 struct Roˇw3.1;
12229 struct Row2.2;
12230 struct Row2.3;ˇ
12231
12232 struct Row4;
12233 struct ˇRow5.1;
12234 struct Row5.2;
12235 struct «Rowˇ»5.3;
12236 struct Row5;
12237 struct Row6;
12238 ˇ
12239 struct Row9.1;
12240 struct «Rowˇ»9.2;
12241 struct «ˇRow»9.3;
12242 struct Row8;
12243 struct Row9;
12244 «ˇ// something on bottom»
12245 struct Row10;"#},
12246 vec![
12247 DiffHunkStatus::Added,
12248 DiffHunkStatus::Added,
12249 DiffHunkStatus::Added,
12250 DiffHunkStatus::Added,
12251 DiffHunkStatus::Added,
12252 ],
12253 indoc! {r#"struct Row;
12254 ˇstruct Row1;
12255 struct Row2;
12256 ˇ
12257 struct Row4;
12258 ˇstruct Row5;
12259 struct Row6;
12260 ˇ
12261 ˇstruct Row8;
12262 struct Row9;
12263 ˇstruct Row10;"#},
12264 base_text,
12265 &mut cx,
12266 );
12267}
12268
12269#[gpui::test]
12270async fn test_modification_reverts(cx: &mut gpui::TestAppContext) {
12271 init_test(cx, |_| {});
12272 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12273 let base_text = indoc! {r#"
12274 struct Row;
12275 struct Row1;
12276 struct Row2;
12277
12278 struct Row4;
12279 struct Row5;
12280 struct Row6;
12281
12282 struct Row8;
12283 struct Row9;
12284 struct Row10;"#};
12285
12286 // Modification hunks behave the same as the addition ones.
12287 assert_hunk_revert(
12288 indoc! {r#"struct Row;
12289 struct Row1;
12290 struct Row33;
12291 ˇ
12292 struct Row4;
12293 struct Row5;
12294 struct Row6;
12295 ˇ
12296 struct Row99;
12297 struct Row9;
12298 struct Row10;"#},
12299 vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified],
12300 indoc! {r#"struct Row;
12301 struct Row1;
12302 struct Row33;
12303 ˇ
12304 struct Row4;
12305 struct Row5;
12306 struct Row6;
12307 ˇ
12308 struct Row99;
12309 struct Row9;
12310 struct Row10;"#},
12311 base_text,
12312 &mut cx,
12313 );
12314 assert_hunk_revert(
12315 indoc! {r#"struct Row;
12316 struct Row1;
12317 struct Row33;
12318 «ˇ
12319 struct Row4;
12320 struct» Row5;
12321 «struct Row6;
12322 ˇ»
12323 struct Row99;
12324 struct Row9;
12325 struct Row10;"#},
12326 vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified],
12327 indoc! {r#"struct Row;
12328 struct Row1;
12329 struct Row33;
12330 «ˇ
12331 struct Row4;
12332 struct» Row5;
12333 «struct Row6;
12334 ˇ»
12335 struct Row99;
12336 struct Row9;
12337 struct Row10;"#},
12338 base_text,
12339 &mut cx,
12340 );
12341
12342 assert_hunk_revert(
12343 indoc! {r#"ˇstruct Row1.1;
12344 struct Row1;
12345 «ˇstr»uct Row22;
12346
12347 struct ˇRow44;
12348 struct Row5;
12349 struct «Rˇ»ow66;ˇ
12350
12351 «struˇ»ct Row88;
12352 struct Row9;
12353 struct Row1011;ˇ"#},
12354 vec![
12355 DiffHunkStatus::Modified,
12356 DiffHunkStatus::Modified,
12357 DiffHunkStatus::Modified,
12358 DiffHunkStatus::Modified,
12359 DiffHunkStatus::Modified,
12360 DiffHunkStatus::Modified,
12361 ],
12362 indoc! {r#"struct Row;
12363 ˇstruct Row1;
12364 struct Row2;
12365 ˇ
12366 struct Row4;
12367 ˇstruct Row5;
12368 struct Row6;
12369 ˇ
12370 struct Row8;
12371 ˇstruct Row9;
12372 struct Row10;ˇ"#},
12373 base_text,
12374 &mut cx,
12375 );
12376}
12377
12378#[gpui::test]
12379async fn test_deleting_over_diff_hunk(cx: &mut gpui::TestAppContext) {
12380 init_test(cx, |_| {});
12381 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12382 let base_text = indoc! {r#"
12383 one
12384
12385 two
12386 three
12387 "#};
12388
12389 cx.set_diff_base(base_text);
12390 cx.set_state("\nˇ\n");
12391 cx.executor().run_until_parked();
12392 cx.update_editor(|editor, _window, cx| {
12393 editor.expand_selected_diff_hunks(cx);
12394 });
12395 cx.executor().run_until_parked();
12396 cx.update_editor(|editor, window, cx| {
12397 editor.backspace(&Default::default(), window, cx);
12398 });
12399 cx.run_until_parked();
12400 cx.assert_state_with_diff(
12401 indoc! {r#"
12402
12403 - two
12404 - threeˇ
12405 +
12406 "#}
12407 .to_string(),
12408 );
12409}
12410
12411#[gpui::test]
12412async fn test_deletion_reverts(cx: &mut gpui::TestAppContext) {
12413 init_test(cx, |_| {});
12414 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12415 let base_text = indoc! {r#"struct Row;
12416struct Row1;
12417struct Row2;
12418
12419struct Row4;
12420struct Row5;
12421struct Row6;
12422
12423struct Row8;
12424struct Row9;
12425struct Row10;"#};
12426
12427 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
12428 assert_hunk_revert(
12429 indoc! {r#"struct Row;
12430 struct Row2;
12431
12432 ˇstruct Row4;
12433 struct Row5;
12434 struct Row6;
12435 ˇ
12436 struct Row8;
12437 struct Row10;"#},
12438 vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
12439 indoc! {r#"struct Row;
12440 struct Row2;
12441
12442 ˇstruct Row4;
12443 struct Row5;
12444 struct Row6;
12445 ˇ
12446 struct Row8;
12447 struct Row10;"#},
12448 base_text,
12449 &mut cx,
12450 );
12451 assert_hunk_revert(
12452 indoc! {r#"struct Row;
12453 struct Row2;
12454
12455 «ˇstruct Row4;
12456 struct» Row5;
12457 «struct Row6;
12458 ˇ»
12459 struct Row8;
12460 struct Row10;"#},
12461 vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
12462 indoc! {r#"struct Row;
12463 struct Row2;
12464
12465 «ˇstruct Row4;
12466 struct» Row5;
12467 «struct Row6;
12468 ˇ»
12469 struct Row8;
12470 struct Row10;"#},
12471 base_text,
12472 &mut cx,
12473 );
12474
12475 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
12476 assert_hunk_revert(
12477 indoc! {r#"struct Row;
12478 ˇstruct Row2;
12479
12480 struct Row4;
12481 struct Row5;
12482 struct Row6;
12483
12484 struct Row8;ˇ
12485 struct Row10;"#},
12486 vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
12487 indoc! {r#"struct Row;
12488 struct Row1;
12489 ˇstruct Row2;
12490
12491 struct Row4;
12492 struct Row5;
12493 struct Row6;
12494
12495 struct Row8;ˇ
12496 struct Row9;
12497 struct Row10;"#},
12498 base_text,
12499 &mut cx,
12500 );
12501 assert_hunk_revert(
12502 indoc! {r#"struct Row;
12503 struct Row2«ˇ;
12504 struct Row4;
12505 struct» Row5;
12506 «struct Row6;
12507
12508 struct Row8;ˇ»
12509 struct Row10;"#},
12510 vec![
12511 DiffHunkStatus::Removed,
12512 DiffHunkStatus::Removed,
12513 DiffHunkStatus::Removed,
12514 ],
12515 indoc! {r#"struct Row;
12516 struct Row1;
12517 struct Row2«ˇ;
12518
12519 struct Row4;
12520 struct» Row5;
12521 «struct Row6;
12522
12523 struct Row8;ˇ»
12524 struct Row9;
12525 struct Row10;"#},
12526 base_text,
12527 &mut cx,
12528 );
12529}
12530
12531#[gpui::test]
12532async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
12533 init_test(cx, |_| {});
12534
12535 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
12536 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
12537 let base_text_3 =
12538 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
12539
12540 let text_1 = edit_first_char_of_every_line(base_text_1);
12541 let text_2 = edit_first_char_of_every_line(base_text_2);
12542 let text_3 = edit_first_char_of_every_line(base_text_3);
12543
12544 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
12545 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
12546 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
12547
12548 let multibuffer = cx.new(|cx| {
12549 let mut multibuffer = MultiBuffer::new(ReadWrite);
12550 multibuffer.push_excerpts(
12551 buffer_1.clone(),
12552 [
12553 ExcerptRange {
12554 context: Point::new(0, 0)..Point::new(3, 0),
12555 primary: None,
12556 },
12557 ExcerptRange {
12558 context: Point::new(5, 0)..Point::new(7, 0),
12559 primary: None,
12560 },
12561 ExcerptRange {
12562 context: Point::new(9, 0)..Point::new(10, 4),
12563 primary: None,
12564 },
12565 ],
12566 cx,
12567 );
12568 multibuffer.push_excerpts(
12569 buffer_2.clone(),
12570 [
12571 ExcerptRange {
12572 context: Point::new(0, 0)..Point::new(3, 0),
12573 primary: None,
12574 },
12575 ExcerptRange {
12576 context: Point::new(5, 0)..Point::new(7, 0),
12577 primary: None,
12578 },
12579 ExcerptRange {
12580 context: Point::new(9, 0)..Point::new(10, 4),
12581 primary: None,
12582 },
12583 ],
12584 cx,
12585 );
12586 multibuffer.push_excerpts(
12587 buffer_3.clone(),
12588 [
12589 ExcerptRange {
12590 context: Point::new(0, 0)..Point::new(3, 0),
12591 primary: None,
12592 },
12593 ExcerptRange {
12594 context: Point::new(5, 0)..Point::new(7, 0),
12595 primary: None,
12596 },
12597 ExcerptRange {
12598 context: Point::new(9, 0)..Point::new(10, 4),
12599 primary: None,
12600 },
12601 ],
12602 cx,
12603 );
12604 multibuffer
12605 });
12606
12607 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12608 editor.update_in(cx, |editor, _window, cx| {
12609 for (buffer, diff_base) in [
12610 (buffer_1.clone(), base_text_1),
12611 (buffer_2.clone(), base_text_2),
12612 (buffer_3.clone(), base_text_3),
12613 ] {
12614 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
12615 editor
12616 .buffer
12617 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
12618 }
12619 });
12620 cx.executor().run_until_parked();
12621
12622 editor.update_in(cx, |editor, window, cx| {
12623 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}");
12624 editor.select_all(&SelectAll, window, cx);
12625 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
12626 });
12627 cx.executor().run_until_parked();
12628
12629 // When all ranges are selected, all buffer hunks are reverted.
12630 editor.update(cx, |editor, cx| {
12631 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");
12632 });
12633 buffer_1.update(cx, |buffer, _| {
12634 assert_eq!(buffer.text(), base_text_1);
12635 });
12636 buffer_2.update(cx, |buffer, _| {
12637 assert_eq!(buffer.text(), base_text_2);
12638 });
12639 buffer_3.update(cx, |buffer, _| {
12640 assert_eq!(buffer.text(), base_text_3);
12641 });
12642
12643 editor.update_in(cx, |editor, window, cx| {
12644 editor.undo(&Default::default(), window, cx);
12645 });
12646
12647 editor.update_in(cx, |editor, window, cx| {
12648 editor.change_selections(None, window, cx, |s| {
12649 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
12650 });
12651 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
12652 });
12653
12654 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
12655 // but not affect buffer_2 and its related excerpts.
12656 editor.update(cx, |editor, cx| {
12657 assert_eq!(
12658 editor.text(cx),
12659 "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}"
12660 );
12661 });
12662 buffer_1.update(cx, |buffer, _| {
12663 assert_eq!(buffer.text(), base_text_1);
12664 });
12665 buffer_2.update(cx, |buffer, _| {
12666 assert_eq!(
12667 buffer.text(),
12668 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
12669 );
12670 });
12671 buffer_3.update(cx, |buffer, _| {
12672 assert_eq!(
12673 buffer.text(),
12674 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
12675 );
12676 });
12677
12678 fn edit_first_char_of_every_line(text: &str) -> String {
12679 text.split('\n')
12680 .map(|line| format!("X{}", &line[1..]))
12681 .collect::<Vec<_>>()
12682 .join("\n")
12683 }
12684}
12685
12686#[gpui::test]
12687async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
12688 init_test(cx, |_| {});
12689
12690 let cols = 4;
12691 let rows = 10;
12692 let sample_text_1 = sample_text(rows, cols, 'a');
12693 assert_eq!(
12694 sample_text_1,
12695 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
12696 );
12697 let sample_text_2 = sample_text(rows, cols, 'l');
12698 assert_eq!(
12699 sample_text_2,
12700 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
12701 );
12702 let sample_text_3 = sample_text(rows, cols, 'v');
12703 assert_eq!(
12704 sample_text_3,
12705 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
12706 );
12707
12708 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
12709 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
12710 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
12711
12712 let multi_buffer = cx.new(|cx| {
12713 let mut multibuffer = MultiBuffer::new(ReadWrite);
12714 multibuffer.push_excerpts(
12715 buffer_1.clone(),
12716 [
12717 ExcerptRange {
12718 context: Point::new(0, 0)..Point::new(3, 0),
12719 primary: None,
12720 },
12721 ExcerptRange {
12722 context: Point::new(5, 0)..Point::new(7, 0),
12723 primary: None,
12724 },
12725 ExcerptRange {
12726 context: Point::new(9, 0)..Point::new(10, 4),
12727 primary: None,
12728 },
12729 ],
12730 cx,
12731 );
12732 multibuffer.push_excerpts(
12733 buffer_2.clone(),
12734 [
12735 ExcerptRange {
12736 context: Point::new(0, 0)..Point::new(3, 0),
12737 primary: None,
12738 },
12739 ExcerptRange {
12740 context: Point::new(5, 0)..Point::new(7, 0),
12741 primary: None,
12742 },
12743 ExcerptRange {
12744 context: Point::new(9, 0)..Point::new(10, 4),
12745 primary: None,
12746 },
12747 ],
12748 cx,
12749 );
12750 multibuffer.push_excerpts(
12751 buffer_3.clone(),
12752 [
12753 ExcerptRange {
12754 context: Point::new(0, 0)..Point::new(3, 0),
12755 primary: None,
12756 },
12757 ExcerptRange {
12758 context: Point::new(5, 0)..Point::new(7, 0),
12759 primary: None,
12760 },
12761 ExcerptRange {
12762 context: Point::new(9, 0)..Point::new(10, 4),
12763 primary: None,
12764 },
12765 ],
12766 cx,
12767 );
12768 multibuffer
12769 });
12770
12771 let fs = FakeFs::new(cx.executor());
12772 fs.insert_tree(
12773 "/a",
12774 json!({
12775 "main.rs": sample_text_1,
12776 "other.rs": sample_text_2,
12777 "lib.rs": sample_text_3,
12778 }),
12779 )
12780 .await;
12781 let project = Project::test(fs, ["/a".as_ref()], cx).await;
12782 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12783 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12784 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
12785 Editor::new(
12786 EditorMode::Full,
12787 multi_buffer,
12788 Some(project.clone()),
12789 true,
12790 window,
12791 cx,
12792 )
12793 });
12794 let multibuffer_item_id = workspace
12795 .update(cx, |workspace, window, cx| {
12796 assert!(
12797 workspace.active_item(cx).is_none(),
12798 "active item should be None before the first item is added"
12799 );
12800 workspace.add_item_to_active_pane(
12801 Box::new(multi_buffer_editor.clone()),
12802 None,
12803 true,
12804 window,
12805 cx,
12806 );
12807 let active_item = workspace
12808 .active_item(cx)
12809 .expect("should have an active item after adding the multi buffer");
12810 assert!(
12811 !active_item.is_singleton(cx),
12812 "A multi buffer was expected to active after adding"
12813 );
12814 active_item.item_id()
12815 })
12816 .unwrap();
12817 cx.executor().run_until_parked();
12818
12819 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12820 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12821 s.select_ranges(Some(1..2))
12822 });
12823 editor.open_excerpts(&OpenExcerpts, window, cx);
12824 });
12825 cx.executor().run_until_parked();
12826 let first_item_id = workspace
12827 .update(cx, |workspace, window, cx| {
12828 let active_item = workspace
12829 .active_item(cx)
12830 .expect("should have an active item after navigating into the 1st buffer");
12831 let first_item_id = active_item.item_id();
12832 assert_ne!(
12833 first_item_id, multibuffer_item_id,
12834 "Should navigate into the 1st buffer and activate it"
12835 );
12836 assert!(
12837 active_item.is_singleton(cx),
12838 "New active item should be a singleton buffer"
12839 );
12840 assert_eq!(
12841 active_item
12842 .act_as::<Editor>(cx)
12843 .expect("should have navigated into an editor for the 1st buffer")
12844 .read(cx)
12845 .text(cx),
12846 sample_text_1
12847 );
12848
12849 workspace
12850 .go_back(workspace.active_pane().downgrade(), window, cx)
12851 .detach_and_log_err(cx);
12852
12853 first_item_id
12854 })
12855 .unwrap();
12856 cx.executor().run_until_parked();
12857 workspace
12858 .update(cx, |workspace, _, cx| {
12859 let active_item = workspace
12860 .active_item(cx)
12861 .expect("should have an active item after navigating back");
12862 assert_eq!(
12863 active_item.item_id(),
12864 multibuffer_item_id,
12865 "Should navigate back to the multi buffer"
12866 );
12867 assert!(!active_item.is_singleton(cx));
12868 })
12869 .unwrap();
12870
12871 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12872 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12873 s.select_ranges(Some(39..40))
12874 });
12875 editor.open_excerpts(&OpenExcerpts, window, cx);
12876 });
12877 cx.executor().run_until_parked();
12878 let second_item_id = workspace
12879 .update(cx, |workspace, window, cx| {
12880 let active_item = workspace
12881 .active_item(cx)
12882 .expect("should have an active item after navigating into the 2nd buffer");
12883 let second_item_id = active_item.item_id();
12884 assert_ne!(
12885 second_item_id, multibuffer_item_id,
12886 "Should navigate away from the multibuffer"
12887 );
12888 assert_ne!(
12889 second_item_id, first_item_id,
12890 "Should navigate into the 2nd buffer and activate it"
12891 );
12892 assert!(
12893 active_item.is_singleton(cx),
12894 "New active item should be a singleton buffer"
12895 );
12896 assert_eq!(
12897 active_item
12898 .act_as::<Editor>(cx)
12899 .expect("should have navigated into an editor")
12900 .read(cx)
12901 .text(cx),
12902 sample_text_2
12903 );
12904
12905 workspace
12906 .go_back(workspace.active_pane().downgrade(), window, cx)
12907 .detach_and_log_err(cx);
12908
12909 second_item_id
12910 })
12911 .unwrap();
12912 cx.executor().run_until_parked();
12913 workspace
12914 .update(cx, |workspace, _, cx| {
12915 let active_item = workspace
12916 .active_item(cx)
12917 .expect("should have an active item after navigating back from the 2nd buffer");
12918 assert_eq!(
12919 active_item.item_id(),
12920 multibuffer_item_id,
12921 "Should navigate back from the 2nd buffer to the multi buffer"
12922 );
12923 assert!(!active_item.is_singleton(cx));
12924 })
12925 .unwrap();
12926
12927 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12928 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12929 s.select_ranges(Some(70..70))
12930 });
12931 editor.open_excerpts(&OpenExcerpts, window, cx);
12932 });
12933 cx.executor().run_until_parked();
12934 workspace
12935 .update(cx, |workspace, window, cx| {
12936 let active_item = workspace
12937 .active_item(cx)
12938 .expect("should have an active item after navigating into the 3rd buffer");
12939 let third_item_id = active_item.item_id();
12940 assert_ne!(
12941 third_item_id, multibuffer_item_id,
12942 "Should navigate into the 3rd buffer and activate it"
12943 );
12944 assert_ne!(third_item_id, first_item_id);
12945 assert_ne!(third_item_id, second_item_id);
12946 assert!(
12947 active_item.is_singleton(cx),
12948 "New active item should be a singleton buffer"
12949 );
12950 assert_eq!(
12951 active_item
12952 .act_as::<Editor>(cx)
12953 .expect("should have navigated into an editor")
12954 .read(cx)
12955 .text(cx),
12956 sample_text_3
12957 );
12958
12959 workspace
12960 .go_back(workspace.active_pane().downgrade(), window, cx)
12961 .detach_and_log_err(cx);
12962 })
12963 .unwrap();
12964 cx.executor().run_until_parked();
12965 workspace
12966 .update(cx, |workspace, _, cx| {
12967 let active_item = workspace
12968 .active_item(cx)
12969 .expect("should have an active item after navigating back from the 3rd buffer");
12970 assert_eq!(
12971 active_item.item_id(),
12972 multibuffer_item_id,
12973 "Should navigate back from the 3rd buffer to the multi buffer"
12974 );
12975 assert!(!active_item.is_singleton(cx));
12976 })
12977 .unwrap();
12978}
12979
12980#[gpui::test]
12981async fn test_toggle_selected_diff_hunks(
12982 executor: BackgroundExecutor,
12983 cx: &mut gpui::TestAppContext,
12984) {
12985 init_test(cx, |_| {});
12986
12987 let mut cx = EditorTestContext::new(cx).await;
12988
12989 let diff_base = r#"
12990 use some::mod;
12991
12992 const A: u32 = 42;
12993
12994 fn main() {
12995 println!("hello");
12996
12997 println!("world");
12998 }
12999 "#
13000 .unindent();
13001
13002 cx.set_state(
13003 &r#"
13004 use some::modified;
13005
13006 ˇ
13007 fn main() {
13008 println!("hello there");
13009
13010 println!("around the");
13011 println!("world");
13012 }
13013 "#
13014 .unindent(),
13015 );
13016
13017 cx.set_diff_base(&diff_base);
13018 executor.run_until_parked();
13019
13020 cx.update_editor(|editor, window, cx| {
13021 editor.go_to_next_hunk(&GoToHunk, window, cx);
13022 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13023 });
13024 executor.run_until_parked();
13025 cx.assert_state_with_diff(
13026 r#"
13027 use some::modified;
13028
13029
13030 fn main() {
13031 - println!("hello");
13032 + ˇ println!("hello there");
13033
13034 println!("around the");
13035 println!("world");
13036 }
13037 "#
13038 .unindent(),
13039 );
13040
13041 cx.update_editor(|editor, window, cx| {
13042 for _ in 0..2 {
13043 editor.go_to_next_hunk(&GoToHunk, window, cx);
13044 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13045 }
13046 });
13047 executor.run_until_parked();
13048 cx.assert_state_with_diff(
13049 r#"
13050 - use some::mod;
13051 + ˇuse some::modified;
13052
13053
13054 fn main() {
13055 - println!("hello");
13056 + println!("hello there");
13057
13058 + println!("around the");
13059 println!("world");
13060 }
13061 "#
13062 .unindent(),
13063 );
13064
13065 cx.update_editor(|editor, window, cx| {
13066 editor.go_to_next_hunk(&GoToHunk, window, cx);
13067 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13068 });
13069 executor.run_until_parked();
13070 cx.assert_state_with_diff(
13071 r#"
13072 - use some::mod;
13073 + use some::modified;
13074
13075 - const A: u32 = 42;
13076 ˇ
13077 fn main() {
13078 - println!("hello");
13079 + println!("hello there");
13080
13081 + println!("around the");
13082 println!("world");
13083 }
13084 "#
13085 .unindent(),
13086 );
13087
13088 cx.update_editor(|editor, window, cx| {
13089 editor.cancel(&Cancel, window, cx);
13090 });
13091
13092 cx.assert_state_with_diff(
13093 r#"
13094 use some::modified;
13095
13096 ˇ
13097 fn main() {
13098 println!("hello there");
13099
13100 println!("around the");
13101 println!("world");
13102 }
13103 "#
13104 .unindent(),
13105 );
13106}
13107
13108#[gpui::test]
13109async fn test_diff_base_change_with_expanded_diff_hunks(
13110 executor: BackgroundExecutor,
13111 cx: &mut gpui::TestAppContext,
13112) {
13113 init_test(cx, |_| {});
13114
13115 let mut cx = EditorTestContext::new(cx).await;
13116
13117 let diff_base = r#"
13118 use some::mod1;
13119 use some::mod2;
13120
13121 const A: u32 = 42;
13122 const B: u32 = 42;
13123 const C: u32 = 42;
13124
13125 fn main() {
13126 println!("hello");
13127
13128 println!("world");
13129 }
13130 "#
13131 .unindent();
13132
13133 cx.set_state(
13134 &r#"
13135 use some::mod2;
13136
13137 const A: u32 = 42;
13138 const C: u32 = 42;
13139
13140 fn main(ˇ) {
13141 //println!("hello");
13142
13143 println!("world");
13144 //
13145 //
13146 }
13147 "#
13148 .unindent(),
13149 );
13150
13151 cx.set_diff_base(&diff_base);
13152 executor.run_until_parked();
13153
13154 cx.update_editor(|editor, window, cx| {
13155 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13156 });
13157 executor.run_until_parked();
13158 cx.assert_state_with_diff(
13159 r#"
13160 - use some::mod1;
13161 use some::mod2;
13162
13163 const A: u32 = 42;
13164 - const B: u32 = 42;
13165 const C: u32 = 42;
13166
13167 fn main(ˇ) {
13168 - println!("hello");
13169 + //println!("hello");
13170
13171 println!("world");
13172 + //
13173 + //
13174 }
13175 "#
13176 .unindent(),
13177 );
13178
13179 cx.set_diff_base("new diff base!");
13180 executor.run_until_parked();
13181 cx.assert_state_with_diff(
13182 r#"
13183 use some::mod2;
13184
13185 const A: u32 = 42;
13186 const C: u32 = 42;
13187
13188 fn main(ˇ) {
13189 //println!("hello");
13190
13191 println!("world");
13192 //
13193 //
13194 }
13195 "#
13196 .unindent(),
13197 );
13198
13199 cx.update_editor(|editor, window, cx| {
13200 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13201 });
13202 executor.run_until_parked();
13203 cx.assert_state_with_diff(
13204 r#"
13205 - new diff base!
13206 + use some::mod2;
13207 +
13208 + const A: u32 = 42;
13209 + const C: u32 = 42;
13210 +
13211 + fn main(ˇ) {
13212 + //println!("hello");
13213 +
13214 + println!("world");
13215 + //
13216 + //
13217 + }
13218 "#
13219 .unindent(),
13220 );
13221}
13222
13223#[gpui::test]
13224async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) {
13225 init_test(cx, |_| {});
13226
13227 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
13228 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
13229 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
13230 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
13231 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
13232 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
13233
13234 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
13235 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
13236 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
13237
13238 let multi_buffer = cx.new(|cx| {
13239 let mut multibuffer = MultiBuffer::new(ReadWrite);
13240 multibuffer.push_excerpts(
13241 buffer_1.clone(),
13242 [
13243 ExcerptRange {
13244 context: Point::new(0, 0)..Point::new(3, 0),
13245 primary: None,
13246 },
13247 ExcerptRange {
13248 context: Point::new(5, 0)..Point::new(7, 0),
13249 primary: None,
13250 },
13251 ExcerptRange {
13252 context: Point::new(9, 0)..Point::new(10, 3),
13253 primary: None,
13254 },
13255 ],
13256 cx,
13257 );
13258 multibuffer.push_excerpts(
13259 buffer_2.clone(),
13260 [
13261 ExcerptRange {
13262 context: Point::new(0, 0)..Point::new(3, 0),
13263 primary: None,
13264 },
13265 ExcerptRange {
13266 context: Point::new(5, 0)..Point::new(7, 0),
13267 primary: None,
13268 },
13269 ExcerptRange {
13270 context: Point::new(9, 0)..Point::new(10, 3),
13271 primary: None,
13272 },
13273 ],
13274 cx,
13275 );
13276 multibuffer.push_excerpts(
13277 buffer_3.clone(),
13278 [
13279 ExcerptRange {
13280 context: Point::new(0, 0)..Point::new(3, 0),
13281 primary: None,
13282 },
13283 ExcerptRange {
13284 context: Point::new(5, 0)..Point::new(7, 0),
13285 primary: None,
13286 },
13287 ExcerptRange {
13288 context: Point::new(9, 0)..Point::new(10, 3),
13289 primary: None,
13290 },
13291 ],
13292 cx,
13293 );
13294 multibuffer
13295 });
13296
13297 let editor = cx.add_window(|window, cx| {
13298 Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
13299 });
13300 editor
13301 .update(cx, |editor, _window, cx| {
13302 for (buffer, diff_base) in [
13303 (buffer_1.clone(), file_1_old),
13304 (buffer_2.clone(), file_2_old),
13305 (buffer_3.clone(), file_3_old),
13306 ] {
13307 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
13308 editor
13309 .buffer
13310 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
13311 }
13312 })
13313 .unwrap();
13314
13315 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13316 cx.run_until_parked();
13317
13318 cx.assert_editor_state(
13319 &"
13320 ˇaaa
13321 ccc
13322 ddd
13323
13324 ggg
13325 hhh
13326
13327
13328 lll
13329 mmm
13330 NNN
13331
13332 qqq
13333 rrr
13334
13335 uuu
13336 111
13337 222
13338 333
13339
13340 666
13341 777
13342
13343 000
13344 !!!"
13345 .unindent(),
13346 );
13347
13348 cx.update_editor(|editor, window, cx| {
13349 editor.select_all(&SelectAll, window, cx);
13350 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13351 });
13352 cx.executor().run_until_parked();
13353
13354 cx.assert_state_with_diff(
13355 "
13356 «aaa
13357 - bbb
13358 ccc
13359 ddd
13360
13361 ggg
13362 hhh
13363
13364
13365 lll
13366 mmm
13367 - nnn
13368 + NNN
13369
13370 qqq
13371 rrr
13372
13373 uuu
13374 111
13375 222
13376 333
13377
13378 + 666
13379 777
13380
13381 000
13382 !!!ˇ»"
13383 .unindent(),
13384 );
13385}
13386
13387#[gpui::test]
13388async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext) {
13389 init_test(cx, |_| {});
13390
13391 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
13392 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
13393
13394 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
13395 let multi_buffer = cx.new(|cx| {
13396 let mut multibuffer = MultiBuffer::new(ReadWrite);
13397 multibuffer.push_excerpts(
13398 buffer.clone(),
13399 [
13400 ExcerptRange {
13401 context: Point::new(0, 0)..Point::new(2, 0),
13402 primary: None,
13403 },
13404 ExcerptRange {
13405 context: Point::new(4, 0)..Point::new(7, 0),
13406 primary: None,
13407 },
13408 ExcerptRange {
13409 context: Point::new(9, 0)..Point::new(10, 0),
13410 primary: None,
13411 },
13412 ],
13413 cx,
13414 );
13415 multibuffer
13416 });
13417
13418 let editor = cx.add_window(|window, cx| {
13419 Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
13420 });
13421 editor
13422 .update(cx, |editor, _window, cx| {
13423 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
13424 editor
13425 .buffer
13426 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
13427 })
13428 .unwrap();
13429
13430 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13431 cx.run_until_parked();
13432
13433 cx.update_editor(|editor, window, cx| {
13434 editor.expand_all_diff_hunks(&Default::default(), window, cx)
13435 });
13436 cx.executor().run_until_parked();
13437
13438 // When the start of a hunk coincides with the start of its excerpt,
13439 // the hunk is expanded. When the start of a a hunk is earlier than
13440 // the start of its excerpt, the hunk is not expanded.
13441 cx.assert_state_with_diff(
13442 "
13443 ˇaaa
13444 - bbb
13445 + BBB
13446
13447 - ddd
13448 - eee
13449 + DDD
13450 + EEE
13451 fff
13452
13453 iii
13454 "
13455 .unindent(),
13456 );
13457}
13458
13459#[gpui::test]
13460async fn test_edits_around_expanded_insertion_hunks(
13461 executor: BackgroundExecutor,
13462 cx: &mut gpui::TestAppContext,
13463) {
13464 init_test(cx, |_| {});
13465
13466 let mut cx = EditorTestContext::new(cx).await;
13467
13468 let diff_base = r#"
13469 use some::mod1;
13470 use some::mod2;
13471
13472 const A: u32 = 42;
13473
13474 fn main() {
13475 println!("hello");
13476
13477 println!("world");
13478 }
13479 "#
13480 .unindent();
13481 executor.run_until_parked();
13482 cx.set_state(
13483 &r#"
13484 use some::mod1;
13485 use some::mod2;
13486
13487 const A: u32 = 42;
13488 const B: u32 = 42;
13489 const C: u32 = 42;
13490 ˇ
13491
13492 fn main() {
13493 println!("hello");
13494
13495 println!("world");
13496 }
13497 "#
13498 .unindent(),
13499 );
13500
13501 cx.set_diff_base(&diff_base);
13502 executor.run_until_parked();
13503
13504 cx.update_editor(|editor, window, cx| {
13505 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13506 });
13507 executor.run_until_parked();
13508
13509 cx.assert_state_with_diff(
13510 r#"
13511 use some::mod1;
13512 use some::mod2;
13513
13514 const A: u32 = 42;
13515 + const B: u32 = 42;
13516 + const C: u32 = 42;
13517 + ˇ
13518
13519 fn main() {
13520 println!("hello");
13521
13522 println!("world");
13523 }
13524 "#
13525 .unindent(),
13526 );
13527
13528 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
13529 executor.run_until_parked();
13530
13531 cx.assert_state_with_diff(
13532 r#"
13533 use some::mod1;
13534 use some::mod2;
13535
13536 const A: u32 = 42;
13537 + const B: u32 = 42;
13538 + const C: u32 = 42;
13539 + const D: u32 = 42;
13540 + ˇ
13541
13542 fn main() {
13543 println!("hello");
13544
13545 println!("world");
13546 }
13547 "#
13548 .unindent(),
13549 );
13550
13551 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
13552 executor.run_until_parked();
13553
13554 cx.assert_state_with_diff(
13555 r#"
13556 use some::mod1;
13557 use some::mod2;
13558
13559 const A: u32 = 42;
13560 + const B: u32 = 42;
13561 + const C: u32 = 42;
13562 + const D: u32 = 42;
13563 + const E: u32 = 42;
13564 + ˇ
13565
13566 fn main() {
13567 println!("hello");
13568
13569 println!("world");
13570 }
13571 "#
13572 .unindent(),
13573 );
13574
13575 cx.update_editor(|editor, window, cx| {
13576 editor.delete_line(&DeleteLine, window, cx);
13577 });
13578 executor.run_until_parked();
13579
13580 cx.assert_state_with_diff(
13581 r#"
13582 use some::mod1;
13583 use some::mod2;
13584
13585 const A: u32 = 42;
13586 + const B: u32 = 42;
13587 + const C: u32 = 42;
13588 + const D: u32 = 42;
13589 + const E: u32 = 42;
13590 ˇ
13591 fn main() {
13592 println!("hello");
13593
13594 println!("world");
13595 }
13596 "#
13597 .unindent(),
13598 );
13599
13600 cx.update_editor(|editor, window, cx| {
13601 editor.move_up(&MoveUp, window, cx);
13602 editor.delete_line(&DeleteLine, window, cx);
13603 editor.move_up(&MoveUp, window, cx);
13604 editor.delete_line(&DeleteLine, window, cx);
13605 editor.move_up(&MoveUp, window, cx);
13606 editor.delete_line(&DeleteLine, window, cx);
13607 });
13608 executor.run_until_parked();
13609 cx.assert_state_with_diff(
13610 r#"
13611 use some::mod1;
13612 use some::mod2;
13613
13614 const A: u32 = 42;
13615 + const B: u32 = 42;
13616 ˇ
13617 fn main() {
13618 println!("hello");
13619
13620 println!("world");
13621 }
13622 "#
13623 .unindent(),
13624 );
13625
13626 cx.update_editor(|editor, window, cx| {
13627 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
13628 editor.delete_line(&DeleteLine, window, cx);
13629 });
13630 executor.run_until_parked();
13631 cx.assert_state_with_diff(
13632 r#"
13633 ˇ
13634 fn main() {
13635 println!("hello");
13636
13637 println!("world");
13638 }
13639 "#
13640 .unindent(),
13641 );
13642}
13643
13644#[gpui::test]
13645async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
13646 init_test(cx, |_| {});
13647
13648 let mut cx = EditorTestContext::new(cx).await;
13649 cx.set_diff_base(indoc! { "
13650 one
13651 two
13652 three
13653 four
13654 five
13655 "
13656 });
13657 cx.set_state(indoc! { "
13658 one
13659 ˇthree
13660 five
13661 "});
13662 cx.run_until_parked();
13663 cx.update_editor(|editor, window, cx| {
13664 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13665 });
13666 cx.assert_state_with_diff(
13667 indoc! { "
13668 one
13669 - two
13670 ˇthree
13671 - four
13672 five
13673 "}
13674 .to_string(),
13675 );
13676 cx.update_editor(|editor, window, cx| {
13677 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13678 });
13679
13680 cx.assert_state_with_diff(
13681 indoc! { "
13682 one
13683 ˇthree
13684 five
13685 "}
13686 .to_string(),
13687 );
13688
13689 cx.set_state(indoc! { "
13690 one
13691 ˇTWO
13692 three
13693 four
13694 five
13695 "});
13696 cx.run_until_parked();
13697 cx.update_editor(|editor, window, cx| {
13698 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13699 });
13700
13701 cx.assert_state_with_diff(
13702 indoc! { "
13703 one
13704 - two
13705 + ˇTWO
13706 three
13707 four
13708 five
13709 "}
13710 .to_string(),
13711 );
13712 cx.update_editor(|editor, window, cx| {
13713 editor.move_up(&Default::default(), window, cx);
13714 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13715 });
13716 cx.assert_state_with_diff(
13717 indoc! { "
13718 one
13719 ˇTWO
13720 three
13721 four
13722 five
13723 "}
13724 .to_string(),
13725 );
13726}
13727
13728#[gpui::test]
13729async fn test_edits_around_expanded_deletion_hunks(
13730 executor: BackgroundExecutor,
13731 cx: &mut gpui::TestAppContext,
13732) {
13733 init_test(cx, |_| {});
13734
13735 let mut cx = EditorTestContext::new(cx).await;
13736
13737 let diff_base = r#"
13738 use some::mod1;
13739 use some::mod2;
13740
13741 const A: u32 = 42;
13742 const B: u32 = 42;
13743 const C: u32 = 42;
13744
13745
13746 fn main() {
13747 println!("hello");
13748
13749 println!("world");
13750 }
13751 "#
13752 .unindent();
13753 executor.run_until_parked();
13754 cx.set_state(
13755 &r#"
13756 use some::mod1;
13757 use some::mod2;
13758
13759 ˇconst B: u32 = 42;
13760 const C: u32 = 42;
13761
13762
13763 fn main() {
13764 println!("hello");
13765
13766 println!("world");
13767 }
13768 "#
13769 .unindent(),
13770 );
13771
13772 cx.set_diff_base(&diff_base);
13773 executor.run_until_parked();
13774
13775 cx.update_editor(|editor, window, cx| {
13776 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13777 });
13778 executor.run_until_parked();
13779
13780 cx.assert_state_with_diff(
13781 r#"
13782 use some::mod1;
13783 use some::mod2;
13784
13785 - const A: u32 = 42;
13786 ˇconst B: u32 = 42;
13787 const C: u32 = 42;
13788
13789
13790 fn main() {
13791 println!("hello");
13792
13793 println!("world");
13794 }
13795 "#
13796 .unindent(),
13797 );
13798
13799 cx.update_editor(|editor, window, cx| {
13800 editor.delete_line(&DeleteLine, window, cx);
13801 });
13802 executor.run_until_parked();
13803 cx.assert_state_with_diff(
13804 r#"
13805 use some::mod1;
13806 use some::mod2;
13807
13808 - const A: u32 = 42;
13809 - const B: u32 = 42;
13810 ˇconst C: u32 = 42;
13811
13812
13813 fn main() {
13814 println!("hello");
13815
13816 println!("world");
13817 }
13818 "#
13819 .unindent(),
13820 );
13821
13822 cx.update_editor(|editor, window, cx| {
13823 editor.delete_line(&DeleteLine, window, cx);
13824 });
13825 executor.run_until_parked();
13826 cx.assert_state_with_diff(
13827 r#"
13828 use some::mod1;
13829 use some::mod2;
13830
13831 - const A: u32 = 42;
13832 - const B: u32 = 42;
13833 - const C: u32 = 42;
13834 ˇ
13835
13836 fn main() {
13837 println!("hello");
13838
13839 println!("world");
13840 }
13841 "#
13842 .unindent(),
13843 );
13844
13845 cx.update_editor(|editor, window, cx| {
13846 editor.handle_input("replacement", window, cx);
13847 });
13848 executor.run_until_parked();
13849 cx.assert_state_with_diff(
13850 r#"
13851 use some::mod1;
13852 use some::mod2;
13853
13854 - const A: u32 = 42;
13855 - const B: u32 = 42;
13856 - const C: u32 = 42;
13857 -
13858 + replacementˇ
13859
13860 fn main() {
13861 println!("hello");
13862
13863 println!("world");
13864 }
13865 "#
13866 .unindent(),
13867 );
13868}
13869
13870#[gpui::test]
13871async fn test_backspace_after_deletion_hunk(
13872 executor: BackgroundExecutor,
13873 cx: &mut gpui::TestAppContext,
13874) {
13875 init_test(cx, |_| {});
13876
13877 let mut cx = EditorTestContext::new(cx).await;
13878
13879 let base_text = r#"
13880 one
13881 two
13882 three
13883 four
13884 five
13885 "#
13886 .unindent();
13887 executor.run_until_parked();
13888 cx.set_state(
13889 &r#"
13890 one
13891 two
13892 fˇour
13893 five
13894 "#
13895 .unindent(),
13896 );
13897
13898 cx.set_diff_base(&base_text);
13899 executor.run_until_parked();
13900
13901 cx.update_editor(|editor, window, cx| {
13902 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13903 });
13904 executor.run_until_parked();
13905
13906 cx.assert_state_with_diff(
13907 r#"
13908 one
13909 two
13910 - three
13911 fˇour
13912 five
13913 "#
13914 .unindent(),
13915 );
13916
13917 cx.update_editor(|editor, window, cx| {
13918 editor.backspace(&Backspace, window, cx);
13919 editor.backspace(&Backspace, window, cx);
13920 });
13921 executor.run_until_parked();
13922 cx.assert_state_with_diff(
13923 r#"
13924 one
13925 two
13926 - threeˇ
13927 - four
13928 + our
13929 five
13930 "#
13931 .unindent(),
13932 );
13933}
13934
13935#[gpui::test]
13936async fn test_edit_after_expanded_modification_hunk(
13937 executor: BackgroundExecutor,
13938 cx: &mut gpui::TestAppContext,
13939) {
13940 init_test(cx, |_| {});
13941
13942 let mut cx = EditorTestContext::new(cx).await;
13943
13944 let diff_base = r#"
13945 use some::mod1;
13946 use some::mod2;
13947
13948 const A: u32 = 42;
13949 const B: u32 = 42;
13950 const C: u32 = 42;
13951 const D: u32 = 42;
13952
13953
13954 fn main() {
13955 println!("hello");
13956
13957 println!("world");
13958 }"#
13959 .unindent();
13960
13961 cx.set_state(
13962 &r#"
13963 use some::mod1;
13964 use some::mod2;
13965
13966 const A: u32 = 42;
13967 const B: u32 = 42;
13968 const C: u32 = 43ˇ
13969 const D: u32 = 42;
13970
13971
13972 fn main() {
13973 println!("hello");
13974
13975 println!("world");
13976 }"#
13977 .unindent(),
13978 );
13979
13980 cx.set_diff_base(&diff_base);
13981 executor.run_until_parked();
13982 cx.update_editor(|editor, window, cx| {
13983 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13984 });
13985 executor.run_until_parked();
13986
13987 cx.assert_state_with_diff(
13988 r#"
13989 use some::mod1;
13990 use some::mod2;
13991
13992 const A: u32 = 42;
13993 const B: u32 = 42;
13994 - const C: u32 = 42;
13995 + const C: u32 = 43ˇ
13996 const D: u32 = 42;
13997
13998
13999 fn main() {
14000 println!("hello");
14001
14002 println!("world");
14003 }"#
14004 .unindent(),
14005 );
14006
14007 cx.update_editor(|editor, window, cx| {
14008 editor.handle_input("\nnew_line\n", window, cx);
14009 });
14010 executor.run_until_parked();
14011
14012 cx.assert_state_with_diff(
14013 r#"
14014 use some::mod1;
14015 use some::mod2;
14016
14017 const A: u32 = 42;
14018 const B: u32 = 42;
14019 - const C: u32 = 42;
14020 + const C: u32 = 43
14021 + new_line
14022 + ˇ
14023 const D: u32 = 42;
14024
14025
14026 fn main() {
14027 println!("hello");
14028
14029 println!("world");
14030 }"#
14031 .unindent(),
14032 );
14033}
14034
14035async fn setup_indent_guides_editor(
14036 text: &str,
14037 cx: &mut gpui::TestAppContext,
14038) -> (BufferId, EditorTestContext) {
14039 init_test(cx, |_| {});
14040
14041 let mut cx = EditorTestContext::new(cx).await;
14042
14043 let buffer_id = cx.update_editor(|editor, window, cx| {
14044 editor.set_text(text, window, cx);
14045 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
14046
14047 buffer_ids[0]
14048 });
14049
14050 (buffer_id, cx)
14051}
14052
14053fn assert_indent_guides(
14054 range: Range<u32>,
14055 expected: Vec<IndentGuide>,
14056 active_indices: Option<Vec<usize>>,
14057 cx: &mut EditorTestContext,
14058) {
14059 let indent_guides = cx.update_editor(|editor, window, cx| {
14060 let snapshot = editor.snapshot(window, cx).display_snapshot;
14061 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
14062 editor,
14063 MultiBufferRow(range.start)..MultiBufferRow(range.end),
14064 true,
14065 &snapshot,
14066 cx,
14067 );
14068
14069 indent_guides.sort_by(|a, b| {
14070 a.depth.cmp(&b.depth).then(
14071 a.start_row
14072 .cmp(&b.start_row)
14073 .then(a.end_row.cmp(&b.end_row)),
14074 )
14075 });
14076 indent_guides
14077 });
14078
14079 if let Some(expected) = active_indices {
14080 let active_indices = cx.update_editor(|editor, window, cx| {
14081 let snapshot = editor.snapshot(window, cx).display_snapshot;
14082 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
14083 });
14084
14085 assert_eq!(
14086 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
14087 expected,
14088 "Active indent guide indices do not match"
14089 );
14090 }
14091
14092 assert_eq!(indent_guides, expected, "Indent guides do not match");
14093}
14094
14095fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
14096 IndentGuide {
14097 buffer_id,
14098 start_row: MultiBufferRow(start_row),
14099 end_row: MultiBufferRow(end_row),
14100 depth,
14101 tab_size: 4,
14102 settings: IndentGuideSettings {
14103 enabled: true,
14104 line_width: 1,
14105 active_line_width: 1,
14106 ..Default::default()
14107 },
14108 }
14109}
14110
14111#[gpui::test]
14112async fn test_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
14113 let (buffer_id, mut cx) = setup_indent_guides_editor(
14114 &"
14115 fn main() {
14116 let a = 1;
14117 }"
14118 .unindent(),
14119 cx,
14120 )
14121 .await;
14122
14123 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
14124}
14125
14126#[gpui::test]
14127async fn test_indent_guide_simple_block(cx: &mut gpui::TestAppContext) {
14128 let (buffer_id, mut cx) = setup_indent_guides_editor(
14129 &"
14130 fn main() {
14131 let a = 1;
14132 let b = 2;
14133 }"
14134 .unindent(),
14135 cx,
14136 )
14137 .await;
14138
14139 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
14140}
14141
14142#[gpui::test]
14143async fn test_indent_guide_nested(cx: &mut gpui::TestAppContext) {
14144 let (buffer_id, mut cx) = setup_indent_guides_editor(
14145 &"
14146 fn main() {
14147 let a = 1;
14148 if a == 3 {
14149 let b = 2;
14150 } else {
14151 let c = 3;
14152 }
14153 }"
14154 .unindent(),
14155 cx,
14156 )
14157 .await;
14158
14159 assert_indent_guides(
14160 0..8,
14161 vec![
14162 indent_guide(buffer_id, 1, 6, 0),
14163 indent_guide(buffer_id, 3, 3, 1),
14164 indent_guide(buffer_id, 5, 5, 1),
14165 ],
14166 None,
14167 &mut cx,
14168 );
14169}
14170
14171#[gpui::test]
14172async fn test_indent_guide_tab(cx: &mut gpui::TestAppContext) {
14173 let (buffer_id, mut cx) = setup_indent_guides_editor(
14174 &"
14175 fn main() {
14176 let a = 1;
14177 let b = 2;
14178 let c = 3;
14179 }"
14180 .unindent(),
14181 cx,
14182 )
14183 .await;
14184
14185 assert_indent_guides(
14186 0..5,
14187 vec![
14188 indent_guide(buffer_id, 1, 3, 0),
14189 indent_guide(buffer_id, 2, 2, 1),
14190 ],
14191 None,
14192 &mut cx,
14193 );
14194}
14195
14196#[gpui::test]
14197async fn test_indent_guide_continues_on_empty_line(cx: &mut gpui::TestAppContext) {
14198 let (buffer_id, mut cx) = setup_indent_guides_editor(
14199 &"
14200 fn main() {
14201 let a = 1;
14202
14203 let c = 3;
14204 }"
14205 .unindent(),
14206 cx,
14207 )
14208 .await;
14209
14210 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
14211}
14212
14213#[gpui::test]
14214async fn test_indent_guide_complex(cx: &mut gpui::TestAppContext) {
14215 let (buffer_id, mut cx) = setup_indent_guides_editor(
14216 &"
14217 fn main() {
14218 let a = 1;
14219
14220 let c = 3;
14221
14222 if a == 3 {
14223 let b = 2;
14224 } else {
14225 let c = 3;
14226 }
14227 }"
14228 .unindent(),
14229 cx,
14230 )
14231 .await;
14232
14233 assert_indent_guides(
14234 0..11,
14235 vec![
14236 indent_guide(buffer_id, 1, 9, 0),
14237 indent_guide(buffer_id, 6, 6, 1),
14238 indent_guide(buffer_id, 8, 8, 1),
14239 ],
14240 None,
14241 &mut cx,
14242 );
14243}
14244
14245#[gpui::test]
14246async fn test_indent_guide_starts_off_screen(cx: &mut gpui::TestAppContext) {
14247 let (buffer_id, mut cx) = setup_indent_guides_editor(
14248 &"
14249 fn main() {
14250 let a = 1;
14251
14252 let c = 3;
14253
14254 if a == 3 {
14255 let b = 2;
14256 } else {
14257 let c = 3;
14258 }
14259 }"
14260 .unindent(),
14261 cx,
14262 )
14263 .await;
14264
14265 assert_indent_guides(
14266 1..11,
14267 vec![
14268 indent_guide(buffer_id, 1, 9, 0),
14269 indent_guide(buffer_id, 6, 6, 1),
14270 indent_guide(buffer_id, 8, 8, 1),
14271 ],
14272 None,
14273 &mut cx,
14274 );
14275}
14276
14277#[gpui::test]
14278async fn test_indent_guide_ends_off_screen(cx: &mut gpui::TestAppContext) {
14279 let (buffer_id, mut cx) = setup_indent_guides_editor(
14280 &"
14281 fn main() {
14282 let a = 1;
14283
14284 let c = 3;
14285
14286 if a == 3 {
14287 let b = 2;
14288 } else {
14289 let c = 3;
14290 }
14291 }"
14292 .unindent(),
14293 cx,
14294 )
14295 .await;
14296
14297 assert_indent_guides(
14298 1..10,
14299 vec![
14300 indent_guide(buffer_id, 1, 9, 0),
14301 indent_guide(buffer_id, 6, 6, 1),
14302 indent_guide(buffer_id, 8, 8, 1),
14303 ],
14304 None,
14305 &mut cx,
14306 );
14307}
14308
14309#[gpui::test]
14310async fn test_indent_guide_without_brackets(cx: &mut gpui::TestAppContext) {
14311 let (buffer_id, mut cx) = setup_indent_guides_editor(
14312 &"
14313 block1
14314 block2
14315 block3
14316 block4
14317 block2
14318 block1
14319 block1"
14320 .unindent(),
14321 cx,
14322 )
14323 .await;
14324
14325 assert_indent_guides(
14326 1..10,
14327 vec![
14328 indent_guide(buffer_id, 1, 4, 0),
14329 indent_guide(buffer_id, 2, 3, 1),
14330 indent_guide(buffer_id, 3, 3, 2),
14331 ],
14332 None,
14333 &mut cx,
14334 );
14335}
14336
14337#[gpui::test]
14338async fn test_indent_guide_ends_before_empty_line(cx: &mut gpui::TestAppContext) {
14339 let (buffer_id, mut cx) = setup_indent_guides_editor(
14340 &"
14341 block1
14342 block2
14343 block3
14344
14345 block1
14346 block1"
14347 .unindent(),
14348 cx,
14349 )
14350 .await;
14351
14352 assert_indent_guides(
14353 0..6,
14354 vec![
14355 indent_guide(buffer_id, 1, 2, 0),
14356 indent_guide(buffer_id, 2, 2, 1),
14357 ],
14358 None,
14359 &mut cx,
14360 );
14361}
14362
14363#[gpui::test]
14364async fn test_indent_guide_continuing_off_screen(cx: &mut gpui::TestAppContext) {
14365 let (buffer_id, mut cx) = setup_indent_guides_editor(
14366 &"
14367 block1
14368
14369
14370
14371 block2
14372 "
14373 .unindent(),
14374 cx,
14375 )
14376 .await;
14377
14378 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
14379}
14380
14381#[gpui::test]
14382async fn test_indent_guide_tabs(cx: &mut gpui::TestAppContext) {
14383 let (buffer_id, mut cx) = setup_indent_guides_editor(
14384 &"
14385 def a:
14386 \tb = 3
14387 \tif True:
14388 \t\tc = 4
14389 \t\td = 5
14390 \tprint(b)
14391 "
14392 .unindent(),
14393 cx,
14394 )
14395 .await;
14396
14397 assert_indent_guides(
14398 0..6,
14399 vec![
14400 indent_guide(buffer_id, 1, 6, 0),
14401 indent_guide(buffer_id, 3, 4, 1),
14402 ],
14403 None,
14404 &mut cx,
14405 );
14406}
14407
14408#[gpui::test]
14409async fn test_active_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
14410 let (buffer_id, mut cx) = setup_indent_guides_editor(
14411 &"
14412 fn main() {
14413 let a = 1;
14414 }"
14415 .unindent(),
14416 cx,
14417 )
14418 .await;
14419
14420 cx.update_editor(|editor, window, cx| {
14421 editor.change_selections(None, window, cx, |s| {
14422 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14423 });
14424 });
14425
14426 assert_indent_guides(
14427 0..3,
14428 vec![indent_guide(buffer_id, 1, 1, 0)],
14429 Some(vec![0]),
14430 &mut cx,
14431 );
14432}
14433
14434#[gpui::test]
14435async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppContext) {
14436 let (buffer_id, mut cx) = setup_indent_guides_editor(
14437 &"
14438 fn main() {
14439 if 1 == 2 {
14440 let a = 1;
14441 }
14442 }"
14443 .unindent(),
14444 cx,
14445 )
14446 .await;
14447
14448 cx.update_editor(|editor, window, cx| {
14449 editor.change_selections(None, window, cx, |s| {
14450 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14451 });
14452 });
14453
14454 assert_indent_guides(
14455 0..4,
14456 vec![
14457 indent_guide(buffer_id, 1, 3, 0),
14458 indent_guide(buffer_id, 2, 2, 1),
14459 ],
14460 Some(vec![1]),
14461 &mut cx,
14462 );
14463
14464 cx.update_editor(|editor, window, cx| {
14465 editor.change_selections(None, window, cx, |s| {
14466 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
14467 });
14468 });
14469
14470 assert_indent_guides(
14471 0..4,
14472 vec![
14473 indent_guide(buffer_id, 1, 3, 0),
14474 indent_guide(buffer_id, 2, 2, 1),
14475 ],
14476 Some(vec![1]),
14477 &mut cx,
14478 );
14479
14480 cx.update_editor(|editor, window, cx| {
14481 editor.change_selections(None, window, cx, |s| {
14482 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
14483 });
14484 });
14485
14486 assert_indent_guides(
14487 0..4,
14488 vec![
14489 indent_guide(buffer_id, 1, 3, 0),
14490 indent_guide(buffer_id, 2, 2, 1),
14491 ],
14492 Some(vec![0]),
14493 &mut cx,
14494 );
14495}
14496
14497#[gpui::test]
14498async fn test_active_indent_guide_empty_line(cx: &mut gpui::TestAppContext) {
14499 let (buffer_id, mut cx) = setup_indent_guides_editor(
14500 &"
14501 fn main() {
14502 let a = 1;
14503
14504 let b = 2;
14505 }"
14506 .unindent(),
14507 cx,
14508 )
14509 .await;
14510
14511 cx.update_editor(|editor, window, cx| {
14512 editor.change_selections(None, window, cx, |s| {
14513 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
14514 });
14515 });
14516
14517 assert_indent_guides(
14518 0..5,
14519 vec![indent_guide(buffer_id, 1, 3, 0)],
14520 Some(vec![0]),
14521 &mut cx,
14522 );
14523}
14524
14525#[gpui::test]
14526async fn test_active_indent_guide_non_matching_indent(cx: &mut gpui::TestAppContext) {
14527 let (buffer_id, mut cx) = setup_indent_guides_editor(
14528 &"
14529 def m:
14530 a = 1
14531 pass"
14532 .unindent(),
14533 cx,
14534 )
14535 .await;
14536
14537 cx.update_editor(|editor, window, cx| {
14538 editor.change_selections(None, window, cx, |s| {
14539 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14540 });
14541 });
14542
14543 assert_indent_guides(
14544 0..3,
14545 vec![indent_guide(buffer_id, 1, 2, 0)],
14546 Some(vec![0]),
14547 &mut cx,
14548 );
14549}
14550
14551#[gpui::test]
14552async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut gpui::TestAppContext) {
14553 init_test(cx, |_| {});
14554 let mut cx = EditorTestContext::new(cx).await;
14555 let text = indoc! {
14556 "
14557 impl A {
14558 fn b() {
14559 0;
14560 3;
14561 5;
14562 6;
14563 7;
14564 }
14565 }
14566 "
14567 };
14568 let base_text = indoc! {
14569 "
14570 impl A {
14571 fn b() {
14572 0;
14573 1;
14574 2;
14575 3;
14576 4;
14577 }
14578 fn c() {
14579 5;
14580 6;
14581 7;
14582 }
14583 }
14584 "
14585 };
14586
14587 cx.update_editor(|editor, window, cx| {
14588 editor.set_text(text, window, cx);
14589
14590 editor.buffer().update(cx, |multibuffer, cx| {
14591 let buffer = multibuffer.as_singleton().unwrap();
14592 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
14593
14594 multibuffer.set_all_diff_hunks_expanded(cx);
14595 multibuffer.add_diff(diff, cx);
14596
14597 buffer.read(cx).remote_id()
14598 })
14599 });
14600 cx.run_until_parked();
14601
14602 cx.assert_state_with_diff(
14603 indoc! { "
14604 impl A {
14605 fn b() {
14606 0;
14607 - 1;
14608 - 2;
14609 3;
14610 - 4;
14611 - }
14612 - fn c() {
14613 5;
14614 6;
14615 7;
14616 }
14617 }
14618 ˇ"
14619 }
14620 .to_string(),
14621 );
14622
14623 let mut actual_guides = cx.update_editor(|editor, window, cx| {
14624 editor
14625 .snapshot(window, cx)
14626 .buffer_snapshot
14627 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
14628 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
14629 .collect::<Vec<_>>()
14630 });
14631 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
14632 assert_eq!(
14633 actual_guides,
14634 vec![
14635 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
14636 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
14637 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
14638 ]
14639 );
14640}
14641
14642#[gpui::test]
14643fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
14644 init_test(cx, |_| {});
14645
14646 let editor = cx.add_window(|window, cx| {
14647 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
14648 build_editor(buffer, window, cx)
14649 });
14650
14651 let render_args = Arc::new(Mutex::new(None));
14652 let snapshot = editor
14653 .update(cx, |editor, window, cx| {
14654 let snapshot = editor.buffer().read(cx).snapshot(cx);
14655 let range =
14656 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
14657
14658 struct RenderArgs {
14659 row: MultiBufferRow,
14660 folded: bool,
14661 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
14662 }
14663
14664 let crease = Crease::inline(
14665 range,
14666 FoldPlaceholder::test(),
14667 {
14668 let toggle_callback = render_args.clone();
14669 move |row, folded, callback, _window, _cx| {
14670 *toggle_callback.lock() = Some(RenderArgs {
14671 row,
14672 folded,
14673 callback,
14674 });
14675 div()
14676 }
14677 },
14678 |_row, _folded, _window, _cx| div(),
14679 );
14680
14681 editor.insert_creases(Some(crease), cx);
14682 let snapshot = editor.snapshot(window, cx);
14683 let _div = snapshot.render_crease_toggle(
14684 MultiBufferRow(1),
14685 false,
14686 cx.entity().clone(),
14687 window,
14688 cx,
14689 );
14690 snapshot
14691 })
14692 .unwrap();
14693
14694 let render_args = render_args.lock().take().unwrap();
14695 assert_eq!(render_args.row, MultiBufferRow(1));
14696 assert!(!render_args.folded);
14697 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
14698
14699 cx.update_window(*editor, |_, window, cx| {
14700 (render_args.callback)(true, window, cx)
14701 })
14702 .unwrap();
14703 let snapshot = editor
14704 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
14705 .unwrap();
14706 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
14707
14708 cx.update_window(*editor, |_, window, cx| {
14709 (render_args.callback)(false, window, cx)
14710 })
14711 .unwrap();
14712 let snapshot = editor
14713 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
14714 .unwrap();
14715 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
14716}
14717
14718#[gpui::test]
14719async fn test_input_text(cx: &mut gpui::TestAppContext) {
14720 init_test(cx, |_| {});
14721 let mut cx = EditorTestContext::new(cx).await;
14722
14723 cx.set_state(
14724 &r#"ˇone
14725 two
14726
14727 three
14728 fourˇ
14729 five
14730
14731 siˇx"#
14732 .unindent(),
14733 );
14734
14735 cx.dispatch_action(HandleInput(String::new()));
14736 cx.assert_editor_state(
14737 &r#"ˇone
14738 two
14739
14740 three
14741 fourˇ
14742 five
14743
14744 siˇx"#
14745 .unindent(),
14746 );
14747
14748 cx.dispatch_action(HandleInput("AAAA".to_string()));
14749 cx.assert_editor_state(
14750 &r#"AAAAˇone
14751 two
14752
14753 three
14754 fourAAAAˇ
14755 five
14756
14757 siAAAAˇx"#
14758 .unindent(),
14759 );
14760}
14761
14762#[gpui::test]
14763async fn test_scroll_cursor_center_top_bottom(cx: &mut gpui::TestAppContext) {
14764 init_test(cx, |_| {});
14765
14766 let mut cx = EditorTestContext::new(cx).await;
14767 cx.set_state(
14768 r#"let foo = 1;
14769let foo = 2;
14770let foo = 3;
14771let fooˇ = 4;
14772let foo = 5;
14773let foo = 6;
14774let foo = 7;
14775let foo = 8;
14776let foo = 9;
14777let foo = 10;
14778let foo = 11;
14779let foo = 12;
14780let foo = 13;
14781let foo = 14;
14782let foo = 15;"#,
14783 );
14784
14785 cx.update_editor(|e, window, cx| {
14786 assert_eq!(
14787 e.next_scroll_position,
14788 NextScrollCursorCenterTopBottom::Center,
14789 "Default next scroll direction is center",
14790 );
14791
14792 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14793 assert_eq!(
14794 e.next_scroll_position,
14795 NextScrollCursorCenterTopBottom::Top,
14796 "After center, next scroll direction should be top",
14797 );
14798
14799 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14800 assert_eq!(
14801 e.next_scroll_position,
14802 NextScrollCursorCenterTopBottom::Bottom,
14803 "After top, next scroll direction should be bottom",
14804 );
14805
14806 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14807 assert_eq!(
14808 e.next_scroll_position,
14809 NextScrollCursorCenterTopBottom::Center,
14810 "After bottom, scrolling should start over",
14811 );
14812
14813 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14814 assert_eq!(
14815 e.next_scroll_position,
14816 NextScrollCursorCenterTopBottom::Top,
14817 "Scrolling continues if retriggered fast enough"
14818 );
14819 });
14820
14821 cx.executor()
14822 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
14823 cx.executor().run_until_parked();
14824 cx.update_editor(|e, _, _| {
14825 assert_eq!(
14826 e.next_scroll_position,
14827 NextScrollCursorCenterTopBottom::Center,
14828 "If scrolling is not triggered fast enough, it should reset"
14829 );
14830 });
14831}
14832
14833#[gpui::test]
14834async fn test_goto_definition_with_find_all_references_fallback(cx: &mut gpui::TestAppContext) {
14835 init_test(cx, |_| {});
14836 let mut cx = EditorLspTestContext::new_rust(
14837 lsp::ServerCapabilities {
14838 definition_provider: Some(lsp::OneOf::Left(true)),
14839 references_provider: Some(lsp::OneOf::Left(true)),
14840 ..lsp::ServerCapabilities::default()
14841 },
14842 cx,
14843 )
14844 .await;
14845
14846 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
14847 let go_to_definition = cx.lsp.handle_request::<lsp::request::GotoDefinition, _, _>(
14848 move |params, _| async move {
14849 if empty_go_to_definition {
14850 Ok(None)
14851 } else {
14852 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
14853 uri: params.text_document_position_params.text_document.uri,
14854 range: lsp::Range::new(lsp::Position::new(4, 3), lsp::Position::new(4, 6)),
14855 })))
14856 }
14857 },
14858 );
14859 let references =
14860 cx.lsp
14861 .handle_request::<lsp::request::References, _, _>(move |params, _| async move {
14862 Ok(Some(vec![lsp::Location {
14863 uri: params.text_document_position.text_document.uri,
14864 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
14865 }]))
14866 });
14867 (go_to_definition, references)
14868 };
14869
14870 cx.set_state(
14871 &r#"fn one() {
14872 let mut a = ˇtwo();
14873 }
14874
14875 fn two() {}"#
14876 .unindent(),
14877 );
14878 set_up_lsp_handlers(false, &mut cx);
14879 let navigated = cx
14880 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
14881 .await
14882 .expect("Failed to navigate to definition");
14883 assert_eq!(
14884 navigated,
14885 Navigated::Yes,
14886 "Should have navigated to definition from the GetDefinition response"
14887 );
14888 cx.assert_editor_state(
14889 &r#"fn one() {
14890 let mut a = two();
14891 }
14892
14893 fn «twoˇ»() {}"#
14894 .unindent(),
14895 );
14896
14897 let editors = cx.update_workspace(|workspace, _, cx| {
14898 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
14899 });
14900 cx.update_editor(|_, _, test_editor_cx| {
14901 assert_eq!(
14902 editors.len(),
14903 1,
14904 "Initially, only one, test, editor should be open in the workspace"
14905 );
14906 assert_eq!(
14907 test_editor_cx.entity(),
14908 editors.last().expect("Asserted len is 1").clone()
14909 );
14910 });
14911
14912 set_up_lsp_handlers(true, &mut cx);
14913 let navigated = cx
14914 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
14915 .await
14916 .expect("Failed to navigate to lookup references");
14917 assert_eq!(
14918 navigated,
14919 Navigated::Yes,
14920 "Should have navigated to references as a fallback after empty GoToDefinition response"
14921 );
14922 // We should not change the selections in the existing file,
14923 // if opening another milti buffer with the references
14924 cx.assert_editor_state(
14925 &r#"fn one() {
14926 let mut a = two();
14927 }
14928
14929 fn «twoˇ»() {}"#
14930 .unindent(),
14931 );
14932 let editors = cx.update_workspace(|workspace, _, cx| {
14933 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
14934 });
14935 cx.update_editor(|_, _, test_editor_cx| {
14936 assert_eq!(
14937 editors.len(),
14938 2,
14939 "After falling back to references search, we open a new editor with the results"
14940 );
14941 let references_fallback_text = editors
14942 .into_iter()
14943 .find(|new_editor| *new_editor != test_editor_cx.entity())
14944 .expect("Should have one non-test editor now")
14945 .read(test_editor_cx)
14946 .text(test_editor_cx);
14947 assert_eq!(
14948 references_fallback_text, "fn one() {\n let mut a = two();\n}",
14949 "Should use the range from the references response and not the GoToDefinition one"
14950 );
14951 });
14952}
14953
14954#[gpui::test]
14955async fn test_find_enclosing_node_with_task(cx: &mut gpui::TestAppContext) {
14956 init_test(cx, |_| {});
14957
14958 let language = Arc::new(Language::new(
14959 LanguageConfig::default(),
14960 Some(tree_sitter_rust::LANGUAGE.into()),
14961 ));
14962
14963 let text = r#"
14964 #[cfg(test)]
14965 mod tests() {
14966 #[test]
14967 fn runnable_1() {
14968 let a = 1;
14969 }
14970
14971 #[test]
14972 fn runnable_2() {
14973 let a = 1;
14974 let b = 2;
14975 }
14976 }
14977 "#
14978 .unindent();
14979
14980 let fs = FakeFs::new(cx.executor());
14981 fs.insert_file("/file.rs", Default::default()).await;
14982
14983 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14984 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14985 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14986 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14987 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
14988
14989 let editor = cx.new_window_entity(|window, cx| {
14990 Editor::new(
14991 EditorMode::Full,
14992 multi_buffer,
14993 Some(project.clone()),
14994 true,
14995 window,
14996 cx,
14997 )
14998 });
14999
15000 editor.update_in(cx, |editor, window, cx| {
15001 editor.tasks.insert(
15002 (buffer.read(cx).remote_id(), 3),
15003 RunnableTasks {
15004 templates: vec![],
15005 offset: MultiBufferOffset(43),
15006 column: 0,
15007 extra_variables: HashMap::default(),
15008 context_range: BufferOffset(43)..BufferOffset(85),
15009 },
15010 );
15011 editor.tasks.insert(
15012 (buffer.read(cx).remote_id(), 8),
15013 RunnableTasks {
15014 templates: vec![],
15015 offset: MultiBufferOffset(86),
15016 column: 0,
15017 extra_variables: HashMap::default(),
15018 context_range: BufferOffset(86)..BufferOffset(191),
15019 },
15020 );
15021
15022 // Test finding task when cursor is inside function body
15023 editor.change_selections(None, window, cx, |s| {
15024 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
15025 });
15026 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
15027 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
15028
15029 // Test finding task when cursor is on function name
15030 editor.change_selections(None, window, cx, |s| {
15031 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
15032 });
15033 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
15034 assert_eq!(row, 8, "Should find task when cursor is on function name");
15035 });
15036}
15037
15038#[gpui::test]
15039async fn test_multi_buffer_folding(cx: &mut gpui::TestAppContext) {
15040 init_test(cx, |_| {});
15041
15042 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
15043 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
15044 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
15045
15046 let fs = FakeFs::new(cx.executor());
15047 fs.insert_tree(
15048 path!("/a"),
15049 json!({
15050 "first.rs": sample_text_1,
15051 "second.rs": sample_text_2,
15052 "third.rs": sample_text_3,
15053 }),
15054 )
15055 .await;
15056 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15057 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15058 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15059 let worktree = project.update(cx, |project, cx| {
15060 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15061 assert_eq!(worktrees.len(), 1);
15062 worktrees.pop().unwrap()
15063 });
15064 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15065
15066 let buffer_1 = project
15067 .update(cx, |project, cx| {
15068 project.open_buffer((worktree_id, "first.rs"), cx)
15069 })
15070 .await
15071 .unwrap();
15072 let buffer_2 = project
15073 .update(cx, |project, cx| {
15074 project.open_buffer((worktree_id, "second.rs"), cx)
15075 })
15076 .await
15077 .unwrap();
15078 let buffer_3 = project
15079 .update(cx, |project, cx| {
15080 project.open_buffer((worktree_id, "third.rs"), cx)
15081 })
15082 .await
15083 .unwrap();
15084
15085 let multi_buffer = cx.new(|cx| {
15086 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15087 multi_buffer.push_excerpts(
15088 buffer_1.clone(),
15089 [
15090 ExcerptRange {
15091 context: Point::new(0, 0)..Point::new(3, 0),
15092 primary: None,
15093 },
15094 ExcerptRange {
15095 context: Point::new(5, 0)..Point::new(7, 0),
15096 primary: None,
15097 },
15098 ExcerptRange {
15099 context: Point::new(9, 0)..Point::new(10, 4),
15100 primary: None,
15101 },
15102 ],
15103 cx,
15104 );
15105 multi_buffer.push_excerpts(
15106 buffer_2.clone(),
15107 [
15108 ExcerptRange {
15109 context: Point::new(0, 0)..Point::new(3, 0),
15110 primary: None,
15111 },
15112 ExcerptRange {
15113 context: Point::new(5, 0)..Point::new(7, 0),
15114 primary: None,
15115 },
15116 ExcerptRange {
15117 context: Point::new(9, 0)..Point::new(10, 4),
15118 primary: None,
15119 },
15120 ],
15121 cx,
15122 );
15123 multi_buffer.push_excerpts(
15124 buffer_3.clone(),
15125 [
15126 ExcerptRange {
15127 context: Point::new(0, 0)..Point::new(3, 0),
15128 primary: None,
15129 },
15130 ExcerptRange {
15131 context: Point::new(5, 0)..Point::new(7, 0),
15132 primary: None,
15133 },
15134 ExcerptRange {
15135 context: Point::new(9, 0)..Point::new(10, 4),
15136 primary: None,
15137 },
15138 ],
15139 cx,
15140 );
15141 multi_buffer
15142 });
15143 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15144 Editor::new(
15145 EditorMode::Full,
15146 multi_buffer,
15147 Some(project.clone()),
15148 true,
15149 window,
15150 cx,
15151 )
15152 });
15153
15154 let full_text = "\n\n\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";
15155 assert_eq!(
15156 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15157 full_text,
15158 );
15159
15160 multi_buffer_editor.update(cx, |editor, cx| {
15161 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
15162 });
15163 assert_eq!(
15164 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15165 "\n\n\n\n\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",
15166 "After folding the first buffer, its text should not be displayed"
15167 );
15168
15169 multi_buffer_editor.update(cx, |editor, cx| {
15170 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
15171 });
15172 assert_eq!(
15173 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15174 "\n\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
15175 "After folding the second buffer, its text should not be displayed"
15176 );
15177
15178 multi_buffer_editor.update(cx, |editor, cx| {
15179 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
15180 });
15181 assert_eq!(
15182 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15183 "\n\n\n\n\n",
15184 "After folding the third buffer, its text should not be displayed"
15185 );
15186
15187 // Emulate selection inside the fold logic, that should work
15188 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15189 editor
15190 .snapshot(window, cx)
15191 .next_line_boundary(Point::new(0, 4));
15192 });
15193
15194 multi_buffer_editor.update(cx, |editor, cx| {
15195 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
15196 });
15197 assert_eq!(
15198 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15199 "\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
15200 "After unfolding the second buffer, its text should be displayed"
15201 );
15202
15203 multi_buffer_editor.update(cx, |editor, cx| {
15204 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
15205 });
15206 assert_eq!(
15207 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15208 "\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",
15209 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
15210 );
15211
15212 multi_buffer_editor.update(cx, |editor, cx| {
15213 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
15214 });
15215 assert_eq!(
15216 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15217 full_text,
15218 "After unfolding the all buffers, all original text should be displayed"
15219 );
15220}
15221
15222#[gpui::test]
15223async fn test_multi_buffer_single_excerpts_folding(cx: &mut gpui::TestAppContext) {
15224 init_test(cx, |_| {});
15225
15226 let sample_text_1 = "1111\n2222\n3333".to_string();
15227 let sample_text_2 = "4444\n5555\n6666".to_string();
15228 let sample_text_3 = "7777\n8888\n9999".to_string();
15229
15230 let fs = FakeFs::new(cx.executor());
15231 fs.insert_tree(
15232 path!("/a"),
15233 json!({
15234 "first.rs": sample_text_1,
15235 "second.rs": sample_text_2,
15236 "third.rs": sample_text_3,
15237 }),
15238 )
15239 .await;
15240 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15241 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15242 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15243 let worktree = project.update(cx, |project, cx| {
15244 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15245 assert_eq!(worktrees.len(), 1);
15246 worktrees.pop().unwrap()
15247 });
15248 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15249
15250 let buffer_1 = project
15251 .update(cx, |project, cx| {
15252 project.open_buffer((worktree_id, "first.rs"), cx)
15253 })
15254 .await
15255 .unwrap();
15256 let buffer_2 = project
15257 .update(cx, |project, cx| {
15258 project.open_buffer((worktree_id, "second.rs"), cx)
15259 })
15260 .await
15261 .unwrap();
15262 let buffer_3 = project
15263 .update(cx, |project, cx| {
15264 project.open_buffer((worktree_id, "third.rs"), cx)
15265 })
15266 .await
15267 .unwrap();
15268
15269 let multi_buffer = cx.new(|cx| {
15270 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15271 multi_buffer.push_excerpts(
15272 buffer_1.clone(),
15273 [ExcerptRange {
15274 context: Point::new(0, 0)..Point::new(3, 0),
15275 primary: None,
15276 }],
15277 cx,
15278 );
15279 multi_buffer.push_excerpts(
15280 buffer_2.clone(),
15281 [ExcerptRange {
15282 context: Point::new(0, 0)..Point::new(3, 0),
15283 primary: None,
15284 }],
15285 cx,
15286 );
15287 multi_buffer.push_excerpts(
15288 buffer_3.clone(),
15289 [ExcerptRange {
15290 context: Point::new(0, 0)..Point::new(3, 0),
15291 primary: None,
15292 }],
15293 cx,
15294 );
15295 multi_buffer
15296 });
15297
15298 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15299 Editor::new(
15300 EditorMode::Full,
15301 multi_buffer,
15302 Some(project.clone()),
15303 true,
15304 window,
15305 cx,
15306 )
15307 });
15308
15309 let full_text = "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n";
15310 assert_eq!(
15311 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15312 full_text,
15313 );
15314
15315 multi_buffer_editor.update(cx, |editor, cx| {
15316 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
15317 });
15318 assert_eq!(
15319 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15320 "\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n",
15321 "After folding the first buffer, its text should not be displayed"
15322 );
15323
15324 multi_buffer_editor.update(cx, |editor, cx| {
15325 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
15326 });
15327
15328 assert_eq!(
15329 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15330 "\n\n\n\n\n\n\n7777\n8888\n9999\n",
15331 "After folding the second buffer, its text should not be displayed"
15332 );
15333
15334 multi_buffer_editor.update(cx, |editor, cx| {
15335 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
15336 });
15337 assert_eq!(
15338 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15339 "\n\n\n\n\n",
15340 "After folding the third buffer, its text should not be displayed"
15341 );
15342
15343 multi_buffer_editor.update(cx, |editor, cx| {
15344 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
15345 });
15346 assert_eq!(
15347 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15348 "\n\n\n\n\n4444\n5555\n6666\n\n\n",
15349 "After unfolding the second buffer, its text should be displayed"
15350 );
15351
15352 multi_buffer_editor.update(cx, |editor, cx| {
15353 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
15354 });
15355 assert_eq!(
15356 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15357 "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n",
15358 "After unfolding the first buffer, its text should be displayed"
15359 );
15360
15361 multi_buffer_editor.update(cx, |editor, cx| {
15362 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
15363 });
15364 assert_eq!(
15365 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15366 full_text,
15367 "After unfolding all buffers, all original text should be displayed"
15368 );
15369}
15370
15371#[gpui::test]
15372async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppContext) {
15373 init_test(cx, |_| {});
15374
15375 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
15376
15377 let fs = FakeFs::new(cx.executor());
15378 fs.insert_tree(
15379 path!("/a"),
15380 json!({
15381 "main.rs": sample_text,
15382 }),
15383 )
15384 .await;
15385 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15386 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15387 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15388 let worktree = project.update(cx, |project, cx| {
15389 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15390 assert_eq!(worktrees.len(), 1);
15391 worktrees.pop().unwrap()
15392 });
15393 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15394
15395 let buffer_1 = project
15396 .update(cx, |project, cx| {
15397 project.open_buffer((worktree_id, "main.rs"), cx)
15398 })
15399 .await
15400 .unwrap();
15401
15402 let multi_buffer = cx.new(|cx| {
15403 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15404 multi_buffer.push_excerpts(
15405 buffer_1.clone(),
15406 [ExcerptRange {
15407 context: Point::new(0, 0)
15408 ..Point::new(
15409 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
15410 0,
15411 ),
15412 primary: None,
15413 }],
15414 cx,
15415 );
15416 multi_buffer
15417 });
15418 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15419 Editor::new(
15420 EditorMode::Full,
15421 multi_buffer,
15422 Some(project.clone()),
15423 true,
15424 window,
15425 cx,
15426 )
15427 });
15428
15429 let selection_range = Point::new(1, 0)..Point::new(2, 0);
15430 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15431 enum TestHighlight {}
15432 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
15433 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
15434 editor.highlight_text::<TestHighlight>(
15435 vec![highlight_range.clone()],
15436 HighlightStyle::color(Hsla::green()),
15437 cx,
15438 );
15439 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
15440 });
15441
15442 let full_text = format!("\n\n\n{sample_text}\n");
15443 assert_eq!(
15444 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15445 full_text,
15446 );
15447}
15448
15449#[gpui::test]
15450async fn test_inline_completion_text(cx: &mut TestAppContext) {
15451 init_test(cx, |_| {});
15452
15453 // Simple insertion
15454 assert_highlighted_edits(
15455 "Hello, world!",
15456 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
15457 true,
15458 cx,
15459 |highlighted_edits, cx| {
15460 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
15461 assert_eq!(highlighted_edits.highlights.len(), 1);
15462 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
15463 assert_eq!(
15464 highlighted_edits.highlights[0].1.background_color,
15465 Some(cx.theme().status().created_background)
15466 );
15467 },
15468 )
15469 .await;
15470
15471 // Replacement
15472 assert_highlighted_edits(
15473 "This is a test.",
15474 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
15475 false,
15476 cx,
15477 |highlighted_edits, cx| {
15478 assert_eq!(highlighted_edits.text, "That is a test.");
15479 assert_eq!(highlighted_edits.highlights.len(), 1);
15480 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
15481 assert_eq!(
15482 highlighted_edits.highlights[0].1.background_color,
15483 Some(cx.theme().status().created_background)
15484 );
15485 },
15486 )
15487 .await;
15488
15489 // Multiple edits
15490 assert_highlighted_edits(
15491 "Hello, world!",
15492 vec![
15493 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
15494 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
15495 ],
15496 false,
15497 cx,
15498 |highlighted_edits, cx| {
15499 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
15500 assert_eq!(highlighted_edits.highlights.len(), 2);
15501 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
15502 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
15503 assert_eq!(
15504 highlighted_edits.highlights[0].1.background_color,
15505 Some(cx.theme().status().created_background)
15506 );
15507 assert_eq!(
15508 highlighted_edits.highlights[1].1.background_color,
15509 Some(cx.theme().status().created_background)
15510 );
15511 },
15512 )
15513 .await;
15514
15515 // Multiple lines with edits
15516 assert_highlighted_edits(
15517 "First line\nSecond line\nThird line\nFourth line",
15518 vec![
15519 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
15520 (
15521 Point::new(2, 0)..Point::new(2, 10),
15522 "New third line".to_string(),
15523 ),
15524 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
15525 ],
15526 false,
15527 cx,
15528 |highlighted_edits, cx| {
15529 assert_eq!(
15530 highlighted_edits.text,
15531 "Second modified\nNew third line\nFourth updated line"
15532 );
15533 assert_eq!(highlighted_edits.highlights.len(), 3);
15534 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
15535 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
15536 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
15537 for highlight in &highlighted_edits.highlights {
15538 assert_eq!(
15539 highlight.1.background_color,
15540 Some(cx.theme().status().created_background)
15541 );
15542 }
15543 },
15544 )
15545 .await;
15546}
15547
15548#[gpui::test]
15549async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
15550 init_test(cx, |_| {});
15551
15552 // Deletion
15553 assert_highlighted_edits(
15554 "Hello, world!",
15555 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
15556 true,
15557 cx,
15558 |highlighted_edits, cx| {
15559 assert_eq!(highlighted_edits.text, "Hello, world!");
15560 assert_eq!(highlighted_edits.highlights.len(), 1);
15561 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
15562 assert_eq!(
15563 highlighted_edits.highlights[0].1.background_color,
15564 Some(cx.theme().status().deleted_background)
15565 );
15566 },
15567 )
15568 .await;
15569
15570 // Insertion
15571 assert_highlighted_edits(
15572 "Hello, world!",
15573 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
15574 true,
15575 cx,
15576 |highlighted_edits, cx| {
15577 assert_eq!(highlighted_edits.highlights.len(), 1);
15578 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
15579 assert_eq!(
15580 highlighted_edits.highlights[0].1.background_color,
15581 Some(cx.theme().status().created_background)
15582 );
15583 },
15584 )
15585 .await;
15586}
15587
15588async fn assert_highlighted_edits(
15589 text: &str,
15590 edits: Vec<(Range<Point>, String)>,
15591 include_deletions: bool,
15592 cx: &mut TestAppContext,
15593 assertion_fn: impl Fn(HighlightedText, &App),
15594) {
15595 let window = cx.add_window(|window, cx| {
15596 let buffer = MultiBuffer::build_simple(text, cx);
15597 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
15598 });
15599 let cx = &mut VisualTestContext::from_window(*window, cx);
15600
15601 let (buffer, snapshot) = window
15602 .update(cx, |editor, _window, cx| {
15603 (
15604 editor.buffer().clone(),
15605 editor.buffer().read(cx).snapshot(cx),
15606 )
15607 })
15608 .unwrap();
15609
15610 let edits = edits
15611 .into_iter()
15612 .map(|(range, edit)| {
15613 (
15614 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
15615 edit,
15616 )
15617 })
15618 .collect::<Vec<_>>();
15619
15620 let text_anchor_edits = edits
15621 .clone()
15622 .into_iter()
15623 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
15624 .collect::<Vec<_>>();
15625
15626 let edit_preview = window
15627 .update(cx, |_, _window, cx| {
15628 buffer
15629 .read(cx)
15630 .as_singleton()
15631 .unwrap()
15632 .read(cx)
15633 .preview_edits(text_anchor_edits.into(), cx)
15634 })
15635 .unwrap()
15636 .await;
15637
15638 cx.update(|_window, cx| {
15639 let highlighted_edits = inline_completion_edit_text(
15640 &snapshot.as_singleton().unwrap().2,
15641 &edits,
15642 &edit_preview,
15643 include_deletions,
15644 cx,
15645 );
15646 assertion_fn(highlighted_edits, cx)
15647 });
15648}
15649
15650#[gpui::test]
15651async fn test_rename_with_duplicate_edits(cx: &mut gpui::TestAppContext) {
15652 init_test(cx, |_| {});
15653 let capabilities = lsp::ServerCapabilities {
15654 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
15655 prepare_provider: Some(true),
15656 work_done_progress_options: Default::default(),
15657 })),
15658 ..Default::default()
15659 };
15660 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
15661
15662 cx.set_state(indoc! {"
15663 struct Fˇoo {}
15664 "});
15665
15666 cx.update_editor(|editor, _, cx| {
15667 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
15668 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
15669 editor.highlight_background::<DocumentHighlightRead>(
15670 &[highlight_range],
15671 |c| c.editor_document_highlight_read_background,
15672 cx,
15673 );
15674 });
15675
15676 let mut prepare_rename_handler =
15677 cx.handle_request::<lsp::request::PrepareRenameRequest, _, _>(move |_, _, _| async move {
15678 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
15679 start: lsp::Position {
15680 line: 0,
15681 character: 7,
15682 },
15683 end: lsp::Position {
15684 line: 0,
15685 character: 10,
15686 },
15687 })))
15688 });
15689 let prepare_rename_task = cx
15690 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
15691 .expect("Prepare rename was not started");
15692 prepare_rename_handler.next().await.unwrap();
15693 prepare_rename_task.await.expect("Prepare rename failed");
15694
15695 let mut rename_handler =
15696 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
15697 let edit = lsp::TextEdit {
15698 range: lsp::Range {
15699 start: lsp::Position {
15700 line: 0,
15701 character: 7,
15702 },
15703 end: lsp::Position {
15704 line: 0,
15705 character: 10,
15706 },
15707 },
15708 new_text: "FooRenamed".to_string(),
15709 };
15710 Ok(Some(lsp::WorkspaceEdit::new(
15711 // Specify the same edit twice
15712 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
15713 )))
15714 });
15715 let rename_task = cx
15716 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
15717 .expect("Confirm rename was not started");
15718 rename_handler.next().await.unwrap();
15719 rename_task.await.expect("Confirm rename failed");
15720 cx.run_until_parked();
15721
15722 // Despite two edits, only one is actually applied as those are identical
15723 cx.assert_editor_state(indoc! {"
15724 struct FooRenamedˇ {}
15725 "});
15726}
15727
15728#[gpui::test]
15729async fn test_rename_without_prepare(cx: &mut gpui::TestAppContext) {
15730 init_test(cx, |_| {});
15731 // These capabilities indicate that the server does not support prepare rename.
15732 let capabilities = lsp::ServerCapabilities {
15733 rename_provider: Some(lsp::OneOf::Left(true)),
15734 ..Default::default()
15735 };
15736 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
15737
15738 cx.set_state(indoc! {"
15739 struct Fˇoo {}
15740 "});
15741
15742 cx.update_editor(|editor, _window, cx| {
15743 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
15744 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
15745 editor.highlight_background::<DocumentHighlightRead>(
15746 &[highlight_range],
15747 |c| c.editor_document_highlight_read_background,
15748 cx,
15749 );
15750 });
15751
15752 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
15753 .expect("Prepare rename was not started")
15754 .await
15755 .expect("Prepare rename failed");
15756
15757 let mut rename_handler =
15758 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
15759 let edit = lsp::TextEdit {
15760 range: lsp::Range {
15761 start: lsp::Position {
15762 line: 0,
15763 character: 7,
15764 },
15765 end: lsp::Position {
15766 line: 0,
15767 character: 10,
15768 },
15769 },
15770 new_text: "FooRenamed".to_string(),
15771 };
15772 Ok(Some(lsp::WorkspaceEdit::new(
15773 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
15774 )))
15775 });
15776 let rename_task = cx
15777 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
15778 .expect("Confirm rename was not started");
15779 rename_handler.next().await.unwrap();
15780 rename_task.await.expect("Confirm rename failed");
15781 cx.run_until_parked();
15782
15783 // Correct range is renamed, as `surrounding_word` is used to find it.
15784 cx.assert_editor_state(indoc! {"
15785 struct FooRenamedˇ {}
15786 "});
15787}
15788
15789fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
15790 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
15791 point..point
15792}
15793
15794fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
15795 let (text, ranges) = marked_text_ranges(marked_text, true);
15796 assert_eq!(editor.text(cx), text);
15797 assert_eq!(
15798 editor.selections.ranges(cx),
15799 ranges,
15800 "Assert selections are {}",
15801 marked_text
15802 );
15803}
15804
15805pub fn handle_signature_help_request(
15806 cx: &mut EditorLspTestContext,
15807 mocked_response: lsp::SignatureHelp,
15808) -> impl Future<Output = ()> {
15809 let mut request =
15810 cx.handle_request::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
15811 let mocked_response = mocked_response.clone();
15812 async move { Ok(Some(mocked_response)) }
15813 });
15814
15815 async move {
15816 request.next().await;
15817 }
15818}
15819
15820/// Handle completion request passing a marked string specifying where the completion
15821/// should be triggered from using '|' character, what range should be replaced, and what completions
15822/// should be returned using '<' and '>' to delimit the range
15823pub fn handle_completion_request(
15824 cx: &mut EditorLspTestContext,
15825 marked_string: &str,
15826 completions: Vec<&'static str>,
15827 counter: Arc<AtomicUsize>,
15828) -> impl Future<Output = ()> {
15829 let complete_from_marker: TextRangeMarker = '|'.into();
15830 let replace_range_marker: TextRangeMarker = ('<', '>').into();
15831 let (_, mut marked_ranges) = marked_text_ranges_by(
15832 marked_string,
15833 vec![complete_from_marker.clone(), replace_range_marker.clone()],
15834 );
15835
15836 let complete_from_position =
15837 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
15838 let replace_range =
15839 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
15840
15841 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
15842 let completions = completions.clone();
15843 counter.fetch_add(1, atomic::Ordering::Release);
15844 async move {
15845 assert_eq!(params.text_document_position.text_document.uri, url.clone());
15846 assert_eq!(
15847 params.text_document_position.position,
15848 complete_from_position
15849 );
15850 Ok(Some(lsp::CompletionResponse::Array(
15851 completions
15852 .iter()
15853 .map(|completion_text| lsp::CompletionItem {
15854 label: completion_text.to_string(),
15855 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15856 range: replace_range,
15857 new_text: completion_text.to_string(),
15858 })),
15859 ..Default::default()
15860 })
15861 .collect(),
15862 )))
15863 }
15864 });
15865
15866 async move {
15867 request.next().await;
15868 }
15869}
15870
15871fn handle_resolve_completion_request(
15872 cx: &mut EditorLspTestContext,
15873 edits: Option<Vec<(&'static str, &'static str)>>,
15874) -> impl Future<Output = ()> {
15875 let edits = edits.map(|edits| {
15876 edits
15877 .iter()
15878 .map(|(marked_string, new_text)| {
15879 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
15880 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
15881 lsp::TextEdit::new(replace_range, new_text.to_string())
15882 })
15883 .collect::<Vec<_>>()
15884 });
15885
15886 let mut request =
15887 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15888 let edits = edits.clone();
15889 async move {
15890 Ok(lsp::CompletionItem {
15891 additional_text_edits: edits,
15892 ..Default::default()
15893 })
15894 }
15895 });
15896
15897 async move {
15898 request.next().await;
15899 }
15900}
15901
15902pub(crate) fn update_test_language_settings(
15903 cx: &mut TestAppContext,
15904 f: impl Fn(&mut AllLanguageSettingsContent),
15905) {
15906 cx.update(|cx| {
15907 SettingsStore::update_global(cx, |store, cx| {
15908 store.update_user_settings::<AllLanguageSettings>(cx, f);
15909 });
15910 });
15911}
15912
15913pub(crate) fn update_test_project_settings(
15914 cx: &mut TestAppContext,
15915 f: impl Fn(&mut ProjectSettings),
15916) {
15917 cx.update(|cx| {
15918 SettingsStore::update_global(cx, |store, cx| {
15919 store.update_user_settings::<ProjectSettings>(cx, f);
15920 });
15921 });
15922}
15923
15924pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
15925 cx.update(|cx| {
15926 assets::Assets.load_test_fonts(cx);
15927 let store = SettingsStore::test(cx);
15928 cx.set_global(store);
15929 theme::init(theme::LoadThemes::JustBase, cx);
15930 release_channel::init(SemanticVersion::default(), cx);
15931 client::init_settings(cx);
15932 language::init(cx);
15933 Project::init_settings(cx);
15934 workspace::init_settings(cx);
15935 crate::init(cx);
15936 });
15937
15938 update_test_language_settings(cx, f);
15939}
15940
15941#[track_caller]
15942fn assert_hunk_revert(
15943 not_reverted_text_with_selections: &str,
15944 expected_hunk_statuses_before: Vec<DiffHunkStatus>,
15945 expected_reverted_text_with_selections: &str,
15946 base_text: &str,
15947 cx: &mut EditorLspTestContext,
15948) {
15949 cx.set_state(not_reverted_text_with_selections);
15950 cx.set_diff_base(base_text);
15951 cx.executor().run_until_parked();
15952
15953 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
15954 let snapshot = editor.snapshot(window, cx);
15955 let reverted_hunk_statuses = snapshot
15956 .buffer_snapshot
15957 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
15958 .map(|hunk| hunk.status())
15959 .collect::<Vec<_>>();
15960
15961 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
15962 reverted_hunk_statuses
15963 });
15964 cx.executor().run_until_parked();
15965 cx.assert_editor_state(expected_reverted_text_with_selections);
15966 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
15967}