1use super::*;
2use crate::{
3 scroll::scroll_amount::ScrollAmount,
4 test::{
5 assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
6 editor_test_context::EditorTestContext, select_ranges,
7 },
8 JoinLines,
9};
10use buffer_diff::{BufferDiff, DiffHunkStatus};
11use futures::StreamExt;
12use gpui::{
13 div, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
14 WindowBounds, WindowOptions,
15};
16use indoc::indoc;
17use language::{
18 language_settings::{
19 AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent, PrettierSettings,
20 },
21 BracketPairConfig,
22 Capability::ReadWrite,
23 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
24 Override, ParsedMarkdown, Point,
25};
26use language_settings::{Formatter, FormatterList, IndentGuideSettings};
27use multi_buffer::IndentGuide;
28use parking_lot::Mutex;
29use pretty_assertions::{assert_eq, assert_ne};
30use project::FakeFs;
31use project::{
32 lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT,
33 project_settings::{LspSettings, ProjectSettings},
34};
35use serde_json::{self, json};
36use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
37use std::{
38 iter,
39 sync::atomic::{self, AtomicUsize},
40};
41use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
42use unindent::Unindent;
43use util::{
44 assert_set_eq, path,
45 test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
46 uri,
47};
48use workspace::{
49 item::{FollowEvent, FollowableItem, Item, ItemHandle},
50 NavigationEntry, ViewId,
51};
52
53#[gpui::test]
54fn test_edit_events(cx: &mut TestAppContext) {
55 init_test(cx, |_| {});
56
57 let buffer = cx.new(|cx| {
58 let mut buffer = language::Buffer::local("123456", cx);
59 buffer.set_group_interval(Duration::from_secs(1));
60 buffer
61 });
62
63 let events = Rc::new(RefCell::new(Vec::new()));
64 let editor1 = cx.add_window({
65 let events = events.clone();
66 |window, cx| {
67 let entity = cx.entity().clone();
68 cx.subscribe_in(
69 &entity,
70 window,
71 move |_, _, event: &EditorEvent, _, _| match event {
72 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
73 EditorEvent::BufferEdited => {
74 events.borrow_mut().push(("editor1", "buffer edited"))
75 }
76 _ => {}
77 },
78 )
79 .detach();
80 Editor::for_buffer(buffer.clone(), None, window, cx)
81 }
82 });
83
84 let editor2 = cx.add_window({
85 let events = events.clone();
86 |window, cx| {
87 cx.subscribe_in(
88 &cx.entity().clone(),
89 window,
90 move |_, _, event: &EditorEvent, _, _| match event {
91 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
92 EditorEvent::BufferEdited => {
93 events.borrow_mut().push(("editor2", "buffer edited"))
94 }
95 _ => {}
96 },
97 )
98 .detach();
99 Editor::for_buffer(buffer.clone(), None, window, cx)
100 }
101 });
102
103 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
104
105 // Mutating editor 1 will emit an `Edited` event only for that editor.
106 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
107 assert_eq!(
108 mem::take(&mut *events.borrow_mut()),
109 [
110 ("editor1", "edited"),
111 ("editor1", "buffer edited"),
112 ("editor2", "buffer edited"),
113 ]
114 );
115
116 // Mutating editor 2 will emit an `Edited` event only for that editor.
117 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
118 assert_eq!(
119 mem::take(&mut *events.borrow_mut()),
120 [
121 ("editor2", "edited"),
122 ("editor1", "buffer edited"),
123 ("editor2", "buffer edited"),
124 ]
125 );
126
127 // Undoing on editor 1 will emit an `Edited` event only for that editor.
128 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
129 assert_eq!(
130 mem::take(&mut *events.borrow_mut()),
131 [
132 ("editor1", "edited"),
133 ("editor1", "buffer edited"),
134 ("editor2", "buffer edited"),
135 ]
136 );
137
138 // Redoing on editor 1 will emit an `Edited` event only for that editor.
139 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
140 assert_eq!(
141 mem::take(&mut *events.borrow_mut()),
142 [
143 ("editor1", "edited"),
144 ("editor1", "buffer edited"),
145 ("editor2", "buffer edited"),
146 ]
147 );
148
149 // Undoing on editor 2 will emit an `Edited` event only for that editor.
150 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
151 assert_eq!(
152 mem::take(&mut *events.borrow_mut()),
153 [
154 ("editor2", "edited"),
155 ("editor1", "buffer edited"),
156 ("editor2", "buffer edited"),
157 ]
158 );
159
160 // Redoing on editor 2 will emit an `Edited` event only for that editor.
161 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
162 assert_eq!(
163 mem::take(&mut *events.borrow_mut()),
164 [
165 ("editor2", "edited"),
166 ("editor1", "buffer edited"),
167 ("editor2", "buffer edited"),
168 ]
169 );
170
171 // No event is emitted when the mutation is a no-op.
172 _ = editor2.update(cx, |editor, window, cx| {
173 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
174
175 editor.backspace(&Backspace, window, cx);
176 });
177 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
178}
179
180#[gpui::test]
181fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
182 init_test(cx, |_| {});
183
184 let mut now = Instant::now();
185 let group_interval = Duration::from_millis(1);
186 let buffer = cx.new(|cx| {
187 let mut buf = language::Buffer::local("123456", cx);
188 buf.set_group_interval(group_interval);
189 buf
190 });
191 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
192 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
193
194 _ = editor.update(cx, |editor, window, cx| {
195 editor.start_transaction_at(now, window, cx);
196 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
197
198 editor.insert("cd", window, cx);
199 editor.end_transaction_at(now, cx);
200 assert_eq!(editor.text(cx), "12cd56");
201 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
202
203 editor.start_transaction_at(now, window, cx);
204 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
205 editor.insert("e", window, cx);
206 editor.end_transaction_at(now, cx);
207 assert_eq!(editor.text(cx), "12cde6");
208 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
209
210 now += group_interval + Duration::from_millis(1);
211 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
212
213 // Simulate an edit in another editor
214 buffer.update(cx, |buffer, cx| {
215 buffer.start_transaction_at(now, cx);
216 buffer.edit([(0..1, "a")], None, cx);
217 buffer.edit([(1..1, "b")], None, cx);
218 buffer.end_transaction_at(now, cx);
219 });
220
221 assert_eq!(editor.text(cx), "ab2cde6");
222 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
223
224 // Last transaction happened past the group interval in a different editor.
225 // Undo it individually and don't restore selections.
226 editor.undo(&Undo, window, cx);
227 assert_eq!(editor.text(cx), "12cde6");
228 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
229
230 // First two transactions happened within the group interval in this editor.
231 // Undo them together and restore selections.
232 editor.undo(&Undo, window, cx);
233 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
234 assert_eq!(editor.text(cx), "123456");
235 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
236
237 // Redo the first two transactions together.
238 editor.redo(&Redo, window, cx);
239 assert_eq!(editor.text(cx), "12cde6");
240 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
241
242 // Redo the last transaction on its own.
243 editor.redo(&Redo, window, cx);
244 assert_eq!(editor.text(cx), "ab2cde6");
245 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
246
247 // Test empty transactions.
248 editor.start_transaction_at(now, window, cx);
249 editor.end_transaction_at(now, cx);
250 editor.undo(&Undo, window, cx);
251 assert_eq!(editor.text(cx), "12cde6");
252 });
253}
254
255#[gpui::test]
256fn test_ime_composition(cx: &mut TestAppContext) {
257 init_test(cx, |_| {});
258
259 let buffer = cx.new(|cx| {
260 let mut buffer = language::Buffer::local("abcde", cx);
261 // Ensure automatic grouping doesn't occur.
262 buffer.set_group_interval(Duration::ZERO);
263 buffer
264 });
265
266 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
267 cx.add_window(|window, cx| {
268 let mut editor = build_editor(buffer.clone(), window, cx);
269
270 // Start a new IME composition.
271 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
272 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
273 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
274 assert_eq!(editor.text(cx), "äbcde");
275 assert_eq!(
276 editor.marked_text_ranges(cx),
277 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
278 );
279
280 // Finalize IME composition.
281 editor.replace_text_in_range(None, "ā", window, cx);
282 assert_eq!(editor.text(cx), "ābcde");
283 assert_eq!(editor.marked_text_ranges(cx), None);
284
285 // IME composition edits are grouped and are undone/redone at once.
286 editor.undo(&Default::default(), window, cx);
287 assert_eq!(editor.text(cx), "abcde");
288 assert_eq!(editor.marked_text_ranges(cx), None);
289 editor.redo(&Default::default(), window, cx);
290 assert_eq!(editor.text(cx), "ābcde");
291 assert_eq!(editor.marked_text_ranges(cx), None);
292
293 // Start a new IME composition.
294 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
295 assert_eq!(
296 editor.marked_text_ranges(cx),
297 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
298 );
299
300 // Undoing during an IME composition cancels it.
301 editor.undo(&Default::default(), window, cx);
302 assert_eq!(editor.text(cx), "ābcde");
303 assert_eq!(editor.marked_text_ranges(cx), None);
304
305 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
306 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
307 assert_eq!(editor.text(cx), "ābcdè");
308 assert_eq!(
309 editor.marked_text_ranges(cx),
310 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
311 );
312
313 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
314 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
315 assert_eq!(editor.text(cx), "ābcdę");
316 assert_eq!(editor.marked_text_ranges(cx), None);
317
318 // Start a new IME composition with multiple cursors.
319 editor.change_selections(None, window, cx, |s| {
320 s.select_ranges([
321 OffsetUtf16(1)..OffsetUtf16(1),
322 OffsetUtf16(3)..OffsetUtf16(3),
323 OffsetUtf16(5)..OffsetUtf16(5),
324 ])
325 });
326 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
327 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
328 assert_eq!(
329 editor.marked_text_ranges(cx),
330 Some(vec![
331 OffsetUtf16(0)..OffsetUtf16(3),
332 OffsetUtf16(4)..OffsetUtf16(7),
333 OffsetUtf16(8)..OffsetUtf16(11)
334 ])
335 );
336
337 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
338 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
339 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
340 assert_eq!(
341 editor.marked_text_ranges(cx),
342 Some(vec![
343 OffsetUtf16(1)..OffsetUtf16(2),
344 OffsetUtf16(5)..OffsetUtf16(6),
345 OffsetUtf16(9)..OffsetUtf16(10)
346 ])
347 );
348
349 // Finalize IME composition with multiple cursors.
350 editor.replace_text_in_range(Some(9..10), "2", window, cx);
351 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
352 assert_eq!(editor.marked_text_ranges(cx), None);
353
354 editor
355 });
356}
357
358#[gpui::test]
359fn test_selection_with_mouse(cx: &mut TestAppContext) {
360 init_test(cx, |_| {});
361
362 let editor = cx.add_window(|window, cx| {
363 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
364 build_editor(buffer, window, cx)
365 });
366
367 _ = editor.update(cx, |editor, window, cx| {
368 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
369 });
370 assert_eq!(
371 editor
372 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
373 .unwrap(),
374 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
375 );
376
377 _ = editor.update(cx, |editor, window, cx| {
378 editor.update_selection(
379 DisplayPoint::new(DisplayRow(3), 3),
380 0,
381 gpui::Point::<f32>::default(),
382 window,
383 cx,
384 );
385 });
386
387 assert_eq!(
388 editor
389 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
390 .unwrap(),
391 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
392 );
393
394 _ = editor.update(cx, |editor, window, cx| {
395 editor.update_selection(
396 DisplayPoint::new(DisplayRow(1), 1),
397 0,
398 gpui::Point::<f32>::default(),
399 window,
400 cx,
401 );
402 });
403
404 assert_eq!(
405 editor
406 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
407 .unwrap(),
408 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
409 );
410
411 _ = editor.update(cx, |editor, window, cx| {
412 editor.end_selection(window, cx);
413 editor.update_selection(
414 DisplayPoint::new(DisplayRow(3), 3),
415 0,
416 gpui::Point::<f32>::default(),
417 window,
418 cx,
419 );
420 });
421
422 assert_eq!(
423 editor
424 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
425 .unwrap(),
426 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
427 );
428
429 _ = editor.update(cx, |editor, window, cx| {
430 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
431 editor.update_selection(
432 DisplayPoint::new(DisplayRow(0), 0),
433 0,
434 gpui::Point::<f32>::default(),
435 window,
436 cx,
437 );
438 });
439
440 assert_eq!(
441 editor
442 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
443 .unwrap(),
444 [
445 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
446 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
447 ]
448 );
449
450 _ = editor.update(cx, |editor, window, cx| {
451 editor.end_selection(window, cx);
452 });
453
454 assert_eq!(
455 editor
456 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
457 .unwrap(),
458 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
459 );
460}
461
462#[gpui::test]
463fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
464 init_test(cx, |_| {});
465
466 let editor = cx.add_window(|window, cx| {
467 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
468 build_editor(buffer, window, cx)
469 });
470
471 _ = editor.update(cx, |editor, window, cx| {
472 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
473 });
474
475 _ = editor.update(cx, |editor, window, cx| {
476 editor.end_selection(window, cx);
477 });
478
479 _ = editor.update(cx, |editor, window, cx| {
480 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
481 });
482
483 _ = editor.update(cx, |editor, window, cx| {
484 editor.end_selection(window, cx);
485 });
486
487 assert_eq!(
488 editor
489 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
490 .unwrap(),
491 [
492 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
493 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
494 ]
495 );
496
497 _ = editor.update(cx, |editor, window, cx| {
498 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
499 });
500
501 _ = editor.update(cx, |editor, window, cx| {
502 editor.end_selection(window, cx);
503 });
504
505 assert_eq!(
506 editor
507 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
508 .unwrap(),
509 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
510 );
511}
512
513#[gpui::test]
514fn test_canceling_pending_selection(cx: &mut TestAppContext) {
515 init_test(cx, |_| {});
516
517 let editor = cx.add_window(|window, cx| {
518 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
519 build_editor(buffer, window, cx)
520 });
521
522 _ = editor.update(cx, |editor, window, cx| {
523 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
524 assert_eq!(
525 editor.selections.display_ranges(cx),
526 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
527 );
528 });
529
530 _ = editor.update(cx, |editor, window, cx| {
531 editor.update_selection(
532 DisplayPoint::new(DisplayRow(3), 3),
533 0,
534 gpui::Point::<f32>::default(),
535 window,
536 cx,
537 );
538 assert_eq!(
539 editor.selections.display_ranges(cx),
540 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
541 );
542 });
543
544 _ = editor.update(cx, |editor, window, cx| {
545 editor.cancel(&Cancel, window, cx);
546 editor.update_selection(
547 DisplayPoint::new(DisplayRow(1), 1),
548 0,
549 gpui::Point::<f32>::default(),
550 window,
551 cx,
552 );
553 assert_eq!(
554 editor.selections.display_ranges(cx),
555 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
556 );
557 });
558}
559
560#[gpui::test]
561fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
562 init_test(cx, |_| {});
563
564 let editor = cx.add_window(|window, cx| {
565 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
566 build_editor(buffer, window, cx)
567 });
568
569 _ = editor.update(cx, |editor, window, cx| {
570 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
571 assert_eq!(
572 editor.selections.display_ranges(cx),
573 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
574 );
575
576 editor.move_down(&Default::default(), window, cx);
577 assert_eq!(
578 editor.selections.display_ranges(cx),
579 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
580 );
581
582 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
583 assert_eq!(
584 editor.selections.display_ranges(cx),
585 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
586 );
587
588 editor.move_up(&Default::default(), window, cx);
589 assert_eq!(
590 editor.selections.display_ranges(cx),
591 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
592 );
593 });
594}
595
596#[gpui::test]
597fn test_clone(cx: &mut TestAppContext) {
598 init_test(cx, |_| {});
599
600 let (text, selection_ranges) = marked_text_ranges(
601 indoc! {"
602 one
603 two
604 threeˇ
605 four
606 fiveˇ
607 "},
608 true,
609 );
610
611 let editor = cx.add_window(|window, cx| {
612 let buffer = MultiBuffer::build_simple(&text, cx);
613 build_editor(buffer, window, cx)
614 });
615
616 _ = editor.update(cx, |editor, window, cx| {
617 editor.change_selections(None, window, cx, |s| {
618 s.select_ranges(selection_ranges.clone())
619 });
620 editor.fold_creases(
621 vec![
622 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
623 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
624 ],
625 true,
626 window,
627 cx,
628 );
629 });
630
631 let cloned_editor = editor
632 .update(cx, |editor, _, cx| {
633 cx.open_window(Default::default(), |window, cx| {
634 cx.new(|cx| editor.clone(window, cx))
635 })
636 })
637 .unwrap()
638 .unwrap();
639
640 let snapshot = editor
641 .update(cx, |e, window, cx| e.snapshot(window, cx))
642 .unwrap();
643 let cloned_snapshot = cloned_editor
644 .update(cx, |e, window, cx| e.snapshot(window, cx))
645 .unwrap();
646
647 assert_eq!(
648 cloned_editor
649 .update(cx, |e, _, cx| e.display_text(cx))
650 .unwrap(),
651 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
652 );
653 assert_eq!(
654 cloned_snapshot
655 .folds_in_range(0..text.len())
656 .collect::<Vec<_>>(),
657 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
658 );
659 assert_set_eq!(
660 cloned_editor
661 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
662 .unwrap(),
663 editor
664 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
665 .unwrap()
666 );
667 assert_set_eq!(
668 cloned_editor
669 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
670 .unwrap(),
671 editor
672 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
673 .unwrap()
674 );
675}
676
677#[gpui::test]
678async fn test_navigation_history(cx: &mut TestAppContext) {
679 init_test(cx, |_| {});
680
681 use workspace::item::Item;
682
683 let fs = FakeFs::new(cx.executor());
684 let project = Project::test(fs, [], cx).await;
685 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
686 let pane = workspace
687 .update(cx, |workspace, _, _| workspace.active_pane().clone())
688 .unwrap();
689
690 _ = workspace.update(cx, |_v, window, cx| {
691 cx.new(|cx| {
692 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
693 let mut editor = build_editor(buffer.clone(), window, cx);
694 let handle = cx.entity();
695 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
696
697 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
698 editor.nav_history.as_mut().unwrap().pop_backward(cx)
699 }
700
701 // Move the cursor a small distance.
702 // Nothing is added to the navigation history.
703 editor.change_selections(None, window, cx, |s| {
704 s.select_display_ranges([
705 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
706 ])
707 });
708 editor.change_selections(None, window, cx, |s| {
709 s.select_display_ranges([
710 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
711 ])
712 });
713 assert!(pop_history(&mut editor, cx).is_none());
714
715 // Move the cursor a large distance.
716 // The history can jump back to the previous position.
717 editor.change_selections(None, window, cx, |s| {
718 s.select_display_ranges([
719 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
720 ])
721 });
722 let nav_entry = pop_history(&mut editor, cx).unwrap();
723 editor.navigate(nav_entry.data.unwrap(), window, cx);
724 assert_eq!(nav_entry.item.id(), cx.entity_id());
725 assert_eq!(
726 editor.selections.display_ranges(cx),
727 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
728 );
729 assert!(pop_history(&mut editor, cx).is_none());
730
731 // Move the cursor a small distance via the mouse.
732 // Nothing is added to the navigation history.
733 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
734 editor.end_selection(window, cx);
735 assert_eq!(
736 editor.selections.display_ranges(cx),
737 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
738 );
739 assert!(pop_history(&mut editor, cx).is_none());
740
741 // Move the cursor a large distance via the mouse.
742 // The history can jump back to the previous position.
743 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
744 editor.end_selection(window, cx);
745 assert_eq!(
746 editor.selections.display_ranges(cx),
747 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
748 );
749 let nav_entry = pop_history(&mut editor, cx).unwrap();
750 editor.navigate(nav_entry.data.unwrap(), window, cx);
751 assert_eq!(nav_entry.item.id(), cx.entity_id());
752 assert_eq!(
753 editor.selections.display_ranges(cx),
754 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
755 );
756 assert!(pop_history(&mut editor, cx).is_none());
757
758 // Set scroll position to check later
759 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
760 let original_scroll_position = editor.scroll_manager.anchor();
761
762 // Jump to the end of the document and adjust scroll
763 editor.move_to_end(&MoveToEnd, window, cx);
764 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
765 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
766
767 let nav_entry = pop_history(&mut editor, cx).unwrap();
768 editor.navigate(nav_entry.data.unwrap(), window, cx);
769 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
770
771 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
772 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
773 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
774 let invalid_point = Point::new(9999, 0);
775 editor.navigate(
776 Box::new(NavigationData {
777 cursor_anchor: invalid_anchor,
778 cursor_position: invalid_point,
779 scroll_anchor: ScrollAnchor {
780 anchor: invalid_anchor,
781 offset: Default::default(),
782 },
783 scroll_top_row: invalid_point.row,
784 }),
785 window,
786 cx,
787 );
788 assert_eq!(
789 editor.selections.display_ranges(cx),
790 &[editor.max_point(cx)..editor.max_point(cx)]
791 );
792 assert_eq!(
793 editor.scroll_position(cx),
794 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
795 );
796
797 editor
798 })
799 });
800}
801
802#[gpui::test]
803fn test_cancel(cx: &mut TestAppContext) {
804 init_test(cx, |_| {});
805
806 let editor = cx.add_window(|window, cx| {
807 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
808 build_editor(buffer, window, cx)
809 });
810
811 _ = editor.update(cx, |editor, window, cx| {
812 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
813 editor.update_selection(
814 DisplayPoint::new(DisplayRow(1), 1),
815 0,
816 gpui::Point::<f32>::default(),
817 window,
818 cx,
819 );
820 editor.end_selection(window, cx);
821
822 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
823 editor.update_selection(
824 DisplayPoint::new(DisplayRow(0), 3),
825 0,
826 gpui::Point::<f32>::default(),
827 window,
828 cx,
829 );
830 editor.end_selection(window, cx);
831 assert_eq!(
832 editor.selections.display_ranges(cx),
833 [
834 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
835 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
836 ]
837 );
838 });
839
840 _ = editor.update(cx, |editor, window, cx| {
841 editor.cancel(&Cancel, window, cx);
842 assert_eq!(
843 editor.selections.display_ranges(cx),
844 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
845 );
846 });
847
848 _ = editor.update(cx, |editor, window, cx| {
849 editor.cancel(&Cancel, window, cx);
850 assert_eq!(
851 editor.selections.display_ranges(cx),
852 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
853 );
854 });
855}
856
857#[gpui::test]
858fn test_fold_action(cx: &mut TestAppContext) {
859 init_test(cx, |_| {});
860
861 let editor = cx.add_window(|window, cx| {
862 let buffer = MultiBuffer::build_simple(
863 &"
864 impl Foo {
865 // Hello!
866
867 fn a() {
868 1
869 }
870
871 fn b() {
872 2
873 }
874
875 fn c() {
876 3
877 }
878 }
879 "
880 .unindent(),
881 cx,
882 );
883 build_editor(buffer.clone(), window, cx)
884 });
885
886 _ = editor.update(cx, |editor, window, cx| {
887 editor.change_selections(None, window, cx, |s| {
888 s.select_display_ranges([
889 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
890 ]);
891 });
892 editor.fold(&Fold, window, cx);
893 assert_eq!(
894 editor.display_text(cx),
895 "
896 impl Foo {
897 // Hello!
898
899 fn a() {
900 1
901 }
902
903 fn b() {⋯
904 }
905
906 fn c() {⋯
907 }
908 }
909 "
910 .unindent(),
911 );
912
913 editor.fold(&Fold, window, cx);
914 assert_eq!(
915 editor.display_text(cx),
916 "
917 impl Foo {⋯
918 }
919 "
920 .unindent(),
921 );
922
923 editor.unfold_lines(&UnfoldLines, window, cx);
924 assert_eq!(
925 editor.display_text(cx),
926 "
927 impl Foo {
928 // Hello!
929
930 fn a() {
931 1
932 }
933
934 fn b() {⋯
935 }
936
937 fn c() {⋯
938 }
939 }
940 "
941 .unindent(),
942 );
943
944 editor.unfold_lines(&UnfoldLines, window, cx);
945 assert_eq!(
946 editor.display_text(cx),
947 editor.buffer.read(cx).read(cx).text()
948 );
949 });
950}
951
952#[gpui::test]
953fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
954 init_test(cx, |_| {});
955
956 let editor = cx.add_window(|window, cx| {
957 let buffer = MultiBuffer::build_simple(
958 &"
959 class Foo:
960 # Hello!
961
962 def a():
963 print(1)
964
965 def b():
966 print(2)
967
968 def c():
969 print(3)
970 "
971 .unindent(),
972 cx,
973 );
974 build_editor(buffer.clone(), window, cx)
975 });
976
977 _ = editor.update(cx, |editor, window, cx| {
978 editor.change_selections(None, window, cx, |s| {
979 s.select_display_ranges([
980 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
981 ]);
982 });
983 editor.fold(&Fold, window, cx);
984 assert_eq!(
985 editor.display_text(cx),
986 "
987 class Foo:
988 # Hello!
989
990 def a():
991 print(1)
992
993 def b():⋯
994
995 def c():⋯
996 "
997 .unindent(),
998 );
999
1000 editor.fold(&Fold, window, cx);
1001 assert_eq!(
1002 editor.display_text(cx),
1003 "
1004 class Foo:⋯
1005 "
1006 .unindent(),
1007 );
1008
1009 editor.unfold_lines(&UnfoldLines, window, cx);
1010 assert_eq!(
1011 editor.display_text(cx),
1012 "
1013 class Foo:
1014 # Hello!
1015
1016 def a():
1017 print(1)
1018
1019 def b():⋯
1020
1021 def c():⋯
1022 "
1023 .unindent(),
1024 );
1025
1026 editor.unfold_lines(&UnfoldLines, window, cx);
1027 assert_eq!(
1028 editor.display_text(cx),
1029 editor.buffer.read(cx).read(cx).text()
1030 );
1031 });
1032}
1033
1034#[gpui::test]
1035fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1036 init_test(cx, |_| {});
1037
1038 let editor = cx.add_window(|window, cx| {
1039 let buffer = MultiBuffer::build_simple(
1040 &"
1041 class Foo:
1042 # Hello!
1043
1044 def a():
1045 print(1)
1046
1047 def b():
1048 print(2)
1049
1050
1051 def c():
1052 print(3)
1053
1054
1055 "
1056 .unindent(),
1057 cx,
1058 );
1059 build_editor(buffer.clone(), window, cx)
1060 });
1061
1062 _ = editor.update(cx, |editor, window, cx| {
1063 editor.change_selections(None, window, cx, |s| {
1064 s.select_display_ranges([
1065 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1066 ]);
1067 });
1068 editor.fold(&Fold, window, cx);
1069 assert_eq!(
1070 editor.display_text(cx),
1071 "
1072 class Foo:
1073 # Hello!
1074
1075 def a():
1076 print(1)
1077
1078 def b():⋯
1079
1080
1081 def c():⋯
1082
1083
1084 "
1085 .unindent(),
1086 );
1087
1088 editor.fold(&Fold, window, cx);
1089 assert_eq!(
1090 editor.display_text(cx),
1091 "
1092 class Foo:⋯
1093
1094
1095 "
1096 .unindent(),
1097 );
1098
1099 editor.unfold_lines(&UnfoldLines, window, cx);
1100 assert_eq!(
1101 editor.display_text(cx),
1102 "
1103 class Foo:
1104 # Hello!
1105
1106 def a():
1107 print(1)
1108
1109 def b():⋯
1110
1111
1112 def c():⋯
1113
1114
1115 "
1116 .unindent(),
1117 );
1118
1119 editor.unfold_lines(&UnfoldLines, window, cx);
1120 assert_eq!(
1121 editor.display_text(cx),
1122 editor.buffer.read(cx).read(cx).text()
1123 );
1124 });
1125}
1126
1127#[gpui::test]
1128fn test_fold_at_level(cx: &mut TestAppContext) {
1129 init_test(cx, |_| {});
1130
1131 let editor = cx.add_window(|window, cx| {
1132 let buffer = MultiBuffer::build_simple(
1133 &"
1134 class Foo:
1135 # Hello!
1136
1137 def a():
1138 print(1)
1139
1140 def b():
1141 print(2)
1142
1143
1144 class Bar:
1145 # World!
1146
1147 def a():
1148 print(1)
1149
1150 def b():
1151 print(2)
1152
1153
1154 "
1155 .unindent(),
1156 cx,
1157 );
1158 build_editor(buffer.clone(), window, cx)
1159 });
1160
1161 _ = editor.update(cx, |editor, window, cx| {
1162 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1163 assert_eq!(
1164 editor.display_text(cx),
1165 "
1166 class Foo:
1167 # Hello!
1168
1169 def a():⋯
1170
1171 def b():⋯
1172
1173
1174 class Bar:
1175 # World!
1176
1177 def a():⋯
1178
1179 def b():⋯
1180
1181
1182 "
1183 .unindent(),
1184 );
1185
1186 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1187 assert_eq!(
1188 editor.display_text(cx),
1189 "
1190 class Foo:⋯
1191
1192
1193 class Bar:⋯
1194
1195
1196 "
1197 .unindent(),
1198 );
1199
1200 editor.unfold_all(&UnfoldAll, window, cx);
1201 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1202 assert_eq!(
1203 editor.display_text(cx),
1204 "
1205 class Foo:
1206 # Hello!
1207
1208 def a():
1209 print(1)
1210
1211 def b():
1212 print(2)
1213
1214
1215 class Bar:
1216 # World!
1217
1218 def a():
1219 print(1)
1220
1221 def b():
1222 print(2)
1223
1224
1225 "
1226 .unindent(),
1227 );
1228
1229 assert_eq!(
1230 editor.display_text(cx),
1231 editor.buffer.read(cx).read(cx).text()
1232 );
1233 });
1234}
1235
1236#[gpui::test]
1237fn test_move_cursor(cx: &mut TestAppContext) {
1238 init_test(cx, |_| {});
1239
1240 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1241 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1242
1243 buffer.update(cx, |buffer, cx| {
1244 buffer.edit(
1245 vec![
1246 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1247 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1248 ],
1249 None,
1250 cx,
1251 );
1252 });
1253 _ = editor.update(cx, |editor, window, cx| {
1254 assert_eq!(
1255 editor.selections.display_ranges(cx),
1256 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1257 );
1258
1259 editor.move_down(&MoveDown, window, cx);
1260 assert_eq!(
1261 editor.selections.display_ranges(cx),
1262 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1263 );
1264
1265 editor.move_right(&MoveRight, window, cx);
1266 assert_eq!(
1267 editor.selections.display_ranges(cx),
1268 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1269 );
1270
1271 editor.move_left(&MoveLeft, window, cx);
1272 assert_eq!(
1273 editor.selections.display_ranges(cx),
1274 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1275 );
1276
1277 editor.move_up(&MoveUp, window, cx);
1278 assert_eq!(
1279 editor.selections.display_ranges(cx),
1280 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1281 );
1282
1283 editor.move_to_end(&MoveToEnd, window, cx);
1284 assert_eq!(
1285 editor.selections.display_ranges(cx),
1286 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1287 );
1288
1289 editor.move_to_beginning(&MoveToBeginning, window, cx);
1290 assert_eq!(
1291 editor.selections.display_ranges(cx),
1292 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1293 );
1294
1295 editor.change_selections(None, window, cx, |s| {
1296 s.select_display_ranges([
1297 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1298 ]);
1299 });
1300 editor.select_to_beginning(&SelectToBeginning, window, cx);
1301 assert_eq!(
1302 editor.selections.display_ranges(cx),
1303 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1304 );
1305
1306 editor.select_to_end(&SelectToEnd, window, cx);
1307 assert_eq!(
1308 editor.selections.display_ranges(cx),
1309 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1310 );
1311 });
1312}
1313
1314// TODO: Re-enable this test
1315#[cfg(target_os = "macos")]
1316#[gpui::test]
1317fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1318 init_test(cx, |_| {});
1319
1320 let editor = cx.add_window(|window, cx| {
1321 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1322 build_editor(buffer.clone(), window, cx)
1323 });
1324
1325 assert_eq!('🟥'.len_utf8(), 4);
1326 assert_eq!('α'.len_utf8(), 2);
1327
1328 _ = editor.update(cx, |editor, window, cx| {
1329 editor.fold_creases(
1330 vec![
1331 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1332 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1333 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1334 ],
1335 true,
1336 window,
1337 cx,
1338 );
1339 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1340
1341 editor.move_right(&MoveRight, window, cx);
1342 assert_eq!(
1343 editor.selections.display_ranges(cx),
1344 &[empty_range(0, "🟥".len())]
1345 );
1346 editor.move_right(&MoveRight, window, cx);
1347 assert_eq!(
1348 editor.selections.display_ranges(cx),
1349 &[empty_range(0, "🟥🟧".len())]
1350 );
1351 editor.move_right(&MoveRight, window, cx);
1352 assert_eq!(
1353 editor.selections.display_ranges(cx),
1354 &[empty_range(0, "🟥🟧⋯".len())]
1355 );
1356
1357 editor.move_down(&MoveDown, window, cx);
1358 assert_eq!(
1359 editor.selections.display_ranges(cx),
1360 &[empty_range(1, "ab⋯e".len())]
1361 );
1362 editor.move_left(&MoveLeft, window, cx);
1363 assert_eq!(
1364 editor.selections.display_ranges(cx),
1365 &[empty_range(1, "ab⋯".len())]
1366 );
1367 editor.move_left(&MoveLeft, window, cx);
1368 assert_eq!(
1369 editor.selections.display_ranges(cx),
1370 &[empty_range(1, "ab".len())]
1371 );
1372 editor.move_left(&MoveLeft, window, cx);
1373 assert_eq!(
1374 editor.selections.display_ranges(cx),
1375 &[empty_range(1, "a".len())]
1376 );
1377
1378 editor.move_down(&MoveDown, window, cx);
1379 assert_eq!(
1380 editor.selections.display_ranges(cx),
1381 &[empty_range(2, "α".len())]
1382 );
1383 editor.move_right(&MoveRight, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[empty_range(2, "αβ".len())]
1387 );
1388 editor.move_right(&MoveRight, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[empty_range(2, "αβ⋯".len())]
1392 );
1393 editor.move_right(&MoveRight, window, cx);
1394 assert_eq!(
1395 editor.selections.display_ranges(cx),
1396 &[empty_range(2, "αβ⋯ε".len())]
1397 );
1398
1399 editor.move_up(&MoveUp, window, cx);
1400 assert_eq!(
1401 editor.selections.display_ranges(cx),
1402 &[empty_range(1, "ab⋯e".len())]
1403 );
1404 editor.move_down(&MoveDown, window, cx);
1405 assert_eq!(
1406 editor.selections.display_ranges(cx),
1407 &[empty_range(2, "αβ⋯ε".len())]
1408 );
1409 editor.move_up(&MoveUp, window, cx);
1410 assert_eq!(
1411 editor.selections.display_ranges(cx),
1412 &[empty_range(1, "ab⋯e".len())]
1413 );
1414
1415 editor.move_up(&MoveUp, window, cx);
1416 assert_eq!(
1417 editor.selections.display_ranges(cx),
1418 &[empty_range(0, "🟥🟧".len())]
1419 );
1420 editor.move_left(&MoveLeft, window, cx);
1421 assert_eq!(
1422 editor.selections.display_ranges(cx),
1423 &[empty_range(0, "🟥".len())]
1424 );
1425 editor.move_left(&MoveLeft, window, cx);
1426 assert_eq!(
1427 editor.selections.display_ranges(cx),
1428 &[empty_range(0, "".len())]
1429 );
1430 });
1431}
1432
1433#[gpui::test]
1434fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1435 init_test(cx, |_| {});
1436
1437 let editor = cx.add_window(|window, cx| {
1438 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1439 build_editor(buffer.clone(), window, cx)
1440 });
1441 _ = editor.update(cx, |editor, window, cx| {
1442 editor.change_selections(None, window, cx, |s| {
1443 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1444 });
1445
1446 // moving above start of document should move selection to start of document,
1447 // but the next move down should still be at the original goal_x
1448 editor.move_up(&MoveUp, window, cx);
1449 assert_eq!(
1450 editor.selections.display_ranges(cx),
1451 &[empty_range(0, "".len())]
1452 );
1453
1454 editor.move_down(&MoveDown, window, cx);
1455 assert_eq!(
1456 editor.selections.display_ranges(cx),
1457 &[empty_range(1, "abcd".len())]
1458 );
1459
1460 editor.move_down(&MoveDown, window, cx);
1461 assert_eq!(
1462 editor.selections.display_ranges(cx),
1463 &[empty_range(2, "αβγ".len())]
1464 );
1465
1466 editor.move_down(&MoveDown, window, cx);
1467 assert_eq!(
1468 editor.selections.display_ranges(cx),
1469 &[empty_range(3, "abcd".len())]
1470 );
1471
1472 editor.move_down(&MoveDown, window, cx);
1473 assert_eq!(
1474 editor.selections.display_ranges(cx),
1475 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1476 );
1477
1478 // moving past end of document should not change goal_x
1479 editor.move_down(&MoveDown, window, cx);
1480 assert_eq!(
1481 editor.selections.display_ranges(cx),
1482 &[empty_range(5, "".len())]
1483 );
1484
1485 editor.move_down(&MoveDown, window, cx);
1486 assert_eq!(
1487 editor.selections.display_ranges(cx),
1488 &[empty_range(5, "".len())]
1489 );
1490
1491 editor.move_up(&MoveUp, window, cx);
1492 assert_eq!(
1493 editor.selections.display_ranges(cx),
1494 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1495 );
1496
1497 editor.move_up(&MoveUp, window, cx);
1498 assert_eq!(
1499 editor.selections.display_ranges(cx),
1500 &[empty_range(3, "abcd".len())]
1501 );
1502
1503 editor.move_up(&MoveUp, window, cx);
1504 assert_eq!(
1505 editor.selections.display_ranges(cx),
1506 &[empty_range(2, "αβγ".len())]
1507 );
1508 });
1509}
1510
1511#[gpui::test]
1512fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1513 init_test(cx, |_| {});
1514 let move_to_beg = MoveToBeginningOfLine {
1515 stop_at_soft_wraps: true,
1516 };
1517
1518 let move_to_end = MoveToEndOfLine {
1519 stop_at_soft_wraps: true,
1520 };
1521
1522 let editor = cx.add_window(|window, cx| {
1523 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1524 build_editor(buffer, window, cx)
1525 });
1526 _ = editor.update(cx, |editor, window, cx| {
1527 editor.change_selections(None, window, cx, |s| {
1528 s.select_display_ranges([
1529 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1530 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1531 ]);
1532 });
1533 });
1534
1535 _ = editor.update(cx, |editor, window, cx| {
1536 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1537 assert_eq!(
1538 editor.selections.display_ranges(cx),
1539 &[
1540 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1541 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1542 ]
1543 );
1544 });
1545
1546 _ = editor.update(cx, |editor, window, cx| {
1547 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1548 assert_eq!(
1549 editor.selections.display_ranges(cx),
1550 &[
1551 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1552 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1553 ]
1554 );
1555 });
1556
1557 _ = editor.update(cx, |editor, window, cx| {
1558 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1559 assert_eq!(
1560 editor.selections.display_ranges(cx),
1561 &[
1562 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1563 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1564 ]
1565 );
1566 });
1567
1568 _ = editor.update(cx, |editor, window, cx| {
1569 editor.move_to_end_of_line(&move_to_end, window, cx);
1570 assert_eq!(
1571 editor.selections.display_ranges(cx),
1572 &[
1573 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1574 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1575 ]
1576 );
1577 });
1578
1579 // Moving to the end of line again is a no-op.
1580 _ = editor.update(cx, |editor, window, cx| {
1581 editor.move_to_end_of_line(&move_to_end, window, cx);
1582 assert_eq!(
1583 editor.selections.display_ranges(cx),
1584 &[
1585 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1586 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1587 ]
1588 );
1589 });
1590
1591 _ = editor.update(cx, |editor, window, cx| {
1592 editor.move_left(&MoveLeft, window, cx);
1593 editor.select_to_beginning_of_line(
1594 &SelectToBeginningOfLine {
1595 stop_at_soft_wraps: true,
1596 },
1597 window,
1598 cx,
1599 );
1600 assert_eq!(
1601 editor.selections.display_ranges(cx),
1602 &[
1603 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1604 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1605 ]
1606 );
1607 });
1608
1609 _ = editor.update(cx, |editor, window, cx| {
1610 editor.select_to_beginning_of_line(
1611 &SelectToBeginningOfLine {
1612 stop_at_soft_wraps: true,
1613 },
1614 window,
1615 cx,
1616 );
1617 assert_eq!(
1618 editor.selections.display_ranges(cx),
1619 &[
1620 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1621 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1622 ]
1623 );
1624 });
1625
1626 _ = editor.update(cx, |editor, window, cx| {
1627 editor.select_to_beginning_of_line(
1628 &SelectToBeginningOfLine {
1629 stop_at_soft_wraps: true,
1630 },
1631 window,
1632 cx,
1633 );
1634 assert_eq!(
1635 editor.selections.display_ranges(cx),
1636 &[
1637 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1638 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1639 ]
1640 );
1641 });
1642
1643 _ = editor.update(cx, |editor, window, cx| {
1644 editor.select_to_end_of_line(
1645 &SelectToEndOfLine {
1646 stop_at_soft_wraps: true,
1647 },
1648 window,
1649 cx,
1650 );
1651 assert_eq!(
1652 editor.selections.display_ranges(cx),
1653 &[
1654 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1655 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1656 ]
1657 );
1658 });
1659
1660 _ = editor.update(cx, |editor, window, cx| {
1661 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1662 assert_eq!(editor.display_text(cx), "ab\n de");
1663 assert_eq!(
1664 editor.selections.display_ranges(cx),
1665 &[
1666 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1667 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1668 ]
1669 );
1670 });
1671
1672 _ = editor.update(cx, |editor, window, cx| {
1673 editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx);
1674 assert_eq!(editor.display_text(cx), "\n");
1675 assert_eq!(
1676 editor.selections.display_ranges(cx),
1677 &[
1678 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1679 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1680 ]
1681 );
1682 });
1683}
1684
1685#[gpui::test]
1686fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1687 init_test(cx, |_| {});
1688 let move_to_beg = MoveToBeginningOfLine {
1689 stop_at_soft_wraps: false,
1690 };
1691
1692 let move_to_end = MoveToEndOfLine {
1693 stop_at_soft_wraps: false,
1694 };
1695
1696 let editor = cx.add_window(|window, cx| {
1697 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1698 build_editor(buffer, window, cx)
1699 });
1700
1701 _ = editor.update(cx, |editor, window, cx| {
1702 editor.set_wrap_width(Some(140.0.into()), cx);
1703
1704 // We expect the following lines after wrapping
1705 // ```
1706 // thequickbrownfox
1707 // jumpedoverthelazydo
1708 // gs
1709 // ```
1710 // The final `gs` was soft-wrapped onto a new line.
1711 assert_eq!(
1712 "thequickbrownfox\njumpedoverthelaz\nydogs",
1713 editor.display_text(cx),
1714 );
1715
1716 // First, let's assert behavior on the first line, that was not soft-wrapped.
1717 // Start the cursor at the `k` on the first line
1718 editor.change_selections(None, window, cx, |s| {
1719 s.select_display_ranges([
1720 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1721 ]);
1722 });
1723
1724 // Moving to the beginning of the line should put us at the beginning of the line.
1725 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1726 assert_eq!(
1727 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1728 editor.selections.display_ranges(cx)
1729 );
1730
1731 // Moving to the end of the line should put us at the end of the line.
1732 editor.move_to_end_of_line(&move_to_end, window, cx);
1733 assert_eq!(
1734 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1735 editor.selections.display_ranges(cx)
1736 );
1737
1738 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1739 // Start the cursor at the last line (`y` that was wrapped to a new line)
1740 editor.change_selections(None, window, cx, |s| {
1741 s.select_display_ranges([
1742 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1743 ]);
1744 });
1745
1746 // Moving to the beginning of the line should put us at the start of the second line of
1747 // display text, i.e., the `j`.
1748 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1749 assert_eq!(
1750 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1751 editor.selections.display_ranges(cx)
1752 );
1753
1754 // Moving to the beginning of the line again should be a no-op.
1755 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1756 assert_eq!(
1757 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1758 editor.selections.display_ranges(cx)
1759 );
1760
1761 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1762 // next display line.
1763 editor.move_to_end_of_line(&move_to_end, window, cx);
1764 assert_eq!(
1765 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1766 editor.selections.display_ranges(cx)
1767 );
1768
1769 // Moving to the end of the line again should be a no-op.
1770 editor.move_to_end_of_line(&move_to_end, window, cx);
1771 assert_eq!(
1772 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1773 editor.selections.display_ranges(cx)
1774 );
1775 });
1776}
1777
1778#[gpui::test]
1779fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1780 init_test(cx, |_| {});
1781
1782 let editor = cx.add_window(|window, cx| {
1783 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1784 build_editor(buffer, window, cx)
1785 });
1786 _ = editor.update(cx, |editor, window, cx| {
1787 editor.change_selections(None, window, cx, |s| {
1788 s.select_display_ranges([
1789 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1790 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1791 ])
1792 });
1793
1794 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1795 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1796
1797 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1798 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1799
1800 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1801 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1802
1803 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1804 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1805
1806 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1807 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1808
1809 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1810 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1811
1812 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1813 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1814
1815 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1816 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1817
1818 editor.move_right(&MoveRight, window, cx);
1819 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1820 assert_selection_ranges(
1821 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1822 editor,
1823 cx,
1824 );
1825
1826 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1827 assert_selection_ranges(
1828 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1829 editor,
1830 cx,
1831 );
1832
1833 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1834 assert_selection_ranges(
1835 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1836 editor,
1837 cx,
1838 );
1839 });
1840}
1841
1842#[gpui::test]
1843fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1844 init_test(cx, |_| {});
1845
1846 let editor = cx.add_window(|window, cx| {
1847 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1848 build_editor(buffer, window, cx)
1849 });
1850
1851 _ = editor.update(cx, |editor, window, cx| {
1852 editor.set_wrap_width(Some(140.0.into()), cx);
1853 assert_eq!(
1854 editor.display_text(cx),
1855 "use one::{\n two::three::\n four::five\n};"
1856 );
1857
1858 editor.change_selections(None, window, cx, |s| {
1859 s.select_display_ranges([
1860 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1861 ]);
1862 });
1863
1864 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1865 assert_eq!(
1866 editor.selections.display_ranges(cx),
1867 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1868 );
1869
1870 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1871 assert_eq!(
1872 editor.selections.display_ranges(cx),
1873 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1874 );
1875
1876 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1877 assert_eq!(
1878 editor.selections.display_ranges(cx),
1879 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1880 );
1881
1882 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1883 assert_eq!(
1884 editor.selections.display_ranges(cx),
1885 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
1886 );
1887
1888 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1889 assert_eq!(
1890 editor.selections.display_ranges(cx),
1891 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1892 );
1893
1894 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1895 assert_eq!(
1896 editor.selections.display_ranges(cx),
1897 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1898 );
1899 });
1900}
1901
1902#[gpui::test]
1903async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1904 init_test(cx, |_| {});
1905 let mut cx = EditorTestContext::new(cx).await;
1906
1907 let line_height = cx.editor(|editor, window, _| {
1908 editor
1909 .style()
1910 .unwrap()
1911 .text
1912 .line_height_in_pixels(window.rem_size())
1913 });
1914 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
1915
1916 cx.set_state(
1917 &r#"ˇone
1918 two
1919
1920 three
1921 fourˇ
1922 five
1923
1924 six"#
1925 .unindent(),
1926 );
1927
1928 cx.update_editor(|editor, window, cx| {
1929 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1930 });
1931 cx.assert_editor_state(
1932 &r#"one
1933 two
1934 ˇ
1935 three
1936 four
1937 five
1938 ˇ
1939 six"#
1940 .unindent(),
1941 );
1942
1943 cx.update_editor(|editor, window, cx| {
1944 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1945 });
1946 cx.assert_editor_state(
1947 &r#"one
1948 two
1949
1950 three
1951 four
1952 five
1953 ˇ
1954 sixˇ"#
1955 .unindent(),
1956 );
1957
1958 cx.update_editor(|editor, window, cx| {
1959 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1960 });
1961 cx.assert_editor_state(
1962 &r#"one
1963 two
1964
1965 three
1966 four
1967 five
1968
1969 sixˇ"#
1970 .unindent(),
1971 );
1972
1973 cx.update_editor(|editor, window, cx| {
1974 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
1975 });
1976 cx.assert_editor_state(
1977 &r#"one
1978 two
1979
1980 three
1981 four
1982 five
1983 ˇ
1984 six"#
1985 .unindent(),
1986 );
1987
1988 cx.update_editor(|editor, window, cx| {
1989 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
1990 });
1991 cx.assert_editor_state(
1992 &r#"one
1993 two
1994 ˇ
1995 three
1996 four
1997 five
1998
1999 six"#
2000 .unindent(),
2001 );
2002
2003 cx.update_editor(|editor, window, cx| {
2004 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2005 });
2006 cx.assert_editor_state(
2007 &r#"ˇone
2008 two
2009
2010 three
2011 four
2012 five
2013
2014 six"#
2015 .unindent(),
2016 );
2017}
2018
2019#[gpui::test]
2020async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
2021 init_test(cx, |_| {});
2022 let mut cx = EditorTestContext::new(cx).await;
2023 let line_height = cx.editor(|editor, window, _| {
2024 editor
2025 .style()
2026 .unwrap()
2027 .text
2028 .line_height_in_pixels(window.rem_size())
2029 });
2030 let window = cx.window;
2031 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2032
2033 cx.set_state(
2034 r#"ˇone
2035 two
2036 three
2037 four
2038 five
2039 six
2040 seven
2041 eight
2042 nine
2043 ten
2044 "#,
2045 );
2046
2047 cx.update_editor(|editor, window, cx| {
2048 assert_eq!(
2049 editor.snapshot(window, cx).scroll_position(),
2050 gpui::Point::new(0., 0.)
2051 );
2052 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2053 assert_eq!(
2054 editor.snapshot(window, cx).scroll_position(),
2055 gpui::Point::new(0., 3.)
2056 );
2057 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2058 assert_eq!(
2059 editor.snapshot(window, cx).scroll_position(),
2060 gpui::Point::new(0., 6.)
2061 );
2062 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2063 assert_eq!(
2064 editor.snapshot(window, cx).scroll_position(),
2065 gpui::Point::new(0., 3.)
2066 );
2067
2068 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2069 assert_eq!(
2070 editor.snapshot(window, cx).scroll_position(),
2071 gpui::Point::new(0., 1.)
2072 );
2073 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2074 assert_eq!(
2075 editor.snapshot(window, cx).scroll_position(),
2076 gpui::Point::new(0., 3.)
2077 );
2078 });
2079}
2080
2081#[gpui::test]
2082async fn test_autoscroll(cx: &mut gpui::TestAppContext) {
2083 init_test(cx, |_| {});
2084 let mut cx = EditorTestContext::new(cx).await;
2085
2086 let line_height = cx.update_editor(|editor, window, cx| {
2087 editor.set_vertical_scroll_margin(2, cx);
2088 editor
2089 .style()
2090 .unwrap()
2091 .text
2092 .line_height_in_pixels(window.rem_size())
2093 });
2094 let window = cx.window;
2095 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2096
2097 cx.set_state(
2098 r#"ˇone
2099 two
2100 three
2101 four
2102 five
2103 six
2104 seven
2105 eight
2106 nine
2107 ten
2108 "#,
2109 );
2110 cx.update_editor(|editor, window, cx| {
2111 assert_eq!(
2112 editor.snapshot(window, cx).scroll_position(),
2113 gpui::Point::new(0., 0.0)
2114 );
2115 });
2116
2117 // Add a cursor below the visible area. Since both cursors cannot fit
2118 // on screen, the editor autoscrolls to reveal the newest cursor, and
2119 // allows the vertical scroll margin below that cursor.
2120 cx.update_editor(|editor, window, cx| {
2121 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2122 selections.select_ranges([
2123 Point::new(0, 0)..Point::new(0, 0),
2124 Point::new(6, 0)..Point::new(6, 0),
2125 ]);
2126 })
2127 });
2128 cx.update_editor(|editor, window, cx| {
2129 assert_eq!(
2130 editor.snapshot(window, cx).scroll_position(),
2131 gpui::Point::new(0., 3.0)
2132 );
2133 });
2134
2135 // Move down. The editor cursor scrolls down to track the newest cursor.
2136 cx.update_editor(|editor, window, cx| {
2137 editor.move_down(&Default::default(), window, cx);
2138 });
2139 cx.update_editor(|editor, window, cx| {
2140 assert_eq!(
2141 editor.snapshot(window, cx).scroll_position(),
2142 gpui::Point::new(0., 4.0)
2143 );
2144 });
2145
2146 // Add a cursor above the visible area. Since both cursors fit on screen,
2147 // the editor scrolls to show both.
2148 cx.update_editor(|editor, window, cx| {
2149 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2150 selections.select_ranges([
2151 Point::new(1, 0)..Point::new(1, 0),
2152 Point::new(6, 0)..Point::new(6, 0),
2153 ]);
2154 })
2155 });
2156 cx.update_editor(|editor, window, cx| {
2157 assert_eq!(
2158 editor.snapshot(window, cx).scroll_position(),
2159 gpui::Point::new(0., 1.0)
2160 );
2161 });
2162}
2163
2164#[gpui::test]
2165async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
2166 init_test(cx, |_| {});
2167 let mut cx = EditorTestContext::new(cx).await;
2168
2169 let line_height = cx.editor(|editor, window, _cx| {
2170 editor
2171 .style()
2172 .unwrap()
2173 .text
2174 .line_height_in_pixels(window.rem_size())
2175 });
2176 let window = cx.window;
2177 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2178 cx.set_state(
2179 &r#"
2180 ˇone
2181 two
2182 threeˇ
2183 four
2184 five
2185 six
2186 seven
2187 eight
2188 nine
2189 ten
2190 "#
2191 .unindent(),
2192 );
2193
2194 cx.update_editor(|editor, window, cx| {
2195 editor.move_page_down(&MovePageDown::default(), window, cx)
2196 });
2197 cx.assert_editor_state(
2198 &r#"
2199 one
2200 two
2201 three
2202 ˇfour
2203 five
2204 sixˇ
2205 seven
2206 eight
2207 nine
2208 ten
2209 "#
2210 .unindent(),
2211 );
2212
2213 cx.update_editor(|editor, window, cx| {
2214 editor.move_page_down(&MovePageDown::default(), window, cx)
2215 });
2216 cx.assert_editor_state(
2217 &r#"
2218 one
2219 two
2220 three
2221 four
2222 five
2223 six
2224 ˇseven
2225 eight
2226 nineˇ
2227 ten
2228 "#
2229 .unindent(),
2230 );
2231
2232 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2233 cx.assert_editor_state(
2234 &r#"
2235 one
2236 two
2237 three
2238 ˇfour
2239 five
2240 sixˇ
2241 seven
2242 eight
2243 nine
2244 ten
2245 "#
2246 .unindent(),
2247 );
2248
2249 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2250 cx.assert_editor_state(
2251 &r#"
2252 ˇone
2253 two
2254 threeˇ
2255 four
2256 five
2257 six
2258 seven
2259 eight
2260 nine
2261 ten
2262 "#
2263 .unindent(),
2264 );
2265
2266 // Test select collapsing
2267 cx.update_editor(|editor, window, cx| {
2268 editor.move_page_down(&MovePageDown::default(), window, cx);
2269 editor.move_page_down(&MovePageDown::default(), window, cx);
2270 editor.move_page_down(&MovePageDown::default(), window, cx);
2271 });
2272 cx.assert_editor_state(
2273 &r#"
2274 one
2275 two
2276 three
2277 four
2278 five
2279 six
2280 seven
2281 eight
2282 nine
2283 ˇten
2284 ˇ"#
2285 .unindent(),
2286 );
2287}
2288
2289#[gpui::test]
2290async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
2291 init_test(cx, |_| {});
2292 let mut cx = EditorTestContext::new(cx).await;
2293 cx.set_state("one «two threeˇ» four");
2294 cx.update_editor(|editor, window, cx| {
2295 editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx);
2296 assert_eq!(editor.text(cx), " four");
2297 });
2298}
2299
2300#[gpui::test]
2301fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2302 init_test(cx, |_| {});
2303
2304 let editor = cx.add_window(|window, cx| {
2305 let buffer = MultiBuffer::build_simple("one two three four", cx);
2306 build_editor(buffer.clone(), window, cx)
2307 });
2308
2309 _ = editor.update(cx, |editor, window, cx| {
2310 editor.change_selections(None, window, cx, |s| {
2311 s.select_display_ranges([
2312 // an empty selection - the preceding word fragment is deleted
2313 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2314 // characters selected - they are deleted
2315 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2316 ])
2317 });
2318 editor.delete_to_previous_word_start(
2319 &DeleteToPreviousWordStart {
2320 ignore_newlines: false,
2321 },
2322 window,
2323 cx,
2324 );
2325 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2326 });
2327
2328 _ = editor.update(cx, |editor, window, cx| {
2329 editor.change_selections(None, window, cx, |s| {
2330 s.select_display_ranges([
2331 // an empty selection - the following word fragment is deleted
2332 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2333 // characters selected - they are deleted
2334 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2335 ])
2336 });
2337 editor.delete_to_next_word_end(
2338 &DeleteToNextWordEnd {
2339 ignore_newlines: false,
2340 },
2341 window,
2342 cx,
2343 );
2344 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2345 });
2346}
2347
2348#[gpui::test]
2349fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2350 init_test(cx, |_| {});
2351
2352 let editor = cx.add_window(|window, cx| {
2353 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2354 build_editor(buffer.clone(), window, cx)
2355 });
2356 let del_to_prev_word_start = DeleteToPreviousWordStart {
2357 ignore_newlines: false,
2358 };
2359 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2360 ignore_newlines: true,
2361 };
2362
2363 _ = editor.update(cx, |editor, window, cx| {
2364 editor.change_selections(None, window, cx, |s| {
2365 s.select_display_ranges([
2366 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2367 ])
2368 });
2369 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2370 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2371 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2372 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2373 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2374 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2375 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2376 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2377 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2378 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2379 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2380 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2381 });
2382}
2383
2384#[gpui::test]
2385fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2386 init_test(cx, |_| {});
2387
2388 let editor = cx.add_window(|window, cx| {
2389 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2390 build_editor(buffer.clone(), window, cx)
2391 });
2392 let del_to_next_word_end = DeleteToNextWordEnd {
2393 ignore_newlines: false,
2394 };
2395 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2396 ignore_newlines: true,
2397 };
2398
2399 _ = editor.update(cx, |editor, window, cx| {
2400 editor.change_selections(None, window, cx, |s| {
2401 s.select_display_ranges([
2402 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2403 ])
2404 });
2405 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2406 assert_eq!(
2407 editor.buffer.read(cx).read(cx).text(),
2408 "one\n two\nthree\n four"
2409 );
2410 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2411 assert_eq!(
2412 editor.buffer.read(cx).read(cx).text(),
2413 "\n two\nthree\n four"
2414 );
2415 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2416 assert_eq!(
2417 editor.buffer.read(cx).read(cx).text(),
2418 "two\nthree\n four"
2419 );
2420 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2421 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2422 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2423 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2424 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2425 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2426 });
2427}
2428
2429#[gpui::test]
2430fn test_newline(cx: &mut TestAppContext) {
2431 init_test(cx, |_| {});
2432
2433 let editor = cx.add_window(|window, cx| {
2434 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2435 build_editor(buffer.clone(), window, cx)
2436 });
2437
2438 _ = editor.update(cx, |editor, window, cx| {
2439 editor.change_selections(None, window, cx, |s| {
2440 s.select_display_ranges([
2441 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2442 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2443 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2444 ])
2445 });
2446
2447 editor.newline(&Newline, window, cx);
2448 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2449 });
2450}
2451
2452#[gpui::test]
2453fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2454 init_test(cx, |_| {});
2455
2456 let editor = cx.add_window(|window, cx| {
2457 let buffer = MultiBuffer::build_simple(
2458 "
2459 a
2460 b(
2461 X
2462 )
2463 c(
2464 X
2465 )
2466 "
2467 .unindent()
2468 .as_str(),
2469 cx,
2470 );
2471 let mut editor = build_editor(buffer.clone(), window, cx);
2472 editor.change_selections(None, window, cx, |s| {
2473 s.select_ranges([
2474 Point::new(2, 4)..Point::new(2, 5),
2475 Point::new(5, 4)..Point::new(5, 5),
2476 ])
2477 });
2478 editor
2479 });
2480
2481 _ = editor.update(cx, |editor, window, cx| {
2482 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2483 editor.buffer.update(cx, |buffer, cx| {
2484 buffer.edit(
2485 [
2486 (Point::new(1, 2)..Point::new(3, 0), ""),
2487 (Point::new(4, 2)..Point::new(6, 0), ""),
2488 ],
2489 None,
2490 cx,
2491 );
2492 assert_eq!(
2493 buffer.read(cx).text(),
2494 "
2495 a
2496 b()
2497 c()
2498 "
2499 .unindent()
2500 );
2501 });
2502 assert_eq!(
2503 editor.selections.ranges(cx),
2504 &[
2505 Point::new(1, 2)..Point::new(1, 2),
2506 Point::new(2, 2)..Point::new(2, 2),
2507 ],
2508 );
2509
2510 editor.newline(&Newline, window, cx);
2511 assert_eq!(
2512 editor.text(cx),
2513 "
2514 a
2515 b(
2516 )
2517 c(
2518 )
2519 "
2520 .unindent()
2521 );
2522
2523 // The selections are moved after the inserted newlines
2524 assert_eq!(
2525 editor.selections.ranges(cx),
2526 &[
2527 Point::new(2, 0)..Point::new(2, 0),
2528 Point::new(4, 0)..Point::new(4, 0),
2529 ],
2530 );
2531 });
2532}
2533
2534#[gpui::test]
2535async fn test_newline_above(cx: &mut gpui::TestAppContext) {
2536 init_test(cx, |settings| {
2537 settings.defaults.tab_size = NonZeroU32::new(4)
2538 });
2539
2540 let language = Arc::new(
2541 Language::new(
2542 LanguageConfig::default(),
2543 Some(tree_sitter_rust::LANGUAGE.into()),
2544 )
2545 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2546 .unwrap(),
2547 );
2548
2549 let mut cx = EditorTestContext::new(cx).await;
2550 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2551 cx.set_state(indoc! {"
2552 const a: ˇA = (
2553 (ˇ
2554 «const_functionˇ»(ˇ),
2555 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2556 )ˇ
2557 ˇ);ˇ
2558 "});
2559
2560 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2561 cx.assert_editor_state(indoc! {"
2562 ˇ
2563 const a: A = (
2564 ˇ
2565 (
2566 ˇ
2567 ˇ
2568 const_function(),
2569 ˇ
2570 ˇ
2571 ˇ
2572 ˇ
2573 something_else,
2574 ˇ
2575 )
2576 ˇ
2577 ˇ
2578 );
2579 "});
2580}
2581
2582#[gpui::test]
2583async fn test_newline_below(cx: &mut gpui::TestAppContext) {
2584 init_test(cx, |settings| {
2585 settings.defaults.tab_size = NonZeroU32::new(4)
2586 });
2587
2588 let language = Arc::new(
2589 Language::new(
2590 LanguageConfig::default(),
2591 Some(tree_sitter_rust::LANGUAGE.into()),
2592 )
2593 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2594 .unwrap(),
2595 );
2596
2597 let mut cx = EditorTestContext::new(cx).await;
2598 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2599 cx.set_state(indoc! {"
2600 const a: ˇA = (
2601 (ˇ
2602 «const_functionˇ»(ˇ),
2603 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2604 )ˇ
2605 ˇ);ˇ
2606 "});
2607
2608 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2609 cx.assert_editor_state(indoc! {"
2610 const a: A = (
2611 ˇ
2612 (
2613 ˇ
2614 const_function(),
2615 ˇ
2616 ˇ
2617 something_else,
2618 ˇ
2619 ˇ
2620 ˇ
2621 ˇ
2622 )
2623 ˇ
2624 );
2625 ˇ
2626 ˇ
2627 "});
2628}
2629
2630#[gpui::test]
2631async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
2632 init_test(cx, |settings| {
2633 settings.defaults.tab_size = NonZeroU32::new(4)
2634 });
2635
2636 let language = Arc::new(Language::new(
2637 LanguageConfig {
2638 line_comments: vec!["//".into()],
2639 ..LanguageConfig::default()
2640 },
2641 None,
2642 ));
2643 {
2644 let mut cx = EditorTestContext::new(cx).await;
2645 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2646 cx.set_state(indoc! {"
2647 // Fooˇ
2648 "});
2649
2650 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2651 cx.assert_editor_state(indoc! {"
2652 // Foo
2653 //ˇ
2654 "});
2655 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2656 cx.set_state(indoc! {"
2657 ˇ// Foo
2658 "});
2659 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2660 cx.assert_editor_state(indoc! {"
2661
2662 ˇ// Foo
2663 "});
2664 }
2665 // Ensure that comment continuations can be disabled.
2666 update_test_language_settings(cx, |settings| {
2667 settings.defaults.extend_comment_on_newline = Some(false);
2668 });
2669 let mut cx = EditorTestContext::new(cx).await;
2670 cx.set_state(indoc! {"
2671 // Fooˇ
2672 "});
2673 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2674 cx.assert_editor_state(indoc! {"
2675 // Foo
2676 ˇ
2677 "});
2678}
2679
2680#[gpui::test]
2681fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2682 init_test(cx, |_| {});
2683
2684 let editor = cx.add_window(|window, cx| {
2685 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2686 let mut editor = build_editor(buffer.clone(), window, cx);
2687 editor.change_selections(None, window, cx, |s| {
2688 s.select_ranges([3..4, 11..12, 19..20])
2689 });
2690 editor
2691 });
2692
2693 _ = editor.update(cx, |editor, window, cx| {
2694 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2695 editor.buffer.update(cx, |buffer, cx| {
2696 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2697 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2698 });
2699 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2700
2701 editor.insert("Z", window, cx);
2702 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2703
2704 // The selections are moved after the inserted characters
2705 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2706 });
2707}
2708
2709#[gpui::test]
2710async fn test_tab(cx: &mut gpui::TestAppContext) {
2711 init_test(cx, |settings| {
2712 settings.defaults.tab_size = NonZeroU32::new(3)
2713 });
2714
2715 let mut cx = EditorTestContext::new(cx).await;
2716 cx.set_state(indoc! {"
2717 ˇabˇc
2718 ˇ🏀ˇ🏀ˇefg
2719 dˇ
2720 "});
2721 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2722 cx.assert_editor_state(indoc! {"
2723 ˇab ˇc
2724 ˇ🏀 ˇ🏀 ˇefg
2725 d ˇ
2726 "});
2727
2728 cx.set_state(indoc! {"
2729 a
2730 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2731 "});
2732 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2733 cx.assert_editor_state(indoc! {"
2734 a
2735 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2736 "});
2737}
2738
2739#[gpui::test]
2740async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) {
2741 init_test(cx, |_| {});
2742
2743 let mut cx = EditorTestContext::new(cx).await;
2744 let language = Arc::new(
2745 Language::new(
2746 LanguageConfig::default(),
2747 Some(tree_sitter_rust::LANGUAGE.into()),
2748 )
2749 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2750 .unwrap(),
2751 );
2752 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2753
2754 // cursors that are already at the suggested indent level insert
2755 // a soft tab. cursors that are to the left of the suggested indent
2756 // auto-indent their line.
2757 cx.set_state(indoc! {"
2758 ˇ
2759 const a: B = (
2760 c(
2761 d(
2762 ˇ
2763 )
2764 ˇ
2765 ˇ )
2766 );
2767 "});
2768 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2769 cx.assert_editor_state(indoc! {"
2770 ˇ
2771 const a: B = (
2772 c(
2773 d(
2774 ˇ
2775 )
2776 ˇ
2777 ˇ)
2778 );
2779 "});
2780
2781 // handle auto-indent when there are multiple cursors on the same line
2782 cx.set_state(indoc! {"
2783 const a: B = (
2784 c(
2785 ˇ ˇ
2786 ˇ )
2787 );
2788 "});
2789 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2790 cx.assert_editor_state(indoc! {"
2791 const a: B = (
2792 c(
2793 ˇ
2794 ˇ)
2795 );
2796 "});
2797}
2798
2799#[gpui::test]
2800async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
2801 init_test(cx, |settings| {
2802 settings.defaults.tab_size = NonZeroU32::new(4)
2803 });
2804
2805 let language = Arc::new(
2806 Language::new(
2807 LanguageConfig::default(),
2808 Some(tree_sitter_rust::LANGUAGE.into()),
2809 )
2810 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2811 .unwrap(),
2812 );
2813
2814 let mut cx = EditorTestContext::new(cx).await;
2815 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2816 cx.set_state(indoc! {"
2817 fn a() {
2818 if b {
2819 \t ˇc
2820 }
2821 }
2822 "});
2823
2824 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2825 cx.assert_editor_state(indoc! {"
2826 fn a() {
2827 if b {
2828 ˇc
2829 }
2830 }
2831 "});
2832}
2833
2834#[gpui::test]
2835async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
2836 init_test(cx, |settings| {
2837 settings.defaults.tab_size = NonZeroU32::new(4);
2838 });
2839
2840 let mut cx = EditorTestContext::new(cx).await;
2841
2842 cx.set_state(indoc! {"
2843 «oneˇ» «twoˇ»
2844 three
2845 four
2846 "});
2847 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2848 cx.assert_editor_state(indoc! {"
2849 «oneˇ» «twoˇ»
2850 three
2851 four
2852 "});
2853
2854 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2855 cx.assert_editor_state(indoc! {"
2856 «oneˇ» «twoˇ»
2857 three
2858 four
2859 "});
2860
2861 // select across line ending
2862 cx.set_state(indoc! {"
2863 one two
2864 t«hree
2865 ˇ» four
2866 "});
2867 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2868 cx.assert_editor_state(indoc! {"
2869 one two
2870 t«hree
2871 ˇ» four
2872 "});
2873
2874 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2875 cx.assert_editor_state(indoc! {"
2876 one two
2877 t«hree
2878 ˇ» four
2879 "});
2880
2881 // Ensure that indenting/outdenting works when the cursor is at column 0.
2882 cx.set_state(indoc! {"
2883 one two
2884 ˇthree
2885 four
2886 "});
2887 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2888 cx.assert_editor_state(indoc! {"
2889 one two
2890 ˇthree
2891 four
2892 "});
2893
2894 cx.set_state(indoc! {"
2895 one two
2896 ˇ three
2897 four
2898 "});
2899 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2900 cx.assert_editor_state(indoc! {"
2901 one two
2902 ˇthree
2903 four
2904 "});
2905}
2906
2907#[gpui::test]
2908async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
2909 init_test(cx, |settings| {
2910 settings.defaults.hard_tabs = Some(true);
2911 });
2912
2913 let mut cx = EditorTestContext::new(cx).await;
2914
2915 // select two ranges on one line
2916 cx.set_state(indoc! {"
2917 «oneˇ» «twoˇ»
2918 three
2919 four
2920 "});
2921 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2922 cx.assert_editor_state(indoc! {"
2923 \t«oneˇ» «twoˇ»
2924 three
2925 four
2926 "});
2927 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2928 cx.assert_editor_state(indoc! {"
2929 \t\t«oneˇ» «twoˇ»
2930 three
2931 four
2932 "});
2933 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2934 cx.assert_editor_state(indoc! {"
2935 \t«oneˇ» «twoˇ»
2936 three
2937 four
2938 "});
2939 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2940 cx.assert_editor_state(indoc! {"
2941 «oneˇ» «twoˇ»
2942 three
2943 four
2944 "});
2945
2946 // select across a line ending
2947 cx.set_state(indoc! {"
2948 one two
2949 t«hree
2950 ˇ»four
2951 "});
2952 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2953 cx.assert_editor_state(indoc! {"
2954 one two
2955 \tt«hree
2956 ˇ»four
2957 "});
2958 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2959 cx.assert_editor_state(indoc! {"
2960 one two
2961 \t\tt«hree
2962 ˇ»four
2963 "});
2964 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2965 cx.assert_editor_state(indoc! {"
2966 one two
2967 \tt«hree
2968 ˇ»four
2969 "});
2970 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2971 cx.assert_editor_state(indoc! {"
2972 one two
2973 t«hree
2974 ˇ»four
2975 "});
2976
2977 // Ensure that indenting/outdenting works when the cursor is at column 0.
2978 cx.set_state(indoc! {"
2979 one two
2980 ˇthree
2981 four
2982 "});
2983 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2984 cx.assert_editor_state(indoc! {"
2985 one two
2986 ˇthree
2987 four
2988 "});
2989 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2990 cx.assert_editor_state(indoc! {"
2991 one two
2992 \tˇthree
2993 four
2994 "});
2995 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2996 cx.assert_editor_state(indoc! {"
2997 one two
2998 ˇthree
2999 four
3000 "});
3001}
3002
3003#[gpui::test]
3004fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3005 init_test(cx, |settings| {
3006 settings.languages.extend([
3007 (
3008 "TOML".into(),
3009 LanguageSettingsContent {
3010 tab_size: NonZeroU32::new(2),
3011 ..Default::default()
3012 },
3013 ),
3014 (
3015 "Rust".into(),
3016 LanguageSettingsContent {
3017 tab_size: NonZeroU32::new(4),
3018 ..Default::default()
3019 },
3020 ),
3021 ]);
3022 });
3023
3024 let toml_language = Arc::new(Language::new(
3025 LanguageConfig {
3026 name: "TOML".into(),
3027 ..Default::default()
3028 },
3029 None,
3030 ));
3031 let rust_language = Arc::new(Language::new(
3032 LanguageConfig {
3033 name: "Rust".into(),
3034 ..Default::default()
3035 },
3036 None,
3037 ));
3038
3039 let toml_buffer =
3040 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3041 let rust_buffer =
3042 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3043 let multibuffer = cx.new(|cx| {
3044 let mut multibuffer = MultiBuffer::new(ReadWrite);
3045 multibuffer.push_excerpts(
3046 toml_buffer.clone(),
3047 [ExcerptRange {
3048 context: Point::new(0, 0)..Point::new(2, 0),
3049 primary: None,
3050 }],
3051 cx,
3052 );
3053 multibuffer.push_excerpts(
3054 rust_buffer.clone(),
3055 [ExcerptRange {
3056 context: Point::new(0, 0)..Point::new(1, 0),
3057 primary: None,
3058 }],
3059 cx,
3060 );
3061 multibuffer
3062 });
3063
3064 cx.add_window(|window, cx| {
3065 let mut editor = build_editor(multibuffer, window, cx);
3066
3067 assert_eq!(
3068 editor.text(cx),
3069 indoc! {"
3070 a = 1
3071 b = 2
3072
3073 const c: usize = 3;
3074 "}
3075 );
3076
3077 select_ranges(
3078 &mut editor,
3079 indoc! {"
3080 «aˇ» = 1
3081 b = 2
3082
3083 «const c:ˇ» usize = 3;
3084 "},
3085 window,
3086 cx,
3087 );
3088
3089 editor.tab(&Tab, window, cx);
3090 assert_text_with_selections(
3091 &mut editor,
3092 indoc! {"
3093 «aˇ» = 1
3094 b = 2
3095
3096 «const c:ˇ» usize = 3;
3097 "},
3098 cx,
3099 );
3100 editor.tab_prev(&TabPrev, window, cx);
3101 assert_text_with_selections(
3102 &mut editor,
3103 indoc! {"
3104 «aˇ» = 1
3105 b = 2
3106
3107 «const c:ˇ» usize = 3;
3108 "},
3109 cx,
3110 );
3111
3112 editor
3113 });
3114}
3115
3116#[gpui::test]
3117async fn test_backspace(cx: &mut gpui::TestAppContext) {
3118 init_test(cx, |_| {});
3119
3120 let mut cx = EditorTestContext::new(cx).await;
3121
3122 // Basic backspace
3123 cx.set_state(indoc! {"
3124 onˇe two three
3125 fou«rˇ» five six
3126 seven «ˇeight nine
3127 »ten
3128 "});
3129 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3130 cx.assert_editor_state(indoc! {"
3131 oˇe two three
3132 fouˇ five six
3133 seven ˇten
3134 "});
3135
3136 // Test backspace inside and around indents
3137 cx.set_state(indoc! {"
3138 zero
3139 ˇone
3140 ˇtwo
3141 ˇ ˇ ˇ three
3142 ˇ ˇ four
3143 "});
3144 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3145 cx.assert_editor_state(indoc! {"
3146 zero
3147 ˇone
3148 ˇtwo
3149 ˇ threeˇ four
3150 "});
3151
3152 // Test backspace with line_mode set to true
3153 cx.update_editor(|e, _, _| e.selections.line_mode = true);
3154 cx.set_state(indoc! {"
3155 The ˇquick ˇbrown
3156 fox jumps over
3157 the lazy dog
3158 ˇThe qu«ick bˇ»rown"});
3159 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3160 cx.assert_editor_state(indoc! {"
3161 ˇfox jumps over
3162 the lazy dogˇ"});
3163}
3164
3165#[gpui::test]
3166async fn test_delete(cx: &mut gpui::TestAppContext) {
3167 init_test(cx, |_| {});
3168
3169 let mut cx = EditorTestContext::new(cx).await;
3170 cx.set_state(indoc! {"
3171 onˇe two three
3172 fou«rˇ» five six
3173 seven «ˇeight nine
3174 »ten
3175 "});
3176 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3177 cx.assert_editor_state(indoc! {"
3178 onˇ two three
3179 fouˇ five six
3180 seven ˇten
3181 "});
3182
3183 // Test backspace with line_mode set to true
3184 cx.update_editor(|e, _, _| e.selections.line_mode = true);
3185 cx.set_state(indoc! {"
3186 The ˇquick ˇbrown
3187 fox «ˇjum»ps over
3188 the lazy dog
3189 ˇThe qu«ick bˇ»rown"});
3190 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3191 cx.assert_editor_state("ˇthe lazy dogˇ");
3192}
3193
3194#[gpui::test]
3195fn test_delete_line(cx: &mut TestAppContext) {
3196 init_test(cx, |_| {});
3197
3198 let editor = cx.add_window(|window, cx| {
3199 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3200 build_editor(buffer, window, cx)
3201 });
3202 _ = editor.update(cx, |editor, window, cx| {
3203 editor.change_selections(None, window, cx, |s| {
3204 s.select_display_ranges([
3205 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3206 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3207 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3208 ])
3209 });
3210 editor.delete_line(&DeleteLine, window, cx);
3211 assert_eq!(editor.display_text(cx), "ghi");
3212 assert_eq!(
3213 editor.selections.display_ranges(cx),
3214 vec![
3215 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3216 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3217 ]
3218 );
3219 });
3220
3221 let editor = cx.add_window(|window, cx| {
3222 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3223 build_editor(buffer, window, cx)
3224 });
3225 _ = editor.update(cx, |editor, window, cx| {
3226 editor.change_selections(None, window, cx, |s| {
3227 s.select_display_ranges([
3228 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3229 ])
3230 });
3231 editor.delete_line(&DeleteLine, window, cx);
3232 assert_eq!(editor.display_text(cx), "ghi\n");
3233 assert_eq!(
3234 editor.selections.display_ranges(cx),
3235 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3236 );
3237 });
3238}
3239
3240#[gpui::test]
3241fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3242 init_test(cx, |_| {});
3243
3244 cx.add_window(|window, cx| {
3245 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3246 let mut editor = build_editor(buffer.clone(), window, cx);
3247 let buffer = buffer.read(cx).as_singleton().unwrap();
3248
3249 assert_eq!(
3250 editor.selections.ranges::<Point>(cx),
3251 &[Point::new(0, 0)..Point::new(0, 0)]
3252 );
3253
3254 // When on single line, replace newline at end by space
3255 editor.join_lines(&JoinLines, window, cx);
3256 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3257 assert_eq!(
3258 editor.selections.ranges::<Point>(cx),
3259 &[Point::new(0, 3)..Point::new(0, 3)]
3260 );
3261
3262 // When multiple lines are selected, remove newlines that are spanned by the selection
3263 editor.change_selections(None, window, cx, |s| {
3264 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3265 });
3266 editor.join_lines(&JoinLines, window, cx);
3267 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3268 assert_eq!(
3269 editor.selections.ranges::<Point>(cx),
3270 &[Point::new(0, 11)..Point::new(0, 11)]
3271 );
3272
3273 // Undo should be transactional
3274 editor.undo(&Undo, window, cx);
3275 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3276 assert_eq!(
3277 editor.selections.ranges::<Point>(cx),
3278 &[Point::new(0, 5)..Point::new(2, 2)]
3279 );
3280
3281 // When joining an empty line don't insert a space
3282 editor.change_selections(None, window, cx, |s| {
3283 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3284 });
3285 editor.join_lines(&JoinLines, window, cx);
3286 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3287 assert_eq!(
3288 editor.selections.ranges::<Point>(cx),
3289 [Point::new(2, 3)..Point::new(2, 3)]
3290 );
3291
3292 // We can remove trailing newlines
3293 editor.join_lines(&JoinLines, window, cx);
3294 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3295 assert_eq!(
3296 editor.selections.ranges::<Point>(cx),
3297 [Point::new(2, 3)..Point::new(2, 3)]
3298 );
3299
3300 // We don't blow up on the last line
3301 editor.join_lines(&JoinLines, window, cx);
3302 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3303 assert_eq!(
3304 editor.selections.ranges::<Point>(cx),
3305 [Point::new(2, 3)..Point::new(2, 3)]
3306 );
3307
3308 // reset to test indentation
3309 editor.buffer.update(cx, |buffer, cx| {
3310 buffer.edit(
3311 [
3312 (Point::new(1, 0)..Point::new(1, 2), " "),
3313 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3314 ],
3315 None,
3316 cx,
3317 )
3318 });
3319
3320 // We remove any leading spaces
3321 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3322 editor.change_selections(None, window, cx, |s| {
3323 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3324 });
3325 editor.join_lines(&JoinLines, window, cx);
3326 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3327
3328 // We don't insert a space for a line containing only spaces
3329 editor.join_lines(&JoinLines, window, cx);
3330 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3331
3332 // We ignore any leading tabs
3333 editor.join_lines(&JoinLines, window, cx);
3334 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3335
3336 editor
3337 });
3338}
3339
3340#[gpui::test]
3341fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3342 init_test(cx, |_| {});
3343
3344 cx.add_window(|window, cx| {
3345 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3346 let mut editor = build_editor(buffer.clone(), window, cx);
3347 let buffer = buffer.read(cx).as_singleton().unwrap();
3348
3349 editor.change_selections(None, window, cx, |s| {
3350 s.select_ranges([
3351 Point::new(0, 2)..Point::new(1, 1),
3352 Point::new(1, 2)..Point::new(1, 2),
3353 Point::new(3, 1)..Point::new(3, 2),
3354 ])
3355 });
3356
3357 editor.join_lines(&JoinLines, window, cx);
3358 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3359
3360 assert_eq!(
3361 editor.selections.ranges::<Point>(cx),
3362 [
3363 Point::new(0, 7)..Point::new(0, 7),
3364 Point::new(1, 3)..Point::new(1, 3)
3365 ]
3366 );
3367 editor
3368 });
3369}
3370
3371#[gpui::test]
3372async fn test_join_lines_with_git_diff_base(
3373 executor: BackgroundExecutor,
3374 cx: &mut gpui::TestAppContext,
3375) {
3376 init_test(cx, |_| {});
3377
3378 let mut cx = EditorTestContext::new(cx).await;
3379
3380 let diff_base = r#"
3381 Line 0
3382 Line 1
3383 Line 2
3384 Line 3
3385 "#
3386 .unindent();
3387
3388 cx.set_state(
3389 &r#"
3390 ˇLine 0
3391 Line 1
3392 Line 2
3393 Line 3
3394 "#
3395 .unindent(),
3396 );
3397
3398 cx.set_diff_base(&diff_base);
3399 executor.run_until_parked();
3400
3401 // Join lines
3402 cx.update_editor(|editor, window, cx| {
3403 editor.join_lines(&JoinLines, window, cx);
3404 });
3405 executor.run_until_parked();
3406
3407 cx.assert_editor_state(
3408 &r#"
3409 Line 0ˇ Line 1
3410 Line 2
3411 Line 3
3412 "#
3413 .unindent(),
3414 );
3415 // Join again
3416 cx.update_editor(|editor, window, cx| {
3417 editor.join_lines(&JoinLines, window, cx);
3418 });
3419 executor.run_until_parked();
3420
3421 cx.assert_editor_state(
3422 &r#"
3423 Line 0 Line 1ˇ Line 2
3424 Line 3
3425 "#
3426 .unindent(),
3427 );
3428}
3429
3430#[gpui::test]
3431async fn test_custom_newlines_cause_no_false_positive_diffs(
3432 executor: BackgroundExecutor,
3433 cx: &mut gpui::TestAppContext,
3434) {
3435 init_test(cx, |_| {});
3436 let mut cx = EditorTestContext::new(cx).await;
3437 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3438 cx.set_diff_base("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3439 executor.run_until_parked();
3440
3441 cx.update_editor(|editor, window, cx| {
3442 let snapshot = editor.snapshot(window, cx);
3443 assert_eq!(
3444 snapshot
3445 .buffer_snapshot
3446 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3447 .collect::<Vec<_>>(),
3448 Vec::new(),
3449 "Should not have any diffs for files with custom newlines"
3450 );
3451 });
3452}
3453
3454#[gpui::test]
3455async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3456 init_test(cx, |_| {});
3457
3458 let mut cx = EditorTestContext::new(cx).await;
3459
3460 // Test sort_lines_case_insensitive()
3461 cx.set_state(indoc! {"
3462 «z
3463 y
3464 x
3465 Z
3466 Y
3467 Xˇ»
3468 "});
3469 cx.update_editor(|e, window, cx| {
3470 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3471 });
3472 cx.assert_editor_state(indoc! {"
3473 «x
3474 X
3475 y
3476 Y
3477 z
3478 Zˇ»
3479 "});
3480
3481 // Test reverse_lines()
3482 cx.set_state(indoc! {"
3483 «5
3484 4
3485 3
3486 2
3487 1ˇ»
3488 "});
3489 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3490 cx.assert_editor_state(indoc! {"
3491 «1
3492 2
3493 3
3494 4
3495 5ˇ»
3496 "});
3497
3498 // Skip testing shuffle_line()
3499
3500 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3501 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3502
3503 // Don't manipulate when cursor is on single line, but expand the selection
3504 cx.set_state(indoc! {"
3505 ddˇdd
3506 ccc
3507 bb
3508 a
3509 "});
3510 cx.update_editor(|e, window, cx| {
3511 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3512 });
3513 cx.assert_editor_state(indoc! {"
3514 «ddddˇ»
3515 ccc
3516 bb
3517 a
3518 "});
3519
3520 // Basic manipulate case
3521 // Start selection moves to column 0
3522 // End of selection shrinks to fit shorter line
3523 cx.set_state(indoc! {"
3524 dd«d
3525 ccc
3526 bb
3527 aaaaaˇ»
3528 "});
3529 cx.update_editor(|e, window, cx| {
3530 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3531 });
3532 cx.assert_editor_state(indoc! {"
3533 «aaaaa
3534 bb
3535 ccc
3536 dddˇ»
3537 "});
3538
3539 // Manipulate case with newlines
3540 cx.set_state(indoc! {"
3541 dd«d
3542 ccc
3543
3544 bb
3545 aaaaa
3546
3547 ˇ»
3548 "});
3549 cx.update_editor(|e, window, cx| {
3550 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3551 });
3552 cx.assert_editor_state(indoc! {"
3553 «
3554
3555 aaaaa
3556 bb
3557 ccc
3558 dddˇ»
3559
3560 "});
3561
3562 // Adding new line
3563 cx.set_state(indoc! {"
3564 aa«a
3565 bbˇ»b
3566 "});
3567 cx.update_editor(|e, window, cx| {
3568 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3569 });
3570 cx.assert_editor_state(indoc! {"
3571 «aaa
3572 bbb
3573 added_lineˇ»
3574 "});
3575
3576 // Removing line
3577 cx.set_state(indoc! {"
3578 aa«a
3579 bbbˇ»
3580 "});
3581 cx.update_editor(|e, window, cx| {
3582 e.manipulate_lines(window, cx, |lines| {
3583 lines.pop();
3584 })
3585 });
3586 cx.assert_editor_state(indoc! {"
3587 «aaaˇ»
3588 "});
3589
3590 // Removing all lines
3591 cx.set_state(indoc! {"
3592 aa«a
3593 bbbˇ»
3594 "});
3595 cx.update_editor(|e, window, cx| {
3596 e.manipulate_lines(window, cx, |lines| {
3597 lines.drain(..);
3598 })
3599 });
3600 cx.assert_editor_state(indoc! {"
3601 ˇ
3602 "});
3603}
3604
3605#[gpui::test]
3606async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3607 init_test(cx, |_| {});
3608
3609 let mut cx = EditorTestContext::new(cx).await;
3610
3611 // Consider continuous selection as single selection
3612 cx.set_state(indoc! {"
3613 Aaa«aa
3614 cˇ»c«c
3615 bb
3616 aaaˇ»aa
3617 "});
3618 cx.update_editor(|e, window, cx| {
3619 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3620 });
3621 cx.assert_editor_state(indoc! {"
3622 «Aaaaa
3623 ccc
3624 bb
3625 aaaaaˇ»
3626 "});
3627
3628 cx.set_state(indoc! {"
3629 Aaa«aa
3630 cˇ»c«c
3631 bb
3632 aaaˇ»aa
3633 "});
3634 cx.update_editor(|e, window, cx| {
3635 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3636 });
3637 cx.assert_editor_state(indoc! {"
3638 «Aaaaa
3639 ccc
3640 bbˇ»
3641 "});
3642
3643 // Consider non continuous selection as distinct dedup operations
3644 cx.set_state(indoc! {"
3645 «aaaaa
3646 bb
3647 aaaaa
3648 aaaaaˇ»
3649
3650 aaa«aaˇ»
3651 "});
3652 cx.update_editor(|e, window, cx| {
3653 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3654 });
3655 cx.assert_editor_state(indoc! {"
3656 «aaaaa
3657 bbˇ»
3658
3659 «aaaaaˇ»
3660 "});
3661}
3662
3663#[gpui::test]
3664async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3665 init_test(cx, |_| {});
3666
3667 let mut cx = EditorTestContext::new(cx).await;
3668
3669 cx.set_state(indoc! {"
3670 «Aaa
3671 aAa
3672 Aaaˇ»
3673 "});
3674 cx.update_editor(|e, window, cx| {
3675 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3676 });
3677 cx.assert_editor_state(indoc! {"
3678 «Aaa
3679 aAaˇ»
3680 "});
3681
3682 cx.set_state(indoc! {"
3683 «Aaa
3684 aAa
3685 aaAˇ»
3686 "});
3687 cx.update_editor(|e, window, cx| {
3688 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3689 });
3690 cx.assert_editor_state(indoc! {"
3691 «Aaaˇ»
3692 "});
3693}
3694
3695#[gpui::test]
3696async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3697 init_test(cx, |_| {});
3698
3699 let mut cx = EditorTestContext::new(cx).await;
3700
3701 // Manipulate with multiple selections on a single line
3702 cx.set_state(indoc! {"
3703 dd«dd
3704 cˇ»c«c
3705 bb
3706 aaaˇ»aa
3707 "});
3708 cx.update_editor(|e, window, cx| {
3709 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3710 });
3711 cx.assert_editor_state(indoc! {"
3712 «aaaaa
3713 bb
3714 ccc
3715 ddddˇ»
3716 "});
3717
3718 // Manipulate with multiple disjoin selections
3719 cx.set_state(indoc! {"
3720 5«
3721 4
3722 3
3723 2
3724 1ˇ»
3725
3726 dd«dd
3727 ccc
3728 bb
3729 aaaˇ»aa
3730 "});
3731 cx.update_editor(|e, window, cx| {
3732 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3733 });
3734 cx.assert_editor_state(indoc! {"
3735 «1
3736 2
3737 3
3738 4
3739 5ˇ»
3740
3741 «aaaaa
3742 bb
3743 ccc
3744 ddddˇ»
3745 "});
3746
3747 // Adding lines on each selection
3748 cx.set_state(indoc! {"
3749 2«
3750 1ˇ»
3751
3752 bb«bb
3753 aaaˇ»aa
3754 "});
3755 cx.update_editor(|e, window, cx| {
3756 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3757 });
3758 cx.assert_editor_state(indoc! {"
3759 «2
3760 1
3761 added lineˇ»
3762
3763 «bbbb
3764 aaaaa
3765 added lineˇ»
3766 "});
3767
3768 // Removing lines on each selection
3769 cx.set_state(indoc! {"
3770 2«
3771 1ˇ»
3772
3773 bb«bb
3774 aaaˇ»aa
3775 "});
3776 cx.update_editor(|e, window, cx| {
3777 e.manipulate_lines(window, cx, |lines| {
3778 lines.pop();
3779 })
3780 });
3781 cx.assert_editor_state(indoc! {"
3782 «2ˇ»
3783
3784 «bbbbˇ»
3785 "});
3786}
3787
3788#[gpui::test]
3789async fn test_manipulate_text(cx: &mut TestAppContext) {
3790 init_test(cx, |_| {});
3791
3792 let mut cx = EditorTestContext::new(cx).await;
3793
3794 // Test convert_to_upper_case()
3795 cx.set_state(indoc! {"
3796 «hello worldˇ»
3797 "});
3798 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3799 cx.assert_editor_state(indoc! {"
3800 «HELLO WORLDˇ»
3801 "});
3802
3803 // Test convert_to_lower_case()
3804 cx.set_state(indoc! {"
3805 «HELLO WORLDˇ»
3806 "});
3807 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3808 cx.assert_editor_state(indoc! {"
3809 «hello worldˇ»
3810 "});
3811
3812 // Test multiple line, single selection case
3813 cx.set_state(indoc! {"
3814 «The quick brown
3815 fox jumps over
3816 the lazy dogˇ»
3817 "});
3818 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3819 cx.assert_editor_state(indoc! {"
3820 «The Quick Brown
3821 Fox Jumps Over
3822 The Lazy Dogˇ»
3823 "});
3824
3825 // Test multiple line, single selection case
3826 cx.set_state(indoc! {"
3827 «The quick brown
3828 fox jumps over
3829 the lazy dogˇ»
3830 "});
3831 cx.update_editor(|e, window, cx| {
3832 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
3833 });
3834 cx.assert_editor_state(indoc! {"
3835 «TheQuickBrown
3836 FoxJumpsOver
3837 TheLazyDogˇ»
3838 "});
3839
3840 // From here on out, test more complex cases of manipulate_text()
3841
3842 // Test no selection case - should affect words cursors are in
3843 // Cursor at beginning, middle, and end of word
3844 cx.set_state(indoc! {"
3845 ˇhello big beauˇtiful worldˇ
3846 "});
3847 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3848 cx.assert_editor_state(indoc! {"
3849 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
3850 "});
3851
3852 // Test multiple selections on a single line and across multiple lines
3853 cx.set_state(indoc! {"
3854 «Theˇ» quick «brown
3855 foxˇ» jumps «overˇ»
3856 the «lazyˇ» dog
3857 "});
3858 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3859 cx.assert_editor_state(indoc! {"
3860 «THEˇ» quick «BROWN
3861 FOXˇ» jumps «OVERˇ»
3862 the «LAZYˇ» dog
3863 "});
3864
3865 // Test case where text length grows
3866 cx.set_state(indoc! {"
3867 «tschüߡ»
3868 "});
3869 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3870 cx.assert_editor_state(indoc! {"
3871 «TSCHÜSSˇ»
3872 "});
3873
3874 // Test to make sure we don't crash when text shrinks
3875 cx.set_state(indoc! {"
3876 aaa_bbbˇ
3877 "});
3878 cx.update_editor(|e, window, cx| {
3879 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3880 });
3881 cx.assert_editor_state(indoc! {"
3882 «aaaBbbˇ»
3883 "});
3884
3885 // Test to make sure we all aware of the fact that each word can grow and shrink
3886 // Final selections should be aware of this fact
3887 cx.set_state(indoc! {"
3888 aaa_bˇbb bbˇb_ccc ˇccc_ddd
3889 "});
3890 cx.update_editor(|e, window, cx| {
3891 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3892 });
3893 cx.assert_editor_state(indoc! {"
3894 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
3895 "});
3896
3897 cx.set_state(indoc! {"
3898 «hElLo, WoRld!ˇ»
3899 "});
3900 cx.update_editor(|e, window, cx| {
3901 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
3902 });
3903 cx.assert_editor_state(indoc! {"
3904 «HeLlO, wOrLD!ˇ»
3905 "});
3906}
3907
3908#[gpui::test]
3909fn test_duplicate_line(cx: &mut TestAppContext) {
3910 init_test(cx, |_| {});
3911
3912 let editor = cx.add_window(|window, cx| {
3913 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3914 build_editor(buffer, window, cx)
3915 });
3916 _ = editor.update(cx, |editor, window, cx| {
3917 editor.change_selections(None, window, cx, |s| {
3918 s.select_display_ranges([
3919 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3920 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3921 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
3922 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3923 ])
3924 });
3925 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
3926 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
3927 assert_eq!(
3928 editor.selections.display_ranges(cx),
3929 vec![
3930 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3931 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3932 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3933 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
3934 ]
3935 );
3936 });
3937
3938 let editor = cx.add_window(|window, cx| {
3939 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3940 build_editor(buffer, window, cx)
3941 });
3942 _ = editor.update(cx, |editor, window, cx| {
3943 editor.change_selections(None, window, cx, |s| {
3944 s.select_display_ranges([
3945 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
3946 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
3947 ])
3948 });
3949 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
3950 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
3951 assert_eq!(
3952 editor.selections.display_ranges(cx),
3953 vec![
3954 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
3955 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
3956 ]
3957 );
3958 });
3959
3960 // With `move_upwards` the selections stay in place, except for
3961 // the lines inserted above them
3962 let editor = cx.add_window(|window, cx| {
3963 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3964 build_editor(buffer, window, cx)
3965 });
3966 _ = editor.update(cx, |editor, window, cx| {
3967 editor.change_selections(None, window, cx, |s| {
3968 s.select_display_ranges([
3969 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3970 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3971 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
3972 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3973 ])
3974 });
3975 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
3976 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
3977 assert_eq!(
3978 editor.selections.display_ranges(cx),
3979 vec![
3980 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3981 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3982 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
3983 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
3984 ]
3985 );
3986 });
3987
3988 let editor = cx.add_window(|window, cx| {
3989 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3990 build_editor(buffer, window, cx)
3991 });
3992 _ = editor.update(cx, |editor, window, cx| {
3993 editor.change_selections(None, window, cx, |s| {
3994 s.select_display_ranges([
3995 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
3996 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
3997 ])
3998 });
3999 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4000 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4001 assert_eq!(
4002 editor.selections.display_ranges(cx),
4003 vec![
4004 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4005 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4006 ]
4007 );
4008 });
4009
4010 let editor = cx.add_window(|window, cx| {
4011 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4012 build_editor(buffer, window, cx)
4013 });
4014 _ = editor.update(cx, |editor, window, cx| {
4015 editor.change_selections(None, window, cx, |s| {
4016 s.select_display_ranges([
4017 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4018 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4019 ])
4020 });
4021 editor.duplicate_selection(&DuplicateSelection, window, cx);
4022 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4023 assert_eq!(
4024 editor.selections.display_ranges(cx),
4025 vec![
4026 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4027 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4028 ]
4029 );
4030 });
4031}
4032
4033#[gpui::test]
4034fn test_move_line_up_down(cx: &mut TestAppContext) {
4035 init_test(cx, |_| {});
4036
4037 let editor = cx.add_window(|window, cx| {
4038 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4039 build_editor(buffer, window, cx)
4040 });
4041 _ = editor.update(cx, |editor, window, cx| {
4042 editor.fold_creases(
4043 vec![
4044 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4045 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4046 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4047 ],
4048 true,
4049 window,
4050 cx,
4051 );
4052 editor.change_selections(None, window, cx, |s| {
4053 s.select_display_ranges([
4054 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4055 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4056 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4057 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4058 ])
4059 });
4060 assert_eq!(
4061 editor.display_text(cx),
4062 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4063 );
4064
4065 editor.move_line_up(&MoveLineUp, window, cx);
4066 assert_eq!(
4067 editor.display_text(cx),
4068 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4069 );
4070 assert_eq!(
4071 editor.selections.display_ranges(cx),
4072 vec![
4073 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4074 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4075 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4076 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4077 ]
4078 );
4079 });
4080
4081 _ = editor.update(cx, |editor, window, cx| {
4082 editor.move_line_down(&MoveLineDown, window, cx);
4083 assert_eq!(
4084 editor.display_text(cx),
4085 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4086 );
4087 assert_eq!(
4088 editor.selections.display_ranges(cx),
4089 vec![
4090 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4091 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4092 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4093 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4094 ]
4095 );
4096 });
4097
4098 _ = editor.update(cx, |editor, window, cx| {
4099 editor.move_line_down(&MoveLineDown, window, cx);
4100 assert_eq!(
4101 editor.display_text(cx),
4102 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4103 );
4104 assert_eq!(
4105 editor.selections.display_ranges(cx),
4106 vec![
4107 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4108 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4109 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4110 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4111 ]
4112 );
4113 });
4114
4115 _ = editor.update(cx, |editor, window, cx| {
4116 editor.move_line_up(&MoveLineUp, window, cx);
4117 assert_eq!(
4118 editor.display_text(cx),
4119 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4120 );
4121 assert_eq!(
4122 editor.selections.display_ranges(cx),
4123 vec![
4124 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4125 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4126 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4127 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4128 ]
4129 );
4130 });
4131}
4132
4133#[gpui::test]
4134fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4135 init_test(cx, |_| {});
4136
4137 let editor = cx.add_window(|window, cx| {
4138 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4139 build_editor(buffer, window, cx)
4140 });
4141 _ = editor.update(cx, |editor, window, cx| {
4142 let snapshot = editor.buffer.read(cx).snapshot(cx);
4143 editor.insert_blocks(
4144 [BlockProperties {
4145 style: BlockStyle::Fixed,
4146 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4147 height: 1,
4148 render: Arc::new(|_| div().into_any()),
4149 priority: 0,
4150 }],
4151 Some(Autoscroll::fit()),
4152 cx,
4153 );
4154 editor.change_selections(None, window, cx, |s| {
4155 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4156 });
4157 editor.move_line_down(&MoveLineDown, window, cx);
4158 });
4159}
4160
4161#[gpui::test]
4162async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4163 init_test(cx, |_| {});
4164
4165 let mut cx = EditorTestContext::new(cx).await;
4166 cx.set_state(
4167 &"
4168 ˇzero
4169 one
4170 two
4171 three
4172 four
4173 five
4174 "
4175 .unindent(),
4176 );
4177
4178 // Create a four-line block that replaces three lines of text.
4179 cx.update_editor(|editor, window, cx| {
4180 let snapshot = editor.snapshot(window, cx);
4181 let snapshot = &snapshot.buffer_snapshot;
4182 let placement = BlockPlacement::Replace(
4183 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4184 );
4185 editor.insert_blocks(
4186 [BlockProperties {
4187 placement,
4188 height: 4,
4189 style: BlockStyle::Sticky,
4190 render: Arc::new(|_| gpui::div().into_any_element()),
4191 priority: 0,
4192 }],
4193 None,
4194 cx,
4195 );
4196 });
4197
4198 // Move down so that the cursor touches the block.
4199 cx.update_editor(|editor, window, cx| {
4200 editor.move_down(&Default::default(), window, cx);
4201 });
4202 cx.assert_editor_state(
4203 &"
4204 zero
4205 «one
4206 two
4207 threeˇ»
4208 four
4209 five
4210 "
4211 .unindent(),
4212 );
4213
4214 // Move down past the block.
4215 cx.update_editor(|editor, window, cx| {
4216 editor.move_down(&Default::default(), window, cx);
4217 });
4218 cx.assert_editor_state(
4219 &"
4220 zero
4221 one
4222 two
4223 three
4224 ˇfour
4225 five
4226 "
4227 .unindent(),
4228 );
4229}
4230
4231#[gpui::test]
4232fn test_transpose(cx: &mut TestAppContext) {
4233 init_test(cx, |_| {});
4234
4235 _ = cx.add_window(|window, cx| {
4236 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4237 editor.set_style(EditorStyle::default(), window, cx);
4238 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4239 editor.transpose(&Default::default(), window, cx);
4240 assert_eq!(editor.text(cx), "bac");
4241 assert_eq!(editor.selections.ranges(cx), [2..2]);
4242
4243 editor.transpose(&Default::default(), window, cx);
4244 assert_eq!(editor.text(cx), "bca");
4245 assert_eq!(editor.selections.ranges(cx), [3..3]);
4246
4247 editor.transpose(&Default::default(), window, cx);
4248 assert_eq!(editor.text(cx), "bac");
4249 assert_eq!(editor.selections.ranges(cx), [3..3]);
4250
4251 editor
4252 });
4253
4254 _ = cx.add_window(|window, cx| {
4255 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4256 editor.set_style(EditorStyle::default(), window, cx);
4257 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4258 editor.transpose(&Default::default(), window, cx);
4259 assert_eq!(editor.text(cx), "acb\nde");
4260 assert_eq!(editor.selections.ranges(cx), [3..3]);
4261
4262 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4263 editor.transpose(&Default::default(), window, cx);
4264 assert_eq!(editor.text(cx), "acbd\ne");
4265 assert_eq!(editor.selections.ranges(cx), [5..5]);
4266
4267 editor.transpose(&Default::default(), window, cx);
4268 assert_eq!(editor.text(cx), "acbde\n");
4269 assert_eq!(editor.selections.ranges(cx), [6..6]);
4270
4271 editor.transpose(&Default::default(), window, cx);
4272 assert_eq!(editor.text(cx), "acbd\ne");
4273 assert_eq!(editor.selections.ranges(cx), [6..6]);
4274
4275 editor
4276 });
4277
4278 _ = cx.add_window(|window, cx| {
4279 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4280 editor.set_style(EditorStyle::default(), window, cx);
4281 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4282 editor.transpose(&Default::default(), window, cx);
4283 assert_eq!(editor.text(cx), "bacd\ne");
4284 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4285
4286 editor.transpose(&Default::default(), window, cx);
4287 assert_eq!(editor.text(cx), "bcade\n");
4288 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4289
4290 editor.transpose(&Default::default(), window, cx);
4291 assert_eq!(editor.text(cx), "bcda\ne");
4292 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4293
4294 editor.transpose(&Default::default(), window, cx);
4295 assert_eq!(editor.text(cx), "bcade\n");
4296 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4297
4298 editor.transpose(&Default::default(), window, cx);
4299 assert_eq!(editor.text(cx), "bcaed\n");
4300 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4301
4302 editor
4303 });
4304
4305 _ = cx.add_window(|window, cx| {
4306 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4307 editor.set_style(EditorStyle::default(), window, cx);
4308 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4309 editor.transpose(&Default::default(), window, cx);
4310 assert_eq!(editor.text(cx), "🏀🍐✋");
4311 assert_eq!(editor.selections.ranges(cx), [8..8]);
4312
4313 editor.transpose(&Default::default(), window, cx);
4314 assert_eq!(editor.text(cx), "🏀✋🍐");
4315 assert_eq!(editor.selections.ranges(cx), [11..11]);
4316
4317 editor.transpose(&Default::default(), window, cx);
4318 assert_eq!(editor.text(cx), "🏀🍐✋");
4319 assert_eq!(editor.selections.ranges(cx), [11..11]);
4320
4321 editor
4322 });
4323}
4324
4325#[gpui::test]
4326async fn test_rewrap(cx: &mut TestAppContext) {
4327 init_test(cx, |_| {});
4328
4329 let mut cx = EditorTestContext::new(cx).await;
4330
4331 let language_with_c_comments = Arc::new(Language::new(
4332 LanguageConfig {
4333 line_comments: vec!["// ".into()],
4334 ..LanguageConfig::default()
4335 },
4336 None,
4337 ));
4338 let language_with_pound_comments = Arc::new(Language::new(
4339 LanguageConfig {
4340 line_comments: vec!["# ".into()],
4341 ..LanguageConfig::default()
4342 },
4343 None,
4344 ));
4345 let markdown_language = Arc::new(Language::new(
4346 LanguageConfig {
4347 name: "Markdown".into(),
4348 ..LanguageConfig::default()
4349 },
4350 None,
4351 ));
4352 let language_with_doc_comments = Arc::new(Language::new(
4353 LanguageConfig {
4354 line_comments: vec!["// ".into(), "/// ".into()],
4355 ..LanguageConfig::default()
4356 },
4357 Some(tree_sitter_rust::LANGUAGE.into()),
4358 ));
4359
4360 let plaintext_language = Arc::new(Language::new(
4361 LanguageConfig {
4362 name: "Plain Text".into(),
4363 ..LanguageConfig::default()
4364 },
4365 None,
4366 ));
4367
4368 assert_rewrap(
4369 indoc! {"
4370 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4371 "},
4372 indoc! {"
4373 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4374 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4375 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4376 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4377 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4378 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4379 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4380 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4381 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4382 // porttitor id. Aliquam id accumsan eros.
4383 "},
4384 language_with_c_comments.clone(),
4385 &mut cx,
4386 );
4387
4388 // Test that rewrapping works inside of a selection
4389 assert_rewrap(
4390 indoc! {"
4391 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.ˇ»
4392 "},
4393 indoc! {"
4394 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4395 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4396 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4397 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4398 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4399 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4400 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4401 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4402 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4403 // porttitor id. Aliquam id accumsan eros.ˇ»
4404 "},
4405 language_with_c_comments.clone(),
4406 &mut cx,
4407 );
4408
4409 // Test that cursors that expand to the same region are collapsed.
4410 assert_rewrap(
4411 indoc! {"
4412 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4413 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4414 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4415 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4416 "},
4417 indoc! {"
4418 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4419 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4420 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4421 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4422 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4423 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4424 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4425 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4426 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4427 // porttitor id. Aliquam id accumsan eros.
4428 "},
4429 language_with_c_comments.clone(),
4430 &mut cx,
4431 );
4432
4433 // Test that non-contiguous selections are treated separately.
4434 assert_rewrap(
4435 indoc! {"
4436 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4437 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4438 //
4439 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4440 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4441 "},
4442 indoc! {"
4443 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4444 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4445 // auctor, eu lacinia sapien scelerisque.
4446 //
4447 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4448 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4449 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4450 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4451 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4452 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4453 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4454 "},
4455 language_with_c_comments.clone(),
4456 &mut cx,
4457 );
4458
4459 // Test that different comment prefixes are supported.
4460 assert_rewrap(
4461 indoc! {"
4462 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4463 "},
4464 indoc! {"
4465 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4466 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4467 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4468 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4469 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4470 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4471 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4472 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4473 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4474 # accumsan eros.
4475 "},
4476 language_with_pound_comments.clone(),
4477 &mut cx,
4478 );
4479
4480 // Test that rewrapping is ignored outside of comments in most languages.
4481 assert_rewrap(
4482 indoc! {"
4483 /// Adds two numbers.
4484 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4485 fn add(a: u32, b: u32) -> u32 {
4486 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
4487 }
4488 "},
4489 indoc! {"
4490 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4491 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4492 fn add(a: u32, b: u32) -> u32 {
4493 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
4494 }
4495 "},
4496 language_with_doc_comments.clone(),
4497 &mut cx,
4498 );
4499
4500 // Test that rewrapping works in Markdown and Plain Text languages.
4501 assert_rewrap(
4502 indoc! {"
4503 # Hello
4504
4505 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
4506 "},
4507 indoc! {"
4508 # Hello
4509
4510 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4511 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4512 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4513 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4514 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4515 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4516 Integer sit amet scelerisque nisi.
4517 "},
4518 markdown_language,
4519 &mut cx,
4520 );
4521
4522 assert_rewrap(
4523 indoc! {"
4524 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
4525 "},
4526 indoc! {"
4527 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4528 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4529 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4530 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4531 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4532 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4533 Integer sit amet scelerisque nisi.
4534 "},
4535 plaintext_language,
4536 &mut cx,
4537 );
4538
4539 // Test rewrapping unaligned comments in a selection.
4540 assert_rewrap(
4541 indoc! {"
4542 fn foo() {
4543 if true {
4544 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4545 // Praesent semper egestas tellus id dignissim.ˇ»
4546 do_something();
4547 } else {
4548 //
4549 }
4550 }
4551 "},
4552 indoc! {"
4553 fn foo() {
4554 if true {
4555 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4556 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4557 // egestas tellus id dignissim.ˇ»
4558 do_something();
4559 } else {
4560 //
4561 }
4562 }
4563 "},
4564 language_with_doc_comments.clone(),
4565 &mut cx,
4566 );
4567
4568 assert_rewrap(
4569 indoc! {"
4570 fn foo() {
4571 if true {
4572 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4573 // Praesent semper egestas tellus id dignissim.»
4574 do_something();
4575 } else {
4576 //
4577 }
4578
4579 }
4580 "},
4581 indoc! {"
4582 fn foo() {
4583 if true {
4584 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4585 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4586 // egestas tellus id dignissim.»
4587 do_something();
4588 } else {
4589 //
4590 }
4591
4592 }
4593 "},
4594 language_with_doc_comments.clone(),
4595 &mut cx,
4596 );
4597
4598 #[track_caller]
4599 fn assert_rewrap(
4600 unwrapped_text: &str,
4601 wrapped_text: &str,
4602 language: Arc<Language>,
4603 cx: &mut EditorTestContext,
4604 ) {
4605 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4606 cx.set_state(unwrapped_text);
4607 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4608 cx.assert_editor_state(wrapped_text);
4609 }
4610}
4611
4612#[gpui::test]
4613async fn test_clipboard(cx: &mut gpui::TestAppContext) {
4614 init_test(cx, |_| {});
4615
4616 let mut cx = EditorTestContext::new(cx).await;
4617
4618 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4619 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4620 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4621
4622 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4623 cx.set_state("two ˇfour ˇsix ˇ");
4624 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4625 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4626
4627 // Paste again but with only two cursors. Since the number of cursors doesn't
4628 // match the number of slices in the clipboard, the entire clipboard text
4629 // is pasted at each cursor.
4630 cx.set_state("ˇtwo one✅ four three six five ˇ");
4631 cx.update_editor(|e, window, cx| {
4632 e.handle_input("( ", window, cx);
4633 e.paste(&Paste, window, cx);
4634 e.handle_input(") ", window, cx);
4635 });
4636 cx.assert_editor_state(
4637 &([
4638 "( one✅ ",
4639 "three ",
4640 "five ) ˇtwo one✅ four three six five ( one✅ ",
4641 "three ",
4642 "five ) ˇ",
4643 ]
4644 .join("\n")),
4645 );
4646
4647 // Cut with three selections, one of which is full-line.
4648 cx.set_state(indoc! {"
4649 1«2ˇ»3
4650 4ˇ567
4651 «8ˇ»9"});
4652 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4653 cx.assert_editor_state(indoc! {"
4654 1ˇ3
4655 ˇ9"});
4656
4657 // Paste with three selections, noticing how the copied selection that was full-line
4658 // gets inserted before the second cursor.
4659 cx.set_state(indoc! {"
4660 1ˇ3
4661 9ˇ
4662 «oˇ»ne"});
4663 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4664 cx.assert_editor_state(indoc! {"
4665 12ˇ3
4666 4567
4667 9ˇ
4668 8ˇne"});
4669
4670 // Copy with a single cursor only, which writes the whole line into the clipboard.
4671 cx.set_state(indoc! {"
4672 The quick brown
4673 fox juˇmps over
4674 the lazy dog"});
4675 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4676 assert_eq!(
4677 cx.read_from_clipboard()
4678 .and_then(|item| item.text().as_deref().map(str::to_string)),
4679 Some("fox jumps over\n".to_string())
4680 );
4681
4682 // Paste with three selections, noticing how the copied full-line selection is inserted
4683 // before the empty selections but replaces the selection that is non-empty.
4684 cx.set_state(indoc! {"
4685 Tˇhe quick brown
4686 «foˇ»x jumps over
4687 tˇhe lazy dog"});
4688 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4689 cx.assert_editor_state(indoc! {"
4690 fox jumps over
4691 Tˇhe quick brown
4692 fox jumps over
4693 ˇx jumps over
4694 fox jumps over
4695 tˇhe lazy dog"});
4696}
4697
4698#[gpui::test]
4699async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
4700 init_test(cx, |_| {});
4701
4702 let mut cx = EditorTestContext::new(cx).await;
4703 let language = Arc::new(Language::new(
4704 LanguageConfig::default(),
4705 Some(tree_sitter_rust::LANGUAGE.into()),
4706 ));
4707 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4708
4709 // Cut an indented block, without the leading whitespace.
4710 cx.set_state(indoc! {"
4711 const a: B = (
4712 c(),
4713 «d(
4714 e,
4715 f
4716 )ˇ»
4717 );
4718 "});
4719 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4720 cx.assert_editor_state(indoc! {"
4721 const a: B = (
4722 c(),
4723 ˇ
4724 );
4725 "});
4726
4727 // Paste it at the same position.
4728 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4729 cx.assert_editor_state(indoc! {"
4730 const a: B = (
4731 c(),
4732 d(
4733 e,
4734 f
4735 )ˇ
4736 );
4737 "});
4738
4739 // Paste it at a line with a lower indent level.
4740 cx.set_state(indoc! {"
4741 ˇ
4742 const a: B = (
4743 c(),
4744 );
4745 "});
4746 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4747 cx.assert_editor_state(indoc! {"
4748 d(
4749 e,
4750 f
4751 )ˇ
4752 const a: B = (
4753 c(),
4754 );
4755 "});
4756
4757 // Cut an indented block, with the leading whitespace.
4758 cx.set_state(indoc! {"
4759 const a: B = (
4760 c(),
4761 « d(
4762 e,
4763 f
4764 )
4765 ˇ»);
4766 "});
4767 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4768 cx.assert_editor_state(indoc! {"
4769 const a: B = (
4770 c(),
4771 ˇ);
4772 "});
4773
4774 // Paste it at the same position.
4775 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4776 cx.assert_editor_state(indoc! {"
4777 const a: B = (
4778 c(),
4779 d(
4780 e,
4781 f
4782 )
4783 ˇ);
4784 "});
4785
4786 // Paste it at a line with a higher indent level.
4787 cx.set_state(indoc! {"
4788 const a: B = (
4789 c(),
4790 d(
4791 e,
4792 fˇ
4793 )
4794 );
4795 "});
4796 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4797 cx.assert_editor_state(indoc! {"
4798 const a: B = (
4799 c(),
4800 d(
4801 e,
4802 f d(
4803 e,
4804 f
4805 )
4806 ˇ
4807 )
4808 );
4809 "});
4810}
4811
4812#[gpui::test]
4813fn test_select_all(cx: &mut TestAppContext) {
4814 init_test(cx, |_| {});
4815
4816 let editor = cx.add_window(|window, cx| {
4817 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
4818 build_editor(buffer, window, cx)
4819 });
4820 _ = editor.update(cx, |editor, window, cx| {
4821 editor.select_all(&SelectAll, window, cx);
4822 assert_eq!(
4823 editor.selections.display_ranges(cx),
4824 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
4825 );
4826 });
4827}
4828
4829#[gpui::test]
4830fn test_select_line(cx: &mut TestAppContext) {
4831 init_test(cx, |_| {});
4832
4833 let editor = cx.add_window(|window, cx| {
4834 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
4835 build_editor(buffer, window, cx)
4836 });
4837 _ = editor.update(cx, |editor, window, cx| {
4838 editor.change_selections(None, window, cx, |s| {
4839 s.select_display_ranges([
4840 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4841 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4842 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4843 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
4844 ])
4845 });
4846 editor.select_line(&SelectLine, window, cx);
4847 assert_eq!(
4848 editor.selections.display_ranges(cx),
4849 vec![
4850 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
4851 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
4852 ]
4853 );
4854 });
4855
4856 _ = editor.update(cx, |editor, window, cx| {
4857 editor.select_line(&SelectLine, window, cx);
4858 assert_eq!(
4859 editor.selections.display_ranges(cx),
4860 vec![
4861 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
4862 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
4863 ]
4864 );
4865 });
4866
4867 _ = editor.update(cx, |editor, window, cx| {
4868 editor.select_line(&SelectLine, window, cx);
4869 assert_eq!(
4870 editor.selections.display_ranges(cx),
4871 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
4872 );
4873 });
4874}
4875
4876#[gpui::test]
4877fn test_split_selection_into_lines(cx: &mut TestAppContext) {
4878 init_test(cx, |_| {});
4879
4880 let editor = cx.add_window(|window, cx| {
4881 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
4882 build_editor(buffer, window, cx)
4883 });
4884 _ = editor.update(cx, |editor, window, cx| {
4885 editor.fold_creases(
4886 vec![
4887 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4888 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4889 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4890 ],
4891 true,
4892 window,
4893 cx,
4894 );
4895 editor.change_selections(None, window, cx, |s| {
4896 s.select_display_ranges([
4897 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4898 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4899 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4900 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
4901 ])
4902 });
4903 assert_eq!(
4904 editor.display_text(cx),
4905 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
4906 );
4907 });
4908
4909 _ = editor.update(cx, |editor, window, cx| {
4910 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
4911 assert_eq!(
4912 editor.display_text(cx),
4913 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
4914 );
4915 assert_eq!(
4916 editor.selections.display_ranges(cx),
4917 [
4918 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4919 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4920 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4921 DisplayPoint::new(DisplayRow(5), 4)..DisplayPoint::new(DisplayRow(5), 4)
4922 ]
4923 );
4924 });
4925
4926 _ = editor.update(cx, |editor, window, cx| {
4927 editor.change_selections(None, window, cx, |s| {
4928 s.select_display_ranges([
4929 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
4930 ])
4931 });
4932 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
4933 assert_eq!(
4934 editor.display_text(cx),
4935 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
4936 );
4937 assert_eq!(
4938 editor.selections.display_ranges(cx),
4939 [
4940 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
4941 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
4942 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
4943 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
4944 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
4945 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
4946 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5),
4947 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(7), 0)
4948 ]
4949 );
4950 });
4951}
4952
4953#[gpui::test]
4954async fn test_add_selection_above_below(cx: &mut TestAppContext) {
4955 init_test(cx, |_| {});
4956
4957 let mut cx = EditorTestContext::new(cx).await;
4958
4959 // let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx);
4960 cx.set_state(indoc!(
4961 r#"abc
4962 defˇghi
4963
4964 jk
4965 nlmo
4966 "#
4967 ));
4968
4969 cx.update_editor(|editor, window, cx| {
4970 editor.add_selection_above(&Default::default(), window, cx);
4971 });
4972
4973 cx.assert_editor_state(indoc!(
4974 r#"abcˇ
4975 defˇghi
4976
4977 jk
4978 nlmo
4979 "#
4980 ));
4981
4982 cx.update_editor(|editor, window, cx| {
4983 editor.add_selection_above(&Default::default(), window, cx);
4984 });
4985
4986 cx.assert_editor_state(indoc!(
4987 r#"abcˇ
4988 defˇghi
4989
4990 jk
4991 nlmo
4992 "#
4993 ));
4994
4995 cx.update_editor(|editor, window, cx| {
4996 editor.add_selection_below(&Default::default(), window, cx);
4997 });
4998
4999 cx.assert_editor_state(indoc!(
5000 r#"abc
5001 defˇghi
5002
5003 jk
5004 nlmo
5005 "#
5006 ));
5007
5008 cx.update_editor(|editor, window, cx| {
5009 editor.undo_selection(&Default::default(), window, cx);
5010 });
5011
5012 cx.assert_editor_state(indoc!(
5013 r#"abcˇ
5014 defˇghi
5015
5016 jk
5017 nlmo
5018 "#
5019 ));
5020
5021 cx.update_editor(|editor, window, cx| {
5022 editor.redo_selection(&Default::default(), window, cx);
5023 });
5024
5025 cx.assert_editor_state(indoc!(
5026 r#"abc
5027 defˇghi
5028
5029 jk
5030 nlmo
5031 "#
5032 ));
5033
5034 cx.update_editor(|editor, window, cx| {
5035 editor.add_selection_below(&Default::default(), window, cx);
5036 });
5037
5038 cx.assert_editor_state(indoc!(
5039 r#"abc
5040 defˇghi
5041
5042 jk
5043 nlmˇo
5044 "#
5045 ));
5046
5047 cx.update_editor(|editor, window, cx| {
5048 editor.add_selection_below(&Default::default(), window, cx);
5049 });
5050
5051 cx.assert_editor_state(indoc!(
5052 r#"abc
5053 defˇghi
5054
5055 jk
5056 nlmˇo
5057 "#
5058 ));
5059
5060 // change selections
5061 cx.set_state(indoc!(
5062 r#"abc
5063 def«ˇg»hi
5064
5065 jk
5066 nlmo
5067 "#
5068 ));
5069
5070 cx.update_editor(|editor, window, cx| {
5071 editor.add_selection_below(&Default::default(), window, cx);
5072 });
5073
5074 cx.assert_editor_state(indoc!(
5075 r#"abc
5076 def«ˇg»hi
5077
5078 jk
5079 nlm«ˇo»
5080 "#
5081 ));
5082
5083 cx.update_editor(|editor, window, cx| {
5084 editor.add_selection_below(&Default::default(), window, cx);
5085 });
5086
5087 cx.assert_editor_state(indoc!(
5088 r#"abc
5089 def«ˇg»hi
5090
5091 jk
5092 nlm«ˇo»
5093 "#
5094 ));
5095
5096 cx.update_editor(|editor, window, cx| {
5097 editor.add_selection_above(&Default::default(), window, cx);
5098 });
5099
5100 cx.assert_editor_state(indoc!(
5101 r#"abc
5102 def«ˇg»hi
5103
5104 jk
5105 nlmo
5106 "#
5107 ));
5108
5109 cx.update_editor(|editor, window, cx| {
5110 editor.add_selection_above(&Default::default(), window, cx);
5111 });
5112
5113 cx.assert_editor_state(indoc!(
5114 r#"abc
5115 def«ˇg»hi
5116
5117 jk
5118 nlmo
5119 "#
5120 ));
5121
5122 // Change selections again
5123 cx.set_state(indoc!(
5124 r#"a«bc
5125 defgˇ»hi
5126
5127 jk
5128 nlmo
5129 "#
5130 ));
5131
5132 cx.update_editor(|editor, window, cx| {
5133 editor.add_selection_below(&Default::default(), window, cx);
5134 });
5135
5136 cx.assert_editor_state(indoc!(
5137 r#"a«bcˇ»
5138 d«efgˇ»hi
5139
5140 j«kˇ»
5141 nlmo
5142 "#
5143 ));
5144
5145 cx.update_editor(|editor, window, cx| {
5146 editor.add_selection_below(&Default::default(), window, cx);
5147 });
5148 cx.assert_editor_state(indoc!(
5149 r#"a«bcˇ»
5150 d«efgˇ»hi
5151
5152 j«kˇ»
5153 n«lmoˇ»
5154 "#
5155 ));
5156 cx.update_editor(|editor, window, cx| {
5157 editor.add_selection_above(&Default::default(), window, cx);
5158 });
5159
5160 cx.assert_editor_state(indoc!(
5161 r#"a«bcˇ»
5162 d«efgˇ»hi
5163
5164 j«kˇ»
5165 nlmo
5166 "#
5167 ));
5168
5169 // Change selections again
5170 cx.set_state(indoc!(
5171 r#"abc
5172 d«ˇefghi
5173
5174 jk
5175 nlm»o
5176 "#
5177 ));
5178
5179 cx.update_editor(|editor, window, cx| {
5180 editor.add_selection_above(&Default::default(), window, cx);
5181 });
5182
5183 cx.assert_editor_state(indoc!(
5184 r#"a«ˇbc»
5185 d«ˇef»ghi
5186
5187 j«ˇk»
5188 n«ˇlm»o
5189 "#
5190 ));
5191
5192 cx.update_editor(|editor, window, cx| {
5193 editor.add_selection_below(&Default::default(), window, cx);
5194 });
5195
5196 cx.assert_editor_state(indoc!(
5197 r#"abc
5198 d«ˇef»ghi
5199
5200 j«ˇk»
5201 n«ˇlm»o
5202 "#
5203 ));
5204}
5205
5206#[gpui::test]
5207async fn test_select_next(cx: &mut gpui::TestAppContext) {
5208 init_test(cx, |_| {});
5209
5210 let mut cx = EditorTestContext::new(cx).await;
5211 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5212
5213 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5214 .unwrap();
5215 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5216
5217 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5218 .unwrap();
5219 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5220
5221 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5222 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5223
5224 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5225 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5226
5227 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5228 .unwrap();
5229 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5230
5231 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5232 .unwrap();
5233 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5234}
5235
5236#[gpui::test]
5237async fn test_select_all_matches(cx: &mut gpui::TestAppContext) {
5238 init_test(cx, |_| {});
5239
5240 let mut cx = EditorTestContext::new(cx).await;
5241
5242 // Test caret-only selections
5243 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5244
5245 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5246 .unwrap();
5247 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5248
5249 // Test left-to-right selections
5250 cx.set_state("abc\n«abcˇ»\nabc");
5251
5252 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5253 .unwrap();
5254 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5255
5256 // Test right-to-left selections
5257 cx.set_state("abc\n«ˇabc»\nabc");
5258
5259 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5260 .unwrap();
5261 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5262}
5263
5264#[gpui::test]
5265async fn test_select_next_with_multiple_carets(cx: &mut gpui::TestAppContext) {
5266 init_test(cx, |_| {});
5267
5268 let mut cx = EditorTestContext::new(cx).await;
5269 cx.set_state(
5270 r#"let foo = 2;
5271lˇet foo = 2;
5272let fooˇ = 2;
5273let foo = 2;
5274let foo = ˇ2;"#,
5275 );
5276
5277 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5278 .unwrap();
5279 cx.assert_editor_state(
5280 r#"let foo = 2;
5281«letˇ» foo = 2;
5282let «fooˇ» = 2;
5283let foo = 2;
5284let foo = «2ˇ»;"#,
5285 );
5286
5287 // noop for multiple selections with different contents
5288 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5289 .unwrap();
5290 cx.assert_editor_state(
5291 r#"let foo = 2;
5292«letˇ» foo = 2;
5293let «fooˇ» = 2;
5294let foo = 2;
5295let foo = «2ˇ»;"#,
5296 );
5297}
5298
5299#[gpui::test]
5300async fn test_select_previous_multibuffer(cx: &mut gpui::TestAppContext) {
5301 init_test(cx, |_| {});
5302
5303 let mut cx =
5304 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
5305
5306 cx.assert_editor_state(indoc! {"
5307 ˇbbb
5308 ccc
5309
5310 bbb
5311 ccc
5312 "});
5313 cx.dispatch_action(SelectPrevious::default());
5314 cx.assert_editor_state(indoc! {"
5315 «bbbˇ»
5316 ccc
5317
5318 bbb
5319 ccc
5320 "});
5321 cx.dispatch_action(SelectPrevious::default());
5322 cx.assert_editor_state(indoc! {"
5323 «bbbˇ»
5324 ccc
5325
5326 «bbbˇ»
5327 ccc
5328 "});
5329}
5330
5331#[gpui::test]
5332async fn test_select_previous_with_single_caret(cx: &mut gpui::TestAppContext) {
5333 init_test(cx, |_| {});
5334
5335 let mut cx = EditorTestContext::new(cx).await;
5336 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5337
5338 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5339 .unwrap();
5340 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5341
5342 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5343 .unwrap();
5344 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5345
5346 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5347 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5348
5349 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5350 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5351
5352 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5353 .unwrap();
5354 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
5355
5356 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5357 .unwrap();
5358 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
5359
5360 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5361 .unwrap();
5362 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5363}
5364
5365#[gpui::test]
5366async fn test_select_previous_empty_buffer(cx: &mut gpui::TestAppContext) {
5367 init_test(cx, |_| {});
5368
5369 let mut cx = EditorTestContext::new(cx).await;
5370 cx.set_state("aˇ");
5371
5372 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5373 .unwrap();
5374 cx.assert_editor_state("«aˇ»");
5375 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5376 .unwrap();
5377 cx.assert_editor_state("«aˇ»");
5378}
5379
5380#[gpui::test]
5381async fn test_select_previous_with_multiple_carets(cx: &mut gpui::TestAppContext) {
5382 init_test(cx, |_| {});
5383
5384 let mut cx = EditorTestContext::new(cx).await;
5385 cx.set_state(
5386 r#"let foo = 2;
5387lˇet foo = 2;
5388let fooˇ = 2;
5389let foo = 2;
5390let foo = ˇ2;"#,
5391 );
5392
5393 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5394 .unwrap();
5395 cx.assert_editor_state(
5396 r#"let foo = 2;
5397«letˇ» foo = 2;
5398let «fooˇ» = 2;
5399let foo = 2;
5400let foo = «2ˇ»;"#,
5401 );
5402
5403 // noop for multiple selections with different contents
5404 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5405 .unwrap();
5406 cx.assert_editor_state(
5407 r#"let foo = 2;
5408«letˇ» foo = 2;
5409let «fooˇ» = 2;
5410let foo = 2;
5411let foo = «2ˇ»;"#,
5412 );
5413}
5414
5415#[gpui::test]
5416async fn test_select_previous_with_single_selection(cx: &mut gpui::TestAppContext) {
5417 init_test(cx, |_| {});
5418
5419 let mut cx = EditorTestContext::new(cx).await;
5420 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5421
5422 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5423 .unwrap();
5424 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5425
5426 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5427 .unwrap();
5428 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5429
5430 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5431 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5432
5433 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5434 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5435
5436 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5437 .unwrap();
5438 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
5439
5440 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5441 .unwrap();
5442 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5443}
5444
5445#[gpui::test]
5446async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
5447 init_test(cx, |_| {});
5448
5449 let language = Arc::new(Language::new(
5450 LanguageConfig::default(),
5451 Some(tree_sitter_rust::LANGUAGE.into()),
5452 ));
5453
5454 let text = r#"
5455 use mod1::mod2::{mod3, mod4};
5456
5457 fn fn_1(param1: bool, param2: &str) {
5458 let var1 = "text";
5459 }
5460 "#
5461 .unindent();
5462
5463 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5464 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5465 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5466
5467 editor
5468 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5469 .await;
5470
5471 editor.update_in(cx, |editor, window, cx| {
5472 editor.change_selections(None, window, cx, |s| {
5473 s.select_display_ranges([
5474 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
5475 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
5476 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
5477 ]);
5478 });
5479 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5480 });
5481 editor.update(cx, |editor, cx| {
5482 assert_text_with_selections(
5483 editor,
5484 indoc! {r#"
5485 use mod1::mod2::{mod3, «mod4ˇ»};
5486
5487 fn fn_1«ˇ(param1: bool, param2: &str)» {
5488 let var1 = "«textˇ»";
5489 }
5490 "#},
5491 cx,
5492 );
5493 });
5494
5495 editor.update_in(cx, |editor, window, cx| {
5496 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5497 });
5498 editor.update(cx, |editor, cx| {
5499 assert_text_with_selections(
5500 editor,
5501 indoc! {r#"
5502 use mod1::mod2::«{mod3, mod4}ˇ»;
5503
5504 «ˇfn fn_1(param1: bool, param2: &str) {
5505 let var1 = "text";
5506 }»
5507 "#},
5508 cx,
5509 );
5510 });
5511
5512 editor.update_in(cx, |editor, window, cx| {
5513 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5514 });
5515 assert_eq!(
5516 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
5517 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
5518 );
5519
5520 // Trying to expand the selected syntax node one more time has no effect.
5521 editor.update_in(cx, |editor, window, cx| {
5522 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5523 });
5524 assert_eq!(
5525 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
5526 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
5527 );
5528
5529 editor.update_in(cx, |editor, window, cx| {
5530 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5531 });
5532 editor.update(cx, |editor, cx| {
5533 assert_text_with_selections(
5534 editor,
5535 indoc! {r#"
5536 use mod1::mod2::«{mod3, mod4}ˇ»;
5537
5538 «ˇfn fn_1(param1: bool, param2: &str) {
5539 let var1 = "text";
5540 }»
5541 "#},
5542 cx,
5543 );
5544 });
5545
5546 editor.update_in(cx, |editor, window, cx| {
5547 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5548 });
5549 editor.update(cx, |editor, cx| {
5550 assert_text_with_selections(
5551 editor,
5552 indoc! {r#"
5553 use mod1::mod2::{mod3, «mod4ˇ»};
5554
5555 fn fn_1«ˇ(param1: bool, param2: &str)» {
5556 let var1 = "«textˇ»";
5557 }
5558 "#},
5559 cx,
5560 );
5561 });
5562
5563 editor.update_in(cx, |editor, window, cx| {
5564 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5565 });
5566 editor.update(cx, |editor, cx| {
5567 assert_text_with_selections(
5568 editor,
5569 indoc! {r#"
5570 use mod1::mod2::{mod3, mo«ˇ»d4};
5571
5572 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
5573 let var1 = "te«ˇ»xt";
5574 }
5575 "#},
5576 cx,
5577 );
5578 });
5579
5580 // Trying to shrink the selected syntax node one more time has no effect.
5581 editor.update_in(cx, |editor, window, cx| {
5582 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5583 });
5584 editor.update_in(cx, |editor, _, cx| {
5585 assert_text_with_selections(
5586 editor,
5587 indoc! {r#"
5588 use mod1::mod2::{mod3, mo«ˇ»d4};
5589
5590 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
5591 let var1 = "te«ˇ»xt";
5592 }
5593 "#},
5594 cx,
5595 );
5596 });
5597
5598 // Ensure that we keep expanding the selection if the larger selection starts or ends within
5599 // a fold.
5600 editor.update_in(cx, |editor, window, cx| {
5601 editor.fold_creases(
5602 vec![
5603 Crease::simple(
5604 Point::new(0, 21)..Point::new(0, 24),
5605 FoldPlaceholder::test(),
5606 ),
5607 Crease::simple(
5608 Point::new(3, 20)..Point::new(3, 22),
5609 FoldPlaceholder::test(),
5610 ),
5611 ],
5612 true,
5613 window,
5614 cx,
5615 );
5616 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5617 });
5618 editor.update(cx, |editor, cx| {
5619 assert_text_with_selections(
5620 editor,
5621 indoc! {r#"
5622 use mod1::mod2::«{mod3, mod4}ˇ»;
5623
5624 fn fn_1«ˇ(param1: bool, param2: &str)» {
5625 «let var1 = "text";ˇ»
5626 }
5627 "#},
5628 cx,
5629 );
5630 });
5631}
5632
5633#[gpui::test]
5634async fn test_fold_function_bodies(cx: &mut gpui::TestAppContext) {
5635 init_test(cx, |_| {});
5636
5637 let base_text = r#"
5638 impl A {
5639 // this is an uncommitted comment
5640
5641 fn b() {
5642 c();
5643 }
5644
5645 // this is another uncommitted comment
5646
5647 fn d() {
5648 // e
5649 // f
5650 }
5651 }
5652
5653 fn g() {
5654 // h
5655 }
5656 "#
5657 .unindent();
5658
5659 let text = r#"
5660 ˇimpl A {
5661
5662 fn b() {
5663 c();
5664 }
5665
5666 fn d() {
5667 // e
5668 // f
5669 }
5670 }
5671
5672 fn g() {
5673 // h
5674 }
5675 "#
5676 .unindent();
5677
5678 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
5679 cx.set_state(&text);
5680 cx.set_diff_base(&base_text);
5681 cx.update_editor(|editor, window, cx| {
5682 editor.expand_all_diff_hunks(&Default::default(), window, cx);
5683 });
5684
5685 cx.assert_state_with_diff(
5686 "
5687 ˇimpl A {
5688 - // this is an uncommitted comment
5689
5690 fn b() {
5691 c();
5692 }
5693
5694 - // this is another uncommitted comment
5695 -
5696 fn d() {
5697 // e
5698 // f
5699 }
5700 }
5701
5702 fn g() {
5703 // h
5704 }
5705 "
5706 .unindent(),
5707 );
5708
5709 let expected_display_text = "
5710 impl A {
5711 // this is an uncommitted comment
5712
5713 fn b() {
5714 ⋯
5715 }
5716
5717 // this is another uncommitted comment
5718
5719 fn d() {
5720 ⋯
5721 }
5722 }
5723
5724 fn g() {
5725 ⋯
5726 }
5727 "
5728 .unindent();
5729
5730 cx.update_editor(|editor, window, cx| {
5731 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
5732 assert_eq!(editor.display_text(cx), expected_display_text);
5733 });
5734}
5735
5736#[gpui::test]
5737async fn test_autoindent(cx: &mut gpui::TestAppContext) {
5738 init_test(cx, |_| {});
5739
5740 let language = Arc::new(
5741 Language::new(
5742 LanguageConfig {
5743 brackets: BracketPairConfig {
5744 pairs: vec![
5745 BracketPair {
5746 start: "{".to_string(),
5747 end: "}".to_string(),
5748 close: false,
5749 surround: false,
5750 newline: true,
5751 },
5752 BracketPair {
5753 start: "(".to_string(),
5754 end: ")".to_string(),
5755 close: false,
5756 surround: false,
5757 newline: true,
5758 },
5759 ],
5760 ..Default::default()
5761 },
5762 ..Default::default()
5763 },
5764 Some(tree_sitter_rust::LANGUAGE.into()),
5765 )
5766 .with_indents_query(
5767 r#"
5768 (_ "(" ")" @end) @indent
5769 (_ "{" "}" @end) @indent
5770 "#,
5771 )
5772 .unwrap(),
5773 );
5774
5775 let text = "fn a() {}";
5776
5777 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5778 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5779 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5780 editor
5781 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5782 .await;
5783
5784 editor.update_in(cx, |editor, window, cx| {
5785 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
5786 editor.newline(&Newline, window, cx);
5787 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
5788 assert_eq!(
5789 editor.selections.ranges(cx),
5790 &[
5791 Point::new(1, 4)..Point::new(1, 4),
5792 Point::new(3, 4)..Point::new(3, 4),
5793 Point::new(5, 0)..Point::new(5, 0)
5794 ]
5795 );
5796 });
5797}
5798
5799#[gpui::test]
5800async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
5801 init_test(cx, |_| {});
5802
5803 {
5804 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
5805 cx.set_state(indoc! {"
5806 impl A {
5807
5808 fn b() {}
5809
5810 «fn c() {
5811
5812 }ˇ»
5813 }
5814 "});
5815
5816 cx.update_editor(|editor, window, cx| {
5817 editor.autoindent(&Default::default(), window, cx);
5818 });
5819
5820 cx.assert_editor_state(indoc! {"
5821 impl A {
5822
5823 fn b() {}
5824
5825 «fn c() {
5826
5827 }ˇ»
5828 }
5829 "});
5830 }
5831
5832 {
5833 let mut cx = EditorTestContext::new_multibuffer(
5834 cx,
5835 [indoc! { "
5836 impl A {
5837 «
5838 // a
5839 fn b(){}
5840 »
5841 «
5842 }
5843 fn c(){}
5844 »
5845 "}],
5846 );
5847
5848 let buffer = cx.update_editor(|editor, _, cx| {
5849 let buffer = editor.buffer().update(cx, |buffer, _| {
5850 buffer.all_buffers().iter().next().unwrap().clone()
5851 });
5852 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5853 buffer
5854 });
5855
5856 cx.run_until_parked();
5857 cx.update_editor(|editor, window, cx| {
5858 editor.select_all(&Default::default(), window, cx);
5859 editor.autoindent(&Default::default(), window, cx)
5860 });
5861 cx.run_until_parked();
5862
5863 cx.update(|_, cx| {
5864 pretty_assertions::assert_eq!(
5865 buffer.read(cx).text(),
5866 indoc! { "
5867 impl A {
5868
5869 // a
5870 fn b(){}
5871
5872
5873 }
5874 fn c(){}
5875
5876 " }
5877 )
5878 });
5879 }
5880}
5881
5882#[gpui::test]
5883async fn test_autoclose_and_auto_surround_pairs(cx: &mut gpui::TestAppContext) {
5884 init_test(cx, |_| {});
5885
5886 let mut cx = EditorTestContext::new(cx).await;
5887
5888 let language = Arc::new(Language::new(
5889 LanguageConfig {
5890 brackets: BracketPairConfig {
5891 pairs: vec![
5892 BracketPair {
5893 start: "{".to_string(),
5894 end: "}".to_string(),
5895 close: true,
5896 surround: true,
5897 newline: true,
5898 },
5899 BracketPair {
5900 start: "(".to_string(),
5901 end: ")".to_string(),
5902 close: true,
5903 surround: true,
5904 newline: true,
5905 },
5906 BracketPair {
5907 start: "/*".to_string(),
5908 end: " */".to_string(),
5909 close: true,
5910 surround: true,
5911 newline: true,
5912 },
5913 BracketPair {
5914 start: "[".to_string(),
5915 end: "]".to_string(),
5916 close: false,
5917 surround: false,
5918 newline: true,
5919 },
5920 BracketPair {
5921 start: "\"".to_string(),
5922 end: "\"".to_string(),
5923 close: true,
5924 surround: true,
5925 newline: false,
5926 },
5927 BracketPair {
5928 start: "<".to_string(),
5929 end: ">".to_string(),
5930 close: false,
5931 surround: true,
5932 newline: true,
5933 },
5934 ],
5935 ..Default::default()
5936 },
5937 autoclose_before: "})]".to_string(),
5938 ..Default::default()
5939 },
5940 Some(tree_sitter_rust::LANGUAGE.into()),
5941 ));
5942
5943 cx.language_registry().add(language.clone());
5944 cx.update_buffer(|buffer, cx| {
5945 buffer.set_language(Some(language), cx);
5946 });
5947
5948 cx.set_state(
5949 &r#"
5950 🏀ˇ
5951 εˇ
5952 ❤️ˇ
5953 "#
5954 .unindent(),
5955 );
5956
5957 // autoclose multiple nested brackets at multiple cursors
5958 cx.update_editor(|editor, window, cx| {
5959 editor.handle_input("{", window, cx);
5960 editor.handle_input("{", window, cx);
5961 editor.handle_input("{", window, cx);
5962 });
5963 cx.assert_editor_state(
5964 &"
5965 🏀{{{ˇ}}}
5966 ε{{{ˇ}}}
5967 ❤️{{{ˇ}}}
5968 "
5969 .unindent(),
5970 );
5971
5972 // insert a different closing bracket
5973 cx.update_editor(|editor, window, cx| {
5974 editor.handle_input(")", window, cx);
5975 });
5976 cx.assert_editor_state(
5977 &"
5978 🏀{{{)ˇ}}}
5979 ε{{{)ˇ}}}
5980 ❤️{{{)ˇ}}}
5981 "
5982 .unindent(),
5983 );
5984
5985 // skip over the auto-closed brackets when typing a closing bracket
5986 cx.update_editor(|editor, window, cx| {
5987 editor.move_right(&MoveRight, window, cx);
5988 editor.handle_input("}", window, cx);
5989 editor.handle_input("}", window, cx);
5990 editor.handle_input("}", window, cx);
5991 });
5992 cx.assert_editor_state(
5993 &"
5994 🏀{{{)}}}}ˇ
5995 ε{{{)}}}}ˇ
5996 ❤️{{{)}}}}ˇ
5997 "
5998 .unindent(),
5999 );
6000
6001 // autoclose multi-character pairs
6002 cx.set_state(
6003 &"
6004 ˇ
6005 ˇ
6006 "
6007 .unindent(),
6008 );
6009 cx.update_editor(|editor, window, cx| {
6010 editor.handle_input("/", window, cx);
6011 editor.handle_input("*", window, cx);
6012 });
6013 cx.assert_editor_state(
6014 &"
6015 /*ˇ */
6016 /*ˇ */
6017 "
6018 .unindent(),
6019 );
6020
6021 // one cursor autocloses a multi-character pair, one cursor
6022 // does not autoclose.
6023 cx.set_state(
6024 &"
6025 /ˇ
6026 ˇ
6027 "
6028 .unindent(),
6029 );
6030 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6031 cx.assert_editor_state(
6032 &"
6033 /*ˇ */
6034 *ˇ
6035 "
6036 .unindent(),
6037 );
6038
6039 // Don't autoclose if the next character isn't whitespace and isn't
6040 // listed in the language's "autoclose_before" section.
6041 cx.set_state("ˇa b");
6042 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6043 cx.assert_editor_state("{ˇa b");
6044
6045 // Don't autoclose if `close` is false for the bracket pair
6046 cx.set_state("ˇ");
6047 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6048 cx.assert_editor_state("[ˇ");
6049
6050 // Surround with brackets if text is selected
6051 cx.set_state("«aˇ» b");
6052 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6053 cx.assert_editor_state("{«aˇ»} b");
6054
6055 // Autclose pair where the start and end characters are the same
6056 cx.set_state("aˇ");
6057 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6058 cx.assert_editor_state("a\"ˇ\"");
6059 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6060 cx.assert_editor_state("a\"\"ˇ");
6061
6062 // Don't autoclose pair if autoclose is disabled
6063 cx.set_state("ˇ");
6064 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6065 cx.assert_editor_state("<ˇ");
6066
6067 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6068 cx.set_state("«aˇ» b");
6069 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6070 cx.assert_editor_state("<«aˇ»> b");
6071}
6072
6073#[gpui::test]
6074async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut gpui::TestAppContext) {
6075 init_test(cx, |settings| {
6076 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6077 });
6078
6079 let mut cx = EditorTestContext::new(cx).await;
6080
6081 let language = Arc::new(Language::new(
6082 LanguageConfig {
6083 brackets: BracketPairConfig {
6084 pairs: vec![
6085 BracketPair {
6086 start: "{".to_string(),
6087 end: "}".to_string(),
6088 close: true,
6089 surround: true,
6090 newline: true,
6091 },
6092 BracketPair {
6093 start: "(".to_string(),
6094 end: ")".to_string(),
6095 close: true,
6096 surround: true,
6097 newline: true,
6098 },
6099 BracketPair {
6100 start: "[".to_string(),
6101 end: "]".to_string(),
6102 close: false,
6103 surround: false,
6104 newline: true,
6105 },
6106 ],
6107 ..Default::default()
6108 },
6109 autoclose_before: "})]".to_string(),
6110 ..Default::default()
6111 },
6112 Some(tree_sitter_rust::LANGUAGE.into()),
6113 ));
6114
6115 cx.language_registry().add(language.clone());
6116 cx.update_buffer(|buffer, cx| {
6117 buffer.set_language(Some(language), cx);
6118 });
6119
6120 cx.set_state(
6121 &"
6122 ˇ
6123 ˇ
6124 ˇ
6125 "
6126 .unindent(),
6127 );
6128
6129 // ensure only matching closing brackets are skipped over
6130 cx.update_editor(|editor, window, cx| {
6131 editor.handle_input("}", window, cx);
6132 editor.move_left(&MoveLeft, window, cx);
6133 editor.handle_input(")", window, cx);
6134 editor.move_left(&MoveLeft, window, cx);
6135 });
6136 cx.assert_editor_state(
6137 &"
6138 ˇ)}
6139 ˇ)}
6140 ˇ)}
6141 "
6142 .unindent(),
6143 );
6144
6145 // skip-over closing brackets at multiple cursors
6146 cx.update_editor(|editor, window, cx| {
6147 editor.handle_input(")", window, cx);
6148 editor.handle_input("}", window, cx);
6149 });
6150 cx.assert_editor_state(
6151 &"
6152 )}ˇ
6153 )}ˇ
6154 )}ˇ
6155 "
6156 .unindent(),
6157 );
6158
6159 // ignore non-close brackets
6160 cx.update_editor(|editor, window, cx| {
6161 editor.handle_input("]", window, cx);
6162 editor.move_left(&MoveLeft, window, cx);
6163 editor.handle_input("]", window, cx);
6164 });
6165 cx.assert_editor_state(
6166 &"
6167 )}]ˇ]
6168 )}]ˇ]
6169 )}]ˇ]
6170 "
6171 .unindent(),
6172 );
6173}
6174
6175#[gpui::test]
6176async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
6177 init_test(cx, |_| {});
6178
6179 let mut cx = EditorTestContext::new(cx).await;
6180
6181 let html_language = Arc::new(
6182 Language::new(
6183 LanguageConfig {
6184 name: "HTML".into(),
6185 brackets: BracketPairConfig {
6186 pairs: vec![
6187 BracketPair {
6188 start: "<".into(),
6189 end: ">".into(),
6190 close: true,
6191 ..Default::default()
6192 },
6193 BracketPair {
6194 start: "{".into(),
6195 end: "}".into(),
6196 close: true,
6197 ..Default::default()
6198 },
6199 BracketPair {
6200 start: "(".into(),
6201 end: ")".into(),
6202 close: true,
6203 ..Default::default()
6204 },
6205 ],
6206 ..Default::default()
6207 },
6208 autoclose_before: "})]>".into(),
6209 ..Default::default()
6210 },
6211 Some(tree_sitter_html::language()),
6212 )
6213 .with_injection_query(
6214 r#"
6215 (script_element
6216 (raw_text) @injection.content
6217 (#set! injection.language "javascript"))
6218 "#,
6219 )
6220 .unwrap(),
6221 );
6222
6223 let javascript_language = Arc::new(Language::new(
6224 LanguageConfig {
6225 name: "JavaScript".into(),
6226 brackets: BracketPairConfig {
6227 pairs: vec![
6228 BracketPair {
6229 start: "/*".into(),
6230 end: " */".into(),
6231 close: true,
6232 ..Default::default()
6233 },
6234 BracketPair {
6235 start: "{".into(),
6236 end: "}".into(),
6237 close: true,
6238 ..Default::default()
6239 },
6240 BracketPair {
6241 start: "(".into(),
6242 end: ")".into(),
6243 close: true,
6244 ..Default::default()
6245 },
6246 ],
6247 ..Default::default()
6248 },
6249 autoclose_before: "})]>".into(),
6250 ..Default::default()
6251 },
6252 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6253 ));
6254
6255 cx.language_registry().add(html_language.clone());
6256 cx.language_registry().add(javascript_language.clone());
6257
6258 cx.update_buffer(|buffer, cx| {
6259 buffer.set_language(Some(html_language), cx);
6260 });
6261
6262 cx.set_state(
6263 &r#"
6264 <body>ˇ
6265 <script>
6266 var x = 1;ˇ
6267 </script>
6268 </body>ˇ
6269 "#
6270 .unindent(),
6271 );
6272
6273 // Precondition: different languages are active at different locations.
6274 cx.update_editor(|editor, window, cx| {
6275 let snapshot = editor.snapshot(window, cx);
6276 let cursors = editor.selections.ranges::<usize>(cx);
6277 let languages = cursors
6278 .iter()
6279 .map(|c| snapshot.language_at(c.start).unwrap().name())
6280 .collect::<Vec<_>>();
6281 assert_eq!(
6282 languages,
6283 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6284 );
6285 });
6286
6287 // Angle brackets autoclose in HTML, but not JavaScript.
6288 cx.update_editor(|editor, window, cx| {
6289 editor.handle_input("<", window, cx);
6290 editor.handle_input("a", window, cx);
6291 });
6292 cx.assert_editor_state(
6293 &r#"
6294 <body><aˇ>
6295 <script>
6296 var x = 1;<aˇ
6297 </script>
6298 </body><aˇ>
6299 "#
6300 .unindent(),
6301 );
6302
6303 // Curly braces and parens autoclose in both HTML and JavaScript.
6304 cx.update_editor(|editor, window, cx| {
6305 editor.handle_input(" b=", window, cx);
6306 editor.handle_input("{", window, cx);
6307 editor.handle_input("c", window, cx);
6308 editor.handle_input("(", window, cx);
6309 });
6310 cx.assert_editor_state(
6311 &r#"
6312 <body><a b={c(ˇ)}>
6313 <script>
6314 var x = 1;<a b={c(ˇ)}
6315 </script>
6316 </body><a b={c(ˇ)}>
6317 "#
6318 .unindent(),
6319 );
6320
6321 // Brackets that were already autoclosed are skipped.
6322 cx.update_editor(|editor, window, cx| {
6323 editor.handle_input(")", window, cx);
6324 editor.handle_input("d", window, cx);
6325 editor.handle_input("}", window, cx);
6326 });
6327 cx.assert_editor_state(
6328 &r#"
6329 <body><a b={c()d}ˇ>
6330 <script>
6331 var x = 1;<a b={c()d}ˇ
6332 </script>
6333 </body><a b={c()d}ˇ>
6334 "#
6335 .unindent(),
6336 );
6337 cx.update_editor(|editor, window, cx| {
6338 editor.handle_input(">", window, cx);
6339 });
6340 cx.assert_editor_state(
6341 &r#"
6342 <body><a b={c()d}>ˇ
6343 <script>
6344 var x = 1;<a b={c()d}>ˇ
6345 </script>
6346 </body><a b={c()d}>ˇ
6347 "#
6348 .unindent(),
6349 );
6350
6351 // Reset
6352 cx.set_state(
6353 &r#"
6354 <body>ˇ
6355 <script>
6356 var x = 1;ˇ
6357 </script>
6358 </body>ˇ
6359 "#
6360 .unindent(),
6361 );
6362
6363 cx.update_editor(|editor, window, cx| {
6364 editor.handle_input("<", window, cx);
6365 });
6366 cx.assert_editor_state(
6367 &r#"
6368 <body><ˇ>
6369 <script>
6370 var x = 1;<ˇ
6371 </script>
6372 </body><ˇ>
6373 "#
6374 .unindent(),
6375 );
6376
6377 // When backspacing, the closing angle brackets are removed.
6378 cx.update_editor(|editor, window, cx| {
6379 editor.backspace(&Backspace, window, cx);
6380 });
6381 cx.assert_editor_state(
6382 &r#"
6383 <body>ˇ
6384 <script>
6385 var x = 1;ˇ
6386 </script>
6387 </body>ˇ
6388 "#
6389 .unindent(),
6390 );
6391
6392 // Block comments autoclose in JavaScript, but not HTML.
6393 cx.update_editor(|editor, window, cx| {
6394 editor.handle_input("/", window, cx);
6395 editor.handle_input("*", window, cx);
6396 });
6397 cx.assert_editor_state(
6398 &r#"
6399 <body>/*ˇ
6400 <script>
6401 var x = 1;/*ˇ */
6402 </script>
6403 </body>/*ˇ
6404 "#
6405 .unindent(),
6406 );
6407}
6408
6409#[gpui::test]
6410async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
6411 init_test(cx, |_| {});
6412
6413 let mut cx = EditorTestContext::new(cx).await;
6414
6415 let rust_language = Arc::new(
6416 Language::new(
6417 LanguageConfig {
6418 name: "Rust".into(),
6419 brackets: serde_json::from_value(json!([
6420 { "start": "{", "end": "}", "close": true, "newline": true },
6421 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
6422 ]))
6423 .unwrap(),
6424 autoclose_before: "})]>".into(),
6425 ..Default::default()
6426 },
6427 Some(tree_sitter_rust::LANGUAGE.into()),
6428 )
6429 .with_override_query("(string_literal) @string")
6430 .unwrap(),
6431 );
6432
6433 cx.language_registry().add(rust_language.clone());
6434 cx.update_buffer(|buffer, cx| {
6435 buffer.set_language(Some(rust_language), cx);
6436 });
6437
6438 cx.set_state(
6439 &r#"
6440 let x = ˇ
6441 "#
6442 .unindent(),
6443 );
6444
6445 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
6446 cx.update_editor(|editor, window, cx| {
6447 editor.handle_input("\"", window, cx);
6448 });
6449 cx.assert_editor_state(
6450 &r#"
6451 let x = "ˇ"
6452 "#
6453 .unindent(),
6454 );
6455
6456 // Inserting another quotation mark. The cursor moves across the existing
6457 // automatically-inserted quotation mark.
6458 cx.update_editor(|editor, window, cx| {
6459 editor.handle_input("\"", window, cx);
6460 });
6461 cx.assert_editor_state(
6462 &r#"
6463 let x = ""ˇ
6464 "#
6465 .unindent(),
6466 );
6467
6468 // Reset
6469 cx.set_state(
6470 &r#"
6471 let x = ˇ
6472 "#
6473 .unindent(),
6474 );
6475
6476 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
6477 cx.update_editor(|editor, window, cx| {
6478 editor.handle_input("\"", window, cx);
6479 editor.handle_input(" ", window, cx);
6480 editor.move_left(&Default::default(), window, cx);
6481 editor.handle_input("\\", window, cx);
6482 editor.handle_input("\"", window, cx);
6483 });
6484 cx.assert_editor_state(
6485 &r#"
6486 let x = "\"ˇ "
6487 "#
6488 .unindent(),
6489 );
6490
6491 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
6492 // mark. Nothing is inserted.
6493 cx.update_editor(|editor, window, cx| {
6494 editor.move_right(&Default::default(), window, cx);
6495 editor.handle_input("\"", window, cx);
6496 });
6497 cx.assert_editor_state(
6498 &r#"
6499 let x = "\" "ˇ
6500 "#
6501 .unindent(),
6502 );
6503}
6504
6505#[gpui::test]
6506async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
6507 init_test(cx, |_| {});
6508
6509 let language = Arc::new(Language::new(
6510 LanguageConfig {
6511 brackets: BracketPairConfig {
6512 pairs: vec![
6513 BracketPair {
6514 start: "{".to_string(),
6515 end: "}".to_string(),
6516 close: true,
6517 surround: true,
6518 newline: true,
6519 },
6520 BracketPair {
6521 start: "/* ".to_string(),
6522 end: "*/".to_string(),
6523 close: true,
6524 surround: true,
6525 ..Default::default()
6526 },
6527 ],
6528 ..Default::default()
6529 },
6530 ..Default::default()
6531 },
6532 Some(tree_sitter_rust::LANGUAGE.into()),
6533 ));
6534
6535 let text = r#"
6536 a
6537 b
6538 c
6539 "#
6540 .unindent();
6541
6542 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6543 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6544 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6545 editor
6546 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6547 .await;
6548
6549 editor.update_in(cx, |editor, window, cx| {
6550 editor.change_selections(None, window, cx, |s| {
6551 s.select_display_ranges([
6552 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6553 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6554 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
6555 ])
6556 });
6557
6558 editor.handle_input("{", window, cx);
6559 editor.handle_input("{", window, cx);
6560 editor.handle_input("{", window, cx);
6561 assert_eq!(
6562 editor.text(cx),
6563 "
6564 {{{a}}}
6565 {{{b}}}
6566 {{{c}}}
6567 "
6568 .unindent()
6569 );
6570 assert_eq!(
6571 editor.selections.display_ranges(cx),
6572 [
6573 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
6574 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
6575 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
6576 ]
6577 );
6578
6579 editor.undo(&Undo, window, cx);
6580 editor.undo(&Undo, window, cx);
6581 editor.undo(&Undo, window, cx);
6582 assert_eq!(
6583 editor.text(cx),
6584 "
6585 a
6586 b
6587 c
6588 "
6589 .unindent()
6590 );
6591 assert_eq!(
6592 editor.selections.display_ranges(cx),
6593 [
6594 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6595 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6596 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
6597 ]
6598 );
6599
6600 // Ensure inserting the first character of a multi-byte bracket pair
6601 // doesn't surround the selections with the bracket.
6602 editor.handle_input("/", window, cx);
6603 assert_eq!(
6604 editor.text(cx),
6605 "
6606 /
6607 /
6608 /
6609 "
6610 .unindent()
6611 );
6612 assert_eq!(
6613 editor.selections.display_ranges(cx),
6614 [
6615 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6616 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6617 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
6618 ]
6619 );
6620
6621 editor.undo(&Undo, window, cx);
6622 assert_eq!(
6623 editor.text(cx),
6624 "
6625 a
6626 b
6627 c
6628 "
6629 .unindent()
6630 );
6631 assert_eq!(
6632 editor.selections.display_ranges(cx),
6633 [
6634 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6635 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6636 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
6637 ]
6638 );
6639
6640 // Ensure inserting the last character of a multi-byte bracket pair
6641 // doesn't surround the selections with the bracket.
6642 editor.handle_input("*", window, cx);
6643 assert_eq!(
6644 editor.text(cx),
6645 "
6646 *
6647 *
6648 *
6649 "
6650 .unindent()
6651 );
6652 assert_eq!(
6653 editor.selections.display_ranges(cx),
6654 [
6655 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6656 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6657 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
6658 ]
6659 );
6660 });
6661}
6662
6663#[gpui::test]
6664async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
6665 init_test(cx, |_| {});
6666
6667 let language = Arc::new(Language::new(
6668 LanguageConfig {
6669 brackets: BracketPairConfig {
6670 pairs: vec![BracketPair {
6671 start: "{".to_string(),
6672 end: "}".to_string(),
6673 close: true,
6674 surround: true,
6675 newline: true,
6676 }],
6677 ..Default::default()
6678 },
6679 autoclose_before: "}".to_string(),
6680 ..Default::default()
6681 },
6682 Some(tree_sitter_rust::LANGUAGE.into()),
6683 ));
6684
6685 let text = r#"
6686 a
6687 b
6688 c
6689 "#
6690 .unindent();
6691
6692 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6693 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6694 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6695 editor
6696 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6697 .await;
6698
6699 editor.update_in(cx, |editor, window, cx| {
6700 editor.change_selections(None, window, cx, |s| {
6701 s.select_ranges([
6702 Point::new(0, 1)..Point::new(0, 1),
6703 Point::new(1, 1)..Point::new(1, 1),
6704 Point::new(2, 1)..Point::new(2, 1),
6705 ])
6706 });
6707
6708 editor.handle_input("{", window, cx);
6709 editor.handle_input("{", window, cx);
6710 editor.handle_input("_", window, cx);
6711 assert_eq!(
6712 editor.text(cx),
6713 "
6714 a{{_}}
6715 b{{_}}
6716 c{{_}}
6717 "
6718 .unindent()
6719 );
6720 assert_eq!(
6721 editor.selections.ranges::<Point>(cx),
6722 [
6723 Point::new(0, 4)..Point::new(0, 4),
6724 Point::new(1, 4)..Point::new(1, 4),
6725 Point::new(2, 4)..Point::new(2, 4)
6726 ]
6727 );
6728
6729 editor.backspace(&Default::default(), window, cx);
6730 editor.backspace(&Default::default(), window, cx);
6731 assert_eq!(
6732 editor.text(cx),
6733 "
6734 a{}
6735 b{}
6736 c{}
6737 "
6738 .unindent()
6739 );
6740 assert_eq!(
6741 editor.selections.ranges::<Point>(cx),
6742 [
6743 Point::new(0, 2)..Point::new(0, 2),
6744 Point::new(1, 2)..Point::new(1, 2),
6745 Point::new(2, 2)..Point::new(2, 2)
6746 ]
6747 );
6748
6749 editor.delete_to_previous_word_start(&Default::default(), window, cx);
6750 assert_eq!(
6751 editor.text(cx),
6752 "
6753 a
6754 b
6755 c
6756 "
6757 .unindent()
6758 );
6759 assert_eq!(
6760 editor.selections.ranges::<Point>(cx),
6761 [
6762 Point::new(0, 1)..Point::new(0, 1),
6763 Point::new(1, 1)..Point::new(1, 1),
6764 Point::new(2, 1)..Point::new(2, 1)
6765 ]
6766 );
6767 });
6768}
6769
6770#[gpui::test]
6771async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut gpui::TestAppContext) {
6772 init_test(cx, |settings| {
6773 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6774 });
6775
6776 let mut cx = EditorTestContext::new(cx).await;
6777
6778 let language = Arc::new(Language::new(
6779 LanguageConfig {
6780 brackets: BracketPairConfig {
6781 pairs: vec![
6782 BracketPair {
6783 start: "{".to_string(),
6784 end: "}".to_string(),
6785 close: true,
6786 surround: true,
6787 newline: true,
6788 },
6789 BracketPair {
6790 start: "(".to_string(),
6791 end: ")".to_string(),
6792 close: true,
6793 surround: true,
6794 newline: true,
6795 },
6796 BracketPair {
6797 start: "[".to_string(),
6798 end: "]".to_string(),
6799 close: false,
6800 surround: true,
6801 newline: true,
6802 },
6803 ],
6804 ..Default::default()
6805 },
6806 autoclose_before: "})]".to_string(),
6807 ..Default::default()
6808 },
6809 Some(tree_sitter_rust::LANGUAGE.into()),
6810 ));
6811
6812 cx.language_registry().add(language.clone());
6813 cx.update_buffer(|buffer, cx| {
6814 buffer.set_language(Some(language), cx);
6815 });
6816
6817 cx.set_state(
6818 &"
6819 {(ˇ)}
6820 [[ˇ]]
6821 {(ˇ)}
6822 "
6823 .unindent(),
6824 );
6825
6826 cx.update_editor(|editor, window, cx| {
6827 editor.backspace(&Default::default(), window, cx);
6828 editor.backspace(&Default::default(), window, cx);
6829 });
6830
6831 cx.assert_editor_state(
6832 &"
6833 ˇ
6834 ˇ]]
6835 ˇ
6836 "
6837 .unindent(),
6838 );
6839
6840 cx.update_editor(|editor, window, cx| {
6841 editor.handle_input("{", window, cx);
6842 editor.handle_input("{", window, cx);
6843 editor.move_right(&MoveRight, window, cx);
6844 editor.move_right(&MoveRight, window, cx);
6845 editor.move_left(&MoveLeft, window, cx);
6846 editor.move_left(&MoveLeft, window, cx);
6847 editor.backspace(&Default::default(), window, cx);
6848 });
6849
6850 cx.assert_editor_state(
6851 &"
6852 {ˇ}
6853 {ˇ}]]
6854 {ˇ}
6855 "
6856 .unindent(),
6857 );
6858
6859 cx.update_editor(|editor, window, cx| {
6860 editor.backspace(&Default::default(), window, cx);
6861 });
6862
6863 cx.assert_editor_state(
6864 &"
6865 ˇ
6866 ˇ]]
6867 ˇ
6868 "
6869 .unindent(),
6870 );
6871}
6872
6873#[gpui::test]
6874async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) {
6875 init_test(cx, |_| {});
6876
6877 let language = Arc::new(Language::new(
6878 LanguageConfig::default(),
6879 Some(tree_sitter_rust::LANGUAGE.into()),
6880 ));
6881
6882 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
6883 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6884 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6885 editor
6886 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6887 .await;
6888
6889 editor.update_in(cx, |editor, window, cx| {
6890 editor.set_auto_replace_emoji_shortcode(true);
6891
6892 editor.handle_input("Hello ", window, cx);
6893 editor.handle_input(":wave", window, cx);
6894 assert_eq!(editor.text(cx), "Hello :wave".unindent());
6895
6896 editor.handle_input(":", window, cx);
6897 assert_eq!(editor.text(cx), "Hello 👋".unindent());
6898
6899 editor.handle_input(" :smile", window, cx);
6900 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
6901
6902 editor.handle_input(":", window, cx);
6903 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
6904
6905 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
6906 editor.handle_input(":wave", window, cx);
6907 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
6908
6909 editor.handle_input(":", window, cx);
6910 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
6911
6912 editor.handle_input(":1", window, cx);
6913 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
6914
6915 editor.handle_input(":", window, cx);
6916 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
6917
6918 // Ensure shortcode does not get replaced when it is part of a word
6919 editor.handle_input(" Test:wave", window, cx);
6920 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
6921
6922 editor.handle_input(":", window, cx);
6923 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
6924
6925 editor.set_auto_replace_emoji_shortcode(false);
6926
6927 // Ensure shortcode does not get replaced when auto replace is off
6928 editor.handle_input(" :wave", window, cx);
6929 assert_eq!(
6930 editor.text(cx),
6931 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
6932 );
6933
6934 editor.handle_input(":", window, cx);
6935 assert_eq!(
6936 editor.text(cx),
6937 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
6938 );
6939 });
6940}
6941
6942#[gpui::test]
6943async fn test_snippet_placeholder_choices(cx: &mut gpui::TestAppContext) {
6944 init_test(cx, |_| {});
6945
6946 let (text, insertion_ranges) = marked_text_ranges(
6947 indoc! {"
6948 ˇ
6949 "},
6950 false,
6951 );
6952
6953 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
6954 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6955
6956 _ = editor.update_in(cx, |editor, window, cx| {
6957 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
6958
6959 editor
6960 .insert_snippet(&insertion_ranges, snippet, window, cx)
6961 .unwrap();
6962
6963 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
6964 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
6965 assert_eq!(editor.text(cx), expected_text);
6966 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
6967 }
6968
6969 assert(
6970 editor,
6971 cx,
6972 indoc! {"
6973 type «» =•
6974 "},
6975 );
6976
6977 assert!(editor.context_menu_visible(), "There should be a matches");
6978 });
6979}
6980
6981#[gpui::test]
6982async fn test_snippets(cx: &mut gpui::TestAppContext) {
6983 init_test(cx, |_| {});
6984
6985 let (text, insertion_ranges) = marked_text_ranges(
6986 indoc! {"
6987 a.ˇ b
6988 a.ˇ b
6989 a.ˇ b
6990 "},
6991 false,
6992 );
6993
6994 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
6995 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6996
6997 editor.update_in(cx, |editor, window, cx| {
6998 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
6999
7000 editor
7001 .insert_snippet(&insertion_ranges, snippet, window, cx)
7002 .unwrap();
7003
7004 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7005 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7006 assert_eq!(editor.text(cx), expected_text);
7007 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7008 }
7009
7010 assert(
7011 editor,
7012 cx,
7013 indoc! {"
7014 a.f(«one», two, «three») b
7015 a.f(«one», two, «three») b
7016 a.f(«one», two, «three») b
7017 "},
7018 );
7019
7020 // Can't move earlier than the first tab stop
7021 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7022 assert(
7023 editor,
7024 cx,
7025 indoc! {"
7026 a.f(«one», two, «three») b
7027 a.f(«one», two, «three») b
7028 a.f(«one», two, «three») b
7029 "},
7030 );
7031
7032 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7033 assert(
7034 editor,
7035 cx,
7036 indoc! {"
7037 a.f(one, «two», three) b
7038 a.f(one, «two», three) b
7039 a.f(one, «two», three) b
7040 "},
7041 );
7042
7043 editor.move_to_prev_snippet_tabstop(window, cx);
7044 assert(
7045 editor,
7046 cx,
7047 indoc! {"
7048 a.f(«one», two, «three») b
7049 a.f(«one», two, «three») b
7050 a.f(«one», two, «three») b
7051 "},
7052 );
7053
7054 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7055 assert(
7056 editor,
7057 cx,
7058 indoc! {"
7059 a.f(one, «two», three) b
7060 a.f(one, «two», three) b
7061 a.f(one, «two», three) b
7062 "},
7063 );
7064 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7065 assert(
7066 editor,
7067 cx,
7068 indoc! {"
7069 a.f(one, two, three)ˇ b
7070 a.f(one, two, three)ˇ b
7071 a.f(one, two, three)ˇ b
7072 "},
7073 );
7074
7075 // As soon as the last tab stop is reached, snippet state is gone
7076 editor.move_to_prev_snippet_tabstop(window, cx);
7077 assert(
7078 editor,
7079 cx,
7080 indoc! {"
7081 a.f(one, two, three)ˇ b
7082 a.f(one, two, three)ˇ b
7083 a.f(one, two, three)ˇ b
7084 "},
7085 );
7086 });
7087}
7088
7089#[gpui::test]
7090async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
7091 init_test(cx, |_| {});
7092
7093 let fs = FakeFs::new(cx.executor());
7094 fs.insert_file(path!("/file.rs"), Default::default()).await;
7095
7096 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
7097
7098 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7099 language_registry.add(rust_lang());
7100 let mut fake_servers = language_registry.register_fake_lsp(
7101 "Rust",
7102 FakeLspAdapter {
7103 capabilities: lsp::ServerCapabilities {
7104 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7105 ..Default::default()
7106 },
7107 ..Default::default()
7108 },
7109 );
7110
7111 let buffer = project
7112 .update(cx, |project, cx| {
7113 project.open_local_buffer(path!("/file.rs"), cx)
7114 })
7115 .await
7116 .unwrap();
7117
7118 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7119 let (editor, cx) = cx.add_window_view(|window, cx| {
7120 build_editor_with_project(project.clone(), buffer, window, cx)
7121 });
7122 editor.update_in(cx, |editor, window, cx| {
7123 editor.set_text("one\ntwo\nthree\n", window, cx)
7124 });
7125 assert!(cx.read(|cx| editor.is_dirty(cx)));
7126
7127 cx.executor().start_waiting();
7128 let fake_server = fake_servers.next().await.unwrap();
7129
7130 let save = editor
7131 .update_in(cx, |editor, window, cx| {
7132 editor.save(true, project.clone(), window, cx)
7133 })
7134 .unwrap();
7135 fake_server
7136 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7137 assert_eq!(
7138 params.text_document.uri,
7139 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7140 );
7141 assert_eq!(params.options.tab_size, 4);
7142 Ok(Some(vec![lsp::TextEdit::new(
7143 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7144 ", ".to_string(),
7145 )]))
7146 })
7147 .next()
7148 .await;
7149 cx.executor().start_waiting();
7150 save.await;
7151
7152 assert_eq!(
7153 editor.update(cx, |editor, cx| editor.text(cx)),
7154 "one, two\nthree\n"
7155 );
7156 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7157
7158 editor.update_in(cx, |editor, window, cx| {
7159 editor.set_text("one\ntwo\nthree\n", window, cx)
7160 });
7161 assert!(cx.read(|cx| editor.is_dirty(cx)));
7162
7163 // Ensure we can still save even if formatting hangs.
7164 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7165 assert_eq!(
7166 params.text_document.uri,
7167 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7168 );
7169 futures::future::pending::<()>().await;
7170 unreachable!()
7171 });
7172 let save = editor
7173 .update_in(cx, |editor, window, cx| {
7174 editor.save(true, project.clone(), window, cx)
7175 })
7176 .unwrap();
7177 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7178 cx.executor().start_waiting();
7179 save.await;
7180 assert_eq!(
7181 editor.update(cx, |editor, cx| editor.text(cx)),
7182 "one\ntwo\nthree\n"
7183 );
7184 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7185
7186 // For non-dirty buffer, no formatting request should be sent
7187 let save = editor
7188 .update_in(cx, |editor, window, cx| {
7189 editor.save(true, project.clone(), window, cx)
7190 })
7191 .unwrap();
7192 let _pending_format_request = fake_server
7193 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7194 panic!("Should not be invoked on non-dirty buffer");
7195 })
7196 .next();
7197 cx.executor().start_waiting();
7198 save.await;
7199
7200 // Set rust language override and assert overridden tabsize is sent to language server
7201 update_test_language_settings(cx, |settings| {
7202 settings.languages.insert(
7203 "Rust".into(),
7204 LanguageSettingsContent {
7205 tab_size: NonZeroU32::new(8),
7206 ..Default::default()
7207 },
7208 );
7209 });
7210
7211 editor.update_in(cx, |editor, window, cx| {
7212 editor.set_text("somehting_new\n", window, cx)
7213 });
7214 assert!(cx.read(|cx| editor.is_dirty(cx)));
7215 let save = editor
7216 .update_in(cx, |editor, window, cx| {
7217 editor.save(true, project.clone(), window, cx)
7218 })
7219 .unwrap();
7220 fake_server
7221 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7222 assert_eq!(
7223 params.text_document.uri,
7224 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7225 );
7226 assert_eq!(params.options.tab_size, 8);
7227 Ok(Some(vec![]))
7228 })
7229 .next()
7230 .await;
7231 cx.executor().start_waiting();
7232 save.await;
7233}
7234
7235#[gpui::test]
7236async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) {
7237 init_test(cx, |_| {});
7238
7239 let cols = 4;
7240 let rows = 10;
7241 let sample_text_1 = sample_text(rows, cols, 'a');
7242 assert_eq!(
7243 sample_text_1,
7244 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7245 );
7246 let sample_text_2 = sample_text(rows, cols, 'l');
7247 assert_eq!(
7248 sample_text_2,
7249 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7250 );
7251 let sample_text_3 = sample_text(rows, cols, 'v');
7252 assert_eq!(
7253 sample_text_3,
7254 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7255 );
7256
7257 let fs = FakeFs::new(cx.executor());
7258 fs.insert_tree(
7259 path!("/a"),
7260 json!({
7261 "main.rs": sample_text_1,
7262 "other.rs": sample_text_2,
7263 "lib.rs": sample_text_3,
7264 }),
7265 )
7266 .await;
7267
7268 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
7269 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7270 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7271
7272 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7273 language_registry.add(rust_lang());
7274 let mut fake_servers = language_registry.register_fake_lsp(
7275 "Rust",
7276 FakeLspAdapter {
7277 capabilities: lsp::ServerCapabilities {
7278 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7279 ..Default::default()
7280 },
7281 ..Default::default()
7282 },
7283 );
7284
7285 let worktree = project.update(cx, |project, cx| {
7286 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7287 assert_eq!(worktrees.len(), 1);
7288 worktrees.pop().unwrap()
7289 });
7290 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
7291
7292 let buffer_1 = project
7293 .update(cx, |project, cx| {
7294 project.open_buffer((worktree_id, "main.rs"), cx)
7295 })
7296 .await
7297 .unwrap();
7298 let buffer_2 = project
7299 .update(cx, |project, cx| {
7300 project.open_buffer((worktree_id, "other.rs"), cx)
7301 })
7302 .await
7303 .unwrap();
7304 let buffer_3 = project
7305 .update(cx, |project, cx| {
7306 project.open_buffer((worktree_id, "lib.rs"), cx)
7307 })
7308 .await
7309 .unwrap();
7310
7311 let multi_buffer = cx.new(|cx| {
7312 let mut multi_buffer = MultiBuffer::new(ReadWrite);
7313 multi_buffer.push_excerpts(
7314 buffer_1.clone(),
7315 [
7316 ExcerptRange {
7317 context: Point::new(0, 0)..Point::new(3, 0),
7318 primary: None,
7319 },
7320 ExcerptRange {
7321 context: Point::new(5, 0)..Point::new(7, 0),
7322 primary: None,
7323 },
7324 ExcerptRange {
7325 context: Point::new(9, 0)..Point::new(10, 4),
7326 primary: None,
7327 },
7328 ],
7329 cx,
7330 );
7331 multi_buffer.push_excerpts(
7332 buffer_2.clone(),
7333 [
7334 ExcerptRange {
7335 context: Point::new(0, 0)..Point::new(3, 0),
7336 primary: None,
7337 },
7338 ExcerptRange {
7339 context: Point::new(5, 0)..Point::new(7, 0),
7340 primary: None,
7341 },
7342 ExcerptRange {
7343 context: Point::new(9, 0)..Point::new(10, 4),
7344 primary: None,
7345 },
7346 ],
7347 cx,
7348 );
7349 multi_buffer.push_excerpts(
7350 buffer_3.clone(),
7351 [
7352 ExcerptRange {
7353 context: Point::new(0, 0)..Point::new(3, 0),
7354 primary: None,
7355 },
7356 ExcerptRange {
7357 context: Point::new(5, 0)..Point::new(7, 0),
7358 primary: None,
7359 },
7360 ExcerptRange {
7361 context: Point::new(9, 0)..Point::new(10, 4),
7362 primary: None,
7363 },
7364 ],
7365 cx,
7366 );
7367 multi_buffer
7368 });
7369 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
7370 Editor::new(
7371 EditorMode::Full,
7372 multi_buffer,
7373 Some(project.clone()),
7374 true,
7375 window,
7376 cx,
7377 )
7378 });
7379
7380 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7381 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7382 s.select_ranges(Some(1..2))
7383 });
7384 editor.insert("|one|two|three|", window, cx);
7385 });
7386 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7387 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7388 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7389 s.select_ranges(Some(60..70))
7390 });
7391 editor.insert("|four|five|six|", window, cx);
7392 });
7393 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7394
7395 // First two buffers should be edited, but not the third one.
7396 assert_eq!(
7397 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7398 "a|one|two|three|aa\nbbbb\ncccc\n\nffff\ngggg\n\njjjj\nllll\nmmmm\nnnnn|four|five|six|\nr\n\nuuuu\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}",
7399 );
7400 buffer_1.update(cx, |buffer, _| {
7401 assert!(buffer.is_dirty());
7402 assert_eq!(
7403 buffer.text(),
7404 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
7405 )
7406 });
7407 buffer_2.update(cx, |buffer, _| {
7408 assert!(buffer.is_dirty());
7409 assert_eq!(
7410 buffer.text(),
7411 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
7412 )
7413 });
7414 buffer_3.update(cx, |buffer, _| {
7415 assert!(!buffer.is_dirty());
7416 assert_eq!(buffer.text(), sample_text_3,)
7417 });
7418 cx.executor().run_until_parked();
7419
7420 cx.executor().start_waiting();
7421 let save = multi_buffer_editor
7422 .update_in(cx, |editor, window, cx| {
7423 editor.save(true, project.clone(), window, cx)
7424 })
7425 .unwrap();
7426
7427 let fake_server = fake_servers.next().await.unwrap();
7428 fake_server
7429 .server
7430 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7431 Ok(Some(vec![lsp::TextEdit::new(
7432 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7433 format!("[{} formatted]", params.text_document.uri),
7434 )]))
7435 })
7436 .detach();
7437 save.await;
7438
7439 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
7440 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
7441 assert_eq!(
7442 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7443 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\n\nffff\ngggg\n\njjjj\n\nlll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|\nr\n\nuuuu\n\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}"),
7444 );
7445 buffer_1.update(cx, |buffer, _| {
7446 assert!(!buffer.is_dirty());
7447 assert_eq!(
7448 buffer.text(),
7449 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
7450 )
7451 });
7452 buffer_2.update(cx, |buffer, _| {
7453 assert!(!buffer.is_dirty());
7454 assert_eq!(
7455 buffer.text(),
7456 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
7457 )
7458 });
7459 buffer_3.update(cx, |buffer, _| {
7460 assert!(!buffer.is_dirty());
7461 assert_eq!(buffer.text(), sample_text_3,)
7462 });
7463}
7464
7465#[gpui::test]
7466async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
7467 init_test(cx, |_| {});
7468
7469 let fs = FakeFs::new(cx.executor());
7470 fs.insert_file(path!("/file.rs"), Default::default()).await;
7471
7472 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
7473
7474 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7475 language_registry.add(rust_lang());
7476 let mut fake_servers = language_registry.register_fake_lsp(
7477 "Rust",
7478 FakeLspAdapter {
7479 capabilities: lsp::ServerCapabilities {
7480 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
7481 ..Default::default()
7482 },
7483 ..Default::default()
7484 },
7485 );
7486
7487 let buffer = project
7488 .update(cx, |project, cx| {
7489 project.open_local_buffer(path!("/file.rs"), cx)
7490 })
7491 .await
7492 .unwrap();
7493
7494 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7495 let (editor, cx) = cx.add_window_view(|window, cx| {
7496 build_editor_with_project(project.clone(), buffer, window, cx)
7497 });
7498 editor.update_in(cx, |editor, window, cx| {
7499 editor.set_text("one\ntwo\nthree\n", window, cx)
7500 });
7501 assert!(cx.read(|cx| editor.is_dirty(cx)));
7502
7503 cx.executor().start_waiting();
7504 let fake_server = fake_servers.next().await.unwrap();
7505
7506 let save = editor
7507 .update_in(cx, |editor, window, cx| {
7508 editor.save(true, project.clone(), window, cx)
7509 })
7510 .unwrap();
7511 fake_server
7512 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
7513 assert_eq!(
7514 params.text_document.uri,
7515 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7516 );
7517 assert_eq!(params.options.tab_size, 4);
7518 Ok(Some(vec![lsp::TextEdit::new(
7519 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7520 ", ".to_string(),
7521 )]))
7522 })
7523 .next()
7524 .await;
7525 cx.executor().start_waiting();
7526 save.await;
7527 assert_eq!(
7528 editor.update(cx, |editor, cx| editor.text(cx)),
7529 "one, two\nthree\n"
7530 );
7531 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7532
7533 editor.update_in(cx, |editor, window, cx| {
7534 editor.set_text("one\ntwo\nthree\n", window, cx)
7535 });
7536 assert!(cx.read(|cx| editor.is_dirty(cx)));
7537
7538 // Ensure we can still save even if formatting hangs.
7539 fake_server.handle_request::<lsp::request::RangeFormatting, _, _>(
7540 move |params, _| async move {
7541 assert_eq!(
7542 params.text_document.uri,
7543 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7544 );
7545 futures::future::pending::<()>().await;
7546 unreachable!()
7547 },
7548 );
7549 let save = editor
7550 .update_in(cx, |editor, window, cx| {
7551 editor.save(true, project.clone(), window, cx)
7552 })
7553 .unwrap();
7554 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7555 cx.executor().start_waiting();
7556 save.await;
7557 assert_eq!(
7558 editor.update(cx, |editor, cx| editor.text(cx)),
7559 "one\ntwo\nthree\n"
7560 );
7561 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7562
7563 // For non-dirty buffer, no formatting request should be sent
7564 let save = editor
7565 .update_in(cx, |editor, window, cx| {
7566 editor.save(true, project.clone(), window, cx)
7567 })
7568 .unwrap();
7569 let _pending_format_request = fake_server
7570 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7571 panic!("Should not be invoked on non-dirty buffer");
7572 })
7573 .next();
7574 cx.executor().start_waiting();
7575 save.await;
7576
7577 // Set Rust language override and assert overridden tabsize is sent to language server
7578 update_test_language_settings(cx, |settings| {
7579 settings.languages.insert(
7580 "Rust".into(),
7581 LanguageSettingsContent {
7582 tab_size: NonZeroU32::new(8),
7583 ..Default::default()
7584 },
7585 );
7586 });
7587
7588 editor.update_in(cx, |editor, window, cx| {
7589 editor.set_text("somehting_new\n", window, cx)
7590 });
7591 assert!(cx.read(|cx| editor.is_dirty(cx)));
7592 let save = editor
7593 .update_in(cx, |editor, window, cx| {
7594 editor.save(true, project.clone(), window, cx)
7595 })
7596 .unwrap();
7597 fake_server
7598 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
7599 assert_eq!(
7600 params.text_document.uri,
7601 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7602 );
7603 assert_eq!(params.options.tab_size, 8);
7604 Ok(Some(vec![]))
7605 })
7606 .next()
7607 .await;
7608 cx.executor().start_waiting();
7609 save.await;
7610}
7611
7612#[gpui::test]
7613async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
7614 init_test(cx, |settings| {
7615 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
7616 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
7617 ))
7618 });
7619
7620 let fs = FakeFs::new(cx.executor());
7621 fs.insert_file(path!("/file.rs"), Default::default()).await;
7622
7623 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
7624
7625 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7626 language_registry.add(Arc::new(Language::new(
7627 LanguageConfig {
7628 name: "Rust".into(),
7629 matcher: LanguageMatcher {
7630 path_suffixes: vec!["rs".to_string()],
7631 ..Default::default()
7632 },
7633 ..LanguageConfig::default()
7634 },
7635 Some(tree_sitter_rust::LANGUAGE.into()),
7636 )));
7637 update_test_language_settings(cx, |settings| {
7638 // Enable Prettier formatting for the same buffer, and ensure
7639 // LSP is called instead of Prettier.
7640 settings.defaults.prettier = Some(PrettierSettings {
7641 allowed: true,
7642 ..PrettierSettings::default()
7643 });
7644 });
7645 let mut fake_servers = language_registry.register_fake_lsp(
7646 "Rust",
7647 FakeLspAdapter {
7648 capabilities: lsp::ServerCapabilities {
7649 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7650 ..Default::default()
7651 },
7652 ..Default::default()
7653 },
7654 );
7655
7656 let buffer = project
7657 .update(cx, |project, cx| {
7658 project.open_local_buffer(path!("/file.rs"), cx)
7659 })
7660 .await
7661 .unwrap();
7662
7663 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7664 let (editor, cx) = cx.add_window_view(|window, cx| {
7665 build_editor_with_project(project.clone(), buffer, window, cx)
7666 });
7667 editor.update_in(cx, |editor, window, cx| {
7668 editor.set_text("one\ntwo\nthree\n", window, cx)
7669 });
7670
7671 cx.executor().start_waiting();
7672 let fake_server = fake_servers.next().await.unwrap();
7673
7674 let format = editor
7675 .update_in(cx, |editor, window, cx| {
7676 editor.perform_format(
7677 project.clone(),
7678 FormatTrigger::Manual,
7679 FormatTarget::Buffers,
7680 window,
7681 cx,
7682 )
7683 })
7684 .unwrap();
7685 fake_server
7686 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7687 assert_eq!(
7688 params.text_document.uri,
7689 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7690 );
7691 assert_eq!(params.options.tab_size, 4);
7692 Ok(Some(vec![lsp::TextEdit::new(
7693 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7694 ", ".to_string(),
7695 )]))
7696 })
7697 .next()
7698 .await;
7699 cx.executor().start_waiting();
7700 format.await;
7701 assert_eq!(
7702 editor.update(cx, |editor, cx| editor.text(cx)),
7703 "one, two\nthree\n"
7704 );
7705
7706 editor.update_in(cx, |editor, window, cx| {
7707 editor.set_text("one\ntwo\nthree\n", window, cx)
7708 });
7709 // Ensure we don't lock if formatting hangs.
7710 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7711 assert_eq!(
7712 params.text_document.uri,
7713 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7714 );
7715 futures::future::pending::<()>().await;
7716 unreachable!()
7717 });
7718 let format = editor
7719 .update_in(cx, |editor, window, cx| {
7720 editor.perform_format(
7721 project,
7722 FormatTrigger::Manual,
7723 FormatTarget::Buffers,
7724 window,
7725 cx,
7726 )
7727 })
7728 .unwrap();
7729 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7730 cx.executor().start_waiting();
7731 format.await;
7732 assert_eq!(
7733 editor.update(cx, |editor, cx| editor.text(cx)),
7734 "one\ntwo\nthree\n"
7735 );
7736}
7737
7738#[gpui::test]
7739async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
7740 init_test(cx, |_| {});
7741
7742 let mut cx = EditorLspTestContext::new_rust(
7743 lsp::ServerCapabilities {
7744 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7745 ..Default::default()
7746 },
7747 cx,
7748 )
7749 .await;
7750
7751 cx.set_state(indoc! {"
7752 one.twoˇ
7753 "});
7754
7755 // The format request takes a long time. When it completes, it inserts
7756 // a newline and an indent before the `.`
7757 cx.lsp
7758 .handle_request::<lsp::request::Formatting, _, _>(move |_, cx| {
7759 let executor = cx.background_executor().clone();
7760 async move {
7761 executor.timer(Duration::from_millis(100)).await;
7762 Ok(Some(vec![lsp::TextEdit {
7763 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
7764 new_text: "\n ".into(),
7765 }]))
7766 }
7767 });
7768
7769 // Submit a format request.
7770 let format_1 = cx
7771 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7772 .unwrap();
7773 cx.executor().run_until_parked();
7774
7775 // Submit a second format request.
7776 let format_2 = cx
7777 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7778 .unwrap();
7779 cx.executor().run_until_parked();
7780
7781 // Wait for both format requests to complete
7782 cx.executor().advance_clock(Duration::from_millis(200));
7783 cx.executor().start_waiting();
7784 format_1.await.unwrap();
7785 cx.executor().start_waiting();
7786 format_2.await.unwrap();
7787
7788 // The formatting edits only happens once.
7789 cx.assert_editor_state(indoc! {"
7790 one
7791 .twoˇ
7792 "});
7793}
7794
7795#[gpui::test]
7796async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
7797 init_test(cx, |settings| {
7798 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
7799 });
7800
7801 let mut cx = EditorLspTestContext::new_rust(
7802 lsp::ServerCapabilities {
7803 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7804 ..Default::default()
7805 },
7806 cx,
7807 )
7808 .await;
7809
7810 // Set up a buffer white some trailing whitespace and no trailing newline.
7811 cx.set_state(
7812 &[
7813 "one ", //
7814 "twoˇ", //
7815 "three ", //
7816 "four", //
7817 ]
7818 .join("\n"),
7819 );
7820
7821 // Submit a format request.
7822 let format = cx
7823 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7824 .unwrap();
7825
7826 // Record which buffer changes have been sent to the language server
7827 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
7828 cx.lsp
7829 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
7830 let buffer_changes = buffer_changes.clone();
7831 move |params, _| {
7832 buffer_changes.lock().extend(
7833 params
7834 .content_changes
7835 .into_iter()
7836 .map(|e| (e.range.unwrap(), e.text)),
7837 );
7838 }
7839 });
7840
7841 // Handle formatting requests to the language server.
7842 cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
7843 let buffer_changes = buffer_changes.clone();
7844 move |_, _| {
7845 // When formatting is requested, trailing whitespace has already been stripped,
7846 // and the trailing newline has already been added.
7847 assert_eq!(
7848 &buffer_changes.lock()[1..],
7849 &[
7850 (
7851 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
7852 "".into()
7853 ),
7854 (
7855 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
7856 "".into()
7857 ),
7858 (
7859 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
7860 "\n".into()
7861 ),
7862 ]
7863 );
7864
7865 // Insert blank lines between each line of the buffer.
7866 async move {
7867 Ok(Some(vec![
7868 lsp::TextEdit {
7869 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
7870 new_text: "\n".into(),
7871 },
7872 lsp::TextEdit {
7873 range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
7874 new_text: "\n".into(),
7875 },
7876 ]))
7877 }
7878 }
7879 });
7880
7881 // After formatting the buffer, the trailing whitespace is stripped,
7882 // a newline is appended, and the edits provided by the language server
7883 // have been applied.
7884 format.await.unwrap();
7885 cx.assert_editor_state(
7886 &[
7887 "one", //
7888 "", //
7889 "twoˇ", //
7890 "", //
7891 "three", //
7892 "four", //
7893 "", //
7894 ]
7895 .join("\n"),
7896 );
7897
7898 // Undoing the formatting undoes the trailing whitespace removal, the
7899 // trailing newline, and the LSP edits.
7900 cx.update_buffer(|buffer, cx| buffer.undo(cx));
7901 cx.assert_editor_state(
7902 &[
7903 "one ", //
7904 "twoˇ", //
7905 "three ", //
7906 "four", //
7907 ]
7908 .join("\n"),
7909 );
7910}
7911
7912#[gpui::test]
7913async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
7914 cx: &mut gpui::TestAppContext,
7915) {
7916 init_test(cx, |_| {});
7917
7918 cx.update(|cx| {
7919 cx.update_global::<SettingsStore, _>(|settings, cx| {
7920 settings.update_user_settings::<EditorSettings>(cx, |settings| {
7921 settings.auto_signature_help = Some(true);
7922 });
7923 });
7924 });
7925
7926 let mut cx = EditorLspTestContext::new_rust(
7927 lsp::ServerCapabilities {
7928 signature_help_provider: Some(lsp::SignatureHelpOptions {
7929 ..Default::default()
7930 }),
7931 ..Default::default()
7932 },
7933 cx,
7934 )
7935 .await;
7936
7937 let language = Language::new(
7938 LanguageConfig {
7939 name: "Rust".into(),
7940 brackets: BracketPairConfig {
7941 pairs: vec![
7942 BracketPair {
7943 start: "{".to_string(),
7944 end: "}".to_string(),
7945 close: true,
7946 surround: true,
7947 newline: true,
7948 },
7949 BracketPair {
7950 start: "(".to_string(),
7951 end: ")".to_string(),
7952 close: true,
7953 surround: true,
7954 newline: true,
7955 },
7956 BracketPair {
7957 start: "/*".to_string(),
7958 end: " */".to_string(),
7959 close: true,
7960 surround: true,
7961 newline: true,
7962 },
7963 BracketPair {
7964 start: "[".to_string(),
7965 end: "]".to_string(),
7966 close: false,
7967 surround: false,
7968 newline: true,
7969 },
7970 BracketPair {
7971 start: "\"".to_string(),
7972 end: "\"".to_string(),
7973 close: true,
7974 surround: true,
7975 newline: false,
7976 },
7977 BracketPair {
7978 start: "<".to_string(),
7979 end: ">".to_string(),
7980 close: false,
7981 surround: true,
7982 newline: true,
7983 },
7984 ],
7985 ..Default::default()
7986 },
7987 autoclose_before: "})]".to_string(),
7988 ..Default::default()
7989 },
7990 Some(tree_sitter_rust::LANGUAGE.into()),
7991 );
7992 let language = Arc::new(language);
7993
7994 cx.language_registry().add(language.clone());
7995 cx.update_buffer(|buffer, cx| {
7996 buffer.set_language(Some(language), cx);
7997 });
7998
7999 cx.set_state(
8000 &r#"
8001 fn main() {
8002 sampleˇ
8003 }
8004 "#
8005 .unindent(),
8006 );
8007
8008 cx.update_editor(|editor, window, cx| {
8009 editor.handle_input("(", window, cx);
8010 });
8011 cx.assert_editor_state(
8012 &"
8013 fn main() {
8014 sample(ˇ)
8015 }
8016 "
8017 .unindent(),
8018 );
8019
8020 let mocked_response = lsp::SignatureHelp {
8021 signatures: vec![lsp::SignatureInformation {
8022 label: "fn sample(param1: u8, param2: u8)".to_string(),
8023 documentation: None,
8024 parameters: Some(vec![
8025 lsp::ParameterInformation {
8026 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8027 documentation: None,
8028 },
8029 lsp::ParameterInformation {
8030 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8031 documentation: None,
8032 },
8033 ]),
8034 active_parameter: None,
8035 }],
8036 active_signature: Some(0),
8037 active_parameter: Some(0),
8038 };
8039 handle_signature_help_request(&mut cx, mocked_response).await;
8040
8041 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8042 .await;
8043
8044 cx.editor(|editor, _, _| {
8045 let signature_help_state = editor.signature_help_state.popover().cloned();
8046 assert!(signature_help_state.is_some());
8047 let ParsedMarkdown {
8048 text, highlights, ..
8049 } = signature_help_state.unwrap().parsed_content;
8050 assert_eq!(text, "param1: u8, param2: u8");
8051 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8052 });
8053}
8054
8055#[gpui::test]
8056async fn test_handle_input_with_different_show_signature_settings(cx: &mut gpui::TestAppContext) {
8057 init_test(cx, |_| {});
8058
8059 cx.update(|cx| {
8060 cx.update_global::<SettingsStore, _>(|settings, cx| {
8061 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8062 settings.auto_signature_help = Some(false);
8063 settings.show_signature_help_after_edits = Some(false);
8064 });
8065 });
8066 });
8067
8068 let mut cx = EditorLspTestContext::new_rust(
8069 lsp::ServerCapabilities {
8070 signature_help_provider: Some(lsp::SignatureHelpOptions {
8071 ..Default::default()
8072 }),
8073 ..Default::default()
8074 },
8075 cx,
8076 )
8077 .await;
8078
8079 let language = Language::new(
8080 LanguageConfig {
8081 name: "Rust".into(),
8082 brackets: BracketPairConfig {
8083 pairs: vec![
8084 BracketPair {
8085 start: "{".to_string(),
8086 end: "}".to_string(),
8087 close: true,
8088 surround: true,
8089 newline: true,
8090 },
8091 BracketPair {
8092 start: "(".to_string(),
8093 end: ")".to_string(),
8094 close: true,
8095 surround: true,
8096 newline: true,
8097 },
8098 BracketPair {
8099 start: "/*".to_string(),
8100 end: " */".to_string(),
8101 close: true,
8102 surround: true,
8103 newline: true,
8104 },
8105 BracketPair {
8106 start: "[".to_string(),
8107 end: "]".to_string(),
8108 close: false,
8109 surround: false,
8110 newline: true,
8111 },
8112 BracketPair {
8113 start: "\"".to_string(),
8114 end: "\"".to_string(),
8115 close: true,
8116 surround: true,
8117 newline: false,
8118 },
8119 BracketPair {
8120 start: "<".to_string(),
8121 end: ">".to_string(),
8122 close: false,
8123 surround: true,
8124 newline: true,
8125 },
8126 ],
8127 ..Default::default()
8128 },
8129 autoclose_before: "})]".to_string(),
8130 ..Default::default()
8131 },
8132 Some(tree_sitter_rust::LANGUAGE.into()),
8133 );
8134 let language = Arc::new(language);
8135
8136 cx.language_registry().add(language.clone());
8137 cx.update_buffer(|buffer, cx| {
8138 buffer.set_language(Some(language), cx);
8139 });
8140
8141 // Ensure that signature_help is not called when no signature help is enabled.
8142 cx.set_state(
8143 &r#"
8144 fn main() {
8145 sampleˇ
8146 }
8147 "#
8148 .unindent(),
8149 );
8150 cx.update_editor(|editor, window, cx| {
8151 editor.handle_input("(", window, cx);
8152 });
8153 cx.assert_editor_state(
8154 &"
8155 fn main() {
8156 sample(ˇ)
8157 }
8158 "
8159 .unindent(),
8160 );
8161 cx.editor(|editor, _, _| {
8162 assert!(editor.signature_help_state.task().is_none());
8163 });
8164
8165 let mocked_response = lsp::SignatureHelp {
8166 signatures: vec![lsp::SignatureInformation {
8167 label: "fn sample(param1: u8, param2: u8)".to_string(),
8168 documentation: None,
8169 parameters: Some(vec![
8170 lsp::ParameterInformation {
8171 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8172 documentation: None,
8173 },
8174 lsp::ParameterInformation {
8175 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8176 documentation: None,
8177 },
8178 ]),
8179 active_parameter: None,
8180 }],
8181 active_signature: Some(0),
8182 active_parameter: Some(0),
8183 };
8184
8185 // Ensure that signature_help is called when enabled afte edits
8186 cx.update(|_, cx| {
8187 cx.update_global::<SettingsStore, _>(|settings, cx| {
8188 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8189 settings.auto_signature_help = Some(false);
8190 settings.show_signature_help_after_edits = Some(true);
8191 });
8192 });
8193 });
8194 cx.set_state(
8195 &r#"
8196 fn main() {
8197 sampleˇ
8198 }
8199 "#
8200 .unindent(),
8201 );
8202 cx.update_editor(|editor, window, cx| {
8203 editor.handle_input("(", window, cx);
8204 });
8205 cx.assert_editor_state(
8206 &"
8207 fn main() {
8208 sample(ˇ)
8209 }
8210 "
8211 .unindent(),
8212 );
8213 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8214 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8215 .await;
8216 cx.update_editor(|editor, _, _| {
8217 let signature_help_state = editor.signature_help_state.popover().cloned();
8218 assert!(signature_help_state.is_some());
8219 let ParsedMarkdown {
8220 text, highlights, ..
8221 } = signature_help_state.unwrap().parsed_content;
8222 assert_eq!(text, "param1: u8, param2: u8");
8223 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8224 editor.signature_help_state = SignatureHelpState::default();
8225 });
8226
8227 // Ensure that signature_help is called when auto signature help override is enabled
8228 cx.update(|_, cx| {
8229 cx.update_global::<SettingsStore, _>(|settings, cx| {
8230 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8231 settings.auto_signature_help = Some(true);
8232 settings.show_signature_help_after_edits = Some(false);
8233 });
8234 });
8235 });
8236 cx.set_state(
8237 &r#"
8238 fn main() {
8239 sampleˇ
8240 }
8241 "#
8242 .unindent(),
8243 );
8244 cx.update_editor(|editor, window, cx| {
8245 editor.handle_input("(", window, cx);
8246 });
8247 cx.assert_editor_state(
8248 &"
8249 fn main() {
8250 sample(ˇ)
8251 }
8252 "
8253 .unindent(),
8254 );
8255 handle_signature_help_request(&mut cx, mocked_response).await;
8256 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8257 .await;
8258 cx.editor(|editor, _, _| {
8259 let signature_help_state = editor.signature_help_state.popover().cloned();
8260 assert!(signature_help_state.is_some());
8261 let ParsedMarkdown {
8262 text, highlights, ..
8263 } = signature_help_state.unwrap().parsed_content;
8264 assert_eq!(text, "param1: u8, param2: u8");
8265 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8266 });
8267}
8268
8269#[gpui::test]
8270async fn test_signature_help(cx: &mut gpui::TestAppContext) {
8271 init_test(cx, |_| {});
8272 cx.update(|cx| {
8273 cx.update_global::<SettingsStore, _>(|settings, cx| {
8274 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8275 settings.auto_signature_help = Some(true);
8276 });
8277 });
8278 });
8279
8280 let mut cx = EditorLspTestContext::new_rust(
8281 lsp::ServerCapabilities {
8282 signature_help_provider: Some(lsp::SignatureHelpOptions {
8283 ..Default::default()
8284 }),
8285 ..Default::default()
8286 },
8287 cx,
8288 )
8289 .await;
8290
8291 // A test that directly calls `show_signature_help`
8292 cx.update_editor(|editor, window, cx| {
8293 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8294 });
8295
8296 let mocked_response = lsp::SignatureHelp {
8297 signatures: vec![lsp::SignatureInformation {
8298 label: "fn sample(param1: u8, param2: u8)".to_string(),
8299 documentation: None,
8300 parameters: Some(vec![
8301 lsp::ParameterInformation {
8302 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8303 documentation: None,
8304 },
8305 lsp::ParameterInformation {
8306 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8307 documentation: None,
8308 },
8309 ]),
8310 active_parameter: None,
8311 }],
8312 active_signature: Some(0),
8313 active_parameter: Some(0),
8314 };
8315 handle_signature_help_request(&mut cx, mocked_response).await;
8316
8317 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8318 .await;
8319
8320 cx.editor(|editor, _, _| {
8321 let signature_help_state = editor.signature_help_state.popover().cloned();
8322 assert!(signature_help_state.is_some());
8323 let ParsedMarkdown {
8324 text, highlights, ..
8325 } = signature_help_state.unwrap().parsed_content;
8326 assert_eq!(text, "param1: u8, param2: u8");
8327 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8328 });
8329
8330 // When exiting outside from inside the brackets, `signature_help` is closed.
8331 cx.set_state(indoc! {"
8332 fn main() {
8333 sample(ˇ);
8334 }
8335
8336 fn sample(param1: u8, param2: u8) {}
8337 "});
8338
8339 cx.update_editor(|editor, window, cx| {
8340 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
8341 });
8342
8343 let mocked_response = lsp::SignatureHelp {
8344 signatures: Vec::new(),
8345 active_signature: None,
8346 active_parameter: None,
8347 };
8348 handle_signature_help_request(&mut cx, mocked_response).await;
8349
8350 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
8351 .await;
8352
8353 cx.editor(|editor, _, _| {
8354 assert!(!editor.signature_help_state.is_shown());
8355 });
8356
8357 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
8358 cx.set_state(indoc! {"
8359 fn main() {
8360 sample(ˇ);
8361 }
8362
8363 fn sample(param1: u8, param2: u8) {}
8364 "});
8365
8366 let mocked_response = lsp::SignatureHelp {
8367 signatures: vec![lsp::SignatureInformation {
8368 label: "fn sample(param1: u8, param2: u8)".to_string(),
8369 documentation: None,
8370 parameters: Some(vec![
8371 lsp::ParameterInformation {
8372 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8373 documentation: None,
8374 },
8375 lsp::ParameterInformation {
8376 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8377 documentation: None,
8378 },
8379 ]),
8380 active_parameter: None,
8381 }],
8382 active_signature: Some(0),
8383 active_parameter: Some(0),
8384 };
8385 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8386 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8387 .await;
8388 cx.editor(|editor, _, _| {
8389 assert!(editor.signature_help_state.is_shown());
8390 });
8391
8392 // Restore the popover with more parameter input
8393 cx.set_state(indoc! {"
8394 fn main() {
8395 sample(param1, param2ˇ);
8396 }
8397
8398 fn sample(param1: u8, param2: u8) {}
8399 "});
8400
8401 let mocked_response = lsp::SignatureHelp {
8402 signatures: vec![lsp::SignatureInformation {
8403 label: "fn sample(param1: u8, param2: u8)".to_string(),
8404 documentation: None,
8405 parameters: Some(vec![
8406 lsp::ParameterInformation {
8407 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8408 documentation: None,
8409 },
8410 lsp::ParameterInformation {
8411 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8412 documentation: None,
8413 },
8414 ]),
8415 active_parameter: None,
8416 }],
8417 active_signature: Some(0),
8418 active_parameter: Some(1),
8419 };
8420 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8421 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8422 .await;
8423
8424 // When selecting a range, the popover is gone.
8425 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
8426 cx.update_editor(|editor, window, cx| {
8427 editor.change_selections(None, window, cx, |s| {
8428 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
8429 })
8430 });
8431 cx.assert_editor_state(indoc! {"
8432 fn main() {
8433 sample(param1, «ˇparam2»);
8434 }
8435
8436 fn sample(param1: u8, param2: u8) {}
8437 "});
8438 cx.editor(|editor, _, _| {
8439 assert!(!editor.signature_help_state.is_shown());
8440 });
8441
8442 // When unselecting again, the popover is back if within the brackets.
8443 cx.update_editor(|editor, window, cx| {
8444 editor.change_selections(None, window, cx, |s| {
8445 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8446 })
8447 });
8448 cx.assert_editor_state(indoc! {"
8449 fn main() {
8450 sample(param1, ˇparam2);
8451 }
8452
8453 fn sample(param1: u8, param2: u8) {}
8454 "});
8455 handle_signature_help_request(&mut cx, mocked_response).await;
8456 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8457 .await;
8458 cx.editor(|editor, _, _| {
8459 assert!(editor.signature_help_state.is_shown());
8460 });
8461
8462 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
8463 cx.update_editor(|editor, window, cx| {
8464 editor.change_selections(None, window, cx, |s| {
8465 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
8466 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8467 })
8468 });
8469 cx.assert_editor_state(indoc! {"
8470 fn main() {
8471 sample(param1, ˇparam2);
8472 }
8473
8474 fn sample(param1: u8, param2: u8) {}
8475 "});
8476
8477 let mocked_response = lsp::SignatureHelp {
8478 signatures: vec![lsp::SignatureInformation {
8479 label: "fn sample(param1: u8, param2: u8)".to_string(),
8480 documentation: None,
8481 parameters: Some(vec![
8482 lsp::ParameterInformation {
8483 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8484 documentation: None,
8485 },
8486 lsp::ParameterInformation {
8487 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8488 documentation: None,
8489 },
8490 ]),
8491 active_parameter: None,
8492 }],
8493 active_signature: Some(0),
8494 active_parameter: Some(1),
8495 };
8496 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8497 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8498 .await;
8499 cx.update_editor(|editor, _, cx| {
8500 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
8501 });
8502 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
8503 .await;
8504 cx.update_editor(|editor, window, cx| {
8505 editor.change_selections(None, window, cx, |s| {
8506 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
8507 })
8508 });
8509 cx.assert_editor_state(indoc! {"
8510 fn main() {
8511 sample(param1, «ˇparam2»);
8512 }
8513
8514 fn sample(param1: u8, param2: u8) {}
8515 "});
8516 cx.update_editor(|editor, window, cx| {
8517 editor.change_selections(None, window, cx, |s| {
8518 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8519 })
8520 });
8521 cx.assert_editor_state(indoc! {"
8522 fn main() {
8523 sample(param1, ˇparam2);
8524 }
8525
8526 fn sample(param1: u8, param2: u8) {}
8527 "});
8528 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
8529 .await;
8530}
8531
8532#[gpui::test]
8533async fn test_completion(cx: &mut gpui::TestAppContext) {
8534 init_test(cx, |_| {});
8535
8536 let mut cx = EditorLspTestContext::new_rust(
8537 lsp::ServerCapabilities {
8538 completion_provider: Some(lsp::CompletionOptions {
8539 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
8540 resolve_provider: Some(true),
8541 ..Default::default()
8542 }),
8543 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
8544 ..Default::default()
8545 },
8546 cx,
8547 )
8548 .await;
8549 let counter = Arc::new(AtomicUsize::new(0));
8550
8551 cx.set_state(indoc! {"
8552 oneˇ
8553 two
8554 three
8555 "});
8556 cx.simulate_keystroke(".");
8557 handle_completion_request(
8558 &mut cx,
8559 indoc! {"
8560 one.|<>
8561 two
8562 three
8563 "},
8564 vec!["first_completion", "second_completion"],
8565 counter.clone(),
8566 )
8567 .await;
8568 cx.condition(|editor, _| editor.context_menu_visible())
8569 .await;
8570 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
8571
8572 let _handler = handle_signature_help_request(
8573 &mut cx,
8574 lsp::SignatureHelp {
8575 signatures: vec![lsp::SignatureInformation {
8576 label: "test signature".to_string(),
8577 documentation: None,
8578 parameters: Some(vec![lsp::ParameterInformation {
8579 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
8580 documentation: None,
8581 }]),
8582 active_parameter: None,
8583 }],
8584 active_signature: None,
8585 active_parameter: None,
8586 },
8587 );
8588 cx.update_editor(|editor, window, cx| {
8589 assert!(
8590 !editor.signature_help_state.is_shown(),
8591 "No signature help was called for"
8592 );
8593 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8594 });
8595 cx.run_until_parked();
8596 cx.update_editor(|editor, _, _| {
8597 assert!(
8598 !editor.signature_help_state.is_shown(),
8599 "No signature help should be shown when completions menu is open"
8600 );
8601 });
8602
8603 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8604 editor.context_menu_next(&Default::default(), window, cx);
8605 editor
8606 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8607 .unwrap()
8608 });
8609 cx.assert_editor_state(indoc! {"
8610 one.second_completionˇ
8611 two
8612 three
8613 "});
8614
8615 handle_resolve_completion_request(
8616 &mut cx,
8617 Some(vec![
8618 (
8619 //This overlaps with the primary completion edit which is
8620 //misbehavior from the LSP spec, test that we filter it out
8621 indoc! {"
8622 one.second_ˇcompletion
8623 two
8624 threeˇ
8625 "},
8626 "overlapping additional edit",
8627 ),
8628 (
8629 indoc! {"
8630 one.second_completion
8631 two
8632 threeˇ
8633 "},
8634 "\nadditional edit",
8635 ),
8636 ]),
8637 )
8638 .await;
8639 apply_additional_edits.await.unwrap();
8640 cx.assert_editor_state(indoc! {"
8641 one.second_completionˇ
8642 two
8643 three
8644 additional edit
8645 "});
8646
8647 cx.set_state(indoc! {"
8648 one.second_completion
8649 twoˇ
8650 threeˇ
8651 additional edit
8652 "});
8653 cx.simulate_keystroke(" ");
8654 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8655 cx.simulate_keystroke("s");
8656 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8657
8658 cx.assert_editor_state(indoc! {"
8659 one.second_completion
8660 two sˇ
8661 three sˇ
8662 additional edit
8663 "});
8664 handle_completion_request(
8665 &mut cx,
8666 indoc! {"
8667 one.second_completion
8668 two s
8669 three <s|>
8670 additional edit
8671 "},
8672 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
8673 counter.clone(),
8674 )
8675 .await;
8676 cx.condition(|editor, _| editor.context_menu_visible())
8677 .await;
8678 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
8679
8680 cx.simulate_keystroke("i");
8681
8682 handle_completion_request(
8683 &mut cx,
8684 indoc! {"
8685 one.second_completion
8686 two si
8687 three <si|>
8688 additional edit
8689 "},
8690 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
8691 counter.clone(),
8692 )
8693 .await;
8694 cx.condition(|editor, _| editor.context_menu_visible())
8695 .await;
8696 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
8697
8698 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8699 editor
8700 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8701 .unwrap()
8702 });
8703 cx.assert_editor_state(indoc! {"
8704 one.second_completion
8705 two sixth_completionˇ
8706 three sixth_completionˇ
8707 additional edit
8708 "});
8709
8710 apply_additional_edits.await.unwrap();
8711
8712 update_test_language_settings(&mut cx, |settings| {
8713 settings.defaults.show_completions_on_input = Some(false);
8714 });
8715 cx.set_state("editorˇ");
8716 cx.simulate_keystroke(".");
8717 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8718 cx.simulate_keystroke("c");
8719 cx.simulate_keystroke("l");
8720 cx.simulate_keystroke("o");
8721 cx.assert_editor_state("editor.cloˇ");
8722 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8723 cx.update_editor(|editor, window, cx| {
8724 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
8725 });
8726 handle_completion_request(
8727 &mut cx,
8728 "editor.<clo|>",
8729 vec!["close", "clobber"],
8730 counter.clone(),
8731 )
8732 .await;
8733 cx.condition(|editor, _| editor.context_menu_visible())
8734 .await;
8735 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
8736
8737 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8738 editor
8739 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8740 .unwrap()
8741 });
8742 cx.assert_editor_state("editor.closeˇ");
8743 handle_resolve_completion_request(&mut cx, None).await;
8744 apply_additional_edits.await.unwrap();
8745}
8746
8747#[gpui::test]
8748async fn test_multiline_completion(cx: &mut gpui::TestAppContext) {
8749 init_test(cx, |_| {});
8750
8751 let fs = FakeFs::new(cx.executor());
8752 fs.insert_tree(
8753 path!("/a"),
8754 json!({
8755 "main.ts": "a",
8756 }),
8757 )
8758 .await;
8759
8760 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8761 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8762 let typescript_language = Arc::new(Language::new(
8763 LanguageConfig {
8764 name: "TypeScript".into(),
8765 matcher: LanguageMatcher {
8766 path_suffixes: vec!["ts".to_string()],
8767 ..LanguageMatcher::default()
8768 },
8769 line_comments: vec!["// ".into()],
8770 ..LanguageConfig::default()
8771 },
8772 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8773 ));
8774 language_registry.add(typescript_language.clone());
8775 let mut fake_servers = language_registry.register_fake_lsp(
8776 "TypeScript",
8777 FakeLspAdapter {
8778 capabilities: lsp::ServerCapabilities {
8779 completion_provider: Some(lsp::CompletionOptions {
8780 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
8781 ..lsp::CompletionOptions::default()
8782 }),
8783 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
8784 ..lsp::ServerCapabilities::default()
8785 },
8786 // Emulate vtsls label generation
8787 label_for_completion: Some(Box::new(|item, _| {
8788 let text = if let Some(description) = item
8789 .label_details
8790 .as_ref()
8791 .and_then(|label_details| label_details.description.as_ref())
8792 {
8793 format!("{} {}", item.label, description)
8794 } else if let Some(detail) = &item.detail {
8795 format!("{} {}", item.label, detail)
8796 } else {
8797 item.label.clone()
8798 };
8799 let len = text.len();
8800 Some(language::CodeLabel {
8801 text,
8802 runs: Vec::new(),
8803 filter_range: 0..len,
8804 })
8805 })),
8806 ..FakeLspAdapter::default()
8807 },
8808 );
8809 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8810 let cx = &mut VisualTestContext::from_window(*workspace, cx);
8811 let worktree_id = workspace
8812 .update(cx, |workspace, _window, cx| {
8813 workspace.project().update(cx, |project, cx| {
8814 project.worktrees(cx).next().unwrap().read(cx).id()
8815 })
8816 })
8817 .unwrap();
8818 let _buffer = project
8819 .update(cx, |project, cx| {
8820 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
8821 })
8822 .await
8823 .unwrap();
8824 let editor = workspace
8825 .update(cx, |workspace, window, cx| {
8826 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
8827 })
8828 .unwrap()
8829 .await
8830 .unwrap()
8831 .downcast::<Editor>()
8832 .unwrap();
8833 let fake_server = fake_servers.next().await.unwrap();
8834
8835 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
8836 let multiline_label_2 = "a\nb\nc\n";
8837 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
8838 let multiline_description = "d\ne\nf\n";
8839 let multiline_detail_2 = "g\nh\ni\n";
8840
8841 let mut completion_handle =
8842 fake_server.handle_request::<lsp::request::Completion, _, _>(move |params, _| async move {
8843 Ok(Some(lsp::CompletionResponse::Array(vec![
8844 lsp::CompletionItem {
8845 label: multiline_label.to_string(),
8846 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8847 range: lsp::Range {
8848 start: lsp::Position {
8849 line: params.text_document_position.position.line,
8850 character: params.text_document_position.position.character,
8851 },
8852 end: lsp::Position {
8853 line: params.text_document_position.position.line,
8854 character: params.text_document_position.position.character,
8855 },
8856 },
8857 new_text: "new_text_1".to_string(),
8858 })),
8859 ..lsp::CompletionItem::default()
8860 },
8861 lsp::CompletionItem {
8862 label: "single line label 1".to_string(),
8863 detail: Some(multiline_detail.to_string()),
8864 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8865 range: lsp::Range {
8866 start: lsp::Position {
8867 line: params.text_document_position.position.line,
8868 character: params.text_document_position.position.character,
8869 },
8870 end: lsp::Position {
8871 line: params.text_document_position.position.line,
8872 character: params.text_document_position.position.character,
8873 },
8874 },
8875 new_text: "new_text_2".to_string(),
8876 })),
8877 ..lsp::CompletionItem::default()
8878 },
8879 lsp::CompletionItem {
8880 label: "single line label 2".to_string(),
8881 label_details: Some(lsp::CompletionItemLabelDetails {
8882 description: Some(multiline_description.to_string()),
8883 detail: None,
8884 }),
8885 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8886 range: lsp::Range {
8887 start: lsp::Position {
8888 line: params.text_document_position.position.line,
8889 character: params.text_document_position.position.character,
8890 },
8891 end: lsp::Position {
8892 line: params.text_document_position.position.line,
8893 character: params.text_document_position.position.character,
8894 },
8895 },
8896 new_text: "new_text_2".to_string(),
8897 })),
8898 ..lsp::CompletionItem::default()
8899 },
8900 lsp::CompletionItem {
8901 label: multiline_label_2.to_string(),
8902 detail: Some(multiline_detail_2.to_string()),
8903 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8904 range: lsp::Range {
8905 start: lsp::Position {
8906 line: params.text_document_position.position.line,
8907 character: params.text_document_position.position.character,
8908 },
8909 end: lsp::Position {
8910 line: params.text_document_position.position.line,
8911 character: params.text_document_position.position.character,
8912 },
8913 },
8914 new_text: "new_text_3".to_string(),
8915 })),
8916 ..lsp::CompletionItem::default()
8917 },
8918 lsp::CompletionItem {
8919 label: "Label with many spaces and \t but without newlines".to_string(),
8920 detail: Some(
8921 "Details with many spaces and \t but without newlines".to_string(),
8922 ),
8923 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8924 range: lsp::Range {
8925 start: lsp::Position {
8926 line: params.text_document_position.position.line,
8927 character: params.text_document_position.position.character,
8928 },
8929 end: lsp::Position {
8930 line: params.text_document_position.position.line,
8931 character: params.text_document_position.position.character,
8932 },
8933 },
8934 new_text: "new_text_4".to_string(),
8935 })),
8936 ..lsp::CompletionItem::default()
8937 },
8938 ])))
8939 });
8940
8941 editor.update_in(cx, |editor, window, cx| {
8942 cx.focus_self(window);
8943 editor.move_to_end(&MoveToEnd, window, cx);
8944 editor.handle_input(".", window, cx);
8945 });
8946 cx.run_until_parked();
8947 completion_handle.next().await.unwrap();
8948
8949 editor.update(cx, |editor, _| {
8950 assert!(editor.context_menu_visible());
8951 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
8952 {
8953 let completion_labels = menu
8954 .completions
8955 .borrow()
8956 .iter()
8957 .map(|c| c.label.text.clone())
8958 .collect::<Vec<_>>();
8959 assert_eq!(
8960 completion_labels,
8961 &[
8962 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
8963 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
8964 "single line label 2 d e f ",
8965 "a b c g h i ",
8966 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
8967 ],
8968 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
8969 );
8970
8971 for completion in menu
8972 .completions
8973 .borrow()
8974 .iter() {
8975 assert_eq!(
8976 completion.label.filter_range,
8977 0..completion.label.text.len(),
8978 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
8979 );
8980 }
8981
8982 } else {
8983 panic!("expected completion menu to be open");
8984 }
8985 });
8986}
8987
8988#[gpui::test]
8989async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
8990 init_test(cx, |_| {});
8991 let mut cx = EditorLspTestContext::new_rust(
8992 lsp::ServerCapabilities {
8993 completion_provider: Some(lsp::CompletionOptions {
8994 trigger_characters: Some(vec![".".to_string()]),
8995 ..Default::default()
8996 }),
8997 ..Default::default()
8998 },
8999 cx,
9000 )
9001 .await;
9002 cx.lsp
9003 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9004 Ok(Some(lsp::CompletionResponse::Array(vec![
9005 lsp::CompletionItem {
9006 label: "first".into(),
9007 ..Default::default()
9008 },
9009 lsp::CompletionItem {
9010 label: "last".into(),
9011 ..Default::default()
9012 },
9013 ])))
9014 });
9015 cx.set_state("variableˇ");
9016 cx.simulate_keystroke(".");
9017 cx.executor().run_until_parked();
9018
9019 cx.update_editor(|editor, _, _| {
9020 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9021 {
9022 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
9023 } else {
9024 panic!("expected completion menu to be open");
9025 }
9026 });
9027
9028 cx.update_editor(|editor, window, cx| {
9029 editor.move_page_down(&MovePageDown::default(), window, cx);
9030 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9031 {
9032 assert!(
9033 menu.selected_item == 1,
9034 "expected PageDown to select the last item from the context menu"
9035 );
9036 } else {
9037 panic!("expected completion menu to stay open after PageDown");
9038 }
9039 });
9040
9041 cx.update_editor(|editor, window, cx| {
9042 editor.move_page_up(&MovePageUp::default(), window, cx);
9043 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9044 {
9045 assert!(
9046 menu.selected_item == 0,
9047 "expected PageUp to select the first item from the context menu"
9048 );
9049 } else {
9050 panic!("expected completion menu to stay open after PageUp");
9051 }
9052 });
9053}
9054
9055#[gpui::test]
9056async fn test_completion_sort(cx: &mut gpui::TestAppContext) {
9057 init_test(cx, |_| {});
9058 let mut cx = EditorLspTestContext::new_rust(
9059 lsp::ServerCapabilities {
9060 completion_provider: Some(lsp::CompletionOptions {
9061 trigger_characters: Some(vec![".".to_string()]),
9062 ..Default::default()
9063 }),
9064 ..Default::default()
9065 },
9066 cx,
9067 )
9068 .await;
9069 cx.lsp
9070 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9071 Ok(Some(lsp::CompletionResponse::Array(vec![
9072 lsp::CompletionItem {
9073 label: "Range".into(),
9074 sort_text: Some("a".into()),
9075 ..Default::default()
9076 },
9077 lsp::CompletionItem {
9078 label: "r".into(),
9079 sort_text: Some("b".into()),
9080 ..Default::default()
9081 },
9082 lsp::CompletionItem {
9083 label: "ret".into(),
9084 sort_text: Some("c".into()),
9085 ..Default::default()
9086 },
9087 lsp::CompletionItem {
9088 label: "return".into(),
9089 sort_text: Some("d".into()),
9090 ..Default::default()
9091 },
9092 lsp::CompletionItem {
9093 label: "slice".into(),
9094 sort_text: Some("d".into()),
9095 ..Default::default()
9096 },
9097 ])))
9098 });
9099 cx.set_state("rˇ");
9100 cx.executor().run_until_parked();
9101 cx.update_editor(|editor, window, cx| {
9102 editor.show_completions(
9103 &ShowCompletions {
9104 trigger: Some("r".into()),
9105 },
9106 window,
9107 cx,
9108 );
9109 });
9110 cx.executor().run_until_parked();
9111
9112 cx.update_editor(|editor, _, _| {
9113 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9114 {
9115 assert_eq!(
9116 completion_menu_entries(&menu),
9117 &["r", "ret", "Range", "return"]
9118 );
9119 } else {
9120 panic!("expected completion menu to be open");
9121 }
9122 });
9123}
9124
9125#[gpui::test]
9126async fn test_no_duplicated_completion_requests(cx: &mut gpui::TestAppContext) {
9127 init_test(cx, |_| {});
9128
9129 let mut cx = EditorLspTestContext::new_rust(
9130 lsp::ServerCapabilities {
9131 completion_provider: Some(lsp::CompletionOptions {
9132 trigger_characters: Some(vec![".".to_string()]),
9133 resolve_provider: Some(true),
9134 ..Default::default()
9135 }),
9136 ..Default::default()
9137 },
9138 cx,
9139 )
9140 .await;
9141
9142 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
9143 cx.simulate_keystroke(".");
9144 let completion_item = lsp::CompletionItem {
9145 label: "Some".into(),
9146 kind: Some(lsp::CompletionItemKind::SNIPPET),
9147 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
9148 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
9149 kind: lsp::MarkupKind::Markdown,
9150 value: "```rust\nSome(2)\n```".to_string(),
9151 })),
9152 deprecated: Some(false),
9153 sort_text: Some("Some".to_string()),
9154 filter_text: Some("Some".to_string()),
9155 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
9156 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9157 range: lsp::Range {
9158 start: lsp::Position {
9159 line: 0,
9160 character: 22,
9161 },
9162 end: lsp::Position {
9163 line: 0,
9164 character: 22,
9165 },
9166 },
9167 new_text: "Some(2)".to_string(),
9168 })),
9169 additional_text_edits: Some(vec![lsp::TextEdit {
9170 range: lsp::Range {
9171 start: lsp::Position {
9172 line: 0,
9173 character: 20,
9174 },
9175 end: lsp::Position {
9176 line: 0,
9177 character: 22,
9178 },
9179 },
9180 new_text: "".to_string(),
9181 }]),
9182 ..Default::default()
9183 };
9184
9185 let closure_completion_item = completion_item.clone();
9186 let counter = Arc::new(AtomicUsize::new(0));
9187 let counter_clone = counter.clone();
9188 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
9189 let task_completion_item = closure_completion_item.clone();
9190 counter_clone.fetch_add(1, atomic::Ordering::Release);
9191 async move {
9192 Ok(Some(lsp::CompletionResponse::Array(vec![
9193 task_completion_item,
9194 ])))
9195 }
9196 });
9197
9198 cx.condition(|editor, _| editor.context_menu_visible())
9199 .await;
9200 cx.assert_editor_state(indoc! {"fn main() { let a = 2.ˇ; }"});
9201 assert!(request.next().await.is_some());
9202 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9203
9204 cx.simulate_keystroke("S");
9205 cx.simulate_keystroke("o");
9206 cx.simulate_keystroke("m");
9207 cx.condition(|editor, _| editor.context_menu_visible())
9208 .await;
9209 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Somˇ; }"});
9210 assert!(request.next().await.is_some());
9211 assert!(request.next().await.is_some());
9212 assert!(request.next().await.is_some());
9213 request.close();
9214 assert!(request.next().await.is_none());
9215 assert_eq!(
9216 counter.load(atomic::Ordering::Acquire),
9217 4,
9218 "With the completions menu open, only one LSP request should happen per input"
9219 );
9220}
9221
9222#[gpui::test]
9223async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
9224 init_test(cx, |_| {});
9225 let mut cx = EditorTestContext::new(cx).await;
9226 let language = Arc::new(Language::new(
9227 LanguageConfig {
9228 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
9229 ..Default::default()
9230 },
9231 Some(tree_sitter_rust::LANGUAGE.into()),
9232 ));
9233 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
9234
9235 // If multiple selections intersect a line, the line is only toggled once.
9236 cx.set_state(indoc! {"
9237 fn a() {
9238 «//b();
9239 ˇ»// «c();
9240 //ˇ» d();
9241 }
9242 "});
9243
9244 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9245
9246 cx.assert_editor_state(indoc! {"
9247 fn a() {
9248 «b();
9249 c();
9250 ˇ» d();
9251 }
9252 "});
9253
9254 // The comment prefix is inserted at the same column for every line in a
9255 // selection.
9256 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9257
9258 cx.assert_editor_state(indoc! {"
9259 fn a() {
9260 // «b();
9261 // c();
9262 ˇ»// d();
9263 }
9264 "});
9265
9266 // If a selection ends at the beginning of a line, that line is not toggled.
9267 cx.set_selections_state(indoc! {"
9268 fn a() {
9269 // b();
9270 «// c();
9271 ˇ» // d();
9272 }
9273 "});
9274
9275 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9276
9277 cx.assert_editor_state(indoc! {"
9278 fn a() {
9279 // b();
9280 «c();
9281 ˇ» // d();
9282 }
9283 "});
9284
9285 // If a selection span a single line and is empty, the line is toggled.
9286 cx.set_state(indoc! {"
9287 fn a() {
9288 a();
9289 b();
9290 ˇ
9291 }
9292 "});
9293
9294 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9295
9296 cx.assert_editor_state(indoc! {"
9297 fn a() {
9298 a();
9299 b();
9300 //•ˇ
9301 }
9302 "});
9303
9304 // If a selection span multiple lines, empty lines are not toggled.
9305 cx.set_state(indoc! {"
9306 fn a() {
9307 «a();
9308
9309 c();ˇ»
9310 }
9311 "});
9312
9313 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9314
9315 cx.assert_editor_state(indoc! {"
9316 fn a() {
9317 // «a();
9318
9319 // c();ˇ»
9320 }
9321 "});
9322
9323 // If a selection includes multiple comment prefixes, all lines are uncommented.
9324 cx.set_state(indoc! {"
9325 fn a() {
9326 «// a();
9327 /// b();
9328 //! c();ˇ»
9329 }
9330 "});
9331
9332 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9333
9334 cx.assert_editor_state(indoc! {"
9335 fn a() {
9336 «a();
9337 b();
9338 c();ˇ»
9339 }
9340 "});
9341}
9342
9343#[gpui::test]
9344async fn test_toggle_comment_ignore_indent(cx: &mut gpui::TestAppContext) {
9345 init_test(cx, |_| {});
9346 let mut cx = EditorTestContext::new(cx).await;
9347 let language = Arc::new(Language::new(
9348 LanguageConfig {
9349 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
9350 ..Default::default()
9351 },
9352 Some(tree_sitter_rust::LANGUAGE.into()),
9353 ));
9354 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
9355
9356 let toggle_comments = &ToggleComments {
9357 advance_downwards: false,
9358 ignore_indent: true,
9359 };
9360
9361 // If multiple selections intersect a line, the line is only toggled once.
9362 cx.set_state(indoc! {"
9363 fn a() {
9364 // «b();
9365 // c();
9366 // ˇ» d();
9367 }
9368 "});
9369
9370 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9371
9372 cx.assert_editor_state(indoc! {"
9373 fn a() {
9374 «b();
9375 c();
9376 ˇ» d();
9377 }
9378 "});
9379
9380 // The comment prefix is inserted at the beginning of each line
9381 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9382
9383 cx.assert_editor_state(indoc! {"
9384 fn a() {
9385 // «b();
9386 // c();
9387 // ˇ» d();
9388 }
9389 "});
9390
9391 // If a selection ends at the beginning of a line, that line is not toggled.
9392 cx.set_selections_state(indoc! {"
9393 fn a() {
9394 // b();
9395 // «c();
9396 ˇ»// d();
9397 }
9398 "});
9399
9400 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9401
9402 cx.assert_editor_state(indoc! {"
9403 fn a() {
9404 // b();
9405 «c();
9406 ˇ»// d();
9407 }
9408 "});
9409
9410 // If a selection span a single line and is empty, the line is toggled.
9411 cx.set_state(indoc! {"
9412 fn a() {
9413 a();
9414 b();
9415 ˇ
9416 }
9417 "});
9418
9419 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9420
9421 cx.assert_editor_state(indoc! {"
9422 fn a() {
9423 a();
9424 b();
9425 //ˇ
9426 }
9427 "});
9428
9429 // If a selection span multiple lines, empty lines are not toggled.
9430 cx.set_state(indoc! {"
9431 fn a() {
9432 «a();
9433
9434 c();ˇ»
9435 }
9436 "});
9437
9438 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9439
9440 cx.assert_editor_state(indoc! {"
9441 fn a() {
9442 // «a();
9443
9444 // c();ˇ»
9445 }
9446 "});
9447
9448 // If a selection includes multiple comment prefixes, all lines are uncommented.
9449 cx.set_state(indoc! {"
9450 fn a() {
9451 // «a();
9452 /// b();
9453 //! c();ˇ»
9454 }
9455 "});
9456
9457 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9458
9459 cx.assert_editor_state(indoc! {"
9460 fn a() {
9461 «a();
9462 b();
9463 c();ˇ»
9464 }
9465 "});
9466}
9467
9468#[gpui::test]
9469async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
9470 init_test(cx, |_| {});
9471
9472 let language = Arc::new(Language::new(
9473 LanguageConfig {
9474 line_comments: vec!["// ".into()],
9475 ..Default::default()
9476 },
9477 Some(tree_sitter_rust::LANGUAGE.into()),
9478 ));
9479
9480 let mut cx = EditorTestContext::new(cx).await;
9481
9482 cx.language_registry().add(language.clone());
9483 cx.update_buffer(|buffer, cx| {
9484 buffer.set_language(Some(language), cx);
9485 });
9486
9487 let toggle_comments = &ToggleComments {
9488 advance_downwards: true,
9489 ignore_indent: false,
9490 };
9491
9492 // Single cursor on one line -> advance
9493 // Cursor moves horizontally 3 characters as well on non-blank line
9494 cx.set_state(indoc!(
9495 "fn a() {
9496 ˇdog();
9497 cat();
9498 }"
9499 ));
9500 cx.update_editor(|editor, window, cx| {
9501 editor.toggle_comments(toggle_comments, window, cx);
9502 });
9503 cx.assert_editor_state(indoc!(
9504 "fn a() {
9505 // dog();
9506 catˇ();
9507 }"
9508 ));
9509
9510 // Single selection on one line -> don't advance
9511 cx.set_state(indoc!(
9512 "fn a() {
9513 «dog()ˇ»;
9514 cat();
9515 }"
9516 ));
9517 cx.update_editor(|editor, window, cx| {
9518 editor.toggle_comments(toggle_comments, window, cx);
9519 });
9520 cx.assert_editor_state(indoc!(
9521 "fn a() {
9522 // «dog()ˇ»;
9523 cat();
9524 }"
9525 ));
9526
9527 // Multiple cursors on one line -> advance
9528 cx.set_state(indoc!(
9529 "fn a() {
9530 ˇdˇog();
9531 cat();
9532 }"
9533 ));
9534 cx.update_editor(|editor, window, cx| {
9535 editor.toggle_comments(toggle_comments, window, cx);
9536 });
9537 cx.assert_editor_state(indoc!(
9538 "fn a() {
9539 // dog();
9540 catˇ(ˇ);
9541 }"
9542 ));
9543
9544 // Multiple cursors on one line, with selection -> don't advance
9545 cx.set_state(indoc!(
9546 "fn a() {
9547 ˇdˇog«()ˇ»;
9548 cat();
9549 }"
9550 ));
9551 cx.update_editor(|editor, window, cx| {
9552 editor.toggle_comments(toggle_comments, window, cx);
9553 });
9554 cx.assert_editor_state(indoc!(
9555 "fn a() {
9556 // ˇdˇog«()ˇ»;
9557 cat();
9558 }"
9559 ));
9560
9561 // Single cursor on one line -> advance
9562 // Cursor moves to column 0 on blank line
9563 cx.set_state(indoc!(
9564 "fn a() {
9565 ˇdog();
9566
9567 cat();
9568 }"
9569 ));
9570 cx.update_editor(|editor, window, cx| {
9571 editor.toggle_comments(toggle_comments, window, cx);
9572 });
9573 cx.assert_editor_state(indoc!(
9574 "fn a() {
9575 // dog();
9576 ˇ
9577 cat();
9578 }"
9579 ));
9580
9581 // Single cursor on one line -> advance
9582 // Cursor starts and ends at column 0
9583 cx.set_state(indoc!(
9584 "fn a() {
9585 ˇ dog();
9586 cat();
9587 }"
9588 ));
9589 cx.update_editor(|editor, window, cx| {
9590 editor.toggle_comments(toggle_comments, window, cx);
9591 });
9592 cx.assert_editor_state(indoc!(
9593 "fn a() {
9594 // dog();
9595 ˇ cat();
9596 }"
9597 ));
9598}
9599
9600#[gpui::test]
9601async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
9602 init_test(cx, |_| {});
9603
9604 let mut cx = EditorTestContext::new(cx).await;
9605
9606 let html_language = Arc::new(
9607 Language::new(
9608 LanguageConfig {
9609 name: "HTML".into(),
9610 block_comment: Some(("<!-- ".into(), " -->".into())),
9611 ..Default::default()
9612 },
9613 Some(tree_sitter_html::language()),
9614 )
9615 .with_injection_query(
9616 r#"
9617 (script_element
9618 (raw_text) @injection.content
9619 (#set! injection.language "javascript"))
9620 "#,
9621 )
9622 .unwrap(),
9623 );
9624
9625 let javascript_language = Arc::new(Language::new(
9626 LanguageConfig {
9627 name: "JavaScript".into(),
9628 line_comments: vec!["// ".into()],
9629 ..Default::default()
9630 },
9631 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9632 ));
9633
9634 cx.language_registry().add(html_language.clone());
9635 cx.language_registry().add(javascript_language.clone());
9636 cx.update_buffer(|buffer, cx| {
9637 buffer.set_language(Some(html_language), cx);
9638 });
9639
9640 // Toggle comments for empty selections
9641 cx.set_state(
9642 &r#"
9643 <p>A</p>ˇ
9644 <p>B</p>ˇ
9645 <p>C</p>ˇ
9646 "#
9647 .unindent(),
9648 );
9649 cx.update_editor(|editor, window, cx| {
9650 editor.toggle_comments(&ToggleComments::default(), window, cx)
9651 });
9652 cx.assert_editor_state(
9653 &r#"
9654 <!-- <p>A</p>ˇ -->
9655 <!-- <p>B</p>ˇ -->
9656 <!-- <p>C</p>ˇ -->
9657 "#
9658 .unindent(),
9659 );
9660 cx.update_editor(|editor, window, cx| {
9661 editor.toggle_comments(&ToggleComments::default(), window, cx)
9662 });
9663 cx.assert_editor_state(
9664 &r#"
9665 <p>A</p>ˇ
9666 <p>B</p>ˇ
9667 <p>C</p>ˇ
9668 "#
9669 .unindent(),
9670 );
9671
9672 // Toggle comments for mixture of empty and non-empty selections, where
9673 // multiple selections occupy a given line.
9674 cx.set_state(
9675 &r#"
9676 <p>A«</p>
9677 <p>ˇ»B</p>ˇ
9678 <p>C«</p>
9679 <p>ˇ»D</p>ˇ
9680 "#
9681 .unindent(),
9682 );
9683
9684 cx.update_editor(|editor, window, cx| {
9685 editor.toggle_comments(&ToggleComments::default(), window, cx)
9686 });
9687 cx.assert_editor_state(
9688 &r#"
9689 <!-- <p>A«</p>
9690 <p>ˇ»B</p>ˇ -->
9691 <!-- <p>C«</p>
9692 <p>ˇ»D</p>ˇ -->
9693 "#
9694 .unindent(),
9695 );
9696 cx.update_editor(|editor, window, cx| {
9697 editor.toggle_comments(&ToggleComments::default(), window, cx)
9698 });
9699 cx.assert_editor_state(
9700 &r#"
9701 <p>A«</p>
9702 <p>ˇ»B</p>ˇ
9703 <p>C«</p>
9704 <p>ˇ»D</p>ˇ
9705 "#
9706 .unindent(),
9707 );
9708
9709 // Toggle comments when different languages are active for different
9710 // selections.
9711 cx.set_state(
9712 &r#"
9713 ˇ<script>
9714 ˇvar x = new Y();
9715 ˇ</script>
9716 "#
9717 .unindent(),
9718 );
9719 cx.executor().run_until_parked();
9720 cx.update_editor(|editor, window, cx| {
9721 editor.toggle_comments(&ToggleComments::default(), window, cx)
9722 });
9723 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
9724 // Uncommenting and commenting from this position brings in even more wrong artifacts.
9725 cx.assert_editor_state(
9726 &r#"
9727 <!-- ˇ<script> -->
9728 // ˇvar x = new Y();
9729 <!-- ˇ</script> -->
9730 "#
9731 .unindent(),
9732 );
9733}
9734
9735#[gpui::test]
9736fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
9737 init_test(cx, |_| {});
9738
9739 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9740 let multibuffer = cx.new(|cx| {
9741 let mut multibuffer = MultiBuffer::new(ReadWrite);
9742 multibuffer.push_excerpts(
9743 buffer.clone(),
9744 [
9745 ExcerptRange {
9746 context: Point::new(0, 0)..Point::new(0, 4),
9747 primary: None,
9748 },
9749 ExcerptRange {
9750 context: Point::new(1, 0)..Point::new(1, 4),
9751 primary: None,
9752 },
9753 ],
9754 cx,
9755 );
9756 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
9757 multibuffer
9758 });
9759
9760 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
9761 editor.update_in(cx, |editor, window, cx| {
9762 assert_eq!(editor.text(cx), "aaaa\nbbbb");
9763 editor.change_selections(None, window, cx, |s| {
9764 s.select_ranges([
9765 Point::new(0, 0)..Point::new(0, 0),
9766 Point::new(1, 0)..Point::new(1, 0),
9767 ])
9768 });
9769
9770 editor.handle_input("X", window, cx);
9771 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
9772 assert_eq!(
9773 editor.selections.ranges(cx),
9774 [
9775 Point::new(0, 1)..Point::new(0, 1),
9776 Point::new(1, 1)..Point::new(1, 1),
9777 ]
9778 );
9779
9780 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
9781 editor.change_selections(None, window, cx, |s| {
9782 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
9783 });
9784 editor.backspace(&Default::default(), window, cx);
9785 assert_eq!(editor.text(cx), "Xa\nbbb");
9786 assert_eq!(
9787 editor.selections.ranges(cx),
9788 [Point::new(1, 0)..Point::new(1, 0)]
9789 );
9790
9791 editor.change_selections(None, window, cx, |s| {
9792 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
9793 });
9794 editor.backspace(&Default::default(), window, cx);
9795 assert_eq!(editor.text(cx), "X\nbb");
9796 assert_eq!(
9797 editor.selections.ranges(cx),
9798 [Point::new(0, 1)..Point::new(0, 1)]
9799 );
9800 });
9801}
9802
9803#[gpui::test]
9804fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
9805 init_test(cx, |_| {});
9806
9807 let markers = vec![('[', ']').into(), ('(', ')').into()];
9808 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
9809 indoc! {"
9810 [aaaa
9811 (bbbb]
9812 cccc)",
9813 },
9814 markers.clone(),
9815 );
9816 let excerpt_ranges = markers.into_iter().map(|marker| {
9817 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
9818 ExcerptRange {
9819 context,
9820 primary: None,
9821 }
9822 });
9823 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
9824 let multibuffer = cx.new(|cx| {
9825 let mut multibuffer = MultiBuffer::new(ReadWrite);
9826 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
9827 multibuffer
9828 });
9829
9830 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
9831 editor.update_in(cx, |editor, window, cx| {
9832 let (expected_text, selection_ranges) = marked_text_ranges(
9833 indoc! {"
9834 aaaa
9835 bˇbbb
9836 bˇbbˇb
9837 cccc"
9838 },
9839 true,
9840 );
9841 assert_eq!(editor.text(cx), expected_text);
9842 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
9843
9844 editor.handle_input("X", window, cx);
9845
9846 let (expected_text, expected_selections) = marked_text_ranges(
9847 indoc! {"
9848 aaaa
9849 bXˇbbXb
9850 bXˇbbXˇb
9851 cccc"
9852 },
9853 false,
9854 );
9855 assert_eq!(editor.text(cx), expected_text);
9856 assert_eq!(editor.selections.ranges(cx), expected_selections);
9857
9858 editor.newline(&Newline, window, cx);
9859 let (expected_text, expected_selections) = marked_text_ranges(
9860 indoc! {"
9861 aaaa
9862 bX
9863 ˇbbX
9864 b
9865 bX
9866 ˇbbX
9867 ˇb
9868 cccc"
9869 },
9870 false,
9871 );
9872 assert_eq!(editor.text(cx), expected_text);
9873 assert_eq!(editor.selections.ranges(cx), expected_selections);
9874 });
9875}
9876
9877#[gpui::test]
9878fn test_refresh_selections(cx: &mut TestAppContext) {
9879 init_test(cx, |_| {});
9880
9881 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9882 let mut excerpt1_id = None;
9883 let multibuffer = cx.new(|cx| {
9884 let mut multibuffer = MultiBuffer::new(ReadWrite);
9885 excerpt1_id = multibuffer
9886 .push_excerpts(
9887 buffer.clone(),
9888 [
9889 ExcerptRange {
9890 context: Point::new(0, 0)..Point::new(1, 4),
9891 primary: None,
9892 },
9893 ExcerptRange {
9894 context: Point::new(1, 0)..Point::new(2, 4),
9895 primary: None,
9896 },
9897 ],
9898 cx,
9899 )
9900 .into_iter()
9901 .next();
9902 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
9903 multibuffer
9904 });
9905
9906 let editor = cx.add_window(|window, cx| {
9907 let mut editor = build_editor(multibuffer.clone(), window, cx);
9908 let snapshot = editor.snapshot(window, cx);
9909 editor.change_selections(None, window, cx, |s| {
9910 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
9911 });
9912 editor.begin_selection(
9913 Point::new(2, 1).to_display_point(&snapshot),
9914 true,
9915 1,
9916 window,
9917 cx,
9918 );
9919 assert_eq!(
9920 editor.selections.ranges(cx),
9921 [
9922 Point::new(1, 3)..Point::new(1, 3),
9923 Point::new(2, 1)..Point::new(2, 1),
9924 ]
9925 );
9926 editor
9927 });
9928
9929 // Refreshing selections is a no-op when excerpts haven't changed.
9930 _ = editor.update(cx, |editor, window, cx| {
9931 editor.change_selections(None, window, cx, |s| s.refresh());
9932 assert_eq!(
9933 editor.selections.ranges(cx),
9934 [
9935 Point::new(1, 3)..Point::new(1, 3),
9936 Point::new(2, 1)..Point::new(2, 1),
9937 ]
9938 );
9939 });
9940
9941 multibuffer.update(cx, |multibuffer, cx| {
9942 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
9943 });
9944 _ = editor.update(cx, |editor, window, cx| {
9945 // Removing an excerpt causes the first selection to become degenerate.
9946 assert_eq!(
9947 editor.selections.ranges(cx),
9948 [
9949 Point::new(0, 0)..Point::new(0, 0),
9950 Point::new(0, 1)..Point::new(0, 1)
9951 ]
9952 );
9953
9954 // Refreshing selections will relocate the first selection to the original buffer
9955 // location.
9956 editor.change_selections(None, window, cx, |s| s.refresh());
9957 assert_eq!(
9958 editor.selections.ranges(cx),
9959 [
9960 Point::new(0, 1)..Point::new(0, 1),
9961 Point::new(0, 3)..Point::new(0, 3)
9962 ]
9963 );
9964 assert!(editor.selections.pending_anchor().is_some());
9965 });
9966}
9967
9968#[gpui::test]
9969fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
9970 init_test(cx, |_| {});
9971
9972 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9973 let mut excerpt1_id = None;
9974 let multibuffer = cx.new(|cx| {
9975 let mut multibuffer = MultiBuffer::new(ReadWrite);
9976 excerpt1_id = multibuffer
9977 .push_excerpts(
9978 buffer.clone(),
9979 [
9980 ExcerptRange {
9981 context: Point::new(0, 0)..Point::new(1, 4),
9982 primary: None,
9983 },
9984 ExcerptRange {
9985 context: Point::new(1, 0)..Point::new(2, 4),
9986 primary: None,
9987 },
9988 ],
9989 cx,
9990 )
9991 .into_iter()
9992 .next();
9993 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
9994 multibuffer
9995 });
9996
9997 let editor = cx.add_window(|window, cx| {
9998 let mut editor = build_editor(multibuffer.clone(), window, cx);
9999 let snapshot = editor.snapshot(window, cx);
10000 editor.begin_selection(
10001 Point::new(1, 3).to_display_point(&snapshot),
10002 false,
10003 1,
10004 window,
10005 cx,
10006 );
10007 assert_eq!(
10008 editor.selections.ranges(cx),
10009 [Point::new(1, 3)..Point::new(1, 3)]
10010 );
10011 editor
10012 });
10013
10014 multibuffer.update(cx, |multibuffer, cx| {
10015 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
10016 });
10017 _ = editor.update(cx, |editor, window, cx| {
10018 assert_eq!(
10019 editor.selections.ranges(cx),
10020 [Point::new(0, 0)..Point::new(0, 0)]
10021 );
10022
10023 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
10024 editor.change_selections(None, window, cx, |s| s.refresh());
10025 assert_eq!(
10026 editor.selections.ranges(cx),
10027 [Point::new(0, 3)..Point::new(0, 3)]
10028 );
10029 assert!(editor.selections.pending_anchor().is_some());
10030 });
10031}
10032
10033#[gpui::test]
10034async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
10035 init_test(cx, |_| {});
10036
10037 let language = Arc::new(
10038 Language::new(
10039 LanguageConfig {
10040 brackets: BracketPairConfig {
10041 pairs: vec![
10042 BracketPair {
10043 start: "{".to_string(),
10044 end: "}".to_string(),
10045 close: true,
10046 surround: true,
10047 newline: true,
10048 },
10049 BracketPair {
10050 start: "/* ".to_string(),
10051 end: " */".to_string(),
10052 close: true,
10053 surround: true,
10054 newline: true,
10055 },
10056 ],
10057 ..Default::default()
10058 },
10059 ..Default::default()
10060 },
10061 Some(tree_sitter_rust::LANGUAGE.into()),
10062 )
10063 .with_indents_query("")
10064 .unwrap(),
10065 );
10066
10067 let text = concat!(
10068 "{ }\n", //
10069 " x\n", //
10070 " /* */\n", //
10071 "x\n", //
10072 "{{} }\n", //
10073 );
10074
10075 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10076 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10077 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10078 editor
10079 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10080 .await;
10081
10082 editor.update_in(cx, |editor, window, cx| {
10083 editor.change_selections(None, window, cx, |s| {
10084 s.select_display_ranges([
10085 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
10086 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
10087 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
10088 ])
10089 });
10090 editor.newline(&Newline, window, cx);
10091
10092 assert_eq!(
10093 editor.buffer().read(cx).read(cx).text(),
10094 concat!(
10095 "{ \n", // Suppress rustfmt
10096 "\n", //
10097 "}\n", //
10098 " x\n", //
10099 " /* \n", //
10100 " \n", //
10101 " */\n", //
10102 "x\n", //
10103 "{{} \n", //
10104 "}\n", //
10105 )
10106 );
10107 });
10108}
10109
10110#[gpui::test]
10111fn test_highlighted_ranges(cx: &mut TestAppContext) {
10112 init_test(cx, |_| {});
10113
10114 let editor = cx.add_window(|window, cx| {
10115 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
10116 build_editor(buffer.clone(), window, cx)
10117 });
10118
10119 _ = editor.update(cx, |editor, window, cx| {
10120 struct Type1;
10121 struct Type2;
10122
10123 let buffer = editor.buffer.read(cx).snapshot(cx);
10124
10125 let anchor_range =
10126 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
10127
10128 editor.highlight_background::<Type1>(
10129 &[
10130 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
10131 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
10132 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
10133 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
10134 ],
10135 |_| Hsla::red(),
10136 cx,
10137 );
10138 editor.highlight_background::<Type2>(
10139 &[
10140 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
10141 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
10142 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
10143 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
10144 ],
10145 |_| Hsla::green(),
10146 cx,
10147 );
10148
10149 let snapshot = editor.snapshot(window, cx);
10150 let mut highlighted_ranges = editor.background_highlights_in_range(
10151 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
10152 &snapshot,
10153 cx.theme().colors(),
10154 );
10155 // Enforce a consistent ordering based on color without relying on the ordering of the
10156 // highlight's `TypeId` which is non-executor.
10157 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
10158 assert_eq!(
10159 highlighted_ranges,
10160 &[
10161 (
10162 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
10163 Hsla::red(),
10164 ),
10165 (
10166 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
10167 Hsla::red(),
10168 ),
10169 (
10170 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
10171 Hsla::green(),
10172 ),
10173 (
10174 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
10175 Hsla::green(),
10176 ),
10177 ]
10178 );
10179 assert_eq!(
10180 editor.background_highlights_in_range(
10181 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
10182 &snapshot,
10183 cx.theme().colors(),
10184 ),
10185 &[(
10186 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
10187 Hsla::red(),
10188 )]
10189 );
10190 });
10191}
10192
10193#[gpui::test]
10194async fn test_following(cx: &mut gpui::TestAppContext) {
10195 init_test(cx, |_| {});
10196
10197 let fs = FakeFs::new(cx.executor());
10198 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
10199
10200 let buffer = project.update(cx, |project, cx| {
10201 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
10202 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
10203 });
10204 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
10205 let follower = cx.update(|cx| {
10206 cx.open_window(
10207 WindowOptions {
10208 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
10209 gpui::Point::new(px(0.), px(0.)),
10210 gpui::Point::new(px(10.), px(80.)),
10211 ))),
10212 ..Default::default()
10213 },
10214 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
10215 )
10216 .unwrap()
10217 });
10218
10219 let is_still_following = Rc::new(RefCell::new(true));
10220 let follower_edit_event_count = Rc::new(RefCell::new(0));
10221 let pending_update = Rc::new(RefCell::new(None));
10222 let leader_entity = leader.root(cx).unwrap();
10223 let follower_entity = follower.root(cx).unwrap();
10224 _ = follower.update(cx, {
10225 let update = pending_update.clone();
10226 let is_still_following = is_still_following.clone();
10227 let follower_edit_event_count = follower_edit_event_count.clone();
10228 |_, window, cx| {
10229 cx.subscribe_in(
10230 &leader_entity,
10231 window,
10232 move |_, leader, event, window, cx| {
10233 leader.read(cx).add_event_to_update_proto(
10234 event,
10235 &mut update.borrow_mut(),
10236 window,
10237 cx,
10238 );
10239 },
10240 )
10241 .detach();
10242
10243 cx.subscribe_in(
10244 &follower_entity,
10245 window,
10246 move |_, _, event: &EditorEvent, _window, _cx| {
10247 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
10248 *is_still_following.borrow_mut() = false;
10249 }
10250
10251 if let EditorEvent::BufferEdited = event {
10252 *follower_edit_event_count.borrow_mut() += 1;
10253 }
10254 },
10255 )
10256 .detach();
10257 }
10258 });
10259
10260 // Update the selections only
10261 _ = leader.update(cx, |leader, window, cx| {
10262 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
10263 });
10264 follower
10265 .update(cx, |follower, window, cx| {
10266 follower.apply_update_proto(
10267 &project,
10268 pending_update.borrow_mut().take().unwrap(),
10269 window,
10270 cx,
10271 )
10272 })
10273 .unwrap()
10274 .await
10275 .unwrap();
10276 _ = follower.update(cx, |follower, _, cx| {
10277 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
10278 });
10279 assert!(*is_still_following.borrow());
10280 assert_eq!(*follower_edit_event_count.borrow(), 0);
10281
10282 // Update the scroll position only
10283 _ = leader.update(cx, |leader, window, cx| {
10284 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
10285 });
10286 follower
10287 .update(cx, |follower, window, cx| {
10288 follower.apply_update_proto(
10289 &project,
10290 pending_update.borrow_mut().take().unwrap(),
10291 window,
10292 cx,
10293 )
10294 })
10295 .unwrap()
10296 .await
10297 .unwrap();
10298 assert_eq!(
10299 follower
10300 .update(cx, |follower, _, cx| follower.scroll_position(cx))
10301 .unwrap(),
10302 gpui::Point::new(1.5, 3.5)
10303 );
10304 assert!(*is_still_following.borrow());
10305 assert_eq!(*follower_edit_event_count.borrow(), 0);
10306
10307 // Update the selections and scroll position. The follower's scroll position is updated
10308 // via autoscroll, not via the leader's exact scroll position.
10309 _ = leader.update(cx, |leader, window, cx| {
10310 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10311 leader.request_autoscroll(Autoscroll::newest(), cx);
10312 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
10313 });
10314 follower
10315 .update(cx, |follower, window, cx| {
10316 follower.apply_update_proto(
10317 &project,
10318 pending_update.borrow_mut().take().unwrap(),
10319 window,
10320 cx,
10321 )
10322 })
10323 .unwrap()
10324 .await
10325 .unwrap();
10326 _ = follower.update(cx, |follower, _, cx| {
10327 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
10328 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
10329 });
10330 assert!(*is_still_following.borrow());
10331
10332 // Creating a pending selection that precedes another selection
10333 _ = leader.update(cx, |leader, window, cx| {
10334 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
10335 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
10336 });
10337 follower
10338 .update(cx, |follower, window, cx| {
10339 follower.apply_update_proto(
10340 &project,
10341 pending_update.borrow_mut().take().unwrap(),
10342 window,
10343 cx,
10344 )
10345 })
10346 .unwrap()
10347 .await
10348 .unwrap();
10349 _ = follower.update(cx, |follower, _, cx| {
10350 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
10351 });
10352 assert!(*is_still_following.borrow());
10353
10354 // Extend the pending selection so that it surrounds another selection
10355 _ = leader.update(cx, |leader, window, cx| {
10356 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
10357 });
10358 follower
10359 .update(cx, |follower, window, cx| {
10360 follower.apply_update_proto(
10361 &project,
10362 pending_update.borrow_mut().take().unwrap(),
10363 window,
10364 cx,
10365 )
10366 })
10367 .unwrap()
10368 .await
10369 .unwrap();
10370 _ = follower.update(cx, |follower, _, cx| {
10371 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
10372 });
10373
10374 // Scrolling locally breaks the follow
10375 _ = follower.update(cx, |follower, window, cx| {
10376 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
10377 follower.set_scroll_anchor(
10378 ScrollAnchor {
10379 anchor: top_anchor,
10380 offset: gpui::Point::new(0.0, 0.5),
10381 },
10382 window,
10383 cx,
10384 );
10385 });
10386 assert!(!(*is_still_following.borrow()));
10387}
10388
10389#[gpui::test]
10390async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
10391 init_test(cx, |_| {});
10392
10393 let fs = FakeFs::new(cx.executor());
10394 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
10395 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10396 let pane = workspace
10397 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10398 .unwrap();
10399
10400 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10401
10402 let leader = pane.update_in(cx, |_, window, cx| {
10403 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
10404 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
10405 });
10406
10407 // Start following the editor when it has no excerpts.
10408 let mut state_message =
10409 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
10410 let workspace_entity = workspace.root(cx).unwrap();
10411 let follower_1 = cx
10412 .update_window(*workspace.deref(), |_, window, cx| {
10413 Editor::from_state_proto(
10414 workspace_entity,
10415 ViewId {
10416 creator: Default::default(),
10417 id: 0,
10418 },
10419 &mut state_message,
10420 window,
10421 cx,
10422 )
10423 })
10424 .unwrap()
10425 .unwrap()
10426 .await
10427 .unwrap();
10428
10429 let update_message = Rc::new(RefCell::new(None));
10430 follower_1.update_in(cx, {
10431 let update = update_message.clone();
10432 |_, window, cx| {
10433 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
10434 leader.read(cx).add_event_to_update_proto(
10435 event,
10436 &mut update.borrow_mut(),
10437 window,
10438 cx,
10439 );
10440 })
10441 .detach();
10442 }
10443 });
10444
10445 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
10446 (
10447 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
10448 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
10449 )
10450 });
10451
10452 // Insert some excerpts.
10453 leader.update(cx, |leader, cx| {
10454 leader.buffer.update(cx, |multibuffer, cx| {
10455 let excerpt_ids = multibuffer.push_excerpts(
10456 buffer_1.clone(),
10457 [
10458 ExcerptRange {
10459 context: 1..6,
10460 primary: None,
10461 },
10462 ExcerptRange {
10463 context: 12..15,
10464 primary: None,
10465 },
10466 ExcerptRange {
10467 context: 0..3,
10468 primary: None,
10469 },
10470 ],
10471 cx,
10472 );
10473 multibuffer.insert_excerpts_after(
10474 excerpt_ids[0],
10475 buffer_2.clone(),
10476 [
10477 ExcerptRange {
10478 context: 8..12,
10479 primary: None,
10480 },
10481 ExcerptRange {
10482 context: 0..6,
10483 primary: None,
10484 },
10485 ],
10486 cx,
10487 );
10488 });
10489 });
10490
10491 // Apply the update of adding the excerpts.
10492 follower_1
10493 .update_in(cx, |follower, window, cx| {
10494 follower.apply_update_proto(
10495 &project,
10496 update_message.borrow().clone().unwrap(),
10497 window,
10498 cx,
10499 )
10500 })
10501 .await
10502 .unwrap();
10503 assert_eq!(
10504 follower_1.update(cx, |editor, cx| editor.text(cx)),
10505 leader.update(cx, |editor, cx| editor.text(cx))
10506 );
10507 update_message.borrow_mut().take();
10508
10509 // Start following separately after it already has excerpts.
10510 let mut state_message =
10511 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
10512 let workspace_entity = workspace.root(cx).unwrap();
10513 let follower_2 = cx
10514 .update_window(*workspace.deref(), |_, window, cx| {
10515 Editor::from_state_proto(
10516 workspace_entity,
10517 ViewId {
10518 creator: Default::default(),
10519 id: 0,
10520 },
10521 &mut state_message,
10522 window,
10523 cx,
10524 )
10525 })
10526 .unwrap()
10527 .unwrap()
10528 .await
10529 .unwrap();
10530 assert_eq!(
10531 follower_2.update(cx, |editor, cx| editor.text(cx)),
10532 leader.update(cx, |editor, cx| editor.text(cx))
10533 );
10534
10535 // Remove some excerpts.
10536 leader.update(cx, |leader, cx| {
10537 leader.buffer.update(cx, |multibuffer, cx| {
10538 let excerpt_ids = multibuffer.excerpt_ids();
10539 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
10540 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
10541 });
10542 });
10543
10544 // Apply the update of removing the excerpts.
10545 follower_1
10546 .update_in(cx, |follower, window, cx| {
10547 follower.apply_update_proto(
10548 &project,
10549 update_message.borrow().clone().unwrap(),
10550 window,
10551 cx,
10552 )
10553 })
10554 .await
10555 .unwrap();
10556 follower_2
10557 .update_in(cx, |follower, window, cx| {
10558 follower.apply_update_proto(
10559 &project,
10560 update_message.borrow().clone().unwrap(),
10561 window,
10562 cx,
10563 )
10564 })
10565 .await
10566 .unwrap();
10567 update_message.borrow_mut().take();
10568 assert_eq!(
10569 follower_1.update(cx, |editor, cx| editor.text(cx)),
10570 leader.update(cx, |editor, cx| editor.text(cx))
10571 );
10572}
10573
10574#[gpui::test]
10575async fn go_to_prev_overlapping_diagnostic(
10576 executor: BackgroundExecutor,
10577 cx: &mut gpui::TestAppContext,
10578) {
10579 init_test(cx, |_| {});
10580
10581 let mut cx = EditorTestContext::new(cx).await;
10582 let lsp_store =
10583 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10584
10585 cx.set_state(indoc! {"
10586 ˇfn func(abc def: i32) -> u32 {
10587 }
10588 "});
10589
10590 cx.update(|_, cx| {
10591 lsp_store.update(cx, |lsp_store, cx| {
10592 lsp_store
10593 .update_diagnostics(
10594 LanguageServerId(0),
10595 lsp::PublishDiagnosticsParams {
10596 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
10597 version: None,
10598 diagnostics: vec![
10599 lsp::Diagnostic {
10600 range: lsp::Range::new(
10601 lsp::Position::new(0, 11),
10602 lsp::Position::new(0, 12),
10603 ),
10604 severity: Some(lsp::DiagnosticSeverity::ERROR),
10605 ..Default::default()
10606 },
10607 lsp::Diagnostic {
10608 range: lsp::Range::new(
10609 lsp::Position::new(0, 12),
10610 lsp::Position::new(0, 15),
10611 ),
10612 severity: Some(lsp::DiagnosticSeverity::ERROR),
10613 ..Default::default()
10614 },
10615 lsp::Diagnostic {
10616 range: lsp::Range::new(
10617 lsp::Position::new(0, 25),
10618 lsp::Position::new(0, 28),
10619 ),
10620 severity: Some(lsp::DiagnosticSeverity::ERROR),
10621 ..Default::default()
10622 },
10623 ],
10624 },
10625 &[],
10626 cx,
10627 )
10628 .unwrap()
10629 });
10630 });
10631
10632 executor.run_until_parked();
10633
10634 cx.update_editor(|editor, window, cx| {
10635 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10636 });
10637
10638 cx.assert_editor_state(indoc! {"
10639 fn func(abc def: i32) -> ˇu32 {
10640 }
10641 "});
10642
10643 cx.update_editor(|editor, window, cx| {
10644 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10645 });
10646
10647 cx.assert_editor_state(indoc! {"
10648 fn func(abc ˇdef: i32) -> u32 {
10649 }
10650 "});
10651
10652 cx.update_editor(|editor, window, cx| {
10653 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10654 });
10655
10656 cx.assert_editor_state(indoc! {"
10657 fn func(abcˇ def: i32) -> u32 {
10658 }
10659 "});
10660
10661 cx.update_editor(|editor, window, cx| {
10662 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10663 });
10664
10665 cx.assert_editor_state(indoc! {"
10666 fn func(abc def: i32) -> ˇu32 {
10667 }
10668 "});
10669}
10670
10671#[gpui::test]
10672async fn cycle_through_same_place_diagnostics(
10673 executor: BackgroundExecutor,
10674 cx: &mut gpui::TestAppContext,
10675) {
10676 init_test(cx, |_| {});
10677
10678 let mut cx = EditorTestContext::new(cx).await;
10679 let lsp_store =
10680 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10681
10682 cx.set_state(indoc! {"
10683 ˇfn func(abc def: i32) -> u32 {
10684 }
10685 "});
10686
10687 cx.update(|_, cx| {
10688 lsp_store.update(cx, |lsp_store, cx| {
10689 lsp_store
10690 .update_diagnostics(
10691 LanguageServerId(0),
10692 lsp::PublishDiagnosticsParams {
10693 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
10694 version: None,
10695 diagnostics: vec![
10696 lsp::Diagnostic {
10697 range: lsp::Range::new(
10698 lsp::Position::new(0, 11),
10699 lsp::Position::new(0, 12),
10700 ),
10701 severity: Some(lsp::DiagnosticSeverity::ERROR),
10702 ..Default::default()
10703 },
10704 lsp::Diagnostic {
10705 range: lsp::Range::new(
10706 lsp::Position::new(0, 12),
10707 lsp::Position::new(0, 15),
10708 ),
10709 severity: Some(lsp::DiagnosticSeverity::ERROR),
10710 ..Default::default()
10711 },
10712 lsp::Diagnostic {
10713 range: lsp::Range::new(
10714 lsp::Position::new(0, 12),
10715 lsp::Position::new(0, 15),
10716 ),
10717 severity: Some(lsp::DiagnosticSeverity::ERROR),
10718 ..Default::default()
10719 },
10720 lsp::Diagnostic {
10721 range: lsp::Range::new(
10722 lsp::Position::new(0, 25),
10723 lsp::Position::new(0, 28),
10724 ),
10725 severity: Some(lsp::DiagnosticSeverity::ERROR),
10726 ..Default::default()
10727 },
10728 ],
10729 },
10730 &[],
10731 cx,
10732 )
10733 .unwrap()
10734 });
10735 });
10736 executor.run_until_parked();
10737
10738 //// Backward
10739
10740 // Fourth diagnostic
10741 cx.update_editor(|editor, window, cx| {
10742 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10743 });
10744 cx.assert_editor_state(indoc! {"
10745 fn func(abc def: i32) -> ˇu32 {
10746 }
10747 "});
10748
10749 // Third diagnostic
10750 cx.update_editor(|editor, window, cx| {
10751 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10752 });
10753 cx.assert_editor_state(indoc! {"
10754 fn func(abc ˇdef: i32) -> u32 {
10755 }
10756 "});
10757
10758 // Second diagnostic, same place
10759 cx.update_editor(|editor, window, cx| {
10760 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10761 });
10762 cx.assert_editor_state(indoc! {"
10763 fn func(abc ˇdef: i32) -> u32 {
10764 }
10765 "});
10766
10767 // First diagnostic
10768 cx.update_editor(|editor, window, cx| {
10769 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10770 });
10771 cx.assert_editor_state(indoc! {"
10772 fn func(abcˇ def: i32) -> u32 {
10773 }
10774 "});
10775
10776 // Wrapped over, fourth diagnostic
10777 cx.update_editor(|editor, window, cx| {
10778 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10779 });
10780 cx.assert_editor_state(indoc! {"
10781 fn func(abc def: i32) -> ˇu32 {
10782 }
10783 "});
10784
10785 cx.update_editor(|editor, window, cx| {
10786 editor.move_to_beginning(&MoveToBeginning, window, cx);
10787 });
10788 cx.assert_editor_state(indoc! {"
10789 ˇfn func(abc def: i32) -> u32 {
10790 }
10791 "});
10792
10793 //// Forward
10794
10795 // First diagnostic
10796 cx.update_editor(|editor, window, cx| {
10797 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10798 });
10799 cx.assert_editor_state(indoc! {"
10800 fn func(abcˇ def: i32) -> u32 {
10801 }
10802 "});
10803
10804 // Second diagnostic
10805 cx.update_editor(|editor, window, cx| {
10806 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10807 });
10808 cx.assert_editor_state(indoc! {"
10809 fn func(abc ˇdef: i32) -> u32 {
10810 }
10811 "});
10812
10813 // Third diagnostic, same place
10814 cx.update_editor(|editor, window, cx| {
10815 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10816 });
10817 cx.assert_editor_state(indoc! {"
10818 fn func(abc ˇdef: i32) -> u32 {
10819 }
10820 "});
10821
10822 // Fourth diagnostic
10823 cx.update_editor(|editor, window, cx| {
10824 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10825 });
10826 cx.assert_editor_state(indoc! {"
10827 fn func(abc def: i32) -> ˇu32 {
10828 }
10829 "});
10830
10831 // Wrapped around, first diagnostic
10832 cx.update_editor(|editor, window, cx| {
10833 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10834 });
10835 cx.assert_editor_state(indoc! {"
10836 fn func(abcˇ def: i32) -> u32 {
10837 }
10838 "});
10839}
10840
10841#[gpui::test]
10842async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
10843 init_test(cx, |_| {});
10844
10845 let mut cx = EditorTestContext::new(cx).await;
10846
10847 cx.set_state(indoc! {"
10848 fn func(abˇc def: i32) -> u32 {
10849 }
10850 "});
10851 let lsp_store =
10852 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10853
10854 cx.update(|_, cx| {
10855 lsp_store.update(cx, |lsp_store, cx| {
10856 lsp_store.update_diagnostics(
10857 LanguageServerId(0),
10858 lsp::PublishDiagnosticsParams {
10859 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
10860 version: None,
10861 diagnostics: vec![lsp::Diagnostic {
10862 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
10863 severity: Some(lsp::DiagnosticSeverity::ERROR),
10864 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
10865 ..Default::default()
10866 }],
10867 },
10868 &[],
10869 cx,
10870 )
10871 })
10872 }).unwrap();
10873 cx.run_until_parked();
10874 cx.update_editor(|editor, window, cx| {
10875 hover_popover::hover(editor, &Default::default(), window, cx)
10876 });
10877 cx.run_until_parked();
10878 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
10879}
10880
10881#[gpui::test]
10882async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
10883 init_test(cx, |_| {});
10884
10885 let mut cx = EditorTestContext::new(cx).await;
10886
10887 let diff_base = r#"
10888 use some::mod;
10889
10890 const A: u32 = 42;
10891
10892 fn main() {
10893 println!("hello");
10894
10895 println!("world");
10896 }
10897 "#
10898 .unindent();
10899
10900 // Edits are modified, removed, modified, added
10901 cx.set_state(
10902 &r#"
10903 use some::modified;
10904
10905 ˇ
10906 fn main() {
10907 println!("hello there");
10908
10909 println!("around the");
10910 println!("world");
10911 }
10912 "#
10913 .unindent(),
10914 );
10915
10916 cx.set_diff_base(&diff_base);
10917 executor.run_until_parked();
10918
10919 cx.update_editor(|editor, window, cx| {
10920 //Wrap around the bottom of the buffer
10921 for _ in 0..3 {
10922 editor.go_to_next_hunk(&GoToHunk, window, cx);
10923 }
10924 });
10925
10926 cx.assert_editor_state(
10927 &r#"
10928 ˇuse some::modified;
10929
10930
10931 fn main() {
10932 println!("hello there");
10933
10934 println!("around the");
10935 println!("world");
10936 }
10937 "#
10938 .unindent(),
10939 );
10940
10941 cx.update_editor(|editor, window, cx| {
10942 //Wrap around the top of the buffer
10943 for _ in 0..2 {
10944 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10945 }
10946 });
10947
10948 cx.assert_editor_state(
10949 &r#"
10950 use some::modified;
10951
10952
10953 fn main() {
10954 ˇ println!("hello there");
10955
10956 println!("around the");
10957 println!("world");
10958 }
10959 "#
10960 .unindent(),
10961 );
10962
10963 cx.update_editor(|editor, window, cx| {
10964 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10965 });
10966
10967 cx.assert_editor_state(
10968 &r#"
10969 use some::modified;
10970
10971 ˇ
10972 fn main() {
10973 println!("hello there");
10974
10975 println!("around the");
10976 println!("world");
10977 }
10978 "#
10979 .unindent(),
10980 );
10981
10982 cx.update_editor(|editor, window, cx| {
10983 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
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 for _ in 0..2 {
11003 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
11004 }
11005 });
11006
11007 cx.assert_editor_state(
11008 &r#"
11009 use some::modified;
11010
11011
11012 fn main() {
11013 ˇ println!("hello there");
11014
11015 println!("around the");
11016 println!("world");
11017 }
11018 "#
11019 .unindent(),
11020 );
11021
11022 cx.update_editor(|editor, window, cx| {
11023 editor.fold(&Fold, window, cx);
11024 });
11025
11026 cx.update_editor(|editor, window, cx| {
11027 editor.go_to_next_hunk(&GoToHunk, window, cx);
11028 });
11029
11030 cx.assert_editor_state(
11031 &r#"
11032 ˇuse some::modified;
11033
11034
11035 fn main() {
11036 println!("hello there");
11037
11038 println!("around the");
11039 println!("world");
11040 }
11041 "#
11042 .unindent(),
11043 );
11044}
11045
11046#[test]
11047fn test_split_words() {
11048 fn split(text: &str) -> Vec<&str> {
11049 split_words(text).collect()
11050 }
11051
11052 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
11053 assert_eq!(split("hello_world"), &["hello_", "world"]);
11054 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
11055 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
11056 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
11057 assert_eq!(split("helloworld"), &["helloworld"]);
11058
11059 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
11060}
11061
11062#[gpui::test]
11063async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
11064 init_test(cx, |_| {});
11065
11066 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
11067 let mut assert = |before, after| {
11068 let _state_context = cx.set_state(before);
11069 cx.run_until_parked();
11070 cx.update_editor(|editor, window, cx| {
11071 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
11072 });
11073 cx.assert_editor_state(after);
11074 };
11075
11076 // Outside bracket jumps to outside of matching bracket
11077 assert("console.logˇ(var);", "console.log(var)ˇ;");
11078 assert("console.log(var)ˇ;", "console.logˇ(var);");
11079
11080 // Inside bracket jumps to inside of matching bracket
11081 assert("console.log(ˇvar);", "console.log(varˇ);");
11082 assert("console.log(varˇ);", "console.log(ˇvar);");
11083
11084 // When outside a bracket and inside, favor jumping to the inside bracket
11085 assert(
11086 "console.log('foo', [1, 2, 3]ˇ);",
11087 "console.log(ˇ'foo', [1, 2, 3]);",
11088 );
11089 assert(
11090 "console.log(ˇ'foo', [1, 2, 3]);",
11091 "console.log('foo', [1, 2, 3]ˇ);",
11092 );
11093
11094 // Bias forward if two options are equally likely
11095 assert(
11096 "let result = curried_fun()ˇ();",
11097 "let result = curried_fun()()ˇ;",
11098 );
11099
11100 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
11101 assert(
11102 indoc! {"
11103 function test() {
11104 console.log('test')ˇ
11105 }"},
11106 indoc! {"
11107 function test() {
11108 console.logˇ('test')
11109 }"},
11110 );
11111}
11112
11113#[gpui::test]
11114async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
11115 init_test(cx, |_| {});
11116
11117 let fs = FakeFs::new(cx.executor());
11118 fs.insert_tree(
11119 path!("/a"),
11120 json!({
11121 "main.rs": "fn main() { let a = 5; }",
11122 "other.rs": "// Test file",
11123 }),
11124 )
11125 .await;
11126 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11127
11128 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11129 language_registry.add(Arc::new(Language::new(
11130 LanguageConfig {
11131 name: "Rust".into(),
11132 matcher: LanguageMatcher {
11133 path_suffixes: vec!["rs".to_string()],
11134 ..Default::default()
11135 },
11136 brackets: BracketPairConfig {
11137 pairs: vec![BracketPair {
11138 start: "{".to_string(),
11139 end: "}".to_string(),
11140 close: true,
11141 surround: true,
11142 newline: true,
11143 }],
11144 disabled_scopes_by_bracket_ix: Vec::new(),
11145 },
11146 ..Default::default()
11147 },
11148 Some(tree_sitter_rust::LANGUAGE.into()),
11149 )));
11150 let mut fake_servers = language_registry.register_fake_lsp(
11151 "Rust",
11152 FakeLspAdapter {
11153 capabilities: lsp::ServerCapabilities {
11154 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
11155 first_trigger_character: "{".to_string(),
11156 more_trigger_character: None,
11157 }),
11158 ..Default::default()
11159 },
11160 ..Default::default()
11161 },
11162 );
11163
11164 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11165
11166 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11167
11168 let worktree_id = workspace
11169 .update(cx, |workspace, _, cx| {
11170 workspace.project().update(cx, |project, cx| {
11171 project.worktrees(cx).next().unwrap().read(cx).id()
11172 })
11173 })
11174 .unwrap();
11175
11176 let buffer = project
11177 .update(cx, |project, cx| {
11178 project.open_local_buffer(path!("/a/main.rs"), cx)
11179 })
11180 .await
11181 .unwrap();
11182 let editor_handle = workspace
11183 .update(cx, |workspace, window, cx| {
11184 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
11185 })
11186 .unwrap()
11187 .await
11188 .unwrap()
11189 .downcast::<Editor>()
11190 .unwrap();
11191
11192 cx.executor().start_waiting();
11193 let fake_server = fake_servers.next().await.unwrap();
11194
11195 fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
11196 assert_eq!(
11197 params.text_document_position.text_document.uri,
11198 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
11199 );
11200 assert_eq!(
11201 params.text_document_position.position,
11202 lsp::Position::new(0, 21),
11203 );
11204
11205 Ok(Some(vec![lsp::TextEdit {
11206 new_text: "]".to_string(),
11207 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11208 }]))
11209 });
11210
11211 editor_handle.update_in(cx, |editor, window, cx| {
11212 window.focus(&editor.focus_handle(cx));
11213 editor.change_selections(None, window, cx, |s| {
11214 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
11215 });
11216 editor.handle_input("{", window, cx);
11217 });
11218
11219 cx.executor().run_until_parked();
11220
11221 buffer.update(cx, |buffer, _| {
11222 assert_eq!(
11223 buffer.text(),
11224 "fn main() { let a = {5}; }",
11225 "No extra braces from on type formatting should appear in the buffer"
11226 )
11227 });
11228}
11229
11230#[gpui::test]
11231async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) {
11232 init_test(cx, |_| {});
11233
11234 let fs = FakeFs::new(cx.executor());
11235 fs.insert_tree(
11236 path!("/a"),
11237 json!({
11238 "main.rs": "fn main() { let a = 5; }",
11239 "other.rs": "// Test file",
11240 }),
11241 )
11242 .await;
11243
11244 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11245
11246 let server_restarts = Arc::new(AtomicUsize::new(0));
11247 let closure_restarts = Arc::clone(&server_restarts);
11248 let language_server_name = "test language server";
11249 let language_name: LanguageName = "Rust".into();
11250
11251 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11252 language_registry.add(Arc::new(Language::new(
11253 LanguageConfig {
11254 name: language_name.clone(),
11255 matcher: LanguageMatcher {
11256 path_suffixes: vec!["rs".to_string()],
11257 ..Default::default()
11258 },
11259 ..Default::default()
11260 },
11261 Some(tree_sitter_rust::LANGUAGE.into()),
11262 )));
11263 let mut fake_servers = language_registry.register_fake_lsp(
11264 "Rust",
11265 FakeLspAdapter {
11266 name: language_server_name,
11267 initialization_options: Some(json!({
11268 "testOptionValue": true
11269 })),
11270 initializer: Some(Box::new(move |fake_server| {
11271 let task_restarts = Arc::clone(&closure_restarts);
11272 fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
11273 task_restarts.fetch_add(1, atomic::Ordering::Release);
11274 futures::future::ready(Ok(()))
11275 });
11276 })),
11277 ..Default::default()
11278 },
11279 );
11280
11281 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11282 let _buffer = project
11283 .update(cx, |project, cx| {
11284 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
11285 })
11286 .await
11287 .unwrap();
11288 let _fake_server = fake_servers.next().await.unwrap();
11289 update_test_language_settings(cx, |language_settings| {
11290 language_settings.languages.insert(
11291 language_name.clone(),
11292 LanguageSettingsContent {
11293 tab_size: NonZeroU32::new(8),
11294 ..Default::default()
11295 },
11296 );
11297 });
11298 cx.executor().run_until_parked();
11299 assert_eq!(
11300 server_restarts.load(atomic::Ordering::Acquire),
11301 0,
11302 "Should not restart LSP server on an unrelated change"
11303 );
11304
11305 update_test_project_settings(cx, |project_settings| {
11306 project_settings.lsp.insert(
11307 "Some other server name".into(),
11308 LspSettings {
11309 binary: None,
11310 settings: None,
11311 initialization_options: Some(json!({
11312 "some other init value": false
11313 })),
11314 },
11315 );
11316 });
11317 cx.executor().run_until_parked();
11318 assert_eq!(
11319 server_restarts.load(atomic::Ordering::Acquire),
11320 0,
11321 "Should not restart LSP server on an unrelated LSP settings change"
11322 );
11323
11324 update_test_project_settings(cx, |project_settings| {
11325 project_settings.lsp.insert(
11326 language_server_name.into(),
11327 LspSettings {
11328 binary: None,
11329 settings: None,
11330 initialization_options: Some(json!({
11331 "anotherInitValue": false
11332 })),
11333 },
11334 );
11335 });
11336 cx.executor().run_until_parked();
11337 assert_eq!(
11338 server_restarts.load(atomic::Ordering::Acquire),
11339 1,
11340 "Should restart LSP server on a related LSP settings change"
11341 );
11342
11343 update_test_project_settings(cx, |project_settings| {
11344 project_settings.lsp.insert(
11345 language_server_name.into(),
11346 LspSettings {
11347 binary: None,
11348 settings: None,
11349 initialization_options: Some(json!({
11350 "anotherInitValue": false
11351 })),
11352 },
11353 );
11354 });
11355 cx.executor().run_until_parked();
11356 assert_eq!(
11357 server_restarts.load(atomic::Ordering::Acquire),
11358 1,
11359 "Should not restart LSP server on a related LSP settings change that is the same"
11360 );
11361
11362 update_test_project_settings(cx, |project_settings| {
11363 project_settings.lsp.insert(
11364 language_server_name.into(),
11365 LspSettings {
11366 binary: None,
11367 settings: None,
11368 initialization_options: None,
11369 },
11370 );
11371 });
11372 cx.executor().run_until_parked();
11373 assert_eq!(
11374 server_restarts.load(atomic::Ordering::Acquire),
11375 2,
11376 "Should restart LSP server on another related LSP settings change"
11377 );
11378}
11379
11380#[gpui::test]
11381async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
11382 init_test(cx, |_| {});
11383
11384 let mut cx = EditorLspTestContext::new_rust(
11385 lsp::ServerCapabilities {
11386 completion_provider: Some(lsp::CompletionOptions {
11387 trigger_characters: Some(vec![".".to_string()]),
11388 resolve_provider: Some(true),
11389 ..Default::default()
11390 }),
11391 ..Default::default()
11392 },
11393 cx,
11394 )
11395 .await;
11396
11397 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11398 cx.simulate_keystroke(".");
11399 let completion_item = lsp::CompletionItem {
11400 label: "some".into(),
11401 kind: Some(lsp::CompletionItemKind::SNIPPET),
11402 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11403 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11404 kind: lsp::MarkupKind::Markdown,
11405 value: "```rust\nSome(2)\n```".to_string(),
11406 })),
11407 deprecated: Some(false),
11408 sort_text: Some("fffffff2".to_string()),
11409 filter_text: Some("some".to_string()),
11410 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11411 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11412 range: lsp::Range {
11413 start: lsp::Position {
11414 line: 0,
11415 character: 22,
11416 },
11417 end: lsp::Position {
11418 line: 0,
11419 character: 22,
11420 },
11421 },
11422 new_text: "Some(2)".to_string(),
11423 })),
11424 additional_text_edits: Some(vec![lsp::TextEdit {
11425 range: lsp::Range {
11426 start: lsp::Position {
11427 line: 0,
11428 character: 20,
11429 },
11430 end: lsp::Position {
11431 line: 0,
11432 character: 22,
11433 },
11434 },
11435 new_text: "".to_string(),
11436 }]),
11437 ..Default::default()
11438 };
11439
11440 let closure_completion_item = completion_item.clone();
11441 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11442 let task_completion_item = closure_completion_item.clone();
11443 async move {
11444 Ok(Some(lsp::CompletionResponse::Array(vec![
11445 task_completion_item,
11446 ])))
11447 }
11448 });
11449
11450 request.next().await;
11451
11452 cx.condition(|editor, _| editor.context_menu_visible())
11453 .await;
11454 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11455 editor
11456 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11457 .unwrap()
11458 });
11459 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
11460
11461 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
11462 let task_completion_item = completion_item.clone();
11463 async move { Ok(task_completion_item) }
11464 })
11465 .next()
11466 .await
11467 .unwrap();
11468 apply_additional_edits.await.unwrap();
11469 cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
11470}
11471
11472#[gpui::test]
11473async fn test_completions_resolve_updates_labels_if_filter_text_matches(
11474 cx: &mut gpui::TestAppContext,
11475) {
11476 init_test(cx, |_| {});
11477
11478 let mut cx = EditorLspTestContext::new_rust(
11479 lsp::ServerCapabilities {
11480 completion_provider: Some(lsp::CompletionOptions {
11481 trigger_characters: Some(vec![".".to_string()]),
11482 resolve_provider: Some(true),
11483 ..Default::default()
11484 }),
11485 ..Default::default()
11486 },
11487 cx,
11488 )
11489 .await;
11490
11491 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11492 cx.simulate_keystroke(".");
11493
11494 let item1 = lsp::CompletionItem {
11495 label: "method id()".to_string(),
11496 filter_text: Some("id".to_string()),
11497 detail: None,
11498 documentation: None,
11499 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11500 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11501 new_text: ".id".to_string(),
11502 })),
11503 ..lsp::CompletionItem::default()
11504 };
11505
11506 let item2 = lsp::CompletionItem {
11507 label: "other".to_string(),
11508 filter_text: Some("other".to_string()),
11509 detail: None,
11510 documentation: None,
11511 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11512 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11513 new_text: ".other".to_string(),
11514 })),
11515 ..lsp::CompletionItem::default()
11516 };
11517
11518 let item1 = item1.clone();
11519 cx.handle_request::<lsp::request::Completion, _, _>({
11520 let item1 = item1.clone();
11521 move |_, _, _| {
11522 let item1 = item1.clone();
11523 let item2 = item2.clone();
11524 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
11525 }
11526 })
11527 .next()
11528 .await;
11529
11530 cx.condition(|editor, _| editor.context_menu_visible())
11531 .await;
11532 cx.update_editor(|editor, _, _| {
11533 let context_menu = editor.context_menu.borrow_mut();
11534 let context_menu = context_menu
11535 .as_ref()
11536 .expect("Should have the context menu deployed");
11537 match context_menu {
11538 CodeContextMenu::Completions(completions_menu) => {
11539 let completions = completions_menu.completions.borrow_mut();
11540 assert_eq!(
11541 completions
11542 .iter()
11543 .map(|completion| &completion.label.text)
11544 .collect::<Vec<_>>(),
11545 vec!["method id()", "other"]
11546 )
11547 }
11548 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11549 }
11550 });
11551
11552 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>({
11553 let item1 = item1.clone();
11554 move |_, item_to_resolve, _| {
11555 let item1 = item1.clone();
11556 async move {
11557 if item1 == item_to_resolve {
11558 Ok(lsp::CompletionItem {
11559 label: "method id()".to_string(),
11560 filter_text: Some("id".to_string()),
11561 detail: Some("Now resolved!".to_string()),
11562 documentation: Some(lsp::Documentation::String("Docs".to_string())),
11563 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11564 range: lsp::Range::new(
11565 lsp::Position::new(0, 22),
11566 lsp::Position::new(0, 22),
11567 ),
11568 new_text: ".id".to_string(),
11569 })),
11570 ..lsp::CompletionItem::default()
11571 })
11572 } else {
11573 Ok(item_to_resolve)
11574 }
11575 }
11576 }
11577 })
11578 .next()
11579 .await
11580 .unwrap();
11581 cx.run_until_parked();
11582
11583 cx.update_editor(|editor, window, cx| {
11584 editor.context_menu_next(&Default::default(), window, cx);
11585 });
11586
11587 cx.update_editor(|editor, _, _| {
11588 let context_menu = editor.context_menu.borrow_mut();
11589 let context_menu = context_menu
11590 .as_ref()
11591 .expect("Should have the context menu deployed");
11592 match context_menu {
11593 CodeContextMenu::Completions(completions_menu) => {
11594 let completions = completions_menu.completions.borrow_mut();
11595 assert_eq!(
11596 completions
11597 .iter()
11598 .map(|completion| &completion.label.text)
11599 .collect::<Vec<_>>(),
11600 vec!["method id() Now resolved!", "other"],
11601 "Should update first completion label, but not second as the filter text did not match."
11602 );
11603 }
11604 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11605 }
11606 });
11607}
11608
11609#[gpui::test]
11610async fn test_completions_resolve_happens_once(cx: &mut gpui::TestAppContext) {
11611 init_test(cx, |_| {});
11612
11613 let mut cx = EditorLspTestContext::new_rust(
11614 lsp::ServerCapabilities {
11615 completion_provider: Some(lsp::CompletionOptions {
11616 trigger_characters: Some(vec![".".to_string()]),
11617 resolve_provider: Some(true),
11618 ..Default::default()
11619 }),
11620 ..Default::default()
11621 },
11622 cx,
11623 )
11624 .await;
11625
11626 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11627 cx.simulate_keystroke(".");
11628
11629 let unresolved_item_1 = lsp::CompletionItem {
11630 label: "id".to_string(),
11631 filter_text: Some("id".to_string()),
11632 detail: None,
11633 documentation: None,
11634 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11635 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11636 new_text: ".id".to_string(),
11637 })),
11638 ..lsp::CompletionItem::default()
11639 };
11640 let resolved_item_1 = lsp::CompletionItem {
11641 additional_text_edits: Some(vec![lsp::TextEdit {
11642 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
11643 new_text: "!!".to_string(),
11644 }]),
11645 ..unresolved_item_1.clone()
11646 };
11647 let unresolved_item_2 = lsp::CompletionItem {
11648 label: "other".to_string(),
11649 filter_text: Some("other".to_string()),
11650 detail: None,
11651 documentation: None,
11652 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11653 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11654 new_text: ".other".to_string(),
11655 })),
11656 ..lsp::CompletionItem::default()
11657 };
11658 let resolved_item_2 = lsp::CompletionItem {
11659 additional_text_edits: Some(vec![lsp::TextEdit {
11660 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
11661 new_text: "??".to_string(),
11662 }]),
11663 ..unresolved_item_2.clone()
11664 };
11665
11666 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
11667 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
11668 cx.lsp
11669 .server
11670 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
11671 let unresolved_item_1 = unresolved_item_1.clone();
11672 let resolved_item_1 = resolved_item_1.clone();
11673 let unresolved_item_2 = unresolved_item_2.clone();
11674 let resolved_item_2 = resolved_item_2.clone();
11675 let resolve_requests_1 = resolve_requests_1.clone();
11676 let resolve_requests_2 = resolve_requests_2.clone();
11677 move |unresolved_request, _| {
11678 let unresolved_item_1 = unresolved_item_1.clone();
11679 let resolved_item_1 = resolved_item_1.clone();
11680 let unresolved_item_2 = unresolved_item_2.clone();
11681 let resolved_item_2 = resolved_item_2.clone();
11682 let resolve_requests_1 = resolve_requests_1.clone();
11683 let resolve_requests_2 = resolve_requests_2.clone();
11684 async move {
11685 if unresolved_request == unresolved_item_1 {
11686 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
11687 Ok(resolved_item_1.clone())
11688 } else if unresolved_request == unresolved_item_2 {
11689 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
11690 Ok(resolved_item_2.clone())
11691 } else {
11692 panic!("Unexpected completion item {unresolved_request:?}")
11693 }
11694 }
11695 }
11696 })
11697 .detach();
11698
11699 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11700 let unresolved_item_1 = unresolved_item_1.clone();
11701 let unresolved_item_2 = unresolved_item_2.clone();
11702 async move {
11703 Ok(Some(lsp::CompletionResponse::Array(vec![
11704 unresolved_item_1,
11705 unresolved_item_2,
11706 ])))
11707 }
11708 })
11709 .next()
11710 .await;
11711
11712 cx.condition(|editor, _| editor.context_menu_visible())
11713 .await;
11714 cx.update_editor(|editor, _, _| {
11715 let context_menu = editor.context_menu.borrow_mut();
11716 let context_menu = context_menu
11717 .as_ref()
11718 .expect("Should have the context menu deployed");
11719 match context_menu {
11720 CodeContextMenu::Completions(completions_menu) => {
11721 let completions = completions_menu.completions.borrow_mut();
11722 assert_eq!(
11723 completions
11724 .iter()
11725 .map(|completion| &completion.label.text)
11726 .collect::<Vec<_>>(),
11727 vec!["id", "other"]
11728 )
11729 }
11730 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11731 }
11732 });
11733 cx.run_until_parked();
11734
11735 cx.update_editor(|editor, window, cx| {
11736 editor.context_menu_next(&ContextMenuNext, window, cx);
11737 });
11738 cx.run_until_parked();
11739 cx.update_editor(|editor, window, cx| {
11740 editor.context_menu_prev(&ContextMenuPrev, window, cx);
11741 });
11742 cx.run_until_parked();
11743 cx.update_editor(|editor, window, cx| {
11744 editor.context_menu_next(&ContextMenuNext, window, cx);
11745 });
11746 cx.run_until_parked();
11747 cx.update_editor(|editor, window, cx| {
11748 editor
11749 .compose_completion(&ComposeCompletion::default(), window, cx)
11750 .expect("No task returned")
11751 })
11752 .await
11753 .expect("Completion failed");
11754 cx.run_until_parked();
11755
11756 cx.update_editor(|editor, _, cx| {
11757 assert_eq!(
11758 resolve_requests_1.load(atomic::Ordering::Acquire),
11759 1,
11760 "Should always resolve once despite multiple selections"
11761 );
11762 assert_eq!(
11763 resolve_requests_2.load(atomic::Ordering::Acquire),
11764 1,
11765 "Should always resolve once after multiple selections and applying the completion"
11766 );
11767 assert_eq!(
11768 editor.text(cx),
11769 "fn main() { let a = ??.other; }",
11770 "Should use resolved data when applying the completion"
11771 );
11772 });
11773}
11774
11775#[gpui::test]
11776async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
11777 init_test(cx, |_| {});
11778
11779 let item_0 = lsp::CompletionItem {
11780 label: "abs".into(),
11781 insert_text: Some("abs".into()),
11782 data: Some(json!({ "very": "special"})),
11783 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
11784 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11785 lsp::InsertReplaceEdit {
11786 new_text: "abs".to_string(),
11787 insert: lsp::Range::default(),
11788 replace: lsp::Range::default(),
11789 },
11790 )),
11791 ..lsp::CompletionItem::default()
11792 };
11793 let items = iter::once(item_0.clone())
11794 .chain((11..51).map(|i| lsp::CompletionItem {
11795 label: format!("item_{}", i),
11796 insert_text: Some(format!("item_{}", i)),
11797 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
11798 ..lsp::CompletionItem::default()
11799 }))
11800 .collect::<Vec<_>>();
11801
11802 let default_commit_characters = vec!["?".to_string()];
11803 let default_data = json!({ "default": "data"});
11804 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
11805 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
11806 let default_edit_range = lsp::Range {
11807 start: lsp::Position {
11808 line: 0,
11809 character: 5,
11810 },
11811 end: lsp::Position {
11812 line: 0,
11813 character: 5,
11814 },
11815 };
11816
11817 let item_0_out = lsp::CompletionItem {
11818 commit_characters: Some(default_commit_characters.clone()),
11819 insert_text_format: Some(default_insert_text_format),
11820 ..item_0
11821 };
11822 let items_out = iter::once(item_0_out)
11823 .chain(items[1..].iter().map(|item| lsp::CompletionItem {
11824 commit_characters: Some(default_commit_characters.clone()),
11825 data: Some(default_data.clone()),
11826 insert_text_mode: Some(default_insert_text_mode),
11827 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11828 range: default_edit_range,
11829 new_text: item.label.clone(),
11830 })),
11831 ..item.clone()
11832 }))
11833 .collect::<Vec<lsp::CompletionItem>>();
11834
11835 let mut cx = EditorLspTestContext::new_rust(
11836 lsp::ServerCapabilities {
11837 completion_provider: Some(lsp::CompletionOptions {
11838 trigger_characters: Some(vec![".".to_string()]),
11839 resolve_provider: Some(true),
11840 ..Default::default()
11841 }),
11842 ..Default::default()
11843 },
11844 cx,
11845 )
11846 .await;
11847
11848 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11849 cx.simulate_keystroke(".");
11850
11851 let completion_data = default_data.clone();
11852 let completion_characters = default_commit_characters.clone();
11853 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11854 let default_data = completion_data.clone();
11855 let default_commit_characters = completion_characters.clone();
11856 let items = items.clone();
11857 async move {
11858 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
11859 items,
11860 item_defaults: Some(lsp::CompletionListItemDefaults {
11861 data: Some(default_data.clone()),
11862 commit_characters: Some(default_commit_characters.clone()),
11863 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
11864 default_edit_range,
11865 )),
11866 insert_text_format: Some(default_insert_text_format),
11867 insert_text_mode: Some(default_insert_text_mode),
11868 }),
11869 ..lsp::CompletionList::default()
11870 })))
11871 }
11872 })
11873 .next()
11874 .await;
11875
11876 let resolved_items = Arc::new(Mutex::new(Vec::new()));
11877 cx.lsp
11878 .server
11879 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
11880 let closure_resolved_items = resolved_items.clone();
11881 move |item_to_resolve, _| {
11882 let closure_resolved_items = closure_resolved_items.clone();
11883 async move {
11884 closure_resolved_items.lock().push(item_to_resolve.clone());
11885 Ok(item_to_resolve)
11886 }
11887 }
11888 })
11889 .detach();
11890
11891 cx.condition(|editor, _| editor.context_menu_visible())
11892 .await;
11893 cx.run_until_parked();
11894 cx.update_editor(|editor, _, _| {
11895 let menu = editor.context_menu.borrow_mut();
11896 match menu.as_ref().expect("should have the completions menu") {
11897 CodeContextMenu::Completions(completions_menu) => {
11898 assert_eq!(
11899 completions_menu
11900 .entries
11901 .borrow()
11902 .iter()
11903 .map(|mat| mat.string.clone())
11904 .collect::<Vec<String>>(),
11905 items_out
11906 .iter()
11907 .map(|completion| completion.label.clone())
11908 .collect::<Vec<String>>()
11909 );
11910 }
11911 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
11912 }
11913 });
11914 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
11915 // with 4 from the end.
11916 assert_eq!(
11917 *resolved_items.lock(),
11918 [
11919 &items_out[0..16],
11920 &items_out[items_out.len() - 4..items_out.len()]
11921 ]
11922 .concat()
11923 .iter()
11924 .cloned()
11925 .collect::<Vec<lsp::CompletionItem>>()
11926 );
11927 resolved_items.lock().clear();
11928
11929 cx.update_editor(|editor, window, cx| {
11930 editor.context_menu_prev(&ContextMenuPrev, window, cx);
11931 });
11932 cx.run_until_parked();
11933 // Completions that have already been resolved are skipped.
11934 assert_eq!(
11935 *resolved_items.lock(),
11936 items_out[items_out.len() - 16..items_out.len() - 4]
11937 .iter()
11938 .cloned()
11939 .collect::<Vec<lsp::CompletionItem>>()
11940 );
11941 resolved_items.lock().clear();
11942}
11943
11944#[gpui::test]
11945async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
11946 init_test(cx, |_| {});
11947
11948 let mut cx = EditorLspTestContext::new(
11949 Language::new(
11950 LanguageConfig {
11951 matcher: LanguageMatcher {
11952 path_suffixes: vec!["jsx".into()],
11953 ..Default::default()
11954 },
11955 overrides: [(
11956 "element".into(),
11957 LanguageConfigOverride {
11958 word_characters: Override::Set(['-'].into_iter().collect()),
11959 ..Default::default()
11960 },
11961 )]
11962 .into_iter()
11963 .collect(),
11964 ..Default::default()
11965 },
11966 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
11967 )
11968 .with_override_query("(jsx_self_closing_element) @element")
11969 .unwrap(),
11970 lsp::ServerCapabilities {
11971 completion_provider: Some(lsp::CompletionOptions {
11972 trigger_characters: Some(vec![":".to_string()]),
11973 ..Default::default()
11974 }),
11975 ..Default::default()
11976 },
11977 cx,
11978 )
11979 .await;
11980
11981 cx.lsp
11982 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11983 Ok(Some(lsp::CompletionResponse::Array(vec![
11984 lsp::CompletionItem {
11985 label: "bg-blue".into(),
11986 ..Default::default()
11987 },
11988 lsp::CompletionItem {
11989 label: "bg-red".into(),
11990 ..Default::default()
11991 },
11992 lsp::CompletionItem {
11993 label: "bg-yellow".into(),
11994 ..Default::default()
11995 },
11996 ])))
11997 });
11998
11999 cx.set_state(r#"<p class="bgˇ" />"#);
12000
12001 // Trigger completion when typing a dash, because the dash is an extra
12002 // word character in the 'element' scope, which contains the cursor.
12003 cx.simulate_keystroke("-");
12004 cx.executor().run_until_parked();
12005 cx.update_editor(|editor, _, _| {
12006 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12007 {
12008 assert_eq!(
12009 completion_menu_entries(&menu),
12010 &["bg-red", "bg-blue", "bg-yellow"]
12011 );
12012 } else {
12013 panic!("expected completion menu to be open");
12014 }
12015 });
12016
12017 cx.simulate_keystroke("l");
12018 cx.executor().run_until_parked();
12019 cx.update_editor(|editor, _, _| {
12020 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12021 {
12022 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
12023 } else {
12024 panic!("expected completion menu to be open");
12025 }
12026 });
12027
12028 // When filtering completions, consider the character after the '-' to
12029 // be the start of a subword.
12030 cx.set_state(r#"<p class="yelˇ" />"#);
12031 cx.simulate_keystroke("l");
12032 cx.executor().run_until_parked();
12033 cx.update_editor(|editor, _, _| {
12034 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12035 {
12036 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
12037 } else {
12038 panic!("expected completion menu to be open");
12039 }
12040 });
12041}
12042
12043fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
12044 let entries = menu.entries.borrow();
12045 entries.iter().map(|mat| mat.string.clone()).collect()
12046}
12047
12048#[gpui::test]
12049async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
12050 init_test(cx, |settings| {
12051 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
12052 FormatterList(vec![Formatter::Prettier].into()),
12053 ))
12054 });
12055
12056 let fs = FakeFs::new(cx.executor());
12057 fs.insert_file(path!("/file.ts"), Default::default()).await;
12058
12059 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
12060 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12061
12062 language_registry.add(Arc::new(Language::new(
12063 LanguageConfig {
12064 name: "TypeScript".into(),
12065 matcher: LanguageMatcher {
12066 path_suffixes: vec!["ts".to_string()],
12067 ..Default::default()
12068 },
12069 ..Default::default()
12070 },
12071 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12072 )));
12073 update_test_language_settings(cx, |settings| {
12074 settings.defaults.prettier = Some(PrettierSettings {
12075 allowed: true,
12076 ..PrettierSettings::default()
12077 });
12078 });
12079
12080 let test_plugin = "test_plugin";
12081 let _ = language_registry.register_fake_lsp(
12082 "TypeScript",
12083 FakeLspAdapter {
12084 prettier_plugins: vec![test_plugin],
12085 ..Default::default()
12086 },
12087 );
12088
12089 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
12090 let buffer = project
12091 .update(cx, |project, cx| {
12092 project.open_local_buffer(path!("/file.ts"), cx)
12093 })
12094 .await
12095 .unwrap();
12096
12097 let buffer_text = "one\ntwo\nthree\n";
12098 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12099 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12100 editor.update_in(cx, |editor, window, cx| {
12101 editor.set_text(buffer_text, window, cx)
12102 });
12103
12104 editor
12105 .update_in(cx, |editor, window, cx| {
12106 editor.perform_format(
12107 project.clone(),
12108 FormatTrigger::Manual,
12109 FormatTarget::Buffers,
12110 window,
12111 cx,
12112 )
12113 })
12114 .unwrap()
12115 .await;
12116 assert_eq!(
12117 editor.update(cx, |editor, cx| editor.text(cx)),
12118 buffer_text.to_string() + prettier_format_suffix,
12119 "Test prettier formatting was not applied to the original buffer text",
12120 );
12121
12122 update_test_language_settings(cx, |settings| {
12123 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
12124 });
12125 let format = editor.update_in(cx, |editor, window, cx| {
12126 editor.perform_format(
12127 project.clone(),
12128 FormatTrigger::Manual,
12129 FormatTarget::Buffers,
12130 window,
12131 cx,
12132 )
12133 });
12134 format.await.unwrap();
12135 assert_eq!(
12136 editor.update(cx, |editor, cx| editor.text(cx)),
12137 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
12138 "Autoformatting (via test prettier) was not applied to the original buffer text",
12139 );
12140}
12141
12142#[gpui::test]
12143async fn test_addition_reverts(cx: &mut gpui::TestAppContext) {
12144 init_test(cx, |_| {});
12145 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12146 let base_text = indoc! {r#"
12147 struct Row;
12148 struct Row1;
12149 struct Row2;
12150
12151 struct Row4;
12152 struct Row5;
12153 struct Row6;
12154
12155 struct Row8;
12156 struct Row9;
12157 struct Row10;"#};
12158
12159 // When addition hunks are not adjacent to carets, no hunk revert is performed
12160 assert_hunk_revert(
12161 indoc! {r#"struct Row;
12162 struct Row1;
12163 struct Row1.1;
12164 struct Row1.2;
12165 struct Row2;ˇ
12166
12167 struct Row4;
12168 struct Row5;
12169 struct Row6;
12170
12171 struct Row8;
12172 ˇstruct Row9;
12173 struct Row9.1;
12174 struct Row9.2;
12175 struct Row9.3;
12176 struct Row10;"#},
12177 vec![DiffHunkStatus::added(), DiffHunkStatus::added()],
12178 indoc! {r#"struct Row;
12179 struct Row1;
12180 struct Row1.1;
12181 struct Row1.2;
12182 struct Row2;ˇ
12183
12184 struct Row4;
12185 struct Row5;
12186 struct Row6;
12187
12188 struct Row8;
12189 ˇstruct Row9;
12190 struct Row9.1;
12191 struct Row9.2;
12192 struct Row9.3;
12193 struct Row10;"#},
12194 base_text,
12195 &mut cx,
12196 );
12197 // Same for selections
12198 assert_hunk_revert(
12199 indoc! {r#"struct Row;
12200 struct Row1;
12201 struct Row2;
12202 struct Row2.1;
12203 struct Row2.2;
12204 «ˇ
12205 struct Row4;
12206 struct» Row5;
12207 «struct Row6;
12208 ˇ»
12209 struct Row9.1;
12210 struct Row9.2;
12211 struct Row9.3;
12212 struct Row8;
12213 struct Row9;
12214 struct Row10;"#},
12215 vec![DiffHunkStatus::added(), DiffHunkStatus::added()],
12216 indoc! {r#"struct Row;
12217 struct Row1;
12218 struct Row2;
12219 struct Row2.1;
12220 struct Row2.2;
12221 «ˇ
12222 struct Row4;
12223 struct» Row5;
12224 «struct Row6;
12225 ˇ»
12226 struct Row9.1;
12227 struct Row9.2;
12228 struct Row9.3;
12229 struct Row8;
12230 struct Row9;
12231 struct Row10;"#},
12232 base_text,
12233 &mut cx,
12234 );
12235
12236 // When carets and selections intersect the addition hunks, those are reverted.
12237 // Adjacent carets got merged.
12238 assert_hunk_revert(
12239 indoc! {r#"struct Row;
12240 ˇ// something on the top
12241 struct Row1;
12242 struct Row2;
12243 struct Roˇw3.1;
12244 struct Row2.2;
12245 struct Row2.3;ˇ
12246
12247 struct Row4;
12248 struct ˇRow5.1;
12249 struct Row5.2;
12250 struct «Rowˇ»5.3;
12251 struct Row5;
12252 struct Row6;
12253 ˇ
12254 struct Row9.1;
12255 struct «Rowˇ»9.2;
12256 struct «ˇRow»9.3;
12257 struct Row8;
12258 struct Row9;
12259 «ˇ// something on bottom»
12260 struct Row10;"#},
12261 vec![
12262 DiffHunkStatus::added(),
12263 DiffHunkStatus::added(),
12264 DiffHunkStatus::added(),
12265 DiffHunkStatus::added(),
12266 DiffHunkStatus::added(),
12267 ],
12268 indoc! {r#"struct Row;
12269 ˇstruct Row1;
12270 struct Row2;
12271 ˇ
12272 struct Row4;
12273 ˇstruct Row5;
12274 struct Row6;
12275 ˇ
12276 ˇstruct Row8;
12277 struct Row9;
12278 ˇstruct Row10;"#},
12279 base_text,
12280 &mut cx,
12281 );
12282}
12283
12284#[gpui::test]
12285async fn test_modification_reverts(cx: &mut gpui::TestAppContext) {
12286 init_test(cx, |_| {});
12287 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12288 let base_text = indoc! {r#"
12289 struct Row;
12290 struct Row1;
12291 struct Row2;
12292
12293 struct Row4;
12294 struct Row5;
12295 struct Row6;
12296
12297 struct Row8;
12298 struct Row9;
12299 struct Row10;"#};
12300
12301 // Modification hunks behave the same as the addition ones.
12302 assert_hunk_revert(
12303 indoc! {r#"struct Row;
12304 struct Row1;
12305 struct Row33;
12306 ˇ
12307 struct Row4;
12308 struct Row5;
12309 struct Row6;
12310 ˇ
12311 struct Row99;
12312 struct Row9;
12313 struct Row10;"#},
12314 vec![DiffHunkStatus::modified(), DiffHunkStatus::modified()],
12315 indoc! {r#"struct Row;
12316 struct Row1;
12317 struct Row33;
12318 ˇ
12319 struct Row4;
12320 struct Row5;
12321 struct Row6;
12322 ˇ
12323 struct Row99;
12324 struct Row9;
12325 struct Row10;"#},
12326 base_text,
12327 &mut cx,
12328 );
12329 assert_hunk_revert(
12330 indoc! {r#"struct Row;
12331 struct Row1;
12332 struct Row33;
12333 «ˇ
12334 struct Row4;
12335 struct» Row5;
12336 «struct Row6;
12337 ˇ»
12338 struct Row99;
12339 struct Row9;
12340 struct Row10;"#},
12341 vec![DiffHunkStatus::modified(), DiffHunkStatus::modified()],
12342 indoc! {r#"struct Row;
12343 struct Row1;
12344 struct Row33;
12345 «ˇ
12346 struct Row4;
12347 struct» Row5;
12348 «struct Row6;
12349 ˇ»
12350 struct Row99;
12351 struct Row9;
12352 struct Row10;"#},
12353 base_text,
12354 &mut cx,
12355 );
12356
12357 assert_hunk_revert(
12358 indoc! {r#"ˇstruct Row1.1;
12359 struct Row1;
12360 «ˇstr»uct Row22;
12361
12362 struct ˇRow44;
12363 struct Row5;
12364 struct «Rˇ»ow66;ˇ
12365
12366 «struˇ»ct Row88;
12367 struct Row9;
12368 struct Row1011;ˇ"#},
12369 vec![
12370 DiffHunkStatus::modified(),
12371 DiffHunkStatus::modified(),
12372 DiffHunkStatus::modified(),
12373 DiffHunkStatus::modified(),
12374 DiffHunkStatus::modified(),
12375 DiffHunkStatus::modified(),
12376 ],
12377 indoc! {r#"struct Row;
12378 ˇstruct Row1;
12379 struct Row2;
12380 ˇ
12381 struct Row4;
12382 ˇstruct Row5;
12383 struct Row6;
12384 ˇ
12385 struct Row8;
12386 ˇstruct Row9;
12387 struct Row10;ˇ"#},
12388 base_text,
12389 &mut cx,
12390 );
12391}
12392
12393#[gpui::test]
12394async fn test_deleting_over_diff_hunk(cx: &mut gpui::TestAppContext) {
12395 init_test(cx, |_| {});
12396 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12397 let base_text = indoc! {r#"
12398 one
12399
12400 two
12401 three
12402 "#};
12403
12404 cx.set_diff_base(base_text);
12405 cx.set_state("\nˇ\n");
12406 cx.executor().run_until_parked();
12407 cx.update_editor(|editor, _window, cx| {
12408 editor.expand_selected_diff_hunks(cx);
12409 });
12410 cx.executor().run_until_parked();
12411 cx.update_editor(|editor, window, cx| {
12412 editor.backspace(&Default::default(), window, cx);
12413 });
12414 cx.run_until_parked();
12415 cx.assert_state_with_diff(
12416 indoc! {r#"
12417
12418 - two
12419 - threeˇ
12420 +
12421 "#}
12422 .to_string(),
12423 );
12424}
12425
12426#[gpui::test]
12427async fn test_deletion_reverts(cx: &mut gpui::TestAppContext) {
12428 init_test(cx, |_| {});
12429 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12430 let base_text = indoc! {r#"struct Row;
12431struct Row1;
12432struct Row2;
12433
12434struct Row4;
12435struct Row5;
12436struct Row6;
12437
12438struct Row8;
12439struct Row9;
12440struct Row10;"#};
12441
12442 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
12443 assert_hunk_revert(
12444 indoc! {r#"struct Row;
12445 struct Row2;
12446
12447 ˇstruct Row4;
12448 struct Row5;
12449 struct Row6;
12450 ˇ
12451 struct Row8;
12452 struct Row10;"#},
12453 vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()],
12454 indoc! {r#"struct Row;
12455 struct Row2;
12456
12457 ˇstruct Row4;
12458 struct Row5;
12459 struct Row6;
12460 ˇ
12461 struct Row8;
12462 struct Row10;"#},
12463 base_text,
12464 &mut cx,
12465 );
12466 assert_hunk_revert(
12467 indoc! {r#"struct Row;
12468 struct Row2;
12469
12470 «ˇstruct Row4;
12471 struct» Row5;
12472 «struct Row6;
12473 ˇ»
12474 struct Row8;
12475 struct Row10;"#},
12476 vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()],
12477 indoc! {r#"struct Row;
12478 struct Row2;
12479
12480 «ˇstruct Row4;
12481 struct» Row5;
12482 «struct Row6;
12483 ˇ»
12484 struct Row8;
12485 struct Row10;"#},
12486 base_text,
12487 &mut cx,
12488 );
12489
12490 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
12491 assert_hunk_revert(
12492 indoc! {r#"struct Row;
12493 ˇstruct Row2;
12494
12495 struct Row4;
12496 struct Row5;
12497 struct Row6;
12498
12499 struct Row8;ˇ
12500 struct Row10;"#},
12501 vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()],
12502 indoc! {r#"struct Row;
12503 struct Row1;
12504 ˇstruct Row2;
12505
12506 struct Row4;
12507 struct Row5;
12508 struct Row6;
12509
12510 struct Row8;ˇ
12511 struct Row9;
12512 struct Row10;"#},
12513 base_text,
12514 &mut cx,
12515 );
12516 assert_hunk_revert(
12517 indoc! {r#"struct Row;
12518 struct Row2«ˇ;
12519 struct Row4;
12520 struct» Row5;
12521 «struct Row6;
12522
12523 struct Row8;ˇ»
12524 struct Row10;"#},
12525 vec![
12526 DiffHunkStatus::removed(),
12527 DiffHunkStatus::removed(),
12528 DiffHunkStatus::removed(),
12529 ],
12530 indoc! {r#"struct Row;
12531 struct Row1;
12532 struct Row2«ˇ;
12533
12534 struct Row4;
12535 struct» Row5;
12536 «struct Row6;
12537
12538 struct Row8;ˇ»
12539 struct Row9;
12540 struct Row10;"#},
12541 base_text,
12542 &mut cx,
12543 );
12544}
12545
12546#[gpui::test]
12547async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
12548 init_test(cx, |_| {});
12549
12550 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
12551 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
12552 let base_text_3 =
12553 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
12554
12555 let text_1 = edit_first_char_of_every_line(base_text_1);
12556 let text_2 = edit_first_char_of_every_line(base_text_2);
12557 let text_3 = edit_first_char_of_every_line(base_text_3);
12558
12559 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
12560 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
12561 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
12562
12563 let multibuffer = cx.new(|cx| {
12564 let mut multibuffer = MultiBuffer::new(ReadWrite);
12565 multibuffer.push_excerpts(
12566 buffer_1.clone(),
12567 [
12568 ExcerptRange {
12569 context: Point::new(0, 0)..Point::new(3, 0),
12570 primary: None,
12571 },
12572 ExcerptRange {
12573 context: Point::new(5, 0)..Point::new(7, 0),
12574 primary: None,
12575 },
12576 ExcerptRange {
12577 context: Point::new(9, 0)..Point::new(10, 4),
12578 primary: None,
12579 },
12580 ],
12581 cx,
12582 );
12583 multibuffer.push_excerpts(
12584 buffer_2.clone(),
12585 [
12586 ExcerptRange {
12587 context: Point::new(0, 0)..Point::new(3, 0),
12588 primary: None,
12589 },
12590 ExcerptRange {
12591 context: Point::new(5, 0)..Point::new(7, 0),
12592 primary: None,
12593 },
12594 ExcerptRange {
12595 context: Point::new(9, 0)..Point::new(10, 4),
12596 primary: None,
12597 },
12598 ],
12599 cx,
12600 );
12601 multibuffer.push_excerpts(
12602 buffer_3.clone(),
12603 [
12604 ExcerptRange {
12605 context: Point::new(0, 0)..Point::new(3, 0),
12606 primary: None,
12607 },
12608 ExcerptRange {
12609 context: Point::new(5, 0)..Point::new(7, 0),
12610 primary: None,
12611 },
12612 ExcerptRange {
12613 context: Point::new(9, 0)..Point::new(10, 4),
12614 primary: None,
12615 },
12616 ],
12617 cx,
12618 );
12619 multibuffer
12620 });
12621
12622 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12623 editor.update_in(cx, |editor, _window, cx| {
12624 for (buffer, diff_base) in [
12625 (buffer_1.clone(), base_text_1),
12626 (buffer_2.clone(), base_text_2),
12627 (buffer_3.clone(), base_text_3),
12628 ] {
12629 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
12630 editor
12631 .buffer
12632 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
12633 }
12634 });
12635 cx.executor().run_until_parked();
12636
12637 editor.update_in(cx, |editor, window, cx| {
12638 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}");
12639 editor.select_all(&SelectAll, window, cx);
12640 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
12641 });
12642 cx.executor().run_until_parked();
12643
12644 // When all ranges are selected, all buffer hunks are reverted.
12645 editor.update(cx, |editor, cx| {
12646 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");
12647 });
12648 buffer_1.update(cx, |buffer, _| {
12649 assert_eq!(buffer.text(), base_text_1);
12650 });
12651 buffer_2.update(cx, |buffer, _| {
12652 assert_eq!(buffer.text(), base_text_2);
12653 });
12654 buffer_3.update(cx, |buffer, _| {
12655 assert_eq!(buffer.text(), base_text_3);
12656 });
12657
12658 editor.update_in(cx, |editor, window, cx| {
12659 editor.undo(&Default::default(), window, cx);
12660 });
12661
12662 editor.update_in(cx, |editor, window, cx| {
12663 editor.change_selections(None, window, cx, |s| {
12664 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
12665 });
12666 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
12667 });
12668
12669 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
12670 // but not affect buffer_2 and its related excerpts.
12671 editor.update(cx, |editor, cx| {
12672 assert_eq!(
12673 editor.text(cx),
12674 "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}"
12675 );
12676 });
12677 buffer_1.update(cx, |buffer, _| {
12678 assert_eq!(buffer.text(), base_text_1);
12679 });
12680 buffer_2.update(cx, |buffer, _| {
12681 assert_eq!(
12682 buffer.text(),
12683 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
12684 );
12685 });
12686 buffer_3.update(cx, |buffer, _| {
12687 assert_eq!(
12688 buffer.text(),
12689 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
12690 );
12691 });
12692
12693 fn edit_first_char_of_every_line(text: &str) -> String {
12694 text.split('\n')
12695 .map(|line| format!("X{}", &line[1..]))
12696 .collect::<Vec<_>>()
12697 .join("\n")
12698 }
12699}
12700
12701#[gpui::test]
12702async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
12703 init_test(cx, |_| {});
12704
12705 let cols = 4;
12706 let rows = 10;
12707 let sample_text_1 = sample_text(rows, cols, 'a');
12708 assert_eq!(
12709 sample_text_1,
12710 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
12711 );
12712 let sample_text_2 = sample_text(rows, cols, 'l');
12713 assert_eq!(
12714 sample_text_2,
12715 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
12716 );
12717 let sample_text_3 = sample_text(rows, cols, 'v');
12718 assert_eq!(
12719 sample_text_3,
12720 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
12721 );
12722
12723 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
12724 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
12725 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
12726
12727 let multi_buffer = cx.new(|cx| {
12728 let mut multibuffer = MultiBuffer::new(ReadWrite);
12729 multibuffer.push_excerpts(
12730 buffer_1.clone(),
12731 [
12732 ExcerptRange {
12733 context: Point::new(0, 0)..Point::new(3, 0),
12734 primary: None,
12735 },
12736 ExcerptRange {
12737 context: Point::new(5, 0)..Point::new(7, 0),
12738 primary: None,
12739 },
12740 ExcerptRange {
12741 context: Point::new(9, 0)..Point::new(10, 4),
12742 primary: None,
12743 },
12744 ],
12745 cx,
12746 );
12747 multibuffer.push_excerpts(
12748 buffer_2.clone(),
12749 [
12750 ExcerptRange {
12751 context: Point::new(0, 0)..Point::new(3, 0),
12752 primary: None,
12753 },
12754 ExcerptRange {
12755 context: Point::new(5, 0)..Point::new(7, 0),
12756 primary: None,
12757 },
12758 ExcerptRange {
12759 context: Point::new(9, 0)..Point::new(10, 4),
12760 primary: None,
12761 },
12762 ],
12763 cx,
12764 );
12765 multibuffer.push_excerpts(
12766 buffer_3.clone(),
12767 [
12768 ExcerptRange {
12769 context: Point::new(0, 0)..Point::new(3, 0),
12770 primary: None,
12771 },
12772 ExcerptRange {
12773 context: Point::new(5, 0)..Point::new(7, 0),
12774 primary: None,
12775 },
12776 ExcerptRange {
12777 context: Point::new(9, 0)..Point::new(10, 4),
12778 primary: None,
12779 },
12780 ],
12781 cx,
12782 );
12783 multibuffer
12784 });
12785
12786 let fs = FakeFs::new(cx.executor());
12787 fs.insert_tree(
12788 "/a",
12789 json!({
12790 "main.rs": sample_text_1,
12791 "other.rs": sample_text_2,
12792 "lib.rs": sample_text_3,
12793 }),
12794 )
12795 .await;
12796 let project = Project::test(fs, ["/a".as_ref()], cx).await;
12797 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12798 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12799 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
12800 Editor::new(
12801 EditorMode::Full,
12802 multi_buffer,
12803 Some(project.clone()),
12804 true,
12805 window,
12806 cx,
12807 )
12808 });
12809 let multibuffer_item_id = workspace
12810 .update(cx, |workspace, window, cx| {
12811 assert!(
12812 workspace.active_item(cx).is_none(),
12813 "active item should be None before the first item is added"
12814 );
12815 workspace.add_item_to_active_pane(
12816 Box::new(multi_buffer_editor.clone()),
12817 None,
12818 true,
12819 window,
12820 cx,
12821 );
12822 let active_item = workspace
12823 .active_item(cx)
12824 .expect("should have an active item after adding the multi buffer");
12825 assert!(
12826 !active_item.is_singleton(cx),
12827 "A multi buffer was expected to active after adding"
12828 );
12829 active_item.item_id()
12830 })
12831 .unwrap();
12832 cx.executor().run_until_parked();
12833
12834 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12835 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12836 s.select_ranges(Some(1..2))
12837 });
12838 editor.open_excerpts(&OpenExcerpts, window, cx);
12839 });
12840 cx.executor().run_until_parked();
12841 let first_item_id = workspace
12842 .update(cx, |workspace, window, cx| {
12843 let active_item = workspace
12844 .active_item(cx)
12845 .expect("should have an active item after navigating into the 1st buffer");
12846 let first_item_id = active_item.item_id();
12847 assert_ne!(
12848 first_item_id, multibuffer_item_id,
12849 "Should navigate into the 1st buffer and activate it"
12850 );
12851 assert!(
12852 active_item.is_singleton(cx),
12853 "New active item should be a singleton buffer"
12854 );
12855 assert_eq!(
12856 active_item
12857 .act_as::<Editor>(cx)
12858 .expect("should have navigated into an editor for the 1st buffer")
12859 .read(cx)
12860 .text(cx),
12861 sample_text_1
12862 );
12863
12864 workspace
12865 .go_back(workspace.active_pane().downgrade(), window, cx)
12866 .detach_and_log_err(cx);
12867
12868 first_item_id
12869 })
12870 .unwrap();
12871 cx.executor().run_until_parked();
12872 workspace
12873 .update(cx, |workspace, _, cx| {
12874 let active_item = workspace
12875 .active_item(cx)
12876 .expect("should have an active item after navigating back");
12877 assert_eq!(
12878 active_item.item_id(),
12879 multibuffer_item_id,
12880 "Should navigate back to the multi buffer"
12881 );
12882 assert!(!active_item.is_singleton(cx));
12883 })
12884 .unwrap();
12885
12886 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12887 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12888 s.select_ranges(Some(39..40))
12889 });
12890 editor.open_excerpts(&OpenExcerpts, window, cx);
12891 });
12892 cx.executor().run_until_parked();
12893 let second_item_id = workspace
12894 .update(cx, |workspace, window, cx| {
12895 let active_item = workspace
12896 .active_item(cx)
12897 .expect("should have an active item after navigating into the 2nd buffer");
12898 let second_item_id = active_item.item_id();
12899 assert_ne!(
12900 second_item_id, multibuffer_item_id,
12901 "Should navigate away from the multibuffer"
12902 );
12903 assert_ne!(
12904 second_item_id, first_item_id,
12905 "Should navigate into the 2nd buffer and activate it"
12906 );
12907 assert!(
12908 active_item.is_singleton(cx),
12909 "New active item should be a singleton buffer"
12910 );
12911 assert_eq!(
12912 active_item
12913 .act_as::<Editor>(cx)
12914 .expect("should have navigated into an editor")
12915 .read(cx)
12916 .text(cx),
12917 sample_text_2
12918 );
12919
12920 workspace
12921 .go_back(workspace.active_pane().downgrade(), window, cx)
12922 .detach_and_log_err(cx);
12923
12924 second_item_id
12925 })
12926 .unwrap();
12927 cx.executor().run_until_parked();
12928 workspace
12929 .update(cx, |workspace, _, cx| {
12930 let active_item = workspace
12931 .active_item(cx)
12932 .expect("should have an active item after navigating back from the 2nd buffer");
12933 assert_eq!(
12934 active_item.item_id(),
12935 multibuffer_item_id,
12936 "Should navigate back from the 2nd buffer to the multi buffer"
12937 );
12938 assert!(!active_item.is_singleton(cx));
12939 })
12940 .unwrap();
12941
12942 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12943 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12944 s.select_ranges(Some(70..70))
12945 });
12946 editor.open_excerpts(&OpenExcerpts, window, cx);
12947 });
12948 cx.executor().run_until_parked();
12949 workspace
12950 .update(cx, |workspace, window, cx| {
12951 let active_item = workspace
12952 .active_item(cx)
12953 .expect("should have an active item after navigating into the 3rd buffer");
12954 let third_item_id = active_item.item_id();
12955 assert_ne!(
12956 third_item_id, multibuffer_item_id,
12957 "Should navigate into the 3rd buffer and activate it"
12958 );
12959 assert_ne!(third_item_id, first_item_id);
12960 assert_ne!(third_item_id, second_item_id);
12961 assert!(
12962 active_item.is_singleton(cx),
12963 "New active item should be a singleton buffer"
12964 );
12965 assert_eq!(
12966 active_item
12967 .act_as::<Editor>(cx)
12968 .expect("should have navigated into an editor")
12969 .read(cx)
12970 .text(cx),
12971 sample_text_3
12972 );
12973
12974 workspace
12975 .go_back(workspace.active_pane().downgrade(), window, cx)
12976 .detach_and_log_err(cx);
12977 })
12978 .unwrap();
12979 cx.executor().run_until_parked();
12980 workspace
12981 .update(cx, |workspace, _, cx| {
12982 let active_item = workspace
12983 .active_item(cx)
12984 .expect("should have an active item after navigating back from the 3rd buffer");
12985 assert_eq!(
12986 active_item.item_id(),
12987 multibuffer_item_id,
12988 "Should navigate back from the 3rd buffer to the multi buffer"
12989 );
12990 assert!(!active_item.is_singleton(cx));
12991 })
12992 .unwrap();
12993}
12994
12995#[gpui::test]
12996async fn test_toggle_selected_diff_hunks(
12997 executor: BackgroundExecutor,
12998 cx: &mut gpui::TestAppContext,
12999) {
13000 init_test(cx, |_| {});
13001
13002 let mut cx = EditorTestContext::new(cx).await;
13003
13004 let diff_base = r#"
13005 use some::mod;
13006
13007 const A: u32 = 42;
13008
13009 fn main() {
13010 println!("hello");
13011
13012 println!("world");
13013 }
13014 "#
13015 .unindent();
13016
13017 cx.set_state(
13018 &r#"
13019 use some::modified;
13020
13021 ˇ
13022 fn main() {
13023 println!("hello there");
13024
13025 println!("around the");
13026 println!("world");
13027 }
13028 "#
13029 .unindent(),
13030 );
13031
13032 cx.set_diff_base(&diff_base);
13033 executor.run_until_parked();
13034
13035 cx.update_editor(|editor, window, cx| {
13036 editor.go_to_next_hunk(&GoToHunk, window, cx);
13037 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13038 });
13039 executor.run_until_parked();
13040 cx.assert_state_with_diff(
13041 r#"
13042 use some::modified;
13043
13044
13045 fn main() {
13046 - println!("hello");
13047 + ˇ println!("hello there");
13048
13049 println!("around the");
13050 println!("world");
13051 }
13052 "#
13053 .unindent(),
13054 );
13055
13056 cx.update_editor(|editor, window, cx| {
13057 for _ in 0..2 {
13058 editor.go_to_next_hunk(&GoToHunk, window, cx);
13059 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13060 }
13061 });
13062 executor.run_until_parked();
13063 cx.assert_state_with_diff(
13064 r#"
13065 - use some::mod;
13066 + ˇuse some::modified;
13067
13068
13069 fn main() {
13070 - println!("hello");
13071 + println!("hello there");
13072
13073 + println!("around the");
13074 println!("world");
13075 }
13076 "#
13077 .unindent(),
13078 );
13079
13080 cx.update_editor(|editor, window, cx| {
13081 editor.go_to_next_hunk(&GoToHunk, window, cx);
13082 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13083 });
13084 executor.run_until_parked();
13085 cx.assert_state_with_diff(
13086 r#"
13087 - use some::mod;
13088 + use some::modified;
13089
13090 - const A: u32 = 42;
13091 ˇ
13092 fn main() {
13093 - println!("hello");
13094 + println!("hello there");
13095
13096 + println!("around the");
13097 println!("world");
13098 }
13099 "#
13100 .unindent(),
13101 );
13102
13103 cx.update_editor(|editor, window, cx| {
13104 editor.cancel(&Cancel, window, cx);
13105 });
13106
13107 cx.assert_state_with_diff(
13108 r#"
13109 use some::modified;
13110
13111 ˇ
13112 fn main() {
13113 println!("hello there");
13114
13115 println!("around the");
13116 println!("world");
13117 }
13118 "#
13119 .unindent(),
13120 );
13121}
13122
13123#[gpui::test]
13124async fn test_diff_base_change_with_expanded_diff_hunks(
13125 executor: BackgroundExecutor,
13126 cx: &mut gpui::TestAppContext,
13127) {
13128 init_test(cx, |_| {});
13129
13130 let mut cx = EditorTestContext::new(cx).await;
13131
13132 let diff_base = r#"
13133 use some::mod1;
13134 use some::mod2;
13135
13136 const A: u32 = 42;
13137 const B: u32 = 42;
13138 const C: u32 = 42;
13139
13140 fn main() {
13141 println!("hello");
13142
13143 println!("world");
13144 }
13145 "#
13146 .unindent();
13147
13148 cx.set_state(
13149 &r#"
13150 use some::mod2;
13151
13152 const A: u32 = 42;
13153 const C: u32 = 42;
13154
13155 fn main(ˇ) {
13156 //println!("hello");
13157
13158 println!("world");
13159 //
13160 //
13161 }
13162 "#
13163 .unindent(),
13164 );
13165
13166 cx.set_diff_base(&diff_base);
13167 executor.run_until_parked();
13168
13169 cx.update_editor(|editor, window, cx| {
13170 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13171 });
13172 executor.run_until_parked();
13173 cx.assert_state_with_diff(
13174 r#"
13175 - use some::mod1;
13176 use some::mod2;
13177
13178 const A: u32 = 42;
13179 - const B: u32 = 42;
13180 const C: u32 = 42;
13181
13182 fn main(ˇ) {
13183 - println!("hello");
13184 + //println!("hello");
13185
13186 println!("world");
13187 + //
13188 + //
13189 }
13190 "#
13191 .unindent(),
13192 );
13193
13194 cx.set_diff_base("new diff base!");
13195 executor.run_until_parked();
13196 cx.assert_state_with_diff(
13197 r#"
13198 use some::mod2;
13199
13200 const A: u32 = 42;
13201 const C: u32 = 42;
13202
13203 fn main(ˇ) {
13204 //println!("hello");
13205
13206 println!("world");
13207 //
13208 //
13209 }
13210 "#
13211 .unindent(),
13212 );
13213
13214 cx.update_editor(|editor, window, cx| {
13215 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13216 });
13217 executor.run_until_parked();
13218 cx.assert_state_with_diff(
13219 r#"
13220 - new diff base!
13221 + use some::mod2;
13222 +
13223 + const A: u32 = 42;
13224 + const C: u32 = 42;
13225 +
13226 + fn main(ˇ) {
13227 + //println!("hello");
13228 +
13229 + println!("world");
13230 + //
13231 + //
13232 + }
13233 "#
13234 .unindent(),
13235 );
13236}
13237
13238#[gpui::test]
13239async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) {
13240 init_test(cx, |_| {});
13241
13242 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
13243 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
13244 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
13245 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
13246 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
13247 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
13248
13249 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
13250 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
13251 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
13252
13253 let multi_buffer = cx.new(|cx| {
13254 let mut multibuffer = MultiBuffer::new(ReadWrite);
13255 multibuffer.push_excerpts(
13256 buffer_1.clone(),
13257 [
13258 ExcerptRange {
13259 context: Point::new(0, 0)..Point::new(3, 0),
13260 primary: None,
13261 },
13262 ExcerptRange {
13263 context: Point::new(5, 0)..Point::new(7, 0),
13264 primary: None,
13265 },
13266 ExcerptRange {
13267 context: Point::new(9, 0)..Point::new(10, 3),
13268 primary: None,
13269 },
13270 ],
13271 cx,
13272 );
13273 multibuffer.push_excerpts(
13274 buffer_2.clone(),
13275 [
13276 ExcerptRange {
13277 context: Point::new(0, 0)..Point::new(3, 0),
13278 primary: None,
13279 },
13280 ExcerptRange {
13281 context: Point::new(5, 0)..Point::new(7, 0),
13282 primary: None,
13283 },
13284 ExcerptRange {
13285 context: Point::new(9, 0)..Point::new(10, 3),
13286 primary: None,
13287 },
13288 ],
13289 cx,
13290 );
13291 multibuffer.push_excerpts(
13292 buffer_3.clone(),
13293 [
13294 ExcerptRange {
13295 context: Point::new(0, 0)..Point::new(3, 0),
13296 primary: None,
13297 },
13298 ExcerptRange {
13299 context: Point::new(5, 0)..Point::new(7, 0),
13300 primary: None,
13301 },
13302 ExcerptRange {
13303 context: Point::new(9, 0)..Point::new(10, 3),
13304 primary: None,
13305 },
13306 ],
13307 cx,
13308 );
13309 multibuffer
13310 });
13311
13312 let editor = cx.add_window(|window, cx| {
13313 Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
13314 });
13315 editor
13316 .update(cx, |editor, _window, cx| {
13317 for (buffer, diff_base) in [
13318 (buffer_1.clone(), file_1_old),
13319 (buffer_2.clone(), file_2_old),
13320 (buffer_3.clone(), file_3_old),
13321 ] {
13322 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
13323 editor
13324 .buffer
13325 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
13326 }
13327 })
13328 .unwrap();
13329
13330 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13331 cx.run_until_parked();
13332
13333 cx.assert_editor_state(
13334 &"
13335 ˇaaa
13336 ccc
13337 ddd
13338
13339 ggg
13340 hhh
13341
13342
13343 lll
13344 mmm
13345 NNN
13346
13347 qqq
13348 rrr
13349
13350 uuu
13351 111
13352 222
13353 333
13354
13355 666
13356 777
13357
13358 000
13359 !!!"
13360 .unindent(),
13361 );
13362
13363 cx.update_editor(|editor, window, cx| {
13364 editor.select_all(&SelectAll, window, cx);
13365 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13366 });
13367 cx.executor().run_until_parked();
13368
13369 cx.assert_state_with_diff(
13370 "
13371 «aaa
13372 - bbb
13373 ccc
13374 ddd
13375
13376 ggg
13377 hhh
13378
13379
13380 lll
13381 mmm
13382 - nnn
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
13402#[gpui::test]
13403async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext) {
13404 init_test(cx, |_| {});
13405
13406 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
13407 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
13408
13409 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
13410 let multi_buffer = cx.new(|cx| {
13411 let mut multibuffer = MultiBuffer::new(ReadWrite);
13412 multibuffer.push_excerpts(
13413 buffer.clone(),
13414 [
13415 ExcerptRange {
13416 context: Point::new(0, 0)..Point::new(2, 0),
13417 primary: None,
13418 },
13419 ExcerptRange {
13420 context: Point::new(4, 0)..Point::new(7, 0),
13421 primary: None,
13422 },
13423 ExcerptRange {
13424 context: Point::new(9, 0)..Point::new(10, 0),
13425 primary: None,
13426 },
13427 ],
13428 cx,
13429 );
13430 multibuffer
13431 });
13432
13433 let editor = cx.add_window(|window, cx| {
13434 Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
13435 });
13436 editor
13437 .update(cx, |editor, _window, cx| {
13438 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
13439 editor
13440 .buffer
13441 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
13442 })
13443 .unwrap();
13444
13445 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13446 cx.run_until_parked();
13447
13448 cx.update_editor(|editor, window, cx| {
13449 editor.expand_all_diff_hunks(&Default::default(), window, cx)
13450 });
13451 cx.executor().run_until_parked();
13452
13453 // When the start of a hunk coincides with the start of its excerpt,
13454 // the hunk is expanded. When the start of a a hunk is earlier than
13455 // the start of its excerpt, the hunk is not expanded.
13456 cx.assert_state_with_diff(
13457 "
13458 ˇaaa
13459 - bbb
13460 + BBB
13461
13462 - ddd
13463 - eee
13464 + DDD
13465 + EEE
13466 fff
13467
13468 iii
13469 "
13470 .unindent(),
13471 );
13472}
13473
13474#[gpui::test]
13475async fn test_edits_around_expanded_insertion_hunks(
13476 executor: BackgroundExecutor,
13477 cx: &mut gpui::TestAppContext,
13478) {
13479 init_test(cx, |_| {});
13480
13481 let mut cx = EditorTestContext::new(cx).await;
13482
13483 let diff_base = r#"
13484 use some::mod1;
13485 use some::mod2;
13486
13487 const A: u32 = 42;
13488
13489 fn main() {
13490 println!("hello");
13491
13492 println!("world");
13493 }
13494 "#
13495 .unindent();
13496 executor.run_until_parked();
13497 cx.set_state(
13498 &r#"
13499 use some::mod1;
13500 use some::mod2;
13501
13502 const A: u32 = 42;
13503 const B: u32 = 42;
13504 const C: u32 = 42;
13505 ˇ
13506
13507 fn main() {
13508 println!("hello");
13509
13510 println!("world");
13511 }
13512 "#
13513 .unindent(),
13514 );
13515
13516 cx.set_diff_base(&diff_base);
13517 executor.run_until_parked();
13518
13519 cx.update_editor(|editor, window, cx| {
13520 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13521 });
13522 executor.run_until_parked();
13523
13524 cx.assert_state_with_diff(
13525 r#"
13526 use some::mod1;
13527 use some::mod2;
13528
13529 const A: u32 = 42;
13530 + const B: u32 = 42;
13531 + const C: u32 = 42;
13532 + ˇ
13533
13534 fn main() {
13535 println!("hello");
13536
13537 println!("world");
13538 }
13539 "#
13540 .unindent(),
13541 );
13542
13543 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
13544 executor.run_until_parked();
13545
13546 cx.assert_state_with_diff(
13547 r#"
13548 use some::mod1;
13549 use some::mod2;
13550
13551 const A: u32 = 42;
13552 + const B: u32 = 42;
13553 + const C: u32 = 42;
13554 + const D: u32 = 42;
13555 + ˇ
13556
13557 fn main() {
13558 println!("hello");
13559
13560 println!("world");
13561 }
13562 "#
13563 .unindent(),
13564 );
13565
13566 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
13567 executor.run_until_parked();
13568
13569 cx.assert_state_with_diff(
13570 r#"
13571 use some::mod1;
13572 use some::mod2;
13573
13574 const A: u32 = 42;
13575 + const B: u32 = 42;
13576 + const C: u32 = 42;
13577 + const D: u32 = 42;
13578 + const E: u32 = 42;
13579 + ˇ
13580
13581 fn main() {
13582 println!("hello");
13583
13584 println!("world");
13585 }
13586 "#
13587 .unindent(),
13588 );
13589
13590 cx.update_editor(|editor, window, cx| {
13591 editor.delete_line(&DeleteLine, window, cx);
13592 });
13593 executor.run_until_parked();
13594
13595 cx.assert_state_with_diff(
13596 r#"
13597 use some::mod1;
13598 use some::mod2;
13599
13600 const A: u32 = 42;
13601 + const B: u32 = 42;
13602 + const C: u32 = 42;
13603 + const D: u32 = 42;
13604 + const E: u32 = 42;
13605 ˇ
13606 fn main() {
13607 println!("hello");
13608
13609 println!("world");
13610 }
13611 "#
13612 .unindent(),
13613 );
13614
13615 cx.update_editor(|editor, window, cx| {
13616 editor.move_up(&MoveUp, window, cx);
13617 editor.delete_line(&DeleteLine, window, cx);
13618 editor.move_up(&MoveUp, window, cx);
13619 editor.delete_line(&DeleteLine, window, cx);
13620 editor.move_up(&MoveUp, window, cx);
13621 editor.delete_line(&DeleteLine, window, cx);
13622 });
13623 executor.run_until_parked();
13624 cx.assert_state_with_diff(
13625 r#"
13626 use some::mod1;
13627 use some::mod2;
13628
13629 const A: u32 = 42;
13630 + const B: u32 = 42;
13631 ˇ
13632 fn main() {
13633 println!("hello");
13634
13635 println!("world");
13636 }
13637 "#
13638 .unindent(),
13639 );
13640
13641 cx.update_editor(|editor, window, cx| {
13642 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
13643 editor.delete_line(&DeleteLine, window, cx);
13644 });
13645 executor.run_until_parked();
13646 cx.assert_state_with_diff(
13647 r#"
13648 ˇ
13649 fn main() {
13650 println!("hello");
13651
13652 println!("world");
13653 }
13654 "#
13655 .unindent(),
13656 );
13657}
13658
13659#[gpui::test]
13660async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
13661 init_test(cx, |_| {});
13662
13663 let mut cx = EditorTestContext::new(cx).await;
13664 cx.set_diff_base(indoc! { "
13665 one
13666 two
13667 three
13668 four
13669 five
13670 "
13671 });
13672 cx.set_state(indoc! { "
13673 one
13674 ˇthree
13675 five
13676 "});
13677 cx.run_until_parked();
13678 cx.update_editor(|editor, window, cx| {
13679 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13680 });
13681 cx.assert_state_with_diff(
13682 indoc! { "
13683 one
13684 - two
13685 ˇthree
13686 - four
13687 five
13688 "}
13689 .to_string(),
13690 );
13691 cx.update_editor(|editor, window, cx| {
13692 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13693 });
13694
13695 cx.assert_state_with_diff(
13696 indoc! { "
13697 one
13698 ˇthree
13699 five
13700 "}
13701 .to_string(),
13702 );
13703
13704 cx.set_state(indoc! { "
13705 one
13706 ˇTWO
13707 three
13708 four
13709 five
13710 "});
13711 cx.run_until_parked();
13712 cx.update_editor(|editor, window, cx| {
13713 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13714 });
13715
13716 cx.assert_state_with_diff(
13717 indoc! { "
13718 one
13719 - two
13720 + ˇTWO
13721 three
13722 four
13723 five
13724 "}
13725 .to_string(),
13726 );
13727 cx.update_editor(|editor, window, cx| {
13728 editor.move_up(&Default::default(), window, cx);
13729 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13730 });
13731 cx.assert_state_with_diff(
13732 indoc! { "
13733 one
13734 ˇTWO
13735 three
13736 four
13737 five
13738 "}
13739 .to_string(),
13740 );
13741}
13742
13743#[gpui::test]
13744async fn test_edits_around_expanded_deletion_hunks(
13745 executor: BackgroundExecutor,
13746 cx: &mut gpui::TestAppContext,
13747) {
13748 init_test(cx, |_| {});
13749
13750 let mut cx = EditorTestContext::new(cx).await;
13751
13752 let diff_base = r#"
13753 use some::mod1;
13754 use some::mod2;
13755
13756 const A: u32 = 42;
13757 const B: u32 = 42;
13758 const C: u32 = 42;
13759
13760
13761 fn main() {
13762 println!("hello");
13763
13764 println!("world");
13765 }
13766 "#
13767 .unindent();
13768 executor.run_until_parked();
13769 cx.set_state(
13770 &r#"
13771 use some::mod1;
13772 use some::mod2;
13773
13774 ˇconst B: u32 = 42;
13775 const C: u32 = 42;
13776
13777
13778 fn main() {
13779 println!("hello");
13780
13781 println!("world");
13782 }
13783 "#
13784 .unindent(),
13785 );
13786
13787 cx.set_diff_base(&diff_base);
13788 executor.run_until_parked();
13789
13790 cx.update_editor(|editor, window, cx| {
13791 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13792 });
13793 executor.run_until_parked();
13794
13795 cx.assert_state_with_diff(
13796 r#"
13797 use some::mod1;
13798 use some::mod2;
13799
13800 - const A: u32 = 42;
13801 ˇconst B: u32 = 42;
13802 const C: u32 = 42;
13803
13804
13805 fn main() {
13806 println!("hello");
13807
13808 println!("world");
13809 }
13810 "#
13811 .unindent(),
13812 );
13813
13814 cx.update_editor(|editor, window, cx| {
13815 editor.delete_line(&DeleteLine, window, cx);
13816 });
13817 executor.run_until_parked();
13818 cx.assert_state_with_diff(
13819 r#"
13820 use some::mod1;
13821 use some::mod2;
13822
13823 - const A: u32 = 42;
13824 - const B: u32 = 42;
13825 ˇconst C: u32 = 42;
13826
13827
13828 fn main() {
13829 println!("hello");
13830
13831 println!("world");
13832 }
13833 "#
13834 .unindent(),
13835 );
13836
13837 cx.update_editor(|editor, window, cx| {
13838 editor.delete_line(&DeleteLine, window, cx);
13839 });
13840 executor.run_until_parked();
13841 cx.assert_state_with_diff(
13842 r#"
13843 use some::mod1;
13844 use some::mod2;
13845
13846 - const A: u32 = 42;
13847 - const B: u32 = 42;
13848 - const C: u32 = 42;
13849 ˇ
13850
13851 fn main() {
13852 println!("hello");
13853
13854 println!("world");
13855 }
13856 "#
13857 .unindent(),
13858 );
13859
13860 cx.update_editor(|editor, window, cx| {
13861 editor.handle_input("replacement", window, cx);
13862 });
13863 executor.run_until_parked();
13864 cx.assert_state_with_diff(
13865 r#"
13866 use some::mod1;
13867 use some::mod2;
13868
13869 - const A: u32 = 42;
13870 - const B: u32 = 42;
13871 - const C: u32 = 42;
13872 -
13873 + replacementˇ
13874
13875 fn main() {
13876 println!("hello");
13877
13878 println!("world");
13879 }
13880 "#
13881 .unindent(),
13882 );
13883}
13884
13885#[gpui::test]
13886async fn test_backspace_after_deletion_hunk(
13887 executor: BackgroundExecutor,
13888 cx: &mut gpui::TestAppContext,
13889) {
13890 init_test(cx, |_| {});
13891
13892 let mut cx = EditorTestContext::new(cx).await;
13893
13894 let base_text = r#"
13895 one
13896 two
13897 three
13898 four
13899 five
13900 "#
13901 .unindent();
13902 executor.run_until_parked();
13903 cx.set_state(
13904 &r#"
13905 one
13906 two
13907 fˇour
13908 five
13909 "#
13910 .unindent(),
13911 );
13912
13913 cx.set_diff_base(&base_text);
13914 executor.run_until_parked();
13915
13916 cx.update_editor(|editor, window, cx| {
13917 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13918 });
13919 executor.run_until_parked();
13920
13921 cx.assert_state_with_diff(
13922 r#"
13923 one
13924 two
13925 - three
13926 fˇour
13927 five
13928 "#
13929 .unindent(),
13930 );
13931
13932 cx.update_editor(|editor, window, cx| {
13933 editor.backspace(&Backspace, window, cx);
13934 editor.backspace(&Backspace, window, cx);
13935 });
13936 executor.run_until_parked();
13937 cx.assert_state_with_diff(
13938 r#"
13939 one
13940 two
13941 - threeˇ
13942 - four
13943 + our
13944 five
13945 "#
13946 .unindent(),
13947 );
13948}
13949
13950#[gpui::test]
13951async fn test_edit_after_expanded_modification_hunk(
13952 executor: BackgroundExecutor,
13953 cx: &mut gpui::TestAppContext,
13954) {
13955 init_test(cx, |_| {});
13956
13957 let mut cx = EditorTestContext::new(cx).await;
13958
13959 let diff_base = r#"
13960 use some::mod1;
13961 use some::mod2;
13962
13963 const A: u32 = 42;
13964 const B: u32 = 42;
13965 const C: u32 = 42;
13966 const D: u32 = 42;
13967
13968
13969 fn main() {
13970 println!("hello");
13971
13972 println!("world");
13973 }"#
13974 .unindent();
13975
13976 cx.set_state(
13977 &r#"
13978 use some::mod1;
13979 use some::mod2;
13980
13981 const A: u32 = 42;
13982 const B: u32 = 42;
13983 const C: u32 = 43ˇ
13984 const D: u32 = 42;
13985
13986
13987 fn main() {
13988 println!("hello");
13989
13990 println!("world");
13991 }"#
13992 .unindent(),
13993 );
13994
13995 cx.set_diff_base(&diff_base);
13996 executor.run_until_parked();
13997 cx.update_editor(|editor, window, cx| {
13998 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13999 });
14000 executor.run_until_parked();
14001
14002 cx.assert_state_with_diff(
14003 r#"
14004 use some::mod1;
14005 use some::mod2;
14006
14007 const A: u32 = 42;
14008 const B: u32 = 42;
14009 - const C: u32 = 42;
14010 + const C: u32 = 43ˇ
14011 const D: u32 = 42;
14012
14013
14014 fn main() {
14015 println!("hello");
14016
14017 println!("world");
14018 }"#
14019 .unindent(),
14020 );
14021
14022 cx.update_editor(|editor, window, cx| {
14023 editor.handle_input("\nnew_line\n", window, cx);
14024 });
14025 executor.run_until_parked();
14026
14027 cx.assert_state_with_diff(
14028 r#"
14029 use some::mod1;
14030 use some::mod2;
14031
14032 const A: u32 = 42;
14033 const B: u32 = 42;
14034 - const C: u32 = 42;
14035 + const C: u32 = 43
14036 + new_line
14037 + ˇ
14038 const D: u32 = 42;
14039
14040
14041 fn main() {
14042 println!("hello");
14043
14044 println!("world");
14045 }"#
14046 .unindent(),
14047 );
14048}
14049
14050async fn setup_indent_guides_editor(
14051 text: &str,
14052 cx: &mut gpui::TestAppContext,
14053) -> (BufferId, EditorTestContext) {
14054 init_test(cx, |_| {});
14055
14056 let mut cx = EditorTestContext::new(cx).await;
14057
14058 let buffer_id = cx.update_editor(|editor, window, cx| {
14059 editor.set_text(text, window, cx);
14060 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
14061
14062 buffer_ids[0]
14063 });
14064
14065 (buffer_id, cx)
14066}
14067
14068fn assert_indent_guides(
14069 range: Range<u32>,
14070 expected: Vec<IndentGuide>,
14071 active_indices: Option<Vec<usize>>,
14072 cx: &mut EditorTestContext,
14073) {
14074 let indent_guides = cx.update_editor(|editor, window, cx| {
14075 let snapshot = editor.snapshot(window, cx).display_snapshot;
14076 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
14077 editor,
14078 MultiBufferRow(range.start)..MultiBufferRow(range.end),
14079 true,
14080 &snapshot,
14081 cx,
14082 );
14083
14084 indent_guides.sort_by(|a, b| {
14085 a.depth.cmp(&b.depth).then(
14086 a.start_row
14087 .cmp(&b.start_row)
14088 .then(a.end_row.cmp(&b.end_row)),
14089 )
14090 });
14091 indent_guides
14092 });
14093
14094 if let Some(expected) = active_indices {
14095 let active_indices = cx.update_editor(|editor, window, cx| {
14096 let snapshot = editor.snapshot(window, cx).display_snapshot;
14097 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
14098 });
14099
14100 assert_eq!(
14101 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
14102 expected,
14103 "Active indent guide indices do not match"
14104 );
14105 }
14106
14107 assert_eq!(indent_guides, expected, "Indent guides do not match");
14108}
14109
14110fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
14111 IndentGuide {
14112 buffer_id,
14113 start_row: MultiBufferRow(start_row),
14114 end_row: MultiBufferRow(end_row),
14115 depth,
14116 tab_size: 4,
14117 settings: IndentGuideSettings {
14118 enabled: true,
14119 line_width: 1,
14120 active_line_width: 1,
14121 ..Default::default()
14122 },
14123 }
14124}
14125
14126#[gpui::test]
14127async fn test_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
14128 let (buffer_id, mut cx) = setup_indent_guides_editor(
14129 &"
14130 fn main() {
14131 let a = 1;
14132 }"
14133 .unindent(),
14134 cx,
14135 )
14136 .await;
14137
14138 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
14139}
14140
14141#[gpui::test]
14142async fn test_indent_guide_simple_block(cx: &mut gpui::TestAppContext) {
14143 let (buffer_id, mut cx) = setup_indent_guides_editor(
14144 &"
14145 fn main() {
14146 let a = 1;
14147 let b = 2;
14148 }"
14149 .unindent(),
14150 cx,
14151 )
14152 .await;
14153
14154 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
14155}
14156
14157#[gpui::test]
14158async fn test_indent_guide_nested(cx: &mut gpui::TestAppContext) {
14159 let (buffer_id, mut cx) = setup_indent_guides_editor(
14160 &"
14161 fn main() {
14162 let a = 1;
14163 if a == 3 {
14164 let b = 2;
14165 } else {
14166 let c = 3;
14167 }
14168 }"
14169 .unindent(),
14170 cx,
14171 )
14172 .await;
14173
14174 assert_indent_guides(
14175 0..8,
14176 vec![
14177 indent_guide(buffer_id, 1, 6, 0),
14178 indent_guide(buffer_id, 3, 3, 1),
14179 indent_guide(buffer_id, 5, 5, 1),
14180 ],
14181 None,
14182 &mut cx,
14183 );
14184}
14185
14186#[gpui::test]
14187async fn test_indent_guide_tab(cx: &mut gpui::TestAppContext) {
14188 let (buffer_id, mut cx) = setup_indent_guides_editor(
14189 &"
14190 fn main() {
14191 let a = 1;
14192 let b = 2;
14193 let c = 3;
14194 }"
14195 .unindent(),
14196 cx,
14197 )
14198 .await;
14199
14200 assert_indent_guides(
14201 0..5,
14202 vec![
14203 indent_guide(buffer_id, 1, 3, 0),
14204 indent_guide(buffer_id, 2, 2, 1),
14205 ],
14206 None,
14207 &mut cx,
14208 );
14209}
14210
14211#[gpui::test]
14212async fn test_indent_guide_continues_on_empty_line(cx: &mut gpui::TestAppContext) {
14213 let (buffer_id, mut cx) = setup_indent_guides_editor(
14214 &"
14215 fn main() {
14216 let a = 1;
14217
14218 let c = 3;
14219 }"
14220 .unindent(),
14221 cx,
14222 )
14223 .await;
14224
14225 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
14226}
14227
14228#[gpui::test]
14229async fn test_indent_guide_complex(cx: &mut gpui::TestAppContext) {
14230 let (buffer_id, mut cx) = setup_indent_guides_editor(
14231 &"
14232 fn main() {
14233 let a = 1;
14234
14235 let c = 3;
14236
14237 if a == 3 {
14238 let b = 2;
14239 } else {
14240 let c = 3;
14241 }
14242 }"
14243 .unindent(),
14244 cx,
14245 )
14246 .await;
14247
14248 assert_indent_guides(
14249 0..11,
14250 vec![
14251 indent_guide(buffer_id, 1, 9, 0),
14252 indent_guide(buffer_id, 6, 6, 1),
14253 indent_guide(buffer_id, 8, 8, 1),
14254 ],
14255 None,
14256 &mut cx,
14257 );
14258}
14259
14260#[gpui::test]
14261async fn test_indent_guide_starts_off_screen(cx: &mut gpui::TestAppContext) {
14262 let (buffer_id, mut cx) = setup_indent_guides_editor(
14263 &"
14264 fn main() {
14265 let a = 1;
14266
14267 let c = 3;
14268
14269 if a == 3 {
14270 let b = 2;
14271 } else {
14272 let c = 3;
14273 }
14274 }"
14275 .unindent(),
14276 cx,
14277 )
14278 .await;
14279
14280 assert_indent_guides(
14281 1..11,
14282 vec![
14283 indent_guide(buffer_id, 1, 9, 0),
14284 indent_guide(buffer_id, 6, 6, 1),
14285 indent_guide(buffer_id, 8, 8, 1),
14286 ],
14287 None,
14288 &mut cx,
14289 );
14290}
14291
14292#[gpui::test]
14293async fn test_indent_guide_ends_off_screen(cx: &mut gpui::TestAppContext) {
14294 let (buffer_id, mut cx) = setup_indent_guides_editor(
14295 &"
14296 fn main() {
14297 let a = 1;
14298
14299 let c = 3;
14300
14301 if a == 3 {
14302 let b = 2;
14303 } else {
14304 let c = 3;
14305 }
14306 }"
14307 .unindent(),
14308 cx,
14309 )
14310 .await;
14311
14312 assert_indent_guides(
14313 1..10,
14314 vec![
14315 indent_guide(buffer_id, 1, 9, 0),
14316 indent_guide(buffer_id, 6, 6, 1),
14317 indent_guide(buffer_id, 8, 8, 1),
14318 ],
14319 None,
14320 &mut cx,
14321 );
14322}
14323
14324#[gpui::test]
14325async fn test_indent_guide_without_brackets(cx: &mut gpui::TestAppContext) {
14326 let (buffer_id, mut cx) = setup_indent_guides_editor(
14327 &"
14328 block1
14329 block2
14330 block3
14331 block4
14332 block2
14333 block1
14334 block1"
14335 .unindent(),
14336 cx,
14337 )
14338 .await;
14339
14340 assert_indent_guides(
14341 1..10,
14342 vec![
14343 indent_guide(buffer_id, 1, 4, 0),
14344 indent_guide(buffer_id, 2, 3, 1),
14345 indent_guide(buffer_id, 3, 3, 2),
14346 ],
14347 None,
14348 &mut cx,
14349 );
14350}
14351
14352#[gpui::test]
14353async fn test_indent_guide_ends_before_empty_line(cx: &mut gpui::TestAppContext) {
14354 let (buffer_id, mut cx) = setup_indent_guides_editor(
14355 &"
14356 block1
14357 block2
14358 block3
14359
14360 block1
14361 block1"
14362 .unindent(),
14363 cx,
14364 )
14365 .await;
14366
14367 assert_indent_guides(
14368 0..6,
14369 vec![
14370 indent_guide(buffer_id, 1, 2, 0),
14371 indent_guide(buffer_id, 2, 2, 1),
14372 ],
14373 None,
14374 &mut cx,
14375 );
14376}
14377
14378#[gpui::test]
14379async fn test_indent_guide_continuing_off_screen(cx: &mut gpui::TestAppContext) {
14380 let (buffer_id, mut cx) = setup_indent_guides_editor(
14381 &"
14382 block1
14383
14384
14385
14386 block2
14387 "
14388 .unindent(),
14389 cx,
14390 )
14391 .await;
14392
14393 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
14394}
14395
14396#[gpui::test]
14397async fn test_indent_guide_tabs(cx: &mut gpui::TestAppContext) {
14398 let (buffer_id, mut cx) = setup_indent_guides_editor(
14399 &"
14400 def a:
14401 \tb = 3
14402 \tif True:
14403 \t\tc = 4
14404 \t\td = 5
14405 \tprint(b)
14406 "
14407 .unindent(),
14408 cx,
14409 )
14410 .await;
14411
14412 assert_indent_guides(
14413 0..6,
14414 vec![
14415 indent_guide(buffer_id, 1, 6, 0),
14416 indent_guide(buffer_id, 3, 4, 1),
14417 ],
14418 None,
14419 &mut cx,
14420 );
14421}
14422
14423#[gpui::test]
14424async fn test_active_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
14425 let (buffer_id, mut cx) = setup_indent_guides_editor(
14426 &"
14427 fn main() {
14428 let a = 1;
14429 }"
14430 .unindent(),
14431 cx,
14432 )
14433 .await;
14434
14435 cx.update_editor(|editor, window, cx| {
14436 editor.change_selections(None, window, cx, |s| {
14437 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14438 });
14439 });
14440
14441 assert_indent_guides(
14442 0..3,
14443 vec![indent_guide(buffer_id, 1, 1, 0)],
14444 Some(vec![0]),
14445 &mut cx,
14446 );
14447}
14448
14449#[gpui::test]
14450async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppContext) {
14451 let (buffer_id, mut cx) = setup_indent_guides_editor(
14452 &"
14453 fn main() {
14454 if 1 == 2 {
14455 let a = 1;
14456 }
14457 }"
14458 .unindent(),
14459 cx,
14460 )
14461 .await;
14462
14463 cx.update_editor(|editor, window, cx| {
14464 editor.change_selections(None, window, cx, |s| {
14465 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14466 });
14467 });
14468
14469 assert_indent_guides(
14470 0..4,
14471 vec![
14472 indent_guide(buffer_id, 1, 3, 0),
14473 indent_guide(buffer_id, 2, 2, 1),
14474 ],
14475 Some(vec![1]),
14476 &mut cx,
14477 );
14478
14479 cx.update_editor(|editor, window, cx| {
14480 editor.change_selections(None, window, cx, |s| {
14481 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
14482 });
14483 });
14484
14485 assert_indent_guides(
14486 0..4,
14487 vec![
14488 indent_guide(buffer_id, 1, 3, 0),
14489 indent_guide(buffer_id, 2, 2, 1),
14490 ],
14491 Some(vec![1]),
14492 &mut cx,
14493 );
14494
14495 cx.update_editor(|editor, window, cx| {
14496 editor.change_selections(None, window, cx, |s| {
14497 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
14498 });
14499 });
14500
14501 assert_indent_guides(
14502 0..4,
14503 vec![
14504 indent_guide(buffer_id, 1, 3, 0),
14505 indent_guide(buffer_id, 2, 2, 1),
14506 ],
14507 Some(vec![0]),
14508 &mut cx,
14509 );
14510}
14511
14512#[gpui::test]
14513async fn test_active_indent_guide_empty_line(cx: &mut gpui::TestAppContext) {
14514 let (buffer_id, mut cx) = setup_indent_guides_editor(
14515 &"
14516 fn main() {
14517 let a = 1;
14518
14519 let b = 2;
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(2, 0)..Point::new(2, 0)])
14529 });
14530 });
14531
14532 assert_indent_guides(
14533 0..5,
14534 vec![indent_guide(buffer_id, 1, 3, 0)],
14535 Some(vec![0]),
14536 &mut cx,
14537 );
14538}
14539
14540#[gpui::test]
14541async fn test_active_indent_guide_non_matching_indent(cx: &mut gpui::TestAppContext) {
14542 let (buffer_id, mut cx) = setup_indent_guides_editor(
14543 &"
14544 def m:
14545 a = 1
14546 pass"
14547 .unindent(),
14548 cx,
14549 )
14550 .await;
14551
14552 cx.update_editor(|editor, window, cx| {
14553 editor.change_selections(None, window, cx, |s| {
14554 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14555 });
14556 });
14557
14558 assert_indent_guides(
14559 0..3,
14560 vec![indent_guide(buffer_id, 1, 2, 0)],
14561 Some(vec![0]),
14562 &mut cx,
14563 );
14564}
14565
14566#[gpui::test]
14567async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut gpui::TestAppContext) {
14568 init_test(cx, |_| {});
14569 let mut cx = EditorTestContext::new(cx).await;
14570 let text = indoc! {
14571 "
14572 impl A {
14573 fn b() {
14574 0;
14575 3;
14576 5;
14577 6;
14578 7;
14579 }
14580 }
14581 "
14582 };
14583 let base_text = indoc! {
14584 "
14585 impl A {
14586 fn b() {
14587 0;
14588 1;
14589 2;
14590 3;
14591 4;
14592 }
14593 fn c() {
14594 5;
14595 6;
14596 7;
14597 }
14598 }
14599 "
14600 };
14601
14602 cx.update_editor(|editor, window, cx| {
14603 editor.set_text(text, window, cx);
14604
14605 editor.buffer().update(cx, |multibuffer, cx| {
14606 let buffer = multibuffer.as_singleton().unwrap();
14607 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
14608
14609 multibuffer.set_all_diff_hunks_expanded(cx);
14610 multibuffer.add_diff(diff, cx);
14611
14612 buffer.read(cx).remote_id()
14613 })
14614 });
14615 cx.run_until_parked();
14616
14617 cx.assert_state_with_diff(
14618 indoc! { "
14619 impl A {
14620 fn b() {
14621 0;
14622 - 1;
14623 - 2;
14624 3;
14625 - 4;
14626 - }
14627 - fn c() {
14628 5;
14629 6;
14630 7;
14631 }
14632 }
14633 ˇ"
14634 }
14635 .to_string(),
14636 );
14637
14638 let mut actual_guides = cx.update_editor(|editor, window, cx| {
14639 editor
14640 .snapshot(window, cx)
14641 .buffer_snapshot
14642 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
14643 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
14644 .collect::<Vec<_>>()
14645 });
14646 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
14647 assert_eq!(
14648 actual_guides,
14649 vec![
14650 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
14651 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
14652 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
14653 ]
14654 );
14655}
14656
14657#[gpui::test]
14658fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
14659 init_test(cx, |_| {});
14660
14661 let editor = cx.add_window(|window, cx| {
14662 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
14663 build_editor(buffer, window, cx)
14664 });
14665
14666 let render_args = Arc::new(Mutex::new(None));
14667 let snapshot = editor
14668 .update(cx, |editor, window, cx| {
14669 let snapshot = editor.buffer().read(cx).snapshot(cx);
14670 let range =
14671 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
14672
14673 struct RenderArgs {
14674 row: MultiBufferRow,
14675 folded: bool,
14676 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
14677 }
14678
14679 let crease = Crease::inline(
14680 range,
14681 FoldPlaceholder::test(),
14682 {
14683 let toggle_callback = render_args.clone();
14684 move |row, folded, callback, _window, _cx| {
14685 *toggle_callback.lock() = Some(RenderArgs {
14686 row,
14687 folded,
14688 callback,
14689 });
14690 div()
14691 }
14692 },
14693 |_row, _folded, _window, _cx| div(),
14694 );
14695
14696 editor.insert_creases(Some(crease), cx);
14697 let snapshot = editor.snapshot(window, cx);
14698 let _div = snapshot.render_crease_toggle(
14699 MultiBufferRow(1),
14700 false,
14701 cx.entity().clone(),
14702 window,
14703 cx,
14704 );
14705 snapshot
14706 })
14707 .unwrap();
14708
14709 let render_args = render_args.lock().take().unwrap();
14710 assert_eq!(render_args.row, MultiBufferRow(1));
14711 assert!(!render_args.folded);
14712 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
14713
14714 cx.update_window(*editor, |_, window, cx| {
14715 (render_args.callback)(true, window, cx)
14716 })
14717 .unwrap();
14718 let snapshot = editor
14719 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
14720 .unwrap();
14721 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
14722
14723 cx.update_window(*editor, |_, window, cx| {
14724 (render_args.callback)(false, window, cx)
14725 })
14726 .unwrap();
14727 let snapshot = editor
14728 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
14729 .unwrap();
14730 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
14731}
14732
14733#[gpui::test]
14734async fn test_input_text(cx: &mut gpui::TestAppContext) {
14735 init_test(cx, |_| {});
14736 let mut cx = EditorTestContext::new(cx).await;
14737
14738 cx.set_state(
14739 &r#"ˇone
14740 two
14741
14742 three
14743 fourˇ
14744 five
14745
14746 siˇx"#
14747 .unindent(),
14748 );
14749
14750 cx.dispatch_action(HandleInput(String::new()));
14751 cx.assert_editor_state(
14752 &r#"ˇone
14753 two
14754
14755 three
14756 fourˇ
14757 five
14758
14759 siˇx"#
14760 .unindent(),
14761 );
14762
14763 cx.dispatch_action(HandleInput("AAAA".to_string()));
14764 cx.assert_editor_state(
14765 &r#"AAAAˇone
14766 two
14767
14768 three
14769 fourAAAAˇ
14770 five
14771
14772 siAAAAˇx"#
14773 .unindent(),
14774 );
14775}
14776
14777#[gpui::test]
14778async fn test_scroll_cursor_center_top_bottom(cx: &mut gpui::TestAppContext) {
14779 init_test(cx, |_| {});
14780
14781 let mut cx = EditorTestContext::new(cx).await;
14782 cx.set_state(
14783 r#"let foo = 1;
14784let foo = 2;
14785let foo = 3;
14786let fooˇ = 4;
14787let foo = 5;
14788let foo = 6;
14789let foo = 7;
14790let foo = 8;
14791let foo = 9;
14792let foo = 10;
14793let foo = 11;
14794let foo = 12;
14795let foo = 13;
14796let foo = 14;
14797let foo = 15;"#,
14798 );
14799
14800 cx.update_editor(|e, window, cx| {
14801 assert_eq!(
14802 e.next_scroll_position,
14803 NextScrollCursorCenterTopBottom::Center,
14804 "Default next scroll direction is center",
14805 );
14806
14807 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14808 assert_eq!(
14809 e.next_scroll_position,
14810 NextScrollCursorCenterTopBottom::Top,
14811 "After center, next scroll direction should be top",
14812 );
14813
14814 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14815 assert_eq!(
14816 e.next_scroll_position,
14817 NextScrollCursorCenterTopBottom::Bottom,
14818 "After top, next scroll direction should be bottom",
14819 );
14820
14821 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14822 assert_eq!(
14823 e.next_scroll_position,
14824 NextScrollCursorCenterTopBottom::Center,
14825 "After bottom, scrolling should start over",
14826 );
14827
14828 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14829 assert_eq!(
14830 e.next_scroll_position,
14831 NextScrollCursorCenterTopBottom::Top,
14832 "Scrolling continues if retriggered fast enough"
14833 );
14834 });
14835
14836 cx.executor()
14837 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
14838 cx.executor().run_until_parked();
14839 cx.update_editor(|e, _, _| {
14840 assert_eq!(
14841 e.next_scroll_position,
14842 NextScrollCursorCenterTopBottom::Center,
14843 "If scrolling is not triggered fast enough, it should reset"
14844 );
14845 });
14846}
14847
14848#[gpui::test]
14849async fn test_goto_definition_with_find_all_references_fallback(cx: &mut gpui::TestAppContext) {
14850 init_test(cx, |_| {});
14851 let mut cx = EditorLspTestContext::new_rust(
14852 lsp::ServerCapabilities {
14853 definition_provider: Some(lsp::OneOf::Left(true)),
14854 references_provider: Some(lsp::OneOf::Left(true)),
14855 ..lsp::ServerCapabilities::default()
14856 },
14857 cx,
14858 )
14859 .await;
14860
14861 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
14862 let go_to_definition = cx.lsp.handle_request::<lsp::request::GotoDefinition, _, _>(
14863 move |params, _| async move {
14864 if empty_go_to_definition {
14865 Ok(None)
14866 } else {
14867 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
14868 uri: params.text_document_position_params.text_document.uri,
14869 range: lsp::Range::new(lsp::Position::new(4, 3), lsp::Position::new(4, 6)),
14870 })))
14871 }
14872 },
14873 );
14874 let references =
14875 cx.lsp
14876 .handle_request::<lsp::request::References, _, _>(move |params, _| async move {
14877 Ok(Some(vec![lsp::Location {
14878 uri: params.text_document_position.text_document.uri,
14879 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
14880 }]))
14881 });
14882 (go_to_definition, references)
14883 };
14884
14885 cx.set_state(
14886 &r#"fn one() {
14887 let mut a = ˇtwo();
14888 }
14889
14890 fn two() {}"#
14891 .unindent(),
14892 );
14893 set_up_lsp_handlers(false, &mut cx);
14894 let navigated = cx
14895 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
14896 .await
14897 .expect("Failed to navigate to definition");
14898 assert_eq!(
14899 navigated,
14900 Navigated::Yes,
14901 "Should have navigated to definition from the GetDefinition response"
14902 );
14903 cx.assert_editor_state(
14904 &r#"fn one() {
14905 let mut a = two();
14906 }
14907
14908 fn «twoˇ»() {}"#
14909 .unindent(),
14910 );
14911
14912 let editors = cx.update_workspace(|workspace, _, cx| {
14913 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
14914 });
14915 cx.update_editor(|_, _, test_editor_cx| {
14916 assert_eq!(
14917 editors.len(),
14918 1,
14919 "Initially, only one, test, editor should be open in the workspace"
14920 );
14921 assert_eq!(
14922 test_editor_cx.entity(),
14923 editors.last().expect("Asserted len is 1").clone()
14924 );
14925 });
14926
14927 set_up_lsp_handlers(true, &mut cx);
14928 let navigated = cx
14929 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
14930 .await
14931 .expect("Failed to navigate to lookup references");
14932 assert_eq!(
14933 navigated,
14934 Navigated::Yes,
14935 "Should have navigated to references as a fallback after empty GoToDefinition response"
14936 );
14937 // We should not change the selections in the existing file,
14938 // if opening another milti buffer with the references
14939 cx.assert_editor_state(
14940 &r#"fn one() {
14941 let mut a = two();
14942 }
14943
14944 fn «twoˇ»() {}"#
14945 .unindent(),
14946 );
14947 let editors = cx.update_workspace(|workspace, _, cx| {
14948 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
14949 });
14950 cx.update_editor(|_, _, test_editor_cx| {
14951 assert_eq!(
14952 editors.len(),
14953 2,
14954 "After falling back to references search, we open a new editor with the results"
14955 );
14956 let references_fallback_text = editors
14957 .into_iter()
14958 .find(|new_editor| *new_editor != test_editor_cx.entity())
14959 .expect("Should have one non-test editor now")
14960 .read(test_editor_cx)
14961 .text(test_editor_cx);
14962 assert_eq!(
14963 references_fallback_text, "fn one() {\n let mut a = two();\n}",
14964 "Should use the range from the references response and not the GoToDefinition one"
14965 );
14966 });
14967}
14968
14969#[gpui::test]
14970async fn test_find_enclosing_node_with_task(cx: &mut gpui::TestAppContext) {
14971 init_test(cx, |_| {});
14972
14973 let language = Arc::new(Language::new(
14974 LanguageConfig::default(),
14975 Some(tree_sitter_rust::LANGUAGE.into()),
14976 ));
14977
14978 let text = r#"
14979 #[cfg(test)]
14980 mod tests() {
14981 #[test]
14982 fn runnable_1() {
14983 let a = 1;
14984 }
14985
14986 #[test]
14987 fn runnable_2() {
14988 let a = 1;
14989 let b = 2;
14990 }
14991 }
14992 "#
14993 .unindent();
14994
14995 let fs = FakeFs::new(cx.executor());
14996 fs.insert_file("/file.rs", Default::default()).await;
14997
14998 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14999 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15000 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15001 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
15002 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
15003
15004 let editor = cx.new_window_entity(|window, cx| {
15005 Editor::new(
15006 EditorMode::Full,
15007 multi_buffer,
15008 Some(project.clone()),
15009 true,
15010 window,
15011 cx,
15012 )
15013 });
15014
15015 editor.update_in(cx, |editor, window, cx| {
15016 editor.tasks.insert(
15017 (buffer.read(cx).remote_id(), 3),
15018 RunnableTasks {
15019 templates: vec![],
15020 offset: MultiBufferOffset(43),
15021 column: 0,
15022 extra_variables: HashMap::default(),
15023 context_range: BufferOffset(43)..BufferOffset(85),
15024 },
15025 );
15026 editor.tasks.insert(
15027 (buffer.read(cx).remote_id(), 8),
15028 RunnableTasks {
15029 templates: vec![],
15030 offset: MultiBufferOffset(86),
15031 column: 0,
15032 extra_variables: HashMap::default(),
15033 context_range: BufferOffset(86)..BufferOffset(191),
15034 },
15035 );
15036
15037 // Test finding task when cursor is inside function body
15038 editor.change_selections(None, window, cx, |s| {
15039 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
15040 });
15041 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
15042 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
15043
15044 // Test finding task when cursor is on function name
15045 editor.change_selections(None, window, cx, |s| {
15046 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
15047 });
15048 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
15049 assert_eq!(row, 8, "Should find task when cursor is on function name");
15050 });
15051}
15052
15053#[gpui::test]
15054async fn test_multi_buffer_folding(cx: &mut gpui::TestAppContext) {
15055 init_test(cx, |_| {});
15056
15057 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
15058 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
15059 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
15060
15061 let fs = FakeFs::new(cx.executor());
15062 fs.insert_tree(
15063 path!("/a"),
15064 json!({
15065 "first.rs": sample_text_1,
15066 "second.rs": sample_text_2,
15067 "third.rs": sample_text_3,
15068 }),
15069 )
15070 .await;
15071 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15072 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15073 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15074 let worktree = project.update(cx, |project, cx| {
15075 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15076 assert_eq!(worktrees.len(), 1);
15077 worktrees.pop().unwrap()
15078 });
15079 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15080
15081 let buffer_1 = project
15082 .update(cx, |project, cx| {
15083 project.open_buffer((worktree_id, "first.rs"), cx)
15084 })
15085 .await
15086 .unwrap();
15087 let buffer_2 = project
15088 .update(cx, |project, cx| {
15089 project.open_buffer((worktree_id, "second.rs"), cx)
15090 })
15091 .await
15092 .unwrap();
15093 let buffer_3 = project
15094 .update(cx, |project, cx| {
15095 project.open_buffer((worktree_id, "third.rs"), cx)
15096 })
15097 .await
15098 .unwrap();
15099
15100 let multi_buffer = cx.new(|cx| {
15101 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15102 multi_buffer.push_excerpts(
15103 buffer_1.clone(),
15104 [
15105 ExcerptRange {
15106 context: Point::new(0, 0)..Point::new(3, 0),
15107 primary: None,
15108 },
15109 ExcerptRange {
15110 context: Point::new(5, 0)..Point::new(7, 0),
15111 primary: None,
15112 },
15113 ExcerptRange {
15114 context: Point::new(9, 0)..Point::new(10, 4),
15115 primary: None,
15116 },
15117 ],
15118 cx,
15119 );
15120 multi_buffer.push_excerpts(
15121 buffer_2.clone(),
15122 [
15123 ExcerptRange {
15124 context: Point::new(0, 0)..Point::new(3, 0),
15125 primary: None,
15126 },
15127 ExcerptRange {
15128 context: Point::new(5, 0)..Point::new(7, 0),
15129 primary: None,
15130 },
15131 ExcerptRange {
15132 context: Point::new(9, 0)..Point::new(10, 4),
15133 primary: None,
15134 },
15135 ],
15136 cx,
15137 );
15138 multi_buffer.push_excerpts(
15139 buffer_3.clone(),
15140 [
15141 ExcerptRange {
15142 context: Point::new(0, 0)..Point::new(3, 0),
15143 primary: None,
15144 },
15145 ExcerptRange {
15146 context: Point::new(5, 0)..Point::new(7, 0),
15147 primary: None,
15148 },
15149 ExcerptRange {
15150 context: Point::new(9, 0)..Point::new(10, 4),
15151 primary: None,
15152 },
15153 ],
15154 cx,
15155 );
15156 multi_buffer
15157 });
15158 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15159 Editor::new(
15160 EditorMode::Full,
15161 multi_buffer,
15162 Some(project.clone()),
15163 true,
15164 window,
15165 cx,
15166 )
15167 });
15168
15169 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";
15170 assert_eq!(
15171 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15172 full_text,
15173 );
15174
15175 multi_buffer_editor.update(cx, |editor, cx| {
15176 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
15177 });
15178 assert_eq!(
15179 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15180 "\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",
15181 "After folding the first buffer, its text should not be displayed"
15182 );
15183
15184 multi_buffer_editor.update(cx, |editor, cx| {
15185 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
15186 });
15187 assert_eq!(
15188 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15189 "\n\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
15190 "After folding the second buffer, its text should not be displayed"
15191 );
15192
15193 multi_buffer_editor.update(cx, |editor, cx| {
15194 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
15195 });
15196 assert_eq!(
15197 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15198 "\n\n\n\n\n",
15199 "After folding the third buffer, its text should not be displayed"
15200 );
15201
15202 // Emulate selection inside the fold logic, that should work
15203 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15204 editor
15205 .snapshot(window, cx)
15206 .next_line_boundary(Point::new(0, 4));
15207 });
15208
15209 multi_buffer_editor.update(cx, |editor, cx| {
15210 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
15211 });
15212 assert_eq!(
15213 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15214 "\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
15215 "After unfolding the second buffer, its text should be displayed"
15216 );
15217
15218 multi_buffer_editor.update(cx, |editor, cx| {
15219 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
15220 });
15221 assert_eq!(
15222 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15223 "\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",
15224 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
15225 );
15226
15227 multi_buffer_editor.update(cx, |editor, cx| {
15228 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
15229 });
15230 assert_eq!(
15231 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15232 full_text,
15233 "After unfolding the all buffers, all original text should be displayed"
15234 );
15235}
15236
15237#[gpui::test]
15238async fn test_multi_buffer_single_excerpts_folding(cx: &mut gpui::TestAppContext) {
15239 init_test(cx, |_| {});
15240
15241 let sample_text_1 = "1111\n2222\n3333".to_string();
15242 let sample_text_2 = "4444\n5555\n6666".to_string();
15243 let sample_text_3 = "7777\n8888\n9999".to_string();
15244
15245 let fs = FakeFs::new(cx.executor());
15246 fs.insert_tree(
15247 path!("/a"),
15248 json!({
15249 "first.rs": sample_text_1,
15250 "second.rs": sample_text_2,
15251 "third.rs": sample_text_3,
15252 }),
15253 )
15254 .await;
15255 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15256 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15257 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15258 let worktree = project.update(cx, |project, cx| {
15259 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15260 assert_eq!(worktrees.len(), 1);
15261 worktrees.pop().unwrap()
15262 });
15263 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15264
15265 let buffer_1 = project
15266 .update(cx, |project, cx| {
15267 project.open_buffer((worktree_id, "first.rs"), cx)
15268 })
15269 .await
15270 .unwrap();
15271 let buffer_2 = project
15272 .update(cx, |project, cx| {
15273 project.open_buffer((worktree_id, "second.rs"), cx)
15274 })
15275 .await
15276 .unwrap();
15277 let buffer_3 = project
15278 .update(cx, |project, cx| {
15279 project.open_buffer((worktree_id, "third.rs"), cx)
15280 })
15281 .await
15282 .unwrap();
15283
15284 let multi_buffer = cx.new(|cx| {
15285 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15286 multi_buffer.push_excerpts(
15287 buffer_1.clone(),
15288 [ExcerptRange {
15289 context: Point::new(0, 0)..Point::new(3, 0),
15290 primary: None,
15291 }],
15292 cx,
15293 );
15294 multi_buffer.push_excerpts(
15295 buffer_2.clone(),
15296 [ExcerptRange {
15297 context: Point::new(0, 0)..Point::new(3, 0),
15298 primary: None,
15299 }],
15300 cx,
15301 );
15302 multi_buffer.push_excerpts(
15303 buffer_3.clone(),
15304 [ExcerptRange {
15305 context: Point::new(0, 0)..Point::new(3, 0),
15306 primary: None,
15307 }],
15308 cx,
15309 );
15310 multi_buffer
15311 });
15312
15313 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15314 Editor::new(
15315 EditorMode::Full,
15316 multi_buffer,
15317 Some(project.clone()),
15318 true,
15319 window,
15320 cx,
15321 )
15322 });
15323
15324 let full_text = "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n";
15325 assert_eq!(
15326 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15327 full_text,
15328 );
15329
15330 multi_buffer_editor.update(cx, |editor, cx| {
15331 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
15332 });
15333 assert_eq!(
15334 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15335 "\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n",
15336 "After folding the first buffer, its text should not be displayed"
15337 );
15338
15339 multi_buffer_editor.update(cx, |editor, cx| {
15340 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
15341 });
15342
15343 assert_eq!(
15344 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15345 "\n\n\n\n\n\n\n7777\n8888\n9999\n",
15346 "After folding the second buffer, its text should not be displayed"
15347 );
15348
15349 multi_buffer_editor.update(cx, |editor, cx| {
15350 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
15351 });
15352 assert_eq!(
15353 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15354 "\n\n\n\n\n",
15355 "After folding the third buffer, its text should not be displayed"
15356 );
15357
15358 multi_buffer_editor.update(cx, |editor, cx| {
15359 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
15360 });
15361 assert_eq!(
15362 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15363 "\n\n\n\n\n4444\n5555\n6666\n\n\n",
15364 "After unfolding the second buffer, its text should be displayed"
15365 );
15366
15367 multi_buffer_editor.update(cx, |editor, cx| {
15368 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
15369 });
15370 assert_eq!(
15371 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15372 "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n",
15373 "After unfolding the first buffer, its text should be displayed"
15374 );
15375
15376 multi_buffer_editor.update(cx, |editor, cx| {
15377 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
15378 });
15379 assert_eq!(
15380 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15381 full_text,
15382 "After unfolding all buffers, all original text should be displayed"
15383 );
15384}
15385
15386#[gpui::test]
15387async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppContext) {
15388 init_test(cx, |_| {});
15389
15390 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
15391
15392 let fs = FakeFs::new(cx.executor());
15393 fs.insert_tree(
15394 path!("/a"),
15395 json!({
15396 "main.rs": sample_text,
15397 }),
15398 )
15399 .await;
15400 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15401 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15402 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15403 let worktree = project.update(cx, |project, cx| {
15404 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15405 assert_eq!(worktrees.len(), 1);
15406 worktrees.pop().unwrap()
15407 });
15408 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15409
15410 let buffer_1 = project
15411 .update(cx, |project, cx| {
15412 project.open_buffer((worktree_id, "main.rs"), cx)
15413 })
15414 .await
15415 .unwrap();
15416
15417 let multi_buffer = cx.new(|cx| {
15418 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15419 multi_buffer.push_excerpts(
15420 buffer_1.clone(),
15421 [ExcerptRange {
15422 context: Point::new(0, 0)
15423 ..Point::new(
15424 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
15425 0,
15426 ),
15427 primary: None,
15428 }],
15429 cx,
15430 );
15431 multi_buffer
15432 });
15433 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15434 Editor::new(
15435 EditorMode::Full,
15436 multi_buffer,
15437 Some(project.clone()),
15438 true,
15439 window,
15440 cx,
15441 )
15442 });
15443
15444 let selection_range = Point::new(1, 0)..Point::new(2, 0);
15445 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15446 enum TestHighlight {}
15447 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
15448 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
15449 editor.highlight_text::<TestHighlight>(
15450 vec![highlight_range.clone()],
15451 HighlightStyle::color(Hsla::green()),
15452 cx,
15453 );
15454 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
15455 });
15456
15457 let full_text = format!("\n\n\n{sample_text}\n");
15458 assert_eq!(
15459 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15460 full_text,
15461 );
15462}
15463
15464#[gpui::test]
15465async fn test_inline_completion_text(cx: &mut TestAppContext) {
15466 init_test(cx, |_| {});
15467
15468 // Simple insertion
15469 assert_highlighted_edits(
15470 "Hello, world!",
15471 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
15472 true,
15473 cx,
15474 |highlighted_edits, cx| {
15475 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
15476 assert_eq!(highlighted_edits.highlights.len(), 1);
15477 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
15478 assert_eq!(
15479 highlighted_edits.highlights[0].1.background_color,
15480 Some(cx.theme().status().created_background)
15481 );
15482 },
15483 )
15484 .await;
15485
15486 // Replacement
15487 assert_highlighted_edits(
15488 "This is a test.",
15489 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
15490 false,
15491 cx,
15492 |highlighted_edits, cx| {
15493 assert_eq!(highlighted_edits.text, "That is a test.");
15494 assert_eq!(highlighted_edits.highlights.len(), 1);
15495 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
15496 assert_eq!(
15497 highlighted_edits.highlights[0].1.background_color,
15498 Some(cx.theme().status().created_background)
15499 );
15500 },
15501 )
15502 .await;
15503
15504 // Multiple edits
15505 assert_highlighted_edits(
15506 "Hello, world!",
15507 vec![
15508 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
15509 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
15510 ],
15511 false,
15512 cx,
15513 |highlighted_edits, cx| {
15514 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
15515 assert_eq!(highlighted_edits.highlights.len(), 2);
15516 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
15517 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
15518 assert_eq!(
15519 highlighted_edits.highlights[0].1.background_color,
15520 Some(cx.theme().status().created_background)
15521 );
15522 assert_eq!(
15523 highlighted_edits.highlights[1].1.background_color,
15524 Some(cx.theme().status().created_background)
15525 );
15526 },
15527 )
15528 .await;
15529
15530 // Multiple lines with edits
15531 assert_highlighted_edits(
15532 "First line\nSecond line\nThird line\nFourth line",
15533 vec![
15534 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
15535 (
15536 Point::new(2, 0)..Point::new(2, 10),
15537 "New third line".to_string(),
15538 ),
15539 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
15540 ],
15541 false,
15542 cx,
15543 |highlighted_edits, cx| {
15544 assert_eq!(
15545 highlighted_edits.text,
15546 "Second modified\nNew third line\nFourth updated line"
15547 );
15548 assert_eq!(highlighted_edits.highlights.len(), 3);
15549 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
15550 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
15551 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
15552 for highlight in &highlighted_edits.highlights {
15553 assert_eq!(
15554 highlight.1.background_color,
15555 Some(cx.theme().status().created_background)
15556 );
15557 }
15558 },
15559 )
15560 .await;
15561}
15562
15563#[gpui::test]
15564async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
15565 init_test(cx, |_| {});
15566
15567 // Deletion
15568 assert_highlighted_edits(
15569 "Hello, world!",
15570 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
15571 true,
15572 cx,
15573 |highlighted_edits, cx| {
15574 assert_eq!(highlighted_edits.text, "Hello, world!");
15575 assert_eq!(highlighted_edits.highlights.len(), 1);
15576 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
15577 assert_eq!(
15578 highlighted_edits.highlights[0].1.background_color,
15579 Some(cx.theme().status().deleted_background)
15580 );
15581 },
15582 )
15583 .await;
15584
15585 // Insertion
15586 assert_highlighted_edits(
15587 "Hello, world!",
15588 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
15589 true,
15590 cx,
15591 |highlighted_edits, cx| {
15592 assert_eq!(highlighted_edits.highlights.len(), 1);
15593 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
15594 assert_eq!(
15595 highlighted_edits.highlights[0].1.background_color,
15596 Some(cx.theme().status().created_background)
15597 );
15598 },
15599 )
15600 .await;
15601}
15602
15603async fn assert_highlighted_edits(
15604 text: &str,
15605 edits: Vec<(Range<Point>, String)>,
15606 include_deletions: bool,
15607 cx: &mut TestAppContext,
15608 assertion_fn: impl Fn(HighlightedText, &App),
15609) {
15610 let window = cx.add_window(|window, cx| {
15611 let buffer = MultiBuffer::build_simple(text, cx);
15612 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
15613 });
15614 let cx = &mut VisualTestContext::from_window(*window, cx);
15615
15616 let (buffer, snapshot) = window
15617 .update(cx, |editor, _window, cx| {
15618 (
15619 editor.buffer().clone(),
15620 editor.buffer().read(cx).snapshot(cx),
15621 )
15622 })
15623 .unwrap();
15624
15625 let edits = edits
15626 .into_iter()
15627 .map(|(range, edit)| {
15628 (
15629 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
15630 edit,
15631 )
15632 })
15633 .collect::<Vec<_>>();
15634
15635 let text_anchor_edits = edits
15636 .clone()
15637 .into_iter()
15638 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
15639 .collect::<Vec<_>>();
15640
15641 let edit_preview = window
15642 .update(cx, |_, _window, cx| {
15643 buffer
15644 .read(cx)
15645 .as_singleton()
15646 .unwrap()
15647 .read(cx)
15648 .preview_edits(text_anchor_edits.into(), cx)
15649 })
15650 .unwrap()
15651 .await;
15652
15653 cx.update(|_window, cx| {
15654 let highlighted_edits = inline_completion_edit_text(
15655 &snapshot.as_singleton().unwrap().2,
15656 &edits,
15657 &edit_preview,
15658 include_deletions,
15659 cx,
15660 );
15661 assertion_fn(highlighted_edits, cx)
15662 });
15663}
15664
15665#[gpui::test]
15666async fn test_rename_with_duplicate_edits(cx: &mut gpui::TestAppContext) {
15667 init_test(cx, |_| {});
15668 let capabilities = lsp::ServerCapabilities {
15669 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
15670 prepare_provider: Some(true),
15671 work_done_progress_options: Default::default(),
15672 })),
15673 ..Default::default()
15674 };
15675 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
15676
15677 cx.set_state(indoc! {"
15678 struct Fˇoo {}
15679 "});
15680
15681 cx.update_editor(|editor, _, cx| {
15682 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
15683 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
15684 editor.highlight_background::<DocumentHighlightRead>(
15685 &[highlight_range],
15686 |c| c.editor_document_highlight_read_background,
15687 cx,
15688 );
15689 });
15690
15691 let mut prepare_rename_handler =
15692 cx.handle_request::<lsp::request::PrepareRenameRequest, _, _>(move |_, _, _| async move {
15693 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
15694 start: lsp::Position {
15695 line: 0,
15696 character: 7,
15697 },
15698 end: lsp::Position {
15699 line: 0,
15700 character: 10,
15701 },
15702 })))
15703 });
15704 let prepare_rename_task = cx
15705 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
15706 .expect("Prepare rename was not started");
15707 prepare_rename_handler.next().await.unwrap();
15708 prepare_rename_task.await.expect("Prepare rename failed");
15709
15710 let mut rename_handler =
15711 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
15712 let edit = lsp::TextEdit {
15713 range: lsp::Range {
15714 start: lsp::Position {
15715 line: 0,
15716 character: 7,
15717 },
15718 end: lsp::Position {
15719 line: 0,
15720 character: 10,
15721 },
15722 },
15723 new_text: "FooRenamed".to_string(),
15724 };
15725 Ok(Some(lsp::WorkspaceEdit::new(
15726 // Specify the same edit twice
15727 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
15728 )))
15729 });
15730 let rename_task = cx
15731 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
15732 .expect("Confirm rename was not started");
15733 rename_handler.next().await.unwrap();
15734 rename_task.await.expect("Confirm rename failed");
15735 cx.run_until_parked();
15736
15737 // Despite two edits, only one is actually applied as those are identical
15738 cx.assert_editor_state(indoc! {"
15739 struct FooRenamedˇ {}
15740 "});
15741}
15742
15743#[gpui::test]
15744async fn test_rename_without_prepare(cx: &mut gpui::TestAppContext) {
15745 init_test(cx, |_| {});
15746 // These capabilities indicate that the server does not support prepare rename.
15747 let capabilities = lsp::ServerCapabilities {
15748 rename_provider: Some(lsp::OneOf::Left(true)),
15749 ..Default::default()
15750 };
15751 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
15752
15753 cx.set_state(indoc! {"
15754 struct Fˇoo {}
15755 "});
15756
15757 cx.update_editor(|editor, _window, cx| {
15758 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
15759 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
15760 editor.highlight_background::<DocumentHighlightRead>(
15761 &[highlight_range],
15762 |c| c.editor_document_highlight_read_background,
15763 cx,
15764 );
15765 });
15766
15767 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
15768 .expect("Prepare rename was not started")
15769 .await
15770 .expect("Prepare rename failed");
15771
15772 let mut rename_handler =
15773 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
15774 let edit = lsp::TextEdit {
15775 range: lsp::Range {
15776 start: lsp::Position {
15777 line: 0,
15778 character: 7,
15779 },
15780 end: lsp::Position {
15781 line: 0,
15782 character: 10,
15783 },
15784 },
15785 new_text: "FooRenamed".to_string(),
15786 };
15787 Ok(Some(lsp::WorkspaceEdit::new(
15788 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
15789 )))
15790 });
15791 let rename_task = cx
15792 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
15793 .expect("Confirm rename was not started");
15794 rename_handler.next().await.unwrap();
15795 rename_task.await.expect("Confirm rename failed");
15796 cx.run_until_parked();
15797
15798 // Correct range is renamed, as `surrounding_word` is used to find it.
15799 cx.assert_editor_state(indoc! {"
15800 struct FooRenamedˇ {}
15801 "});
15802}
15803
15804fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
15805 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
15806 point..point
15807}
15808
15809fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
15810 let (text, ranges) = marked_text_ranges(marked_text, true);
15811 assert_eq!(editor.text(cx), text);
15812 assert_eq!(
15813 editor.selections.ranges(cx),
15814 ranges,
15815 "Assert selections are {}",
15816 marked_text
15817 );
15818}
15819
15820pub fn handle_signature_help_request(
15821 cx: &mut EditorLspTestContext,
15822 mocked_response: lsp::SignatureHelp,
15823) -> impl Future<Output = ()> {
15824 let mut request =
15825 cx.handle_request::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
15826 let mocked_response = mocked_response.clone();
15827 async move { Ok(Some(mocked_response)) }
15828 });
15829
15830 async move {
15831 request.next().await;
15832 }
15833}
15834
15835/// Handle completion request passing a marked string specifying where the completion
15836/// should be triggered from using '|' character, what range should be replaced, and what completions
15837/// should be returned using '<' and '>' to delimit the range
15838pub fn handle_completion_request(
15839 cx: &mut EditorLspTestContext,
15840 marked_string: &str,
15841 completions: Vec<&'static str>,
15842 counter: Arc<AtomicUsize>,
15843) -> impl Future<Output = ()> {
15844 let complete_from_marker: TextRangeMarker = '|'.into();
15845 let replace_range_marker: TextRangeMarker = ('<', '>').into();
15846 let (_, mut marked_ranges) = marked_text_ranges_by(
15847 marked_string,
15848 vec![complete_from_marker.clone(), replace_range_marker.clone()],
15849 );
15850
15851 let complete_from_position =
15852 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
15853 let replace_range =
15854 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
15855
15856 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
15857 let completions = completions.clone();
15858 counter.fetch_add(1, atomic::Ordering::Release);
15859 async move {
15860 assert_eq!(params.text_document_position.text_document.uri, url.clone());
15861 assert_eq!(
15862 params.text_document_position.position,
15863 complete_from_position
15864 );
15865 Ok(Some(lsp::CompletionResponse::Array(
15866 completions
15867 .iter()
15868 .map(|completion_text| lsp::CompletionItem {
15869 label: completion_text.to_string(),
15870 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15871 range: replace_range,
15872 new_text: completion_text.to_string(),
15873 })),
15874 ..Default::default()
15875 })
15876 .collect(),
15877 )))
15878 }
15879 });
15880
15881 async move {
15882 request.next().await;
15883 }
15884}
15885
15886fn handle_resolve_completion_request(
15887 cx: &mut EditorLspTestContext,
15888 edits: Option<Vec<(&'static str, &'static str)>>,
15889) -> impl Future<Output = ()> {
15890 let edits = edits.map(|edits| {
15891 edits
15892 .iter()
15893 .map(|(marked_string, new_text)| {
15894 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
15895 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
15896 lsp::TextEdit::new(replace_range, new_text.to_string())
15897 })
15898 .collect::<Vec<_>>()
15899 });
15900
15901 let mut request =
15902 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15903 let edits = edits.clone();
15904 async move {
15905 Ok(lsp::CompletionItem {
15906 additional_text_edits: edits,
15907 ..Default::default()
15908 })
15909 }
15910 });
15911
15912 async move {
15913 request.next().await;
15914 }
15915}
15916
15917pub(crate) fn update_test_language_settings(
15918 cx: &mut TestAppContext,
15919 f: impl Fn(&mut AllLanguageSettingsContent),
15920) {
15921 cx.update(|cx| {
15922 SettingsStore::update_global(cx, |store, cx| {
15923 store.update_user_settings::<AllLanguageSettings>(cx, f);
15924 });
15925 });
15926}
15927
15928pub(crate) fn update_test_project_settings(
15929 cx: &mut TestAppContext,
15930 f: impl Fn(&mut ProjectSettings),
15931) {
15932 cx.update(|cx| {
15933 SettingsStore::update_global(cx, |store, cx| {
15934 store.update_user_settings::<ProjectSettings>(cx, f);
15935 });
15936 });
15937}
15938
15939pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
15940 cx.update(|cx| {
15941 assets::Assets.load_test_fonts(cx);
15942 let store = SettingsStore::test(cx);
15943 cx.set_global(store);
15944 theme::init(theme::LoadThemes::JustBase, cx);
15945 release_channel::init(SemanticVersion::default(), cx);
15946 client::init_settings(cx);
15947 language::init(cx);
15948 Project::init_settings(cx);
15949 workspace::init_settings(cx);
15950 crate::init(cx);
15951 });
15952
15953 update_test_language_settings(cx, f);
15954}
15955
15956#[track_caller]
15957fn assert_hunk_revert(
15958 not_reverted_text_with_selections: &str,
15959 expected_hunk_statuses_before: Vec<DiffHunkStatus>,
15960 expected_reverted_text_with_selections: &str,
15961 base_text: &str,
15962 cx: &mut EditorLspTestContext,
15963) {
15964 cx.set_state(not_reverted_text_with_selections);
15965 cx.set_diff_base(base_text);
15966 cx.executor().run_until_parked();
15967
15968 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
15969 let snapshot = editor.snapshot(window, cx);
15970 let reverted_hunk_statuses = snapshot
15971 .buffer_snapshot
15972 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
15973 .map(|hunk| hunk.status())
15974 .collect::<Vec<_>>();
15975
15976 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
15977 reverted_hunk_statuses
15978 });
15979 cx.executor().run_until_parked();
15980 cx.assert_editor_state(expected_reverted_text_with_selections);
15981 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
15982}