1use super::*;
2use crate::{
3 scroll::scroll_amount::ScrollAmount,
4 test::{
5 assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
6 editor_test_context::EditorTestContext, select_ranges,
7 },
8 JoinLines,
9};
10use buffer_diff::{BufferDiff, DiffHunkStatus};
11use futures::StreamExt;
12use gpui::{
13 div, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
14 WindowBounds, WindowOptions,
15};
16use indoc::indoc;
17use language::{
18 language_settings::{
19 AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent, PrettierSettings,
20 },
21 BracketPairConfig,
22 Capability::ReadWrite,
23 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
24 Override, ParsedMarkdown, Point,
25};
26use language_settings::{Formatter, FormatterList, IndentGuideSettings};
27use multi_buffer::IndentGuide;
28use parking_lot::Mutex;
29use pretty_assertions::{assert_eq, assert_ne};
30use project::FakeFs;
31use project::{
32 lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT,
33 project_settings::{LspSettings, ProjectSettings},
34};
35use serde_json::{self, json};
36use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
37use std::{
38 iter,
39 sync::atomic::{self, AtomicUsize},
40};
41use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
42use unindent::Unindent;
43use util::{
44 assert_set_eq, path,
45 test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
46 uri,
47};
48use workspace::{
49 item::{FollowEvent, FollowableItem, Item, ItemHandle},
50 NavigationEntry, ViewId,
51};
52
53#[gpui::test]
54fn test_edit_events(cx: &mut TestAppContext) {
55 init_test(cx, |_| {});
56
57 let buffer = cx.new(|cx| {
58 let mut buffer = language::Buffer::local("123456", cx);
59 buffer.set_group_interval(Duration::from_secs(1));
60 buffer
61 });
62
63 let events = Rc::new(RefCell::new(Vec::new()));
64 let editor1 = cx.add_window({
65 let events = events.clone();
66 |window, cx| {
67 let entity = cx.entity().clone();
68 cx.subscribe_in(
69 &entity,
70 window,
71 move |_, _, event: &EditorEvent, _, _| match event {
72 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
73 EditorEvent::BufferEdited => {
74 events.borrow_mut().push(("editor1", "buffer edited"))
75 }
76 _ => {}
77 },
78 )
79 .detach();
80 Editor::for_buffer(buffer.clone(), None, window, cx)
81 }
82 });
83
84 let editor2 = cx.add_window({
85 let events = events.clone();
86 |window, cx| {
87 cx.subscribe_in(
88 &cx.entity().clone(),
89 window,
90 move |_, _, event: &EditorEvent, _, _| match event {
91 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
92 EditorEvent::BufferEdited => {
93 events.borrow_mut().push(("editor2", "buffer edited"))
94 }
95 _ => {}
96 },
97 )
98 .detach();
99 Editor::for_buffer(buffer.clone(), None, window, cx)
100 }
101 });
102
103 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
104
105 // Mutating editor 1 will emit an `Edited` event only for that editor.
106 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
107 assert_eq!(
108 mem::take(&mut *events.borrow_mut()),
109 [
110 ("editor1", "edited"),
111 ("editor1", "buffer edited"),
112 ("editor2", "buffer edited"),
113 ]
114 );
115
116 // Mutating editor 2 will emit an `Edited` event only for that editor.
117 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
118 assert_eq!(
119 mem::take(&mut *events.borrow_mut()),
120 [
121 ("editor2", "edited"),
122 ("editor1", "buffer edited"),
123 ("editor2", "buffer edited"),
124 ]
125 );
126
127 // Undoing on editor 1 will emit an `Edited` event only for that editor.
128 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
129 assert_eq!(
130 mem::take(&mut *events.borrow_mut()),
131 [
132 ("editor1", "edited"),
133 ("editor1", "buffer edited"),
134 ("editor2", "buffer edited"),
135 ]
136 );
137
138 // Redoing on editor 1 will emit an `Edited` event only for that editor.
139 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
140 assert_eq!(
141 mem::take(&mut *events.borrow_mut()),
142 [
143 ("editor1", "edited"),
144 ("editor1", "buffer edited"),
145 ("editor2", "buffer edited"),
146 ]
147 );
148
149 // Undoing on editor 2 will emit an `Edited` event only for that editor.
150 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
151 assert_eq!(
152 mem::take(&mut *events.borrow_mut()),
153 [
154 ("editor2", "edited"),
155 ("editor1", "buffer edited"),
156 ("editor2", "buffer edited"),
157 ]
158 );
159
160 // Redoing on editor 2 will emit an `Edited` event only for that editor.
161 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
162 assert_eq!(
163 mem::take(&mut *events.borrow_mut()),
164 [
165 ("editor2", "edited"),
166 ("editor1", "buffer edited"),
167 ("editor2", "buffer edited"),
168 ]
169 );
170
171 // No event is emitted when the mutation is a no-op.
172 _ = editor2.update(cx, |editor, window, cx| {
173 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
174
175 editor.backspace(&Backspace, window, cx);
176 });
177 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
178}
179
180#[gpui::test]
181fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
182 init_test(cx, |_| {});
183
184 let mut now = Instant::now();
185 let group_interval = Duration::from_millis(1);
186 let buffer = cx.new(|cx| {
187 let mut buf = language::Buffer::local("123456", cx);
188 buf.set_group_interval(group_interval);
189 buf
190 });
191 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
192 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
193
194 _ = editor.update(cx, |editor, window, cx| {
195 editor.start_transaction_at(now, window, cx);
196 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
197
198 editor.insert("cd", window, cx);
199 editor.end_transaction_at(now, cx);
200 assert_eq!(editor.text(cx), "12cd56");
201 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
202
203 editor.start_transaction_at(now, window, cx);
204 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
205 editor.insert("e", window, cx);
206 editor.end_transaction_at(now, cx);
207 assert_eq!(editor.text(cx), "12cde6");
208 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
209
210 now += group_interval + Duration::from_millis(1);
211 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
212
213 // Simulate an edit in another editor
214 buffer.update(cx, |buffer, cx| {
215 buffer.start_transaction_at(now, cx);
216 buffer.edit([(0..1, "a")], None, cx);
217 buffer.edit([(1..1, "b")], None, cx);
218 buffer.end_transaction_at(now, cx);
219 });
220
221 assert_eq!(editor.text(cx), "ab2cde6");
222 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
223
224 // Last transaction happened past the group interval in a different editor.
225 // Undo it individually and don't restore selections.
226 editor.undo(&Undo, window, cx);
227 assert_eq!(editor.text(cx), "12cde6");
228 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
229
230 // First two transactions happened within the group interval in this editor.
231 // Undo them together and restore selections.
232 editor.undo(&Undo, window, cx);
233 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
234 assert_eq!(editor.text(cx), "123456");
235 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
236
237 // Redo the first two transactions together.
238 editor.redo(&Redo, window, cx);
239 assert_eq!(editor.text(cx), "12cde6");
240 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
241
242 // Redo the last transaction on its own.
243 editor.redo(&Redo, window, cx);
244 assert_eq!(editor.text(cx), "ab2cde6");
245 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
246
247 // Test empty transactions.
248 editor.start_transaction_at(now, window, cx);
249 editor.end_transaction_at(now, cx);
250 editor.undo(&Undo, window, cx);
251 assert_eq!(editor.text(cx), "12cde6");
252 });
253}
254
255#[gpui::test]
256fn test_ime_composition(cx: &mut TestAppContext) {
257 init_test(cx, |_| {});
258
259 let buffer = cx.new(|cx| {
260 let mut buffer = language::Buffer::local("abcde", cx);
261 // Ensure automatic grouping doesn't occur.
262 buffer.set_group_interval(Duration::ZERO);
263 buffer
264 });
265
266 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
267 cx.add_window(|window, cx| {
268 let mut editor = build_editor(buffer.clone(), window, cx);
269
270 // Start a new IME composition.
271 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
272 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
273 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
274 assert_eq!(editor.text(cx), "äbcde");
275 assert_eq!(
276 editor.marked_text_ranges(cx),
277 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
278 );
279
280 // Finalize IME composition.
281 editor.replace_text_in_range(None, "ā", window, cx);
282 assert_eq!(editor.text(cx), "ābcde");
283 assert_eq!(editor.marked_text_ranges(cx), None);
284
285 // IME composition edits are grouped and are undone/redone at once.
286 editor.undo(&Default::default(), window, cx);
287 assert_eq!(editor.text(cx), "abcde");
288 assert_eq!(editor.marked_text_ranges(cx), None);
289 editor.redo(&Default::default(), window, cx);
290 assert_eq!(editor.text(cx), "ābcde");
291 assert_eq!(editor.marked_text_ranges(cx), None);
292
293 // Start a new IME composition.
294 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
295 assert_eq!(
296 editor.marked_text_ranges(cx),
297 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
298 );
299
300 // Undoing during an IME composition cancels it.
301 editor.undo(&Default::default(), window, cx);
302 assert_eq!(editor.text(cx), "ābcde");
303 assert_eq!(editor.marked_text_ranges(cx), None);
304
305 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
306 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
307 assert_eq!(editor.text(cx), "ābcdè");
308 assert_eq!(
309 editor.marked_text_ranges(cx),
310 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
311 );
312
313 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
314 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
315 assert_eq!(editor.text(cx), "ābcdę");
316 assert_eq!(editor.marked_text_ranges(cx), None);
317
318 // Start a new IME composition with multiple cursors.
319 editor.change_selections(None, window, cx, |s| {
320 s.select_ranges([
321 OffsetUtf16(1)..OffsetUtf16(1),
322 OffsetUtf16(3)..OffsetUtf16(3),
323 OffsetUtf16(5)..OffsetUtf16(5),
324 ])
325 });
326 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
327 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
328 assert_eq!(
329 editor.marked_text_ranges(cx),
330 Some(vec![
331 OffsetUtf16(0)..OffsetUtf16(3),
332 OffsetUtf16(4)..OffsetUtf16(7),
333 OffsetUtf16(8)..OffsetUtf16(11)
334 ])
335 );
336
337 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
338 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
339 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
340 assert_eq!(
341 editor.marked_text_ranges(cx),
342 Some(vec![
343 OffsetUtf16(1)..OffsetUtf16(2),
344 OffsetUtf16(5)..OffsetUtf16(6),
345 OffsetUtf16(9)..OffsetUtf16(10)
346 ])
347 );
348
349 // Finalize IME composition with multiple cursors.
350 editor.replace_text_in_range(Some(9..10), "2", window, cx);
351 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
352 assert_eq!(editor.marked_text_ranges(cx), None);
353
354 editor
355 });
356}
357
358#[gpui::test]
359fn test_selection_with_mouse(cx: &mut TestAppContext) {
360 init_test(cx, |_| {});
361
362 let editor = cx.add_window(|window, cx| {
363 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
364 build_editor(buffer, window, cx)
365 });
366
367 _ = editor.update(cx, |editor, window, cx| {
368 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
369 });
370 assert_eq!(
371 editor
372 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
373 .unwrap(),
374 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
375 );
376
377 _ = editor.update(cx, |editor, window, cx| {
378 editor.update_selection(
379 DisplayPoint::new(DisplayRow(3), 3),
380 0,
381 gpui::Point::<f32>::default(),
382 window,
383 cx,
384 );
385 });
386
387 assert_eq!(
388 editor
389 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
390 .unwrap(),
391 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
392 );
393
394 _ = editor.update(cx, |editor, window, cx| {
395 editor.update_selection(
396 DisplayPoint::new(DisplayRow(1), 1),
397 0,
398 gpui::Point::<f32>::default(),
399 window,
400 cx,
401 );
402 });
403
404 assert_eq!(
405 editor
406 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
407 .unwrap(),
408 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
409 );
410
411 _ = editor.update(cx, |editor, window, cx| {
412 editor.end_selection(window, cx);
413 editor.update_selection(
414 DisplayPoint::new(DisplayRow(3), 3),
415 0,
416 gpui::Point::<f32>::default(),
417 window,
418 cx,
419 );
420 });
421
422 assert_eq!(
423 editor
424 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
425 .unwrap(),
426 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
427 );
428
429 _ = editor.update(cx, |editor, window, cx| {
430 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
431 editor.update_selection(
432 DisplayPoint::new(DisplayRow(0), 0),
433 0,
434 gpui::Point::<f32>::default(),
435 window,
436 cx,
437 );
438 });
439
440 assert_eq!(
441 editor
442 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
443 .unwrap(),
444 [
445 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
446 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
447 ]
448 );
449
450 _ = editor.update(cx, |editor, window, cx| {
451 editor.end_selection(window, cx);
452 });
453
454 assert_eq!(
455 editor
456 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
457 .unwrap(),
458 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
459 );
460}
461
462#[gpui::test]
463fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
464 init_test(cx, |_| {});
465
466 let editor = cx.add_window(|window, cx| {
467 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
468 build_editor(buffer, window, cx)
469 });
470
471 _ = editor.update(cx, |editor, window, cx| {
472 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
473 });
474
475 _ = editor.update(cx, |editor, window, cx| {
476 editor.end_selection(window, cx);
477 });
478
479 _ = editor.update(cx, |editor, window, cx| {
480 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
481 });
482
483 _ = editor.update(cx, |editor, window, cx| {
484 editor.end_selection(window, cx);
485 });
486
487 assert_eq!(
488 editor
489 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
490 .unwrap(),
491 [
492 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
493 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
494 ]
495 );
496
497 _ = editor.update(cx, |editor, window, cx| {
498 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
499 });
500
501 _ = editor.update(cx, |editor, window, cx| {
502 editor.end_selection(window, cx);
503 });
504
505 assert_eq!(
506 editor
507 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
508 .unwrap(),
509 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
510 );
511}
512
513#[gpui::test]
514fn test_canceling_pending_selection(cx: &mut TestAppContext) {
515 init_test(cx, |_| {});
516
517 let editor = cx.add_window(|window, cx| {
518 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
519 build_editor(buffer, window, cx)
520 });
521
522 _ = editor.update(cx, |editor, window, cx| {
523 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
524 assert_eq!(
525 editor.selections.display_ranges(cx),
526 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
527 );
528 });
529
530 _ = editor.update(cx, |editor, window, cx| {
531 editor.update_selection(
532 DisplayPoint::new(DisplayRow(3), 3),
533 0,
534 gpui::Point::<f32>::default(),
535 window,
536 cx,
537 );
538 assert_eq!(
539 editor.selections.display_ranges(cx),
540 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
541 );
542 });
543
544 _ = editor.update(cx, |editor, window, cx| {
545 editor.cancel(&Cancel, window, cx);
546 editor.update_selection(
547 DisplayPoint::new(DisplayRow(1), 1),
548 0,
549 gpui::Point::<f32>::default(),
550 window,
551 cx,
552 );
553 assert_eq!(
554 editor.selections.display_ranges(cx),
555 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
556 );
557 });
558}
559
560#[gpui::test]
561fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
562 init_test(cx, |_| {});
563
564 let editor = cx.add_window(|window, cx| {
565 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
566 build_editor(buffer, window, cx)
567 });
568
569 _ = editor.update(cx, |editor, window, cx| {
570 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
571 assert_eq!(
572 editor.selections.display_ranges(cx),
573 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
574 );
575
576 editor.move_down(&Default::default(), window, cx);
577 assert_eq!(
578 editor.selections.display_ranges(cx),
579 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
580 );
581
582 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
583 assert_eq!(
584 editor.selections.display_ranges(cx),
585 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
586 );
587
588 editor.move_up(&Default::default(), window, cx);
589 assert_eq!(
590 editor.selections.display_ranges(cx),
591 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
592 );
593 });
594}
595
596#[gpui::test]
597fn test_clone(cx: &mut TestAppContext) {
598 init_test(cx, |_| {});
599
600 let (text, selection_ranges) = marked_text_ranges(
601 indoc! {"
602 one
603 two
604 threeˇ
605 four
606 fiveˇ
607 "},
608 true,
609 );
610
611 let editor = cx.add_window(|window, cx| {
612 let buffer = MultiBuffer::build_simple(&text, cx);
613 build_editor(buffer, window, cx)
614 });
615
616 _ = editor.update(cx, |editor, window, cx| {
617 editor.change_selections(None, window, cx, |s| {
618 s.select_ranges(selection_ranges.clone())
619 });
620 editor.fold_creases(
621 vec![
622 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
623 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
624 ],
625 true,
626 window,
627 cx,
628 );
629 });
630
631 let cloned_editor = editor
632 .update(cx, |editor, _, cx| {
633 cx.open_window(Default::default(), |window, cx| {
634 cx.new(|cx| editor.clone(window, cx))
635 })
636 })
637 .unwrap()
638 .unwrap();
639
640 let snapshot = editor
641 .update(cx, |e, window, cx| e.snapshot(window, cx))
642 .unwrap();
643 let cloned_snapshot = cloned_editor
644 .update(cx, |e, window, cx| e.snapshot(window, cx))
645 .unwrap();
646
647 assert_eq!(
648 cloned_editor
649 .update(cx, |e, _, cx| e.display_text(cx))
650 .unwrap(),
651 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
652 );
653 assert_eq!(
654 cloned_snapshot
655 .folds_in_range(0..text.len())
656 .collect::<Vec<_>>(),
657 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
658 );
659 assert_set_eq!(
660 cloned_editor
661 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
662 .unwrap(),
663 editor
664 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
665 .unwrap()
666 );
667 assert_set_eq!(
668 cloned_editor
669 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
670 .unwrap(),
671 editor
672 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
673 .unwrap()
674 );
675}
676
677#[gpui::test]
678async fn test_navigation_history(cx: &mut TestAppContext) {
679 init_test(cx, |_| {});
680
681 use workspace::item::Item;
682
683 let fs = FakeFs::new(cx.executor());
684 let project = Project::test(fs, [], cx).await;
685 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
686 let pane = workspace
687 .update(cx, |workspace, _, _| workspace.active_pane().clone())
688 .unwrap();
689
690 _ = workspace.update(cx, |_v, window, cx| {
691 cx.new(|cx| {
692 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
693 let mut editor = build_editor(buffer.clone(), window, cx);
694 let handle = cx.entity();
695 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
696
697 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
698 editor.nav_history.as_mut().unwrap().pop_backward(cx)
699 }
700
701 // Move the cursor a small distance.
702 // Nothing is added to the navigation history.
703 editor.change_selections(None, window, cx, |s| {
704 s.select_display_ranges([
705 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
706 ])
707 });
708 editor.change_selections(None, window, cx, |s| {
709 s.select_display_ranges([
710 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
711 ])
712 });
713 assert!(pop_history(&mut editor, cx).is_none());
714
715 // Move the cursor a large distance.
716 // The history can jump back to the previous position.
717 editor.change_selections(None, window, cx, |s| {
718 s.select_display_ranges([
719 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
720 ])
721 });
722 let nav_entry = pop_history(&mut editor, cx).unwrap();
723 editor.navigate(nav_entry.data.unwrap(), window, cx);
724 assert_eq!(nav_entry.item.id(), cx.entity_id());
725 assert_eq!(
726 editor.selections.display_ranges(cx),
727 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
728 );
729 assert!(pop_history(&mut editor, cx).is_none());
730
731 // Move the cursor a small distance via the mouse.
732 // Nothing is added to the navigation history.
733 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
734 editor.end_selection(window, cx);
735 assert_eq!(
736 editor.selections.display_ranges(cx),
737 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
738 );
739 assert!(pop_history(&mut editor, cx).is_none());
740
741 // Move the cursor a large distance via the mouse.
742 // The history can jump back to the previous position.
743 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
744 editor.end_selection(window, cx);
745 assert_eq!(
746 editor.selections.display_ranges(cx),
747 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
748 );
749 let nav_entry = pop_history(&mut editor, cx).unwrap();
750 editor.navigate(nav_entry.data.unwrap(), window, cx);
751 assert_eq!(nav_entry.item.id(), cx.entity_id());
752 assert_eq!(
753 editor.selections.display_ranges(cx),
754 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
755 );
756 assert!(pop_history(&mut editor, cx).is_none());
757
758 // Set scroll position to check later
759 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
760 let original_scroll_position = editor.scroll_manager.anchor();
761
762 // Jump to the end of the document and adjust scroll
763 editor.move_to_end(&MoveToEnd, window, cx);
764 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
765 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
766
767 let nav_entry = pop_history(&mut editor, cx).unwrap();
768 editor.navigate(nav_entry.data.unwrap(), window, cx);
769 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
770
771 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
772 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
773 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
774 let invalid_point = Point::new(9999, 0);
775 editor.navigate(
776 Box::new(NavigationData {
777 cursor_anchor: invalid_anchor,
778 cursor_position: invalid_point,
779 scroll_anchor: ScrollAnchor {
780 anchor: invalid_anchor,
781 offset: Default::default(),
782 },
783 scroll_top_row: invalid_point.row,
784 }),
785 window,
786 cx,
787 );
788 assert_eq!(
789 editor.selections.display_ranges(cx),
790 &[editor.max_point(cx)..editor.max_point(cx)]
791 );
792 assert_eq!(
793 editor.scroll_position(cx),
794 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
795 );
796
797 editor
798 })
799 });
800}
801
802#[gpui::test]
803fn test_cancel(cx: &mut TestAppContext) {
804 init_test(cx, |_| {});
805
806 let editor = cx.add_window(|window, cx| {
807 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
808 build_editor(buffer, window, cx)
809 });
810
811 _ = editor.update(cx, |editor, window, cx| {
812 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
813 editor.update_selection(
814 DisplayPoint::new(DisplayRow(1), 1),
815 0,
816 gpui::Point::<f32>::default(),
817 window,
818 cx,
819 );
820 editor.end_selection(window, cx);
821
822 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
823 editor.update_selection(
824 DisplayPoint::new(DisplayRow(0), 3),
825 0,
826 gpui::Point::<f32>::default(),
827 window,
828 cx,
829 );
830 editor.end_selection(window, cx);
831 assert_eq!(
832 editor.selections.display_ranges(cx),
833 [
834 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
835 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
836 ]
837 );
838 });
839
840 _ = editor.update(cx, |editor, window, cx| {
841 editor.cancel(&Cancel, window, cx);
842 assert_eq!(
843 editor.selections.display_ranges(cx),
844 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
845 );
846 });
847
848 _ = editor.update(cx, |editor, window, cx| {
849 editor.cancel(&Cancel, window, cx);
850 assert_eq!(
851 editor.selections.display_ranges(cx),
852 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
853 );
854 });
855}
856
857#[gpui::test]
858fn test_fold_action(cx: &mut TestAppContext) {
859 init_test(cx, |_| {});
860
861 let editor = cx.add_window(|window, cx| {
862 let buffer = MultiBuffer::build_simple(
863 &"
864 impl Foo {
865 // Hello!
866
867 fn a() {
868 1
869 }
870
871 fn b() {
872 2
873 }
874
875 fn c() {
876 3
877 }
878 }
879 "
880 .unindent(),
881 cx,
882 );
883 build_editor(buffer.clone(), window, cx)
884 });
885
886 _ = editor.update(cx, |editor, window, cx| {
887 editor.change_selections(None, window, cx, |s| {
888 s.select_display_ranges([
889 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
890 ]);
891 });
892 editor.fold(&Fold, window, cx);
893 assert_eq!(
894 editor.display_text(cx),
895 "
896 impl Foo {
897 // Hello!
898
899 fn a() {
900 1
901 }
902
903 fn b() {⋯
904 }
905
906 fn c() {⋯
907 }
908 }
909 "
910 .unindent(),
911 );
912
913 editor.fold(&Fold, window, cx);
914 assert_eq!(
915 editor.display_text(cx),
916 "
917 impl Foo {⋯
918 }
919 "
920 .unindent(),
921 );
922
923 editor.unfold_lines(&UnfoldLines, window, cx);
924 assert_eq!(
925 editor.display_text(cx),
926 "
927 impl Foo {
928 // Hello!
929
930 fn a() {
931 1
932 }
933
934 fn b() {⋯
935 }
936
937 fn c() {⋯
938 }
939 }
940 "
941 .unindent(),
942 );
943
944 editor.unfold_lines(&UnfoldLines, window, cx);
945 assert_eq!(
946 editor.display_text(cx),
947 editor.buffer.read(cx).read(cx).text()
948 );
949 });
950}
951
952#[gpui::test]
953fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
954 init_test(cx, |_| {});
955
956 let editor = cx.add_window(|window, cx| {
957 let buffer = MultiBuffer::build_simple(
958 &"
959 class Foo:
960 # Hello!
961
962 def a():
963 print(1)
964
965 def b():
966 print(2)
967
968 def c():
969 print(3)
970 "
971 .unindent(),
972 cx,
973 );
974 build_editor(buffer.clone(), window, cx)
975 });
976
977 _ = editor.update(cx, |editor, window, cx| {
978 editor.change_selections(None, window, cx, |s| {
979 s.select_display_ranges([
980 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
981 ]);
982 });
983 editor.fold(&Fold, window, cx);
984 assert_eq!(
985 editor.display_text(cx),
986 "
987 class Foo:
988 # Hello!
989
990 def a():
991 print(1)
992
993 def b():⋯
994
995 def c():⋯
996 "
997 .unindent(),
998 );
999
1000 editor.fold(&Fold, window, cx);
1001 assert_eq!(
1002 editor.display_text(cx),
1003 "
1004 class Foo:⋯
1005 "
1006 .unindent(),
1007 );
1008
1009 editor.unfold_lines(&UnfoldLines, window, cx);
1010 assert_eq!(
1011 editor.display_text(cx),
1012 "
1013 class Foo:
1014 # Hello!
1015
1016 def a():
1017 print(1)
1018
1019 def b():⋯
1020
1021 def c():⋯
1022 "
1023 .unindent(),
1024 );
1025
1026 editor.unfold_lines(&UnfoldLines, window, cx);
1027 assert_eq!(
1028 editor.display_text(cx),
1029 editor.buffer.read(cx).read(cx).text()
1030 );
1031 });
1032}
1033
1034#[gpui::test]
1035fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1036 init_test(cx, |_| {});
1037
1038 let editor = cx.add_window(|window, cx| {
1039 let buffer = MultiBuffer::build_simple(
1040 &"
1041 class Foo:
1042 # Hello!
1043
1044 def a():
1045 print(1)
1046
1047 def b():
1048 print(2)
1049
1050
1051 def c():
1052 print(3)
1053
1054
1055 "
1056 .unindent(),
1057 cx,
1058 );
1059 build_editor(buffer.clone(), window, cx)
1060 });
1061
1062 _ = editor.update(cx, |editor, window, cx| {
1063 editor.change_selections(None, window, cx, |s| {
1064 s.select_display_ranges([
1065 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1066 ]);
1067 });
1068 editor.fold(&Fold, window, cx);
1069 assert_eq!(
1070 editor.display_text(cx),
1071 "
1072 class Foo:
1073 # Hello!
1074
1075 def a():
1076 print(1)
1077
1078 def b():⋯
1079
1080
1081 def c():⋯
1082
1083
1084 "
1085 .unindent(),
1086 );
1087
1088 editor.fold(&Fold, window, cx);
1089 assert_eq!(
1090 editor.display_text(cx),
1091 "
1092 class Foo:⋯
1093
1094
1095 "
1096 .unindent(),
1097 );
1098
1099 editor.unfold_lines(&UnfoldLines, window, cx);
1100 assert_eq!(
1101 editor.display_text(cx),
1102 "
1103 class Foo:
1104 # Hello!
1105
1106 def a():
1107 print(1)
1108
1109 def b():⋯
1110
1111
1112 def c():⋯
1113
1114
1115 "
1116 .unindent(),
1117 );
1118
1119 editor.unfold_lines(&UnfoldLines, window, cx);
1120 assert_eq!(
1121 editor.display_text(cx),
1122 editor.buffer.read(cx).read(cx).text()
1123 );
1124 });
1125}
1126
1127#[gpui::test]
1128fn test_fold_at_level(cx: &mut TestAppContext) {
1129 init_test(cx, |_| {});
1130
1131 let editor = cx.add_window(|window, cx| {
1132 let buffer = MultiBuffer::build_simple(
1133 &"
1134 class Foo:
1135 # Hello!
1136
1137 def a():
1138 print(1)
1139
1140 def b():
1141 print(2)
1142
1143
1144 class Bar:
1145 # World!
1146
1147 def a():
1148 print(1)
1149
1150 def b():
1151 print(2)
1152
1153
1154 "
1155 .unindent(),
1156 cx,
1157 );
1158 build_editor(buffer.clone(), window, cx)
1159 });
1160
1161 _ = editor.update(cx, |editor, window, cx| {
1162 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1163 assert_eq!(
1164 editor.display_text(cx),
1165 "
1166 class Foo:
1167 # Hello!
1168
1169 def a():⋯
1170
1171 def b():⋯
1172
1173
1174 class Bar:
1175 # World!
1176
1177 def a():⋯
1178
1179 def b():⋯
1180
1181
1182 "
1183 .unindent(),
1184 );
1185
1186 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1187 assert_eq!(
1188 editor.display_text(cx),
1189 "
1190 class Foo:⋯
1191
1192
1193 class Bar:⋯
1194
1195
1196 "
1197 .unindent(),
1198 );
1199
1200 editor.unfold_all(&UnfoldAll, window, cx);
1201 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1202 assert_eq!(
1203 editor.display_text(cx),
1204 "
1205 class Foo:
1206 # Hello!
1207
1208 def a():
1209 print(1)
1210
1211 def b():
1212 print(2)
1213
1214
1215 class Bar:
1216 # World!
1217
1218 def a():
1219 print(1)
1220
1221 def b():
1222 print(2)
1223
1224
1225 "
1226 .unindent(),
1227 );
1228
1229 assert_eq!(
1230 editor.display_text(cx),
1231 editor.buffer.read(cx).read(cx).text()
1232 );
1233 });
1234}
1235
1236#[gpui::test]
1237fn test_move_cursor(cx: &mut TestAppContext) {
1238 init_test(cx, |_| {});
1239
1240 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1241 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1242
1243 buffer.update(cx, |buffer, cx| {
1244 buffer.edit(
1245 vec![
1246 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1247 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1248 ],
1249 None,
1250 cx,
1251 );
1252 });
1253 _ = editor.update(cx, |editor, window, cx| {
1254 assert_eq!(
1255 editor.selections.display_ranges(cx),
1256 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1257 );
1258
1259 editor.move_down(&MoveDown, window, cx);
1260 assert_eq!(
1261 editor.selections.display_ranges(cx),
1262 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1263 );
1264
1265 editor.move_right(&MoveRight, window, cx);
1266 assert_eq!(
1267 editor.selections.display_ranges(cx),
1268 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1269 );
1270
1271 editor.move_left(&MoveLeft, window, cx);
1272 assert_eq!(
1273 editor.selections.display_ranges(cx),
1274 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1275 );
1276
1277 editor.move_up(&MoveUp, window, cx);
1278 assert_eq!(
1279 editor.selections.display_ranges(cx),
1280 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1281 );
1282
1283 editor.move_to_end(&MoveToEnd, window, cx);
1284 assert_eq!(
1285 editor.selections.display_ranges(cx),
1286 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1287 );
1288
1289 editor.move_to_beginning(&MoveToBeginning, window, cx);
1290 assert_eq!(
1291 editor.selections.display_ranges(cx),
1292 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1293 );
1294
1295 editor.change_selections(None, window, cx, |s| {
1296 s.select_display_ranges([
1297 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1298 ]);
1299 });
1300 editor.select_to_beginning(&SelectToBeginning, window, cx);
1301 assert_eq!(
1302 editor.selections.display_ranges(cx),
1303 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1304 );
1305
1306 editor.select_to_end(&SelectToEnd, window, cx);
1307 assert_eq!(
1308 editor.selections.display_ranges(cx),
1309 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1310 );
1311 });
1312}
1313
1314// TODO: Re-enable this test
1315#[cfg(target_os = "macos")]
1316#[gpui::test]
1317fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1318 init_test(cx, |_| {});
1319
1320 let editor = cx.add_window(|window, cx| {
1321 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1322 build_editor(buffer.clone(), window, cx)
1323 });
1324
1325 assert_eq!('🟥'.len_utf8(), 4);
1326 assert_eq!('α'.len_utf8(), 2);
1327
1328 _ = editor.update(cx, |editor, window, cx| {
1329 editor.fold_creases(
1330 vec![
1331 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1332 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1333 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1334 ],
1335 true,
1336 window,
1337 cx,
1338 );
1339 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1340
1341 editor.move_right(&MoveRight, window, cx);
1342 assert_eq!(
1343 editor.selections.display_ranges(cx),
1344 &[empty_range(0, "🟥".len())]
1345 );
1346 editor.move_right(&MoveRight, window, cx);
1347 assert_eq!(
1348 editor.selections.display_ranges(cx),
1349 &[empty_range(0, "🟥🟧".len())]
1350 );
1351 editor.move_right(&MoveRight, window, cx);
1352 assert_eq!(
1353 editor.selections.display_ranges(cx),
1354 &[empty_range(0, "🟥🟧⋯".len())]
1355 );
1356
1357 editor.move_down(&MoveDown, window, cx);
1358 assert_eq!(
1359 editor.selections.display_ranges(cx),
1360 &[empty_range(1, "ab⋯e".len())]
1361 );
1362 editor.move_left(&MoveLeft, window, cx);
1363 assert_eq!(
1364 editor.selections.display_ranges(cx),
1365 &[empty_range(1, "ab⋯".len())]
1366 );
1367 editor.move_left(&MoveLeft, window, cx);
1368 assert_eq!(
1369 editor.selections.display_ranges(cx),
1370 &[empty_range(1, "ab".len())]
1371 );
1372 editor.move_left(&MoveLeft, window, cx);
1373 assert_eq!(
1374 editor.selections.display_ranges(cx),
1375 &[empty_range(1, "a".len())]
1376 );
1377
1378 editor.move_down(&MoveDown, window, cx);
1379 assert_eq!(
1380 editor.selections.display_ranges(cx),
1381 &[empty_range(2, "α".len())]
1382 );
1383 editor.move_right(&MoveRight, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[empty_range(2, "αβ".len())]
1387 );
1388 editor.move_right(&MoveRight, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[empty_range(2, "αβ⋯".len())]
1392 );
1393 editor.move_right(&MoveRight, window, cx);
1394 assert_eq!(
1395 editor.selections.display_ranges(cx),
1396 &[empty_range(2, "αβ⋯ε".len())]
1397 );
1398
1399 editor.move_up(&MoveUp, window, cx);
1400 assert_eq!(
1401 editor.selections.display_ranges(cx),
1402 &[empty_range(1, "ab⋯e".len())]
1403 );
1404 editor.move_down(&MoveDown, window, cx);
1405 assert_eq!(
1406 editor.selections.display_ranges(cx),
1407 &[empty_range(2, "αβ⋯ε".len())]
1408 );
1409 editor.move_up(&MoveUp, window, cx);
1410 assert_eq!(
1411 editor.selections.display_ranges(cx),
1412 &[empty_range(1, "ab⋯e".len())]
1413 );
1414
1415 editor.move_up(&MoveUp, window, cx);
1416 assert_eq!(
1417 editor.selections.display_ranges(cx),
1418 &[empty_range(0, "🟥🟧".len())]
1419 );
1420 editor.move_left(&MoveLeft, window, cx);
1421 assert_eq!(
1422 editor.selections.display_ranges(cx),
1423 &[empty_range(0, "🟥".len())]
1424 );
1425 editor.move_left(&MoveLeft, window, cx);
1426 assert_eq!(
1427 editor.selections.display_ranges(cx),
1428 &[empty_range(0, "".len())]
1429 );
1430 });
1431}
1432
1433#[gpui::test]
1434fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1435 init_test(cx, |_| {});
1436
1437 let editor = cx.add_window(|window, cx| {
1438 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1439 build_editor(buffer.clone(), window, cx)
1440 });
1441 _ = editor.update(cx, |editor, window, cx| {
1442 editor.change_selections(None, window, cx, |s| {
1443 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1444 });
1445
1446 // moving above start of document should move selection to start of document,
1447 // but the next move down should still be at the original goal_x
1448 editor.move_up(&MoveUp, window, cx);
1449 assert_eq!(
1450 editor.selections.display_ranges(cx),
1451 &[empty_range(0, "".len())]
1452 );
1453
1454 editor.move_down(&MoveDown, window, cx);
1455 assert_eq!(
1456 editor.selections.display_ranges(cx),
1457 &[empty_range(1, "abcd".len())]
1458 );
1459
1460 editor.move_down(&MoveDown, window, cx);
1461 assert_eq!(
1462 editor.selections.display_ranges(cx),
1463 &[empty_range(2, "αβγ".len())]
1464 );
1465
1466 editor.move_down(&MoveDown, window, cx);
1467 assert_eq!(
1468 editor.selections.display_ranges(cx),
1469 &[empty_range(3, "abcd".len())]
1470 );
1471
1472 editor.move_down(&MoveDown, window, cx);
1473 assert_eq!(
1474 editor.selections.display_ranges(cx),
1475 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1476 );
1477
1478 // moving past end of document should not change goal_x
1479 editor.move_down(&MoveDown, window, cx);
1480 assert_eq!(
1481 editor.selections.display_ranges(cx),
1482 &[empty_range(5, "".len())]
1483 );
1484
1485 editor.move_down(&MoveDown, window, cx);
1486 assert_eq!(
1487 editor.selections.display_ranges(cx),
1488 &[empty_range(5, "".len())]
1489 );
1490
1491 editor.move_up(&MoveUp, window, cx);
1492 assert_eq!(
1493 editor.selections.display_ranges(cx),
1494 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1495 );
1496
1497 editor.move_up(&MoveUp, window, cx);
1498 assert_eq!(
1499 editor.selections.display_ranges(cx),
1500 &[empty_range(3, "abcd".len())]
1501 );
1502
1503 editor.move_up(&MoveUp, window, cx);
1504 assert_eq!(
1505 editor.selections.display_ranges(cx),
1506 &[empty_range(2, "αβγ".len())]
1507 );
1508 });
1509}
1510
1511#[gpui::test]
1512fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1513 init_test(cx, |_| {});
1514 let move_to_beg = MoveToBeginningOfLine {
1515 stop_at_soft_wraps: true,
1516 };
1517
1518 let move_to_end = MoveToEndOfLine {
1519 stop_at_soft_wraps: true,
1520 };
1521
1522 let editor = cx.add_window(|window, cx| {
1523 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1524 build_editor(buffer, window, cx)
1525 });
1526 _ = editor.update(cx, |editor, window, cx| {
1527 editor.change_selections(None, window, cx, |s| {
1528 s.select_display_ranges([
1529 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1530 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1531 ]);
1532 });
1533 });
1534
1535 _ = editor.update(cx, |editor, window, cx| {
1536 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1537 assert_eq!(
1538 editor.selections.display_ranges(cx),
1539 &[
1540 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1541 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1542 ]
1543 );
1544 });
1545
1546 _ = editor.update(cx, |editor, window, cx| {
1547 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1548 assert_eq!(
1549 editor.selections.display_ranges(cx),
1550 &[
1551 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1552 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1553 ]
1554 );
1555 });
1556
1557 _ = editor.update(cx, |editor, window, cx| {
1558 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1559 assert_eq!(
1560 editor.selections.display_ranges(cx),
1561 &[
1562 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1563 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1564 ]
1565 );
1566 });
1567
1568 _ = editor.update(cx, |editor, window, cx| {
1569 editor.move_to_end_of_line(&move_to_end, window, cx);
1570 assert_eq!(
1571 editor.selections.display_ranges(cx),
1572 &[
1573 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1574 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1575 ]
1576 );
1577 });
1578
1579 // Moving to the end of line again is a no-op.
1580 _ = editor.update(cx, |editor, window, cx| {
1581 editor.move_to_end_of_line(&move_to_end, window, cx);
1582 assert_eq!(
1583 editor.selections.display_ranges(cx),
1584 &[
1585 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1586 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1587 ]
1588 );
1589 });
1590
1591 _ = editor.update(cx, |editor, window, cx| {
1592 editor.move_left(&MoveLeft, window, cx);
1593 editor.select_to_beginning_of_line(
1594 &SelectToBeginningOfLine {
1595 stop_at_soft_wraps: true,
1596 },
1597 window,
1598 cx,
1599 );
1600 assert_eq!(
1601 editor.selections.display_ranges(cx),
1602 &[
1603 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1604 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1605 ]
1606 );
1607 });
1608
1609 _ = editor.update(cx, |editor, window, cx| {
1610 editor.select_to_beginning_of_line(
1611 &SelectToBeginningOfLine {
1612 stop_at_soft_wraps: true,
1613 },
1614 window,
1615 cx,
1616 );
1617 assert_eq!(
1618 editor.selections.display_ranges(cx),
1619 &[
1620 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1621 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1622 ]
1623 );
1624 });
1625
1626 _ = editor.update(cx, |editor, window, cx| {
1627 editor.select_to_beginning_of_line(
1628 &SelectToBeginningOfLine {
1629 stop_at_soft_wraps: true,
1630 },
1631 window,
1632 cx,
1633 );
1634 assert_eq!(
1635 editor.selections.display_ranges(cx),
1636 &[
1637 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1638 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1639 ]
1640 );
1641 });
1642
1643 _ = editor.update(cx, |editor, window, cx| {
1644 editor.select_to_end_of_line(
1645 &SelectToEndOfLine {
1646 stop_at_soft_wraps: true,
1647 },
1648 window,
1649 cx,
1650 );
1651 assert_eq!(
1652 editor.selections.display_ranges(cx),
1653 &[
1654 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1655 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1656 ]
1657 );
1658 });
1659
1660 _ = editor.update(cx, |editor, window, cx| {
1661 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1662 assert_eq!(editor.display_text(cx), "ab\n de");
1663 assert_eq!(
1664 editor.selections.display_ranges(cx),
1665 &[
1666 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1667 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1668 ]
1669 );
1670 });
1671
1672 _ = editor.update(cx, |editor, window, cx| {
1673 editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx);
1674 assert_eq!(editor.display_text(cx), "\n");
1675 assert_eq!(
1676 editor.selections.display_ranges(cx),
1677 &[
1678 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1679 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1680 ]
1681 );
1682 });
1683}
1684
1685#[gpui::test]
1686fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1687 init_test(cx, |_| {});
1688 let move_to_beg = MoveToBeginningOfLine {
1689 stop_at_soft_wraps: false,
1690 };
1691
1692 let move_to_end = MoveToEndOfLine {
1693 stop_at_soft_wraps: false,
1694 };
1695
1696 let editor = cx.add_window(|window, cx| {
1697 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1698 build_editor(buffer, window, cx)
1699 });
1700
1701 _ = editor.update(cx, |editor, window, cx| {
1702 editor.set_wrap_width(Some(140.0.into()), cx);
1703
1704 // We expect the following lines after wrapping
1705 // ```
1706 // thequickbrownfox
1707 // jumpedoverthelazydo
1708 // gs
1709 // ```
1710 // The final `gs` was soft-wrapped onto a new line.
1711 assert_eq!(
1712 "thequickbrownfox\njumpedoverthelaz\nydogs",
1713 editor.display_text(cx),
1714 );
1715
1716 // First, let's assert behavior on the first line, that was not soft-wrapped.
1717 // Start the cursor at the `k` on the first line
1718 editor.change_selections(None, window, cx, |s| {
1719 s.select_display_ranges([
1720 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1721 ]);
1722 });
1723
1724 // Moving to the beginning of the line should put us at the beginning of the line.
1725 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1726 assert_eq!(
1727 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1728 editor.selections.display_ranges(cx)
1729 );
1730
1731 // Moving to the end of the line should put us at the end of the line.
1732 editor.move_to_end_of_line(&move_to_end, window, cx);
1733 assert_eq!(
1734 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1735 editor.selections.display_ranges(cx)
1736 );
1737
1738 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1739 // Start the cursor at the last line (`y` that was wrapped to a new line)
1740 editor.change_selections(None, window, cx, |s| {
1741 s.select_display_ranges([
1742 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1743 ]);
1744 });
1745
1746 // Moving to the beginning of the line should put us at the start of the second line of
1747 // display text, i.e., the `j`.
1748 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1749 assert_eq!(
1750 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1751 editor.selections.display_ranges(cx)
1752 );
1753
1754 // Moving to the beginning of the line again should be a no-op.
1755 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1756 assert_eq!(
1757 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1758 editor.selections.display_ranges(cx)
1759 );
1760
1761 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1762 // next display line.
1763 editor.move_to_end_of_line(&move_to_end, window, cx);
1764 assert_eq!(
1765 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1766 editor.selections.display_ranges(cx)
1767 );
1768
1769 // Moving to the end of the line again should be a no-op.
1770 editor.move_to_end_of_line(&move_to_end, window, cx);
1771 assert_eq!(
1772 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1773 editor.selections.display_ranges(cx)
1774 );
1775 });
1776}
1777
1778#[gpui::test]
1779fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1780 init_test(cx, |_| {});
1781
1782 let editor = cx.add_window(|window, cx| {
1783 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1784 build_editor(buffer, window, cx)
1785 });
1786 _ = editor.update(cx, |editor, window, cx| {
1787 editor.change_selections(None, window, cx, |s| {
1788 s.select_display_ranges([
1789 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1790 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1791 ])
1792 });
1793
1794 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1795 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1796
1797 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1798 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1799
1800 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1801 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1802
1803 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1804 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1805
1806 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1807 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1808
1809 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1810 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1811
1812 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1813 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1814
1815 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1816 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1817
1818 editor.move_right(&MoveRight, window, cx);
1819 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1820 assert_selection_ranges(
1821 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1822 editor,
1823 cx,
1824 );
1825
1826 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1827 assert_selection_ranges(
1828 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1829 editor,
1830 cx,
1831 );
1832
1833 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1834 assert_selection_ranges(
1835 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1836 editor,
1837 cx,
1838 );
1839 });
1840}
1841
1842#[gpui::test]
1843fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1844 init_test(cx, |_| {});
1845
1846 let editor = cx.add_window(|window, cx| {
1847 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1848 build_editor(buffer, window, cx)
1849 });
1850
1851 _ = editor.update(cx, |editor, window, cx| {
1852 editor.set_wrap_width(Some(140.0.into()), cx);
1853 assert_eq!(
1854 editor.display_text(cx),
1855 "use one::{\n two::three::\n four::five\n};"
1856 );
1857
1858 editor.change_selections(None, window, cx, |s| {
1859 s.select_display_ranges([
1860 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1861 ]);
1862 });
1863
1864 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1865 assert_eq!(
1866 editor.selections.display_ranges(cx),
1867 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1868 );
1869
1870 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1871 assert_eq!(
1872 editor.selections.display_ranges(cx),
1873 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1874 );
1875
1876 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1877 assert_eq!(
1878 editor.selections.display_ranges(cx),
1879 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1880 );
1881
1882 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1883 assert_eq!(
1884 editor.selections.display_ranges(cx),
1885 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
1886 );
1887
1888 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1889 assert_eq!(
1890 editor.selections.display_ranges(cx),
1891 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1892 );
1893
1894 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1895 assert_eq!(
1896 editor.selections.display_ranges(cx),
1897 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1898 );
1899 });
1900}
1901
1902#[gpui::test]
1903async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1904 init_test(cx, |_| {});
1905 let mut cx = EditorTestContext::new(cx).await;
1906
1907 let line_height = cx.editor(|editor, window, _| {
1908 editor
1909 .style()
1910 .unwrap()
1911 .text
1912 .line_height_in_pixels(window.rem_size())
1913 });
1914 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
1915
1916 cx.set_state(
1917 &r#"ˇone
1918 two
1919
1920 three
1921 fourˇ
1922 five
1923
1924 six"#
1925 .unindent(),
1926 );
1927
1928 cx.update_editor(|editor, window, cx| {
1929 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1930 });
1931 cx.assert_editor_state(
1932 &r#"one
1933 two
1934 ˇ
1935 three
1936 four
1937 five
1938 ˇ
1939 six"#
1940 .unindent(),
1941 );
1942
1943 cx.update_editor(|editor, window, cx| {
1944 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1945 });
1946 cx.assert_editor_state(
1947 &r#"one
1948 two
1949
1950 three
1951 four
1952 five
1953 ˇ
1954 sixˇ"#
1955 .unindent(),
1956 );
1957
1958 cx.update_editor(|editor, window, cx| {
1959 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1960 });
1961 cx.assert_editor_state(
1962 &r#"one
1963 two
1964
1965 three
1966 four
1967 five
1968
1969 sixˇ"#
1970 .unindent(),
1971 );
1972
1973 cx.update_editor(|editor, window, cx| {
1974 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
1975 });
1976 cx.assert_editor_state(
1977 &r#"one
1978 two
1979
1980 three
1981 four
1982 five
1983 ˇ
1984 six"#
1985 .unindent(),
1986 );
1987
1988 cx.update_editor(|editor, window, cx| {
1989 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
1990 });
1991 cx.assert_editor_state(
1992 &r#"one
1993 two
1994 ˇ
1995 three
1996 four
1997 five
1998
1999 six"#
2000 .unindent(),
2001 );
2002
2003 cx.update_editor(|editor, window, cx| {
2004 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2005 });
2006 cx.assert_editor_state(
2007 &r#"ˇone
2008 two
2009
2010 three
2011 four
2012 five
2013
2014 six"#
2015 .unindent(),
2016 );
2017}
2018
2019#[gpui::test]
2020async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
2021 init_test(cx, |_| {});
2022 let mut cx = EditorTestContext::new(cx).await;
2023 let line_height = cx.editor(|editor, window, _| {
2024 editor
2025 .style()
2026 .unwrap()
2027 .text
2028 .line_height_in_pixels(window.rem_size())
2029 });
2030 let window = cx.window;
2031 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2032
2033 cx.set_state(
2034 r#"ˇone
2035 two
2036 three
2037 four
2038 five
2039 six
2040 seven
2041 eight
2042 nine
2043 ten
2044 "#,
2045 );
2046
2047 cx.update_editor(|editor, window, cx| {
2048 assert_eq!(
2049 editor.snapshot(window, cx).scroll_position(),
2050 gpui::Point::new(0., 0.)
2051 );
2052 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2053 assert_eq!(
2054 editor.snapshot(window, cx).scroll_position(),
2055 gpui::Point::new(0., 3.)
2056 );
2057 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2058 assert_eq!(
2059 editor.snapshot(window, cx).scroll_position(),
2060 gpui::Point::new(0., 6.)
2061 );
2062 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2063 assert_eq!(
2064 editor.snapshot(window, cx).scroll_position(),
2065 gpui::Point::new(0., 3.)
2066 );
2067
2068 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2069 assert_eq!(
2070 editor.snapshot(window, cx).scroll_position(),
2071 gpui::Point::new(0., 1.)
2072 );
2073 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2074 assert_eq!(
2075 editor.snapshot(window, cx).scroll_position(),
2076 gpui::Point::new(0., 3.)
2077 );
2078 });
2079}
2080
2081#[gpui::test]
2082async fn test_autoscroll(cx: &mut gpui::TestAppContext) {
2083 init_test(cx, |_| {});
2084 let mut cx = EditorTestContext::new(cx).await;
2085
2086 let line_height = cx.update_editor(|editor, window, cx| {
2087 editor.set_vertical_scroll_margin(2, cx);
2088 editor
2089 .style()
2090 .unwrap()
2091 .text
2092 .line_height_in_pixels(window.rem_size())
2093 });
2094 let window = cx.window;
2095 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2096
2097 cx.set_state(
2098 r#"ˇone
2099 two
2100 three
2101 four
2102 five
2103 six
2104 seven
2105 eight
2106 nine
2107 ten
2108 "#,
2109 );
2110 cx.update_editor(|editor, window, cx| {
2111 assert_eq!(
2112 editor.snapshot(window, cx).scroll_position(),
2113 gpui::Point::new(0., 0.0)
2114 );
2115 });
2116
2117 // Add a cursor below the visible area. Since both cursors cannot fit
2118 // on screen, the editor autoscrolls to reveal the newest cursor, and
2119 // allows the vertical scroll margin below that cursor.
2120 cx.update_editor(|editor, window, cx| {
2121 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2122 selections.select_ranges([
2123 Point::new(0, 0)..Point::new(0, 0),
2124 Point::new(6, 0)..Point::new(6, 0),
2125 ]);
2126 })
2127 });
2128 cx.update_editor(|editor, window, cx| {
2129 assert_eq!(
2130 editor.snapshot(window, cx).scroll_position(),
2131 gpui::Point::new(0., 3.0)
2132 );
2133 });
2134
2135 // Move down. The editor cursor scrolls down to track the newest cursor.
2136 cx.update_editor(|editor, window, cx| {
2137 editor.move_down(&Default::default(), window, cx);
2138 });
2139 cx.update_editor(|editor, window, cx| {
2140 assert_eq!(
2141 editor.snapshot(window, cx).scroll_position(),
2142 gpui::Point::new(0., 4.0)
2143 );
2144 });
2145
2146 // Add a cursor above the visible area. Since both cursors fit on screen,
2147 // the editor scrolls to show both.
2148 cx.update_editor(|editor, window, cx| {
2149 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2150 selections.select_ranges([
2151 Point::new(1, 0)..Point::new(1, 0),
2152 Point::new(6, 0)..Point::new(6, 0),
2153 ]);
2154 })
2155 });
2156 cx.update_editor(|editor, window, cx| {
2157 assert_eq!(
2158 editor.snapshot(window, cx).scroll_position(),
2159 gpui::Point::new(0., 1.0)
2160 );
2161 });
2162}
2163
2164#[gpui::test]
2165async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
2166 init_test(cx, |_| {});
2167 let mut cx = EditorTestContext::new(cx).await;
2168
2169 let line_height = cx.editor(|editor, window, _cx| {
2170 editor
2171 .style()
2172 .unwrap()
2173 .text
2174 .line_height_in_pixels(window.rem_size())
2175 });
2176 let window = cx.window;
2177 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2178 cx.set_state(
2179 &r#"
2180 ˇone
2181 two
2182 threeˇ
2183 four
2184 five
2185 six
2186 seven
2187 eight
2188 nine
2189 ten
2190 "#
2191 .unindent(),
2192 );
2193
2194 cx.update_editor(|editor, window, cx| {
2195 editor.move_page_down(&MovePageDown::default(), window, cx)
2196 });
2197 cx.assert_editor_state(
2198 &r#"
2199 one
2200 two
2201 three
2202 ˇfour
2203 five
2204 sixˇ
2205 seven
2206 eight
2207 nine
2208 ten
2209 "#
2210 .unindent(),
2211 );
2212
2213 cx.update_editor(|editor, window, cx| {
2214 editor.move_page_down(&MovePageDown::default(), window, cx)
2215 });
2216 cx.assert_editor_state(
2217 &r#"
2218 one
2219 two
2220 three
2221 four
2222 five
2223 six
2224 ˇseven
2225 eight
2226 nineˇ
2227 ten
2228 "#
2229 .unindent(),
2230 );
2231
2232 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2233 cx.assert_editor_state(
2234 &r#"
2235 one
2236 two
2237 three
2238 ˇfour
2239 five
2240 sixˇ
2241 seven
2242 eight
2243 nine
2244 ten
2245 "#
2246 .unindent(),
2247 );
2248
2249 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2250 cx.assert_editor_state(
2251 &r#"
2252 ˇone
2253 two
2254 threeˇ
2255 four
2256 five
2257 six
2258 seven
2259 eight
2260 nine
2261 ten
2262 "#
2263 .unindent(),
2264 );
2265
2266 // Test select collapsing
2267 cx.update_editor(|editor, window, cx| {
2268 editor.move_page_down(&MovePageDown::default(), window, cx);
2269 editor.move_page_down(&MovePageDown::default(), window, cx);
2270 editor.move_page_down(&MovePageDown::default(), window, cx);
2271 });
2272 cx.assert_editor_state(
2273 &r#"
2274 one
2275 two
2276 three
2277 four
2278 five
2279 six
2280 seven
2281 eight
2282 nine
2283 ˇten
2284 ˇ"#
2285 .unindent(),
2286 );
2287}
2288
2289#[gpui::test]
2290async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
2291 init_test(cx, |_| {});
2292 let mut cx = EditorTestContext::new(cx).await;
2293 cx.set_state("one «two threeˇ» four");
2294 cx.update_editor(|editor, window, cx| {
2295 editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx);
2296 assert_eq!(editor.text(cx), " four");
2297 });
2298}
2299
2300#[gpui::test]
2301fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2302 init_test(cx, |_| {});
2303
2304 let editor = cx.add_window(|window, cx| {
2305 let buffer = MultiBuffer::build_simple("one two three four", cx);
2306 build_editor(buffer.clone(), window, cx)
2307 });
2308
2309 _ = editor.update(cx, |editor, window, cx| {
2310 editor.change_selections(None, window, cx, |s| {
2311 s.select_display_ranges([
2312 // an empty selection - the preceding word fragment is deleted
2313 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2314 // characters selected - they are deleted
2315 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2316 ])
2317 });
2318 editor.delete_to_previous_word_start(
2319 &DeleteToPreviousWordStart {
2320 ignore_newlines: false,
2321 },
2322 window,
2323 cx,
2324 );
2325 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2326 });
2327
2328 _ = editor.update(cx, |editor, window, cx| {
2329 editor.change_selections(None, window, cx, |s| {
2330 s.select_display_ranges([
2331 // an empty selection - the following word fragment is deleted
2332 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2333 // characters selected - they are deleted
2334 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2335 ])
2336 });
2337 editor.delete_to_next_word_end(
2338 &DeleteToNextWordEnd {
2339 ignore_newlines: false,
2340 },
2341 window,
2342 cx,
2343 );
2344 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2345 });
2346}
2347
2348#[gpui::test]
2349fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2350 init_test(cx, |_| {});
2351
2352 let editor = cx.add_window(|window, cx| {
2353 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2354 build_editor(buffer.clone(), window, cx)
2355 });
2356 let del_to_prev_word_start = DeleteToPreviousWordStart {
2357 ignore_newlines: false,
2358 };
2359 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2360 ignore_newlines: true,
2361 };
2362
2363 _ = editor.update(cx, |editor, window, cx| {
2364 editor.change_selections(None, window, cx, |s| {
2365 s.select_display_ranges([
2366 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2367 ])
2368 });
2369 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2370 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2371 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2372 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2373 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2374 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2375 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2376 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2377 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2378 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2379 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2380 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2381 });
2382}
2383
2384#[gpui::test]
2385fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2386 init_test(cx, |_| {});
2387
2388 let editor = cx.add_window(|window, cx| {
2389 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2390 build_editor(buffer.clone(), window, cx)
2391 });
2392 let del_to_next_word_end = DeleteToNextWordEnd {
2393 ignore_newlines: false,
2394 };
2395 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2396 ignore_newlines: true,
2397 };
2398
2399 _ = editor.update(cx, |editor, window, cx| {
2400 editor.change_selections(None, window, cx, |s| {
2401 s.select_display_ranges([
2402 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2403 ])
2404 });
2405 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2406 assert_eq!(
2407 editor.buffer.read(cx).read(cx).text(),
2408 "one\n two\nthree\n four"
2409 );
2410 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2411 assert_eq!(
2412 editor.buffer.read(cx).read(cx).text(),
2413 "\n two\nthree\n four"
2414 );
2415 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2416 assert_eq!(
2417 editor.buffer.read(cx).read(cx).text(),
2418 "two\nthree\n four"
2419 );
2420 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2421 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2422 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2423 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2424 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2425 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2426 });
2427}
2428
2429#[gpui::test]
2430fn test_newline(cx: &mut TestAppContext) {
2431 init_test(cx, |_| {});
2432
2433 let editor = cx.add_window(|window, cx| {
2434 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2435 build_editor(buffer.clone(), window, cx)
2436 });
2437
2438 _ = editor.update(cx, |editor, window, cx| {
2439 editor.change_selections(None, window, cx, |s| {
2440 s.select_display_ranges([
2441 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2442 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2443 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2444 ])
2445 });
2446
2447 editor.newline(&Newline, window, cx);
2448 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2449 });
2450}
2451
2452#[gpui::test]
2453fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2454 init_test(cx, |_| {});
2455
2456 let editor = cx.add_window(|window, cx| {
2457 let buffer = MultiBuffer::build_simple(
2458 "
2459 a
2460 b(
2461 X
2462 )
2463 c(
2464 X
2465 )
2466 "
2467 .unindent()
2468 .as_str(),
2469 cx,
2470 );
2471 let mut editor = build_editor(buffer.clone(), window, cx);
2472 editor.change_selections(None, window, cx, |s| {
2473 s.select_ranges([
2474 Point::new(2, 4)..Point::new(2, 5),
2475 Point::new(5, 4)..Point::new(5, 5),
2476 ])
2477 });
2478 editor
2479 });
2480
2481 _ = editor.update(cx, |editor, window, cx| {
2482 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2483 editor.buffer.update(cx, |buffer, cx| {
2484 buffer.edit(
2485 [
2486 (Point::new(1, 2)..Point::new(3, 0), ""),
2487 (Point::new(4, 2)..Point::new(6, 0), ""),
2488 ],
2489 None,
2490 cx,
2491 );
2492 assert_eq!(
2493 buffer.read(cx).text(),
2494 "
2495 a
2496 b()
2497 c()
2498 "
2499 .unindent()
2500 );
2501 });
2502 assert_eq!(
2503 editor.selections.ranges(cx),
2504 &[
2505 Point::new(1, 2)..Point::new(1, 2),
2506 Point::new(2, 2)..Point::new(2, 2),
2507 ],
2508 );
2509
2510 editor.newline(&Newline, window, cx);
2511 assert_eq!(
2512 editor.text(cx),
2513 "
2514 a
2515 b(
2516 )
2517 c(
2518 )
2519 "
2520 .unindent()
2521 );
2522
2523 // The selections are moved after the inserted newlines
2524 assert_eq!(
2525 editor.selections.ranges(cx),
2526 &[
2527 Point::new(2, 0)..Point::new(2, 0),
2528 Point::new(4, 0)..Point::new(4, 0),
2529 ],
2530 );
2531 });
2532}
2533
2534#[gpui::test]
2535async fn test_newline_above(cx: &mut gpui::TestAppContext) {
2536 init_test(cx, |settings| {
2537 settings.defaults.tab_size = NonZeroU32::new(4)
2538 });
2539
2540 let language = Arc::new(
2541 Language::new(
2542 LanguageConfig::default(),
2543 Some(tree_sitter_rust::LANGUAGE.into()),
2544 )
2545 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2546 .unwrap(),
2547 );
2548
2549 let mut cx = EditorTestContext::new(cx).await;
2550 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2551 cx.set_state(indoc! {"
2552 const a: ˇA = (
2553 (ˇ
2554 «const_functionˇ»(ˇ),
2555 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2556 )ˇ
2557 ˇ);ˇ
2558 "});
2559
2560 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2561 cx.assert_editor_state(indoc! {"
2562 ˇ
2563 const a: A = (
2564 ˇ
2565 (
2566 ˇ
2567 ˇ
2568 const_function(),
2569 ˇ
2570 ˇ
2571 ˇ
2572 ˇ
2573 something_else,
2574 ˇ
2575 )
2576 ˇ
2577 ˇ
2578 );
2579 "});
2580}
2581
2582#[gpui::test]
2583async fn test_newline_below(cx: &mut gpui::TestAppContext) {
2584 init_test(cx, |settings| {
2585 settings.defaults.tab_size = NonZeroU32::new(4)
2586 });
2587
2588 let language = Arc::new(
2589 Language::new(
2590 LanguageConfig::default(),
2591 Some(tree_sitter_rust::LANGUAGE.into()),
2592 )
2593 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2594 .unwrap(),
2595 );
2596
2597 let mut cx = EditorTestContext::new(cx).await;
2598 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2599 cx.set_state(indoc! {"
2600 const a: ˇA = (
2601 (ˇ
2602 «const_functionˇ»(ˇ),
2603 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2604 )ˇ
2605 ˇ);ˇ
2606 "});
2607
2608 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2609 cx.assert_editor_state(indoc! {"
2610 const a: A = (
2611 ˇ
2612 (
2613 ˇ
2614 const_function(),
2615 ˇ
2616 ˇ
2617 something_else,
2618 ˇ
2619 ˇ
2620 ˇ
2621 ˇ
2622 )
2623 ˇ
2624 );
2625 ˇ
2626 ˇ
2627 "});
2628}
2629
2630#[gpui::test]
2631async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
2632 init_test(cx, |settings| {
2633 settings.defaults.tab_size = NonZeroU32::new(4)
2634 });
2635
2636 let language = Arc::new(Language::new(
2637 LanguageConfig {
2638 line_comments: vec!["//".into()],
2639 ..LanguageConfig::default()
2640 },
2641 None,
2642 ));
2643 {
2644 let mut cx = EditorTestContext::new(cx).await;
2645 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2646 cx.set_state(indoc! {"
2647 // Fooˇ
2648 "});
2649
2650 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2651 cx.assert_editor_state(indoc! {"
2652 // Foo
2653 //ˇ
2654 "});
2655 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2656 cx.set_state(indoc! {"
2657 ˇ// Foo
2658 "});
2659 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2660 cx.assert_editor_state(indoc! {"
2661
2662 ˇ// Foo
2663 "});
2664 }
2665 // Ensure that comment continuations can be disabled.
2666 update_test_language_settings(cx, |settings| {
2667 settings.defaults.extend_comment_on_newline = Some(false);
2668 });
2669 let mut cx = EditorTestContext::new(cx).await;
2670 cx.set_state(indoc! {"
2671 // Fooˇ
2672 "});
2673 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2674 cx.assert_editor_state(indoc! {"
2675 // Foo
2676 ˇ
2677 "});
2678}
2679
2680#[gpui::test]
2681fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2682 init_test(cx, |_| {});
2683
2684 let editor = cx.add_window(|window, cx| {
2685 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2686 let mut editor = build_editor(buffer.clone(), window, cx);
2687 editor.change_selections(None, window, cx, |s| {
2688 s.select_ranges([3..4, 11..12, 19..20])
2689 });
2690 editor
2691 });
2692
2693 _ = editor.update(cx, |editor, window, cx| {
2694 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2695 editor.buffer.update(cx, |buffer, cx| {
2696 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2697 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2698 });
2699 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2700
2701 editor.insert("Z", window, cx);
2702 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2703
2704 // The selections are moved after the inserted characters
2705 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2706 });
2707}
2708
2709#[gpui::test]
2710async fn test_tab(cx: &mut gpui::TestAppContext) {
2711 init_test(cx, |settings| {
2712 settings.defaults.tab_size = NonZeroU32::new(3)
2713 });
2714
2715 let mut cx = EditorTestContext::new(cx).await;
2716 cx.set_state(indoc! {"
2717 ˇabˇc
2718 ˇ🏀ˇ🏀ˇefg
2719 dˇ
2720 "});
2721 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2722 cx.assert_editor_state(indoc! {"
2723 ˇab ˇc
2724 ˇ🏀 ˇ🏀 ˇefg
2725 d ˇ
2726 "});
2727
2728 cx.set_state(indoc! {"
2729 a
2730 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2731 "});
2732 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2733 cx.assert_editor_state(indoc! {"
2734 a
2735 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2736 "});
2737}
2738
2739#[gpui::test]
2740async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) {
2741 init_test(cx, |_| {});
2742
2743 let mut cx = EditorTestContext::new(cx).await;
2744 let language = Arc::new(
2745 Language::new(
2746 LanguageConfig::default(),
2747 Some(tree_sitter_rust::LANGUAGE.into()),
2748 )
2749 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2750 .unwrap(),
2751 );
2752 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2753
2754 // cursors that are already at the suggested indent level insert
2755 // a soft tab. cursors that are to the left of the suggested indent
2756 // auto-indent their line.
2757 cx.set_state(indoc! {"
2758 ˇ
2759 const a: B = (
2760 c(
2761 d(
2762 ˇ
2763 )
2764 ˇ
2765 ˇ )
2766 );
2767 "});
2768 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2769 cx.assert_editor_state(indoc! {"
2770 ˇ
2771 const a: B = (
2772 c(
2773 d(
2774 ˇ
2775 )
2776 ˇ
2777 ˇ)
2778 );
2779 "});
2780
2781 // handle auto-indent when there are multiple cursors on the same line
2782 cx.set_state(indoc! {"
2783 const a: B = (
2784 c(
2785 ˇ ˇ
2786 ˇ )
2787 );
2788 "});
2789 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2790 cx.assert_editor_state(indoc! {"
2791 const a: B = (
2792 c(
2793 ˇ
2794 ˇ)
2795 );
2796 "});
2797}
2798
2799#[gpui::test]
2800async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
2801 init_test(cx, |settings| {
2802 settings.defaults.tab_size = NonZeroU32::new(4)
2803 });
2804
2805 let language = Arc::new(
2806 Language::new(
2807 LanguageConfig::default(),
2808 Some(tree_sitter_rust::LANGUAGE.into()),
2809 )
2810 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2811 .unwrap(),
2812 );
2813
2814 let mut cx = EditorTestContext::new(cx).await;
2815 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2816 cx.set_state(indoc! {"
2817 fn a() {
2818 if b {
2819 \t ˇc
2820 }
2821 }
2822 "});
2823
2824 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2825 cx.assert_editor_state(indoc! {"
2826 fn a() {
2827 if b {
2828 ˇc
2829 }
2830 }
2831 "});
2832}
2833
2834#[gpui::test]
2835async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
2836 init_test(cx, |settings| {
2837 settings.defaults.tab_size = NonZeroU32::new(4);
2838 });
2839
2840 let mut cx = EditorTestContext::new(cx).await;
2841
2842 cx.set_state(indoc! {"
2843 «oneˇ» «twoˇ»
2844 three
2845 four
2846 "});
2847 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2848 cx.assert_editor_state(indoc! {"
2849 «oneˇ» «twoˇ»
2850 three
2851 four
2852 "});
2853
2854 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2855 cx.assert_editor_state(indoc! {"
2856 «oneˇ» «twoˇ»
2857 three
2858 four
2859 "});
2860
2861 // select across line ending
2862 cx.set_state(indoc! {"
2863 one two
2864 t«hree
2865 ˇ» four
2866 "});
2867 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2868 cx.assert_editor_state(indoc! {"
2869 one two
2870 t«hree
2871 ˇ» four
2872 "});
2873
2874 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2875 cx.assert_editor_state(indoc! {"
2876 one two
2877 t«hree
2878 ˇ» four
2879 "});
2880
2881 // Ensure that indenting/outdenting works when the cursor is at column 0.
2882 cx.set_state(indoc! {"
2883 one two
2884 ˇthree
2885 four
2886 "});
2887 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2888 cx.assert_editor_state(indoc! {"
2889 one two
2890 ˇthree
2891 four
2892 "});
2893
2894 cx.set_state(indoc! {"
2895 one two
2896 ˇ three
2897 four
2898 "});
2899 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2900 cx.assert_editor_state(indoc! {"
2901 one two
2902 ˇthree
2903 four
2904 "});
2905}
2906
2907#[gpui::test]
2908async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
2909 init_test(cx, |settings| {
2910 settings.defaults.hard_tabs = Some(true);
2911 });
2912
2913 let mut cx = EditorTestContext::new(cx).await;
2914
2915 // select two ranges on one line
2916 cx.set_state(indoc! {"
2917 «oneˇ» «twoˇ»
2918 three
2919 four
2920 "});
2921 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2922 cx.assert_editor_state(indoc! {"
2923 \t«oneˇ» «twoˇ»
2924 three
2925 four
2926 "});
2927 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2928 cx.assert_editor_state(indoc! {"
2929 \t\t«oneˇ» «twoˇ»
2930 three
2931 four
2932 "});
2933 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2934 cx.assert_editor_state(indoc! {"
2935 \t«oneˇ» «twoˇ»
2936 three
2937 four
2938 "});
2939 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2940 cx.assert_editor_state(indoc! {"
2941 «oneˇ» «twoˇ»
2942 three
2943 four
2944 "});
2945
2946 // select across a line ending
2947 cx.set_state(indoc! {"
2948 one two
2949 t«hree
2950 ˇ»four
2951 "});
2952 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2953 cx.assert_editor_state(indoc! {"
2954 one two
2955 \tt«hree
2956 ˇ»four
2957 "});
2958 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2959 cx.assert_editor_state(indoc! {"
2960 one two
2961 \t\tt«hree
2962 ˇ»four
2963 "});
2964 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2965 cx.assert_editor_state(indoc! {"
2966 one two
2967 \tt«hree
2968 ˇ»four
2969 "});
2970 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2971 cx.assert_editor_state(indoc! {"
2972 one two
2973 t«hree
2974 ˇ»four
2975 "});
2976
2977 // Ensure that indenting/outdenting works when the cursor is at column 0.
2978 cx.set_state(indoc! {"
2979 one two
2980 ˇthree
2981 four
2982 "});
2983 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2984 cx.assert_editor_state(indoc! {"
2985 one two
2986 ˇthree
2987 four
2988 "});
2989 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2990 cx.assert_editor_state(indoc! {"
2991 one two
2992 \tˇthree
2993 four
2994 "});
2995 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2996 cx.assert_editor_state(indoc! {"
2997 one two
2998 ˇthree
2999 four
3000 "});
3001}
3002
3003#[gpui::test]
3004fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3005 init_test(cx, |settings| {
3006 settings.languages.extend([
3007 (
3008 "TOML".into(),
3009 LanguageSettingsContent {
3010 tab_size: NonZeroU32::new(2),
3011 ..Default::default()
3012 },
3013 ),
3014 (
3015 "Rust".into(),
3016 LanguageSettingsContent {
3017 tab_size: NonZeroU32::new(4),
3018 ..Default::default()
3019 },
3020 ),
3021 ]);
3022 });
3023
3024 let toml_language = Arc::new(Language::new(
3025 LanguageConfig {
3026 name: "TOML".into(),
3027 ..Default::default()
3028 },
3029 None,
3030 ));
3031 let rust_language = Arc::new(Language::new(
3032 LanguageConfig {
3033 name: "Rust".into(),
3034 ..Default::default()
3035 },
3036 None,
3037 ));
3038
3039 let toml_buffer =
3040 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3041 let rust_buffer =
3042 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3043 let multibuffer = cx.new(|cx| {
3044 let mut multibuffer = MultiBuffer::new(ReadWrite);
3045 multibuffer.push_excerpts(
3046 toml_buffer.clone(),
3047 [ExcerptRange {
3048 context: Point::new(0, 0)..Point::new(2, 0),
3049 primary: None,
3050 }],
3051 cx,
3052 );
3053 multibuffer.push_excerpts(
3054 rust_buffer.clone(),
3055 [ExcerptRange {
3056 context: Point::new(0, 0)..Point::new(1, 0),
3057 primary: None,
3058 }],
3059 cx,
3060 );
3061 multibuffer
3062 });
3063
3064 cx.add_window(|window, cx| {
3065 let mut editor = build_editor(multibuffer, window, cx);
3066
3067 assert_eq!(
3068 editor.text(cx),
3069 indoc! {"
3070 a = 1
3071 b = 2
3072
3073 const c: usize = 3;
3074 "}
3075 );
3076
3077 select_ranges(
3078 &mut editor,
3079 indoc! {"
3080 «aˇ» = 1
3081 b = 2
3082
3083 «const c:ˇ» usize = 3;
3084 "},
3085 window,
3086 cx,
3087 );
3088
3089 editor.tab(&Tab, window, cx);
3090 assert_text_with_selections(
3091 &mut editor,
3092 indoc! {"
3093 «aˇ» = 1
3094 b = 2
3095
3096 «const c:ˇ» usize = 3;
3097 "},
3098 cx,
3099 );
3100 editor.tab_prev(&TabPrev, window, cx);
3101 assert_text_with_selections(
3102 &mut editor,
3103 indoc! {"
3104 «aˇ» = 1
3105 b = 2
3106
3107 «const c:ˇ» usize = 3;
3108 "},
3109 cx,
3110 );
3111
3112 editor
3113 });
3114}
3115
3116#[gpui::test]
3117async fn test_backspace(cx: &mut gpui::TestAppContext) {
3118 init_test(cx, |_| {});
3119
3120 let mut cx = EditorTestContext::new(cx).await;
3121
3122 // Basic backspace
3123 cx.set_state(indoc! {"
3124 onˇe two three
3125 fou«rˇ» five six
3126 seven «ˇeight nine
3127 »ten
3128 "});
3129 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3130 cx.assert_editor_state(indoc! {"
3131 oˇe two three
3132 fouˇ five six
3133 seven ˇten
3134 "});
3135
3136 // Test backspace inside and around indents
3137 cx.set_state(indoc! {"
3138 zero
3139 ˇone
3140 ˇtwo
3141 ˇ ˇ ˇ three
3142 ˇ ˇ four
3143 "});
3144 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3145 cx.assert_editor_state(indoc! {"
3146 zero
3147 ˇone
3148 ˇtwo
3149 ˇ threeˇ four
3150 "});
3151
3152 // Test backspace with line_mode set to true
3153 cx.update_editor(|e, _, _| e.selections.line_mode = true);
3154 cx.set_state(indoc! {"
3155 The ˇquick ˇbrown
3156 fox jumps over
3157 the lazy dog
3158 ˇThe qu«ick bˇ»rown"});
3159 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3160 cx.assert_editor_state(indoc! {"
3161 ˇfox jumps over
3162 the lazy dogˇ"});
3163}
3164
3165#[gpui::test]
3166async fn test_delete(cx: &mut gpui::TestAppContext) {
3167 init_test(cx, |_| {});
3168
3169 let mut cx = EditorTestContext::new(cx).await;
3170 cx.set_state(indoc! {"
3171 onˇe two three
3172 fou«rˇ» five six
3173 seven «ˇeight nine
3174 »ten
3175 "});
3176 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3177 cx.assert_editor_state(indoc! {"
3178 onˇ two three
3179 fouˇ five six
3180 seven ˇten
3181 "});
3182
3183 // Test backspace with line_mode set to true
3184 cx.update_editor(|e, _, _| e.selections.line_mode = true);
3185 cx.set_state(indoc! {"
3186 The ˇquick ˇbrown
3187 fox «ˇjum»ps over
3188 the lazy dog
3189 ˇThe qu«ick bˇ»rown"});
3190 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3191 cx.assert_editor_state("ˇthe lazy dogˇ");
3192}
3193
3194#[gpui::test]
3195fn test_delete_line(cx: &mut TestAppContext) {
3196 init_test(cx, |_| {});
3197
3198 let editor = cx.add_window(|window, cx| {
3199 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3200 build_editor(buffer, window, cx)
3201 });
3202 _ = editor.update(cx, |editor, window, cx| {
3203 editor.change_selections(None, window, cx, |s| {
3204 s.select_display_ranges([
3205 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3206 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3207 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3208 ])
3209 });
3210 editor.delete_line(&DeleteLine, window, cx);
3211 assert_eq!(editor.display_text(cx), "ghi");
3212 assert_eq!(
3213 editor.selections.display_ranges(cx),
3214 vec![
3215 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3216 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3217 ]
3218 );
3219 });
3220
3221 let editor = cx.add_window(|window, cx| {
3222 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3223 build_editor(buffer, window, cx)
3224 });
3225 _ = editor.update(cx, |editor, window, cx| {
3226 editor.change_selections(None, window, cx, |s| {
3227 s.select_display_ranges([
3228 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3229 ])
3230 });
3231 editor.delete_line(&DeleteLine, window, cx);
3232 assert_eq!(editor.display_text(cx), "ghi\n");
3233 assert_eq!(
3234 editor.selections.display_ranges(cx),
3235 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3236 );
3237 });
3238}
3239
3240#[gpui::test]
3241fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3242 init_test(cx, |_| {});
3243
3244 cx.add_window(|window, cx| {
3245 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3246 let mut editor = build_editor(buffer.clone(), window, cx);
3247 let buffer = buffer.read(cx).as_singleton().unwrap();
3248
3249 assert_eq!(
3250 editor.selections.ranges::<Point>(cx),
3251 &[Point::new(0, 0)..Point::new(0, 0)]
3252 );
3253
3254 // When on single line, replace newline at end by space
3255 editor.join_lines(&JoinLines, window, cx);
3256 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3257 assert_eq!(
3258 editor.selections.ranges::<Point>(cx),
3259 &[Point::new(0, 3)..Point::new(0, 3)]
3260 );
3261
3262 // When multiple lines are selected, remove newlines that are spanned by the selection
3263 editor.change_selections(None, window, cx, |s| {
3264 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3265 });
3266 editor.join_lines(&JoinLines, window, cx);
3267 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3268 assert_eq!(
3269 editor.selections.ranges::<Point>(cx),
3270 &[Point::new(0, 11)..Point::new(0, 11)]
3271 );
3272
3273 // Undo should be transactional
3274 editor.undo(&Undo, window, cx);
3275 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3276 assert_eq!(
3277 editor.selections.ranges::<Point>(cx),
3278 &[Point::new(0, 5)..Point::new(2, 2)]
3279 );
3280
3281 // When joining an empty line don't insert a space
3282 editor.change_selections(None, window, cx, |s| {
3283 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3284 });
3285 editor.join_lines(&JoinLines, window, cx);
3286 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3287 assert_eq!(
3288 editor.selections.ranges::<Point>(cx),
3289 [Point::new(2, 3)..Point::new(2, 3)]
3290 );
3291
3292 // We can remove trailing newlines
3293 editor.join_lines(&JoinLines, window, cx);
3294 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3295 assert_eq!(
3296 editor.selections.ranges::<Point>(cx),
3297 [Point::new(2, 3)..Point::new(2, 3)]
3298 );
3299
3300 // We don't blow up on the last line
3301 editor.join_lines(&JoinLines, window, cx);
3302 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3303 assert_eq!(
3304 editor.selections.ranges::<Point>(cx),
3305 [Point::new(2, 3)..Point::new(2, 3)]
3306 );
3307
3308 // reset to test indentation
3309 editor.buffer.update(cx, |buffer, cx| {
3310 buffer.edit(
3311 [
3312 (Point::new(1, 0)..Point::new(1, 2), " "),
3313 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3314 ],
3315 None,
3316 cx,
3317 )
3318 });
3319
3320 // We remove any leading spaces
3321 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3322 editor.change_selections(None, window, cx, |s| {
3323 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3324 });
3325 editor.join_lines(&JoinLines, window, cx);
3326 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3327
3328 // We don't insert a space for a line containing only spaces
3329 editor.join_lines(&JoinLines, window, cx);
3330 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3331
3332 // We ignore any leading tabs
3333 editor.join_lines(&JoinLines, window, cx);
3334 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3335
3336 editor
3337 });
3338}
3339
3340#[gpui::test]
3341fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3342 init_test(cx, |_| {});
3343
3344 cx.add_window(|window, cx| {
3345 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3346 let mut editor = build_editor(buffer.clone(), window, cx);
3347 let buffer = buffer.read(cx).as_singleton().unwrap();
3348
3349 editor.change_selections(None, window, cx, |s| {
3350 s.select_ranges([
3351 Point::new(0, 2)..Point::new(1, 1),
3352 Point::new(1, 2)..Point::new(1, 2),
3353 Point::new(3, 1)..Point::new(3, 2),
3354 ])
3355 });
3356
3357 editor.join_lines(&JoinLines, window, cx);
3358 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3359
3360 assert_eq!(
3361 editor.selections.ranges::<Point>(cx),
3362 [
3363 Point::new(0, 7)..Point::new(0, 7),
3364 Point::new(1, 3)..Point::new(1, 3)
3365 ]
3366 );
3367 editor
3368 });
3369}
3370
3371#[gpui::test]
3372async fn test_join_lines_with_git_diff_base(
3373 executor: BackgroundExecutor,
3374 cx: &mut gpui::TestAppContext,
3375) {
3376 init_test(cx, |_| {});
3377
3378 let mut cx = EditorTestContext::new(cx).await;
3379
3380 let diff_base = r#"
3381 Line 0
3382 Line 1
3383 Line 2
3384 Line 3
3385 "#
3386 .unindent();
3387
3388 cx.set_state(
3389 &r#"
3390 ˇLine 0
3391 Line 1
3392 Line 2
3393 Line 3
3394 "#
3395 .unindent(),
3396 );
3397
3398 cx.set_diff_base(&diff_base);
3399 executor.run_until_parked();
3400
3401 // Join lines
3402 cx.update_editor(|editor, window, cx| {
3403 editor.join_lines(&JoinLines, window, cx);
3404 });
3405 executor.run_until_parked();
3406
3407 cx.assert_editor_state(
3408 &r#"
3409 Line 0ˇ Line 1
3410 Line 2
3411 Line 3
3412 "#
3413 .unindent(),
3414 );
3415 // Join again
3416 cx.update_editor(|editor, window, cx| {
3417 editor.join_lines(&JoinLines, window, cx);
3418 });
3419 executor.run_until_parked();
3420
3421 cx.assert_editor_state(
3422 &r#"
3423 Line 0 Line 1ˇ Line 2
3424 Line 3
3425 "#
3426 .unindent(),
3427 );
3428}
3429
3430#[gpui::test]
3431async fn test_custom_newlines_cause_no_false_positive_diffs(
3432 executor: BackgroundExecutor,
3433 cx: &mut gpui::TestAppContext,
3434) {
3435 init_test(cx, |_| {});
3436 let mut cx = EditorTestContext::new(cx).await;
3437 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3438 cx.set_diff_base("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3439 executor.run_until_parked();
3440
3441 cx.update_editor(|editor, window, cx| {
3442 let snapshot = editor.snapshot(window, cx);
3443 assert_eq!(
3444 snapshot
3445 .buffer_snapshot
3446 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3447 .collect::<Vec<_>>(),
3448 Vec::new(),
3449 "Should not have any diffs for files with custom newlines"
3450 );
3451 });
3452}
3453
3454#[gpui::test]
3455async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3456 init_test(cx, |_| {});
3457
3458 let mut cx = EditorTestContext::new(cx).await;
3459
3460 // Test sort_lines_case_insensitive()
3461 cx.set_state(indoc! {"
3462 «z
3463 y
3464 x
3465 Z
3466 Y
3467 Xˇ»
3468 "});
3469 cx.update_editor(|e, window, cx| {
3470 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3471 });
3472 cx.assert_editor_state(indoc! {"
3473 «x
3474 X
3475 y
3476 Y
3477 z
3478 Zˇ»
3479 "});
3480
3481 // Test reverse_lines()
3482 cx.set_state(indoc! {"
3483 «5
3484 4
3485 3
3486 2
3487 1ˇ»
3488 "});
3489 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3490 cx.assert_editor_state(indoc! {"
3491 «1
3492 2
3493 3
3494 4
3495 5ˇ»
3496 "});
3497
3498 // Skip testing shuffle_line()
3499
3500 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3501 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3502
3503 // Don't manipulate when cursor is on single line, but expand the selection
3504 cx.set_state(indoc! {"
3505 ddˇdd
3506 ccc
3507 bb
3508 a
3509 "});
3510 cx.update_editor(|e, window, cx| {
3511 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3512 });
3513 cx.assert_editor_state(indoc! {"
3514 «ddddˇ»
3515 ccc
3516 bb
3517 a
3518 "});
3519
3520 // Basic manipulate case
3521 // Start selection moves to column 0
3522 // End of selection shrinks to fit shorter line
3523 cx.set_state(indoc! {"
3524 dd«d
3525 ccc
3526 bb
3527 aaaaaˇ»
3528 "});
3529 cx.update_editor(|e, window, cx| {
3530 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3531 });
3532 cx.assert_editor_state(indoc! {"
3533 «aaaaa
3534 bb
3535 ccc
3536 dddˇ»
3537 "});
3538
3539 // Manipulate case with newlines
3540 cx.set_state(indoc! {"
3541 dd«d
3542 ccc
3543
3544 bb
3545 aaaaa
3546
3547 ˇ»
3548 "});
3549 cx.update_editor(|e, window, cx| {
3550 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3551 });
3552 cx.assert_editor_state(indoc! {"
3553 «
3554
3555 aaaaa
3556 bb
3557 ccc
3558 dddˇ»
3559
3560 "});
3561
3562 // Adding new line
3563 cx.set_state(indoc! {"
3564 aa«a
3565 bbˇ»b
3566 "});
3567 cx.update_editor(|e, window, cx| {
3568 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3569 });
3570 cx.assert_editor_state(indoc! {"
3571 «aaa
3572 bbb
3573 added_lineˇ»
3574 "});
3575
3576 // Removing line
3577 cx.set_state(indoc! {"
3578 aa«a
3579 bbbˇ»
3580 "});
3581 cx.update_editor(|e, window, cx| {
3582 e.manipulate_lines(window, cx, |lines| {
3583 lines.pop();
3584 })
3585 });
3586 cx.assert_editor_state(indoc! {"
3587 «aaaˇ»
3588 "});
3589
3590 // Removing all lines
3591 cx.set_state(indoc! {"
3592 aa«a
3593 bbbˇ»
3594 "});
3595 cx.update_editor(|e, window, cx| {
3596 e.manipulate_lines(window, cx, |lines| {
3597 lines.drain(..);
3598 })
3599 });
3600 cx.assert_editor_state(indoc! {"
3601 ˇ
3602 "});
3603}
3604
3605#[gpui::test]
3606async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3607 init_test(cx, |_| {});
3608
3609 let mut cx = EditorTestContext::new(cx).await;
3610
3611 // Consider continuous selection as single selection
3612 cx.set_state(indoc! {"
3613 Aaa«aa
3614 cˇ»c«c
3615 bb
3616 aaaˇ»aa
3617 "});
3618 cx.update_editor(|e, window, cx| {
3619 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3620 });
3621 cx.assert_editor_state(indoc! {"
3622 «Aaaaa
3623 ccc
3624 bb
3625 aaaaaˇ»
3626 "});
3627
3628 cx.set_state(indoc! {"
3629 Aaa«aa
3630 cˇ»c«c
3631 bb
3632 aaaˇ»aa
3633 "});
3634 cx.update_editor(|e, window, cx| {
3635 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3636 });
3637 cx.assert_editor_state(indoc! {"
3638 «Aaaaa
3639 ccc
3640 bbˇ»
3641 "});
3642
3643 // Consider non continuous selection as distinct dedup operations
3644 cx.set_state(indoc! {"
3645 «aaaaa
3646 bb
3647 aaaaa
3648 aaaaaˇ»
3649
3650 aaa«aaˇ»
3651 "});
3652 cx.update_editor(|e, window, cx| {
3653 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3654 });
3655 cx.assert_editor_state(indoc! {"
3656 «aaaaa
3657 bbˇ»
3658
3659 «aaaaaˇ»
3660 "});
3661}
3662
3663#[gpui::test]
3664async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3665 init_test(cx, |_| {});
3666
3667 let mut cx = EditorTestContext::new(cx).await;
3668
3669 cx.set_state(indoc! {"
3670 «Aaa
3671 aAa
3672 Aaaˇ»
3673 "});
3674 cx.update_editor(|e, window, cx| {
3675 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3676 });
3677 cx.assert_editor_state(indoc! {"
3678 «Aaa
3679 aAaˇ»
3680 "});
3681
3682 cx.set_state(indoc! {"
3683 «Aaa
3684 aAa
3685 aaAˇ»
3686 "});
3687 cx.update_editor(|e, window, cx| {
3688 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3689 });
3690 cx.assert_editor_state(indoc! {"
3691 «Aaaˇ»
3692 "});
3693}
3694
3695#[gpui::test]
3696async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3697 init_test(cx, |_| {});
3698
3699 let mut cx = EditorTestContext::new(cx).await;
3700
3701 // Manipulate with multiple selections on a single line
3702 cx.set_state(indoc! {"
3703 dd«dd
3704 cˇ»c«c
3705 bb
3706 aaaˇ»aa
3707 "});
3708 cx.update_editor(|e, window, cx| {
3709 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3710 });
3711 cx.assert_editor_state(indoc! {"
3712 «aaaaa
3713 bb
3714 ccc
3715 ddddˇ»
3716 "});
3717
3718 // Manipulate with multiple disjoin selections
3719 cx.set_state(indoc! {"
3720 5«
3721 4
3722 3
3723 2
3724 1ˇ»
3725
3726 dd«dd
3727 ccc
3728 bb
3729 aaaˇ»aa
3730 "});
3731 cx.update_editor(|e, window, cx| {
3732 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3733 });
3734 cx.assert_editor_state(indoc! {"
3735 «1
3736 2
3737 3
3738 4
3739 5ˇ»
3740
3741 «aaaaa
3742 bb
3743 ccc
3744 ddddˇ»
3745 "});
3746
3747 // Adding lines on each selection
3748 cx.set_state(indoc! {"
3749 2«
3750 1ˇ»
3751
3752 bb«bb
3753 aaaˇ»aa
3754 "});
3755 cx.update_editor(|e, window, cx| {
3756 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3757 });
3758 cx.assert_editor_state(indoc! {"
3759 «2
3760 1
3761 added lineˇ»
3762
3763 «bbbb
3764 aaaaa
3765 added lineˇ»
3766 "});
3767
3768 // Removing lines on each selection
3769 cx.set_state(indoc! {"
3770 2«
3771 1ˇ»
3772
3773 bb«bb
3774 aaaˇ»aa
3775 "});
3776 cx.update_editor(|e, window, cx| {
3777 e.manipulate_lines(window, cx, |lines| {
3778 lines.pop();
3779 })
3780 });
3781 cx.assert_editor_state(indoc! {"
3782 «2ˇ»
3783
3784 «bbbbˇ»
3785 "});
3786}
3787
3788#[gpui::test]
3789async fn test_manipulate_text(cx: &mut TestAppContext) {
3790 init_test(cx, |_| {});
3791
3792 let mut cx = EditorTestContext::new(cx).await;
3793
3794 // Test convert_to_upper_case()
3795 cx.set_state(indoc! {"
3796 «hello worldˇ»
3797 "});
3798 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3799 cx.assert_editor_state(indoc! {"
3800 «HELLO WORLDˇ»
3801 "});
3802
3803 // Test convert_to_lower_case()
3804 cx.set_state(indoc! {"
3805 «HELLO WORLDˇ»
3806 "});
3807 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3808 cx.assert_editor_state(indoc! {"
3809 «hello worldˇ»
3810 "});
3811
3812 // Test multiple line, single selection case
3813 cx.set_state(indoc! {"
3814 «The quick brown
3815 fox jumps over
3816 the lazy dogˇ»
3817 "});
3818 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3819 cx.assert_editor_state(indoc! {"
3820 «The Quick Brown
3821 Fox Jumps Over
3822 The Lazy Dogˇ»
3823 "});
3824
3825 // Test multiple line, single selection case
3826 cx.set_state(indoc! {"
3827 «The quick brown
3828 fox jumps over
3829 the lazy dogˇ»
3830 "});
3831 cx.update_editor(|e, window, cx| {
3832 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
3833 });
3834 cx.assert_editor_state(indoc! {"
3835 «TheQuickBrown
3836 FoxJumpsOver
3837 TheLazyDogˇ»
3838 "});
3839
3840 // From here on out, test more complex cases of manipulate_text()
3841
3842 // Test no selection case - should affect words cursors are in
3843 // Cursor at beginning, middle, and end of word
3844 cx.set_state(indoc! {"
3845 ˇhello big beauˇtiful worldˇ
3846 "});
3847 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3848 cx.assert_editor_state(indoc! {"
3849 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
3850 "});
3851
3852 // Test multiple selections on a single line and across multiple lines
3853 cx.set_state(indoc! {"
3854 «Theˇ» quick «brown
3855 foxˇ» jumps «overˇ»
3856 the «lazyˇ» dog
3857 "});
3858 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3859 cx.assert_editor_state(indoc! {"
3860 «THEˇ» quick «BROWN
3861 FOXˇ» jumps «OVERˇ»
3862 the «LAZYˇ» dog
3863 "});
3864
3865 // Test case where text length grows
3866 cx.set_state(indoc! {"
3867 «tschüߡ»
3868 "});
3869 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3870 cx.assert_editor_state(indoc! {"
3871 «TSCHÜSSˇ»
3872 "});
3873
3874 // Test to make sure we don't crash when text shrinks
3875 cx.set_state(indoc! {"
3876 aaa_bbbˇ
3877 "});
3878 cx.update_editor(|e, window, cx| {
3879 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3880 });
3881 cx.assert_editor_state(indoc! {"
3882 «aaaBbbˇ»
3883 "});
3884
3885 // Test to make sure we all aware of the fact that each word can grow and shrink
3886 // Final selections should be aware of this fact
3887 cx.set_state(indoc! {"
3888 aaa_bˇbb bbˇb_ccc ˇccc_ddd
3889 "});
3890 cx.update_editor(|e, window, cx| {
3891 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3892 });
3893 cx.assert_editor_state(indoc! {"
3894 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
3895 "});
3896
3897 cx.set_state(indoc! {"
3898 «hElLo, WoRld!ˇ»
3899 "});
3900 cx.update_editor(|e, window, cx| {
3901 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
3902 });
3903 cx.assert_editor_state(indoc! {"
3904 «HeLlO, wOrLD!ˇ»
3905 "});
3906}
3907
3908#[gpui::test]
3909fn test_duplicate_line(cx: &mut TestAppContext) {
3910 init_test(cx, |_| {});
3911
3912 let editor = cx.add_window(|window, cx| {
3913 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3914 build_editor(buffer, window, cx)
3915 });
3916 _ = editor.update(cx, |editor, window, cx| {
3917 editor.change_selections(None, window, cx, |s| {
3918 s.select_display_ranges([
3919 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3920 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3921 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
3922 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3923 ])
3924 });
3925 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
3926 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
3927 assert_eq!(
3928 editor.selections.display_ranges(cx),
3929 vec![
3930 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3931 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3932 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3933 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
3934 ]
3935 );
3936 });
3937
3938 let editor = cx.add_window(|window, cx| {
3939 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3940 build_editor(buffer, window, cx)
3941 });
3942 _ = editor.update(cx, |editor, window, cx| {
3943 editor.change_selections(None, window, cx, |s| {
3944 s.select_display_ranges([
3945 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
3946 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
3947 ])
3948 });
3949 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
3950 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
3951 assert_eq!(
3952 editor.selections.display_ranges(cx),
3953 vec![
3954 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
3955 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
3956 ]
3957 );
3958 });
3959
3960 // With `move_upwards` the selections stay in place, except for
3961 // the lines inserted above them
3962 let editor = cx.add_window(|window, cx| {
3963 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3964 build_editor(buffer, window, cx)
3965 });
3966 _ = editor.update(cx, |editor, window, cx| {
3967 editor.change_selections(None, window, cx, |s| {
3968 s.select_display_ranges([
3969 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3970 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3971 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
3972 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3973 ])
3974 });
3975 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
3976 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
3977 assert_eq!(
3978 editor.selections.display_ranges(cx),
3979 vec![
3980 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3981 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3982 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
3983 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
3984 ]
3985 );
3986 });
3987
3988 let editor = cx.add_window(|window, cx| {
3989 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3990 build_editor(buffer, window, cx)
3991 });
3992 _ = editor.update(cx, |editor, window, cx| {
3993 editor.change_selections(None, window, cx, |s| {
3994 s.select_display_ranges([
3995 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
3996 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
3997 ])
3998 });
3999 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4000 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4001 assert_eq!(
4002 editor.selections.display_ranges(cx),
4003 vec![
4004 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4005 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4006 ]
4007 );
4008 });
4009
4010 let editor = cx.add_window(|window, cx| {
4011 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4012 build_editor(buffer, window, cx)
4013 });
4014 _ = editor.update(cx, |editor, window, cx| {
4015 editor.change_selections(None, window, cx, |s| {
4016 s.select_display_ranges([
4017 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4018 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4019 ])
4020 });
4021 editor.duplicate_selection(&DuplicateSelection, window, cx);
4022 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4023 assert_eq!(
4024 editor.selections.display_ranges(cx),
4025 vec![
4026 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4027 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4028 ]
4029 );
4030 });
4031}
4032
4033#[gpui::test]
4034fn test_move_line_up_down(cx: &mut TestAppContext) {
4035 init_test(cx, |_| {});
4036
4037 let editor = cx.add_window(|window, cx| {
4038 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4039 build_editor(buffer, window, cx)
4040 });
4041 _ = editor.update(cx, |editor, window, cx| {
4042 editor.fold_creases(
4043 vec![
4044 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4045 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4046 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4047 ],
4048 true,
4049 window,
4050 cx,
4051 );
4052 editor.change_selections(None, window, cx, |s| {
4053 s.select_display_ranges([
4054 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4055 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4056 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4057 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4058 ])
4059 });
4060 assert_eq!(
4061 editor.display_text(cx),
4062 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4063 );
4064
4065 editor.move_line_up(&MoveLineUp, window, cx);
4066 assert_eq!(
4067 editor.display_text(cx),
4068 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4069 );
4070 assert_eq!(
4071 editor.selections.display_ranges(cx),
4072 vec![
4073 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4074 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4075 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4076 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4077 ]
4078 );
4079 });
4080
4081 _ = editor.update(cx, |editor, window, cx| {
4082 editor.move_line_down(&MoveLineDown, window, cx);
4083 assert_eq!(
4084 editor.display_text(cx),
4085 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4086 );
4087 assert_eq!(
4088 editor.selections.display_ranges(cx),
4089 vec![
4090 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4091 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4092 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4093 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4094 ]
4095 );
4096 });
4097
4098 _ = editor.update(cx, |editor, window, cx| {
4099 editor.move_line_down(&MoveLineDown, window, cx);
4100 assert_eq!(
4101 editor.display_text(cx),
4102 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4103 );
4104 assert_eq!(
4105 editor.selections.display_ranges(cx),
4106 vec![
4107 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4108 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4109 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4110 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4111 ]
4112 );
4113 });
4114
4115 _ = editor.update(cx, |editor, window, cx| {
4116 editor.move_line_up(&MoveLineUp, window, cx);
4117 assert_eq!(
4118 editor.display_text(cx),
4119 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4120 );
4121 assert_eq!(
4122 editor.selections.display_ranges(cx),
4123 vec![
4124 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4125 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4126 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4127 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4128 ]
4129 );
4130 });
4131}
4132
4133#[gpui::test]
4134fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4135 init_test(cx, |_| {});
4136
4137 let editor = cx.add_window(|window, cx| {
4138 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4139 build_editor(buffer, window, cx)
4140 });
4141 _ = editor.update(cx, |editor, window, cx| {
4142 let snapshot = editor.buffer.read(cx).snapshot(cx);
4143 editor.insert_blocks(
4144 [BlockProperties {
4145 style: BlockStyle::Fixed,
4146 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4147 height: 1,
4148 render: Arc::new(|_| div().into_any()),
4149 priority: 0,
4150 }],
4151 Some(Autoscroll::fit()),
4152 cx,
4153 );
4154 editor.change_selections(None, window, cx, |s| {
4155 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4156 });
4157 editor.move_line_down(&MoveLineDown, window, cx);
4158 });
4159}
4160
4161#[gpui::test]
4162async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4163 init_test(cx, |_| {});
4164
4165 let mut cx = EditorTestContext::new(cx).await;
4166 cx.set_state(
4167 &"
4168 ˇzero
4169 one
4170 two
4171 three
4172 four
4173 five
4174 "
4175 .unindent(),
4176 );
4177
4178 // Create a four-line block that replaces three lines of text.
4179 cx.update_editor(|editor, window, cx| {
4180 let snapshot = editor.snapshot(window, cx);
4181 let snapshot = &snapshot.buffer_snapshot;
4182 let placement = BlockPlacement::Replace(
4183 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4184 );
4185 editor.insert_blocks(
4186 [BlockProperties {
4187 placement,
4188 height: 4,
4189 style: BlockStyle::Sticky,
4190 render: Arc::new(|_| gpui::div().into_any_element()),
4191 priority: 0,
4192 }],
4193 None,
4194 cx,
4195 );
4196 });
4197
4198 // Move down so that the cursor touches the block.
4199 cx.update_editor(|editor, window, cx| {
4200 editor.move_down(&Default::default(), window, cx);
4201 });
4202 cx.assert_editor_state(
4203 &"
4204 zero
4205 «one
4206 two
4207 threeˇ»
4208 four
4209 five
4210 "
4211 .unindent(),
4212 );
4213
4214 // Move down past the block.
4215 cx.update_editor(|editor, window, cx| {
4216 editor.move_down(&Default::default(), window, cx);
4217 });
4218 cx.assert_editor_state(
4219 &"
4220 zero
4221 one
4222 two
4223 three
4224 ˇfour
4225 five
4226 "
4227 .unindent(),
4228 );
4229}
4230
4231#[gpui::test]
4232fn test_transpose(cx: &mut TestAppContext) {
4233 init_test(cx, |_| {});
4234
4235 _ = cx.add_window(|window, cx| {
4236 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4237 editor.set_style(EditorStyle::default(), window, cx);
4238 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4239 editor.transpose(&Default::default(), window, cx);
4240 assert_eq!(editor.text(cx), "bac");
4241 assert_eq!(editor.selections.ranges(cx), [2..2]);
4242
4243 editor.transpose(&Default::default(), window, cx);
4244 assert_eq!(editor.text(cx), "bca");
4245 assert_eq!(editor.selections.ranges(cx), [3..3]);
4246
4247 editor.transpose(&Default::default(), window, cx);
4248 assert_eq!(editor.text(cx), "bac");
4249 assert_eq!(editor.selections.ranges(cx), [3..3]);
4250
4251 editor
4252 });
4253
4254 _ = cx.add_window(|window, cx| {
4255 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4256 editor.set_style(EditorStyle::default(), window, cx);
4257 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4258 editor.transpose(&Default::default(), window, cx);
4259 assert_eq!(editor.text(cx), "acb\nde");
4260 assert_eq!(editor.selections.ranges(cx), [3..3]);
4261
4262 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4263 editor.transpose(&Default::default(), window, cx);
4264 assert_eq!(editor.text(cx), "acbd\ne");
4265 assert_eq!(editor.selections.ranges(cx), [5..5]);
4266
4267 editor.transpose(&Default::default(), window, cx);
4268 assert_eq!(editor.text(cx), "acbde\n");
4269 assert_eq!(editor.selections.ranges(cx), [6..6]);
4270
4271 editor.transpose(&Default::default(), window, cx);
4272 assert_eq!(editor.text(cx), "acbd\ne");
4273 assert_eq!(editor.selections.ranges(cx), [6..6]);
4274
4275 editor
4276 });
4277
4278 _ = cx.add_window(|window, cx| {
4279 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4280 editor.set_style(EditorStyle::default(), window, cx);
4281 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4282 editor.transpose(&Default::default(), window, cx);
4283 assert_eq!(editor.text(cx), "bacd\ne");
4284 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4285
4286 editor.transpose(&Default::default(), window, cx);
4287 assert_eq!(editor.text(cx), "bcade\n");
4288 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4289
4290 editor.transpose(&Default::default(), window, cx);
4291 assert_eq!(editor.text(cx), "bcda\ne");
4292 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4293
4294 editor.transpose(&Default::default(), window, cx);
4295 assert_eq!(editor.text(cx), "bcade\n");
4296 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4297
4298 editor.transpose(&Default::default(), window, cx);
4299 assert_eq!(editor.text(cx), "bcaed\n");
4300 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4301
4302 editor
4303 });
4304
4305 _ = cx.add_window(|window, cx| {
4306 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4307 editor.set_style(EditorStyle::default(), window, cx);
4308 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4309 editor.transpose(&Default::default(), window, cx);
4310 assert_eq!(editor.text(cx), "🏀🍐✋");
4311 assert_eq!(editor.selections.ranges(cx), [8..8]);
4312
4313 editor.transpose(&Default::default(), window, cx);
4314 assert_eq!(editor.text(cx), "🏀✋🍐");
4315 assert_eq!(editor.selections.ranges(cx), [11..11]);
4316
4317 editor.transpose(&Default::default(), window, cx);
4318 assert_eq!(editor.text(cx), "🏀🍐✋");
4319 assert_eq!(editor.selections.ranges(cx), [11..11]);
4320
4321 editor
4322 });
4323}
4324
4325#[gpui::test]
4326async fn test_rewrap(cx: &mut TestAppContext) {
4327 init_test(cx, |_| {});
4328
4329 let mut cx = EditorTestContext::new(cx).await;
4330
4331 let language_with_c_comments = Arc::new(Language::new(
4332 LanguageConfig {
4333 line_comments: vec!["// ".into()],
4334 ..LanguageConfig::default()
4335 },
4336 None,
4337 ));
4338 let language_with_pound_comments = Arc::new(Language::new(
4339 LanguageConfig {
4340 line_comments: vec!["# ".into()],
4341 ..LanguageConfig::default()
4342 },
4343 None,
4344 ));
4345 let markdown_language = Arc::new(Language::new(
4346 LanguageConfig {
4347 name: "Markdown".into(),
4348 ..LanguageConfig::default()
4349 },
4350 None,
4351 ));
4352 let language_with_doc_comments = Arc::new(Language::new(
4353 LanguageConfig {
4354 line_comments: vec!["// ".into(), "/// ".into()],
4355 ..LanguageConfig::default()
4356 },
4357 Some(tree_sitter_rust::LANGUAGE.into()),
4358 ));
4359
4360 let plaintext_language = Arc::new(Language::new(
4361 LanguageConfig {
4362 name: "Plain Text".into(),
4363 ..LanguageConfig::default()
4364 },
4365 None,
4366 ));
4367
4368 assert_rewrap(
4369 indoc! {"
4370 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4371 "},
4372 indoc! {"
4373 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4374 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4375 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4376 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4377 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4378 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4379 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4380 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4381 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4382 // porttitor id. Aliquam id accumsan eros.
4383 "},
4384 language_with_c_comments.clone(),
4385 &mut cx,
4386 );
4387
4388 // Test that rewrapping works inside of a selection
4389 assert_rewrap(
4390 indoc! {"
4391 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.ˇ»
4392 "},
4393 indoc! {"
4394 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4395 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4396 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4397 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4398 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4399 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4400 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4401 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4402 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4403 // porttitor id. Aliquam id accumsan eros.ˇ»
4404 "},
4405 language_with_c_comments.clone(),
4406 &mut cx,
4407 );
4408
4409 // Test that cursors that expand to the same region are collapsed.
4410 assert_rewrap(
4411 indoc! {"
4412 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4413 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4414 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4415 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4416 "},
4417 indoc! {"
4418 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4419 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4420 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4421 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4422 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4423 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4424 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4425 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4426 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4427 // porttitor id. Aliquam id accumsan eros.
4428 "},
4429 language_with_c_comments.clone(),
4430 &mut cx,
4431 );
4432
4433 // Test that non-contiguous selections are treated separately.
4434 assert_rewrap(
4435 indoc! {"
4436 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4437 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4438 //
4439 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4440 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4441 "},
4442 indoc! {"
4443 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4444 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4445 // auctor, eu lacinia sapien scelerisque.
4446 //
4447 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4448 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4449 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4450 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4451 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4452 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4453 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4454 "},
4455 language_with_c_comments.clone(),
4456 &mut cx,
4457 );
4458
4459 // Test that different comment prefixes are supported.
4460 assert_rewrap(
4461 indoc! {"
4462 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4463 "},
4464 indoc! {"
4465 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4466 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4467 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4468 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4469 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4470 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4471 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4472 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4473 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4474 # accumsan eros.
4475 "},
4476 language_with_pound_comments.clone(),
4477 &mut cx,
4478 );
4479
4480 // Test that rewrapping is ignored outside of comments in most languages.
4481 assert_rewrap(
4482 indoc! {"
4483 /// Adds two numbers.
4484 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4485 fn add(a: u32, b: u32) -> u32 {
4486 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
4487 }
4488 "},
4489 indoc! {"
4490 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4491 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4492 fn add(a: u32, b: u32) -> u32 {
4493 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
4494 }
4495 "},
4496 language_with_doc_comments.clone(),
4497 &mut cx,
4498 );
4499
4500 // Test that rewrapping works in Markdown and Plain Text languages.
4501 assert_rewrap(
4502 indoc! {"
4503 # Hello
4504
4505 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
4506 "},
4507 indoc! {"
4508 # Hello
4509
4510 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4511 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4512 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4513 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4514 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4515 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4516 Integer sit amet scelerisque nisi.
4517 "},
4518 markdown_language,
4519 &mut cx,
4520 );
4521
4522 assert_rewrap(
4523 indoc! {"
4524 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
4525 "},
4526 indoc! {"
4527 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4528 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4529 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4530 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4531 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4532 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4533 Integer sit amet scelerisque nisi.
4534 "},
4535 plaintext_language,
4536 &mut cx,
4537 );
4538
4539 // Test rewrapping unaligned comments in a selection.
4540 assert_rewrap(
4541 indoc! {"
4542 fn foo() {
4543 if true {
4544 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4545 // Praesent semper egestas tellus id dignissim.ˇ»
4546 do_something();
4547 } else {
4548 //
4549 }
4550 }
4551 "},
4552 indoc! {"
4553 fn foo() {
4554 if true {
4555 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4556 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4557 // egestas tellus id dignissim.ˇ»
4558 do_something();
4559 } else {
4560 //
4561 }
4562 }
4563 "},
4564 language_with_doc_comments.clone(),
4565 &mut cx,
4566 );
4567
4568 assert_rewrap(
4569 indoc! {"
4570 fn foo() {
4571 if true {
4572 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4573 // Praesent semper egestas tellus id dignissim.»
4574 do_something();
4575 } else {
4576 //
4577 }
4578
4579 }
4580 "},
4581 indoc! {"
4582 fn foo() {
4583 if true {
4584 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4585 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4586 // egestas tellus id dignissim.»
4587 do_something();
4588 } else {
4589 //
4590 }
4591
4592 }
4593 "},
4594 language_with_doc_comments.clone(),
4595 &mut cx,
4596 );
4597
4598 #[track_caller]
4599 fn assert_rewrap(
4600 unwrapped_text: &str,
4601 wrapped_text: &str,
4602 language: Arc<Language>,
4603 cx: &mut EditorTestContext,
4604 ) {
4605 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4606 cx.set_state(unwrapped_text);
4607 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4608 cx.assert_editor_state(wrapped_text);
4609 }
4610}
4611
4612#[gpui::test]
4613async fn test_clipboard(cx: &mut gpui::TestAppContext) {
4614 init_test(cx, |_| {});
4615
4616 let mut cx = EditorTestContext::new(cx).await;
4617
4618 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4619 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4620 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4621
4622 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4623 cx.set_state("two ˇfour ˇsix ˇ");
4624 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4625 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4626
4627 // Paste again but with only two cursors. Since the number of cursors doesn't
4628 // match the number of slices in the clipboard, the entire clipboard text
4629 // is pasted at each cursor.
4630 cx.set_state("ˇtwo one✅ four three six five ˇ");
4631 cx.update_editor(|e, window, cx| {
4632 e.handle_input("( ", window, cx);
4633 e.paste(&Paste, window, cx);
4634 e.handle_input(") ", window, cx);
4635 });
4636 cx.assert_editor_state(
4637 &([
4638 "( one✅ ",
4639 "three ",
4640 "five ) ˇtwo one✅ four three six five ( one✅ ",
4641 "three ",
4642 "five ) ˇ",
4643 ]
4644 .join("\n")),
4645 );
4646
4647 // Cut with three selections, one of which is full-line.
4648 cx.set_state(indoc! {"
4649 1«2ˇ»3
4650 4ˇ567
4651 «8ˇ»9"});
4652 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4653 cx.assert_editor_state(indoc! {"
4654 1ˇ3
4655 ˇ9"});
4656
4657 // Paste with three selections, noticing how the copied selection that was full-line
4658 // gets inserted before the second cursor.
4659 cx.set_state(indoc! {"
4660 1ˇ3
4661 9ˇ
4662 «oˇ»ne"});
4663 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4664 cx.assert_editor_state(indoc! {"
4665 12ˇ3
4666 4567
4667 9ˇ
4668 8ˇne"});
4669
4670 // Copy with a single cursor only, which writes the whole line into the clipboard.
4671 cx.set_state(indoc! {"
4672 The quick brown
4673 fox juˇmps over
4674 the lazy dog"});
4675 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4676 assert_eq!(
4677 cx.read_from_clipboard()
4678 .and_then(|item| item.text().as_deref().map(str::to_string)),
4679 Some("fox jumps over\n".to_string())
4680 );
4681
4682 // Paste with three selections, noticing how the copied full-line selection is inserted
4683 // before the empty selections but replaces the selection that is non-empty.
4684 cx.set_state(indoc! {"
4685 Tˇhe quick brown
4686 «foˇ»x jumps over
4687 tˇhe lazy dog"});
4688 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4689 cx.assert_editor_state(indoc! {"
4690 fox jumps over
4691 Tˇhe quick brown
4692 fox jumps over
4693 ˇx jumps over
4694 fox jumps over
4695 tˇhe lazy dog"});
4696}
4697
4698#[gpui::test]
4699async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
4700 init_test(cx, |_| {});
4701
4702 let mut cx = EditorTestContext::new(cx).await;
4703 let language = Arc::new(Language::new(
4704 LanguageConfig::default(),
4705 Some(tree_sitter_rust::LANGUAGE.into()),
4706 ));
4707 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4708
4709 // Cut an indented block, without the leading whitespace.
4710 cx.set_state(indoc! {"
4711 const a: B = (
4712 c(),
4713 «d(
4714 e,
4715 f
4716 )ˇ»
4717 );
4718 "});
4719 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4720 cx.assert_editor_state(indoc! {"
4721 const a: B = (
4722 c(),
4723 ˇ
4724 );
4725 "});
4726
4727 // Paste it at the same position.
4728 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4729 cx.assert_editor_state(indoc! {"
4730 const a: B = (
4731 c(),
4732 d(
4733 e,
4734 f
4735 )ˇ
4736 );
4737 "});
4738
4739 // Paste it at a line with a lower indent level.
4740 cx.set_state(indoc! {"
4741 ˇ
4742 const a: B = (
4743 c(),
4744 );
4745 "});
4746 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4747 cx.assert_editor_state(indoc! {"
4748 d(
4749 e,
4750 f
4751 )ˇ
4752 const a: B = (
4753 c(),
4754 );
4755 "});
4756
4757 // Cut an indented block, with the leading whitespace.
4758 cx.set_state(indoc! {"
4759 const a: B = (
4760 c(),
4761 « d(
4762 e,
4763 f
4764 )
4765 ˇ»);
4766 "});
4767 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4768 cx.assert_editor_state(indoc! {"
4769 const a: B = (
4770 c(),
4771 ˇ);
4772 "});
4773
4774 // Paste it at the same position.
4775 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4776 cx.assert_editor_state(indoc! {"
4777 const a: B = (
4778 c(),
4779 d(
4780 e,
4781 f
4782 )
4783 ˇ);
4784 "});
4785
4786 // Paste it at a line with a higher indent level.
4787 cx.set_state(indoc! {"
4788 const a: B = (
4789 c(),
4790 d(
4791 e,
4792 fˇ
4793 )
4794 );
4795 "});
4796 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4797 cx.assert_editor_state(indoc! {"
4798 const a: B = (
4799 c(),
4800 d(
4801 e,
4802 f d(
4803 e,
4804 f
4805 )
4806 ˇ
4807 )
4808 );
4809 "});
4810}
4811
4812#[gpui::test]
4813fn test_select_all(cx: &mut TestAppContext) {
4814 init_test(cx, |_| {});
4815
4816 let editor = cx.add_window(|window, cx| {
4817 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
4818 build_editor(buffer, window, cx)
4819 });
4820 _ = editor.update(cx, |editor, window, cx| {
4821 editor.select_all(&SelectAll, window, cx);
4822 assert_eq!(
4823 editor.selections.display_ranges(cx),
4824 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
4825 );
4826 });
4827}
4828
4829#[gpui::test]
4830fn test_select_line(cx: &mut TestAppContext) {
4831 init_test(cx, |_| {});
4832
4833 let editor = cx.add_window(|window, cx| {
4834 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
4835 build_editor(buffer, window, cx)
4836 });
4837 _ = editor.update(cx, |editor, window, cx| {
4838 editor.change_selections(None, window, cx, |s| {
4839 s.select_display_ranges([
4840 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4841 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4842 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4843 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
4844 ])
4845 });
4846 editor.select_line(&SelectLine, window, cx);
4847 assert_eq!(
4848 editor.selections.display_ranges(cx),
4849 vec![
4850 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
4851 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
4852 ]
4853 );
4854 });
4855
4856 _ = editor.update(cx, |editor, window, cx| {
4857 editor.select_line(&SelectLine, window, cx);
4858 assert_eq!(
4859 editor.selections.display_ranges(cx),
4860 vec![
4861 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
4862 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
4863 ]
4864 );
4865 });
4866
4867 _ = editor.update(cx, |editor, window, cx| {
4868 editor.select_line(&SelectLine, window, cx);
4869 assert_eq!(
4870 editor.selections.display_ranges(cx),
4871 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
4872 );
4873 });
4874}
4875
4876#[gpui::test]
4877async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
4878 init_test(cx, |_| {});
4879 let mut cx = EditorTestContext::new(cx).await;
4880
4881 #[track_caller]
4882 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
4883 cx.set_state(initial_state);
4884 cx.update_editor(|e, window, cx| {
4885 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
4886 });
4887 cx.assert_editor_state(expected_state);
4888 }
4889
4890 // Selection starts and ends at the middle of lines, left-to-right
4891 test(
4892 &mut cx,
4893 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
4894 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
4895 );
4896 // Same thing, right-to-left
4897 test(
4898 &mut cx,
4899 "aa\nb«b\ncc\ndd\neˇ»e\nff",
4900 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
4901 );
4902
4903 // Whole buffer, left-to-right, last line *doesn't* end with newline
4904 test(
4905 &mut cx,
4906 "«ˇaa\nbb\ncc\ndd\nee\nff»",
4907 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
4908 );
4909 // Same thing, right-to-left
4910 test(
4911 &mut cx,
4912 "«aa\nbb\ncc\ndd\nee\nffˇ»",
4913 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
4914 );
4915
4916 // Whole buffer, left-to-right, last line ends with newline
4917 test(
4918 &mut cx,
4919 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
4920 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
4921 );
4922 // Same thing, right-to-left
4923 test(
4924 &mut cx,
4925 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
4926 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
4927 );
4928
4929 // Starts at the end of a line, ends at the start of another
4930 test(
4931 &mut cx,
4932 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
4933 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
4934 );
4935}
4936
4937#[gpui::test]
4938async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
4939 init_test(cx, |_| {});
4940
4941 let editor = cx.add_window(|window, cx| {
4942 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
4943 build_editor(buffer, window, cx)
4944 });
4945
4946 // setup
4947 _ = editor.update(cx, |editor, window, cx| {
4948 editor.fold_creases(
4949 vec![
4950 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4951 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4952 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4953 ],
4954 true,
4955 window,
4956 cx,
4957 );
4958 assert_eq!(
4959 editor.display_text(cx),
4960 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
4961 );
4962 });
4963
4964 _ = editor.update(cx, |editor, window, cx| {
4965 editor.change_selections(None, window, cx, |s| {
4966 s.select_display_ranges([
4967 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4968 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4969 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4970 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
4971 ])
4972 });
4973 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
4974 assert_eq!(
4975 editor.display_text(cx),
4976 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
4977 );
4978 });
4979 EditorTestContext::for_editor(editor, cx)
4980 .await
4981 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
4982
4983 _ = editor.update(cx, |editor, window, cx| {
4984 editor.change_selections(None, window, cx, |s| {
4985 s.select_display_ranges([
4986 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
4987 ])
4988 });
4989 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
4990 assert_eq!(
4991 editor.display_text(cx),
4992 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
4993 );
4994 assert_eq!(
4995 editor.selections.display_ranges(cx),
4996 [
4997 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
4998 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
4999 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5000 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5001 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5002 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5003 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5004 ]
5005 );
5006 });
5007 EditorTestContext::for_editor(editor, cx)
5008 .await
5009 .assert_editor_state(
5010 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5011 );
5012}
5013
5014#[gpui::test]
5015async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5016 init_test(cx, |_| {});
5017
5018 let mut cx = EditorTestContext::new(cx).await;
5019
5020 cx.set_state(indoc!(
5021 r#"abc
5022 defˇghi
5023
5024 jk
5025 nlmo
5026 "#
5027 ));
5028
5029 cx.update_editor(|editor, window, cx| {
5030 editor.add_selection_above(&Default::default(), window, cx);
5031 });
5032
5033 cx.assert_editor_state(indoc!(
5034 r#"abcˇ
5035 defˇghi
5036
5037 jk
5038 nlmo
5039 "#
5040 ));
5041
5042 cx.update_editor(|editor, window, cx| {
5043 editor.add_selection_above(&Default::default(), window, cx);
5044 });
5045
5046 cx.assert_editor_state(indoc!(
5047 r#"abcˇ
5048 defˇghi
5049
5050 jk
5051 nlmo
5052 "#
5053 ));
5054
5055 cx.update_editor(|editor, window, cx| {
5056 editor.add_selection_below(&Default::default(), window, cx);
5057 });
5058
5059 cx.assert_editor_state(indoc!(
5060 r#"abc
5061 defˇghi
5062
5063 jk
5064 nlmo
5065 "#
5066 ));
5067
5068 cx.update_editor(|editor, window, cx| {
5069 editor.undo_selection(&Default::default(), window, cx);
5070 });
5071
5072 cx.assert_editor_state(indoc!(
5073 r#"abcˇ
5074 defˇghi
5075
5076 jk
5077 nlmo
5078 "#
5079 ));
5080
5081 cx.update_editor(|editor, window, cx| {
5082 editor.redo_selection(&Default::default(), window, cx);
5083 });
5084
5085 cx.assert_editor_state(indoc!(
5086 r#"abc
5087 defˇghi
5088
5089 jk
5090 nlmo
5091 "#
5092 ));
5093
5094 cx.update_editor(|editor, window, cx| {
5095 editor.add_selection_below(&Default::default(), window, cx);
5096 });
5097
5098 cx.assert_editor_state(indoc!(
5099 r#"abc
5100 defˇghi
5101
5102 jk
5103 nlmˇo
5104 "#
5105 ));
5106
5107 cx.update_editor(|editor, window, cx| {
5108 editor.add_selection_below(&Default::default(), window, cx);
5109 });
5110
5111 cx.assert_editor_state(indoc!(
5112 r#"abc
5113 defˇghi
5114
5115 jk
5116 nlmˇo
5117 "#
5118 ));
5119
5120 // change selections
5121 cx.set_state(indoc!(
5122 r#"abc
5123 def«ˇg»hi
5124
5125 jk
5126 nlmo
5127 "#
5128 ));
5129
5130 cx.update_editor(|editor, window, cx| {
5131 editor.add_selection_below(&Default::default(), window, cx);
5132 });
5133
5134 cx.assert_editor_state(indoc!(
5135 r#"abc
5136 def«ˇg»hi
5137
5138 jk
5139 nlm«ˇo»
5140 "#
5141 ));
5142
5143 cx.update_editor(|editor, window, cx| {
5144 editor.add_selection_below(&Default::default(), window, cx);
5145 });
5146
5147 cx.assert_editor_state(indoc!(
5148 r#"abc
5149 def«ˇg»hi
5150
5151 jk
5152 nlm«ˇo»
5153 "#
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#"abc
5162 def«ˇg»hi
5163
5164 jk
5165 nlmo
5166 "#
5167 ));
5168
5169 cx.update_editor(|editor, window, cx| {
5170 editor.add_selection_above(&Default::default(), window, cx);
5171 });
5172
5173 cx.assert_editor_state(indoc!(
5174 r#"abc
5175 def«ˇg»hi
5176
5177 jk
5178 nlmo
5179 "#
5180 ));
5181
5182 // Change selections again
5183 cx.set_state(indoc!(
5184 r#"a«bc
5185 defgˇ»hi
5186
5187 jk
5188 nlmo
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#"a«bcˇ»
5198 d«efgˇ»hi
5199
5200 j«kˇ»
5201 nlmo
5202 "#
5203 ));
5204
5205 cx.update_editor(|editor, window, cx| {
5206 editor.add_selection_below(&Default::default(), window, cx);
5207 });
5208 cx.assert_editor_state(indoc!(
5209 r#"a«bcˇ»
5210 d«efgˇ»hi
5211
5212 j«kˇ»
5213 n«lmoˇ»
5214 "#
5215 ));
5216 cx.update_editor(|editor, window, cx| {
5217 editor.add_selection_above(&Default::default(), window, cx);
5218 });
5219
5220 cx.assert_editor_state(indoc!(
5221 r#"a«bcˇ»
5222 d«efgˇ»hi
5223
5224 j«kˇ»
5225 nlmo
5226 "#
5227 ));
5228
5229 // Change selections again
5230 cx.set_state(indoc!(
5231 r#"abc
5232 d«ˇefghi
5233
5234 jk
5235 nlm»o
5236 "#
5237 ));
5238
5239 cx.update_editor(|editor, window, cx| {
5240 editor.add_selection_above(&Default::default(), window, cx);
5241 });
5242
5243 cx.assert_editor_state(indoc!(
5244 r#"a«ˇbc»
5245 d«ˇef»ghi
5246
5247 j«ˇk»
5248 n«ˇlm»o
5249 "#
5250 ));
5251
5252 cx.update_editor(|editor, window, cx| {
5253 editor.add_selection_below(&Default::default(), window, cx);
5254 });
5255
5256 cx.assert_editor_state(indoc!(
5257 r#"abc
5258 d«ˇef»ghi
5259
5260 j«ˇk»
5261 n«ˇlm»o
5262 "#
5263 ));
5264}
5265
5266#[gpui::test]
5267async fn test_select_next(cx: &mut gpui::TestAppContext) {
5268 init_test(cx, |_| {});
5269
5270 let mut cx = EditorTestContext::new(cx).await;
5271 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5272
5273 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5274 .unwrap();
5275 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5276
5277 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5278 .unwrap();
5279 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5280
5281 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5282 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5283
5284 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5285 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5286
5287 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5288 .unwrap();
5289 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5290
5291 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5292 .unwrap();
5293 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5294}
5295
5296#[gpui::test]
5297async fn test_select_all_matches(cx: &mut gpui::TestAppContext) {
5298 init_test(cx, |_| {});
5299
5300 let mut cx = EditorTestContext::new(cx).await;
5301
5302 // Test caret-only selections
5303 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5304
5305 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5306 .unwrap();
5307 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5308
5309 // Test left-to-right selections
5310 cx.set_state("abc\n«abcˇ»\nabc");
5311
5312 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5313 .unwrap();
5314 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5315
5316 // Test right-to-left selections
5317 cx.set_state("abc\n«ˇabc»\nabc");
5318
5319 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5320 .unwrap();
5321 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5322}
5323
5324#[gpui::test]
5325async fn test_select_next_with_multiple_carets(cx: &mut gpui::TestAppContext) {
5326 init_test(cx, |_| {});
5327
5328 let mut cx = EditorTestContext::new(cx).await;
5329 cx.set_state(
5330 r#"let foo = 2;
5331lˇet foo = 2;
5332let fooˇ = 2;
5333let foo = 2;
5334let foo = ˇ2;"#,
5335 );
5336
5337 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5338 .unwrap();
5339 cx.assert_editor_state(
5340 r#"let foo = 2;
5341«letˇ» foo = 2;
5342let «fooˇ» = 2;
5343let foo = 2;
5344let foo = «2ˇ»;"#,
5345 );
5346
5347 // noop for multiple selections with different contents
5348 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5349 .unwrap();
5350 cx.assert_editor_state(
5351 r#"let foo = 2;
5352«letˇ» foo = 2;
5353let «fooˇ» = 2;
5354let foo = 2;
5355let foo = «2ˇ»;"#,
5356 );
5357}
5358
5359#[gpui::test]
5360async fn test_select_previous_multibuffer(cx: &mut gpui::TestAppContext) {
5361 init_test(cx, |_| {});
5362
5363 let mut cx =
5364 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
5365
5366 cx.assert_editor_state(indoc! {"
5367 ˇbbb
5368 ccc
5369
5370 bbb
5371 ccc
5372 "});
5373 cx.dispatch_action(SelectPrevious::default());
5374 cx.assert_editor_state(indoc! {"
5375 «bbbˇ»
5376 ccc
5377
5378 bbb
5379 ccc
5380 "});
5381 cx.dispatch_action(SelectPrevious::default());
5382 cx.assert_editor_state(indoc! {"
5383 «bbbˇ»
5384 ccc
5385
5386 «bbbˇ»
5387 ccc
5388 "});
5389}
5390
5391#[gpui::test]
5392async fn test_select_previous_with_single_caret(cx: &mut gpui::TestAppContext) {
5393 init_test(cx, |_| {});
5394
5395 let mut cx = EditorTestContext::new(cx).await;
5396 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5397
5398 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5399 .unwrap();
5400 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5401
5402 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5403 .unwrap();
5404 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5405
5406 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5407 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5408
5409 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5410 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5411
5412 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5413 .unwrap();
5414 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
5415
5416 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5417 .unwrap();
5418 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
5419
5420 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5421 .unwrap();
5422 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5423}
5424
5425#[gpui::test]
5426async fn test_select_previous_empty_buffer(cx: &mut gpui::TestAppContext) {
5427 init_test(cx, |_| {});
5428
5429 let mut cx = EditorTestContext::new(cx).await;
5430 cx.set_state("aˇ");
5431
5432 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5433 .unwrap();
5434 cx.assert_editor_state("«aˇ»");
5435 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5436 .unwrap();
5437 cx.assert_editor_state("«aˇ»");
5438}
5439
5440#[gpui::test]
5441async fn test_select_previous_with_multiple_carets(cx: &mut gpui::TestAppContext) {
5442 init_test(cx, |_| {});
5443
5444 let mut cx = EditorTestContext::new(cx).await;
5445 cx.set_state(
5446 r#"let foo = 2;
5447lˇet foo = 2;
5448let fooˇ = 2;
5449let foo = 2;
5450let foo = ˇ2;"#,
5451 );
5452
5453 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5454 .unwrap();
5455 cx.assert_editor_state(
5456 r#"let foo = 2;
5457«letˇ» foo = 2;
5458let «fooˇ» = 2;
5459let foo = 2;
5460let foo = «2ˇ»;"#,
5461 );
5462
5463 // noop for multiple selections with different contents
5464 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5465 .unwrap();
5466 cx.assert_editor_state(
5467 r#"let foo = 2;
5468«letˇ» foo = 2;
5469let «fooˇ» = 2;
5470let foo = 2;
5471let foo = «2ˇ»;"#,
5472 );
5473}
5474
5475#[gpui::test]
5476async fn test_select_previous_with_single_selection(cx: &mut gpui::TestAppContext) {
5477 init_test(cx, |_| {});
5478
5479 let mut cx = EditorTestContext::new(cx).await;
5480 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5481
5482 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5483 .unwrap();
5484 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5485
5486 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5487 .unwrap();
5488 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5489
5490 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5491 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5492
5493 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5494 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5495
5496 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5497 .unwrap();
5498 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
5499
5500 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5501 .unwrap();
5502 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5503}
5504
5505#[gpui::test]
5506async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
5507 init_test(cx, |_| {});
5508
5509 let language = Arc::new(Language::new(
5510 LanguageConfig::default(),
5511 Some(tree_sitter_rust::LANGUAGE.into()),
5512 ));
5513
5514 let text = r#"
5515 use mod1::mod2::{mod3, mod4};
5516
5517 fn fn_1(param1: bool, param2: &str) {
5518 let var1 = "text";
5519 }
5520 "#
5521 .unindent();
5522
5523 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5524 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5525 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5526
5527 editor
5528 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5529 .await;
5530
5531 editor.update_in(cx, |editor, window, cx| {
5532 editor.change_selections(None, window, cx, |s| {
5533 s.select_display_ranges([
5534 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
5535 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
5536 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
5537 ]);
5538 });
5539 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5540 });
5541 editor.update(cx, |editor, cx| {
5542 assert_text_with_selections(
5543 editor,
5544 indoc! {r#"
5545 use mod1::mod2::{mod3, «mod4ˇ»};
5546
5547 fn fn_1«ˇ(param1: bool, param2: &str)» {
5548 let var1 = "«textˇ»";
5549 }
5550 "#},
5551 cx,
5552 );
5553 });
5554
5555 editor.update_in(cx, |editor, window, cx| {
5556 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5557 });
5558 editor.update(cx, |editor, cx| {
5559 assert_text_with_selections(
5560 editor,
5561 indoc! {r#"
5562 use mod1::mod2::«{mod3, mod4}ˇ»;
5563
5564 «ˇfn fn_1(param1: bool, param2: &str) {
5565 let var1 = "text";
5566 }»
5567 "#},
5568 cx,
5569 );
5570 });
5571
5572 editor.update_in(cx, |editor, window, cx| {
5573 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5574 });
5575 assert_eq!(
5576 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
5577 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
5578 );
5579
5580 // Trying to expand the selected syntax node one more time has no effect.
5581 editor.update_in(cx, |editor, window, cx| {
5582 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5583 });
5584 assert_eq!(
5585 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
5586 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
5587 );
5588
5589 editor.update_in(cx, |editor, window, cx| {
5590 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5591 });
5592 editor.update(cx, |editor, cx| {
5593 assert_text_with_selections(
5594 editor,
5595 indoc! {r#"
5596 use mod1::mod2::«{mod3, mod4}ˇ»;
5597
5598 «ˇfn fn_1(param1: bool, param2: &str) {
5599 let var1 = "text";
5600 }»
5601 "#},
5602 cx,
5603 );
5604 });
5605
5606 editor.update_in(cx, |editor, window, cx| {
5607 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5608 });
5609 editor.update(cx, |editor, cx| {
5610 assert_text_with_selections(
5611 editor,
5612 indoc! {r#"
5613 use mod1::mod2::{mod3, «mod4ˇ»};
5614
5615 fn fn_1«ˇ(param1: bool, param2: &str)» {
5616 let var1 = "«textˇ»";
5617 }
5618 "#},
5619 cx,
5620 );
5621 });
5622
5623 editor.update_in(cx, |editor, window, cx| {
5624 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5625 });
5626 editor.update(cx, |editor, cx| {
5627 assert_text_with_selections(
5628 editor,
5629 indoc! {r#"
5630 use mod1::mod2::{mod3, mo«ˇ»d4};
5631
5632 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
5633 let var1 = "te«ˇ»xt";
5634 }
5635 "#},
5636 cx,
5637 );
5638 });
5639
5640 // Trying to shrink the selected syntax node one more time has no effect.
5641 editor.update_in(cx, |editor, window, cx| {
5642 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5643 });
5644 editor.update_in(cx, |editor, _, cx| {
5645 assert_text_with_selections(
5646 editor,
5647 indoc! {r#"
5648 use mod1::mod2::{mod3, mo«ˇ»d4};
5649
5650 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
5651 let var1 = "te«ˇ»xt";
5652 }
5653 "#},
5654 cx,
5655 );
5656 });
5657
5658 // Ensure that we keep expanding the selection if the larger selection starts or ends within
5659 // a fold.
5660 editor.update_in(cx, |editor, window, cx| {
5661 editor.fold_creases(
5662 vec![
5663 Crease::simple(
5664 Point::new(0, 21)..Point::new(0, 24),
5665 FoldPlaceholder::test(),
5666 ),
5667 Crease::simple(
5668 Point::new(3, 20)..Point::new(3, 22),
5669 FoldPlaceholder::test(),
5670 ),
5671 ],
5672 true,
5673 window,
5674 cx,
5675 );
5676 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5677 });
5678 editor.update(cx, |editor, cx| {
5679 assert_text_with_selections(
5680 editor,
5681 indoc! {r#"
5682 use mod1::mod2::«{mod3, mod4}ˇ»;
5683
5684 fn fn_1«ˇ(param1: bool, param2: &str)» {
5685 «let var1 = "text";ˇ»
5686 }
5687 "#},
5688 cx,
5689 );
5690 });
5691}
5692
5693#[gpui::test]
5694async fn test_fold_function_bodies(cx: &mut gpui::TestAppContext) {
5695 init_test(cx, |_| {});
5696
5697 let base_text = r#"
5698 impl A {
5699 // this is an uncommitted comment
5700
5701 fn b() {
5702 c();
5703 }
5704
5705 // this is another uncommitted comment
5706
5707 fn d() {
5708 // e
5709 // f
5710 }
5711 }
5712
5713 fn g() {
5714 // h
5715 }
5716 "#
5717 .unindent();
5718
5719 let text = r#"
5720 ˇimpl A {
5721
5722 fn b() {
5723 c();
5724 }
5725
5726 fn d() {
5727 // e
5728 // f
5729 }
5730 }
5731
5732 fn g() {
5733 // h
5734 }
5735 "#
5736 .unindent();
5737
5738 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
5739 cx.set_state(&text);
5740 cx.set_diff_base(&base_text);
5741 cx.update_editor(|editor, window, cx| {
5742 editor.expand_all_diff_hunks(&Default::default(), window, cx);
5743 });
5744
5745 cx.assert_state_with_diff(
5746 "
5747 ˇimpl A {
5748 - // this is an uncommitted comment
5749
5750 fn b() {
5751 c();
5752 }
5753
5754 - // this is another uncommitted comment
5755 -
5756 fn d() {
5757 // e
5758 // f
5759 }
5760 }
5761
5762 fn g() {
5763 // h
5764 }
5765 "
5766 .unindent(),
5767 );
5768
5769 let expected_display_text = "
5770 impl A {
5771 // this is an uncommitted comment
5772
5773 fn b() {
5774 ⋯
5775 }
5776
5777 // this is another uncommitted comment
5778
5779 fn d() {
5780 ⋯
5781 }
5782 }
5783
5784 fn g() {
5785 ⋯
5786 }
5787 "
5788 .unindent();
5789
5790 cx.update_editor(|editor, window, cx| {
5791 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
5792 assert_eq!(editor.display_text(cx), expected_display_text);
5793 });
5794}
5795
5796#[gpui::test]
5797async fn test_autoindent(cx: &mut gpui::TestAppContext) {
5798 init_test(cx, |_| {});
5799
5800 let language = Arc::new(
5801 Language::new(
5802 LanguageConfig {
5803 brackets: BracketPairConfig {
5804 pairs: vec![
5805 BracketPair {
5806 start: "{".to_string(),
5807 end: "}".to_string(),
5808 close: false,
5809 surround: false,
5810 newline: true,
5811 },
5812 BracketPair {
5813 start: "(".to_string(),
5814 end: ")".to_string(),
5815 close: false,
5816 surround: false,
5817 newline: true,
5818 },
5819 ],
5820 ..Default::default()
5821 },
5822 ..Default::default()
5823 },
5824 Some(tree_sitter_rust::LANGUAGE.into()),
5825 )
5826 .with_indents_query(
5827 r#"
5828 (_ "(" ")" @end) @indent
5829 (_ "{" "}" @end) @indent
5830 "#,
5831 )
5832 .unwrap(),
5833 );
5834
5835 let text = "fn a() {}";
5836
5837 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5838 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5839 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5840 editor
5841 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5842 .await;
5843
5844 editor.update_in(cx, |editor, window, cx| {
5845 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
5846 editor.newline(&Newline, window, cx);
5847 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
5848 assert_eq!(
5849 editor.selections.ranges(cx),
5850 &[
5851 Point::new(1, 4)..Point::new(1, 4),
5852 Point::new(3, 4)..Point::new(3, 4),
5853 Point::new(5, 0)..Point::new(5, 0)
5854 ]
5855 );
5856 });
5857}
5858
5859#[gpui::test]
5860async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
5861 init_test(cx, |_| {});
5862
5863 {
5864 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
5865 cx.set_state(indoc! {"
5866 impl A {
5867
5868 fn b() {}
5869
5870 «fn c() {
5871
5872 }ˇ»
5873 }
5874 "});
5875
5876 cx.update_editor(|editor, window, cx| {
5877 editor.autoindent(&Default::default(), window, cx);
5878 });
5879
5880 cx.assert_editor_state(indoc! {"
5881 impl A {
5882
5883 fn b() {}
5884
5885 «fn c() {
5886
5887 }ˇ»
5888 }
5889 "});
5890 }
5891
5892 {
5893 let mut cx = EditorTestContext::new_multibuffer(
5894 cx,
5895 [indoc! { "
5896 impl A {
5897 «
5898 // a
5899 fn b(){}
5900 »
5901 «
5902 }
5903 fn c(){}
5904 »
5905 "}],
5906 );
5907
5908 let buffer = cx.update_editor(|editor, _, cx| {
5909 let buffer = editor.buffer().update(cx, |buffer, _| {
5910 buffer.all_buffers().iter().next().unwrap().clone()
5911 });
5912 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5913 buffer
5914 });
5915
5916 cx.run_until_parked();
5917 cx.update_editor(|editor, window, cx| {
5918 editor.select_all(&Default::default(), window, cx);
5919 editor.autoindent(&Default::default(), window, cx)
5920 });
5921 cx.run_until_parked();
5922
5923 cx.update(|_, cx| {
5924 pretty_assertions::assert_eq!(
5925 buffer.read(cx).text(),
5926 indoc! { "
5927 impl A {
5928
5929 // a
5930 fn b(){}
5931
5932
5933 }
5934 fn c(){}
5935
5936 " }
5937 )
5938 });
5939 }
5940}
5941
5942#[gpui::test]
5943async fn test_autoclose_and_auto_surround_pairs(cx: &mut gpui::TestAppContext) {
5944 init_test(cx, |_| {});
5945
5946 let mut cx = EditorTestContext::new(cx).await;
5947
5948 let language = Arc::new(Language::new(
5949 LanguageConfig {
5950 brackets: BracketPairConfig {
5951 pairs: vec![
5952 BracketPair {
5953 start: "{".to_string(),
5954 end: "}".to_string(),
5955 close: true,
5956 surround: true,
5957 newline: true,
5958 },
5959 BracketPair {
5960 start: "(".to_string(),
5961 end: ")".to_string(),
5962 close: true,
5963 surround: true,
5964 newline: true,
5965 },
5966 BracketPair {
5967 start: "/*".to_string(),
5968 end: " */".to_string(),
5969 close: true,
5970 surround: true,
5971 newline: true,
5972 },
5973 BracketPair {
5974 start: "[".to_string(),
5975 end: "]".to_string(),
5976 close: false,
5977 surround: false,
5978 newline: true,
5979 },
5980 BracketPair {
5981 start: "\"".to_string(),
5982 end: "\"".to_string(),
5983 close: true,
5984 surround: true,
5985 newline: false,
5986 },
5987 BracketPair {
5988 start: "<".to_string(),
5989 end: ">".to_string(),
5990 close: false,
5991 surround: true,
5992 newline: true,
5993 },
5994 ],
5995 ..Default::default()
5996 },
5997 autoclose_before: "})]".to_string(),
5998 ..Default::default()
5999 },
6000 Some(tree_sitter_rust::LANGUAGE.into()),
6001 ));
6002
6003 cx.language_registry().add(language.clone());
6004 cx.update_buffer(|buffer, cx| {
6005 buffer.set_language(Some(language), cx);
6006 });
6007
6008 cx.set_state(
6009 &r#"
6010 🏀ˇ
6011 εˇ
6012 ❤️ˇ
6013 "#
6014 .unindent(),
6015 );
6016
6017 // autoclose multiple nested brackets at multiple cursors
6018 cx.update_editor(|editor, window, cx| {
6019 editor.handle_input("{", window, cx);
6020 editor.handle_input("{", window, cx);
6021 editor.handle_input("{", window, cx);
6022 });
6023 cx.assert_editor_state(
6024 &"
6025 🏀{{{ˇ}}}
6026 ε{{{ˇ}}}
6027 ❤️{{{ˇ}}}
6028 "
6029 .unindent(),
6030 );
6031
6032 // insert a different closing bracket
6033 cx.update_editor(|editor, window, cx| {
6034 editor.handle_input(")", window, cx);
6035 });
6036 cx.assert_editor_state(
6037 &"
6038 🏀{{{)ˇ}}}
6039 ε{{{)ˇ}}}
6040 ❤️{{{)ˇ}}}
6041 "
6042 .unindent(),
6043 );
6044
6045 // skip over the auto-closed brackets when typing a closing bracket
6046 cx.update_editor(|editor, window, cx| {
6047 editor.move_right(&MoveRight, window, cx);
6048 editor.handle_input("}", window, cx);
6049 editor.handle_input("}", window, cx);
6050 editor.handle_input("}", window, cx);
6051 });
6052 cx.assert_editor_state(
6053 &"
6054 🏀{{{)}}}}ˇ
6055 ε{{{)}}}}ˇ
6056 ❤️{{{)}}}}ˇ
6057 "
6058 .unindent(),
6059 );
6060
6061 // autoclose multi-character pairs
6062 cx.set_state(
6063 &"
6064 ˇ
6065 ˇ
6066 "
6067 .unindent(),
6068 );
6069 cx.update_editor(|editor, window, cx| {
6070 editor.handle_input("/", window, cx);
6071 editor.handle_input("*", window, cx);
6072 });
6073 cx.assert_editor_state(
6074 &"
6075 /*ˇ */
6076 /*ˇ */
6077 "
6078 .unindent(),
6079 );
6080
6081 // one cursor autocloses a multi-character pair, one cursor
6082 // does not autoclose.
6083 cx.set_state(
6084 &"
6085 /ˇ
6086 ˇ
6087 "
6088 .unindent(),
6089 );
6090 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6091 cx.assert_editor_state(
6092 &"
6093 /*ˇ */
6094 *ˇ
6095 "
6096 .unindent(),
6097 );
6098
6099 // Don't autoclose if the next character isn't whitespace and isn't
6100 // listed in the language's "autoclose_before" section.
6101 cx.set_state("ˇa b");
6102 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6103 cx.assert_editor_state("{ˇa b");
6104
6105 // Don't autoclose if `close` is false for the bracket pair
6106 cx.set_state("ˇ");
6107 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6108 cx.assert_editor_state("[ˇ");
6109
6110 // Surround with brackets if text is selected
6111 cx.set_state("«aˇ» b");
6112 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6113 cx.assert_editor_state("{«aˇ»} b");
6114
6115 // Autclose pair where the start and end characters are the same
6116 cx.set_state("aˇ");
6117 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6118 cx.assert_editor_state("a\"ˇ\"");
6119 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6120 cx.assert_editor_state("a\"\"ˇ");
6121
6122 // Don't autoclose pair if autoclose is disabled
6123 cx.set_state("ˇ");
6124 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6125 cx.assert_editor_state("<ˇ");
6126
6127 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6128 cx.set_state("«aˇ» b");
6129 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6130 cx.assert_editor_state("<«aˇ»> b");
6131}
6132
6133#[gpui::test]
6134async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut gpui::TestAppContext) {
6135 init_test(cx, |settings| {
6136 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6137 });
6138
6139 let mut cx = EditorTestContext::new(cx).await;
6140
6141 let language = Arc::new(Language::new(
6142 LanguageConfig {
6143 brackets: BracketPairConfig {
6144 pairs: vec![
6145 BracketPair {
6146 start: "{".to_string(),
6147 end: "}".to_string(),
6148 close: true,
6149 surround: true,
6150 newline: true,
6151 },
6152 BracketPair {
6153 start: "(".to_string(),
6154 end: ")".to_string(),
6155 close: true,
6156 surround: true,
6157 newline: true,
6158 },
6159 BracketPair {
6160 start: "[".to_string(),
6161 end: "]".to_string(),
6162 close: false,
6163 surround: false,
6164 newline: true,
6165 },
6166 ],
6167 ..Default::default()
6168 },
6169 autoclose_before: "})]".to_string(),
6170 ..Default::default()
6171 },
6172 Some(tree_sitter_rust::LANGUAGE.into()),
6173 ));
6174
6175 cx.language_registry().add(language.clone());
6176 cx.update_buffer(|buffer, cx| {
6177 buffer.set_language(Some(language), cx);
6178 });
6179
6180 cx.set_state(
6181 &"
6182 ˇ
6183 ˇ
6184 ˇ
6185 "
6186 .unindent(),
6187 );
6188
6189 // ensure only matching closing brackets are skipped over
6190 cx.update_editor(|editor, window, cx| {
6191 editor.handle_input("}", window, cx);
6192 editor.move_left(&MoveLeft, window, cx);
6193 editor.handle_input(")", window, cx);
6194 editor.move_left(&MoveLeft, window, cx);
6195 });
6196 cx.assert_editor_state(
6197 &"
6198 ˇ)}
6199 ˇ)}
6200 ˇ)}
6201 "
6202 .unindent(),
6203 );
6204
6205 // skip-over closing brackets at multiple cursors
6206 cx.update_editor(|editor, window, cx| {
6207 editor.handle_input(")", window, cx);
6208 editor.handle_input("}", window, cx);
6209 });
6210 cx.assert_editor_state(
6211 &"
6212 )}ˇ
6213 )}ˇ
6214 )}ˇ
6215 "
6216 .unindent(),
6217 );
6218
6219 // ignore non-close brackets
6220 cx.update_editor(|editor, window, cx| {
6221 editor.handle_input("]", window, cx);
6222 editor.move_left(&MoveLeft, window, cx);
6223 editor.handle_input("]", window, cx);
6224 });
6225 cx.assert_editor_state(
6226 &"
6227 )}]ˇ]
6228 )}]ˇ]
6229 )}]ˇ]
6230 "
6231 .unindent(),
6232 );
6233}
6234
6235#[gpui::test]
6236async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
6237 init_test(cx, |_| {});
6238
6239 let mut cx = EditorTestContext::new(cx).await;
6240
6241 let html_language = Arc::new(
6242 Language::new(
6243 LanguageConfig {
6244 name: "HTML".into(),
6245 brackets: BracketPairConfig {
6246 pairs: vec![
6247 BracketPair {
6248 start: "<".into(),
6249 end: ">".into(),
6250 close: true,
6251 ..Default::default()
6252 },
6253 BracketPair {
6254 start: "{".into(),
6255 end: "}".into(),
6256 close: true,
6257 ..Default::default()
6258 },
6259 BracketPair {
6260 start: "(".into(),
6261 end: ")".into(),
6262 close: true,
6263 ..Default::default()
6264 },
6265 ],
6266 ..Default::default()
6267 },
6268 autoclose_before: "})]>".into(),
6269 ..Default::default()
6270 },
6271 Some(tree_sitter_html::language()),
6272 )
6273 .with_injection_query(
6274 r#"
6275 (script_element
6276 (raw_text) @injection.content
6277 (#set! injection.language "javascript"))
6278 "#,
6279 )
6280 .unwrap(),
6281 );
6282
6283 let javascript_language = Arc::new(Language::new(
6284 LanguageConfig {
6285 name: "JavaScript".into(),
6286 brackets: BracketPairConfig {
6287 pairs: vec![
6288 BracketPair {
6289 start: "/*".into(),
6290 end: " */".into(),
6291 close: true,
6292 ..Default::default()
6293 },
6294 BracketPair {
6295 start: "{".into(),
6296 end: "}".into(),
6297 close: true,
6298 ..Default::default()
6299 },
6300 BracketPair {
6301 start: "(".into(),
6302 end: ")".into(),
6303 close: true,
6304 ..Default::default()
6305 },
6306 ],
6307 ..Default::default()
6308 },
6309 autoclose_before: "})]>".into(),
6310 ..Default::default()
6311 },
6312 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6313 ));
6314
6315 cx.language_registry().add(html_language.clone());
6316 cx.language_registry().add(javascript_language.clone());
6317
6318 cx.update_buffer(|buffer, cx| {
6319 buffer.set_language(Some(html_language), cx);
6320 });
6321
6322 cx.set_state(
6323 &r#"
6324 <body>ˇ
6325 <script>
6326 var x = 1;ˇ
6327 </script>
6328 </body>ˇ
6329 "#
6330 .unindent(),
6331 );
6332
6333 // Precondition: different languages are active at different locations.
6334 cx.update_editor(|editor, window, cx| {
6335 let snapshot = editor.snapshot(window, cx);
6336 let cursors = editor.selections.ranges::<usize>(cx);
6337 let languages = cursors
6338 .iter()
6339 .map(|c| snapshot.language_at(c.start).unwrap().name())
6340 .collect::<Vec<_>>();
6341 assert_eq!(
6342 languages,
6343 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6344 );
6345 });
6346
6347 // Angle brackets autoclose in HTML, but not JavaScript.
6348 cx.update_editor(|editor, window, cx| {
6349 editor.handle_input("<", window, cx);
6350 editor.handle_input("a", window, cx);
6351 });
6352 cx.assert_editor_state(
6353 &r#"
6354 <body><aˇ>
6355 <script>
6356 var x = 1;<aˇ
6357 </script>
6358 </body><aˇ>
6359 "#
6360 .unindent(),
6361 );
6362
6363 // Curly braces and parens autoclose in both HTML and JavaScript.
6364 cx.update_editor(|editor, window, cx| {
6365 editor.handle_input(" b=", window, cx);
6366 editor.handle_input("{", window, cx);
6367 editor.handle_input("c", window, cx);
6368 editor.handle_input("(", window, cx);
6369 });
6370 cx.assert_editor_state(
6371 &r#"
6372 <body><a b={c(ˇ)}>
6373 <script>
6374 var x = 1;<a b={c(ˇ)}
6375 </script>
6376 </body><a b={c(ˇ)}>
6377 "#
6378 .unindent(),
6379 );
6380
6381 // Brackets that were already autoclosed are skipped.
6382 cx.update_editor(|editor, window, cx| {
6383 editor.handle_input(")", window, cx);
6384 editor.handle_input("d", window, cx);
6385 editor.handle_input("}", window, cx);
6386 });
6387 cx.assert_editor_state(
6388 &r#"
6389 <body><a b={c()d}ˇ>
6390 <script>
6391 var x = 1;<a b={c()d}ˇ
6392 </script>
6393 </body><a b={c()d}ˇ>
6394 "#
6395 .unindent(),
6396 );
6397 cx.update_editor(|editor, window, cx| {
6398 editor.handle_input(">", window, cx);
6399 });
6400 cx.assert_editor_state(
6401 &r#"
6402 <body><a b={c()d}>ˇ
6403 <script>
6404 var x = 1;<a b={c()d}>ˇ
6405 </script>
6406 </body><a b={c()d}>ˇ
6407 "#
6408 .unindent(),
6409 );
6410
6411 // Reset
6412 cx.set_state(
6413 &r#"
6414 <body>ˇ
6415 <script>
6416 var x = 1;ˇ
6417 </script>
6418 </body>ˇ
6419 "#
6420 .unindent(),
6421 );
6422
6423 cx.update_editor(|editor, window, cx| {
6424 editor.handle_input("<", window, cx);
6425 });
6426 cx.assert_editor_state(
6427 &r#"
6428 <body><ˇ>
6429 <script>
6430 var x = 1;<ˇ
6431 </script>
6432 </body><ˇ>
6433 "#
6434 .unindent(),
6435 );
6436
6437 // When backspacing, the closing angle brackets are removed.
6438 cx.update_editor(|editor, window, cx| {
6439 editor.backspace(&Backspace, window, cx);
6440 });
6441 cx.assert_editor_state(
6442 &r#"
6443 <body>ˇ
6444 <script>
6445 var x = 1;ˇ
6446 </script>
6447 </body>ˇ
6448 "#
6449 .unindent(),
6450 );
6451
6452 // Block comments autoclose in JavaScript, but not HTML.
6453 cx.update_editor(|editor, window, cx| {
6454 editor.handle_input("/", window, cx);
6455 editor.handle_input("*", window, cx);
6456 });
6457 cx.assert_editor_state(
6458 &r#"
6459 <body>/*ˇ
6460 <script>
6461 var x = 1;/*ˇ */
6462 </script>
6463 </body>/*ˇ
6464 "#
6465 .unindent(),
6466 );
6467}
6468
6469#[gpui::test]
6470async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
6471 init_test(cx, |_| {});
6472
6473 let mut cx = EditorTestContext::new(cx).await;
6474
6475 let rust_language = Arc::new(
6476 Language::new(
6477 LanguageConfig {
6478 name: "Rust".into(),
6479 brackets: serde_json::from_value(json!([
6480 { "start": "{", "end": "}", "close": true, "newline": true },
6481 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
6482 ]))
6483 .unwrap(),
6484 autoclose_before: "})]>".into(),
6485 ..Default::default()
6486 },
6487 Some(tree_sitter_rust::LANGUAGE.into()),
6488 )
6489 .with_override_query("(string_literal) @string")
6490 .unwrap(),
6491 );
6492
6493 cx.language_registry().add(rust_language.clone());
6494 cx.update_buffer(|buffer, cx| {
6495 buffer.set_language(Some(rust_language), cx);
6496 });
6497
6498 cx.set_state(
6499 &r#"
6500 let x = ˇ
6501 "#
6502 .unindent(),
6503 );
6504
6505 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
6506 cx.update_editor(|editor, window, cx| {
6507 editor.handle_input("\"", window, cx);
6508 });
6509 cx.assert_editor_state(
6510 &r#"
6511 let x = "ˇ"
6512 "#
6513 .unindent(),
6514 );
6515
6516 // Inserting another quotation mark. The cursor moves across the existing
6517 // automatically-inserted quotation mark.
6518 cx.update_editor(|editor, window, cx| {
6519 editor.handle_input("\"", window, cx);
6520 });
6521 cx.assert_editor_state(
6522 &r#"
6523 let x = ""ˇ
6524 "#
6525 .unindent(),
6526 );
6527
6528 // Reset
6529 cx.set_state(
6530 &r#"
6531 let x = ˇ
6532 "#
6533 .unindent(),
6534 );
6535
6536 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
6537 cx.update_editor(|editor, window, cx| {
6538 editor.handle_input("\"", window, cx);
6539 editor.handle_input(" ", window, cx);
6540 editor.move_left(&Default::default(), window, cx);
6541 editor.handle_input("\\", window, cx);
6542 editor.handle_input("\"", window, cx);
6543 });
6544 cx.assert_editor_state(
6545 &r#"
6546 let x = "\"ˇ "
6547 "#
6548 .unindent(),
6549 );
6550
6551 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
6552 // mark. Nothing is inserted.
6553 cx.update_editor(|editor, window, cx| {
6554 editor.move_right(&Default::default(), window, cx);
6555 editor.handle_input("\"", window, cx);
6556 });
6557 cx.assert_editor_state(
6558 &r#"
6559 let x = "\" "ˇ
6560 "#
6561 .unindent(),
6562 );
6563}
6564
6565#[gpui::test]
6566async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
6567 init_test(cx, |_| {});
6568
6569 let language = Arc::new(Language::new(
6570 LanguageConfig {
6571 brackets: BracketPairConfig {
6572 pairs: vec![
6573 BracketPair {
6574 start: "{".to_string(),
6575 end: "}".to_string(),
6576 close: true,
6577 surround: true,
6578 newline: true,
6579 },
6580 BracketPair {
6581 start: "/* ".to_string(),
6582 end: "*/".to_string(),
6583 close: true,
6584 surround: true,
6585 ..Default::default()
6586 },
6587 ],
6588 ..Default::default()
6589 },
6590 ..Default::default()
6591 },
6592 Some(tree_sitter_rust::LANGUAGE.into()),
6593 ));
6594
6595 let text = r#"
6596 a
6597 b
6598 c
6599 "#
6600 .unindent();
6601
6602 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6603 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6604 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6605 editor
6606 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6607 .await;
6608
6609 editor.update_in(cx, |editor, window, cx| {
6610 editor.change_selections(None, window, cx, |s| {
6611 s.select_display_ranges([
6612 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6613 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6614 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
6615 ])
6616 });
6617
6618 editor.handle_input("{", window, cx);
6619 editor.handle_input("{", window, cx);
6620 editor.handle_input("{", window, cx);
6621 assert_eq!(
6622 editor.text(cx),
6623 "
6624 {{{a}}}
6625 {{{b}}}
6626 {{{c}}}
6627 "
6628 .unindent()
6629 );
6630 assert_eq!(
6631 editor.selections.display_ranges(cx),
6632 [
6633 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
6634 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
6635 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
6636 ]
6637 );
6638
6639 editor.undo(&Undo, window, cx);
6640 editor.undo(&Undo, window, cx);
6641 editor.undo(&Undo, window, cx);
6642 assert_eq!(
6643 editor.text(cx),
6644 "
6645 a
6646 b
6647 c
6648 "
6649 .unindent()
6650 );
6651 assert_eq!(
6652 editor.selections.display_ranges(cx),
6653 [
6654 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6655 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6656 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
6657 ]
6658 );
6659
6660 // Ensure inserting the first character of a multi-byte bracket pair
6661 // doesn't surround the selections with the bracket.
6662 editor.handle_input("/", window, cx);
6663 assert_eq!(
6664 editor.text(cx),
6665 "
6666 /
6667 /
6668 /
6669 "
6670 .unindent()
6671 );
6672 assert_eq!(
6673 editor.selections.display_ranges(cx),
6674 [
6675 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6676 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6677 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
6678 ]
6679 );
6680
6681 editor.undo(&Undo, window, cx);
6682 assert_eq!(
6683 editor.text(cx),
6684 "
6685 a
6686 b
6687 c
6688 "
6689 .unindent()
6690 );
6691 assert_eq!(
6692 editor.selections.display_ranges(cx),
6693 [
6694 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6695 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6696 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
6697 ]
6698 );
6699
6700 // Ensure inserting the last character of a multi-byte bracket pair
6701 // doesn't surround the selections with the bracket.
6702 editor.handle_input("*", window, cx);
6703 assert_eq!(
6704 editor.text(cx),
6705 "
6706 *
6707 *
6708 *
6709 "
6710 .unindent()
6711 );
6712 assert_eq!(
6713 editor.selections.display_ranges(cx),
6714 [
6715 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6716 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6717 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
6718 ]
6719 );
6720 });
6721}
6722
6723#[gpui::test]
6724async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
6725 init_test(cx, |_| {});
6726
6727 let language = Arc::new(Language::new(
6728 LanguageConfig {
6729 brackets: BracketPairConfig {
6730 pairs: vec![BracketPair {
6731 start: "{".to_string(),
6732 end: "}".to_string(),
6733 close: true,
6734 surround: true,
6735 newline: true,
6736 }],
6737 ..Default::default()
6738 },
6739 autoclose_before: "}".to_string(),
6740 ..Default::default()
6741 },
6742 Some(tree_sitter_rust::LANGUAGE.into()),
6743 ));
6744
6745 let text = r#"
6746 a
6747 b
6748 c
6749 "#
6750 .unindent();
6751
6752 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6753 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6754 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6755 editor
6756 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6757 .await;
6758
6759 editor.update_in(cx, |editor, window, cx| {
6760 editor.change_selections(None, window, cx, |s| {
6761 s.select_ranges([
6762 Point::new(0, 1)..Point::new(0, 1),
6763 Point::new(1, 1)..Point::new(1, 1),
6764 Point::new(2, 1)..Point::new(2, 1),
6765 ])
6766 });
6767
6768 editor.handle_input("{", window, cx);
6769 editor.handle_input("{", window, cx);
6770 editor.handle_input("_", window, cx);
6771 assert_eq!(
6772 editor.text(cx),
6773 "
6774 a{{_}}
6775 b{{_}}
6776 c{{_}}
6777 "
6778 .unindent()
6779 );
6780 assert_eq!(
6781 editor.selections.ranges::<Point>(cx),
6782 [
6783 Point::new(0, 4)..Point::new(0, 4),
6784 Point::new(1, 4)..Point::new(1, 4),
6785 Point::new(2, 4)..Point::new(2, 4)
6786 ]
6787 );
6788
6789 editor.backspace(&Default::default(), window, cx);
6790 editor.backspace(&Default::default(), window, cx);
6791 assert_eq!(
6792 editor.text(cx),
6793 "
6794 a{}
6795 b{}
6796 c{}
6797 "
6798 .unindent()
6799 );
6800 assert_eq!(
6801 editor.selections.ranges::<Point>(cx),
6802 [
6803 Point::new(0, 2)..Point::new(0, 2),
6804 Point::new(1, 2)..Point::new(1, 2),
6805 Point::new(2, 2)..Point::new(2, 2)
6806 ]
6807 );
6808
6809 editor.delete_to_previous_word_start(&Default::default(), window, cx);
6810 assert_eq!(
6811 editor.text(cx),
6812 "
6813 a
6814 b
6815 c
6816 "
6817 .unindent()
6818 );
6819 assert_eq!(
6820 editor.selections.ranges::<Point>(cx),
6821 [
6822 Point::new(0, 1)..Point::new(0, 1),
6823 Point::new(1, 1)..Point::new(1, 1),
6824 Point::new(2, 1)..Point::new(2, 1)
6825 ]
6826 );
6827 });
6828}
6829
6830#[gpui::test]
6831async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut gpui::TestAppContext) {
6832 init_test(cx, |settings| {
6833 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6834 });
6835
6836 let mut cx = EditorTestContext::new(cx).await;
6837
6838 let language = Arc::new(Language::new(
6839 LanguageConfig {
6840 brackets: BracketPairConfig {
6841 pairs: vec![
6842 BracketPair {
6843 start: "{".to_string(),
6844 end: "}".to_string(),
6845 close: true,
6846 surround: true,
6847 newline: true,
6848 },
6849 BracketPair {
6850 start: "(".to_string(),
6851 end: ")".to_string(),
6852 close: true,
6853 surround: true,
6854 newline: true,
6855 },
6856 BracketPair {
6857 start: "[".to_string(),
6858 end: "]".to_string(),
6859 close: false,
6860 surround: true,
6861 newline: true,
6862 },
6863 ],
6864 ..Default::default()
6865 },
6866 autoclose_before: "})]".to_string(),
6867 ..Default::default()
6868 },
6869 Some(tree_sitter_rust::LANGUAGE.into()),
6870 ));
6871
6872 cx.language_registry().add(language.clone());
6873 cx.update_buffer(|buffer, cx| {
6874 buffer.set_language(Some(language), cx);
6875 });
6876
6877 cx.set_state(
6878 &"
6879 {(ˇ)}
6880 [[ˇ]]
6881 {(ˇ)}
6882 "
6883 .unindent(),
6884 );
6885
6886 cx.update_editor(|editor, window, cx| {
6887 editor.backspace(&Default::default(), window, cx);
6888 editor.backspace(&Default::default(), window, cx);
6889 });
6890
6891 cx.assert_editor_state(
6892 &"
6893 ˇ
6894 ˇ]]
6895 ˇ
6896 "
6897 .unindent(),
6898 );
6899
6900 cx.update_editor(|editor, window, cx| {
6901 editor.handle_input("{", window, cx);
6902 editor.handle_input("{", window, cx);
6903 editor.move_right(&MoveRight, window, cx);
6904 editor.move_right(&MoveRight, window, cx);
6905 editor.move_left(&MoveLeft, window, cx);
6906 editor.move_left(&MoveLeft, window, cx);
6907 editor.backspace(&Default::default(), window, cx);
6908 });
6909
6910 cx.assert_editor_state(
6911 &"
6912 {ˇ}
6913 {ˇ}]]
6914 {ˇ}
6915 "
6916 .unindent(),
6917 );
6918
6919 cx.update_editor(|editor, window, cx| {
6920 editor.backspace(&Default::default(), window, cx);
6921 });
6922
6923 cx.assert_editor_state(
6924 &"
6925 ˇ
6926 ˇ]]
6927 ˇ
6928 "
6929 .unindent(),
6930 );
6931}
6932
6933#[gpui::test]
6934async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) {
6935 init_test(cx, |_| {});
6936
6937 let language = Arc::new(Language::new(
6938 LanguageConfig::default(),
6939 Some(tree_sitter_rust::LANGUAGE.into()),
6940 ));
6941
6942 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
6943 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6944 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6945 editor
6946 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6947 .await;
6948
6949 editor.update_in(cx, |editor, window, cx| {
6950 editor.set_auto_replace_emoji_shortcode(true);
6951
6952 editor.handle_input("Hello ", window, cx);
6953 editor.handle_input(":wave", window, cx);
6954 assert_eq!(editor.text(cx), "Hello :wave".unindent());
6955
6956 editor.handle_input(":", window, cx);
6957 assert_eq!(editor.text(cx), "Hello 👋".unindent());
6958
6959 editor.handle_input(" :smile", window, cx);
6960 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
6961
6962 editor.handle_input(":", window, cx);
6963 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
6964
6965 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
6966 editor.handle_input(":wave", window, cx);
6967 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
6968
6969 editor.handle_input(":", window, cx);
6970 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
6971
6972 editor.handle_input(":1", window, cx);
6973 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
6974
6975 editor.handle_input(":", window, cx);
6976 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
6977
6978 // Ensure shortcode does not get replaced when it is part of a word
6979 editor.handle_input(" Test:wave", window, cx);
6980 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
6981
6982 editor.handle_input(":", window, cx);
6983 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
6984
6985 editor.set_auto_replace_emoji_shortcode(false);
6986
6987 // Ensure shortcode does not get replaced when auto replace is off
6988 editor.handle_input(" :wave", window, cx);
6989 assert_eq!(
6990 editor.text(cx),
6991 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
6992 );
6993
6994 editor.handle_input(":", window, cx);
6995 assert_eq!(
6996 editor.text(cx),
6997 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
6998 );
6999 });
7000}
7001
7002#[gpui::test]
7003async fn test_snippet_placeholder_choices(cx: &mut gpui::TestAppContext) {
7004 init_test(cx, |_| {});
7005
7006 let (text, insertion_ranges) = marked_text_ranges(
7007 indoc! {"
7008 ˇ
7009 "},
7010 false,
7011 );
7012
7013 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7014 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7015
7016 _ = editor.update_in(cx, |editor, window, cx| {
7017 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7018
7019 editor
7020 .insert_snippet(&insertion_ranges, snippet, window, cx)
7021 .unwrap();
7022
7023 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7024 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7025 assert_eq!(editor.text(cx), expected_text);
7026 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7027 }
7028
7029 assert(
7030 editor,
7031 cx,
7032 indoc! {"
7033 type «» =•
7034 "},
7035 );
7036
7037 assert!(editor.context_menu_visible(), "There should be a matches");
7038 });
7039}
7040
7041#[gpui::test]
7042async fn test_snippets(cx: &mut gpui::TestAppContext) {
7043 init_test(cx, |_| {});
7044
7045 let (text, insertion_ranges) = marked_text_ranges(
7046 indoc! {"
7047 a.ˇ b
7048 a.ˇ b
7049 a.ˇ b
7050 "},
7051 false,
7052 );
7053
7054 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7055 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7056
7057 editor.update_in(cx, |editor, window, cx| {
7058 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
7059
7060 editor
7061 .insert_snippet(&insertion_ranges, snippet, window, cx)
7062 .unwrap();
7063
7064 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7065 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7066 assert_eq!(editor.text(cx), expected_text);
7067 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7068 }
7069
7070 assert(
7071 editor,
7072 cx,
7073 indoc! {"
7074 a.f(«one», two, «three») b
7075 a.f(«one», two, «three») b
7076 a.f(«one», two, «three») b
7077 "},
7078 );
7079
7080 // Can't move earlier than the first tab stop
7081 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7082 assert(
7083 editor,
7084 cx,
7085 indoc! {"
7086 a.f(«one», two, «three») b
7087 a.f(«one», two, «three») b
7088 a.f(«one», two, «three») b
7089 "},
7090 );
7091
7092 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7093 assert(
7094 editor,
7095 cx,
7096 indoc! {"
7097 a.f(one, «two», three) b
7098 a.f(one, «two», three) b
7099 a.f(one, «two», three) b
7100 "},
7101 );
7102
7103 editor.move_to_prev_snippet_tabstop(window, cx);
7104 assert(
7105 editor,
7106 cx,
7107 indoc! {"
7108 a.f(«one», two, «three») b
7109 a.f(«one», two, «three») b
7110 a.f(«one», two, «three») b
7111 "},
7112 );
7113
7114 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7115 assert(
7116 editor,
7117 cx,
7118 indoc! {"
7119 a.f(one, «two», three) b
7120 a.f(one, «two», three) b
7121 a.f(one, «two», three) b
7122 "},
7123 );
7124 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7125 assert(
7126 editor,
7127 cx,
7128 indoc! {"
7129 a.f(one, two, three)ˇ b
7130 a.f(one, two, three)ˇ b
7131 a.f(one, two, three)ˇ b
7132 "},
7133 );
7134
7135 // As soon as the last tab stop is reached, snippet state is gone
7136 editor.move_to_prev_snippet_tabstop(window, cx);
7137 assert(
7138 editor,
7139 cx,
7140 indoc! {"
7141 a.f(one, two, three)ˇ b
7142 a.f(one, two, three)ˇ b
7143 a.f(one, two, three)ˇ b
7144 "},
7145 );
7146 });
7147}
7148
7149#[gpui::test]
7150async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
7151 init_test(cx, |_| {});
7152
7153 let fs = FakeFs::new(cx.executor());
7154 fs.insert_file(path!("/file.rs"), Default::default()).await;
7155
7156 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
7157
7158 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7159 language_registry.add(rust_lang());
7160 let mut fake_servers = language_registry.register_fake_lsp(
7161 "Rust",
7162 FakeLspAdapter {
7163 capabilities: lsp::ServerCapabilities {
7164 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7165 ..Default::default()
7166 },
7167 ..Default::default()
7168 },
7169 );
7170
7171 let buffer = project
7172 .update(cx, |project, cx| {
7173 project.open_local_buffer(path!("/file.rs"), cx)
7174 })
7175 .await
7176 .unwrap();
7177
7178 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7179 let (editor, cx) = cx.add_window_view(|window, cx| {
7180 build_editor_with_project(project.clone(), buffer, window, cx)
7181 });
7182 editor.update_in(cx, |editor, window, cx| {
7183 editor.set_text("one\ntwo\nthree\n", window, cx)
7184 });
7185 assert!(cx.read(|cx| editor.is_dirty(cx)));
7186
7187 cx.executor().start_waiting();
7188 let fake_server = fake_servers.next().await.unwrap();
7189
7190 let save = editor
7191 .update_in(cx, |editor, window, cx| {
7192 editor.save(true, project.clone(), window, cx)
7193 })
7194 .unwrap();
7195 fake_server
7196 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7197 assert_eq!(
7198 params.text_document.uri,
7199 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7200 );
7201 assert_eq!(params.options.tab_size, 4);
7202 Ok(Some(vec![lsp::TextEdit::new(
7203 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7204 ", ".to_string(),
7205 )]))
7206 })
7207 .next()
7208 .await;
7209 cx.executor().start_waiting();
7210 save.await;
7211
7212 assert_eq!(
7213 editor.update(cx, |editor, cx| editor.text(cx)),
7214 "one, two\nthree\n"
7215 );
7216 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7217
7218 editor.update_in(cx, |editor, window, cx| {
7219 editor.set_text("one\ntwo\nthree\n", window, cx)
7220 });
7221 assert!(cx.read(|cx| editor.is_dirty(cx)));
7222
7223 // Ensure we can still save even if formatting hangs.
7224 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7225 assert_eq!(
7226 params.text_document.uri,
7227 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7228 );
7229 futures::future::pending::<()>().await;
7230 unreachable!()
7231 });
7232 let save = editor
7233 .update_in(cx, |editor, window, cx| {
7234 editor.save(true, project.clone(), window, cx)
7235 })
7236 .unwrap();
7237 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7238 cx.executor().start_waiting();
7239 save.await;
7240 assert_eq!(
7241 editor.update(cx, |editor, cx| editor.text(cx)),
7242 "one\ntwo\nthree\n"
7243 );
7244 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7245
7246 // For non-dirty buffer, no formatting request should be sent
7247 let save = editor
7248 .update_in(cx, |editor, window, cx| {
7249 editor.save(true, project.clone(), window, cx)
7250 })
7251 .unwrap();
7252 let _pending_format_request = fake_server
7253 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7254 panic!("Should not be invoked on non-dirty buffer");
7255 })
7256 .next();
7257 cx.executor().start_waiting();
7258 save.await;
7259
7260 // Set rust language override and assert overridden tabsize is sent to language server
7261 update_test_language_settings(cx, |settings| {
7262 settings.languages.insert(
7263 "Rust".into(),
7264 LanguageSettingsContent {
7265 tab_size: NonZeroU32::new(8),
7266 ..Default::default()
7267 },
7268 );
7269 });
7270
7271 editor.update_in(cx, |editor, window, cx| {
7272 editor.set_text("somehting_new\n", window, cx)
7273 });
7274 assert!(cx.read(|cx| editor.is_dirty(cx)));
7275 let save = editor
7276 .update_in(cx, |editor, window, cx| {
7277 editor.save(true, project.clone(), window, cx)
7278 })
7279 .unwrap();
7280 fake_server
7281 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7282 assert_eq!(
7283 params.text_document.uri,
7284 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7285 );
7286 assert_eq!(params.options.tab_size, 8);
7287 Ok(Some(vec![]))
7288 })
7289 .next()
7290 .await;
7291 cx.executor().start_waiting();
7292 save.await;
7293}
7294
7295#[gpui::test]
7296async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) {
7297 init_test(cx, |_| {});
7298
7299 let cols = 4;
7300 let rows = 10;
7301 let sample_text_1 = sample_text(rows, cols, 'a');
7302 assert_eq!(
7303 sample_text_1,
7304 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7305 );
7306 let sample_text_2 = sample_text(rows, cols, 'l');
7307 assert_eq!(
7308 sample_text_2,
7309 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7310 );
7311 let sample_text_3 = sample_text(rows, cols, 'v');
7312 assert_eq!(
7313 sample_text_3,
7314 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7315 );
7316
7317 let fs = FakeFs::new(cx.executor());
7318 fs.insert_tree(
7319 path!("/a"),
7320 json!({
7321 "main.rs": sample_text_1,
7322 "other.rs": sample_text_2,
7323 "lib.rs": sample_text_3,
7324 }),
7325 )
7326 .await;
7327
7328 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
7329 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7330 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7331
7332 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7333 language_registry.add(rust_lang());
7334 let mut fake_servers = language_registry.register_fake_lsp(
7335 "Rust",
7336 FakeLspAdapter {
7337 capabilities: lsp::ServerCapabilities {
7338 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7339 ..Default::default()
7340 },
7341 ..Default::default()
7342 },
7343 );
7344
7345 let worktree = project.update(cx, |project, cx| {
7346 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7347 assert_eq!(worktrees.len(), 1);
7348 worktrees.pop().unwrap()
7349 });
7350 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
7351
7352 let buffer_1 = project
7353 .update(cx, |project, cx| {
7354 project.open_buffer((worktree_id, "main.rs"), cx)
7355 })
7356 .await
7357 .unwrap();
7358 let buffer_2 = project
7359 .update(cx, |project, cx| {
7360 project.open_buffer((worktree_id, "other.rs"), cx)
7361 })
7362 .await
7363 .unwrap();
7364 let buffer_3 = project
7365 .update(cx, |project, cx| {
7366 project.open_buffer((worktree_id, "lib.rs"), cx)
7367 })
7368 .await
7369 .unwrap();
7370
7371 let multi_buffer = cx.new(|cx| {
7372 let mut multi_buffer = MultiBuffer::new(ReadWrite);
7373 multi_buffer.push_excerpts(
7374 buffer_1.clone(),
7375 [
7376 ExcerptRange {
7377 context: Point::new(0, 0)..Point::new(3, 0),
7378 primary: None,
7379 },
7380 ExcerptRange {
7381 context: Point::new(5, 0)..Point::new(7, 0),
7382 primary: None,
7383 },
7384 ExcerptRange {
7385 context: Point::new(9, 0)..Point::new(10, 4),
7386 primary: None,
7387 },
7388 ],
7389 cx,
7390 );
7391 multi_buffer.push_excerpts(
7392 buffer_2.clone(),
7393 [
7394 ExcerptRange {
7395 context: Point::new(0, 0)..Point::new(3, 0),
7396 primary: None,
7397 },
7398 ExcerptRange {
7399 context: Point::new(5, 0)..Point::new(7, 0),
7400 primary: None,
7401 },
7402 ExcerptRange {
7403 context: Point::new(9, 0)..Point::new(10, 4),
7404 primary: None,
7405 },
7406 ],
7407 cx,
7408 );
7409 multi_buffer.push_excerpts(
7410 buffer_3.clone(),
7411 [
7412 ExcerptRange {
7413 context: Point::new(0, 0)..Point::new(3, 0),
7414 primary: None,
7415 },
7416 ExcerptRange {
7417 context: Point::new(5, 0)..Point::new(7, 0),
7418 primary: None,
7419 },
7420 ExcerptRange {
7421 context: Point::new(9, 0)..Point::new(10, 4),
7422 primary: None,
7423 },
7424 ],
7425 cx,
7426 );
7427 multi_buffer
7428 });
7429 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
7430 Editor::new(
7431 EditorMode::Full,
7432 multi_buffer,
7433 Some(project.clone()),
7434 true,
7435 window,
7436 cx,
7437 )
7438 });
7439
7440 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7441 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7442 s.select_ranges(Some(1..2))
7443 });
7444 editor.insert("|one|two|three|", window, cx);
7445 });
7446 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7447 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7448 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7449 s.select_ranges(Some(60..70))
7450 });
7451 editor.insert("|four|five|six|", window, cx);
7452 });
7453 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7454
7455 // First two buffers should be edited, but not the third one.
7456 assert_eq!(
7457 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7458 "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}",
7459 );
7460 buffer_1.update(cx, |buffer, _| {
7461 assert!(buffer.is_dirty());
7462 assert_eq!(
7463 buffer.text(),
7464 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
7465 )
7466 });
7467 buffer_2.update(cx, |buffer, _| {
7468 assert!(buffer.is_dirty());
7469 assert_eq!(
7470 buffer.text(),
7471 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
7472 )
7473 });
7474 buffer_3.update(cx, |buffer, _| {
7475 assert!(!buffer.is_dirty());
7476 assert_eq!(buffer.text(), sample_text_3,)
7477 });
7478 cx.executor().run_until_parked();
7479
7480 cx.executor().start_waiting();
7481 let save = multi_buffer_editor
7482 .update_in(cx, |editor, window, cx| {
7483 editor.save(true, project.clone(), window, cx)
7484 })
7485 .unwrap();
7486
7487 let fake_server = fake_servers.next().await.unwrap();
7488 fake_server
7489 .server
7490 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7491 Ok(Some(vec![lsp::TextEdit::new(
7492 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7493 format!("[{} formatted]", params.text_document.uri),
7494 )]))
7495 })
7496 .detach();
7497 save.await;
7498
7499 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
7500 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
7501 assert_eq!(
7502 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7503 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}"),
7504 );
7505 buffer_1.update(cx, |buffer, _| {
7506 assert!(!buffer.is_dirty());
7507 assert_eq!(
7508 buffer.text(),
7509 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
7510 )
7511 });
7512 buffer_2.update(cx, |buffer, _| {
7513 assert!(!buffer.is_dirty());
7514 assert_eq!(
7515 buffer.text(),
7516 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
7517 )
7518 });
7519 buffer_3.update(cx, |buffer, _| {
7520 assert!(!buffer.is_dirty());
7521 assert_eq!(buffer.text(), sample_text_3,)
7522 });
7523}
7524
7525#[gpui::test]
7526async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
7527 init_test(cx, |_| {});
7528
7529 let fs = FakeFs::new(cx.executor());
7530 fs.insert_file(path!("/file.rs"), Default::default()).await;
7531
7532 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
7533
7534 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7535 language_registry.add(rust_lang());
7536 let mut fake_servers = language_registry.register_fake_lsp(
7537 "Rust",
7538 FakeLspAdapter {
7539 capabilities: lsp::ServerCapabilities {
7540 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
7541 ..Default::default()
7542 },
7543 ..Default::default()
7544 },
7545 );
7546
7547 let buffer = project
7548 .update(cx, |project, cx| {
7549 project.open_local_buffer(path!("/file.rs"), cx)
7550 })
7551 .await
7552 .unwrap();
7553
7554 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7555 let (editor, cx) = cx.add_window_view(|window, cx| {
7556 build_editor_with_project(project.clone(), buffer, window, cx)
7557 });
7558 editor.update_in(cx, |editor, window, cx| {
7559 editor.set_text("one\ntwo\nthree\n", window, cx)
7560 });
7561 assert!(cx.read(|cx| editor.is_dirty(cx)));
7562
7563 cx.executor().start_waiting();
7564 let fake_server = fake_servers.next().await.unwrap();
7565
7566 let save = editor
7567 .update_in(cx, |editor, window, cx| {
7568 editor.save(true, project.clone(), window, cx)
7569 })
7570 .unwrap();
7571 fake_server
7572 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
7573 assert_eq!(
7574 params.text_document.uri,
7575 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7576 );
7577 assert_eq!(params.options.tab_size, 4);
7578 Ok(Some(vec![lsp::TextEdit::new(
7579 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7580 ", ".to_string(),
7581 )]))
7582 })
7583 .next()
7584 .await;
7585 cx.executor().start_waiting();
7586 save.await;
7587 assert_eq!(
7588 editor.update(cx, |editor, cx| editor.text(cx)),
7589 "one, two\nthree\n"
7590 );
7591 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7592
7593 editor.update_in(cx, |editor, window, cx| {
7594 editor.set_text("one\ntwo\nthree\n", window, cx)
7595 });
7596 assert!(cx.read(|cx| editor.is_dirty(cx)));
7597
7598 // Ensure we can still save even if formatting hangs.
7599 fake_server.handle_request::<lsp::request::RangeFormatting, _, _>(
7600 move |params, _| async move {
7601 assert_eq!(
7602 params.text_document.uri,
7603 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7604 );
7605 futures::future::pending::<()>().await;
7606 unreachable!()
7607 },
7608 );
7609 let save = editor
7610 .update_in(cx, |editor, window, cx| {
7611 editor.save(true, project.clone(), window, cx)
7612 })
7613 .unwrap();
7614 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7615 cx.executor().start_waiting();
7616 save.await;
7617 assert_eq!(
7618 editor.update(cx, |editor, cx| editor.text(cx)),
7619 "one\ntwo\nthree\n"
7620 );
7621 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7622
7623 // For non-dirty buffer, no formatting request should be sent
7624 let save = editor
7625 .update_in(cx, |editor, window, cx| {
7626 editor.save(true, project.clone(), window, cx)
7627 })
7628 .unwrap();
7629 let _pending_format_request = fake_server
7630 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7631 panic!("Should not be invoked on non-dirty buffer");
7632 })
7633 .next();
7634 cx.executor().start_waiting();
7635 save.await;
7636
7637 // Set Rust language override and assert overridden tabsize is sent to language server
7638 update_test_language_settings(cx, |settings| {
7639 settings.languages.insert(
7640 "Rust".into(),
7641 LanguageSettingsContent {
7642 tab_size: NonZeroU32::new(8),
7643 ..Default::default()
7644 },
7645 );
7646 });
7647
7648 editor.update_in(cx, |editor, window, cx| {
7649 editor.set_text("somehting_new\n", window, cx)
7650 });
7651 assert!(cx.read(|cx| editor.is_dirty(cx)));
7652 let save = editor
7653 .update_in(cx, |editor, window, cx| {
7654 editor.save(true, project.clone(), window, cx)
7655 })
7656 .unwrap();
7657 fake_server
7658 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
7659 assert_eq!(
7660 params.text_document.uri,
7661 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7662 );
7663 assert_eq!(params.options.tab_size, 8);
7664 Ok(Some(vec![]))
7665 })
7666 .next()
7667 .await;
7668 cx.executor().start_waiting();
7669 save.await;
7670}
7671
7672#[gpui::test]
7673async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
7674 init_test(cx, |settings| {
7675 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
7676 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
7677 ))
7678 });
7679
7680 let fs = FakeFs::new(cx.executor());
7681 fs.insert_file(path!("/file.rs"), Default::default()).await;
7682
7683 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
7684
7685 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7686 language_registry.add(Arc::new(Language::new(
7687 LanguageConfig {
7688 name: "Rust".into(),
7689 matcher: LanguageMatcher {
7690 path_suffixes: vec!["rs".to_string()],
7691 ..Default::default()
7692 },
7693 ..LanguageConfig::default()
7694 },
7695 Some(tree_sitter_rust::LANGUAGE.into()),
7696 )));
7697 update_test_language_settings(cx, |settings| {
7698 // Enable Prettier formatting for the same buffer, and ensure
7699 // LSP is called instead of Prettier.
7700 settings.defaults.prettier = Some(PrettierSettings {
7701 allowed: true,
7702 ..PrettierSettings::default()
7703 });
7704 });
7705 let mut fake_servers = language_registry.register_fake_lsp(
7706 "Rust",
7707 FakeLspAdapter {
7708 capabilities: lsp::ServerCapabilities {
7709 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7710 ..Default::default()
7711 },
7712 ..Default::default()
7713 },
7714 );
7715
7716 let buffer = project
7717 .update(cx, |project, cx| {
7718 project.open_local_buffer(path!("/file.rs"), cx)
7719 })
7720 .await
7721 .unwrap();
7722
7723 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7724 let (editor, cx) = cx.add_window_view(|window, cx| {
7725 build_editor_with_project(project.clone(), buffer, window, cx)
7726 });
7727 editor.update_in(cx, |editor, window, cx| {
7728 editor.set_text("one\ntwo\nthree\n", window, cx)
7729 });
7730
7731 cx.executor().start_waiting();
7732 let fake_server = fake_servers.next().await.unwrap();
7733
7734 let format = editor
7735 .update_in(cx, |editor, window, cx| {
7736 editor.perform_format(
7737 project.clone(),
7738 FormatTrigger::Manual,
7739 FormatTarget::Buffers,
7740 window,
7741 cx,
7742 )
7743 })
7744 .unwrap();
7745 fake_server
7746 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7747 assert_eq!(
7748 params.text_document.uri,
7749 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7750 );
7751 assert_eq!(params.options.tab_size, 4);
7752 Ok(Some(vec![lsp::TextEdit::new(
7753 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7754 ", ".to_string(),
7755 )]))
7756 })
7757 .next()
7758 .await;
7759 cx.executor().start_waiting();
7760 format.await;
7761 assert_eq!(
7762 editor.update(cx, |editor, cx| editor.text(cx)),
7763 "one, two\nthree\n"
7764 );
7765
7766 editor.update_in(cx, |editor, window, cx| {
7767 editor.set_text("one\ntwo\nthree\n", window, cx)
7768 });
7769 // Ensure we don't lock if formatting hangs.
7770 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7771 assert_eq!(
7772 params.text_document.uri,
7773 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7774 );
7775 futures::future::pending::<()>().await;
7776 unreachable!()
7777 });
7778 let format = editor
7779 .update_in(cx, |editor, window, cx| {
7780 editor.perform_format(
7781 project,
7782 FormatTrigger::Manual,
7783 FormatTarget::Buffers,
7784 window,
7785 cx,
7786 )
7787 })
7788 .unwrap();
7789 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7790 cx.executor().start_waiting();
7791 format.await;
7792 assert_eq!(
7793 editor.update(cx, |editor, cx| editor.text(cx)),
7794 "one\ntwo\nthree\n"
7795 );
7796}
7797
7798#[gpui::test]
7799async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
7800 init_test(cx, |_| {});
7801
7802 let mut cx = EditorLspTestContext::new_rust(
7803 lsp::ServerCapabilities {
7804 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7805 ..Default::default()
7806 },
7807 cx,
7808 )
7809 .await;
7810
7811 cx.set_state(indoc! {"
7812 one.twoˇ
7813 "});
7814
7815 // The format request takes a long time. When it completes, it inserts
7816 // a newline and an indent before the `.`
7817 cx.lsp
7818 .handle_request::<lsp::request::Formatting, _, _>(move |_, cx| {
7819 let executor = cx.background_executor().clone();
7820 async move {
7821 executor.timer(Duration::from_millis(100)).await;
7822 Ok(Some(vec![lsp::TextEdit {
7823 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
7824 new_text: "\n ".into(),
7825 }]))
7826 }
7827 });
7828
7829 // Submit a format request.
7830 let format_1 = cx
7831 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7832 .unwrap();
7833 cx.executor().run_until_parked();
7834
7835 // Submit a second format request.
7836 let format_2 = cx
7837 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7838 .unwrap();
7839 cx.executor().run_until_parked();
7840
7841 // Wait for both format requests to complete
7842 cx.executor().advance_clock(Duration::from_millis(200));
7843 cx.executor().start_waiting();
7844 format_1.await.unwrap();
7845 cx.executor().start_waiting();
7846 format_2.await.unwrap();
7847
7848 // The formatting edits only happens once.
7849 cx.assert_editor_state(indoc! {"
7850 one
7851 .twoˇ
7852 "});
7853}
7854
7855#[gpui::test]
7856async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
7857 init_test(cx, |settings| {
7858 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
7859 });
7860
7861 let mut cx = EditorLspTestContext::new_rust(
7862 lsp::ServerCapabilities {
7863 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7864 ..Default::default()
7865 },
7866 cx,
7867 )
7868 .await;
7869
7870 // Set up a buffer white some trailing whitespace and no trailing newline.
7871 cx.set_state(
7872 &[
7873 "one ", //
7874 "twoˇ", //
7875 "three ", //
7876 "four", //
7877 ]
7878 .join("\n"),
7879 );
7880
7881 // Submit a format request.
7882 let format = cx
7883 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7884 .unwrap();
7885
7886 // Record which buffer changes have been sent to the language server
7887 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
7888 cx.lsp
7889 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
7890 let buffer_changes = buffer_changes.clone();
7891 move |params, _| {
7892 buffer_changes.lock().extend(
7893 params
7894 .content_changes
7895 .into_iter()
7896 .map(|e| (e.range.unwrap(), e.text)),
7897 );
7898 }
7899 });
7900
7901 // Handle formatting requests to the language server.
7902 cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
7903 let buffer_changes = buffer_changes.clone();
7904 move |_, _| {
7905 // When formatting is requested, trailing whitespace has already been stripped,
7906 // and the trailing newline has already been added.
7907 assert_eq!(
7908 &buffer_changes.lock()[1..],
7909 &[
7910 (
7911 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
7912 "".into()
7913 ),
7914 (
7915 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
7916 "".into()
7917 ),
7918 (
7919 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
7920 "\n".into()
7921 ),
7922 ]
7923 );
7924
7925 // Insert blank lines between each line of the buffer.
7926 async move {
7927 Ok(Some(vec![
7928 lsp::TextEdit {
7929 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
7930 new_text: "\n".into(),
7931 },
7932 lsp::TextEdit {
7933 range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
7934 new_text: "\n".into(),
7935 },
7936 ]))
7937 }
7938 }
7939 });
7940
7941 // After formatting the buffer, the trailing whitespace is stripped,
7942 // a newline is appended, and the edits provided by the language server
7943 // have been applied.
7944 format.await.unwrap();
7945 cx.assert_editor_state(
7946 &[
7947 "one", //
7948 "", //
7949 "twoˇ", //
7950 "", //
7951 "three", //
7952 "four", //
7953 "", //
7954 ]
7955 .join("\n"),
7956 );
7957
7958 // Undoing the formatting undoes the trailing whitespace removal, the
7959 // trailing newline, and the LSP edits.
7960 cx.update_buffer(|buffer, cx| buffer.undo(cx));
7961 cx.assert_editor_state(
7962 &[
7963 "one ", //
7964 "twoˇ", //
7965 "three ", //
7966 "four", //
7967 ]
7968 .join("\n"),
7969 );
7970}
7971
7972#[gpui::test]
7973async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
7974 cx: &mut gpui::TestAppContext,
7975) {
7976 init_test(cx, |_| {});
7977
7978 cx.update(|cx| {
7979 cx.update_global::<SettingsStore, _>(|settings, cx| {
7980 settings.update_user_settings::<EditorSettings>(cx, |settings| {
7981 settings.auto_signature_help = Some(true);
7982 });
7983 });
7984 });
7985
7986 let mut cx = EditorLspTestContext::new_rust(
7987 lsp::ServerCapabilities {
7988 signature_help_provider: Some(lsp::SignatureHelpOptions {
7989 ..Default::default()
7990 }),
7991 ..Default::default()
7992 },
7993 cx,
7994 )
7995 .await;
7996
7997 let language = Language::new(
7998 LanguageConfig {
7999 name: "Rust".into(),
8000 brackets: BracketPairConfig {
8001 pairs: vec![
8002 BracketPair {
8003 start: "{".to_string(),
8004 end: "}".to_string(),
8005 close: true,
8006 surround: true,
8007 newline: true,
8008 },
8009 BracketPair {
8010 start: "(".to_string(),
8011 end: ")".to_string(),
8012 close: true,
8013 surround: true,
8014 newline: true,
8015 },
8016 BracketPair {
8017 start: "/*".to_string(),
8018 end: " */".to_string(),
8019 close: true,
8020 surround: true,
8021 newline: true,
8022 },
8023 BracketPair {
8024 start: "[".to_string(),
8025 end: "]".to_string(),
8026 close: false,
8027 surround: false,
8028 newline: true,
8029 },
8030 BracketPair {
8031 start: "\"".to_string(),
8032 end: "\"".to_string(),
8033 close: true,
8034 surround: true,
8035 newline: false,
8036 },
8037 BracketPair {
8038 start: "<".to_string(),
8039 end: ">".to_string(),
8040 close: false,
8041 surround: true,
8042 newline: true,
8043 },
8044 ],
8045 ..Default::default()
8046 },
8047 autoclose_before: "})]".to_string(),
8048 ..Default::default()
8049 },
8050 Some(tree_sitter_rust::LANGUAGE.into()),
8051 );
8052 let language = Arc::new(language);
8053
8054 cx.language_registry().add(language.clone());
8055 cx.update_buffer(|buffer, cx| {
8056 buffer.set_language(Some(language), cx);
8057 });
8058
8059 cx.set_state(
8060 &r#"
8061 fn main() {
8062 sampleˇ
8063 }
8064 "#
8065 .unindent(),
8066 );
8067
8068 cx.update_editor(|editor, window, cx| {
8069 editor.handle_input("(", window, cx);
8070 });
8071 cx.assert_editor_state(
8072 &"
8073 fn main() {
8074 sample(ˇ)
8075 }
8076 "
8077 .unindent(),
8078 );
8079
8080 let mocked_response = lsp::SignatureHelp {
8081 signatures: vec![lsp::SignatureInformation {
8082 label: "fn sample(param1: u8, param2: u8)".to_string(),
8083 documentation: None,
8084 parameters: Some(vec![
8085 lsp::ParameterInformation {
8086 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8087 documentation: None,
8088 },
8089 lsp::ParameterInformation {
8090 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8091 documentation: None,
8092 },
8093 ]),
8094 active_parameter: None,
8095 }],
8096 active_signature: Some(0),
8097 active_parameter: Some(0),
8098 };
8099 handle_signature_help_request(&mut cx, mocked_response).await;
8100
8101 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8102 .await;
8103
8104 cx.editor(|editor, _, _| {
8105 let signature_help_state = editor.signature_help_state.popover().cloned();
8106 assert!(signature_help_state.is_some());
8107 let ParsedMarkdown {
8108 text, highlights, ..
8109 } = signature_help_state.unwrap().parsed_content;
8110 assert_eq!(text, "param1: u8, param2: u8");
8111 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8112 });
8113}
8114
8115#[gpui::test]
8116async fn test_handle_input_with_different_show_signature_settings(cx: &mut gpui::TestAppContext) {
8117 init_test(cx, |_| {});
8118
8119 cx.update(|cx| {
8120 cx.update_global::<SettingsStore, _>(|settings, cx| {
8121 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8122 settings.auto_signature_help = Some(false);
8123 settings.show_signature_help_after_edits = Some(false);
8124 });
8125 });
8126 });
8127
8128 let mut cx = EditorLspTestContext::new_rust(
8129 lsp::ServerCapabilities {
8130 signature_help_provider: Some(lsp::SignatureHelpOptions {
8131 ..Default::default()
8132 }),
8133 ..Default::default()
8134 },
8135 cx,
8136 )
8137 .await;
8138
8139 let language = Language::new(
8140 LanguageConfig {
8141 name: "Rust".into(),
8142 brackets: BracketPairConfig {
8143 pairs: vec![
8144 BracketPair {
8145 start: "{".to_string(),
8146 end: "}".to_string(),
8147 close: true,
8148 surround: true,
8149 newline: true,
8150 },
8151 BracketPair {
8152 start: "(".to_string(),
8153 end: ")".to_string(),
8154 close: true,
8155 surround: true,
8156 newline: true,
8157 },
8158 BracketPair {
8159 start: "/*".to_string(),
8160 end: " */".to_string(),
8161 close: true,
8162 surround: true,
8163 newline: true,
8164 },
8165 BracketPair {
8166 start: "[".to_string(),
8167 end: "]".to_string(),
8168 close: false,
8169 surround: false,
8170 newline: true,
8171 },
8172 BracketPair {
8173 start: "\"".to_string(),
8174 end: "\"".to_string(),
8175 close: true,
8176 surround: true,
8177 newline: false,
8178 },
8179 BracketPair {
8180 start: "<".to_string(),
8181 end: ">".to_string(),
8182 close: false,
8183 surround: true,
8184 newline: true,
8185 },
8186 ],
8187 ..Default::default()
8188 },
8189 autoclose_before: "})]".to_string(),
8190 ..Default::default()
8191 },
8192 Some(tree_sitter_rust::LANGUAGE.into()),
8193 );
8194 let language = Arc::new(language);
8195
8196 cx.language_registry().add(language.clone());
8197 cx.update_buffer(|buffer, cx| {
8198 buffer.set_language(Some(language), cx);
8199 });
8200
8201 // Ensure that signature_help is not called when no signature help is enabled.
8202 cx.set_state(
8203 &r#"
8204 fn main() {
8205 sampleˇ
8206 }
8207 "#
8208 .unindent(),
8209 );
8210 cx.update_editor(|editor, window, cx| {
8211 editor.handle_input("(", window, cx);
8212 });
8213 cx.assert_editor_state(
8214 &"
8215 fn main() {
8216 sample(ˇ)
8217 }
8218 "
8219 .unindent(),
8220 );
8221 cx.editor(|editor, _, _| {
8222 assert!(editor.signature_help_state.task().is_none());
8223 });
8224
8225 let mocked_response = lsp::SignatureHelp {
8226 signatures: vec![lsp::SignatureInformation {
8227 label: "fn sample(param1: u8, param2: u8)".to_string(),
8228 documentation: None,
8229 parameters: Some(vec![
8230 lsp::ParameterInformation {
8231 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8232 documentation: None,
8233 },
8234 lsp::ParameterInformation {
8235 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8236 documentation: None,
8237 },
8238 ]),
8239 active_parameter: None,
8240 }],
8241 active_signature: Some(0),
8242 active_parameter: Some(0),
8243 };
8244
8245 // Ensure that signature_help is called when enabled afte edits
8246 cx.update(|_, cx| {
8247 cx.update_global::<SettingsStore, _>(|settings, cx| {
8248 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8249 settings.auto_signature_help = Some(false);
8250 settings.show_signature_help_after_edits = Some(true);
8251 });
8252 });
8253 });
8254 cx.set_state(
8255 &r#"
8256 fn main() {
8257 sampleˇ
8258 }
8259 "#
8260 .unindent(),
8261 );
8262 cx.update_editor(|editor, window, cx| {
8263 editor.handle_input("(", window, cx);
8264 });
8265 cx.assert_editor_state(
8266 &"
8267 fn main() {
8268 sample(ˇ)
8269 }
8270 "
8271 .unindent(),
8272 );
8273 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8274 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8275 .await;
8276 cx.update_editor(|editor, _, _| {
8277 let signature_help_state = editor.signature_help_state.popover().cloned();
8278 assert!(signature_help_state.is_some());
8279 let ParsedMarkdown {
8280 text, highlights, ..
8281 } = signature_help_state.unwrap().parsed_content;
8282 assert_eq!(text, "param1: u8, param2: u8");
8283 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8284 editor.signature_help_state = SignatureHelpState::default();
8285 });
8286
8287 // Ensure that signature_help is called when auto signature help override is enabled
8288 cx.update(|_, cx| {
8289 cx.update_global::<SettingsStore, _>(|settings, cx| {
8290 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8291 settings.auto_signature_help = Some(true);
8292 settings.show_signature_help_after_edits = Some(false);
8293 });
8294 });
8295 });
8296 cx.set_state(
8297 &r#"
8298 fn main() {
8299 sampleˇ
8300 }
8301 "#
8302 .unindent(),
8303 );
8304 cx.update_editor(|editor, window, cx| {
8305 editor.handle_input("(", window, cx);
8306 });
8307 cx.assert_editor_state(
8308 &"
8309 fn main() {
8310 sample(ˇ)
8311 }
8312 "
8313 .unindent(),
8314 );
8315 handle_signature_help_request(&mut cx, mocked_response).await;
8316 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8317 .await;
8318 cx.editor(|editor, _, _| {
8319 let signature_help_state = editor.signature_help_state.popover().cloned();
8320 assert!(signature_help_state.is_some());
8321 let ParsedMarkdown {
8322 text, highlights, ..
8323 } = signature_help_state.unwrap().parsed_content;
8324 assert_eq!(text, "param1: u8, param2: u8");
8325 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8326 });
8327}
8328
8329#[gpui::test]
8330async fn test_signature_help(cx: &mut gpui::TestAppContext) {
8331 init_test(cx, |_| {});
8332 cx.update(|cx| {
8333 cx.update_global::<SettingsStore, _>(|settings, cx| {
8334 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8335 settings.auto_signature_help = Some(true);
8336 });
8337 });
8338 });
8339
8340 let mut cx = EditorLspTestContext::new_rust(
8341 lsp::ServerCapabilities {
8342 signature_help_provider: Some(lsp::SignatureHelpOptions {
8343 ..Default::default()
8344 }),
8345 ..Default::default()
8346 },
8347 cx,
8348 )
8349 .await;
8350
8351 // A test that directly calls `show_signature_help`
8352 cx.update_editor(|editor, window, cx| {
8353 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8354 });
8355
8356 let mocked_response = lsp::SignatureHelp {
8357 signatures: vec![lsp::SignatureInformation {
8358 label: "fn sample(param1: u8, param2: u8)".to_string(),
8359 documentation: None,
8360 parameters: Some(vec![
8361 lsp::ParameterInformation {
8362 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8363 documentation: None,
8364 },
8365 lsp::ParameterInformation {
8366 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8367 documentation: None,
8368 },
8369 ]),
8370 active_parameter: None,
8371 }],
8372 active_signature: Some(0),
8373 active_parameter: Some(0),
8374 };
8375 handle_signature_help_request(&mut cx, mocked_response).await;
8376
8377 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8378 .await;
8379
8380 cx.editor(|editor, _, _| {
8381 let signature_help_state = editor.signature_help_state.popover().cloned();
8382 assert!(signature_help_state.is_some());
8383 let ParsedMarkdown {
8384 text, highlights, ..
8385 } = signature_help_state.unwrap().parsed_content;
8386 assert_eq!(text, "param1: u8, param2: u8");
8387 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8388 });
8389
8390 // When exiting outside from inside the brackets, `signature_help` is closed.
8391 cx.set_state(indoc! {"
8392 fn main() {
8393 sample(ˇ);
8394 }
8395
8396 fn sample(param1: u8, param2: u8) {}
8397 "});
8398
8399 cx.update_editor(|editor, window, cx| {
8400 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
8401 });
8402
8403 let mocked_response = lsp::SignatureHelp {
8404 signatures: Vec::new(),
8405 active_signature: None,
8406 active_parameter: None,
8407 };
8408 handle_signature_help_request(&mut cx, mocked_response).await;
8409
8410 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
8411 .await;
8412
8413 cx.editor(|editor, _, _| {
8414 assert!(!editor.signature_help_state.is_shown());
8415 });
8416
8417 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
8418 cx.set_state(indoc! {"
8419 fn main() {
8420 sample(ˇ);
8421 }
8422
8423 fn sample(param1: u8, param2: u8) {}
8424 "});
8425
8426 let mocked_response = lsp::SignatureHelp {
8427 signatures: vec![lsp::SignatureInformation {
8428 label: "fn sample(param1: u8, param2: u8)".to_string(),
8429 documentation: None,
8430 parameters: Some(vec![
8431 lsp::ParameterInformation {
8432 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8433 documentation: None,
8434 },
8435 lsp::ParameterInformation {
8436 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8437 documentation: None,
8438 },
8439 ]),
8440 active_parameter: None,
8441 }],
8442 active_signature: Some(0),
8443 active_parameter: Some(0),
8444 };
8445 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8446 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8447 .await;
8448 cx.editor(|editor, _, _| {
8449 assert!(editor.signature_help_state.is_shown());
8450 });
8451
8452 // Restore the popover with more parameter input
8453 cx.set_state(indoc! {"
8454 fn main() {
8455 sample(param1, param2ˇ);
8456 }
8457
8458 fn sample(param1: u8, param2: u8) {}
8459 "});
8460
8461 let mocked_response = lsp::SignatureHelp {
8462 signatures: vec![lsp::SignatureInformation {
8463 label: "fn sample(param1: u8, param2: u8)".to_string(),
8464 documentation: None,
8465 parameters: Some(vec![
8466 lsp::ParameterInformation {
8467 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8468 documentation: None,
8469 },
8470 lsp::ParameterInformation {
8471 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8472 documentation: None,
8473 },
8474 ]),
8475 active_parameter: None,
8476 }],
8477 active_signature: Some(0),
8478 active_parameter: Some(1),
8479 };
8480 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8481 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8482 .await;
8483
8484 // When selecting a range, the popover is gone.
8485 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
8486 cx.update_editor(|editor, window, cx| {
8487 editor.change_selections(None, window, cx, |s| {
8488 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
8489 })
8490 });
8491 cx.assert_editor_state(indoc! {"
8492 fn main() {
8493 sample(param1, «ˇparam2»);
8494 }
8495
8496 fn sample(param1: u8, param2: u8) {}
8497 "});
8498 cx.editor(|editor, _, _| {
8499 assert!(!editor.signature_help_state.is_shown());
8500 });
8501
8502 // When unselecting again, the popover is back if within the brackets.
8503 cx.update_editor(|editor, window, cx| {
8504 editor.change_selections(None, window, cx, |s| {
8505 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8506 })
8507 });
8508 cx.assert_editor_state(indoc! {"
8509 fn main() {
8510 sample(param1, ˇparam2);
8511 }
8512
8513 fn sample(param1: u8, param2: u8) {}
8514 "});
8515 handle_signature_help_request(&mut cx, mocked_response).await;
8516 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8517 .await;
8518 cx.editor(|editor, _, _| {
8519 assert!(editor.signature_help_state.is_shown());
8520 });
8521
8522 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
8523 cx.update_editor(|editor, window, cx| {
8524 editor.change_selections(None, window, cx, |s| {
8525 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
8526 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8527 })
8528 });
8529 cx.assert_editor_state(indoc! {"
8530 fn main() {
8531 sample(param1, ˇparam2);
8532 }
8533
8534 fn sample(param1: u8, param2: u8) {}
8535 "});
8536
8537 let mocked_response = lsp::SignatureHelp {
8538 signatures: vec![lsp::SignatureInformation {
8539 label: "fn sample(param1: u8, param2: u8)".to_string(),
8540 documentation: None,
8541 parameters: Some(vec![
8542 lsp::ParameterInformation {
8543 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8544 documentation: None,
8545 },
8546 lsp::ParameterInformation {
8547 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8548 documentation: None,
8549 },
8550 ]),
8551 active_parameter: None,
8552 }],
8553 active_signature: Some(0),
8554 active_parameter: Some(1),
8555 };
8556 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8557 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8558 .await;
8559 cx.update_editor(|editor, _, cx| {
8560 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
8561 });
8562 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
8563 .await;
8564 cx.update_editor(|editor, window, cx| {
8565 editor.change_selections(None, window, cx, |s| {
8566 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
8567 })
8568 });
8569 cx.assert_editor_state(indoc! {"
8570 fn main() {
8571 sample(param1, «ˇparam2»);
8572 }
8573
8574 fn sample(param1: u8, param2: u8) {}
8575 "});
8576 cx.update_editor(|editor, window, cx| {
8577 editor.change_selections(None, window, cx, |s| {
8578 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8579 })
8580 });
8581 cx.assert_editor_state(indoc! {"
8582 fn main() {
8583 sample(param1, ˇparam2);
8584 }
8585
8586 fn sample(param1: u8, param2: u8) {}
8587 "});
8588 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
8589 .await;
8590}
8591
8592#[gpui::test]
8593async fn test_completion(cx: &mut gpui::TestAppContext) {
8594 init_test(cx, |_| {});
8595
8596 let mut cx = EditorLspTestContext::new_rust(
8597 lsp::ServerCapabilities {
8598 completion_provider: Some(lsp::CompletionOptions {
8599 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
8600 resolve_provider: Some(true),
8601 ..Default::default()
8602 }),
8603 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
8604 ..Default::default()
8605 },
8606 cx,
8607 )
8608 .await;
8609 let counter = Arc::new(AtomicUsize::new(0));
8610
8611 cx.set_state(indoc! {"
8612 oneˇ
8613 two
8614 three
8615 "});
8616 cx.simulate_keystroke(".");
8617 handle_completion_request(
8618 &mut cx,
8619 indoc! {"
8620 one.|<>
8621 two
8622 three
8623 "},
8624 vec!["first_completion", "second_completion"],
8625 counter.clone(),
8626 )
8627 .await;
8628 cx.condition(|editor, _| editor.context_menu_visible())
8629 .await;
8630 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
8631
8632 let _handler = handle_signature_help_request(
8633 &mut cx,
8634 lsp::SignatureHelp {
8635 signatures: vec![lsp::SignatureInformation {
8636 label: "test signature".to_string(),
8637 documentation: None,
8638 parameters: Some(vec![lsp::ParameterInformation {
8639 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
8640 documentation: None,
8641 }]),
8642 active_parameter: None,
8643 }],
8644 active_signature: None,
8645 active_parameter: None,
8646 },
8647 );
8648 cx.update_editor(|editor, window, cx| {
8649 assert!(
8650 !editor.signature_help_state.is_shown(),
8651 "No signature help was called for"
8652 );
8653 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8654 });
8655 cx.run_until_parked();
8656 cx.update_editor(|editor, _, _| {
8657 assert!(
8658 !editor.signature_help_state.is_shown(),
8659 "No signature help should be shown when completions menu is open"
8660 );
8661 });
8662
8663 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8664 editor.context_menu_next(&Default::default(), window, cx);
8665 editor
8666 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8667 .unwrap()
8668 });
8669 cx.assert_editor_state(indoc! {"
8670 one.second_completionˇ
8671 two
8672 three
8673 "});
8674
8675 handle_resolve_completion_request(
8676 &mut cx,
8677 Some(vec![
8678 (
8679 //This overlaps with the primary completion edit which is
8680 //misbehavior from the LSP spec, test that we filter it out
8681 indoc! {"
8682 one.second_ˇcompletion
8683 two
8684 threeˇ
8685 "},
8686 "overlapping additional edit",
8687 ),
8688 (
8689 indoc! {"
8690 one.second_completion
8691 two
8692 threeˇ
8693 "},
8694 "\nadditional edit",
8695 ),
8696 ]),
8697 )
8698 .await;
8699 apply_additional_edits.await.unwrap();
8700 cx.assert_editor_state(indoc! {"
8701 one.second_completionˇ
8702 two
8703 three
8704 additional edit
8705 "});
8706
8707 cx.set_state(indoc! {"
8708 one.second_completion
8709 twoˇ
8710 threeˇ
8711 additional edit
8712 "});
8713 cx.simulate_keystroke(" ");
8714 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8715 cx.simulate_keystroke("s");
8716 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8717
8718 cx.assert_editor_state(indoc! {"
8719 one.second_completion
8720 two sˇ
8721 three sˇ
8722 additional edit
8723 "});
8724 handle_completion_request(
8725 &mut cx,
8726 indoc! {"
8727 one.second_completion
8728 two s
8729 three <s|>
8730 additional edit
8731 "},
8732 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
8733 counter.clone(),
8734 )
8735 .await;
8736 cx.condition(|editor, _| editor.context_menu_visible())
8737 .await;
8738 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
8739
8740 cx.simulate_keystroke("i");
8741
8742 handle_completion_request(
8743 &mut cx,
8744 indoc! {"
8745 one.second_completion
8746 two si
8747 three <si|>
8748 additional edit
8749 "},
8750 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
8751 counter.clone(),
8752 )
8753 .await;
8754 cx.condition(|editor, _| editor.context_menu_visible())
8755 .await;
8756 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
8757
8758 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8759 editor
8760 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8761 .unwrap()
8762 });
8763 cx.assert_editor_state(indoc! {"
8764 one.second_completion
8765 two sixth_completionˇ
8766 three sixth_completionˇ
8767 additional edit
8768 "});
8769
8770 apply_additional_edits.await.unwrap();
8771
8772 update_test_language_settings(&mut cx, |settings| {
8773 settings.defaults.show_completions_on_input = Some(false);
8774 });
8775 cx.set_state("editorˇ");
8776 cx.simulate_keystroke(".");
8777 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8778 cx.simulate_keystroke("c");
8779 cx.simulate_keystroke("l");
8780 cx.simulate_keystroke("o");
8781 cx.assert_editor_state("editor.cloˇ");
8782 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8783 cx.update_editor(|editor, window, cx| {
8784 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
8785 });
8786 handle_completion_request(
8787 &mut cx,
8788 "editor.<clo|>",
8789 vec!["close", "clobber"],
8790 counter.clone(),
8791 )
8792 .await;
8793 cx.condition(|editor, _| editor.context_menu_visible())
8794 .await;
8795 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
8796
8797 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8798 editor
8799 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8800 .unwrap()
8801 });
8802 cx.assert_editor_state("editor.closeˇ");
8803 handle_resolve_completion_request(&mut cx, None).await;
8804 apply_additional_edits.await.unwrap();
8805}
8806
8807#[gpui::test]
8808async fn test_multiline_completion(cx: &mut gpui::TestAppContext) {
8809 init_test(cx, |_| {});
8810
8811 let fs = FakeFs::new(cx.executor());
8812 fs.insert_tree(
8813 path!("/a"),
8814 json!({
8815 "main.ts": "a",
8816 }),
8817 )
8818 .await;
8819
8820 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8821 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8822 let typescript_language = Arc::new(Language::new(
8823 LanguageConfig {
8824 name: "TypeScript".into(),
8825 matcher: LanguageMatcher {
8826 path_suffixes: vec!["ts".to_string()],
8827 ..LanguageMatcher::default()
8828 },
8829 line_comments: vec!["// ".into()],
8830 ..LanguageConfig::default()
8831 },
8832 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8833 ));
8834 language_registry.add(typescript_language.clone());
8835 let mut fake_servers = language_registry.register_fake_lsp(
8836 "TypeScript",
8837 FakeLspAdapter {
8838 capabilities: lsp::ServerCapabilities {
8839 completion_provider: Some(lsp::CompletionOptions {
8840 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
8841 ..lsp::CompletionOptions::default()
8842 }),
8843 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
8844 ..lsp::ServerCapabilities::default()
8845 },
8846 // Emulate vtsls label generation
8847 label_for_completion: Some(Box::new(|item, _| {
8848 let text = if let Some(description) = item
8849 .label_details
8850 .as_ref()
8851 .and_then(|label_details| label_details.description.as_ref())
8852 {
8853 format!("{} {}", item.label, description)
8854 } else if let Some(detail) = &item.detail {
8855 format!("{} {}", item.label, detail)
8856 } else {
8857 item.label.clone()
8858 };
8859 let len = text.len();
8860 Some(language::CodeLabel {
8861 text,
8862 runs: Vec::new(),
8863 filter_range: 0..len,
8864 })
8865 })),
8866 ..FakeLspAdapter::default()
8867 },
8868 );
8869 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8870 let cx = &mut VisualTestContext::from_window(*workspace, cx);
8871 let worktree_id = workspace
8872 .update(cx, |workspace, _window, cx| {
8873 workspace.project().update(cx, |project, cx| {
8874 project.worktrees(cx).next().unwrap().read(cx).id()
8875 })
8876 })
8877 .unwrap();
8878 let _buffer = project
8879 .update(cx, |project, cx| {
8880 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
8881 })
8882 .await
8883 .unwrap();
8884 let editor = workspace
8885 .update(cx, |workspace, window, cx| {
8886 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
8887 })
8888 .unwrap()
8889 .await
8890 .unwrap()
8891 .downcast::<Editor>()
8892 .unwrap();
8893 let fake_server = fake_servers.next().await.unwrap();
8894
8895 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
8896 let multiline_label_2 = "a\nb\nc\n";
8897 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
8898 let multiline_description = "d\ne\nf\n";
8899 let multiline_detail_2 = "g\nh\ni\n";
8900
8901 let mut completion_handle =
8902 fake_server.handle_request::<lsp::request::Completion, _, _>(move |params, _| async move {
8903 Ok(Some(lsp::CompletionResponse::Array(vec![
8904 lsp::CompletionItem {
8905 label: multiline_label.to_string(),
8906 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8907 range: lsp::Range {
8908 start: lsp::Position {
8909 line: params.text_document_position.position.line,
8910 character: params.text_document_position.position.character,
8911 },
8912 end: lsp::Position {
8913 line: params.text_document_position.position.line,
8914 character: params.text_document_position.position.character,
8915 },
8916 },
8917 new_text: "new_text_1".to_string(),
8918 })),
8919 ..lsp::CompletionItem::default()
8920 },
8921 lsp::CompletionItem {
8922 label: "single line label 1".to_string(),
8923 detail: Some(multiline_detail.to_string()),
8924 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8925 range: lsp::Range {
8926 start: lsp::Position {
8927 line: params.text_document_position.position.line,
8928 character: params.text_document_position.position.character,
8929 },
8930 end: lsp::Position {
8931 line: params.text_document_position.position.line,
8932 character: params.text_document_position.position.character,
8933 },
8934 },
8935 new_text: "new_text_2".to_string(),
8936 })),
8937 ..lsp::CompletionItem::default()
8938 },
8939 lsp::CompletionItem {
8940 label: "single line label 2".to_string(),
8941 label_details: Some(lsp::CompletionItemLabelDetails {
8942 description: Some(multiline_description.to_string()),
8943 detail: None,
8944 }),
8945 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8946 range: lsp::Range {
8947 start: lsp::Position {
8948 line: params.text_document_position.position.line,
8949 character: params.text_document_position.position.character,
8950 },
8951 end: lsp::Position {
8952 line: params.text_document_position.position.line,
8953 character: params.text_document_position.position.character,
8954 },
8955 },
8956 new_text: "new_text_2".to_string(),
8957 })),
8958 ..lsp::CompletionItem::default()
8959 },
8960 lsp::CompletionItem {
8961 label: multiline_label_2.to_string(),
8962 detail: Some(multiline_detail_2.to_string()),
8963 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8964 range: lsp::Range {
8965 start: lsp::Position {
8966 line: params.text_document_position.position.line,
8967 character: params.text_document_position.position.character,
8968 },
8969 end: lsp::Position {
8970 line: params.text_document_position.position.line,
8971 character: params.text_document_position.position.character,
8972 },
8973 },
8974 new_text: "new_text_3".to_string(),
8975 })),
8976 ..lsp::CompletionItem::default()
8977 },
8978 lsp::CompletionItem {
8979 label: "Label with many spaces and \t but without newlines".to_string(),
8980 detail: Some(
8981 "Details with many spaces and \t but without newlines".to_string(),
8982 ),
8983 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8984 range: lsp::Range {
8985 start: lsp::Position {
8986 line: params.text_document_position.position.line,
8987 character: params.text_document_position.position.character,
8988 },
8989 end: lsp::Position {
8990 line: params.text_document_position.position.line,
8991 character: params.text_document_position.position.character,
8992 },
8993 },
8994 new_text: "new_text_4".to_string(),
8995 })),
8996 ..lsp::CompletionItem::default()
8997 },
8998 ])))
8999 });
9000
9001 editor.update_in(cx, |editor, window, cx| {
9002 cx.focus_self(window);
9003 editor.move_to_end(&MoveToEnd, window, cx);
9004 editor.handle_input(".", window, cx);
9005 });
9006 cx.run_until_parked();
9007 completion_handle.next().await.unwrap();
9008
9009 editor.update(cx, |editor, _| {
9010 assert!(editor.context_menu_visible());
9011 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9012 {
9013 let completion_labels = menu
9014 .completions
9015 .borrow()
9016 .iter()
9017 .map(|c| c.label.text.clone())
9018 .collect::<Vec<_>>();
9019 assert_eq!(
9020 completion_labels,
9021 &[
9022 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
9023 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
9024 "single line label 2 d e f ",
9025 "a b c g h i ",
9026 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
9027 ],
9028 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
9029 );
9030
9031 for completion in menu
9032 .completions
9033 .borrow()
9034 .iter() {
9035 assert_eq!(
9036 completion.label.filter_range,
9037 0..completion.label.text.len(),
9038 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
9039 );
9040 }
9041
9042 } else {
9043 panic!("expected completion menu to be open");
9044 }
9045 });
9046}
9047
9048#[gpui::test]
9049async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
9050 init_test(cx, |_| {});
9051 let mut cx = EditorLspTestContext::new_rust(
9052 lsp::ServerCapabilities {
9053 completion_provider: Some(lsp::CompletionOptions {
9054 trigger_characters: Some(vec![".".to_string()]),
9055 ..Default::default()
9056 }),
9057 ..Default::default()
9058 },
9059 cx,
9060 )
9061 .await;
9062 cx.lsp
9063 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9064 Ok(Some(lsp::CompletionResponse::Array(vec![
9065 lsp::CompletionItem {
9066 label: "first".into(),
9067 ..Default::default()
9068 },
9069 lsp::CompletionItem {
9070 label: "last".into(),
9071 ..Default::default()
9072 },
9073 ])))
9074 });
9075 cx.set_state("variableˇ");
9076 cx.simulate_keystroke(".");
9077 cx.executor().run_until_parked();
9078
9079 cx.update_editor(|editor, _, _| {
9080 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9081 {
9082 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
9083 } else {
9084 panic!("expected completion menu to be open");
9085 }
9086 });
9087
9088 cx.update_editor(|editor, window, cx| {
9089 editor.move_page_down(&MovePageDown::default(), window, cx);
9090 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9091 {
9092 assert!(
9093 menu.selected_item == 1,
9094 "expected PageDown to select the last item from the context menu"
9095 );
9096 } else {
9097 panic!("expected completion menu to stay open after PageDown");
9098 }
9099 });
9100
9101 cx.update_editor(|editor, window, cx| {
9102 editor.move_page_up(&MovePageUp::default(), window, cx);
9103 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9104 {
9105 assert!(
9106 menu.selected_item == 0,
9107 "expected PageUp to select the first item from the context menu"
9108 );
9109 } else {
9110 panic!("expected completion menu to stay open after PageUp");
9111 }
9112 });
9113}
9114
9115#[gpui::test]
9116async fn test_completion_sort(cx: &mut gpui::TestAppContext) {
9117 init_test(cx, |_| {});
9118 let mut cx = EditorLspTestContext::new_rust(
9119 lsp::ServerCapabilities {
9120 completion_provider: Some(lsp::CompletionOptions {
9121 trigger_characters: Some(vec![".".to_string()]),
9122 ..Default::default()
9123 }),
9124 ..Default::default()
9125 },
9126 cx,
9127 )
9128 .await;
9129 cx.lsp
9130 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9131 Ok(Some(lsp::CompletionResponse::Array(vec![
9132 lsp::CompletionItem {
9133 label: "Range".into(),
9134 sort_text: Some("a".into()),
9135 ..Default::default()
9136 },
9137 lsp::CompletionItem {
9138 label: "r".into(),
9139 sort_text: Some("b".into()),
9140 ..Default::default()
9141 },
9142 lsp::CompletionItem {
9143 label: "ret".into(),
9144 sort_text: Some("c".into()),
9145 ..Default::default()
9146 },
9147 lsp::CompletionItem {
9148 label: "return".into(),
9149 sort_text: Some("d".into()),
9150 ..Default::default()
9151 },
9152 lsp::CompletionItem {
9153 label: "slice".into(),
9154 sort_text: Some("d".into()),
9155 ..Default::default()
9156 },
9157 ])))
9158 });
9159 cx.set_state("rˇ");
9160 cx.executor().run_until_parked();
9161 cx.update_editor(|editor, window, cx| {
9162 editor.show_completions(
9163 &ShowCompletions {
9164 trigger: Some("r".into()),
9165 },
9166 window,
9167 cx,
9168 );
9169 });
9170 cx.executor().run_until_parked();
9171
9172 cx.update_editor(|editor, _, _| {
9173 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9174 {
9175 assert_eq!(
9176 completion_menu_entries(&menu),
9177 &["r", "ret", "Range", "return"]
9178 );
9179 } else {
9180 panic!("expected completion menu to be open");
9181 }
9182 });
9183}
9184
9185#[gpui::test]
9186async fn test_no_duplicated_completion_requests(cx: &mut gpui::TestAppContext) {
9187 init_test(cx, |_| {});
9188
9189 let mut cx = EditorLspTestContext::new_rust(
9190 lsp::ServerCapabilities {
9191 completion_provider: Some(lsp::CompletionOptions {
9192 trigger_characters: Some(vec![".".to_string()]),
9193 resolve_provider: Some(true),
9194 ..Default::default()
9195 }),
9196 ..Default::default()
9197 },
9198 cx,
9199 )
9200 .await;
9201
9202 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
9203 cx.simulate_keystroke(".");
9204 let completion_item = lsp::CompletionItem {
9205 label: "Some".into(),
9206 kind: Some(lsp::CompletionItemKind::SNIPPET),
9207 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
9208 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
9209 kind: lsp::MarkupKind::Markdown,
9210 value: "```rust\nSome(2)\n```".to_string(),
9211 })),
9212 deprecated: Some(false),
9213 sort_text: Some("Some".to_string()),
9214 filter_text: Some("Some".to_string()),
9215 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
9216 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9217 range: lsp::Range {
9218 start: lsp::Position {
9219 line: 0,
9220 character: 22,
9221 },
9222 end: lsp::Position {
9223 line: 0,
9224 character: 22,
9225 },
9226 },
9227 new_text: "Some(2)".to_string(),
9228 })),
9229 additional_text_edits: Some(vec![lsp::TextEdit {
9230 range: lsp::Range {
9231 start: lsp::Position {
9232 line: 0,
9233 character: 20,
9234 },
9235 end: lsp::Position {
9236 line: 0,
9237 character: 22,
9238 },
9239 },
9240 new_text: "".to_string(),
9241 }]),
9242 ..Default::default()
9243 };
9244
9245 let closure_completion_item = completion_item.clone();
9246 let counter = Arc::new(AtomicUsize::new(0));
9247 let counter_clone = counter.clone();
9248 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
9249 let task_completion_item = closure_completion_item.clone();
9250 counter_clone.fetch_add(1, atomic::Ordering::Release);
9251 async move {
9252 Ok(Some(lsp::CompletionResponse::Array(vec![
9253 task_completion_item,
9254 ])))
9255 }
9256 });
9257
9258 cx.condition(|editor, _| editor.context_menu_visible())
9259 .await;
9260 cx.assert_editor_state(indoc! {"fn main() { let a = 2.ˇ; }"});
9261 assert!(request.next().await.is_some());
9262 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9263
9264 cx.simulate_keystroke("S");
9265 cx.simulate_keystroke("o");
9266 cx.simulate_keystroke("m");
9267 cx.condition(|editor, _| editor.context_menu_visible())
9268 .await;
9269 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Somˇ; }"});
9270 assert!(request.next().await.is_some());
9271 assert!(request.next().await.is_some());
9272 assert!(request.next().await.is_some());
9273 request.close();
9274 assert!(request.next().await.is_none());
9275 assert_eq!(
9276 counter.load(atomic::Ordering::Acquire),
9277 4,
9278 "With the completions menu open, only one LSP request should happen per input"
9279 );
9280}
9281
9282#[gpui::test]
9283async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
9284 init_test(cx, |_| {});
9285 let mut cx = EditorTestContext::new(cx).await;
9286 let language = Arc::new(Language::new(
9287 LanguageConfig {
9288 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
9289 ..Default::default()
9290 },
9291 Some(tree_sitter_rust::LANGUAGE.into()),
9292 ));
9293 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
9294
9295 // If multiple selections intersect a line, the line is only toggled once.
9296 cx.set_state(indoc! {"
9297 fn a() {
9298 «//b();
9299 ˇ»// «c();
9300 //ˇ» d();
9301 }
9302 "});
9303
9304 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9305
9306 cx.assert_editor_state(indoc! {"
9307 fn a() {
9308 «b();
9309 c();
9310 ˇ» d();
9311 }
9312 "});
9313
9314 // The comment prefix is inserted at the same column for every line in a
9315 // selection.
9316 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9317
9318 cx.assert_editor_state(indoc! {"
9319 fn a() {
9320 // «b();
9321 // c();
9322 ˇ»// d();
9323 }
9324 "});
9325
9326 // If a selection ends at the beginning of a line, that line is not toggled.
9327 cx.set_selections_state(indoc! {"
9328 fn a() {
9329 // b();
9330 «// c();
9331 ˇ» // d();
9332 }
9333 "});
9334
9335 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9336
9337 cx.assert_editor_state(indoc! {"
9338 fn a() {
9339 // b();
9340 «c();
9341 ˇ» // d();
9342 }
9343 "});
9344
9345 // If a selection span a single line and is empty, the line is toggled.
9346 cx.set_state(indoc! {"
9347 fn a() {
9348 a();
9349 b();
9350 ˇ
9351 }
9352 "});
9353
9354 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9355
9356 cx.assert_editor_state(indoc! {"
9357 fn a() {
9358 a();
9359 b();
9360 //•ˇ
9361 }
9362 "});
9363
9364 // If a selection span multiple lines, empty lines are not toggled.
9365 cx.set_state(indoc! {"
9366 fn a() {
9367 «a();
9368
9369 c();ˇ»
9370 }
9371 "});
9372
9373 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9374
9375 cx.assert_editor_state(indoc! {"
9376 fn a() {
9377 // «a();
9378
9379 // c();ˇ»
9380 }
9381 "});
9382
9383 // If a selection includes multiple comment prefixes, all lines are uncommented.
9384 cx.set_state(indoc! {"
9385 fn a() {
9386 «// a();
9387 /// b();
9388 //! c();ˇ»
9389 }
9390 "});
9391
9392 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9393
9394 cx.assert_editor_state(indoc! {"
9395 fn a() {
9396 «a();
9397 b();
9398 c();ˇ»
9399 }
9400 "});
9401}
9402
9403#[gpui::test]
9404async fn test_toggle_comment_ignore_indent(cx: &mut gpui::TestAppContext) {
9405 init_test(cx, |_| {});
9406 let mut cx = EditorTestContext::new(cx).await;
9407 let language = Arc::new(Language::new(
9408 LanguageConfig {
9409 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
9410 ..Default::default()
9411 },
9412 Some(tree_sitter_rust::LANGUAGE.into()),
9413 ));
9414 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
9415
9416 let toggle_comments = &ToggleComments {
9417 advance_downwards: false,
9418 ignore_indent: true,
9419 };
9420
9421 // If multiple selections intersect a line, the line is only toggled once.
9422 cx.set_state(indoc! {"
9423 fn a() {
9424 // «b();
9425 // c();
9426 // ˇ» d();
9427 }
9428 "});
9429
9430 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9431
9432 cx.assert_editor_state(indoc! {"
9433 fn a() {
9434 «b();
9435 c();
9436 ˇ» d();
9437 }
9438 "});
9439
9440 // The comment prefix is inserted at the beginning of each line
9441 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9442
9443 cx.assert_editor_state(indoc! {"
9444 fn a() {
9445 // «b();
9446 // c();
9447 // ˇ» d();
9448 }
9449 "});
9450
9451 // If a selection ends at the beginning of a line, that line is not toggled.
9452 cx.set_selections_state(indoc! {"
9453 fn a() {
9454 // b();
9455 // «c();
9456 ˇ»// d();
9457 }
9458 "});
9459
9460 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9461
9462 cx.assert_editor_state(indoc! {"
9463 fn a() {
9464 // b();
9465 «c();
9466 ˇ»// d();
9467 }
9468 "});
9469
9470 // If a selection span a single line and is empty, the line is toggled.
9471 cx.set_state(indoc! {"
9472 fn a() {
9473 a();
9474 b();
9475 ˇ
9476 }
9477 "});
9478
9479 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9480
9481 cx.assert_editor_state(indoc! {"
9482 fn a() {
9483 a();
9484 b();
9485 //ˇ
9486 }
9487 "});
9488
9489 // If a selection span multiple lines, empty lines are not toggled.
9490 cx.set_state(indoc! {"
9491 fn a() {
9492 «a();
9493
9494 c();ˇ»
9495 }
9496 "});
9497
9498 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9499
9500 cx.assert_editor_state(indoc! {"
9501 fn a() {
9502 // «a();
9503
9504 // c();ˇ»
9505 }
9506 "});
9507
9508 // If a selection includes multiple comment prefixes, all lines are uncommented.
9509 cx.set_state(indoc! {"
9510 fn a() {
9511 // «a();
9512 /// b();
9513 //! c();ˇ»
9514 }
9515 "});
9516
9517 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9518
9519 cx.assert_editor_state(indoc! {"
9520 fn a() {
9521 «a();
9522 b();
9523 c();ˇ»
9524 }
9525 "});
9526}
9527
9528#[gpui::test]
9529async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
9530 init_test(cx, |_| {});
9531
9532 let language = Arc::new(Language::new(
9533 LanguageConfig {
9534 line_comments: vec!["// ".into()],
9535 ..Default::default()
9536 },
9537 Some(tree_sitter_rust::LANGUAGE.into()),
9538 ));
9539
9540 let mut cx = EditorTestContext::new(cx).await;
9541
9542 cx.language_registry().add(language.clone());
9543 cx.update_buffer(|buffer, cx| {
9544 buffer.set_language(Some(language), cx);
9545 });
9546
9547 let toggle_comments = &ToggleComments {
9548 advance_downwards: true,
9549 ignore_indent: false,
9550 };
9551
9552 // Single cursor on one line -> advance
9553 // Cursor moves horizontally 3 characters as well on non-blank line
9554 cx.set_state(indoc!(
9555 "fn a() {
9556 ˇdog();
9557 cat();
9558 }"
9559 ));
9560 cx.update_editor(|editor, window, cx| {
9561 editor.toggle_comments(toggle_comments, window, cx);
9562 });
9563 cx.assert_editor_state(indoc!(
9564 "fn a() {
9565 // dog();
9566 catˇ();
9567 }"
9568 ));
9569
9570 // Single selection on one line -> don't advance
9571 cx.set_state(indoc!(
9572 "fn a() {
9573 «dog()ˇ»;
9574 cat();
9575 }"
9576 ));
9577 cx.update_editor(|editor, window, cx| {
9578 editor.toggle_comments(toggle_comments, window, cx);
9579 });
9580 cx.assert_editor_state(indoc!(
9581 "fn a() {
9582 // «dog()ˇ»;
9583 cat();
9584 }"
9585 ));
9586
9587 // Multiple cursors on one line -> advance
9588 cx.set_state(indoc!(
9589 "fn a() {
9590 ˇdˇog();
9591 cat();
9592 }"
9593 ));
9594 cx.update_editor(|editor, window, cx| {
9595 editor.toggle_comments(toggle_comments, window, cx);
9596 });
9597 cx.assert_editor_state(indoc!(
9598 "fn a() {
9599 // dog();
9600 catˇ(ˇ);
9601 }"
9602 ));
9603
9604 // Multiple cursors on one line, with selection -> don't advance
9605 cx.set_state(indoc!(
9606 "fn a() {
9607 ˇdˇog«()ˇ»;
9608 cat();
9609 }"
9610 ));
9611 cx.update_editor(|editor, window, cx| {
9612 editor.toggle_comments(toggle_comments, window, cx);
9613 });
9614 cx.assert_editor_state(indoc!(
9615 "fn a() {
9616 // ˇdˇog«()ˇ»;
9617 cat();
9618 }"
9619 ));
9620
9621 // Single cursor on one line -> advance
9622 // Cursor moves to column 0 on blank line
9623 cx.set_state(indoc!(
9624 "fn a() {
9625 ˇdog();
9626
9627 cat();
9628 }"
9629 ));
9630 cx.update_editor(|editor, window, cx| {
9631 editor.toggle_comments(toggle_comments, window, cx);
9632 });
9633 cx.assert_editor_state(indoc!(
9634 "fn a() {
9635 // dog();
9636 ˇ
9637 cat();
9638 }"
9639 ));
9640
9641 // Single cursor on one line -> advance
9642 // Cursor starts and ends at column 0
9643 cx.set_state(indoc!(
9644 "fn a() {
9645 ˇ dog();
9646 cat();
9647 }"
9648 ));
9649 cx.update_editor(|editor, window, cx| {
9650 editor.toggle_comments(toggle_comments, window, cx);
9651 });
9652 cx.assert_editor_state(indoc!(
9653 "fn a() {
9654 // dog();
9655 ˇ cat();
9656 }"
9657 ));
9658}
9659
9660#[gpui::test]
9661async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
9662 init_test(cx, |_| {});
9663
9664 let mut cx = EditorTestContext::new(cx).await;
9665
9666 let html_language = Arc::new(
9667 Language::new(
9668 LanguageConfig {
9669 name: "HTML".into(),
9670 block_comment: Some(("<!-- ".into(), " -->".into())),
9671 ..Default::default()
9672 },
9673 Some(tree_sitter_html::language()),
9674 )
9675 .with_injection_query(
9676 r#"
9677 (script_element
9678 (raw_text) @injection.content
9679 (#set! injection.language "javascript"))
9680 "#,
9681 )
9682 .unwrap(),
9683 );
9684
9685 let javascript_language = Arc::new(Language::new(
9686 LanguageConfig {
9687 name: "JavaScript".into(),
9688 line_comments: vec!["// ".into()],
9689 ..Default::default()
9690 },
9691 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9692 ));
9693
9694 cx.language_registry().add(html_language.clone());
9695 cx.language_registry().add(javascript_language.clone());
9696 cx.update_buffer(|buffer, cx| {
9697 buffer.set_language(Some(html_language), cx);
9698 });
9699
9700 // Toggle comments for empty selections
9701 cx.set_state(
9702 &r#"
9703 <p>A</p>ˇ
9704 <p>B</p>ˇ
9705 <p>C</p>ˇ
9706 "#
9707 .unindent(),
9708 );
9709 cx.update_editor(|editor, window, cx| {
9710 editor.toggle_comments(&ToggleComments::default(), window, cx)
9711 });
9712 cx.assert_editor_state(
9713 &r#"
9714 <!-- <p>A</p>ˇ -->
9715 <!-- <p>B</p>ˇ -->
9716 <!-- <p>C</p>ˇ -->
9717 "#
9718 .unindent(),
9719 );
9720 cx.update_editor(|editor, window, cx| {
9721 editor.toggle_comments(&ToggleComments::default(), window, cx)
9722 });
9723 cx.assert_editor_state(
9724 &r#"
9725 <p>A</p>ˇ
9726 <p>B</p>ˇ
9727 <p>C</p>ˇ
9728 "#
9729 .unindent(),
9730 );
9731
9732 // Toggle comments for mixture of empty and non-empty selections, where
9733 // multiple selections occupy a given line.
9734 cx.set_state(
9735 &r#"
9736 <p>A«</p>
9737 <p>ˇ»B</p>ˇ
9738 <p>C«</p>
9739 <p>ˇ»D</p>ˇ
9740 "#
9741 .unindent(),
9742 );
9743
9744 cx.update_editor(|editor, window, cx| {
9745 editor.toggle_comments(&ToggleComments::default(), window, cx)
9746 });
9747 cx.assert_editor_state(
9748 &r#"
9749 <!-- <p>A«</p>
9750 <p>ˇ»B</p>ˇ -->
9751 <!-- <p>C«</p>
9752 <p>ˇ»D</p>ˇ -->
9753 "#
9754 .unindent(),
9755 );
9756 cx.update_editor(|editor, window, cx| {
9757 editor.toggle_comments(&ToggleComments::default(), window, cx)
9758 });
9759 cx.assert_editor_state(
9760 &r#"
9761 <p>A«</p>
9762 <p>ˇ»B</p>ˇ
9763 <p>C«</p>
9764 <p>ˇ»D</p>ˇ
9765 "#
9766 .unindent(),
9767 );
9768
9769 // Toggle comments when different languages are active for different
9770 // selections.
9771 cx.set_state(
9772 &r#"
9773 ˇ<script>
9774 ˇvar x = new Y();
9775 ˇ</script>
9776 "#
9777 .unindent(),
9778 );
9779 cx.executor().run_until_parked();
9780 cx.update_editor(|editor, window, cx| {
9781 editor.toggle_comments(&ToggleComments::default(), window, cx)
9782 });
9783 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
9784 // Uncommenting and commenting from this position brings in even more wrong artifacts.
9785 cx.assert_editor_state(
9786 &r#"
9787 <!-- ˇ<script> -->
9788 // ˇvar x = new Y();
9789 <!-- ˇ</script> -->
9790 "#
9791 .unindent(),
9792 );
9793}
9794
9795#[gpui::test]
9796fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
9797 init_test(cx, |_| {});
9798
9799 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9800 let multibuffer = cx.new(|cx| {
9801 let mut multibuffer = MultiBuffer::new(ReadWrite);
9802 multibuffer.push_excerpts(
9803 buffer.clone(),
9804 [
9805 ExcerptRange {
9806 context: Point::new(0, 0)..Point::new(0, 4),
9807 primary: None,
9808 },
9809 ExcerptRange {
9810 context: Point::new(1, 0)..Point::new(1, 4),
9811 primary: None,
9812 },
9813 ],
9814 cx,
9815 );
9816 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
9817 multibuffer
9818 });
9819
9820 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
9821 editor.update_in(cx, |editor, window, cx| {
9822 assert_eq!(editor.text(cx), "aaaa\nbbbb");
9823 editor.change_selections(None, window, cx, |s| {
9824 s.select_ranges([
9825 Point::new(0, 0)..Point::new(0, 0),
9826 Point::new(1, 0)..Point::new(1, 0),
9827 ])
9828 });
9829
9830 editor.handle_input("X", window, cx);
9831 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
9832 assert_eq!(
9833 editor.selections.ranges(cx),
9834 [
9835 Point::new(0, 1)..Point::new(0, 1),
9836 Point::new(1, 1)..Point::new(1, 1),
9837 ]
9838 );
9839
9840 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
9841 editor.change_selections(None, window, cx, |s| {
9842 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
9843 });
9844 editor.backspace(&Default::default(), window, cx);
9845 assert_eq!(editor.text(cx), "Xa\nbbb");
9846 assert_eq!(
9847 editor.selections.ranges(cx),
9848 [Point::new(1, 0)..Point::new(1, 0)]
9849 );
9850
9851 editor.change_selections(None, window, cx, |s| {
9852 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
9853 });
9854 editor.backspace(&Default::default(), window, cx);
9855 assert_eq!(editor.text(cx), "X\nbb");
9856 assert_eq!(
9857 editor.selections.ranges(cx),
9858 [Point::new(0, 1)..Point::new(0, 1)]
9859 );
9860 });
9861}
9862
9863#[gpui::test]
9864fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
9865 init_test(cx, |_| {});
9866
9867 let markers = vec![('[', ']').into(), ('(', ')').into()];
9868 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
9869 indoc! {"
9870 [aaaa
9871 (bbbb]
9872 cccc)",
9873 },
9874 markers.clone(),
9875 );
9876 let excerpt_ranges = markers.into_iter().map(|marker| {
9877 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
9878 ExcerptRange {
9879 context,
9880 primary: None,
9881 }
9882 });
9883 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
9884 let multibuffer = cx.new(|cx| {
9885 let mut multibuffer = MultiBuffer::new(ReadWrite);
9886 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
9887 multibuffer
9888 });
9889
9890 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
9891 editor.update_in(cx, |editor, window, cx| {
9892 let (expected_text, selection_ranges) = marked_text_ranges(
9893 indoc! {"
9894 aaaa
9895 bˇbbb
9896 bˇbbˇb
9897 cccc"
9898 },
9899 true,
9900 );
9901 assert_eq!(editor.text(cx), expected_text);
9902 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
9903
9904 editor.handle_input("X", window, cx);
9905
9906 let (expected_text, expected_selections) = marked_text_ranges(
9907 indoc! {"
9908 aaaa
9909 bXˇbbXb
9910 bXˇbbXˇb
9911 cccc"
9912 },
9913 false,
9914 );
9915 assert_eq!(editor.text(cx), expected_text);
9916 assert_eq!(editor.selections.ranges(cx), expected_selections);
9917
9918 editor.newline(&Newline, window, cx);
9919 let (expected_text, expected_selections) = marked_text_ranges(
9920 indoc! {"
9921 aaaa
9922 bX
9923 ˇbbX
9924 b
9925 bX
9926 ˇbbX
9927 ˇb
9928 cccc"
9929 },
9930 false,
9931 );
9932 assert_eq!(editor.text(cx), expected_text);
9933 assert_eq!(editor.selections.ranges(cx), expected_selections);
9934 });
9935}
9936
9937#[gpui::test]
9938fn test_refresh_selections(cx: &mut TestAppContext) {
9939 init_test(cx, |_| {});
9940
9941 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9942 let mut excerpt1_id = None;
9943 let multibuffer = cx.new(|cx| {
9944 let mut multibuffer = MultiBuffer::new(ReadWrite);
9945 excerpt1_id = multibuffer
9946 .push_excerpts(
9947 buffer.clone(),
9948 [
9949 ExcerptRange {
9950 context: Point::new(0, 0)..Point::new(1, 4),
9951 primary: None,
9952 },
9953 ExcerptRange {
9954 context: Point::new(1, 0)..Point::new(2, 4),
9955 primary: None,
9956 },
9957 ],
9958 cx,
9959 )
9960 .into_iter()
9961 .next();
9962 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
9963 multibuffer
9964 });
9965
9966 let editor = cx.add_window(|window, cx| {
9967 let mut editor = build_editor(multibuffer.clone(), window, cx);
9968 let snapshot = editor.snapshot(window, cx);
9969 editor.change_selections(None, window, cx, |s| {
9970 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
9971 });
9972 editor.begin_selection(
9973 Point::new(2, 1).to_display_point(&snapshot),
9974 true,
9975 1,
9976 window,
9977 cx,
9978 );
9979 assert_eq!(
9980 editor.selections.ranges(cx),
9981 [
9982 Point::new(1, 3)..Point::new(1, 3),
9983 Point::new(2, 1)..Point::new(2, 1),
9984 ]
9985 );
9986 editor
9987 });
9988
9989 // Refreshing selections is a no-op when excerpts haven't changed.
9990 _ = editor.update(cx, |editor, window, cx| {
9991 editor.change_selections(None, window, cx, |s| s.refresh());
9992 assert_eq!(
9993 editor.selections.ranges(cx),
9994 [
9995 Point::new(1, 3)..Point::new(1, 3),
9996 Point::new(2, 1)..Point::new(2, 1),
9997 ]
9998 );
9999 });
10000
10001 multibuffer.update(cx, |multibuffer, cx| {
10002 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
10003 });
10004 _ = editor.update(cx, |editor, window, cx| {
10005 // Removing an excerpt causes the first selection to become degenerate.
10006 assert_eq!(
10007 editor.selections.ranges(cx),
10008 [
10009 Point::new(0, 0)..Point::new(0, 0),
10010 Point::new(0, 1)..Point::new(0, 1)
10011 ]
10012 );
10013
10014 // Refreshing selections will relocate the first selection to the original buffer
10015 // location.
10016 editor.change_selections(None, window, cx, |s| s.refresh());
10017 assert_eq!(
10018 editor.selections.ranges(cx),
10019 [
10020 Point::new(0, 1)..Point::new(0, 1),
10021 Point::new(0, 3)..Point::new(0, 3)
10022 ]
10023 );
10024 assert!(editor.selections.pending_anchor().is_some());
10025 });
10026}
10027
10028#[gpui::test]
10029fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
10030 init_test(cx, |_| {});
10031
10032 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10033 let mut excerpt1_id = None;
10034 let multibuffer = cx.new(|cx| {
10035 let mut multibuffer = MultiBuffer::new(ReadWrite);
10036 excerpt1_id = multibuffer
10037 .push_excerpts(
10038 buffer.clone(),
10039 [
10040 ExcerptRange {
10041 context: Point::new(0, 0)..Point::new(1, 4),
10042 primary: None,
10043 },
10044 ExcerptRange {
10045 context: Point::new(1, 0)..Point::new(2, 4),
10046 primary: None,
10047 },
10048 ],
10049 cx,
10050 )
10051 .into_iter()
10052 .next();
10053 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
10054 multibuffer
10055 });
10056
10057 let editor = cx.add_window(|window, cx| {
10058 let mut editor = build_editor(multibuffer.clone(), window, cx);
10059 let snapshot = editor.snapshot(window, cx);
10060 editor.begin_selection(
10061 Point::new(1, 3).to_display_point(&snapshot),
10062 false,
10063 1,
10064 window,
10065 cx,
10066 );
10067 assert_eq!(
10068 editor.selections.ranges(cx),
10069 [Point::new(1, 3)..Point::new(1, 3)]
10070 );
10071 editor
10072 });
10073
10074 multibuffer.update(cx, |multibuffer, cx| {
10075 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
10076 });
10077 _ = editor.update(cx, |editor, window, cx| {
10078 assert_eq!(
10079 editor.selections.ranges(cx),
10080 [Point::new(0, 0)..Point::new(0, 0)]
10081 );
10082
10083 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
10084 editor.change_selections(None, window, cx, |s| s.refresh());
10085 assert_eq!(
10086 editor.selections.ranges(cx),
10087 [Point::new(0, 3)..Point::new(0, 3)]
10088 );
10089 assert!(editor.selections.pending_anchor().is_some());
10090 });
10091}
10092
10093#[gpui::test]
10094async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
10095 init_test(cx, |_| {});
10096
10097 let language = Arc::new(
10098 Language::new(
10099 LanguageConfig {
10100 brackets: BracketPairConfig {
10101 pairs: vec![
10102 BracketPair {
10103 start: "{".to_string(),
10104 end: "}".to_string(),
10105 close: true,
10106 surround: true,
10107 newline: true,
10108 },
10109 BracketPair {
10110 start: "/* ".to_string(),
10111 end: " */".to_string(),
10112 close: true,
10113 surround: true,
10114 newline: true,
10115 },
10116 ],
10117 ..Default::default()
10118 },
10119 ..Default::default()
10120 },
10121 Some(tree_sitter_rust::LANGUAGE.into()),
10122 )
10123 .with_indents_query("")
10124 .unwrap(),
10125 );
10126
10127 let text = concat!(
10128 "{ }\n", //
10129 " x\n", //
10130 " /* */\n", //
10131 "x\n", //
10132 "{{} }\n", //
10133 );
10134
10135 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10136 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10137 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10138 editor
10139 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10140 .await;
10141
10142 editor.update_in(cx, |editor, window, cx| {
10143 editor.change_selections(None, window, cx, |s| {
10144 s.select_display_ranges([
10145 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
10146 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
10147 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
10148 ])
10149 });
10150 editor.newline(&Newline, window, cx);
10151
10152 assert_eq!(
10153 editor.buffer().read(cx).read(cx).text(),
10154 concat!(
10155 "{ \n", // Suppress rustfmt
10156 "\n", //
10157 "}\n", //
10158 " x\n", //
10159 " /* \n", //
10160 " \n", //
10161 " */\n", //
10162 "x\n", //
10163 "{{} \n", //
10164 "}\n", //
10165 )
10166 );
10167 });
10168}
10169
10170#[gpui::test]
10171fn test_highlighted_ranges(cx: &mut TestAppContext) {
10172 init_test(cx, |_| {});
10173
10174 let editor = cx.add_window(|window, cx| {
10175 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
10176 build_editor(buffer.clone(), window, cx)
10177 });
10178
10179 _ = editor.update(cx, |editor, window, cx| {
10180 struct Type1;
10181 struct Type2;
10182
10183 let buffer = editor.buffer.read(cx).snapshot(cx);
10184
10185 let anchor_range =
10186 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
10187
10188 editor.highlight_background::<Type1>(
10189 &[
10190 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
10191 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
10192 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
10193 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
10194 ],
10195 |_| Hsla::red(),
10196 cx,
10197 );
10198 editor.highlight_background::<Type2>(
10199 &[
10200 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
10201 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
10202 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
10203 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
10204 ],
10205 |_| Hsla::green(),
10206 cx,
10207 );
10208
10209 let snapshot = editor.snapshot(window, cx);
10210 let mut highlighted_ranges = editor.background_highlights_in_range(
10211 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
10212 &snapshot,
10213 cx.theme().colors(),
10214 );
10215 // Enforce a consistent ordering based on color without relying on the ordering of the
10216 // highlight's `TypeId` which is non-executor.
10217 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
10218 assert_eq!(
10219 highlighted_ranges,
10220 &[
10221 (
10222 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
10223 Hsla::red(),
10224 ),
10225 (
10226 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
10227 Hsla::red(),
10228 ),
10229 (
10230 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
10231 Hsla::green(),
10232 ),
10233 (
10234 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
10235 Hsla::green(),
10236 ),
10237 ]
10238 );
10239 assert_eq!(
10240 editor.background_highlights_in_range(
10241 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
10242 &snapshot,
10243 cx.theme().colors(),
10244 ),
10245 &[(
10246 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
10247 Hsla::red(),
10248 )]
10249 );
10250 });
10251}
10252
10253#[gpui::test]
10254async fn test_following(cx: &mut gpui::TestAppContext) {
10255 init_test(cx, |_| {});
10256
10257 let fs = FakeFs::new(cx.executor());
10258 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
10259
10260 let buffer = project.update(cx, |project, cx| {
10261 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
10262 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
10263 });
10264 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
10265 let follower = cx.update(|cx| {
10266 cx.open_window(
10267 WindowOptions {
10268 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
10269 gpui::Point::new(px(0.), px(0.)),
10270 gpui::Point::new(px(10.), px(80.)),
10271 ))),
10272 ..Default::default()
10273 },
10274 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
10275 )
10276 .unwrap()
10277 });
10278
10279 let is_still_following = Rc::new(RefCell::new(true));
10280 let follower_edit_event_count = Rc::new(RefCell::new(0));
10281 let pending_update = Rc::new(RefCell::new(None));
10282 let leader_entity = leader.root(cx).unwrap();
10283 let follower_entity = follower.root(cx).unwrap();
10284 _ = follower.update(cx, {
10285 let update = pending_update.clone();
10286 let is_still_following = is_still_following.clone();
10287 let follower_edit_event_count = follower_edit_event_count.clone();
10288 |_, window, cx| {
10289 cx.subscribe_in(
10290 &leader_entity,
10291 window,
10292 move |_, leader, event, window, cx| {
10293 leader.read(cx).add_event_to_update_proto(
10294 event,
10295 &mut update.borrow_mut(),
10296 window,
10297 cx,
10298 );
10299 },
10300 )
10301 .detach();
10302
10303 cx.subscribe_in(
10304 &follower_entity,
10305 window,
10306 move |_, _, event: &EditorEvent, _window, _cx| {
10307 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
10308 *is_still_following.borrow_mut() = false;
10309 }
10310
10311 if let EditorEvent::BufferEdited = event {
10312 *follower_edit_event_count.borrow_mut() += 1;
10313 }
10314 },
10315 )
10316 .detach();
10317 }
10318 });
10319
10320 // Update the selections only
10321 _ = leader.update(cx, |leader, window, cx| {
10322 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
10323 });
10324 follower
10325 .update(cx, |follower, window, cx| {
10326 follower.apply_update_proto(
10327 &project,
10328 pending_update.borrow_mut().take().unwrap(),
10329 window,
10330 cx,
10331 )
10332 })
10333 .unwrap()
10334 .await
10335 .unwrap();
10336 _ = follower.update(cx, |follower, _, cx| {
10337 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
10338 });
10339 assert!(*is_still_following.borrow());
10340 assert_eq!(*follower_edit_event_count.borrow(), 0);
10341
10342 // Update the scroll position only
10343 _ = leader.update(cx, |leader, window, cx| {
10344 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
10345 });
10346 follower
10347 .update(cx, |follower, window, cx| {
10348 follower.apply_update_proto(
10349 &project,
10350 pending_update.borrow_mut().take().unwrap(),
10351 window,
10352 cx,
10353 )
10354 })
10355 .unwrap()
10356 .await
10357 .unwrap();
10358 assert_eq!(
10359 follower
10360 .update(cx, |follower, _, cx| follower.scroll_position(cx))
10361 .unwrap(),
10362 gpui::Point::new(1.5, 3.5)
10363 );
10364 assert!(*is_still_following.borrow());
10365 assert_eq!(*follower_edit_event_count.borrow(), 0);
10366
10367 // Update the selections and scroll position. The follower's scroll position is updated
10368 // via autoscroll, not via the leader's exact scroll position.
10369 _ = leader.update(cx, |leader, window, cx| {
10370 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10371 leader.request_autoscroll(Autoscroll::newest(), cx);
10372 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
10373 });
10374 follower
10375 .update(cx, |follower, window, cx| {
10376 follower.apply_update_proto(
10377 &project,
10378 pending_update.borrow_mut().take().unwrap(),
10379 window,
10380 cx,
10381 )
10382 })
10383 .unwrap()
10384 .await
10385 .unwrap();
10386 _ = follower.update(cx, |follower, _, cx| {
10387 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
10388 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
10389 });
10390 assert!(*is_still_following.borrow());
10391
10392 // Creating a pending selection that precedes another selection
10393 _ = leader.update(cx, |leader, window, cx| {
10394 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
10395 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
10396 });
10397 follower
10398 .update(cx, |follower, window, cx| {
10399 follower.apply_update_proto(
10400 &project,
10401 pending_update.borrow_mut().take().unwrap(),
10402 window,
10403 cx,
10404 )
10405 })
10406 .unwrap()
10407 .await
10408 .unwrap();
10409 _ = follower.update(cx, |follower, _, cx| {
10410 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
10411 });
10412 assert!(*is_still_following.borrow());
10413
10414 // Extend the pending selection so that it surrounds another selection
10415 _ = leader.update(cx, |leader, window, cx| {
10416 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
10417 });
10418 follower
10419 .update(cx, |follower, window, cx| {
10420 follower.apply_update_proto(
10421 &project,
10422 pending_update.borrow_mut().take().unwrap(),
10423 window,
10424 cx,
10425 )
10426 })
10427 .unwrap()
10428 .await
10429 .unwrap();
10430 _ = follower.update(cx, |follower, _, cx| {
10431 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
10432 });
10433
10434 // Scrolling locally breaks the follow
10435 _ = follower.update(cx, |follower, window, cx| {
10436 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
10437 follower.set_scroll_anchor(
10438 ScrollAnchor {
10439 anchor: top_anchor,
10440 offset: gpui::Point::new(0.0, 0.5),
10441 },
10442 window,
10443 cx,
10444 );
10445 });
10446 assert!(!(*is_still_following.borrow()));
10447}
10448
10449#[gpui::test]
10450async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
10451 init_test(cx, |_| {});
10452
10453 let fs = FakeFs::new(cx.executor());
10454 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
10455 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10456 let pane = workspace
10457 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10458 .unwrap();
10459
10460 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10461
10462 let leader = pane.update_in(cx, |_, window, cx| {
10463 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
10464 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
10465 });
10466
10467 // Start following the editor when it has no excerpts.
10468 let mut state_message =
10469 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
10470 let workspace_entity = workspace.root(cx).unwrap();
10471 let follower_1 = cx
10472 .update_window(*workspace.deref(), |_, window, cx| {
10473 Editor::from_state_proto(
10474 workspace_entity,
10475 ViewId {
10476 creator: Default::default(),
10477 id: 0,
10478 },
10479 &mut state_message,
10480 window,
10481 cx,
10482 )
10483 })
10484 .unwrap()
10485 .unwrap()
10486 .await
10487 .unwrap();
10488
10489 let update_message = Rc::new(RefCell::new(None));
10490 follower_1.update_in(cx, {
10491 let update = update_message.clone();
10492 |_, window, cx| {
10493 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
10494 leader.read(cx).add_event_to_update_proto(
10495 event,
10496 &mut update.borrow_mut(),
10497 window,
10498 cx,
10499 );
10500 })
10501 .detach();
10502 }
10503 });
10504
10505 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
10506 (
10507 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
10508 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
10509 )
10510 });
10511
10512 // Insert some excerpts.
10513 leader.update(cx, |leader, cx| {
10514 leader.buffer.update(cx, |multibuffer, cx| {
10515 let excerpt_ids = multibuffer.push_excerpts(
10516 buffer_1.clone(),
10517 [
10518 ExcerptRange {
10519 context: 1..6,
10520 primary: None,
10521 },
10522 ExcerptRange {
10523 context: 12..15,
10524 primary: None,
10525 },
10526 ExcerptRange {
10527 context: 0..3,
10528 primary: None,
10529 },
10530 ],
10531 cx,
10532 );
10533 multibuffer.insert_excerpts_after(
10534 excerpt_ids[0],
10535 buffer_2.clone(),
10536 [
10537 ExcerptRange {
10538 context: 8..12,
10539 primary: None,
10540 },
10541 ExcerptRange {
10542 context: 0..6,
10543 primary: None,
10544 },
10545 ],
10546 cx,
10547 );
10548 });
10549 });
10550
10551 // Apply the update of adding the excerpts.
10552 follower_1
10553 .update_in(cx, |follower, window, cx| {
10554 follower.apply_update_proto(
10555 &project,
10556 update_message.borrow().clone().unwrap(),
10557 window,
10558 cx,
10559 )
10560 })
10561 .await
10562 .unwrap();
10563 assert_eq!(
10564 follower_1.update(cx, |editor, cx| editor.text(cx)),
10565 leader.update(cx, |editor, cx| editor.text(cx))
10566 );
10567 update_message.borrow_mut().take();
10568
10569 // Start following separately after it already has excerpts.
10570 let mut state_message =
10571 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
10572 let workspace_entity = workspace.root(cx).unwrap();
10573 let follower_2 = cx
10574 .update_window(*workspace.deref(), |_, window, cx| {
10575 Editor::from_state_proto(
10576 workspace_entity,
10577 ViewId {
10578 creator: Default::default(),
10579 id: 0,
10580 },
10581 &mut state_message,
10582 window,
10583 cx,
10584 )
10585 })
10586 .unwrap()
10587 .unwrap()
10588 .await
10589 .unwrap();
10590 assert_eq!(
10591 follower_2.update(cx, |editor, cx| editor.text(cx)),
10592 leader.update(cx, |editor, cx| editor.text(cx))
10593 );
10594
10595 // Remove some excerpts.
10596 leader.update(cx, |leader, cx| {
10597 leader.buffer.update(cx, |multibuffer, cx| {
10598 let excerpt_ids = multibuffer.excerpt_ids();
10599 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
10600 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
10601 });
10602 });
10603
10604 // Apply the update of removing the excerpts.
10605 follower_1
10606 .update_in(cx, |follower, window, cx| {
10607 follower.apply_update_proto(
10608 &project,
10609 update_message.borrow().clone().unwrap(),
10610 window,
10611 cx,
10612 )
10613 })
10614 .await
10615 .unwrap();
10616 follower_2
10617 .update_in(cx, |follower, window, cx| {
10618 follower.apply_update_proto(
10619 &project,
10620 update_message.borrow().clone().unwrap(),
10621 window,
10622 cx,
10623 )
10624 })
10625 .await
10626 .unwrap();
10627 update_message.borrow_mut().take();
10628 assert_eq!(
10629 follower_1.update(cx, |editor, cx| editor.text(cx)),
10630 leader.update(cx, |editor, cx| editor.text(cx))
10631 );
10632}
10633
10634#[gpui::test]
10635async fn go_to_prev_overlapping_diagnostic(
10636 executor: BackgroundExecutor,
10637 cx: &mut gpui::TestAppContext,
10638) {
10639 init_test(cx, |_| {});
10640
10641 let mut cx = EditorTestContext::new(cx).await;
10642 let lsp_store =
10643 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10644
10645 cx.set_state(indoc! {"
10646 ˇfn func(abc def: i32) -> u32 {
10647 }
10648 "});
10649
10650 cx.update(|_, cx| {
10651 lsp_store.update(cx, |lsp_store, cx| {
10652 lsp_store
10653 .update_diagnostics(
10654 LanguageServerId(0),
10655 lsp::PublishDiagnosticsParams {
10656 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
10657 version: None,
10658 diagnostics: vec![
10659 lsp::Diagnostic {
10660 range: lsp::Range::new(
10661 lsp::Position::new(0, 11),
10662 lsp::Position::new(0, 12),
10663 ),
10664 severity: Some(lsp::DiagnosticSeverity::ERROR),
10665 ..Default::default()
10666 },
10667 lsp::Diagnostic {
10668 range: lsp::Range::new(
10669 lsp::Position::new(0, 12),
10670 lsp::Position::new(0, 15),
10671 ),
10672 severity: Some(lsp::DiagnosticSeverity::ERROR),
10673 ..Default::default()
10674 },
10675 lsp::Diagnostic {
10676 range: lsp::Range::new(
10677 lsp::Position::new(0, 25),
10678 lsp::Position::new(0, 28),
10679 ),
10680 severity: Some(lsp::DiagnosticSeverity::ERROR),
10681 ..Default::default()
10682 },
10683 ],
10684 },
10685 &[],
10686 cx,
10687 )
10688 .unwrap()
10689 });
10690 });
10691
10692 executor.run_until_parked();
10693
10694 cx.update_editor(|editor, window, cx| {
10695 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10696 });
10697
10698 cx.assert_editor_state(indoc! {"
10699 fn func(abc def: i32) -> ˇu32 {
10700 }
10701 "});
10702
10703 cx.update_editor(|editor, window, cx| {
10704 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10705 });
10706
10707 cx.assert_editor_state(indoc! {"
10708 fn func(abc ˇdef: i32) -> u32 {
10709 }
10710 "});
10711
10712 cx.update_editor(|editor, window, cx| {
10713 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10714 });
10715
10716 cx.assert_editor_state(indoc! {"
10717 fn func(abcˇ def: i32) -> u32 {
10718 }
10719 "});
10720
10721 cx.update_editor(|editor, window, cx| {
10722 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10723 });
10724
10725 cx.assert_editor_state(indoc! {"
10726 fn func(abc def: i32) -> ˇu32 {
10727 }
10728 "});
10729}
10730
10731#[gpui::test]
10732async fn cycle_through_same_place_diagnostics(
10733 executor: BackgroundExecutor,
10734 cx: &mut gpui::TestAppContext,
10735) {
10736 init_test(cx, |_| {});
10737
10738 let mut cx = EditorTestContext::new(cx).await;
10739 let lsp_store =
10740 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10741
10742 cx.set_state(indoc! {"
10743 ˇfn func(abc def: i32) -> u32 {
10744 }
10745 "});
10746
10747 cx.update(|_, cx| {
10748 lsp_store.update(cx, |lsp_store, cx| {
10749 lsp_store
10750 .update_diagnostics(
10751 LanguageServerId(0),
10752 lsp::PublishDiagnosticsParams {
10753 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
10754 version: None,
10755 diagnostics: vec![
10756 lsp::Diagnostic {
10757 range: lsp::Range::new(
10758 lsp::Position::new(0, 11),
10759 lsp::Position::new(0, 12),
10760 ),
10761 severity: Some(lsp::DiagnosticSeverity::ERROR),
10762 ..Default::default()
10763 },
10764 lsp::Diagnostic {
10765 range: lsp::Range::new(
10766 lsp::Position::new(0, 12),
10767 lsp::Position::new(0, 15),
10768 ),
10769 severity: Some(lsp::DiagnosticSeverity::ERROR),
10770 ..Default::default()
10771 },
10772 lsp::Diagnostic {
10773 range: lsp::Range::new(
10774 lsp::Position::new(0, 12),
10775 lsp::Position::new(0, 15),
10776 ),
10777 severity: Some(lsp::DiagnosticSeverity::ERROR),
10778 ..Default::default()
10779 },
10780 lsp::Diagnostic {
10781 range: lsp::Range::new(
10782 lsp::Position::new(0, 25),
10783 lsp::Position::new(0, 28),
10784 ),
10785 severity: Some(lsp::DiagnosticSeverity::ERROR),
10786 ..Default::default()
10787 },
10788 ],
10789 },
10790 &[],
10791 cx,
10792 )
10793 .unwrap()
10794 });
10795 });
10796 executor.run_until_parked();
10797
10798 //// Backward
10799
10800 // Fourth diagnostic
10801 cx.update_editor(|editor, window, cx| {
10802 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10803 });
10804 cx.assert_editor_state(indoc! {"
10805 fn func(abc def: i32) -> ˇu32 {
10806 }
10807 "});
10808
10809 // Third diagnostic
10810 cx.update_editor(|editor, window, cx| {
10811 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10812 });
10813 cx.assert_editor_state(indoc! {"
10814 fn func(abc ˇdef: i32) -> u32 {
10815 }
10816 "});
10817
10818 // Second diagnostic, same place
10819 cx.update_editor(|editor, window, cx| {
10820 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10821 });
10822 cx.assert_editor_state(indoc! {"
10823 fn func(abc ˇdef: i32) -> u32 {
10824 }
10825 "});
10826
10827 // First diagnostic
10828 cx.update_editor(|editor, window, cx| {
10829 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10830 });
10831 cx.assert_editor_state(indoc! {"
10832 fn func(abcˇ def: i32) -> u32 {
10833 }
10834 "});
10835
10836 // Wrapped over, fourth diagnostic
10837 cx.update_editor(|editor, window, cx| {
10838 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10839 });
10840 cx.assert_editor_state(indoc! {"
10841 fn func(abc def: i32) -> ˇu32 {
10842 }
10843 "});
10844
10845 cx.update_editor(|editor, window, cx| {
10846 editor.move_to_beginning(&MoveToBeginning, window, cx);
10847 });
10848 cx.assert_editor_state(indoc! {"
10849 ˇfn func(abc def: i32) -> u32 {
10850 }
10851 "});
10852
10853 //// Forward
10854
10855 // First diagnostic
10856 cx.update_editor(|editor, window, cx| {
10857 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10858 });
10859 cx.assert_editor_state(indoc! {"
10860 fn func(abcˇ def: i32) -> u32 {
10861 }
10862 "});
10863
10864 // Second diagnostic
10865 cx.update_editor(|editor, window, cx| {
10866 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10867 });
10868 cx.assert_editor_state(indoc! {"
10869 fn func(abc ˇdef: i32) -> u32 {
10870 }
10871 "});
10872
10873 // Third diagnostic, same place
10874 cx.update_editor(|editor, window, cx| {
10875 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10876 });
10877 cx.assert_editor_state(indoc! {"
10878 fn func(abc ˇdef: i32) -> u32 {
10879 }
10880 "});
10881
10882 // Fourth diagnostic
10883 cx.update_editor(|editor, window, cx| {
10884 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10885 });
10886 cx.assert_editor_state(indoc! {"
10887 fn func(abc def: i32) -> ˇu32 {
10888 }
10889 "});
10890
10891 // Wrapped around, first diagnostic
10892 cx.update_editor(|editor, window, cx| {
10893 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10894 });
10895 cx.assert_editor_state(indoc! {"
10896 fn func(abcˇ def: i32) -> u32 {
10897 }
10898 "});
10899}
10900
10901#[gpui::test]
10902async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
10903 init_test(cx, |_| {});
10904
10905 let mut cx = EditorTestContext::new(cx).await;
10906
10907 cx.set_state(indoc! {"
10908 fn func(abˇc def: i32) -> u32 {
10909 }
10910 "});
10911 let lsp_store =
10912 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10913
10914 cx.update(|_, cx| {
10915 lsp_store.update(cx, |lsp_store, cx| {
10916 lsp_store.update_diagnostics(
10917 LanguageServerId(0),
10918 lsp::PublishDiagnosticsParams {
10919 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
10920 version: None,
10921 diagnostics: vec![lsp::Diagnostic {
10922 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
10923 severity: Some(lsp::DiagnosticSeverity::ERROR),
10924 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
10925 ..Default::default()
10926 }],
10927 },
10928 &[],
10929 cx,
10930 )
10931 })
10932 }).unwrap();
10933 cx.run_until_parked();
10934 cx.update_editor(|editor, window, cx| {
10935 hover_popover::hover(editor, &Default::default(), window, cx)
10936 });
10937 cx.run_until_parked();
10938 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
10939}
10940
10941#[gpui::test]
10942async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
10943 init_test(cx, |_| {});
10944
10945 let mut cx = EditorTestContext::new(cx).await;
10946
10947 let diff_base = r#"
10948 use some::mod;
10949
10950 const A: u32 = 42;
10951
10952 fn main() {
10953 println!("hello");
10954
10955 println!("world");
10956 }
10957 "#
10958 .unindent();
10959
10960 // Edits are modified, removed, modified, added
10961 cx.set_state(
10962 &r#"
10963 use some::modified;
10964
10965 ˇ
10966 fn main() {
10967 println!("hello there");
10968
10969 println!("around the");
10970 println!("world");
10971 }
10972 "#
10973 .unindent(),
10974 );
10975
10976 cx.set_diff_base(&diff_base);
10977 executor.run_until_parked();
10978
10979 cx.update_editor(|editor, window, cx| {
10980 //Wrap around the bottom of the buffer
10981 for _ in 0..3 {
10982 editor.go_to_next_hunk(&GoToHunk, window, cx);
10983 }
10984 });
10985
10986 cx.assert_editor_state(
10987 &r#"
10988 ˇuse some::modified;
10989
10990
10991 fn main() {
10992 println!("hello there");
10993
10994 println!("around the");
10995 println!("world");
10996 }
10997 "#
10998 .unindent(),
10999 );
11000
11001 cx.update_editor(|editor, window, cx| {
11002 //Wrap around the top of the buffer
11003 for _ in 0..2 {
11004 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
11005 }
11006 });
11007
11008 cx.assert_editor_state(
11009 &r#"
11010 use some::modified;
11011
11012
11013 fn main() {
11014 ˇ println!("hello there");
11015
11016 println!("around the");
11017 println!("world");
11018 }
11019 "#
11020 .unindent(),
11021 );
11022
11023 cx.update_editor(|editor, window, cx| {
11024 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
11025 });
11026
11027 cx.assert_editor_state(
11028 &r#"
11029 use some::modified;
11030
11031 ˇ
11032 fn main() {
11033 println!("hello there");
11034
11035 println!("around the");
11036 println!("world");
11037 }
11038 "#
11039 .unindent(),
11040 );
11041
11042 cx.update_editor(|editor, window, cx| {
11043 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
11044 });
11045
11046 cx.assert_editor_state(
11047 &r#"
11048 ˇuse some::modified;
11049
11050
11051 fn main() {
11052 println!("hello there");
11053
11054 println!("around the");
11055 println!("world");
11056 }
11057 "#
11058 .unindent(),
11059 );
11060
11061 cx.update_editor(|editor, window, cx| {
11062 for _ in 0..2 {
11063 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
11064 }
11065 });
11066
11067 cx.assert_editor_state(
11068 &r#"
11069 use some::modified;
11070
11071
11072 fn main() {
11073 ˇ println!("hello there");
11074
11075 println!("around the");
11076 println!("world");
11077 }
11078 "#
11079 .unindent(),
11080 );
11081
11082 cx.update_editor(|editor, window, cx| {
11083 editor.fold(&Fold, window, cx);
11084 });
11085
11086 cx.update_editor(|editor, window, cx| {
11087 editor.go_to_next_hunk(&GoToHunk, window, cx);
11088 });
11089
11090 cx.assert_editor_state(
11091 &r#"
11092 ˇuse some::modified;
11093
11094
11095 fn main() {
11096 println!("hello there");
11097
11098 println!("around the");
11099 println!("world");
11100 }
11101 "#
11102 .unindent(),
11103 );
11104}
11105
11106#[test]
11107fn test_split_words() {
11108 fn split(text: &str) -> Vec<&str> {
11109 split_words(text).collect()
11110 }
11111
11112 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
11113 assert_eq!(split("hello_world"), &["hello_", "world"]);
11114 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
11115 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
11116 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
11117 assert_eq!(split("helloworld"), &["helloworld"]);
11118
11119 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
11120}
11121
11122#[gpui::test]
11123async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
11124 init_test(cx, |_| {});
11125
11126 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
11127 let mut assert = |before, after| {
11128 let _state_context = cx.set_state(before);
11129 cx.run_until_parked();
11130 cx.update_editor(|editor, window, cx| {
11131 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
11132 });
11133 cx.assert_editor_state(after);
11134 };
11135
11136 // Outside bracket jumps to outside of matching bracket
11137 assert("console.logˇ(var);", "console.log(var)ˇ;");
11138 assert("console.log(var)ˇ;", "console.logˇ(var);");
11139
11140 // Inside bracket jumps to inside of matching bracket
11141 assert("console.log(ˇvar);", "console.log(varˇ);");
11142 assert("console.log(varˇ);", "console.log(ˇvar);");
11143
11144 // When outside a bracket and inside, favor jumping to the inside bracket
11145 assert(
11146 "console.log('foo', [1, 2, 3]ˇ);",
11147 "console.log(ˇ'foo', [1, 2, 3]);",
11148 );
11149 assert(
11150 "console.log(ˇ'foo', [1, 2, 3]);",
11151 "console.log('foo', [1, 2, 3]ˇ);",
11152 );
11153
11154 // Bias forward if two options are equally likely
11155 assert(
11156 "let result = curried_fun()ˇ();",
11157 "let result = curried_fun()()ˇ;",
11158 );
11159
11160 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
11161 assert(
11162 indoc! {"
11163 function test() {
11164 console.log('test')ˇ
11165 }"},
11166 indoc! {"
11167 function test() {
11168 console.logˇ('test')
11169 }"},
11170 );
11171}
11172
11173#[gpui::test]
11174async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
11175 init_test(cx, |_| {});
11176
11177 let fs = FakeFs::new(cx.executor());
11178 fs.insert_tree(
11179 path!("/a"),
11180 json!({
11181 "main.rs": "fn main() { let a = 5; }",
11182 "other.rs": "// Test file",
11183 }),
11184 )
11185 .await;
11186 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11187
11188 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11189 language_registry.add(Arc::new(Language::new(
11190 LanguageConfig {
11191 name: "Rust".into(),
11192 matcher: LanguageMatcher {
11193 path_suffixes: vec!["rs".to_string()],
11194 ..Default::default()
11195 },
11196 brackets: BracketPairConfig {
11197 pairs: vec![BracketPair {
11198 start: "{".to_string(),
11199 end: "}".to_string(),
11200 close: true,
11201 surround: true,
11202 newline: true,
11203 }],
11204 disabled_scopes_by_bracket_ix: Vec::new(),
11205 },
11206 ..Default::default()
11207 },
11208 Some(tree_sitter_rust::LANGUAGE.into()),
11209 )));
11210 let mut fake_servers = language_registry.register_fake_lsp(
11211 "Rust",
11212 FakeLspAdapter {
11213 capabilities: lsp::ServerCapabilities {
11214 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
11215 first_trigger_character: "{".to_string(),
11216 more_trigger_character: None,
11217 }),
11218 ..Default::default()
11219 },
11220 ..Default::default()
11221 },
11222 );
11223
11224 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11225
11226 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11227
11228 let worktree_id = workspace
11229 .update(cx, |workspace, _, cx| {
11230 workspace.project().update(cx, |project, cx| {
11231 project.worktrees(cx).next().unwrap().read(cx).id()
11232 })
11233 })
11234 .unwrap();
11235
11236 let buffer = project
11237 .update(cx, |project, cx| {
11238 project.open_local_buffer(path!("/a/main.rs"), cx)
11239 })
11240 .await
11241 .unwrap();
11242 let editor_handle = workspace
11243 .update(cx, |workspace, window, cx| {
11244 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
11245 })
11246 .unwrap()
11247 .await
11248 .unwrap()
11249 .downcast::<Editor>()
11250 .unwrap();
11251
11252 cx.executor().start_waiting();
11253 let fake_server = fake_servers.next().await.unwrap();
11254
11255 fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
11256 assert_eq!(
11257 params.text_document_position.text_document.uri,
11258 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
11259 );
11260 assert_eq!(
11261 params.text_document_position.position,
11262 lsp::Position::new(0, 21),
11263 );
11264
11265 Ok(Some(vec![lsp::TextEdit {
11266 new_text: "]".to_string(),
11267 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11268 }]))
11269 });
11270
11271 editor_handle.update_in(cx, |editor, window, cx| {
11272 window.focus(&editor.focus_handle(cx));
11273 editor.change_selections(None, window, cx, |s| {
11274 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
11275 });
11276 editor.handle_input("{", window, cx);
11277 });
11278
11279 cx.executor().run_until_parked();
11280
11281 buffer.update(cx, |buffer, _| {
11282 assert_eq!(
11283 buffer.text(),
11284 "fn main() { let a = {5}; }",
11285 "No extra braces from on type formatting should appear in the buffer"
11286 )
11287 });
11288}
11289
11290#[gpui::test]
11291async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) {
11292 init_test(cx, |_| {});
11293
11294 let fs = FakeFs::new(cx.executor());
11295 fs.insert_tree(
11296 path!("/a"),
11297 json!({
11298 "main.rs": "fn main() { let a = 5; }",
11299 "other.rs": "// Test file",
11300 }),
11301 )
11302 .await;
11303
11304 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11305
11306 let server_restarts = Arc::new(AtomicUsize::new(0));
11307 let closure_restarts = Arc::clone(&server_restarts);
11308 let language_server_name = "test language server";
11309 let language_name: LanguageName = "Rust".into();
11310
11311 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11312 language_registry.add(Arc::new(Language::new(
11313 LanguageConfig {
11314 name: language_name.clone(),
11315 matcher: LanguageMatcher {
11316 path_suffixes: vec!["rs".to_string()],
11317 ..Default::default()
11318 },
11319 ..Default::default()
11320 },
11321 Some(tree_sitter_rust::LANGUAGE.into()),
11322 )));
11323 let mut fake_servers = language_registry.register_fake_lsp(
11324 "Rust",
11325 FakeLspAdapter {
11326 name: language_server_name,
11327 initialization_options: Some(json!({
11328 "testOptionValue": true
11329 })),
11330 initializer: Some(Box::new(move |fake_server| {
11331 let task_restarts = Arc::clone(&closure_restarts);
11332 fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
11333 task_restarts.fetch_add(1, atomic::Ordering::Release);
11334 futures::future::ready(Ok(()))
11335 });
11336 })),
11337 ..Default::default()
11338 },
11339 );
11340
11341 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11342 let _buffer = project
11343 .update(cx, |project, cx| {
11344 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
11345 })
11346 .await
11347 .unwrap();
11348 let _fake_server = fake_servers.next().await.unwrap();
11349 update_test_language_settings(cx, |language_settings| {
11350 language_settings.languages.insert(
11351 language_name.clone(),
11352 LanguageSettingsContent {
11353 tab_size: NonZeroU32::new(8),
11354 ..Default::default()
11355 },
11356 );
11357 });
11358 cx.executor().run_until_parked();
11359 assert_eq!(
11360 server_restarts.load(atomic::Ordering::Acquire),
11361 0,
11362 "Should not restart LSP server on an unrelated change"
11363 );
11364
11365 update_test_project_settings(cx, |project_settings| {
11366 project_settings.lsp.insert(
11367 "Some other server name".into(),
11368 LspSettings {
11369 binary: None,
11370 settings: None,
11371 initialization_options: Some(json!({
11372 "some other init value": false
11373 })),
11374 },
11375 );
11376 });
11377 cx.executor().run_until_parked();
11378 assert_eq!(
11379 server_restarts.load(atomic::Ordering::Acquire),
11380 0,
11381 "Should not restart LSP server on an unrelated LSP settings change"
11382 );
11383
11384 update_test_project_settings(cx, |project_settings| {
11385 project_settings.lsp.insert(
11386 language_server_name.into(),
11387 LspSettings {
11388 binary: None,
11389 settings: None,
11390 initialization_options: Some(json!({
11391 "anotherInitValue": false
11392 })),
11393 },
11394 );
11395 });
11396 cx.executor().run_until_parked();
11397 assert_eq!(
11398 server_restarts.load(atomic::Ordering::Acquire),
11399 1,
11400 "Should restart LSP server on a related LSP settings change"
11401 );
11402
11403 update_test_project_settings(cx, |project_settings| {
11404 project_settings.lsp.insert(
11405 language_server_name.into(),
11406 LspSettings {
11407 binary: None,
11408 settings: None,
11409 initialization_options: Some(json!({
11410 "anotherInitValue": false
11411 })),
11412 },
11413 );
11414 });
11415 cx.executor().run_until_parked();
11416 assert_eq!(
11417 server_restarts.load(atomic::Ordering::Acquire),
11418 1,
11419 "Should not restart LSP server on a related LSP settings change that is the same"
11420 );
11421
11422 update_test_project_settings(cx, |project_settings| {
11423 project_settings.lsp.insert(
11424 language_server_name.into(),
11425 LspSettings {
11426 binary: None,
11427 settings: None,
11428 initialization_options: None,
11429 },
11430 );
11431 });
11432 cx.executor().run_until_parked();
11433 assert_eq!(
11434 server_restarts.load(atomic::Ordering::Acquire),
11435 2,
11436 "Should restart LSP server on another related LSP settings change"
11437 );
11438}
11439
11440#[gpui::test]
11441async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
11442 init_test(cx, |_| {});
11443
11444 let mut cx = EditorLspTestContext::new_rust(
11445 lsp::ServerCapabilities {
11446 completion_provider: Some(lsp::CompletionOptions {
11447 trigger_characters: Some(vec![".".to_string()]),
11448 resolve_provider: Some(true),
11449 ..Default::default()
11450 }),
11451 ..Default::default()
11452 },
11453 cx,
11454 )
11455 .await;
11456
11457 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11458 cx.simulate_keystroke(".");
11459 let completion_item = lsp::CompletionItem {
11460 label: "some".into(),
11461 kind: Some(lsp::CompletionItemKind::SNIPPET),
11462 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11463 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11464 kind: lsp::MarkupKind::Markdown,
11465 value: "```rust\nSome(2)\n```".to_string(),
11466 })),
11467 deprecated: Some(false),
11468 sort_text: Some("fffffff2".to_string()),
11469 filter_text: Some("some".to_string()),
11470 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11471 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11472 range: lsp::Range {
11473 start: lsp::Position {
11474 line: 0,
11475 character: 22,
11476 },
11477 end: lsp::Position {
11478 line: 0,
11479 character: 22,
11480 },
11481 },
11482 new_text: "Some(2)".to_string(),
11483 })),
11484 additional_text_edits: Some(vec![lsp::TextEdit {
11485 range: lsp::Range {
11486 start: lsp::Position {
11487 line: 0,
11488 character: 20,
11489 },
11490 end: lsp::Position {
11491 line: 0,
11492 character: 22,
11493 },
11494 },
11495 new_text: "".to_string(),
11496 }]),
11497 ..Default::default()
11498 };
11499
11500 let closure_completion_item = completion_item.clone();
11501 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11502 let task_completion_item = closure_completion_item.clone();
11503 async move {
11504 Ok(Some(lsp::CompletionResponse::Array(vec![
11505 task_completion_item,
11506 ])))
11507 }
11508 });
11509
11510 request.next().await;
11511
11512 cx.condition(|editor, _| editor.context_menu_visible())
11513 .await;
11514 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11515 editor
11516 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11517 .unwrap()
11518 });
11519 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
11520
11521 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
11522 let task_completion_item = completion_item.clone();
11523 async move { Ok(task_completion_item) }
11524 })
11525 .next()
11526 .await
11527 .unwrap();
11528 apply_additional_edits.await.unwrap();
11529 cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
11530}
11531
11532#[gpui::test]
11533async fn test_completions_resolve_updates_labels_if_filter_text_matches(
11534 cx: &mut gpui::TestAppContext,
11535) {
11536 init_test(cx, |_| {});
11537
11538 let mut cx = EditorLspTestContext::new_rust(
11539 lsp::ServerCapabilities {
11540 completion_provider: Some(lsp::CompletionOptions {
11541 trigger_characters: Some(vec![".".to_string()]),
11542 resolve_provider: Some(true),
11543 ..Default::default()
11544 }),
11545 ..Default::default()
11546 },
11547 cx,
11548 )
11549 .await;
11550
11551 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11552 cx.simulate_keystroke(".");
11553
11554 let item1 = lsp::CompletionItem {
11555 label: "method id()".to_string(),
11556 filter_text: Some("id".to_string()),
11557 detail: None,
11558 documentation: None,
11559 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11560 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11561 new_text: ".id".to_string(),
11562 })),
11563 ..lsp::CompletionItem::default()
11564 };
11565
11566 let item2 = lsp::CompletionItem {
11567 label: "other".to_string(),
11568 filter_text: Some("other".to_string()),
11569 detail: None,
11570 documentation: None,
11571 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11572 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11573 new_text: ".other".to_string(),
11574 })),
11575 ..lsp::CompletionItem::default()
11576 };
11577
11578 let item1 = item1.clone();
11579 cx.handle_request::<lsp::request::Completion, _, _>({
11580 let item1 = item1.clone();
11581 move |_, _, _| {
11582 let item1 = item1.clone();
11583 let item2 = item2.clone();
11584 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
11585 }
11586 })
11587 .next()
11588 .await;
11589
11590 cx.condition(|editor, _| editor.context_menu_visible())
11591 .await;
11592 cx.update_editor(|editor, _, _| {
11593 let context_menu = editor.context_menu.borrow_mut();
11594 let context_menu = context_menu
11595 .as_ref()
11596 .expect("Should have the context menu deployed");
11597 match context_menu {
11598 CodeContextMenu::Completions(completions_menu) => {
11599 let completions = completions_menu.completions.borrow_mut();
11600 assert_eq!(
11601 completions
11602 .iter()
11603 .map(|completion| &completion.label.text)
11604 .collect::<Vec<_>>(),
11605 vec!["method id()", "other"]
11606 )
11607 }
11608 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11609 }
11610 });
11611
11612 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>({
11613 let item1 = item1.clone();
11614 move |_, item_to_resolve, _| {
11615 let item1 = item1.clone();
11616 async move {
11617 if item1 == item_to_resolve {
11618 Ok(lsp::CompletionItem {
11619 label: "method id()".to_string(),
11620 filter_text: Some("id".to_string()),
11621 detail: Some("Now resolved!".to_string()),
11622 documentation: Some(lsp::Documentation::String("Docs".to_string())),
11623 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11624 range: lsp::Range::new(
11625 lsp::Position::new(0, 22),
11626 lsp::Position::new(0, 22),
11627 ),
11628 new_text: ".id".to_string(),
11629 })),
11630 ..lsp::CompletionItem::default()
11631 })
11632 } else {
11633 Ok(item_to_resolve)
11634 }
11635 }
11636 }
11637 })
11638 .next()
11639 .await
11640 .unwrap();
11641 cx.run_until_parked();
11642
11643 cx.update_editor(|editor, window, cx| {
11644 editor.context_menu_next(&Default::default(), window, cx);
11645 });
11646
11647 cx.update_editor(|editor, _, _| {
11648 let context_menu = editor.context_menu.borrow_mut();
11649 let context_menu = context_menu
11650 .as_ref()
11651 .expect("Should have the context menu deployed");
11652 match context_menu {
11653 CodeContextMenu::Completions(completions_menu) => {
11654 let completions = completions_menu.completions.borrow_mut();
11655 assert_eq!(
11656 completions
11657 .iter()
11658 .map(|completion| &completion.label.text)
11659 .collect::<Vec<_>>(),
11660 vec!["method id() Now resolved!", "other"],
11661 "Should update first completion label, but not second as the filter text did not match."
11662 );
11663 }
11664 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11665 }
11666 });
11667}
11668
11669#[gpui::test]
11670async fn test_completions_resolve_happens_once(cx: &mut gpui::TestAppContext) {
11671 init_test(cx, |_| {});
11672
11673 let mut cx = EditorLspTestContext::new_rust(
11674 lsp::ServerCapabilities {
11675 completion_provider: Some(lsp::CompletionOptions {
11676 trigger_characters: Some(vec![".".to_string()]),
11677 resolve_provider: Some(true),
11678 ..Default::default()
11679 }),
11680 ..Default::default()
11681 },
11682 cx,
11683 )
11684 .await;
11685
11686 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11687 cx.simulate_keystroke(".");
11688
11689 let unresolved_item_1 = lsp::CompletionItem {
11690 label: "id".to_string(),
11691 filter_text: Some("id".to_string()),
11692 detail: None,
11693 documentation: None,
11694 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11695 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11696 new_text: ".id".to_string(),
11697 })),
11698 ..lsp::CompletionItem::default()
11699 };
11700 let resolved_item_1 = lsp::CompletionItem {
11701 additional_text_edits: Some(vec![lsp::TextEdit {
11702 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
11703 new_text: "!!".to_string(),
11704 }]),
11705 ..unresolved_item_1.clone()
11706 };
11707 let unresolved_item_2 = lsp::CompletionItem {
11708 label: "other".to_string(),
11709 filter_text: Some("other".to_string()),
11710 detail: None,
11711 documentation: None,
11712 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11713 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11714 new_text: ".other".to_string(),
11715 })),
11716 ..lsp::CompletionItem::default()
11717 };
11718 let resolved_item_2 = lsp::CompletionItem {
11719 additional_text_edits: Some(vec![lsp::TextEdit {
11720 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
11721 new_text: "??".to_string(),
11722 }]),
11723 ..unresolved_item_2.clone()
11724 };
11725
11726 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
11727 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
11728 cx.lsp
11729 .server
11730 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
11731 let unresolved_item_1 = unresolved_item_1.clone();
11732 let resolved_item_1 = resolved_item_1.clone();
11733 let unresolved_item_2 = unresolved_item_2.clone();
11734 let resolved_item_2 = resolved_item_2.clone();
11735 let resolve_requests_1 = resolve_requests_1.clone();
11736 let resolve_requests_2 = resolve_requests_2.clone();
11737 move |unresolved_request, _| {
11738 let unresolved_item_1 = unresolved_item_1.clone();
11739 let resolved_item_1 = resolved_item_1.clone();
11740 let unresolved_item_2 = unresolved_item_2.clone();
11741 let resolved_item_2 = resolved_item_2.clone();
11742 let resolve_requests_1 = resolve_requests_1.clone();
11743 let resolve_requests_2 = resolve_requests_2.clone();
11744 async move {
11745 if unresolved_request == unresolved_item_1 {
11746 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
11747 Ok(resolved_item_1.clone())
11748 } else if unresolved_request == unresolved_item_2 {
11749 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
11750 Ok(resolved_item_2.clone())
11751 } else {
11752 panic!("Unexpected completion item {unresolved_request:?}")
11753 }
11754 }
11755 }
11756 })
11757 .detach();
11758
11759 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11760 let unresolved_item_1 = unresolved_item_1.clone();
11761 let unresolved_item_2 = unresolved_item_2.clone();
11762 async move {
11763 Ok(Some(lsp::CompletionResponse::Array(vec![
11764 unresolved_item_1,
11765 unresolved_item_2,
11766 ])))
11767 }
11768 })
11769 .next()
11770 .await;
11771
11772 cx.condition(|editor, _| editor.context_menu_visible())
11773 .await;
11774 cx.update_editor(|editor, _, _| {
11775 let context_menu = editor.context_menu.borrow_mut();
11776 let context_menu = context_menu
11777 .as_ref()
11778 .expect("Should have the context menu deployed");
11779 match context_menu {
11780 CodeContextMenu::Completions(completions_menu) => {
11781 let completions = completions_menu.completions.borrow_mut();
11782 assert_eq!(
11783 completions
11784 .iter()
11785 .map(|completion| &completion.label.text)
11786 .collect::<Vec<_>>(),
11787 vec!["id", "other"]
11788 )
11789 }
11790 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11791 }
11792 });
11793 cx.run_until_parked();
11794
11795 cx.update_editor(|editor, window, cx| {
11796 editor.context_menu_next(&ContextMenuNext, window, cx);
11797 });
11798 cx.run_until_parked();
11799 cx.update_editor(|editor, window, cx| {
11800 editor.context_menu_prev(&ContextMenuPrev, window, cx);
11801 });
11802 cx.run_until_parked();
11803 cx.update_editor(|editor, window, cx| {
11804 editor.context_menu_next(&ContextMenuNext, window, cx);
11805 });
11806 cx.run_until_parked();
11807 cx.update_editor(|editor, window, cx| {
11808 editor
11809 .compose_completion(&ComposeCompletion::default(), window, cx)
11810 .expect("No task returned")
11811 })
11812 .await
11813 .expect("Completion failed");
11814 cx.run_until_parked();
11815
11816 cx.update_editor(|editor, _, cx| {
11817 assert_eq!(
11818 resolve_requests_1.load(atomic::Ordering::Acquire),
11819 1,
11820 "Should always resolve once despite multiple selections"
11821 );
11822 assert_eq!(
11823 resolve_requests_2.load(atomic::Ordering::Acquire),
11824 1,
11825 "Should always resolve once after multiple selections and applying the completion"
11826 );
11827 assert_eq!(
11828 editor.text(cx),
11829 "fn main() { let a = ??.other; }",
11830 "Should use resolved data when applying the completion"
11831 );
11832 });
11833}
11834
11835#[gpui::test]
11836async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
11837 init_test(cx, |_| {});
11838
11839 let item_0 = lsp::CompletionItem {
11840 label: "abs".into(),
11841 insert_text: Some("abs".into()),
11842 data: Some(json!({ "very": "special"})),
11843 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
11844 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11845 lsp::InsertReplaceEdit {
11846 new_text: "abs".to_string(),
11847 insert: lsp::Range::default(),
11848 replace: lsp::Range::default(),
11849 },
11850 )),
11851 ..lsp::CompletionItem::default()
11852 };
11853 let items = iter::once(item_0.clone())
11854 .chain((11..51).map(|i| lsp::CompletionItem {
11855 label: format!("item_{}", i),
11856 insert_text: Some(format!("item_{}", i)),
11857 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
11858 ..lsp::CompletionItem::default()
11859 }))
11860 .collect::<Vec<_>>();
11861
11862 let default_commit_characters = vec!["?".to_string()];
11863 let default_data = json!({ "default": "data"});
11864 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
11865 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
11866 let default_edit_range = lsp::Range {
11867 start: lsp::Position {
11868 line: 0,
11869 character: 5,
11870 },
11871 end: lsp::Position {
11872 line: 0,
11873 character: 5,
11874 },
11875 };
11876
11877 let item_0_out = lsp::CompletionItem {
11878 commit_characters: Some(default_commit_characters.clone()),
11879 insert_text_format: Some(default_insert_text_format),
11880 ..item_0
11881 };
11882 let items_out = iter::once(item_0_out)
11883 .chain(items[1..].iter().map(|item| lsp::CompletionItem {
11884 commit_characters: Some(default_commit_characters.clone()),
11885 data: Some(default_data.clone()),
11886 insert_text_mode: Some(default_insert_text_mode),
11887 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11888 range: default_edit_range,
11889 new_text: item.label.clone(),
11890 })),
11891 ..item.clone()
11892 }))
11893 .collect::<Vec<lsp::CompletionItem>>();
11894
11895 let mut cx = EditorLspTestContext::new_rust(
11896 lsp::ServerCapabilities {
11897 completion_provider: Some(lsp::CompletionOptions {
11898 trigger_characters: Some(vec![".".to_string()]),
11899 resolve_provider: Some(true),
11900 ..Default::default()
11901 }),
11902 ..Default::default()
11903 },
11904 cx,
11905 )
11906 .await;
11907
11908 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11909 cx.simulate_keystroke(".");
11910
11911 let completion_data = default_data.clone();
11912 let completion_characters = default_commit_characters.clone();
11913 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11914 let default_data = completion_data.clone();
11915 let default_commit_characters = completion_characters.clone();
11916 let items = items.clone();
11917 async move {
11918 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
11919 items,
11920 item_defaults: Some(lsp::CompletionListItemDefaults {
11921 data: Some(default_data.clone()),
11922 commit_characters: Some(default_commit_characters.clone()),
11923 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
11924 default_edit_range,
11925 )),
11926 insert_text_format: Some(default_insert_text_format),
11927 insert_text_mode: Some(default_insert_text_mode),
11928 }),
11929 ..lsp::CompletionList::default()
11930 })))
11931 }
11932 })
11933 .next()
11934 .await;
11935
11936 let resolved_items = Arc::new(Mutex::new(Vec::new()));
11937 cx.lsp
11938 .server
11939 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
11940 let closure_resolved_items = resolved_items.clone();
11941 move |item_to_resolve, _| {
11942 let closure_resolved_items = closure_resolved_items.clone();
11943 async move {
11944 closure_resolved_items.lock().push(item_to_resolve.clone());
11945 Ok(item_to_resolve)
11946 }
11947 }
11948 })
11949 .detach();
11950
11951 cx.condition(|editor, _| editor.context_menu_visible())
11952 .await;
11953 cx.run_until_parked();
11954 cx.update_editor(|editor, _, _| {
11955 let menu = editor.context_menu.borrow_mut();
11956 match menu.as_ref().expect("should have the completions menu") {
11957 CodeContextMenu::Completions(completions_menu) => {
11958 assert_eq!(
11959 completions_menu
11960 .entries
11961 .borrow()
11962 .iter()
11963 .map(|mat| mat.string.clone())
11964 .collect::<Vec<String>>(),
11965 items_out
11966 .iter()
11967 .map(|completion| completion.label.clone())
11968 .collect::<Vec<String>>()
11969 );
11970 }
11971 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
11972 }
11973 });
11974 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
11975 // with 4 from the end.
11976 assert_eq!(
11977 *resolved_items.lock(),
11978 [
11979 &items_out[0..16],
11980 &items_out[items_out.len() - 4..items_out.len()]
11981 ]
11982 .concat()
11983 .iter()
11984 .cloned()
11985 .collect::<Vec<lsp::CompletionItem>>()
11986 );
11987 resolved_items.lock().clear();
11988
11989 cx.update_editor(|editor, window, cx| {
11990 editor.context_menu_prev(&ContextMenuPrev, window, cx);
11991 });
11992 cx.run_until_parked();
11993 // Completions that have already been resolved are skipped.
11994 assert_eq!(
11995 *resolved_items.lock(),
11996 items_out[items_out.len() - 16..items_out.len() - 4]
11997 .iter()
11998 .cloned()
11999 .collect::<Vec<lsp::CompletionItem>>()
12000 );
12001 resolved_items.lock().clear();
12002}
12003
12004#[gpui::test]
12005async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
12006 init_test(cx, |_| {});
12007
12008 let mut cx = EditorLspTestContext::new(
12009 Language::new(
12010 LanguageConfig {
12011 matcher: LanguageMatcher {
12012 path_suffixes: vec!["jsx".into()],
12013 ..Default::default()
12014 },
12015 overrides: [(
12016 "element".into(),
12017 LanguageConfigOverride {
12018 word_characters: Override::Set(['-'].into_iter().collect()),
12019 ..Default::default()
12020 },
12021 )]
12022 .into_iter()
12023 .collect(),
12024 ..Default::default()
12025 },
12026 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12027 )
12028 .with_override_query("(jsx_self_closing_element) @element")
12029 .unwrap(),
12030 lsp::ServerCapabilities {
12031 completion_provider: Some(lsp::CompletionOptions {
12032 trigger_characters: Some(vec![":".to_string()]),
12033 ..Default::default()
12034 }),
12035 ..Default::default()
12036 },
12037 cx,
12038 )
12039 .await;
12040
12041 cx.lsp
12042 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12043 Ok(Some(lsp::CompletionResponse::Array(vec![
12044 lsp::CompletionItem {
12045 label: "bg-blue".into(),
12046 ..Default::default()
12047 },
12048 lsp::CompletionItem {
12049 label: "bg-red".into(),
12050 ..Default::default()
12051 },
12052 lsp::CompletionItem {
12053 label: "bg-yellow".into(),
12054 ..Default::default()
12055 },
12056 ])))
12057 });
12058
12059 cx.set_state(r#"<p class="bgˇ" />"#);
12060
12061 // Trigger completion when typing a dash, because the dash is an extra
12062 // word character in the 'element' scope, which contains the cursor.
12063 cx.simulate_keystroke("-");
12064 cx.executor().run_until_parked();
12065 cx.update_editor(|editor, _, _| {
12066 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12067 {
12068 assert_eq!(
12069 completion_menu_entries(&menu),
12070 &["bg-red", "bg-blue", "bg-yellow"]
12071 );
12072 } else {
12073 panic!("expected completion menu to be open");
12074 }
12075 });
12076
12077 cx.simulate_keystroke("l");
12078 cx.executor().run_until_parked();
12079 cx.update_editor(|editor, _, _| {
12080 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12081 {
12082 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
12083 } else {
12084 panic!("expected completion menu to be open");
12085 }
12086 });
12087
12088 // When filtering completions, consider the character after the '-' to
12089 // be the start of a subword.
12090 cx.set_state(r#"<p class="yelˇ" />"#);
12091 cx.simulate_keystroke("l");
12092 cx.executor().run_until_parked();
12093 cx.update_editor(|editor, _, _| {
12094 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12095 {
12096 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
12097 } else {
12098 panic!("expected completion menu to be open");
12099 }
12100 });
12101}
12102
12103fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
12104 let entries = menu.entries.borrow();
12105 entries.iter().map(|mat| mat.string.clone()).collect()
12106}
12107
12108#[gpui::test]
12109async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
12110 init_test(cx, |settings| {
12111 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
12112 FormatterList(vec![Formatter::Prettier].into()),
12113 ))
12114 });
12115
12116 let fs = FakeFs::new(cx.executor());
12117 fs.insert_file(path!("/file.ts"), Default::default()).await;
12118
12119 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
12120 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12121
12122 language_registry.add(Arc::new(Language::new(
12123 LanguageConfig {
12124 name: "TypeScript".into(),
12125 matcher: LanguageMatcher {
12126 path_suffixes: vec!["ts".to_string()],
12127 ..Default::default()
12128 },
12129 ..Default::default()
12130 },
12131 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12132 )));
12133 update_test_language_settings(cx, |settings| {
12134 settings.defaults.prettier = Some(PrettierSettings {
12135 allowed: true,
12136 ..PrettierSettings::default()
12137 });
12138 });
12139
12140 let test_plugin = "test_plugin";
12141 let _ = language_registry.register_fake_lsp(
12142 "TypeScript",
12143 FakeLspAdapter {
12144 prettier_plugins: vec![test_plugin],
12145 ..Default::default()
12146 },
12147 );
12148
12149 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
12150 let buffer = project
12151 .update(cx, |project, cx| {
12152 project.open_local_buffer(path!("/file.ts"), cx)
12153 })
12154 .await
12155 .unwrap();
12156
12157 let buffer_text = "one\ntwo\nthree\n";
12158 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12159 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12160 editor.update_in(cx, |editor, window, cx| {
12161 editor.set_text(buffer_text, window, cx)
12162 });
12163
12164 editor
12165 .update_in(cx, |editor, window, cx| {
12166 editor.perform_format(
12167 project.clone(),
12168 FormatTrigger::Manual,
12169 FormatTarget::Buffers,
12170 window,
12171 cx,
12172 )
12173 })
12174 .unwrap()
12175 .await;
12176 assert_eq!(
12177 editor.update(cx, |editor, cx| editor.text(cx)),
12178 buffer_text.to_string() + prettier_format_suffix,
12179 "Test prettier formatting was not applied to the original buffer text",
12180 );
12181
12182 update_test_language_settings(cx, |settings| {
12183 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
12184 });
12185 let format = editor.update_in(cx, |editor, window, cx| {
12186 editor.perform_format(
12187 project.clone(),
12188 FormatTrigger::Manual,
12189 FormatTarget::Buffers,
12190 window,
12191 cx,
12192 )
12193 });
12194 format.await.unwrap();
12195 assert_eq!(
12196 editor.update(cx, |editor, cx| editor.text(cx)),
12197 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
12198 "Autoformatting (via test prettier) was not applied to the original buffer text",
12199 );
12200}
12201
12202#[gpui::test]
12203async fn test_addition_reverts(cx: &mut gpui::TestAppContext) {
12204 init_test(cx, |_| {});
12205 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12206 let base_text = indoc! {r#"
12207 struct Row;
12208 struct Row1;
12209 struct Row2;
12210
12211 struct Row4;
12212 struct Row5;
12213 struct Row6;
12214
12215 struct Row8;
12216 struct Row9;
12217 struct Row10;"#};
12218
12219 // When addition hunks are not adjacent to carets, no hunk revert is performed
12220 assert_hunk_revert(
12221 indoc! {r#"struct Row;
12222 struct Row1;
12223 struct Row1.1;
12224 struct Row1.2;
12225 struct Row2;ˇ
12226
12227 struct Row4;
12228 struct Row5;
12229 struct Row6;
12230
12231 struct Row8;
12232 ˇstruct Row9;
12233 struct Row9.1;
12234 struct Row9.2;
12235 struct Row9.3;
12236 struct Row10;"#},
12237 vec![DiffHunkStatus::added(), DiffHunkStatus::added()],
12238 indoc! {r#"struct Row;
12239 struct Row1;
12240 struct Row1.1;
12241 struct Row1.2;
12242 struct Row2;ˇ
12243
12244 struct Row4;
12245 struct Row5;
12246 struct Row6;
12247
12248 struct Row8;
12249 ˇstruct Row9;
12250 struct Row9.1;
12251 struct Row9.2;
12252 struct Row9.3;
12253 struct Row10;"#},
12254 base_text,
12255 &mut cx,
12256 );
12257 // Same for selections
12258 assert_hunk_revert(
12259 indoc! {r#"struct Row;
12260 struct Row1;
12261 struct Row2;
12262 struct Row2.1;
12263 struct Row2.2;
12264 «ˇ
12265 struct Row4;
12266 struct» Row5;
12267 «struct Row6;
12268 ˇ»
12269 struct Row9.1;
12270 struct Row9.2;
12271 struct Row9.3;
12272 struct Row8;
12273 struct Row9;
12274 struct Row10;"#},
12275 vec![DiffHunkStatus::added(), DiffHunkStatus::added()],
12276 indoc! {r#"struct Row;
12277 struct Row1;
12278 struct Row2;
12279 struct Row2.1;
12280 struct Row2.2;
12281 «ˇ
12282 struct Row4;
12283 struct» Row5;
12284 «struct Row6;
12285 ˇ»
12286 struct Row9.1;
12287 struct Row9.2;
12288 struct Row9.3;
12289 struct Row8;
12290 struct Row9;
12291 struct Row10;"#},
12292 base_text,
12293 &mut cx,
12294 );
12295
12296 // When carets and selections intersect the addition hunks, those are reverted.
12297 // Adjacent carets got merged.
12298 assert_hunk_revert(
12299 indoc! {r#"struct Row;
12300 ˇ// something on the top
12301 struct Row1;
12302 struct Row2;
12303 struct Roˇw3.1;
12304 struct Row2.2;
12305 struct Row2.3;ˇ
12306
12307 struct Row4;
12308 struct ˇRow5.1;
12309 struct Row5.2;
12310 struct «Rowˇ»5.3;
12311 struct Row5;
12312 struct Row6;
12313 ˇ
12314 struct Row9.1;
12315 struct «Rowˇ»9.2;
12316 struct «ˇRow»9.3;
12317 struct Row8;
12318 struct Row9;
12319 «ˇ// something on bottom»
12320 struct Row10;"#},
12321 vec![
12322 DiffHunkStatus::added(),
12323 DiffHunkStatus::added(),
12324 DiffHunkStatus::added(),
12325 DiffHunkStatus::added(),
12326 DiffHunkStatus::added(),
12327 ],
12328 indoc! {r#"struct Row;
12329 ˇstruct Row1;
12330 struct Row2;
12331 ˇ
12332 struct Row4;
12333 ˇstruct Row5;
12334 struct Row6;
12335 ˇ
12336 ˇstruct Row8;
12337 struct Row9;
12338 ˇstruct Row10;"#},
12339 base_text,
12340 &mut cx,
12341 );
12342}
12343
12344#[gpui::test]
12345async fn test_modification_reverts(cx: &mut gpui::TestAppContext) {
12346 init_test(cx, |_| {});
12347 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12348 let base_text = indoc! {r#"
12349 struct Row;
12350 struct Row1;
12351 struct Row2;
12352
12353 struct Row4;
12354 struct Row5;
12355 struct Row6;
12356
12357 struct Row8;
12358 struct Row9;
12359 struct Row10;"#};
12360
12361 // Modification hunks behave the same as the addition ones.
12362 assert_hunk_revert(
12363 indoc! {r#"struct Row;
12364 struct Row1;
12365 struct Row33;
12366 ˇ
12367 struct Row4;
12368 struct Row5;
12369 struct Row6;
12370 ˇ
12371 struct Row99;
12372 struct Row9;
12373 struct Row10;"#},
12374 vec![DiffHunkStatus::modified(), DiffHunkStatus::modified()],
12375 indoc! {r#"struct Row;
12376 struct Row1;
12377 struct Row33;
12378 ˇ
12379 struct Row4;
12380 struct Row5;
12381 struct Row6;
12382 ˇ
12383 struct Row99;
12384 struct Row9;
12385 struct Row10;"#},
12386 base_text,
12387 &mut cx,
12388 );
12389 assert_hunk_revert(
12390 indoc! {r#"struct Row;
12391 struct Row1;
12392 struct Row33;
12393 «ˇ
12394 struct Row4;
12395 struct» Row5;
12396 «struct Row6;
12397 ˇ»
12398 struct Row99;
12399 struct Row9;
12400 struct Row10;"#},
12401 vec![DiffHunkStatus::modified(), DiffHunkStatus::modified()],
12402 indoc! {r#"struct Row;
12403 struct Row1;
12404 struct Row33;
12405 «ˇ
12406 struct Row4;
12407 struct» Row5;
12408 «struct Row6;
12409 ˇ»
12410 struct Row99;
12411 struct Row9;
12412 struct Row10;"#},
12413 base_text,
12414 &mut cx,
12415 );
12416
12417 assert_hunk_revert(
12418 indoc! {r#"ˇstruct Row1.1;
12419 struct Row1;
12420 «ˇstr»uct Row22;
12421
12422 struct ˇRow44;
12423 struct Row5;
12424 struct «Rˇ»ow66;ˇ
12425
12426 «struˇ»ct Row88;
12427 struct Row9;
12428 struct Row1011;ˇ"#},
12429 vec![
12430 DiffHunkStatus::modified(),
12431 DiffHunkStatus::modified(),
12432 DiffHunkStatus::modified(),
12433 DiffHunkStatus::modified(),
12434 DiffHunkStatus::modified(),
12435 DiffHunkStatus::modified(),
12436 ],
12437 indoc! {r#"struct Row;
12438 ˇstruct Row1;
12439 struct Row2;
12440 ˇ
12441 struct Row4;
12442 ˇstruct Row5;
12443 struct Row6;
12444 ˇ
12445 struct Row8;
12446 ˇstruct Row9;
12447 struct Row10;ˇ"#},
12448 base_text,
12449 &mut cx,
12450 );
12451}
12452
12453#[gpui::test]
12454async fn test_deleting_over_diff_hunk(cx: &mut gpui::TestAppContext) {
12455 init_test(cx, |_| {});
12456 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12457 let base_text = indoc! {r#"
12458 one
12459
12460 two
12461 three
12462 "#};
12463
12464 cx.set_diff_base(base_text);
12465 cx.set_state("\nˇ\n");
12466 cx.executor().run_until_parked();
12467 cx.update_editor(|editor, _window, cx| {
12468 editor.expand_selected_diff_hunks(cx);
12469 });
12470 cx.executor().run_until_parked();
12471 cx.update_editor(|editor, window, cx| {
12472 editor.backspace(&Default::default(), window, cx);
12473 });
12474 cx.run_until_parked();
12475 cx.assert_state_with_diff(
12476 indoc! {r#"
12477
12478 - two
12479 - threeˇ
12480 +
12481 "#}
12482 .to_string(),
12483 );
12484}
12485
12486#[gpui::test]
12487async fn test_deletion_reverts(cx: &mut gpui::TestAppContext) {
12488 init_test(cx, |_| {});
12489 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12490 let base_text = indoc! {r#"struct Row;
12491struct Row1;
12492struct Row2;
12493
12494struct Row4;
12495struct Row5;
12496struct Row6;
12497
12498struct Row8;
12499struct Row9;
12500struct Row10;"#};
12501
12502 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
12503 assert_hunk_revert(
12504 indoc! {r#"struct Row;
12505 struct Row2;
12506
12507 ˇstruct Row4;
12508 struct Row5;
12509 struct Row6;
12510 ˇ
12511 struct Row8;
12512 struct Row10;"#},
12513 vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()],
12514 indoc! {r#"struct Row;
12515 struct Row2;
12516
12517 ˇstruct Row4;
12518 struct Row5;
12519 struct Row6;
12520 ˇ
12521 struct Row8;
12522 struct Row10;"#},
12523 base_text,
12524 &mut cx,
12525 );
12526 assert_hunk_revert(
12527 indoc! {r#"struct Row;
12528 struct Row2;
12529
12530 «ˇstruct Row4;
12531 struct» Row5;
12532 «struct Row6;
12533 ˇ»
12534 struct Row8;
12535 struct Row10;"#},
12536 vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()],
12537 indoc! {r#"struct Row;
12538 struct Row2;
12539
12540 «ˇstruct Row4;
12541 struct» Row5;
12542 «struct Row6;
12543 ˇ»
12544 struct Row8;
12545 struct Row10;"#},
12546 base_text,
12547 &mut cx,
12548 );
12549
12550 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
12551 assert_hunk_revert(
12552 indoc! {r#"struct Row;
12553 ˇstruct Row2;
12554
12555 struct Row4;
12556 struct Row5;
12557 struct Row6;
12558
12559 struct Row8;ˇ
12560 struct Row10;"#},
12561 vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()],
12562 indoc! {r#"struct Row;
12563 struct Row1;
12564 ˇstruct Row2;
12565
12566 struct Row4;
12567 struct Row5;
12568 struct Row6;
12569
12570 struct Row8;ˇ
12571 struct Row9;
12572 struct Row10;"#},
12573 base_text,
12574 &mut cx,
12575 );
12576 assert_hunk_revert(
12577 indoc! {r#"struct Row;
12578 struct Row2«ˇ;
12579 struct Row4;
12580 struct» Row5;
12581 «struct Row6;
12582
12583 struct Row8;ˇ»
12584 struct Row10;"#},
12585 vec![
12586 DiffHunkStatus::removed(),
12587 DiffHunkStatus::removed(),
12588 DiffHunkStatus::removed(),
12589 ],
12590 indoc! {r#"struct Row;
12591 struct Row1;
12592 struct Row2«ˇ;
12593
12594 struct Row4;
12595 struct» Row5;
12596 «struct Row6;
12597
12598 struct Row8;ˇ»
12599 struct Row9;
12600 struct Row10;"#},
12601 base_text,
12602 &mut cx,
12603 );
12604}
12605
12606#[gpui::test]
12607async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
12608 init_test(cx, |_| {});
12609
12610 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
12611 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
12612 let base_text_3 =
12613 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
12614
12615 let text_1 = edit_first_char_of_every_line(base_text_1);
12616 let text_2 = edit_first_char_of_every_line(base_text_2);
12617 let text_3 = edit_first_char_of_every_line(base_text_3);
12618
12619 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
12620 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
12621 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
12622
12623 let multibuffer = cx.new(|cx| {
12624 let mut multibuffer = MultiBuffer::new(ReadWrite);
12625 multibuffer.push_excerpts(
12626 buffer_1.clone(),
12627 [
12628 ExcerptRange {
12629 context: Point::new(0, 0)..Point::new(3, 0),
12630 primary: None,
12631 },
12632 ExcerptRange {
12633 context: Point::new(5, 0)..Point::new(7, 0),
12634 primary: None,
12635 },
12636 ExcerptRange {
12637 context: Point::new(9, 0)..Point::new(10, 4),
12638 primary: None,
12639 },
12640 ],
12641 cx,
12642 );
12643 multibuffer.push_excerpts(
12644 buffer_2.clone(),
12645 [
12646 ExcerptRange {
12647 context: Point::new(0, 0)..Point::new(3, 0),
12648 primary: None,
12649 },
12650 ExcerptRange {
12651 context: Point::new(5, 0)..Point::new(7, 0),
12652 primary: None,
12653 },
12654 ExcerptRange {
12655 context: Point::new(9, 0)..Point::new(10, 4),
12656 primary: None,
12657 },
12658 ],
12659 cx,
12660 );
12661 multibuffer.push_excerpts(
12662 buffer_3.clone(),
12663 [
12664 ExcerptRange {
12665 context: Point::new(0, 0)..Point::new(3, 0),
12666 primary: None,
12667 },
12668 ExcerptRange {
12669 context: Point::new(5, 0)..Point::new(7, 0),
12670 primary: None,
12671 },
12672 ExcerptRange {
12673 context: Point::new(9, 0)..Point::new(10, 4),
12674 primary: None,
12675 },
12676 ],
12677 cx,
12678 );
12679 multibuffer
12680 });
12681
12682 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12683 editor.update_in(cx, |editor, _window, cx| {
12684 for (buffer, diff_base) in [
12685 (buffer_1.clone(), base_text_1),
12686 (buffer_2.clone(), base_text_2),
12687 (buffer_3.clone(), base_text_3),
12688 ] {
12689 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
12690 editor
12691 .buffer
12692 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
12693 }
12694 });
12695 cx.executor().run_until_parked();
12696
12697 editor.update_in(cx, |editor, window, cx| {
12698 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}");
12699 editor.select_all(&SelectAll, window, cx);
12700 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
12701 });
12702 cx.executor().run_until_parked();
12703
12704 // When all ranges are selected, all buffer hunks are reverted.
12705 editor.update(cx, |editor, cx| {
12706 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");
12707 });
12708 buffer_1.update(cx, |buffer, _| {
12709 assert_eq!(buffer.text(), base_text_1);
12710 });
12711 buffer_2.update(cx, |buffer, _| {
12712 assert_eq!(buffer.text(), base_text_2);
12713 });
12714 buffer_3.update(cx, |buffer, _| {
12715 assert_eq!(buffer.text(), base_text_3);
12716 });
12717
12718 editor.update_in(cx, |editor, window, cx| {
12719 editor.undo(&Default::default(), window, cx);
12720 });
12721
12722 editor.update_in(cx, |editor, window, cx| {
12723 editor.change_selections(None, window, cx, |s| {
12724 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
12725 });
12726 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
12727 });
12728
12729 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
12730 // but not affect buffer_2 and its related excerpts.
12731 editor.update(cx, |editor, cx| {
12732 assert_eq!(
12733 editor.text(cx),
12734 "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}"
12735 );
12736 });
12737 buffer_1.update(cx, |buffer, _| {
12738 assert_eq!(buffer.text(), base_text_1);
12739 });
12740 buffer_2.update(cx, |buffer, _| {
12741 assert_eq!(
12742 buffer.text(),
12743 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
12744 );
12745 });
12746 buffer_3.update(cx, |buffer, _| {
12747 assert_eq!(
12748 buffer.text(),
12749 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
12750 );
12751 });
12752
12753 fn edit_first_char_of_every_line(text: &str) -> String {
12754 text.split('\n')
12755 .map(|line| format!("X{}", &line[1..]))
12756 .collect::<Vec<_>>()
12757 .join("\n")
12758 }
12759}
12760
12761#[gpui::test]
12762async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
12763 init_test(cx, |_| {});
12764
12765 let cols = 4;
12766 let rows = 10;
12767 let sample_text_1 = sample_text(rows, cols, 'a');
12768 assert_eq!(
12769 sample_text_1,
12770 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
12771 );
12772 let sample_text_2 = sample_text(rows, cols, 'l');
12773 assert_eq!(
12774 sample_text_2,
12775 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
12776 );
12777 let sample_text_3 = sample_text(rows, cols, 'v');
12778 assert_eq!(
12779 sample_text_3,
12780 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
12781 );
12782
12783 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
12784 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
12785 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
12786
12787 let multi_buffer = cx.new(|cx| {
12788 let mut multibuffer = MultiBuffer::new(ReadWrite);
12789 multibuffer.push_excerpts(
12790 buffer_1.clone(),
12791 [
12792 ExcerptRange {
12793 context: Point::new(0, 0)..Point::new(3, 0),
12794 primary: None,
12795 },
12796 ExcerptRange {
12797 context: Point::new(5, 0)..Point::new(7, 0),
12798 primary: None,
12799 },
12800 ExcerptRange {
12801 context: Point::new(9, 0)..Point::new(10, 4),
12802 primary: None,
12803 },
12804 ],
12805 cx,
12806 );
12807 multibuffer.push_excerpts(
12808 buffer_2.clone(),
12809 [
12810 ExcerptRange {
12811 context: Point::new(0, 0)..Point::new(3, 0),
12812 primary: None,
12813 },
12814 ExcerptRange {
12815 context: Point::new(5, 0)..Point::new(7, 0),
12816 primary: None,
12817 },
12818 ExcerptRange {
12819 context: Point::new(9, 0)..Point::new(10, 4),
12820 primary: None,
12821 },
12822 ],
12823 cx,
12824 );
12825 multibuffer.push_excerpts(
12826 buffer_3.clone(),
12827 [
12828 ExcerptRange {
12829 context: Point::new(0, 0)..Point::new(3, 0),
12830 primary: None,
12831 },
12832 ExcerptRange {
12833 context: Point::new(5, 0)..Point::new(7, 0),
12834 primary: None,
12835 },
12836 ExcerptRange {
12837 context: Point::new(9, 0)..Point::new(10, 4),
12838 primary: None,
12839 },
12840 ],
12841 cx,
12842 );
12843 multibuffer
12844 });
12845
12846 let fs = FakeFs::new(cx.executor());
12847 fs.insert_tree(
12848 "/a",
12849 json!({
12850 "main.rs": sample_text_1,
12851 "other.rs": sample_text_2,
12852 "lib.rs": sample_text_3,
12853 }),
12854 )
12855 .await;
12856 let project = Project::test(fs, ["/a".as_ref()], cx).await;
12857 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12858 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12859 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
12860 Editor::new(
12861 EditorMode::Full,
12862 multi_buffer,
12863 Some(project.clone()),
12864 true,
12865 window,
12866 cx,
12867 )
12868 });
12869 let multibuffer_item_id = workspace
12870 .update(cx, |workspace, window, cx| {
12871 assert!(
12872 workspace.active_item(cx).is_none(),
12873 "active item should be None before the first item is added"
12874 );
12875 workspace.add_item_to_active_pane(
12876 Box::new(multi_buffer_editor.clone()),
12877 None,
12878 true,
12879 window,
12880 cx,
12881 );
12882 let active_item = workspace
12883 .active_item(cx)
12884 .expect("should have an active item after adding the multi buffer");
12885 assert!(
12886 !active_item.is_singleton(cx),
12887 "A multi buffer was expected to active after adding"
12888 );
12889 active_item.item_id()
12890 })
12891 .unwrap();
12892 cx.executor().run_until_parked();
12893
12894 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12895 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12896 s.select_ranges(Some(1..2))
12897 });
12898 editor.open_excerpts(&OpenExcerpts, window, cx);
12899 });
12900 cx.executor().run_until_parked();
12901 let first_item_id = workspace
12902 .update(cx, |workspace, window, cx| {
12903 let active_item = workspace
12904 .active_item(cx)
12905 .expect("should have an active item after navigating into the 1st buffer");
12906 let first_item_id = active_item.item_id();
12907 assert_ne!(
12908 first_item_id, multibuffer_item_id,
12909 "Should navigate into the 1st buffer and activate it"
12910 );
12911 assert!(
12912 active_item.is_singleton(cx),
12913 "New active item should be a singleton buffer"
12914 );
12915 assert_eq!(
12916 active_item
12917 .act_as::<Editor>(cx)
12918 .expect("should have navigated into an editor for the 1st buffer")
12919 .read(cx)
12920 .text(cx),
12921 sample_text_1
12922 );
12923
12924 workspace
12925 .go_back(workspace.active_pane().downgrade(), window, cx)
12926 .detach_and_log_err(cx);
12927
12928 first_item_id
12929 })
12930 .unwrap();
12931 cx.executor().run_until_parked();
12932 workspace
12933 .update(cx, |workspace, _, cx| {
12934 let active_item = workspace
12935 .active_item(cx)
12936 .expect("should have an active item after navigating back");
12937 assert_eq!(
12938 active_item.item_id(),
12939 multibuffer_item_id,
12940 "Should navigate back to the multi buffer"
12941 );
12942 assert!(!active_item.is_singleton(cx));
12943 })
12944 .unwrap();
12945
12946 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12947 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12948 s.select_ranges(Some(39..40))
12949 });
12950 editor.open_excerpts(&OpenExcerpts, window, cx);
12951 });
12952 cx.executor().run_until_parked();
12953 let second_item_id = workspace
12954 .update(cx, |workspace, window, cx| {
12955 let active_item = workspace
12956 .active_item(cx)
12957 .expect("should have an active item after navigating into the 2nd buffer");
12958 let second_item_id = active_item.item_id();
12959 assert_ne!(
12960 second_item_id, multibuffer_item_id,
12961 "Should navigate away from the multibuffer"
12962 );
12963 assert_ne!(
12964 second_item_id, first_item_id,
12965 "Should navigate into the 2nd buffer and activate it"
12966 );
12967 assert!(
12968 active_item.is_singleton(cx),
12969 "New active item should be a singleton buffer"
12970 );
12971 assert_eq!(
12972 active_item
12973 .act_as::<Editor>(cx)
12974 .expect("should have navigated into an editor")
12975 .read(cx)
12976 .text(cx),
12977 sample_text_2
12978 );
12979
12980 workspace
12981 .go_back(workspace.active_pane().downgrade(), window, cx)
12982 .detach_and_log_err(cx);
12983
12984 second_item_id
12985 })
12986 .unwrap();
12987 cx.executor().run_until_parked();
12988 workspace
12989 .update(cx, |workspace, _, cx| {
12990 let active_item = workspace
12991 .active_item(cx)
12992 .expect("should have an active item after navigating back from the 2nd buffer");
12993 assert_eq!(
12994 active_item.item_id(),
12995 multibuffer_item_id,
12996 "Should navigate back from the 2nd buffer to the multi buffer"
12997 );
12998 assert!(!active_item.is_singleton(cx));
12999 })
13000 .unwrap();
13001
13002 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13003 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
13004 s.select_ranges(Some(70..70))
13005 });
13006 editor.open_excerpts(&OpenExcerpts, window, cx);
13007 });
13008 cx.executor().run_until_parked();
13009 workspace
13010 .update(cx, |workspace, window, cx| {
13011 let active_item = workspace
13012 .active_item(cx)
13013 .expect("should have an active item after navigating into the 3rd buffer");
13014 let third_item_id = active_item.item_id();
13015 assert_ne!(
13016 third_item_id, multibuffer_item_id,
13017 "Should navigate into the 3rd buffer and activate it"
13018 );
13019 assert_ne!(third_item_id, first_item_id);
13020 assert_ne!(third_item_id, second_item_id);
13021 assert!(
13022 active_item.is_singleton(cx),
13023 "New active item should be a singleton buffer"
13024 );
13025 assert_eq!(
13026 active_item
13027 .act_as::<Editor>(cx)
13028 .expect("should have navigated into an editor")
13029 .read(cx)
13030 .text(cx),
13031 sample_text_3
13032 );
13033
13034 workspace
13035 .go_back(workspace.active_pane().downgrade(), window, cx)
13036 .detach_and_log_err(cx);
13037 })
13038 .unwrap();
13039 cx.executor().run_until_parked();
13040 workspace
13041 .update(cx, |workspace, _, cx| {
13042 let active_item = workspace
13043 .active_item(cx)
13044 .expect("should have an active item after navigating back from the 3rd buffer");
13045 assert_eq!(
13046 active_item.item_id(),
13047 multibuffer_item_id,
13048 "Should navigate back from the 3rd buffer to the multi buffer"
13049 );
13050 assert!(!active_item.is_singleton(cx));
13051 })
13052 .unwrap();
13053}
13054
13055#[gpui::test]
13056async fn test_toggle_selected_diff_hunks(
13057 executor: BackgroundExecutor,
13058 cx: &mut gpui::TestAppContext,
13059) {
13060 init_test(cx, |_| {});
13061
13062 let mut cx = EditorTestContext::new(cx).await;
13063
13064 let diff_base = r#"
13065 use some::mod;
13066
13067 const A: u32 = 42;
13068
13069 fn main() {
13070 println!("hello");
13071
13072 println!("world");
13073 }
13074 "#
13075 .unindent();
13076
13077 cx.set_state(
13078 &r#"
13079 use some::modified;
13080
13081 ˇ
13082 fn main() {
13083 println!("hello there");
13084
13085 println!("around the");
13086 println!("world");
13087 }
13088 "#
13089 .unindent(),
13090 );
13091
13092 cx.set_diff_base(&diff_base);
13093 executor.run_until_parked();
13094
13095 cx.update_editor(|editor, window, cx| {
13096 editor.go_to_next_hunk(&GoToHunk, window, cx);
13097 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13098 });
13099 executor.run_until_parked();
13100 cx.assert_state_with_diff(
13101 r#"
13102 use some::modified;
13103
13104
13105 fn main() {
13106 - println!("hello");
13107 + ˇ println!("hello there");
13108
13109 println!("around the");
13110 println!("world");
13111 }
13112 "#
13113 .unindent(),
13114 );
13115
13116 cx.update_editor(|editor, window, cx| {
13117 for _ in 0..2 {
13118 editor.go_to_next_hunk(&GoToHunk, window, cx);
13119 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13120 }
13121 });
13122 executor.run_until_parked();
13123 cx.assert_state_with_diff(
13124 r#"
13125 - use some::mod;
13126 + ˇuse some::modified;
13127
13128
13129 fn main() {
13130 - println!("hello");
13131 + println!("hello there");
13132
13133 + println!("around the");
13134 println!("world");
13135 }
13136 "#
13137 .unindent(),
13138 );
13139
13140 cx.update_editor(|editor, window, cx| {
13141 editor.go_to_next_hunk(&GoToHunk, window, cx);
13142 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13143 });
13144 executor.run_until_parked();
13145 cx.assert_state_with_diff(
13146 r#"
13147 - use some::mod;
13148 + use some::modified;
13149
13150 - const A: u32 = 42;
13151 ˇ
13152 fn main() {
13153 - println!("hello");
13154 + println!("hello there");
13155
13156 + println!("around the");
13157 println!("world");
13158 }
13159 "#
13160 .unindent(),
13161 );
13162
13163 cx.update_editor(|editor, window, cx| {
13164 editor.cancel(&Cancel, window, cx);
13165 });
13166
13167 cx.assert_state_with_diff(
13168 r#"
13169 use some::modified;
13170
13171 ˇ
13172 fn main() {
13173 println!("hello there");
13174
13175 println!("around the");
13176 println!("world");
13177 }
13178 "#
13179 .unindent(),
13180 );
13181}
13182
13183#[gpui::test]
13184async fn test_diff_base_change_with_expanded_diff_hunks(
13185 executor: BackgroundExecutor,
13186 cx: &mut gpui::TestAppContext,
13187) {
13188 init_test(cx, |_| {});
13189
13190 let mut cx = EditorTestContext::new(cx).await;
13191
13192 let diff_base = r#"
13193 use some::mod1;
13194 use some::mod2;
13195
13196 const A: u32 = 42;
13197 const B: u32 = 42;
13198 const C: u32 = 42;
13199
13200 fn main() {
13201 println!("hello");
13202
13203 println!("world");
13204 }
13205 "#
13206 .unindent();
13207
13208 cx.set_state(
13209 &r#"
13210 use some::mod2;
13211
13212 const A: u32 = 42;
13213 const C: u32 = 42;
13214
13215 fn main(ˇ) {
13216 //println!("hello");
13217
13218 println!("world");
13219 //
13220 //
13221 }
13222 "#
13223 .unindent(),
13224 );
13225
13226 cx.set_diff_base(&diff_base);
13227 executor.run_until_parked();
13228
13229 cx.update_editor(|editor, window, cx| {
13230 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13231 });
13232 executor.run_until_parked();
13233 cx.assert_state_with_diff(
13234 r#"
13235 - use some::mod1;
13236 use some::mod2;
13237
13238 const A: u32 = 42;
13239 - const B: u32 = 42;
13240 const C: u32 = 42;
13241
13242 fn main(ˇ) {
13243 - println!("hello");
13244 + //println!("hello");
13245
13246 println!("world");
13247 + //
13248 + //
13249 }
13250 "#
13251 .unindent(),
13252 );
13253
13254 cx.set_diff_base("new diff base!");
13255 executor.run_until_parked();
13256 cx.assert_state_with_diff(
13257 r#"
13258 - new diff base!
13259 + use some::mod2;
13260 +
13261 + const A: u32 = 42;
13262 + const C: u32 = 42;
13263 +
13264 + fn main(ˇ) {
13265 + //println!("hello");
13266 +
13267 + println!("world");
13268 + //
13269 + //
13270 + }
13271 "#
13272 .unindent(),
13273 );
13274}
13275
13276#[gpui::test]
13277async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) {
13278 init_test(cx, |_| {});
13279
13280 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
13281 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
13282 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
13283 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
13284 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
13285 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
13286
13287 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
13288 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
13289 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
13290
13291 let multi_buffer = cx.new(|cx| {
13292 let mut multibuffer = MultiBuffer::new(ReadWrite);
13293 multibuffer.push_excerpts(
13294 buffer_1.clone(),
13295 [
13296 ExcerptRange {
13297 context: Point::new(0, 0)..Point::new(3, 0),
13298 primary: None,
13299 },
13300 ExcerptRange {
13301 context: Point::new(5, 0)..Point::new(7, 0),
13302 primary: None,
13303 },
13304 ExcerptRange {
13305 context: Point::new(9, 0)..Point::new(10, 3),
13306 primary: None,
13307 },
13308 ],
13309 cx,
13310 );
13311 multibuffer.push_excerpts(
13312 buffer_2.clone(),
13313 [
13314 ExcerptRange {
13315 context: Point::new(0, 0)..Point::new(3, 0),
13316 primary: None,
13317 },
13318 ExcerptRange {
13319 context: Point::new(5, 0)..Point::new(7, 0),
13320 primary: None,
13321 },
13322 ExcerptRange {
13323 context: Point::new(9, 0)..Point::new(10, 3),
13324 primary: None,
13325 },
13326 ],
13327 cx,
13328 );
13329 multibuffer.push_excerpts(
13330 buffer_3.clone(),
13331 [
13332 ExcerptRange {
13333 context: Point::new(0, 0)..Point::new(3, 0),
13334 primary: None,
13335 },
13336 ExcerptRange {
13337 context: Point::new(5, 0)..Point::new(7, 0),
13338 primary: None,
13339 },
13340 ExcerptRange {
13341 context: Point::new(9, 0)..Point::new(10, 3),
13342 primary: None,
13343 },
13344 ],
13345 cx,
13346 );
13347 multibuffer
13348 });
13349
13350 let editor = cx.add_window(|window, cx| {
13351 Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
13352 });
13353 editor
13354 .update(cx, |editor, _window, cx| {
13355 for (buffer, diff_base) in [
13356 (buffer_1.clone(), file_1_old),
13357 (buffer_2.clone(), file_2_old),
13358 (buffer_3.clone(), file_3_old),
13359 ] {
13360 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
13361 editor
13362 .buffer
13363 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
13364 }
13365 })
13366 .unwrap();
13367
13368 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13369 cx.run_until_parked();
13370
13371 cx.assert_editor_state(
13372 &"
13373 ˇaaa
13374 ccc
13375 ddd
13376
13377 ggg
13378 hhh
13379
13380
13381 lll
13382 mmm
13383 NNN
13384
13385 qqq
13386 rrr
13387
13388 uuu
13389 111
13390 222
13391 333
13392
13393 666
13394 777
13395
13396 000
13397 !!!"
13398 .unindent(),
13399 );
13400
13401 cx.update_editor(|editor, window, cx| {
13402 editor.select_all(&SelectAll, window, cx);
13403 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13404 });
13405 cx.executor().run_until_parked();
13406
13407 cx.assert_state_with_diff(
13408 "
13409 «aaa
13410 - bbb
13411 ccc
13412 ddd
13413
13414 ggg
13415 hhh
13416
13417
13418 lll
13419 mmm
13420 - nnn
13421 + NNN
13422
13423 qqq
13424 rrr
13425
13426 uuu
13427 111
13428 222
13429 333
13430
13431 + 666
13432 777
13433
13434 000
13435 !!!ˇ»"
13436 .unindent(),
13437 );
13438}
13439
13440#[gpui::test]
13441async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext) {
13442 init_test(cx, |_| {});
13443
13444 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
13445 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
13446
13447 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
13448 let multi_buffer = cx.new(|cx| {
13449 let mut multibuffer = MultiBuffer::new(ReadWrite);
13450 multibuffer.push_excerpts(
13451 buffer.clone(),
13452 [
13453 ExcerptRange {
13454 context: Point::new(0, 0)..Point::new(2, 0),
13455 primary: None,
13456 },
13457 ExcerptRange {
13458 context: Point::new(4, 0)..Point::new(7, 0),
13459 primary: None,
13460 },
13461 ExcerptRange {
13462 context: Point::new(9, 0)..Point::new(10, 0),
13463 primary: None,
13464 },
13465 ],
13466 cx,
13467 );
13468 multibuffer
13469 });
13470
13471 let editor = cx.add_window(|window, cx| {
13472 Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
13473 });
13474 editor
13475 .update(cx, |editor, _window, cx| {
13476 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
13477 editor
13478 .buffer
13479 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
13480 })
13481 .unwrap();
13482
13483 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13484 cx.run_until_parked();
13485
13486 cx.update_editor(|editor, window, cx| {
13487 editor.expand_all_diff_hunks(&Default::default(), window, cx)
13488 });
13489 cx.executor().run_until_parked();
13490
13491 // When the start of a hunk coincides with the start of its excerpt,
13492 // the hunk is expanded. When the start of a a hunk is earlier than
13493 // the start of its excerpt, the hunk is not expanded.
13494 cx.assert_state_with_diff(
13495 "
13496 ˇaaa
13497 - bbb
13498 + BBB
13499
13500 - ddd
13501 - eee
13502 + DDD
13503 + EEE
13504 fff
13505
13506 iii
13507 "
13508 .unindent(),
13509 );
13510}
13511
13512#[gpui::test]
13513async fn test_edits_around_expanded_insertion_hunks(
13514 executor: BackgroundExecutor,
13515 cx: &mut gpui::TestAppContext,
13516) {
13517 init_test(cx, |_| {});
13518
13519 let mut cx = EditorTestContext::new(cx).await;
13520
13521 let diff_base = r#"
13522 use some::mod1;
13523 use some::mod2;
13524
13525 const A: u32 = 42;
13526
13527 fn main() {
13528 println!("hello");
13529
13530 println!("world");
13531 }
13532 "#
13533 .unindent();
13534 executor.run_until_parked();
13535 cx.set_state(
13536 &r#"
13537 use some::mod1;
13538 use some::mod2;
13539
13540 const A: u32 = 42;
13541 const B: u32 = 42;
13542 const C: u32 = 42;
13543 ˇ
13544
13545 fn main() {
13546 println!("hello");
13547
13548 println!("world");
13549 }
13550 "#
13551 .unindent(),
13552 );
13553
13554 cx.set_diff_base(&diff_base);
13555 executor.run_until_parked();
13556
13557 cx.update_editor(|editor, window, cx| {
13558 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13559 });
13560 executor.run_until_parked();
13561
13562 cx.assert_state_with_diff(
13563 r#"
13564 use some::mod1;
13565 use some::mod2;
13566
13567 const A: u32 = 42;
13568 + const B: u32 = 42;
13569 + const C: u32 = 42;
13570 + ˇ
13571
13572 fn main() {
13573 println!("hello");
13574
13575 println!("world");
13576 }
13577 "#
13578 .unindent(),
13579 );
13580
13581 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
13582 executor.run_until_parked();
13583
13584 cx.assert_state_with_diff(
13585 r#"
13586 use some::mod1;
13587 use some::mod2;
13588
13589 const A: u32 = 42;
13590 + const B: u32 = 42;
13591 + const C: u32 = 42;
13592 + const D: u32 = 42;
13593 + ˇ
13594
13595 fn main() {
13596 println!("hello");
13597
13598 println!("world");
13599 }
13600 "#
13601 .unindent(),
13602 );
13603
13604 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
13605 executor.run_until_parked();
13606
13607 cx.assert_state_with_diff(
13608 r#"
13609 use some::mod1;
13610 use some::mod2;
13611
13612 const A: u32 = 42;
13613 + const B: u32 = 42;
13614 + const C: u32 = 42;
13615 + const D: u32 = 42;
13616 + const E: u32 = 42;
13617 + ˇ
13618
13619 fn main() {
13620 println!("hello");
13621
13622 println!("world");
13623 }
13624 "#
13625 .unindent(),
13626 );
13627
13628 cx.update_editor(|editor, window, cx| {
13629 editor.delete_line(&DeleteLine, window, cx);
13630 });
13631 executor.run_until_parked();
13632
13633 cx.assert_state_with_diff(
13634 r#"
13635 use some::mod1;
13636 use some::mod2;
13637
13638 const A: u32 = 42;
13639 + const B: u32 = 42;
13640 + const C: u32 = 42;
13641 + const D: u32 = 42;
13642 + const E: u32 = 42;
13643 ˇ
13644 fn main() {
13645 println!("hello");
13646
13647 println!("world");
13648 }
13649 "#
13650 .unindent(),
13651 );
13652
13653 cx.update_editor(|editor, window, cx| {
13654 editor.move_up(&MoveUp, window, cx);
13655 editor.delete_line(&DeleteLine, window, cx);
13656 editor.move_up(&MoveUp, window, cx);
13657 editor.delete_line(&DeleteLine, window, cx);
13658 editor.move_up(&MoveUp, window, cx);
13659 editor.delete_line(&DeleteLine, window, cx);
13660 });
13661 executor.run_until_parked();
13662 cx.assert_state_with_diff(
13663 r#"
13664 use some::mod1;
13665 use some::mod2;
13666
13667 const A: u32 = 42;
13668 + const B: u32 = 42;
13669 ˇ
13670 fn main() {
13671 println!("hello");
13672
13673 println!("world");
13674 }
13675 "#
13676 .unindent(),
13677 );
13678
13679 cx.update_editor(|editor, window, cx| {
13680 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
13681 editor.delete_line(&DeleteLine, window, cx);
13682 });
13683 executor.run_until_parked();
13684 cx.assert_state_with_diff(
13685 r#"
13686 ˇ
13687 fn main() {
13688 println!("hello");
13689
13690 println!("world");
13691 }
13692 "#
13693 .unindent(),
13694 );
13695}
13696
13697#[gpui::test]
13698async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
13699 init_test(cx, |_| {});
13700
13701 let mut cx = EditorTestContext::new(cx).await;
13702 cx.set_diff_base(indoc! { "
13703 one
13704 two
13705 three
13706 four
13707 five
13708 "
13709 });
13710 cx.set_state(indoc! { "
13711 one
13712 ˇthree
13713 five
13714 "});
13715 cx.run_until_parked();
13716 cx.update_editor(|editor, window, cx| {
13717 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13718 });
13719 cx.assert_state_with_diff(
13720 indoc! { "
13721 one
13722 - two
13723 ˇthree
13724 - four
13725 five
13726 "}
13727 .to_string(),
13728 );
13729 cx.update_editor(|editor, window, cx| {
13730 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13731 });
13732
13733 cx.assert_state_with_diff(
13734 indoc! { "
13735 one
13736 ˇthree
13737 five
13738 "}
13739 .to_string(),
13740 );
13741
13742 cx.set_state(indoc! { "
13743 one
13744 ˇTWO
13745 three
13746 four
13747 five
13748 "});
13749 cx.run_until_parked();
13750 cx.update_editor(|editor, window, cx| {
13751 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13752 });
13753
13754 cx.assert_state_with_diff(
13755 indoc! { "
13756 one
13757 - two
13758 + ˇTWO
13759 three
13760 four
13761 five
13762 "}
13763 .to_string(),
13764 );
13765 cx.update_editor(|editor, window, cx| {
13766 editor.move_up(&Default::default(), window, cx);
13767 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13768 });
13769 cx.assert_state_with_diff(
13770 indoc! { "
13771 one
13772 ˇTWO
13773 three
13774 four
13775 five
13776 "}
13777 .to_string(),
13778 );
13779}
13780
13781#[gpui::test]
13782async fn test_edits_around_expanded_deletion_hunks(
13783 executor: BackgroundExecutor,
13784 cx: &mut gpui::TestAppContext,
13785) {
13786 init_test(cx, |_| {});
13787
13788 let mut cx = EditorTestContext::new(cx).await;
13789
13790 let diff_base = r#"
13791 use some::mod1;
13792 use some::mod2;
13793
13794 const A: u32 = 42;
13795 const B: u32 = 42;
13796 const C: u32 = 42;
13797
13798
13799 fn main() {
13800 println!("hello");
13801
13802 println!("world");
13803 }
13804 "#
13805 .unindent();
13806 executor.run_until_parked();
13807 cx.set_state(
13808 &r#"
13809 use some::mod1;
13810 use some::mod2;
13811
13812 ˇconst B: u32 = 42;
13813 const C: u32 = 42;
13814
13815
13816 fn main() {
13817 println!("hello");
13818
13819 println!("world");
13820 }
13821 "#
13822 .unindent(),
13823 );
13824
13825 cx.set_diff_base(&diff_base);
13826 executor.run_until_parked();
13827
13828 cx.update_editor(|editor, window, cx| {
13829 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13830 });
13831 executor.run_until_parked();
13832
13833 cx.assert_state_with_diff(
13834 r#"
13835 use some::mod1;
13836 use some::mod2;
13837
13838 - const A: u32 = 42;
13839 ˇconst B: u32 = 42;
13840 const C: u32 = 42;
13841
13842
13843 fn main() {
13844 println!("hello");
13845
13846 println!("world");
13847 }
13848 "#
13849 .unindent(),
13850 );
13851
13852 cx.update_editor(|editor, window, cx| {
13853 editor.delete_line(&DeleteLine, window, cx);
13854 });
13855 executor.run_until_parked();
13856 cx.assert_state_with_diff(
13857 r#"
13858 use some::mod1;
13859 use some::mod2;
13860
13861 - const A: u32 = 42;
13862 - const B: u32 = 42;
13863 ˇconst C: u32 = 42;
13864
13865
13866 fn main() {
13867 println!("hello");
13868
13869 println!("world");
13870 }
13871 "#
13872 .unindent(),
13873 );
13874
13875 cx.update_editor(|editor, window, cx| {
13876 editor.delete_line(&DeleteLine, window, cx);
13877 });
13878 executor.run_until_parked();
13879 cx.assert_state_with_diff(
13880 r#"
13881 use some::mod1;
13882 use some::mod2;
13883
13884 - const A: u32 = 42;
13885 - const B: u32 = 42;
13886 - const C: u32 = 42;
13887 ˇ
13888
13889 fn main() {
13890 println!("hello");
13891
13892 println!("world");
13893 }
13894 "#
13895 .unindent(),
13896 );
13897
13898 cx.update_editor(|editor, window, cx| {
13899 editor.handle_input("replacement", window, cx);
13900 });
13901 executor.run_until_parked();
13902 cx.assert_state_with_diff(
13903 r#"
13904 use some::mod1;
13905 use some::mod2;
13906
13907 - const A: u32 = 42;
13908 - const B: u32 = 42;
13909 - const C: u32 = 42;
13910 -
13911 + replacementˇ
13912
13913 fn main() {
13914 println!("hello");
13915
13916 println!("world");
13917 }
13918 "#
13919 .unindent(),
13920 );
13921}
13922
13923#[gpui::test]
13924async fn test_backspace_after_deletion_hunk(
13925 executor: BackgroundExecutor,
13926 cx: &mut gpui::TestAppContext,
13927) {
13928 init_test(cx, |_| {});
13929
13930 let mut cx = EditorTestContext::new(cx).await;
13931
13932 let base_text = r#"
13933 one
13934 two
13935 three
13936 four
13937 five
13938 "#
13939 .unindent();
13940 executor.run_until_parked();
13941 cx.set_state(
13942 &r#"
13943 one
13944 two
13945 fˇour
13946 five
13947 "#
13948 .unindent(),
13949 );
13950
13951 cx.set_diff_base(&base_text);
13952 executor.run_until_parked();
13953
13954 cx.update_editor(|editor, window, cx| {
13955 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13956 });
13957 executor.run_until_parked();
13958
13959 cx.assert_state_with_diff(
13960 r#"
13961 one
13962 two
13963 - three
13964 fˇour
13965 five
13966 "#
13967 .unindent(),
13968 );
13969
13970 cx.update_editor(|editor, window, cx| {
13971 editor.backspace(&Backspace, window, cx);
13972 editor.backspace(&Backspace, window, cx);
13973 });
13974 executor.run_until_parked();
13975 cx.assert_state_with_diff(
13976 r#"
13977 one
13978 two
13979 - threeˇ
13980 - four
13981 + our
13982 five
13983 "#
13984 .unindent(),
13985 );
13986}
13987
13988#[gpui::test]
13989async fn test_edit_after_expanded_modification_hunk(
13990 executor: BackgroundExecutor,
13991 cx: &mut gpui::TestAppContext,
13992) {
13993 init_test(cx, |_| {});
13994
13995 let mut cx = EditorTestContext::new(cx).await;
13996
13997 let diff_base = r#"
13998 use some::mod1;
13999 use some::mod2;
14000
14001 const A: u32 = 42;
14002 const B: u32 = 42;
14003 const C: u32 = 42;
14004 const D: u32 = 42;
14005
14006
14007 fn main() {
14008 println!("hello");
14009
14010 println!("world");
14011 }"#
14012 .unindent();
14013
14014 cx.set_state(
14015 &r#"
14016 use some::mod1;
14017 use some::mod2;
14018
14019 const A: u32 = 42;
14020 const B: u32 = 42;
14021 const C: u32 = 43ˇ
14022 const D: u32 = 42;
14023
14024
14025 fn main() {
14026 println!("hello");
14027
14028 println!("world");
14029 }"#
14030 .unindent(),
14031 );
14032
14033 cx.set_diff_base(&diff_base);
14034 executor.run_until_parked();
14035 cx.update_editor(|editor, window, cx| {
14036 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
14037 });
14038 executor.run_until_parked();
14039
14040 cx.assert_state_with_diff(
14041 r#"
14042 use some::mod1;
14043 use some::mod2;
14044
14045 const A: u32 = 42;
14046 const B: u32 = 42;
14047 - const C: u32 = 42;
14048 + const C: u32 = 43ˇ
14049 const D: u32 = 42;
14050
14051
14052 fn main() {
14053 println!("hello");
14054
14055 println!("world");
14056 }"#
14057 .unindent(),
14058 );
14059
14060 cx.update_editor(|editor, window, cx| {
14061 editor.handle_input("\nnew_line\n", window, cx);
14062 });
14063 executor.run_until_parked();
14064
14065 cx.assert_state_with_diff(
14066 r#"
14067 use some::mod1;
14068 use some::mod2;
14069
14070 const A: u32 = 42;
14071 const B: u32 = 42;
14072 - const C: u32 = 42;
14073 + const C: u32 = 43
14074 + new_line
14075 + ˇ
14076 const D: u32 = 42;
14077
14078
14079 fn main() {
14080 println!("hello");
14081
14082 println!("world");
14083 }"#
14084 .unindent(),
14085 );
14086}
14087
14088#[gpui::test]
14089async fn test_stage_and_unstage_added_file_hunk(
14090 executor: BackgroundExecutor,
14091 cx: &mut gpui::TestAppContext,
14092) {
14093 init_test(cx, |_| {});
14094
14095 let mut cx = EditorTestContext::new(cx).await;
14096 cx.update_editor(|editor, _, cx| {
14097 editor.set_expand_all_diff_hunks(cx);
14098 });
14099
14100 let working_copy = r#"
14101 ˇfn main() {
14102 println!("hello, world!");
14103 }
14104 "#
14105 .unindent();
14106
14107 cx.set_state(&working_copy);
14108 executor.run_until_parked();
14109
14110 cx.assert_state_with_diff(
14111 r#"
14112 + ˇfn main() {
14113 + println!("hello, world!");
14114 + }
14115 "#
14116 .unindent(),
14117 );
14118 cx.assert_index_text(None);
14119
14120 cx.update_editor(|editor, window, cx| {
14121 editor.toggle_staged_selected_diff_hunks(&ToggleStagedSelectedDiffHunks, window, cx);
14122 });
14123 executor.run_until_parked();
14124 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
14125 cx.assert_state_with_diff(
14126 r#"
14127 + ˇfn main() {
14128 + println!("hello, world!");
14129 + }
14130 "#
14131 .unindent(),
14132 );
14133
14134 cx.update_editor(|editor, window, cx| {
14135 editor.toggle_staged_selected_diff_hunks(&ToggleStagedSelectedDiffHunks, window, cx);
14136 });
14137 executor.run_until_parked();
14138 cx.assert_index_text(None);
14139}
14140
14141async fn setup_indent_guides_editor(
14142 text: &str,
14143 cx: &mut gpui::TestAppContext,
14144) -> (BufferId, EditorTestContext) {
14145 init_test(cx, |_| {});
14146
14147 let mut cx = EditorTestContext::new(cx).await;
14148
14149 let buffer_id = cx.update_editor(|editor, window, cx| {
14150 editor.set_text(text, window, cx);
14151 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
14152
14153 buffer_ids[0]
14154 });
14155
14156 (buffer_id, cx)
14157}
14158
14159fn assert_indent_guides(
14160 range: Range<u32>,
14161 expected: Vec<IndentGuide>,
14162 active_indices: Option<Vec<usize>>,
14163 cx: &mut EditorTestContext,
14164) {
14165 let indent_guides = cx.update_editor(|editor, window, cx| {
14166 let snapshot = editor.snapshot(window, cx).display_snapshot;
14167 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
14168 editor,
14169 MultiBufferRow(range.start)..MultiBufferRow(range.end),
14170 true,
14171 &snapshot,
14172 cx,
14173 );
14174
14175 indent_guides.sort_by(|a, b| {
14176 a.depth.cmp(&b.depth).then(
14177 a.start_row
14178 .cmp(&b.start_row)
14179 .then(a.end_row.cmp(&b.end_row)),
14180 )
14181 });
14182 indent_guides
14183 });
14184
14185 if let Some(expected) = active_indices {
14186 let active_indices = cx.update_editor(|editor, window, cx| {
14187 let snapshot = editor.snapshot(window, cx).display_snapshot;
14188 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
14189 });
14190
14191 assert_eq!(
14192 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
14193 expected,
14194 "Active indent guide indices do not match"
14195 );
14196 }
14197
14198 assert_eq!(indent_guides, expected, "Indent guides do not match");
14199}
14200
14201fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
14202 IndentGuide {
14203 buffer_id,
14204 start_row: MultiBufferRow(start_row),
14205 end_row: MultiBufferRow(end_row),
14206 depth,
14207 tab_size: 4,
14208 settings: IndentGuideSettings {
14209 enabled: true,
14210 line_width: 1,
14211 active_line_width: 1,
14212 ..Default::default()
14213 },
14214 }
14215}
14216
14217#[gpui::test]
14218async fn test_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
14219 let (buffer_id, mut cx) = setup_indent_guides_editor(
14220 &"
14221 fn main() {
14222 let a = 1;
14223 }"
14224 .unindent(),
14225 cx,
14226 )
14227 .await;
14228
14229 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
14230}
14231
14232#[gpui::test]
14233async fn test_indent_guide_simple_block(cx: &mut gpui::TestAppContext) {
14234 let (buffer_id, mut cx) = setup_indent_guides_editor(
14235 &"
14236 fn main() {
14237 let a = 1;
14238 let b = 2;
14239 }"
14240 .unindent(),
14241 cx,
14242 )
14243 .await;
14244
14245 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
14246}
14247
14248#[gpui::test]
14249async fn test_indent_guide_nested(cx: &mut gpui::TestAppContext) {
14250 let (buffer_id, mut cx) = setup_indent_guides_editor(
14251 &"
14252 fn main() {
14253 let a = 1;
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 0..8,
14267 vec![
14268 indent_guide(buffer_id, 1, 6, 0),
14269 indent_guide(buffer_id, 3, 3, 1),
14270 indent_guide(buffer_id, 5, 5, 1),
14271 ],
14272 None,
14273 &mut cx,
14274 );
14275}
14276
14277#[gpui::test]
14278async fn test_indent_guide_tab(cx: &mut gpui::TestAppContext) {
14279 let (buffer_id, mut cx) = setup_indent_guides_editor(
14280 &"
14281 fn main() {
14282 let a = 1;
14283 let b = 2;
14284 let c = 3;
14285 }"
14286 .unindent(),
14287 cx,
14288 )
14289 .await;
14290
14291 assert_indent_guides(
14292 0..5,
14293 vec![
14294 indent_guide(buffer_id, 1, 3, 0),
14295 indent_guide(buffer_id, 2, 2, 1),
14296 ],
14297 None,
14298 &mut cx,
14299 );
14300}
14301
14302#[gpui::test]
14303async fn test_indent_guide_continues_on_empty_line(cx: &mut gpui::TestAppContext) {
14304 let (buffer_id, mut cx) = setup_indent_guides_editor(
14305 &"
14306 fn main() {
14307 let a = 1;
14308
14309 let c = 3;
14310 }"
14311 .unindent(),
14312 cx,
14313 )
14314 .await;
14315
14316 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
14317}
14318
14319#[gpui::test]
14320async fn test_indent_guide_complex(cx: &mut gpui::TestAppContext) {
14321 let (buffer_id, mut cx) = setup_indent_guides_editor(
14322 &"
14323 fn main() {
14324 let a = 1;
14325
14326 let c = 3;
14327
14328 if a == 3 {
14329 let b = 2;
14330 } else {
14331 let c = 3;
14332 }
14333 }"
14334 .unindent(),
14335 cx,
14336 )
14337 .await;
14338
14339 assert_indent_guides(
14340 0..11,
14341 vec![
14342 indent_guide(buffer_id, 1, 9, 0),
14343 indent_guide(buffer_id, 6, 6, 1),
14344 indent_guide(buffer_id, 8, 8, 1),
14345 ],
14346 None,
14347 &mut cx,
14348 );
14349}
14350
14351#[gpui::test]
14352async fn test_indent_guide_starts_off_screen(cx: &mut gpui::TestAppContext) {
14353 let (buffer_id, mut cx) = setup_indent_guides_editor(
14354 &"
14355 fn main() {
14356 let a = 1;
14357
14358 let c = 3;
14359
14360 if a == 3 {
14361 let b = 2;
14362 } else {
14363 let c = 3;
14364 }
14365 }"
14366 .unindent(),
14367 cx,
14368 )
14369 .await;
14370
14371 assert_indent_guides(
14372 1..11,
14373 vec![
14374 indent_guide(buffer_id, 1, 9, 0),
14375 indent_guide(buffer_id, 6, 6, 1),
14376 indent_guide(buffer_id, 8, 8, 1),
14377 ],
14378 None,
14379 &mut cx,
14380 );
14381}
14382
14383#[gpui::test]
14384async fn test_indent_guide_ends_off_screen(cx: &mut gpui::TestAppContext) {
14385 let (buffer_id, mut cx) = setup_indent_guides_editor(
14386 &"
14387 fn main() {
14388 let a = 1;
14389
14390 let c = 3;
14391
14392 if a == 3 {
14393 let b = 2;
14394 } else {
14395 let c = 3;
14396 }
14397 }"
14398 .unindent(),
14399 cx,
14400 )
14401 .await;
14402
14403 assert_indent_guides(
14404 1..10,
14405 vec![
14406 indent_guide(buffer_id, 1, 9, 0),
14407 indent_guide(buffer_id, 6, 6, 1),
14408 indent_guide(buffer_id, 8, 8, 1),
14409 ],
14410 None,
14411 &mut cx,
14412 );
14413}
14414
14415#[gpui::test]
14416async fn test_indent_guide_without_brackets(cx: &mut gpui::TestAppContext) {
14417 let (buffer_id, mut cx) = setup_indent_guides_editor(
14418 &"
14419 block1
14420 block2
14421 block3
14422 block4
14423 block2
14424 block1
14425 block1"
14426 .unindent(),
14427 cx,
14428 )
14429 .await;
14430
14431 assert_indent_guides(
14432 1..10,
14433 vec![
14434 indent_guide(buffer_id, 1, 4, 0),
14435 indent_guide(buffer_id, 2, 3, 1),
14436 indent_guide(buffer_id, 3, 3, 2),
14437 ],
14438 None,
14439 &mut cx,
14440 );
14441}
14442
14443#[gpui::test]
14444async fn test_indent_guide_ends_before_empty_line(cx: &mut gpui::TestAppContext) {
14445 let (buffer_id, mut cx) = setup_indent_guides_editor(
14446 &"
14447 block1
14448 block2
14449 block3
14450
14451 block1
14452 block1"
14453 .unindent(),
14454 cx,
14455 )
14456 .await;
14457
14458 assert_indent_guides(
14459 0..6,
14460 vec![
14461 indent_guide(buffer_id, 1, 2, 0),
14462 indent_guide(buffer_id, 2, 2, 1),
14463 ],
14464 None,
14465 &mut cx,
14466 );
14467}
14468
14469#[gpui::test]
14470async fn test_indent_guide_continuing_off_screen(cx: &mut gpui::TestAppContext) {
14471 let (buffer_id, mut cx) = setup_indent_guides_editor(
14472 &"
14473 block1
14474
14475
14476
14477 block2
14478 "
14479 .unindent(),
14480 cx,
14481 )
14482 .await;
14483
14484 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
14485}
14486
14487#[gpui::test]
14488async fn test_indent_guide_tabs(cx: &mut gpui::TestAppContext) {
14489 let (buffer_id, mut cx) = setup_indent_guides_editor(
14490 &"
14491 def a:
14492 \tb = 3
14493 \tif True:
14494 \t\tc = 4
14495 \t\td = 5
14496 \tprint(b)
14497 "
14498 .unindent(),
14499 cx,
14500 )
14501 .await;
14502
14503 assert_indent_guides(
14504 0..6,
14505 vec![
14506 indent_guide(buffer_id, 1, 6, 0),
14507 indent_guide(buffer_id, 3, 4, 1),
14508 ],
14509 None,
14510 &mut cx,
14511 );
14512}
14513
14514#[gpui::test]
14515async fn test_active_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
14516 let (buffer_id, mut cx) = setup_indent_guides_editor(
14517 &"
14518 fn main() {
14519 let a = 1;
14520 }"
14521 .unindent(),
14522 cx,
14523 )
14524 .await;
14525
14526 cx.update_editor(|editor, window, cx| {
14527 editor.change_selections(None, window, cx, |s| {
14528 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14529 });
14530 });
14531
14532 assert_indent_guides(
14533 0..3,
14534 vec![indent_guide(buffer_id, 1, 1, 0)],
14535 Some(vec![0]),
14536 &mut cx,
14537 );
14538}
14539
14540#[gpui::test]
14541async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppContext) {
14542 let (buffer_id, mut cx) = setup_indent_guides_editor(
14543 &"
14544 fn main() {
14545 if 1 == 2 {
14546 let a = 1;
14547 }
14548 }"
14549 .unindent(),
14550 cx,
14551 )
14552 .await;
14553
14554 cx.update_editor(|editor, window, cx| {
14555 editor.change_selections(None, window, cx, |s| {
14556 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14557 });
14558 });
14559
14560 assert_indent_guides(
14561 0..4,
14562 vec![
14563 indent_guide(buffer_id, 1, 3, 0),
14564 indent_guide(buffer_id, 2, 2, 1),
14565 ],
14566 Some(vec![1]),
14567 &mut cx,
14568 );
14569
14570 cx.update_editor(|editor, window, cx| {
14571 editor.change_selections(None, window, cx, |s| {
14572 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
14573 });
14574 });
14575
14576 assert_indent_guides(
14577 0..4,
14578 vec![
14579 indent_guide(buffer_id, 1, 3, 0),
14580 indent_guide(buffer_id, 2, 2, 1),
14581 ],
14582 Some(vec![1]),
14583 &mut cx,
14584 );
14585
14586 cx.update_editor(|editor, window, cx| {
14587 editor.change_selections(None, window, cx, |s| {
14588 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
14589 });
14590 });
14591
14592 assert_indent_guides(
14593 0..4,
14594 vec![
14595 indent_guide(buffer_id, 1, 3, 0),
14596 indent_guide(buffer_id, 2, 2, 1),
14597 ],
14598 Some(vec![0]),
14599 &mut cx,
14600 );
14601}
14602
14603#[gpui::test]
14604async fn test_active_indent_guide_empty_line(cx: &mut gpui::TestAppContext) {
14605 let (buffer_id, mut cx) = setup_indent_guides_editor(
14606 &"
14607 fn main() {
14608 let a = 1;
14609
14610 let b = 2;
14611 }"
14612 .unindent(),
14613 cx,
14614 )
14615 .await;
14616
14617 cx.update_editor(|editor, window, cx| {
14618 editor.change_selections(None, window, cx, |s| {
14619 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
14620 });
14621 });
14622
14623 assert_indent_guides(
14624 0..5,
14625 vec![indent_guide(buffer_id, 1, 3, 0)],
14626 Some(vec![0]),
14627 &mut cx,
14628 );
14629}
14630
14631#[gpui::test]
14632async fn test_active_indent_guide_non_matching_indent(cx: &mut gpui::TestAppContext) {
14633 let (buffer_id, mut cx) = setup_indent_guides_editor(
14634 &"
14635 def m:
14636 a = 1
14637 pass"
14638 .unindent(),
14639 cx,
14640 )
14641 .await;
14642
14643 cx.update_editor(|editor, window, cx| {
14644 editor.change_selections(None, window, cx, |s| {
14645 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14646 });
14647 });
14648
14649 assert_indent_guides(
14650 0..3,
14651 vec![indent_guide(buffer_id, 1, 2, 0)],
14652 Some(vec![0]),
14653 &mut cx,
14654 );
14655}
14656
14657#[gpui::test]
14658async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut gpui::TestAppContext) {
14659 init_test(cx, |_| {});
14660 let mut cx = EditorTestContext::new(cx).await;
14661 let text = indoc! {
14662 "
14663 impl A {
14664 fn b() {
14665 0;
14666 3;
14667 5;
14668 6;
14669 7;
14670 }
14671 }
14672 "
14673 };
14674 let base_text = indoc! {
14675 "
14676 impl A {
14677 fn b() {
14678 0;
14679 1;
14680 2;
14681 3;
14682 4;
14683 }
14684 fn c() {
14685 5;
14686 6;
14687 7;
14688 }
14689 }
14690 "
14691 };
14692
14693 cx.update_editor(|editor, window, cx| {
14694 editor.set_text(text, window, cx);
14695
14696 editor.buffer().update(cx, |multibuffer, cx| {
14697 let buffer = multibuffer.as_singleton().unwrap();
14698 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
14699
14700 multibuffer.set_all_diff_hunks_expanded(cx);
14701 multibuffer.add_diff(diff, cx);
14702
14703 buffer.read(cx).remote_id()
14704 })
14705 });
14706 cx.run_until_parked();
14707
14708 cx.assert_state_with_diff(
14709 indoc! { "
14710 impl A {
14711 fn b() {
14712 0;
14713 - 1;
14714 - 2;
14715 3;
14716 - 4;
14717 - }
14718 - fn c() {
14719 5;
14720 6;
14721 7;
14722 }
14723 }
14724 ˇ"
14725 }
14726 .to_string(),
14727 );
14728
14729 let mut actual_guides = cx.update_editor(|editor, window, cx| {
14730 editor
14731 .snapshot(window, cx)
14732 .buffer_snapshot
14733 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
14734 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
14735 .collect::<Vec<_>>()
14736 });
14737 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
14738 assert_eq!(
14739 actual_guides,
14740 vec![
14741 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
14742 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
14743 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
14744 ]
14745 );
14746}
14747
14748#[gpui::test]
14749fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
14750 init_test(cx, |_| {});
14751
14752 let editor = cx.add_window(|window, cx| {
14753 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
14754 build_editor(buffer, window, cx)
14755 });
14756
14757 let render_args = Arc::new(Mutex::new(None));
14758 let snapshot = editor
14759 .update(cx, |editor, window, cx| {
14760 let snapshot = editor.buffer().read(cx).snapshot(cx);
14761 let range =
14762 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
14763
14764 struct RenderArgs {
14765 row: MultiBufferRow,
14766 folded: bool,
14767 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
14768 }
14769
14770 let crease = Crease::inline(
14771 range,
14772 FoldPlaceholder::test(),
14773 {
14774 let toggle_callback = render_args.clone();
14775 move |row, folded, callback, _window, _cx| {
14776 *toggle_callback.lock() = Some(RenderArgs {
14777 row,
14778 folded,
14779 callback,
14780 });
14781 div()
14782 }
14783 },
14784 |_row, _folded, _window, _cx| div(),
14785 );
14786
14787 editor.insert_creases(Some(crease), cx);
14788 let snapshot = editor.snapshot(window, cx);
14789 let _div = snapshot.render_crease_toggle(
14790 MultiBufferRow(1),
14791 false,
14792 cx.entity().clone(),
14793 window,
14794 cx,
14795 );
14796 snapshot
14797 })
14798 .unwrap();
14799
14800 let render_args = render_args.lock().take().unwrap();
14801 assert_eq!(render_args.row, MultiBufferRow(1));
14802 assert!(!render_args.folded);
14803 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
14804
14805 cx.update_window(*editor, |_, window, cx| {
14806 (render_args.callback)(true, window, cx)
14807 })
14808 .unwrap();
14809 let snapshot = editor
14810 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
14811 .unwrap();
14812 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
14813
14814 cx.update_window(*editor, |_, window, cx| {
14815 (render_args.callback)(false, window, cx)
14816 })
14817 .unwrap();
14818 let snapshot = editor
14819 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
14820 .unwrap();
14821 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
14822}
14823
14824#[gpui::test]
14825async fn test_input_text(cx: &mut gpui::TestAppContext) {
14826 init_test(cx, |_| {});
14827 let mut cx = EditorTestContext::new(cx).await;
14828
14829 cx.set_state(
14830 &r#"ˇone
14831 two
14832
14833 three
14834 fourˇ
14835 five
14836
14837 siˇx"#
14838 .unindent(),
14839 );
14840
14841 cx.dispatch_action(HandleInput(String::new()));
14842 cx.assert_editor_state(
14843 &r#"ˇone
14844 two
14845
14846 three
14847 fourˇ
14848 five
14849
14850 siˇx"#
14851 .unindent(),
14852 );
14853
14854 cx.dispatch_action(HandleInput("AAAA".to_string()));
14855 cx.assert_editor_state(
14856 &r#"AAAAˇone
14857 two
14858
14859 three
14860 fourAAAAˇ
14861 five
14862
14863 siAAAAˇx"#
14864 .unindent(),
14865 );
14866}
14867
14868#[gpui::test]
14869async fn test_scroll_cursor_center_top_bottom(cx: &mut gpui::TestAppContext) {
14870 init_test(cx, |_| {});
14871
14872 let mut cx = EditorTestContext::new(cx).await;
14873 cx.set_state(
14874 r#"let foo = 1;
14875let foo = 2;
14876let foo = 3;
14877let fooˇ = 4;
14878let foo = 5;
14879let foo = 6;
14880let foo = 7;
14881let foo = 8;
14882let foo = 9;
14883let foo = 10;
14884let foo = 11;
14885let foo = 12;
14886let foo = 13;
14887let foo = 14;
14888let foo = 15;"#,
14889 );
14890
14891 cx.update_editor(|e, window, cx| {
14892 assert_eq!(
14893 e.next_scroll_position,
14894 NextScrollCursorCenterTopBottom::Center,
14895 "Default next scroll direction is center",
14896 );
14897
14898 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14899 assert_eq!(
14900 e.next_scroll_position,
14901 NextScrollCursorCenterTopBottom::Top,
14902 "After center, next scroll direction should be top",
14903 );
14904
14905 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14906 assert_eq!(
14907 e.next_scroll_position,
14908 NextScrollCursorCenterTopBottom::Bottom,
14909 "After top, next scroll direction should be bottom",
14910 );
14911
14912 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14913 assert_eq!(
14914 e.next_scroll_position,
14915 NextScrollCursorCenterTopBottom::Center,
14916 "After bottom, scrolling should start over",
14917 );
14918
14919 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14920 assert_eq!(
14921 e.next_scroll_position,
14922 NextScrollCursorCenterTopBottom::Top,
14923 "Scrolling continues if retriggered fast enough"
14924 );
14925 });
14926
14927 cx.executor()
14928 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
14929 cx.executor().run_until_parked();
14930 cx.update_editor(|e, _, _| {
14931 assert_eq!(
14932 e.next_scroll_position,
14933 NextScrollCursorCenterTopBottom::Center,
14934 "If scrolling is not triggered fast enough, it should reset"
14935 );
14936 });
14937}
14938
14939#[gpui::test]
14940async fn test_goto_definition_with_find_all_references_fallback(cx: &mut gpui::TestAppContext) {
14941 init_test(cx, |_| {});
14942 let mut cx = EditorLspTestContext::new_rust(
14943 lsp::ServerCapabilities {
14944 definition_provider: Some(lsp::OneOf::Left(true)),
14945 references_provider: Some(lsp::OneOf::Left(true)),
14946 ..lsp::ServerCapabilities::default()
14947 },
14948 cx,
14949 )
14950 .await;
14951
14952 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
14953 let go_to_definition = cx.lsp.handle_request::<lsp::request::GotoDefinition, _, _>(
14954 move |params, _| async move {
14955 if empty_go_to_definition {
14956 Ok(None)
14957 } else {
14958 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
14959 uri: params.text_document_position_params.text_document.uri,
14960 range: lsp::Range::new(lsp::Position::new(4, 3), lsp::Position::new(4, 6)),
14961 })))
14962 }
14963 },
14964 );
14965 let references =
14966 cx.lsp
14967 .handle_request::<lsp::request::References, _, _>(move |params, _| async move {
14968 Ok(Some(vec![lsp::Location {
14969 uri: params.text_document_position.text_document.uri,
14970 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
14971 }]))
14972 });
14973 (go_to_definition, references)
14974 };
14975
14976 cx.set_state(
14977 &r#"fn one() {
14978 let mut a = ˇtwo();
14979 }
14980
14981 fn two() {}"#
14982 .unindent(),
14983 );
14984 set_up_lsp_handlers(false, &mut cx);
14985 let navigated = cx
14986 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
14987 .await
14988 .expect("Failed to navigate to definition");
14989 assert_eq!(
14990 navigated,
14991 Navigated::Yes,
14992 "Should have navigated to definition from the GetDefinition response"
14993 );
14994 cx.assert_editor_state(
14995 &r#"fn one() {
14996 let mut a = two();
14997 }
14998
14999 fn «twoˇ»() {}"#
15000 .unindent(),
15001 );
15002
15003 let editors = cx.update_workspace(|workspace, _, cx| {
15004 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
15005 });
15006 cx.update_editor(|_, _, test_editor_cx| {
15007 assert_eq!(
15008 editors.len(),
15009 1,
15010 "Initially, only one, test, editor should be open in the workspace"
15011 );
15012 assert_eq!(
15013 test_editor_cx.entity(),
15014 editors.last().expect("Asserted len is 1").clone()
15015 );
15016 });
15017
15018 set_up_lsp_handlers(true, &mut cx);
15019 let navigated = cx
15020 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
15021 .await
15022 .expect("Failed to navigate to lookup references");
15023 assert_eq!(
15024 navigated,
15025 Navigated::Yes,
15026 "Should have navigated to references as a fallback after empty GoToDefinition response"
15027 );
15028 // We should not change the selections in the existing file,
15029 // if opening another milti buffer with the references
15030 cx.assert_editor_state(
15031 &r#"fn one() {
15032 let mut a = two();
15033 }
15034
15035 fn «twoˇ»() {}"#
15036 .unindent(),
15037 );
15038 let editors = cx.update_workspace(|workspace, _, cx| {
15039 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
15040 });
15041 cx.update_editor(|_, _, test_editor_cx| {
15042 assert_eq!(
15043 editors.len(),
15044 2,
15045 "After falling back to references search, we open a new editor with the results"
15046 );
15047 let references_fallback_text = editors
15048 .into_iter()
15049 .find(|new_editor| *new_editor != test_editor_cx.entity())
15050 .expect("Should have one non-test editor now")
15051 .read(test_editor_cx)
15052 .text(test_editor_cx);
15053 assert_eq!(
15054 references_fallback_text, "fn one() {\n let mut a = two();\n}",
15055 "Should use the range from the references response and not the GoToDefinition one"
15056 );
15057 });
15058}
15059
15060#[gpui::test]
15061async fn test_find_enclosing_node_with_task(cx: &mut gpui::TestAppContext) {
15062 init_test(cx, |_| {});
15063
15064 let language = Arc::new(Language::new(
15065 LanguageConfig::default(),
15066 Some(tree_sitter_rust::LANGUAGE.into()),
15067 ));
15068
15069 let text = r#"
15070 #[cfg(test)]
15071 mod tests() {
15072 #[test]
15073 fn runnable_1() {
15074 let a = 1;
15075 }
15076
15077 #[test]
15078 fn runnable_2() {
15079 let a = 1;
15080 let b = 2;
15081 }
15082 }
15083 "#
15084 .unindent();
15085
15086 let fs = FakeFs::new(cx.executor());
15087 fs.insert_file("/file.rs", Default::default()).await;
15088
15089 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15090 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15091 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15092 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
15093 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
15094
15095 let editor = cx.new_window_entity(|window, cx| {
15096 Editor::new(
15097 EditorMode::Full,
15098 multi_buffer,
15099 Some(project.clone()),
15100 true,
15101 window,
15102 cx,
15103 )
15104 });
15105
15106 editor.update_in(cx, |editor, window, cx| {
15107 editor.tasks.insert(
15108 (buffer.read(cx).remote_id(), 3),
15109 RunnableTasks {
15110 templates: vec![],
15111 offset: MultiBufferOffset(43),
15112 column: 0,
15113 extra_variables: HashMap::default(),
15114 context_range: BufferOffset(43)..BufferOffset(85),
15115 },
15116 );
15117 editor.tasks.insert(
15118 (buffer.read(cx).remote_id(), 8),
15119 RunnableTasks {
15120 templates: vec![],
15121 offset: MultiBufferOffset(86),
15122 column: 0,
15123 extra_variables: HashMap::default(),
15124 context_range: BufferOffset(86)..BufferOffset(191),
15125 },
15126 );
15127
15128 // Test finding task when cursor is inside function body
15129 editor.change_selections(None, window, cx, |s| {
15130 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
15131 });
15132 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
15133 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
15134
15135 // Test finding task when cursor is on function name
15136 editor.change_selections(None, window, cx, |s| {
15137 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
15138 });
15139 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
15140 assert_eq!(row, 8, "Should find task when cursor is on function name");
15141 });
15142}
15143
15144#[gpui::test]
15145async fn test_multi_buffer_folding(cx: &mut gpui::TestAppContext) {
15146 init_test(cx, |_| {});
15147
15148 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
15149 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
15150 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
15151
15152 let fs = FakeFs::new(cx.executor());
15153 fs.insert_tree(
15154 path!("/a"),
15155 json!({
15156 "first.rs": sample_text_1,
15157 "second.rs": sample_text_2,
15158 "third.rs": sample_text_3,
15159 }),
15160 )
15161 .await;
15162 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15163 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15164 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15165 let worktree = project.update(cx, |project, cx| {
15166 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15167 assert_eq!(worktrees.len(), 1);
15168 worktrees.pop().unwrap()
15169 });
15170 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15171
15172 let buffer_1 = project
15173 .update(cx, |project, cx| {
15174 project.open_buffer((worktree_id, "first.rs"), cx)
15175 })
15176 .await
15177 .unwrap();
15178 let buffer_2 = project
15179 .update(cx, |project, cx| {
15180 project.open_buffer((worktree_id, "second.rs"), cx)
15181 })
15182 .await
15183 .unwrap();
15184 let buffer_3 = project
15185 .update(cx, |project, cx| {
15186 project.open_buffer((worktree_id, "third.rs"), cx)
15187 })
15188 .await
15189 .unwrap();
15190
15191 let multi_buffer = cx.new(|cx| {
15192 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15193 multi_buffer.push_excerpts(
15194 buffer_1.clone(),
15195 [
15196 ExcerptRange {
15197 context: Point::new(0, 0)..Point::new(3, 0),
15198 primary: None,
15199 },
15200 ExcerptRange {
15201 context: Point::new(5, 0)..Point::new(7, 0),
15202 primary: None,
15203 },
15204 ExcerptRange {
15205 context: Point::new(9, 0)..Point::new(10, 4),
15206 primary: None,
15207 },
15208 ],
15209 cx,
15210 );
15211 multi_buffer.push_excerpts(
15212 buffer_2.clone(),
15213 [
15214 ExcerptRange {
15215 context: Point::new(0, 0)..Point::new(3, 0),
15216 primary: None,
15217 },
15218 ExcerptRange {
15219 context: Point::new(5, 0)..Point::new(7, 0),
15220 primary: None,
15221 },
15222 ExcerptRange {
15223 context: Point::new(9, 0)..Point::new(10, 4),
15224 primary: None,
15225 },
15226 ],
15227 cx,
15228 );
15229 multi_buffer.push_excerpts(
15230 buffer_3.clone(),
15231 [
15232 ExcerptRange {
15233 context: Point::new(0, 0)..Point::new(3, 0),
15234 primary: None,
15235 },
15236 ExcerptRange {
15237 context: Point::new(5, 0)..Point::new(7, 0),
15238 primary: None,
15239 },
15240 ExcerptRange {
15241 context: Point::new(9, 0)..Point::new(10, 4),
15242 primary: None,
15243 },
15244 ],
15245 cx,
15246 );
15247 multi_buffer
15248 });
15249 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15250 Editor::new(
15251 EditorMode::Full,
15252 multi_buffer,
15253 Some(project.clone()),
15254 true,
15255 window,
15256 cx,
15257 )
15258 });
15259
15260 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";
15261 assert_eq!(
15262 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15263 full_text,
15264 );
15265
15266 multi_buffer_editor.update(cx, |editor, cx| {
15267 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
15268 });
15269 assert_eq!(
15270 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15271 "\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",
15272 "After folding the first buffer, its text should not be displayed"
15273 );
15274
15275 multi_buffer_editor.update(cx, |editor, cx| {
15276 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
15277 });
15278 assert_eq!(
15279 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15280 "\n\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
15281 "After folding the second buffer, its text should not be displayed"
15282 );
15283
15284 multi_buffer_editor.update(cx, |editor, cx| {
15285 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
15286 });
15287 assert_eq!(
15288 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15289 "\n\n\n\n\n",
15290 "After folding the third buffer, its text should not be displayed"
15291 );
15292
15293 // Emulate selection inside the fold logic, that should work
15294 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15295 editor
15296 .snapshot(window, cx)
15297 .next_line_boundary(Point::new(0, 4));
15298 });
15299
15300 multi_buffer_editor.update(cx, |editor, cx| {
15301 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
15302 });
15303 assert_eq!(
15304 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15305 "\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
15306 "After unfolding the second buffer, its text should be displayed"
15307 );
15308
15309 multi_buffer_editor.update(cx, |editor, cx| {
15310 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
15311 });
15312 assert_eq!(
15313 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15314 "\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",
15315 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
15316 );
15317
15318 multi_buffer_editor.update(cx, |editor, cx| {
15319 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
15320 });
15321 assert_eq!(
15322 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15323 full_text,
15324 "After unfolding the all buffers, all original text should be displayed"
15325 );
15326}
15327
15328#[gpui::test]
15329async fn test_multi_buffer_single_excerpts_folding(cx: &mut gpui::TestAppContext) {
15330 init_test(cx, |_| {});
15331
15332 let sample_text_1 = "1111\n2222\n3333".to_string();
15333 let sample_text_2 = "4444\n5555\n6666".to_string();
15334 let sample_text_3 = "7777\n8888\n9999".to_string();
15335
15336 let fs = FakeFs::new(cx.executor());
15337 fs.insert_tree(
15338 path!("/a"),
15339 json!({
15340 "first.rs": sample_text_1,
15341 "second.rs": sample_text_2,
15342 "third.rs": sample_text_3,
15343 }),
15344 )
15345 .await;
15346 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15347 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15348 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15349 let worktree = project.update(cx, |project, cx| {
15350 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15351 assert_eq!(worktrees.len(), 1);
15352 worktrees.pop().unwrap()
15353 });
15354 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15355
15356 let buffer_1 = project
15357 .update(cx, |project, cx| {
15358 project.open_buffer((worktree_id, "first.rs"), cx)
15359 })
15360 .await
15361 .unwrap();
15362 let buffer_2 = project
15363 .update(cx, |project, cx| {
15364 project.open_buffer((worktree_id, "second.rs"), cx)
15365 })
15366 .await
15367 .unwrap();
15368 let buffer_3 = project
15369 .update(cx, |project, cx| {
15370 project.open_buffer((worktree_id, "third.rs"), cx)
15371 })
15372 .await
15373 .unwrap();
15374
15375 let multi_buffer = cx.new(|cx| {
15376 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15377 multi_buffer.push_excerpts(
15378 buffer_1.clone(),
15379 [ExcerptRange {
15380 context: Point::new(0, 0)..Point::new(3, 0),
15381 primary: None,
15382 }],
15383 cx,
15384 );
15385 multi_buffer.push_excerpts(
15386 buffer_2.clone(),
15387 [ExcerptRange {
15388 context: Point::new(0, 0)..Point::new(3, 0),
15389 primary: None,
15390 }],
15391 cx,
15392 );
15393 multi_buffer.push_excerpts(
15394 buffer_3.clone(),
15395 [ExcerptRange {
15396 context: Point::new(0, 0)..Point::new(3, 0),
15397 primary: None,
15398 }],
15399 cx,
15400 );
15401 multi_buffer
15402 });
15403
15404 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15405 Editor::new(
15406 EditorMode::Full,
15407 multi_buffer,
15408 Some(project.clone()),
15409 true,
15410 window,
15411 cx,
15412 )
15413 });
15414
15415 let full_text = "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n";
15416 assert_eq!(
15417 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15418 full_text,
15419 );
15420
15421 multi_buffer_editor.update(cx, |editor, cx| {
15422 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
15423 });
15424 assert_eq!(
15425 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15426 "\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n",
15427 "After folding the first buffer, its text should not be displayed"
15428 );
15429
15430 multi_buffer_editor.update(cx, |editor, cx| {
15431 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
15432 });
15433
15434 assert_eq!(
15435 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15436 "\n\n\n\n\n\n\n7777\n8888\n9999\n",
15437 "After folding the second buffer, its text should not be displayed"
15438 );
15439
15440 multi_buffer_editor.update(cx, |editor, cx| {
15441 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
15442 });
15443 assert_eq!(
15444 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15445 "\n\n\n\n\n",
15446 "After folding the third buffer, its text should not be displayed"
15447 );
15448
15449 multi_buffer_editor.update(cx, |editor, cx| {
15450 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
15451 });
15452 assert_eq!(
15453 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15454 "\n\n\n\n\n4444\n5555\n6666\n\n\n",
15455 "After unfolding the second buffer, its text should be displayed"
15456 );
15457
15458 multi_buffer_editor.update(cx, |editor, cx| {
15459 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
15460 });
15461 assert_eq!(
15462 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15463 "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n",
15464 "After unfolding the first buffer, its text should be displayed"
15465 );
15466
15467 multi_buffer_editor.update(cx, |editor, cx| {
15468 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
15469 });
15470 assert_eq!(
15471 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15472 full_text,
15473 "After unfolding all buffers, all original text should be displayed"
15474 );
15475}
15476
15477#[gpui::test]
15478async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppContext) {
15479 init_test(cx, |_| {});
15480
15481 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
15482
15483 let fs = FakeFs::new(cx.executor());
15484 fs.insert_tree(
15485 path!("/a"),
15486 json!({
15487 "main.rs": sample_text,
15488 }),
15489 )
15490 .await;
15491 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15492 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15493 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15494 let worktree = project.update(cx, |project, cx| {
15495 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15496 assert_eq!(worktrees.len(), 1);
15497 worktrees.pop().unwrap()
15498 });
15499 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15500
15501 let buffer_1 = project
15502 .update(cx, |project, cx| {
15503 project.open_buffer((worktree_id, "main.rs"), cx)
15504 })
15505 .await
15506 .unwrap();
15507
15508 let multi_buffer = cx.new(|cx| {
15509 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15510 multi_buffer.push_excerpts(
15511 buffer_1.clone(),
15512 [ExcerptRange {
15513 context: Point::new(0, 0)
15514 ..Point::new(
15515 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
15516 0,
15517 ),
15518 primary: None,
15519 }],
15520 cx,
15521 );
15522 multi_buffer
15523 });
15524 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15525 Editor::new(
15526 EditorMode::Full,
15527 multi_buffer,
15528 Some(project.clone()),
15529 true,
15530 window,
15531 cx,
15532 )
15533 });
15534
15535 let selection_range = Point::new(1, 0)..Point::new(2, 0);
15536 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15537 enum TestHighlight {}
15538 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
15539 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
15540 editor.highlight_text::<TestHighlight>(
15541 vec![highlight_range.clone()],
15542 HighlightStyle::color(Hsla::green()),
15543 cx,
15544 );
15545 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
15546 });
15547
15548 let full_text = format!("\n\n\n{sample_text}\n");
15549 assert_eq!(
15550 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15551 full_text,
15552 );
15553}
15554
15555#[gpui::test]
15556async fn test_inline_completion_text(cx: &mut TestAppContext) {
15557 init_test(cx, |_| {});
15558
15559 // Simple insertion
15560 assert_highlighted_edits(
15561 "Hello, world!",
15562 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
15563 true,
15564 cx,
15565 |highlighted_edits, cx| {
15566 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
15567 assert_eq!(highlighted_edits.highlights.len(), 1);
15568 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
15569 assert_eq!(
15570 highlighted_edits.highlights[0].1.background_color,
15571 Some(cx.theme().status().created_background)
15572 );
15573 },
15574 )
15575 .await;
15576
15577 // Replacement
15578 assert_highlighted_edits(
15579 "This is a test.",
15580 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
15581 false,
15582 cx,
15583 |highlighted_edits, cx| {
15584 assert_eq!(highlighted_edits.text, "That is a test.");
15585 assert_eq!(highlighted_edits.highlights.len(), 1);
15586 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
15587 assert_eq!(
15588 highlighted_edits.highlights[0].1.background_color,
15589 Some(cx.theme().status().created_background)
15590 );
15591 },
15592 )
15593 .await;
15594
15595 // Multiple edits
15596 assert_highlighted_edits(
15597 "Hello, world!",
15598 vec![
15599 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
15600 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
15601 ],
15602 false,
15603 cx,
15604 |highlighted_edits, cx| {
15605 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
15606 assert_eq!(highlighted_edits.highlights.len(), 2);
15607 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
15608 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
15609 assert_eq!(
15610 highlighted_edits.highlights[0].1.background_color,
15611 Some(cx.theme().status().created_background)
15612 );
15613 assert_eq!(
15614 highlighted_edits.highlights[1].1.background_color,
15615 Some(cx.theme().status().created_background)
15616 );
15617 },
15618 )
15619 .await;
15620
15621 // Multiple lines with edits
15622 assert_highlighted_edits(
15623 "First line\nSecond line\nThird line\nFourth line",
15624 vec![
15625 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
15626 (
15627 Point::new(2, 0)..Point::new(2, 10),
15628 "New third line".to_string(),
15629 ),
15630 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
15631 ],
15632 false,
15633 cx,
15634 |highlighted_edits, cx| {
15635 assert_eq!(
15636 highlighted_edits.text,
15637 "Second modified\nNew third line\nFourth updated line"
15638 );
15639 assert_eq!(highlighted_edits.highlights.len(), 3);
15640 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
15641 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
15642 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
15643 for highlight in &highlighted_edits.highlights {
15644 assert_eq!(
15645 highlight.1.background_color,
15646 Some(cx.theme().status().created_background)
15647 );
15648 }
15649 },
15650 )
15651 .await;
15652}
15653
15654#[gpui::test]
15655async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
15656 init_test(cx, |_| {});
15657
15658 // Deletion
15659 assert_highlighted_edits(
15660 "Hello, world!",
15661 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
15662 true,
15663 cx,
15664 |highlighted_edits, cx| {
15665 assert_eq!(highlighted_edits.text, "Hello, world!");
15666 assert_eq!(highlighted_edits.highlights.len(), 1);
15667 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
15668 assert_eq!(
15669 highlighted_edits.highlights[0].1.background_color,
15670 Some(cx.theme().status().deleted_background)
15671 );
15672 },
15673 )
15674 .await;
15675
15676 // Insertion
15677 assert_highlighted_edits(
15678 "Hello, world!",
15679 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
15680 true,
15681 cx,
15682 |highlighted_edits, cx| {
15683 assert_eq!(highlighted_edits.highlights.len(), 1);
15684 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
15685 assert_eq!(
15686 highlighted_edits.highlights[0].1.background_color,
15687 Some(cx.theme().status().created_background)
15688 );
15689 },
15690 )
15691 .await;
15692}
15693
15694async fn assert_highlighted_edits(
15695 text: &str,
15696 edits: Vec<(Range<Point>, String)>,
15697 include_deletions: bool,
15698 cx: &mut TestAppContext,
15699 assertion_fn: impl Fn(HighlightedText, &App),
15700) {
15701 let window = cx.add_window(|window, cx| {
15702 let buffer = MultiBuffer::build_simple(text, cx);
15703 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
15704 });
15705 let cx = &mut VisualTestContext::from_window(*window, cx);
15706
15707 let (buffer, snapshot) = window
15708 .update(cx, |editor, _window, cx| {
15709 (
15710 editor.buffer().clone(),
15711 editor.buffer().read(cx).snapshot(cx),
15712 )
15713 })
15714 .unwrap();
15715
15716 let edits = edits
15717 .into_iter()
15718 .map(|(range, edit)| {
15719 (
15720 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
15721 edit,
15722 )
15723 })
15724 .collect::<Vec<_>>();
15725
15726 let text_anchor_edits = edits
15727 .clone()
15728 .into_iter()
15729 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
15730 .collect::<Vec<_>>();
15731
15732 let edit_preview = window
15733 .update(cx, |_, _window, cx| {
15734 buffer
15735 .read(cx)
15736 .as_singleton()
15737 .unwrap()
15738 .read(cx)
15739 .preview_edits(text_anchor_edits.into(), cx)
15740 })
15741 .unwrap()
15742 .await;
15743
15744 cx.update(|_window, cx| {
15745 let highlighted_edits = inline_completion_edit_text(
15746 &snapshot.as_singleton().unwrap().2,
15747 &edits,
15748 &edit_preview,
15749 include_deletions,
15750 cx,
15751 );
15752 assertion_fn(highlighted_edits, cx)
15753 });
15754}
15755
15756#[gpui::test]
15757async fn test_rename_with_duplicate_edits(cx: &mut gpui::TestAppContext) {
15758 init_test(cx, |_| {});
15759 let capabilities = lsp::ServerCapabilities {
15760 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
15761 prepare_provider: Some(true),
15762 work_done_progress_options: Default::default(),
15763 })),
15764 ..Default::default()
15765 };
15766 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
15767
15768 cx.set_state(indoc! {"
15769 struct Fˇoo {}
15770 "});
15771
15772 cx.update_editor(|editor, _, cx| {
15773 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
15774 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
15775 editor.highlight_background::<DocumentHighlightRead>(
15776 &[highlight_range],
15777 |c| c.editor_document_highlight_read_background,
15778 cx,
15779 );
15780 });
15781
15782 let mut prepare_rename_handler =
15783 cx.handle_request::<lsp::request::PrepareRenameRequest, _, _>(move |_, _, _| async move {
15784 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
15785 start: lsp::Position {
15786 line: 0,
15787 character: 7,
15788 },
15789 end: lsp::Position {
15790 line: 0,
15791 character: 10,
15792 },
15793 })))
15794 });
15795 let prepare_rename_task = cx
15796 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
15797 .expect("Prepare rename was not started");
15798 prepare_rename_handler.next().await.unwrap();
15799 prepare_rename_task.await.expect("Prepare rename failed");
15800
15801 let mut rename_handler =
15802 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
15803 let edit = lsp::TextEdit {
15804 range: lsp::Range {
15805 start: lsp::Position {
15806 line: 0,
15807 character: 7,
15808 },
15809 end: lsp::Position {
15810 line: 0,
15811 character: 10,
15812 },
15813 },
15814 new_text: "FooRenamed".to_string(),
15815 };
15816 Ok(Some(lsp::WorkspaceEdit::new(
15817 // Specify the same edit twice
15818 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
15819 )))
15820 });
15821 let rename_task = cx
15822 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
15823 .expect("Confirm rename was not started");
15824 rename_handler.next().await.unwrap();
15825 rename_task.await.expect("Confirm rename failed");
15826 cx.run_until_parked();
15827
15828 // Despite two edits, only one is actually applied as those are identical
15829 cx.assert_editor_state(indoc! {"
15830 struct FooRenamedˇ {}
15831 "});
15832}
15833
15834#[gpui::test]
15835async fn test_rename_without_prepare(cx: &mut gpui::TestAppContext) {
15836 init_test(cx, |_| {});
15837 // These capabilities indicate that the server does not support prepare rename.
15838 let capabilities = lsp::ServerCapabilities {
15839 rename_provider: Some(lsp::OneOf::Left(true)),
15840 ..Default::default()
15841 };
15842 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
15843
15844 cx.set_state(indoc! {"
15845 struct Fˇoo {}
15846 "});
15847
15848 cx.update_editor(|editor, _window, cx| {
15849 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
15850 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
15851 editor.highlight_background::<DocumentHighlightRead>(
15852 &[highlight_range],
15853 |c| c.editor_document_highlight_read_background,
15854 cx,
15855 );
15856 });
15857
15858 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
15859 .expect("Prepare rename was not started")
15860 .await
15861 .expect("Prepare rename failed");
15862
15863 let mut rename_handler =
15864 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
15865 let edit = lsp::TextEdit {
15866 range: lsp::Range {
15867 start: lsp::Position {
15868 line: 0,
15869 character: 7,
15870 },
15871 end: lsp::Position {
15872 line: 0,
15873 character: 10,
15874 },
15875 },
15876 new_text: "FooRenamed".to_string(),
15877 };
15878 Ok(Some(lsp::WorkspaceEdit::new(
15879 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
15880 )))
15881 });
15882 let rename_task = cx
15883 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
15884 .expect("Confirm rename was not started");
15885 rename_handler.next().await.unwrap();
15886 rename_task.await.expect("Confirm rename failed");
15887 cx.run_until_parked();
15888
15889 // Correct range is renamed, as `surrounding_word` is used to find it.
15890 cx.assert_editor_state(indoc! {"
15891 struct FooRenamedˇ {}
15892 "});
15893}
15894
15895fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
15896 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
15897 point..point
15898}
15899
15900fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
15901 let (text, ranges) = marked_text_ranges(marked_text, true);
15902 assert_eq!(editor.text(cx), text);
15903 assert_eq!(
15904 editor.selections.ranges(cx),
15905 ranges,
15906 "Assert selections are {}",
15907 marked_text
15908 );
15909}
15910
15911pub fn handle_signature_help_request(
15912 cx: &mut EditorLspTestContext,
15913 mocked_response: lsp::SignatureHelp,
15914) -> impl Future<Output = ()> {
15915 let mut request =
15916 cx.handle_request::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
15917 let mocked_response = mocked_response.clone();
15918 async move { Ok(Some(mocked_response)) }
15919 });
15920
15921 async move {
15922 request.next().await;
15923 }
15924}
15925
15926/// Handle completion request passing a marked string specifying where the completion
15927/// should be triggered from using '|' character, what range should be replaced, and what completions
15928/// should be returned using '<' and '>' to delimit the range
15929pub fn handle_completion_request(
15930 cx: &mut EditorLspTestContext,
15931 marked_string: &str,
15932 completions: Vec<&'static str>,
15933 counter: Arc<AtomicUsize>,
15934) -> impl Future<Output = ()> {
15935 let complete_from_marker: TextRangeMarker = '|'.into();
15936 let replace_range_marker: TextRangeMarker = ('<', '>').into();
15937 let (_, mut marked_ranges) = marked_text_ranges_by(
15938 marked_string,
15939 vec![complete_from_marker.clone(), replace_range_marker.clone()],
15940 );
15941
15942 let complete_from_position =
15943 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
15944 let replace_range =
15945 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
15946
15947 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
15948 let completions = completions.clone();
15949 counter.fetch_add(1, atomic::Ordering::Release);
15950 async move {
15951 assert_eq!(params.text_document_position.text_document.uri, url.clone());
15952 assert_eq!(
15953 params.text_document_position.position,
15954 complete_from_position
15955 );
15956 Ok(Some(lsp::CompletionResponse::Array(
15957 completions
15958 .iter()
15959 .map(|completion_text| lsp::CompletionItem {
15960 label: completion_text.to_string(),
15961 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15962 range: replace_range,
15963 new_text: completion_text.to_string(),
15964 })),
15965 ..Default::default()
15966 })
15967 .collect(),
15968 )))
15969 }
15970 });
15971
15972 async move {
15973 request.next().await;
15974 }
15975}
15976
15977fn handle_resolve_completion_request(
15978 cx: &mut EditorLspTestContext,
15979 edits: Option<Vec<(&'static str, &'static str)>>,
15980) -> impl Future<Output = ()> {
15981 let edits = edits.map(|edits| {
15982 edits
15983 .iter()
15984 .map(|(marked_string, new_text)| {
15985 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
15986 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
15987 lsp::TextEdit::new(replace_range, new_text.to_string())
15988 })
15989 .collect::<Vec<_>>()
15990 });
15991
15992 let mut request =
15993 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15994 let edits = edits.clone();
15995 async move {
15996 Ok(lsp::CompletionItem {
15997 additional_text_edits: edits,
15998 ..Default::default()
15999 })
16000 }
16001 });
16002
16003 async move {
16004 request.next().await;
16005 }
16006}
16007
16008pub(crate) fn update_test_language_settings(
16009 cx: &mut TestAppContext,
16010 f: impl Fn(&mut AllLanguageSettingsContent),
16011) {
16012 cx.update(|cx| {
16013 SettingsStore::update_global(cx, |store, cx| {
16014 store.update_user_settings::<AllLanguageSettings>(cx, f);
16015 });
16016 });
16017}
16018
16019pub(crate) fn update_test_project_settings(
16020 cx: &mut TestAppContext,
16021 f: impl Fn(&mut ProjectSettings),
16022) {
16023 cx.update(|cx| {
16024 SettingsStore::update_global(cx, |store, cx| {
16025 store.update_user_settings::<ProjectSettings>(cx, f);
16026 });
16027 });
16028}
16029
16030pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
16031 cx.update(|cx| {
16032 assets::Assets.load_test_fonts(cx);
16033 let store = SettingsStore::test(cx);
16034 cx.set_global(store);
16035 theme::init(theme::LoadThemes::JustBase, cx);
16036 release_channel::init(SemanticVersion::default(), cx);
16037 client::init_settings(cx);
16038 language::init(cx);
16039 Project::init_settings(cx);
16040 workspace::init_settings(cx);
16041 crate::init(cx);
16042 });
16043
16044 update_test_language_settings(cx, f);
16045}
16046
16047#[track_caller]
16048fn assert_hunk_revert(
16049 not_reverted_text_with_selections: &str,
16050 expected_hunk_statuses_before: Vec<DiffHunkStatus>,
16051 expected_reverted_text_with_selections: &str,
16052 base_text: &str,
16053 cx: &mut EditorLspTestContext,
16054) {
16055 cx.set_state(not_reverted_text_with_selections);
16056 cx.set_diff_base(base_text);
16057 cx.executor().run_until_parked();
16058
16059 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
16060 let snapshot = editor.snapshot(window, cx);
16061 let reverted_hunk_statuses = snapshot
16062 .buffer_snapshot
16063 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
16064 .map(|hunk| hunk.status())
16065 .collect::<Vec<_>>();
16066
16067 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
16068 reverted_hunk_statuses
16069 });
16070 cx.executor().run_until_parked();
16071 cx.assert_editor_state(expected_reverted_text_with_selections);
16072 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
16073}