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 futures::StreamExt;
11use gpui::{
12 div, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
13 WindowBounds, WindowOptions,
14};
15use indoc::indoc;
16use language::{
17 language_settings::{
18 AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent, PrettierSettings,
19 },
20 BracketPairConfig,
21 Capability::ReadWrite,
22 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
23 Override, ParsedMarkdown, Point,
24};
25use language_settings::{Formatter, FormatterList, IndentGuideSettings};
26use multi_buffer::IndentGuide;
27use parking_lot::Mutex;
28use pretty_assertions::{assert_eq, assert_ne};
29use project::{buffer_store::BufferChangeSet, FakeFs};
30use project::{
31 lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT,
32 project_settings::{LspSettings, ProjectSettings},
33};
34use serde_json::{self, json};
35use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
36use std::{
37 iter,
38 sync::atomic::{self, AtomicUsize},
39};
40use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
41use unindent::Unindent;
42use util::{
43 assert_set_eq,
44 test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
45};
46use workspace::{
47 item::{FollowEvent, FollowableItem, Item, ItemHandle},
48 NavigationEntry, ViewId,
49};
50
51#[gpui::test]
52fn test_edit_events(cx: &mut TestAppContext) {
53 init_test(cx, |_| {});
54
55 let buffer = cx.new(|cx| {
56 let mut buffer = language::Buffer::local("123456", cx);
57 buffer.set_group_interval(Duration::from_secs(1));
58 buffer
59 });
60
61 let events = Rc::new(RefCell::new(Vec::new()));
62 let editor1 = cx.add_window({
63 let events = events.clone();
64 |window, cx| {
65 let entity = cx.entity().clone();
66 cx.subscribe_in(
67 &entity,
68 window,
69 move |_, _, event: &EditorEvent, _, _| match event {
70 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
71 EditorEvent::BufferEdited => {
72 events.borrow_mut().push(("editor1", "buffer edited"))
73 }
74 _ => {}
75 },
76 )
77 .detach();
78 Editor::for_buffer(buffer.clone(), None, window, cx)
79 }
80 });
81
82 let editor2 = cx.add_window({
83 let events = events.clone();
84 |window, cx| {
85 cx.subscribe_in(
86 &cx.entity().clone(),
87 window,
88 move |_, _, event: &EditorEvent, _, _| match event {
89 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
90 EditorEvent::BufferEdited => {
91 events.borrow_mut().push(("editor2", "buffer edited"))
92 }
93 _ => {}
94 },
95 )
96 .detach();
97 Editor::for_buffer(buffer.clone(), None, window, cx)
98 }
99 });
100
101 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
102
103 // Mutating editor 1 will emit an `Edited` event only for that editor.
104 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
105 assert_eq!(
106 mem::take(&mut *events.borrow_mut()),
107 [
108 ("editor1", "edited"),
109 ("editor1", "buffer edited"),
110 ("editor2", "buffer edited"),
111 ]
112 );
113
114 // Mutating editor 2 will emit an `Edited` event only for that editor.
115 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
116 assert_eq!(
117 mem::take(&mut *events.borrow_mut()),
118 [
119 ("editor2", "edited"),
120 ("editor1", "buffer edited"),
121 ("editor2", "buffer edited"),
122 ]
123 );
124
125 // Undoing on editor 1 will emit an `Edited` event only for that editor.
126 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
127 assert_eq!(
128 mem::take(&mut *events.borrow_mut()),
129 [
130 ("editor1", "edited"),
131 ("editor1", "buffer edited"),
132 ("editor2", "buffer edited"),
133 ]
134 );
135
136 // Redoing on editor 1 will emit an `Edited` event only for that editor.
137 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
138 assert_eq!(
139 mem::take(&mut *events.borrow_mut()),
140 [
141 ("editor1", "edited"),
142 ("editor1", "buffer edited"),
143 ("editor2", "buffer edited"),
144 ]
145 );
146
147 // Undoing on editor 2 will emit an `Edited` event only for that editor.
148 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
149 assert_eq!(
150 mem::take(&mut *events.borrow_mut()),
151 [
152 ("editor2", "edited"),
153 ("editor1", "buffer edited"),
154 ("editor2", "buffer edited"),
155 ]
156 );
157
158 // Redoing on editor 2 will emit an `Edited` event only for that editor.
159 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
160 assert_eq!(
161 mem::take(&mut *events.borrow_mut()),
162 [
163 ("editor2", "edited"),
164 ("editor1", "buffer edited"),
165 ("editor2", "buffer edited"),
166 ]
167 );
168
169 // No event is emitted when the mutation is a no-op.
170 _ = editor2.update(cx, |editor, window, cx| {
171 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
172
173 editor.backspace(&Backspace, window, cx);
174 });
175 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
176}
177
178#[gpui::test]
179fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
180 init_test(cx, |_| {});
181
182 let mut now = Instant::now();
183 let group_interval = Duration::from_millis(1);
184 let buffer = cx.new(|cx| {
185 let mut buf = language::Buffer::local("123456", cx);
186 buf.set_group_interval(group_interval);
187 buf
188 });
189 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
190 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
191
192 _ = editor.update(cx, |editor, window, cx| {
193 editor.start_transaction_at(now, window, cx);
194 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
195
196 editor.insert("cd", window, cx);
197 editor.end_transaction_at(now, cx);
198 assert_eq!(editor.text(cx), "12cd56");
199 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
200
201 editor.start_transaction_at(now, window, cx);
202 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
203 editor.insert("e", window, cx);
204 editor.end_transaction_at(now, cx);
205 assert_eq!(editor.text(cx), "12cde6");
206 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
207
208 now += group_interval + Duration::from_millis(1);
209 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
210
211 // Simulate an edit in another editor
212 buffer.update(cx, |buffer, cx| {
213 buffer.start_transaction_at(now, cx);
214 buffer.edit([(0..1, "a")], None, cx);
215 buffer.edit([(1..1, "b")], None, cx);
216 buffer.end_transaction_at(now, cx);
217 });
218
219 assert_eq!(editor.text(cx), "ab2cde6");
220 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
221
222 // Last transaction happened past the group interval in a different editor.
223 // Undo it individually and don't restore selections.
224 editor.undo(&Undo, window, cx);
225 assert_eq!(editor.text(cx), "12cde6");
226 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
227
228 // First two transactions happened within the group interval in this editor.
229 // Undo them together and restore selections.
230 editor.undo(&Undo, window, cx);
231 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
232 assert_eq!(editor.text(cx), "123456");
233 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
234
235 // Redo the first two transactions together.
236 editor.redo(&Redo, window, cx);
237 assert_eq!(editor.text(cx), "12cde6");
238 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
239
240 // Redo the last transaction on its own.
241 editor.redo(&Redo, window, cx);
242 assert_eq!(editor.text(cx), "ab2cde6");
243 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
244
245 // Test empty transactions.
246 editor.start_transaction_at(now, window, cx);
247 editor.end_transaction_at(now, cx);
248 editor.undo(&Undo, window, cx);
249 assert_eq!(editor.text(cx), "12cde6");
250 });
251}
252
253#[gpui::test]
254fn test_ime_composition(cx: &mut TestAppContext) {
255 init_test(cx, |_| {});
256
257 let buffer = cx.new(|cx| {
258 let mut buffer = language::Buffer::local("abcde", cx);
259 // Ensure automatic grouping doesn't occur.
260 buffer.set_group_interval(Duration::ZERO);
261 buffer
262 });
263
264 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
265 cx.add_window(|window, cx| {
266 let mut editor = build_editor(buffer.clone(), window, cx);
267
268 // Start a new IME composition.
269 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
270 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
271 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
272 assert_eq!(editor.text(cx), "äbcde");
273 assert_eq!(
274 editor.marked_text_ranges(cx),
275 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
276 );
277
278 // Finalize IME composition.
279 editor.replace_text_in_range(None, "ā", window, cx);
280 assert_eq!(editor.text(cx), "ābcde");
281 assert_eq!(editor.marked_text_ranges(cx), None);
282
283 // IME composition edits are grouped and are undone/redone at once.
284 editor.undo(&Default::default(), window, cx);
285 assert_eq!(editor.text(cx), "abcde");
286 assert_eq!(editor.marked_text_ranges(cx), None);
287 editor.redo(&Default::default(), window, cx);
288 assert_eq!(editor.text(cx), "ābcde");
289 assert_eq!(editor.marked_text_ranges(cx), None);
290
291 // Start a new IME composition.
292 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
293 assert_eq!(
294 editor.marked_text_ranges(cx),
295 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
296 );
297
298 // Undoing during an IME composition cancels it.
299 editor.undo(&Default::default(), window, cx);
300 assert_eq!(editor.text(cx), "ābcde");
301 assert_eq!(editor.marked_text_ranges(cx), None);
302
303 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
304 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
305 assert_eq!(editor.text(cx), "ābcdè");
306 assert_eq!(
307 editor.marked_text_ranges(cx),
308 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
309 );
310
311 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
312 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
313 assert_eq!(editor.text(cx), "ābcdę");
314 assert_eq!(editor.marked_text_ranges(cx), None);
315
316 // Start a new IME composition with multiple cursors.
317 editor.change_selections(None, window, cx, |s| {
318 s.select_ranges([
319 OffsetUtf16(1)..OffsetUtf16(1),
320 OffsetUtf16(3)..OffsetUtf16(3),
321 OffsetUtf16(5)..OffsetUtf16(5),
322 ])
323 });
324 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
325 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
326 assert_eq!(
327 editor.marked_text_ranges(cx),
328 Some(vec![
329 OffsetUtf16(0)..OffsetUtf16(3),
330 OffsetUtf16(4)..OffsetUtf16(7),
331 OffsetUtf16(8)..OffsetUtf16(11)
332 ])
333 );
334
335 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
336 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
337 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
338 assert_eq!(
339 editor.marked_text_ranges(cx),
340 Some(vec![
341 OffsetUtf16(1)..OffsetUtf16(2),
342 OffsetUtf16(5)..OffsetUtf16(6),
343 OffsetUtf16(9)..OffsetUtf16(10)
344 ])
345 );
346
347 // Finalize IME composition with multiple cursors.
348 editor.replace_text_in_range(Some(9..10), "2", window, cx);
349 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
350 assert_eq!(editor.marked_text_ranges(cx), None);
351
352 editor
353 });
354}
355
356#[gpui::test]
357fn test_selection_with_mouse(cx: &mut TestAppContext) {
358 init_test(cx, |_| {});
359
360 let editor = cx.add_window(|window, cx| {
361 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
362 build_editor(buffer, window, cx)
363 });
364
365 _ = editor.update(cx, |editor, window, cx| {
366 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
367 });
368 assert_eq!(
369 editor
370 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
371 .unwrap(),
372 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
373 );
374
375 _ = editor.update(cx, |editor, window, cx| {
376 editor.update_selection(
377 DisplayPoint::new(DisplayRow(3), 3),
378 0,
379 gpui::Point::<f32>::default(),
380 window,
381 cx,
382 );
383 });
384
385 assert_eq!(
386 editor
387 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
388 .unwrap(),
389 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
390 );
391
392 _ = editor.update(cx, |editor, window, cx| {
393 editor.update_selection(
394 DisplayPoint::new(DisplayRow(1), 1),
395 0,
396 gpui::Point::<f32>::default(),
397 window,
398 cx,
399 );
400 });
401
402 assert_eq!(
403 editor
404 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
405 .unwrap(),
406 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
407 );
408
409 _ = editor.update(cx, |editor, window, cx| {
410 editor.end_selection(window, cx);
411 editor.update_selection(
412 DisplayPoint::new(DisplayRow(3), 3),
413 0,
414 gpui::Point::<f32>::default(),
415 window,
416 cx,
417 );
418 });
419
420 assert_eq!(
421 editor
422 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
423 .unwrap(),
424 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
425 );
426
427 _ = editor.update(cx, |editor, window, cx| {
428 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
429 editor.update_selection(
430 DisplayPoint::new(DisplayRow(0), 0),
431 0,
432 gpui::Point::<f32>::default(),
433 window,
434 cx,
435 );
436 });
437
438 assert_eq!(
439 editor
440 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
441 .unwrap(),
442 [
443 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
444 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
445 ]
446 );
447
448 _ = editor.update(cx, |editor, window, cx| {
449 editor.end_selection(window, cx);
450 });
451
452 assert_eq!(
453 editor
454 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
455 .unwrap(),
456 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
457 );
458}
459
460#[gpui::test]
461fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
462 init_test(cx, |_| {});
463
464 let editor = cx.add_window(|window, cx| {
465 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
466 build_editor(buffer, window, cx)
467 });
468
469 _ = editor.update(cx, |editor, window, cx| {
470 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
471 });
472
473 _ = editor.update(cx, |editor, window, cx| {
474 editor.end_selection(window, cx);
475 });
476
477 _ = editor.update(cx, |editor, window, cx| {
478 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
479 });
480
481 _ = editor.update(cx, |editor, window, cx| {
482 editor.end_selection(window, cx);
483 });
484
485 assert_eq!(
486 editor
487 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
488 .unwrap(),
489 [
490 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
491 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
492 ]
493 );
494
495 _ = editor.update(cx, |editor, window, cx| {
496 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
497 });
498
499 _ = editor.update(cx, |editor, window, cx| {
500 editor.end_selection(window, cx);
501 });
502
503 assert_eq!(
504 editor
505 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
506 .unwrap(),
507 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
508 );
509}
510
511#[gpui::test]
512fn test_canceling_pending_selection(cx: &mut TestAppContext) {
513 init_test(cx, |_| {});
514
515 let editor = cx.add_window(|window, cx| {
516 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
517 build_editor(buffer, window, cx)
518 });
519
520 _ = editor.update(cx, |editor, window, cx| {
521 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
522 assert_eq!(
523 editor.selections.display_ranges(cx),
524 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
525 );
526 });
527
528 _ = editor.update(cx, |editor, window, cx| {
529 editor.update_selection(
530 DisplayPoint::new(DisplayRow(3), 3),
531 0,
532 gpui::Point::<f32>::default(),
533 window,
534 cx,
535 );
536 assert_eq!(
537 editor.selections.display_ranges(cx),
538 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
539 );
540 });
541
542 _ = editor.update(cx, |editor, window, cx| {
543 editor.cancel(&Cancel, window, cx);
544 editor.update_selection(
545 DisplayPoint::new(DisplayRow(1), 1),
546 0,
547 gpui::Point::<f32>::default(),
548 window,
549 cx,
550 );
551 assert_eq!(
552 editor.selections.display_ranges(cx),
553 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
554 );
555 });
556}
557
558#[gpui::test]
559fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
560 init_test(cx, |_| {});
561
562 let editor = cx.add_window(|window, cx| {
563 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
564 build_editor(buffer, window, cx)
565 });
566
567 _ = editor.update(cx, |editor, window, cx| {
568 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
569 assert_eq!(
570 editor.selections.display_ranges(cx),
571 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
572 );
573
574 editor.move_down(&Default::default(), window, cx);
575 assert_eq!(
576 editor.selections.display_ranges(cx),
577 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
578 );
579
580 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
581 assert_eq!(
582 editor.selections.display_ranges(cx),
583 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
584 );
585
586 editor.move_up(&Default::default(), window, cx);
587 assert_eq!(
588 editor.selections.display_ranges(cx),
589 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
590 );
591 });
592}
593
594#[gpui::test]
595fn test_clone(cx: &mut TestAppContext) {
596 init_test(cx, |_| {});
597
598 let (text, selection_ranges) = marked_text_ranges(
599 indoc! {"
600 one
601 two
602 threeˇ
603 four
604 fiveˇ
605 "},
606 true,
607 );
608
609 let editor = cx.add_window(|window, cx| {
610 let buffer = MultiBuffer::build_simple(&text, cx);
611 build_editor(buffer, window, cx)
612 });
613
614 _ = editor.update(cx, |editor, window, cx| {
615 editor.change_selections(None, window, cx, |s| {
616 s.select_ranges(selection_ranges.clone())
617 });
618 editor.fold_creases(
619 vec![
620 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
621 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
622 ],
623 true,
624 window,
625 cx,
626 );
627 });
628
629 let cloned_editor = editor
630 .update(cx, |editor, _, cx| {
631 cx.open_window(Default::default(), |window, cx| {
632 cx.new(|cx| editor.clone(window, cx))
633 })
634 })
635 .unwrap()
636 .unwrap();
637
638 let snapshot = editor
639 .update(cx, |e, window, cx| e.snapshot(window, cx))
640 .unwrap();
641 let cloned_snapshot = cloned_editor
642 .update(cx, |e, window, cx| e.snapshot(window, cx))
643 .unwrap();
644
645 assert_eq!(
646 cloned_editor
647 .update(cx, |e, _, cx| e.display_text(cx))
648 .unwrap(),
649 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
650 );
651 assert_eq!(
652 cloned_snapshot
653 .folds_in_range(0..text.len())
654 .collect::<Vec<_>>(),
655 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
656 );
657 assert_set_eq!(
658 cloned_editor
659 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
660 .unwrap(),
661 editor
662 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
663 .unwrap()
664 );
665 assert_set_eq!(
666 cloned_editor
667 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
668 .unwrap(),
669 editor
670 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
671 .unwrap()
672 );
673}
674
675#[gpui::test]
676async fn test_navigation_history(cx: &mut TestAppContext) {
677 init_test(cx, |_| {});
678
679 use workspace::item::Item;
680
681 let fs = FakeFs::new(cx.executor());
682 let project = Project::test(fs, [], cx).await;
683 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
684 let pane = workspace
685 .update(cx, |workspace, _, _| workspace.active_pane().clone())
686 .unwrap();
687
688 _ = workspace.update(cx, |_v, window, cx| {
689 cx.new(|cx| {
690 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
691 let mut editor = build_editor(buffer.clone(), window, cx);
692 let handle = cx.entity();
693 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
694
695 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
696 editor.nav_history.as_mut().unwrap().pop_backward(cx)
697 }
698
699 // Move the cursor a small distance.
700 // Nothing is added to the navigation history.
701 editor.change_selections(None, window, cx, |s| {
702 s.select_display_ranges([
703 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
704 ])
705 });
706 editor.change_selections(None, window, cx, |s| {
707 s.select_display_ranges([
708 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
709 ])
710 });
711 assert!(pop_history(&mut editor, cx).is_none());
712
713 // Move the cursor a large distance.
714 // The history can jump back to the previous position.
715 editor.change_selections(None, window, cx, |s| {
716 s.select_display_ranges([
717 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
718 ])
719 });
720 let nav_entry = pop_history(&mut editor, cx).unwrap();
721 editor.navigate(nav_entry.data.unwrap(), window, cx);
722 assert_eq!(nav_entry.item.id(), cx.entity_id());
723 assert_eq!(
724 editor.selections.display_ranges(cx),
725 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
726 );
727 assert!(pop_history(&mut editor, cx).is_none());
728
729 // Move the cursor a small distance via the mouse.
730 // Nothing is added to the navigation history.
731 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
732 editor.end_selection(window, cx);
733 assert_eq!(
734 editor.selections.display_ranges(cx),
735 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
736 );
737 assert!(pop_history(&mut editor, cx).is_none());
738
739 // Move the cursor a large distance via the mouse.
740 // The history can jump back to the previous position.
741 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
742 editor.end_selection(window, cx);
743 assert_eq!(
744 editor.selections.display_ranges(cx),
745 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
746 );
747 let nav_entry = pop_history(&mut editor, cx).unwrap();
748 editor.navigate(nav_entry.data.unwrap(), window, cx);
749 assert_eq!(nav_entry.item.id(), cx.entity_id());
750 assert_eq!(
751 editor.selections.display_ranges(cx),
752 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
753 );
754 assert!(pop_history(&mut editor, cx).is_none());
755
756 // Set scroll position to check later
757 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
758 let original_scroll_position = editor.scroll_manager.anchor();
759
760 // Jump to the end of the document and adjust scroll
761 editor.move_to_end(&MoveToEnd, window, cx);
762 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
763 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
764
765 let nav_entry = pop_history(&mut editor, cx).unwrap();
766 editor.navigate(nav_entry.data.unwrap(), window, cx);
767 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
768
769 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
770 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
771 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
772 let invalid_point = Point::new(9999, 0);
773 editor.navigate(
774 Box::new(NavigationData {
775 cursor_anchor: invalid_anchor,
776 cursor_position: invalid_point,
777 scroll_anchor: ScrollAnchor {
778 anchor: invalid_anchor,
779 offset: Default::default(),
780 },
781 scroll_top_row: invalid_point.row,
782 }),
783 window,
784 cx,
785 );
786 assert_eq!(
787 editor.selections.display_ranges(cx),
788 &[editor.max_point(cx)..editor.max_point(cx)]
789 );
790 assert_eq!(
791 editor.scroll_position(cx),
792 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
793 );
794
795 editor
796 })
797 });
798}
799
800#[gpui::test]
801fn test_cancel(cx: &mut TestAppContext) {
802 init_test(cx, |_| {});
803
804 let editor = cx.add_window(|window, cx| {
805 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
806 build_editor(buffer, window, cx)
807 });
808
809 _ = editor.update(cx, |editor, window, cx| {
810 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
811 editor.update_selection(
812 DisplayPoint::new(DisplayRow(1), 1),
813 0,
814 gpui::Point::<f32>::default(),
815 window,
816 cx,
817 );
818 editor.end_selection(window, cx);
819
820 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
821 editor.update_selection(
822 DisplayPoint::new(DisplayRow(0), 3),
823 0,
824 gpui::Point::<f32>::default(),
825 window,
826 cx,
827 );
828 editor.end_selection(window, cx);
829 assert_eq!(
830 editor.selections.display_ranges(cx),
831 [
832 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
833 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
834 ]
835 );
836 });
837
838 _ = editor.update(cx, |editor, window, cx| {
839 editor.cancel(&Cancel, window, cx);
840 assert_eq!(
841 editor.selections.display_ranges(cx),
842 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
843 );
844 });
845
846 _ = editor.update(cx, |editor, window, cx| {
847 editor.cancel(&Cancel, window, cx);
848 assert_eq!(
849 editor.selections.display_ranges(cx),
850 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
851 );
852 });
853}
854
855#[gpui::test]
856fn test_fold_action(cx: &mut TestAppContext) {
857 init_test(cx, |_| {});
858
859 let editor = cx.add_window(|window, cx| {
860 let buffer = MultiBuffer::build_simple(
861 &"
862 impl Foo {
863 // Hello!
864
865 fn a() {
866 1
867 }
868
869 fn b() {
870 2
871 }
872
873 fn c() {
874 3
875 }
876 }
877 "
878 .unindent(),
879 cx,
880 );
881 build_editor(buffer.clone(), window, cx)
882 });
883
884 _ = editor.update(cx, |editor, window, cx| {
885 editor.change_selections(None, window, cx, |s| {
886 s.select_display_ranges([
887 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
888 ]);
889 });
890 editor.fold(&Fold, window, cx);
891 assert_eq!(
892 editor.display_text(cx),
893 "
894 impl Foo {
895 // Hello!
896
897 fn a() {
898 1
899 }
900
901 fn b() {⋯
902 }
903
904 fn c() {⋯
905 }
906 }
907 "
908 .unindent(),
909 );
910
911 editor.fold(&Fold, window, cx);
912 assert_eq!(
913 editor.display_text(cx),
914 "
915 impl Foo {⋯
916 }
917 "
918 .unindent(),
919 );
920
921 editor.unfold_lines(&UnfoldLines, window, cx);
922 assert_eq!(
923 editor.display_text(cx),
924 "
925 impl Foo {
926 // Hello!
927
928 fn a() {
929 1
930 }
931
932 fn b() {⋯
933 }
934
935 fn c() {⋯
936 }
937 }
938 "
939 .unindent(),
940 );
941
942 editor.unfold_lines(&UnfoldLines, window, cx);
943 assert_eq!(
944 editor.display_text(cx),
945 editor.buffer.read(cx).read(cx).text()
946 );
947 });
948}
949
950#[gpui::test]
951fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
952 init_test(cx, |_| {});
953
954 let editor = cx.add_window(|window, cx| {
955 let buffer = MultiBuffer::build_simple(
956 &"
957 class Foo:
958 # Hello!
959
960 def a():
961 print(1)
962
963 def b():
964 print(2)
965
966 def c():
967 print(3)
968 "
969 .unindent(),
970 cx,
971 );
972 build_editor(buffer.clone(), window, cx)
973 });
974
975 _ = editor.update(cx, |editor, window, cx| {
976 editor.change_selections(None, window, cx, |s| {
977 s.select_display_ranges([
978 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
979 ]);
980 });
981 editor.fold(&Fold, window, cx);
982 assert_eq!(
983 editor.display_text(cx),
984 "
985 class Foo:
986 # Hello!
987
988 def a():
989 print(1)
990
991 def b():⋯
992
993 def c():⋯
994 "
995 .unindent(),
996 );
997
998 editor.fold(&Fold, window, cx);
999 assert_eq!(
1000 editor.display_text(cx),
1001 "
1002 class Foo:⋯
1003 "
1004 .unindent(),
1005 );
1006
1007 editor.unfold_lines(&UnfoldLines, window, cx);
1008 assert_eq!(
1009 editor.display_text(cx),
1010 "
1011 class Foo:
1012 # Hello!
1013
1014 def a():
1015 print(1)
1016
1017 def b():⋯
1018
1019 def c():⋯
1020 "
1021 .unindent(),
1022 );
1023
1024 editor.unfold_lines(&UnfoldLines, window, cx);
1025 assert_eq!(
1026 editor.display_text(cx),
1027 editor.buffer.read(cx).read(cx).text()
1028 );
1029 });
1030}
1031
1032#[gpui::test]
1033fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1034 init_test(cx, |_| {});
1035
1036 let editor = cx.add_window(|window, cx| {
1037 let buffer = MultiBuffer::build_simple(
1038 &"
1039 class Foo:
1040 # Hello!
1041
1042 def a():
1043 print(1)
1044
1045 def b():
1046 print(2)
1047
1048
1049 def c():
1050 print(3)
1051
1052
1053 "
1054 .unindent(),
1055 cx,
1056 );
1057 build_editor(buffer.clone(), window, cx)
1058 });
1059
1060 _ = editor.update(cx, |editor, window, cx| {
1061 editor.change_selections(None, window, cx, |s| {
1062 s.select_display_ranges([
1063 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1064 ]);
1065 });
1066 editor.fold(&Fold, window, cx);
1067 assert_eq!(
1068 editor.display_text(cx),
1069 "
1070 class Foo:
1071 # Hello!
1072
1073 def a():
1074 print(1)
1075
1076 def b():⋯
1077
1078
1079 def c():⋯
1080
1081
1082 "
1083 .unindent(),
1084 );
1085
1086 editor.fold(&Fold, window, cx);
1087 assert_eq!(
1088 editor.display_text(cx),
1089 "
1090 class Foo:⋯
1091
1092
1093 "
1094 .unindent(),
1095 );
1096
1097 editor.unfold_lines(&UnfoldLines, window, cx);
1098 assert_eq!(
1099 editor.display_text(cx),
1100 "
1101 class Foo:
1102 # Hello!
1103
1104 def a():
1105 print(1)
1106
1107 def b():⋯
1108
1109
1110 def c():⋯
1111
1112
1113 "
1114 .unindent(),
1115 );
1116
1117 editor.unfold_lines(&UnfoldLines, window, cx);
1118 assert_eq!(
1119 editor.display_text(cx),
1120 editor.buffer.read(cx).read(cx).text()
1121 );
1122 });
1123}
1124
1125#[gpui::test]
1126fn test_fold_at_level(cx: &mut TestAppContext) {
1127 init_test(cx, |_| {});
1128
1129 let editor = cx.add_window(|window, cx| {
1130 let buffer = MultiBuffer::build_simple(
1131 &"
1132 class Foo:
1133 # Hello!
1134
1135 def a():
1136 print(1)
1137
1138 def b():
1139 print(2)
1140
1141
1142 class Bar:
1143 # World!
1144
1145 def a():
1146 print(1)
1147
1148 def b():
1149 print(2)
1150
1151
1152 "
1153 .unindent(),
1154 cx,
1155 );
1156 build_editor(buffer.clone(), window, cx)
1157 });
1158
1159 _ = editor.update(cx, |editor, window, cx| {
1160 editor.fold_at_level(&FoldAtLevel { level: 2 }, window, cx);
1161 assert_eq!(
1162 editor.display_text(cx),
1163 "
1164 class Foo:
1165 # Hello!
1166
1167 def a():⋯
1168
1169 def b():⋯
1170
1171
1172 class Bar:
1173 # World!
1174
1175 def a():⋯
1176
1177 def b():⋯
1178
1179
1180 "
1181 .unindent(),
1182 );
1183
1184 editor.fold_at_level(&FoldAtLevel { level: 1 }, window, cx);
1185 assert_eq!(
1186 editor.display_text(cx),
1187 "
1188 class Foo:⋯
1189
1190
1191 class Bar:⋯
1192
1193
1194 "
1195 .unindent(),
1196 );
1197
1198 editor.unfold_all(&UnfoldAll, window, cx);
1199 editor.fold_at_level(&FoldAtLevel { level: 0 }, window, cx);
1200 assert_eq!(
1201 editor.display_text(cx),
1202 "
1203 class Foo:
1204 # Hello!
1205
1206 def a():
1207 print(1)
1208
1209 def b():
1210 print(2)
1211
1212
1213 class Bar:
1214 # World!
1215
1216 def a():
1217 print(1)
1218
1219 def b():
1220 print(2)
1221
1222
1223 "
1224 .unindent(),
1225 );
1226
1227 assert_eq!(
1228 editor.display_text(cx),
1229 editor.buffer.read(cx).read(cx).text()
1230 );
1231 });
1232}
1233
1234#[gpui::test]
1235fn test_move_cursor(cx: &mut TestAppContext) {
1236 init_test(cx, |_| {});
1237
1238 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1239 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1240
1241 buffer.update(cx, |buffer, cx| {
1242 buffer.edit(
1243 vec![
1244 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1245 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1246 ],
1247 None,
1248 cx,
1249 );
1250 });
1251 _ = editor.update(cx, |editor, window, cx| {
1252 assert_eq!(
1253 editor.selections.display_ranges(cx),
1254 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1255 );
1256
1257 editor.move_down(&MoveDown, window, cx);
1258 assert_eq!(
1259 editor.selections.display_ranges(cx),
1260 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1261 );
1262
1263 editor.move_right(&MoveRight, window, cx);
1264 assert_eq!(
1265 editor.selections.display_ranges(cx),
1266 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1267 );
1268
1269 editor.move_left(&MoveLeft, window, cx);
1270 assert_eq!(
1271 editor.selections.display_ranges(cx),
1272 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1273 );
1274
1275 editor.move_up(&MoveUp, window, cx);
1276 assert_eq!(
1277 editor.selections.display_ranges(cx),
1278 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1279 );
1280
1281 editor.move_to_end(&MoveToEnd, window, cx);
1282 assert_eq!(
1283 editor.selections.display_ranges(cx),
1284 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1285 );
1286
1287 editor.move_to_beginning(&MoveToBeginning, window, cx);
1288 assert_eq!(
1289 editor.selections.display_ranges(cx),
1290 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1291 );
1292
1293 editor.change_selections(None, window, cx, |s| {
1294 s.select_display_ranges([
1295 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1296 ]);
1297 });
1298 editor.select_to_beginning(&SelectToBeginning, window, cx);
1299 assert_eq!(
1300 editor.selections.display_ranges(cx),
1301 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1302 );
1303
1304 editor.select_to_end(&SelectToEnd, window, cx);
1305 assert_eq!(
1306 editor.selections.display_ranges(cx),
1307 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1308 );
1309 });
1310}
1311
1312// TODO: Re-enable this test
1313#[cfg(target_os = "macos")]
1314#[gpui::test]
1315fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1316 init_test(cx, |_| {});
1317
1318 let editor = cx.add_window(|window, cx| {
1319 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1320 build_editor(buffer.clone(), window, cx)
1321 });
1322
1323 assert_eq!('🟥'.len_utf8(), 4);
1324 assert_eq!('α'.len_utf8(), 2);
1325
1326 _ = editor.update(cx, |editor, window, cx| {
1327 editor.fold_creases(
1328 vec![
1329 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1330 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1331 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1332 ],
1333 true,
1334 window,
1335 cx,
1336 );
1337 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1338
1339 editor.move_right(&MoveRight, window, cx);
1340 assert_eq!(
1341 editor.selections.display_ranges(cx),
1342 &[empty_range(0, "🟥".len())]
1343 );
1344 editor.move_right(&MoveRight, window, cx);
1345 assert_eq!(
1346 editor.selections.display_ranges(cx),
1347 &[empty_range(0, "🟥🟧".len())]
1348 );
1349 editor.move_right(&MoveRight, window, cx);
1350 assert_eq!(
1351 editor.selections.display_ranges(cx),
1352 &[empty_range(0, "🟥🟧⋯".len())]
1353 );
1354
1355 editor.move_down(&MoveDown, window, cx);
1356 assert_eq!(
1357 editor.selections.display_ranges(cx),
1358 &[empty_range(1, "ab⋯e".len())]
1359 );
1360 editor.move_left(&MoveLeft, window, cx);
1361 assert_eq!(
1362 editor.selections.display_ranges(cx),
1363 &[empty_range(1, "ab⋯".len())]
1364 );
1365 editor.move_left(&MoveLeft, window, cx);
1366 assert_eq!(
1367 editor.selections.display_ranges(cx),
1368 &[empty_range(1, "ab".len())]
1369 );
1370 editor.move_left(&MoveLeft, window, cx);
1371 assert_eq!(
1372 editor.selections.display_ranges(cx),
1373 &[empty_range(1, "a".len())]
1374 );
1375
1376 editor.move_down(&MoveDown, window, cx);
1377 assert_eq!(
1378 editor.selections.display_ranges(cx),
1379 &[empty_range(2, "α".len())]
1380 );
1381 editor.move_right(&MoveRight, window, cx);
1382 assert_eq!(
1383 editor.selections.display_ranges(cx),
1384 &[empty_range(2, "αβ".len())]
1385 );
1386 editor.move_right(&MoveRight, window, cx);
1387 assert_eq!(
1388 editor.selections.display_ranges(cx),
1389 &[empty_range(2, "αβ⋯".len())]
1390 );
1391 editor.move_right(&MoveRight, window, cx);
1392 assert_eq!(
1393 editor.selections.display_ranges(cx),
1394 &[empty_range(2, "αβ⋯ε".len())]
1395 );
1396
1397 editor.move_up(&MoveUp, window, cx);
1398 assert_eq!(
1399 editor.selections.display_ranges(cx),
1400 &[empty_range(1, "ab⋯e".len())]
1401 );
1402 editor.move_down(&MoveDown, window, cx);
1403 assert_eq!(
1404 editor.selections.display_ranges(cx),
1405 &[empty_range(2, "αβ⋯ε".len())]
1406 );
1407 editor.move_up(&MoveUp, window, cx);
1408 assert_eq!(
1409 editor.selections.display_ranges(cx),
1410 &[empty_range(1, "ab⋯e".len())]
1411 );
1412
1413 editor.move_up(&MoveUp, window, cx);
1414 assert_eq!(
1415 editor.selections.display_ranges(cx),
1416 &[empty_range(0, "🟥🟧".len())]
1417 );
1418 editor.move_left(&MoveLeft, window, cx);
1419 assert_eq!(
1420 editor.selections.display_ranges(cx),
1421 &[empty_range(0, "🟥".len())]
1422 );
1423 editor.move_left(&MoveLeft, window, cx);
1424 assert_eq!(
1425 editor.selections.display_ranges(cx),
1426 &[empty_range(0, "".len())]
1427 );
1428 });
1429}
1430
1431#[gpui::test]
1432fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1433 init_test(cx, |_| {});
1434
1435 let editor = cx.add_window(|window, cx| {
1436 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1437 build_editor(buffer.clone(), window, cx)
1438 });
1439 _ = editor.update(cx, |editor, window, cx| {
1440 editor.change_selections(None, window, cx, |s| {
1441 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1442 });
1443
1444 // moving above start of document should move selection to start of document,
1445 // but the next move down should still be at the original goal_x
1446 editor.move_up(&MoveUp, window, cx);
1447 assert_eq!(
1448 editor.selections.display_ranges(cx),
1449 &[empty_range(0, "".len())]
1450 );
1451
1452 editor.move_down(&MoveDown, window, cx);
1453 assert_eq!(
1454 editor.selections.display_ranges(cx),
1455 &[empty_range(1, "abcd".len())]
1456 );
1457
1458 editor.move_down(&MoveDown, window, cx);
1459 assert_eq!(
1460 editor.selections.display_ranges(cx),
1461 &[empty_range(2, "αβγ".len())]
1462 );
1463
1464 editor.move_down(&MoveDown, window, cx);
1465 assert_eq!(
1466 editor.selections.display_ranges(cx),
1467 &[empty_range(3, "abcd".len())]
1468 );
1469
1470 editor.move_down(&MoveDown, window, cx);
1471 assert_eq!(
1472 editor.selections.display_ranges(cx),
1473 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1474 );
1475
1476 // moving past end of document should not change goal_x
1477 editor.move_down(&MoveDown, window, cx);
1478 assert_eq!(
1479 editor.selections.display_ranges(cx),
1480 &[empty_range(5, "".len())]
1481 );
1482
1483 editor.move_down(&MoveDown, window, cx);
1484 assert_eq!(
1485 editor.selections.display_ranges(cx),
1486 &[empty_range(5, "".len())]
1487 );
1488
1489 editor.move_up(&MoveUp, window, cx);
1490 assert_eq!(
1491 editor.selections.display_ranges(cx),
1492 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1493 );
1494
1495 editor.move_up(&MoveUp, window, cx);
1496 assert_eq!(
1497 editor.selections.display_ranges(cx),
1498 &[empty_range(3, "abcd".len())]
1499 );
1500
1501 editor.move_up(&MoveUp, window, cx);
1502 assert_eq!(
1503 editor.selections.display_ranges(cx),
1504 &[empty_range(2, "αβγ".len())]
1505 );
1506 });
1507}
1508
1509#[gpui::test]
1510fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1511 init_test(cx, |_| {});
1512 let move_to_beg = MoveToBeginningOfLine {
1513 stop_at_soft_wraps: true,
1514 };
1515
1516 let move_to_end = MoveToEndOfLine {
1517 stop_at_soft_wraps: true,
1518 };
1519
1520 let editor = cx.add_window(|window, cx| {
1521 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1522 build_editor(buffer, window, cx)
1523 });
1524 _ = editor.update(cx, |editor, window, cx| {
1525 editor.change_selections(None, window, cx, |s| {
1526 s.select_display_ranges([
1527 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1528 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1529 ]);
1530 });
1531 });
1532
1533 _ = editor.update(cx, |editor, window, cx| {
1534 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1535 assert_eq!(
1536 editor.selections.display_ranges(cx),
1537 &[
1538 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1539 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1540 ]
1541 );
1542 });
1543
1544 _ = editor.update(cx, |editor, window, cx| {
1545 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1546 assert_eq!(
1547 editor.selections.display_ranges(cx),
1548 &[
1549 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1550 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1551 ]
1552 );
1553 });
1554
1555 _ = editor.update(cx, |editor, window, cx| {
1556 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1557 assert_eq!(
1558 editor.selections.display_ranges(cx),
1559 &[
1560 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1561 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1562 ]
1563 );
1564 });
1565
1566 _ = editor.update(cx, |editor, window, cx| {
1567 editor.move_to_end_of_line(&move_to_end, window, cx);
1568 assert_eq!(
1569 editor.selections.display_ranges(cx),
1570 &[
1571 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1572 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1573 ]
1574 );
1575 });
1576
1577 // Moving to the end of line again is a no-op.
1578 _ = editor.update(cx, |editor, window, cx| {
1579 editor.move_to_end_of_line(&move_to_end, window, cx);
1580 assert_eq!(
1581 editor.selections.display_ranges(cx),
1582 &[
1583 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1584 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1585 ]
1586 );
1587 });
1588
1589 _ = editor.update(cx, |editor, window, cx| {
1590 editor.move_left(&MoveLeft, window, cx);
1591 editor.select_to_beginning_of_line(
1592 &SelectToBeginningOfLine {
1593 stop_at_soft_wraps: true,
1594 },
1595 window,
1596 cx,
1597 );
1598 assert_eq!(
1599 editor.selections.display_ranges(cx),
1600 &[
1601 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1602 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1603 ]
1604 );
1605 });
1606
1607 _ = editor.update(cx, |editor, window, cx| {
1608 editor.select_to_beginning_of_line(
1609 &SelectToBeginningOfLine {
1610 stop_at_soft_wraps: true,
1611 },
1612 window,
1613 cx,
1614 );
1615 assert_eq!(
1616 editor.selections.display_ranges(cx),
1617 &[
1618 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1619 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1620 ]
1621 );
1622 });
1623
1624 _ = editor.update(cx, |editor, window, cx| {
1625 editor.select_to_beginning_of_line(
1626 &SelectToBeginningOfLine {
1627 stop_at_soft_wraps: true,
1628 },
1629 window,
1630 cx,
1631 );
1632 assert_eq!(
1633 editor.selections.display_ranges(cx),
1634 &[
1635 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1636 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1637 ]
1638 );
1639 });
1640
1641 _ = editor.update(cx, |editor, window, cx| {
1642 editor.select_to_end_of_line(
1643 &SelectToEndOfLine {
1644 stop_at_soft_wraps: true,
1645 },
1646 window,
1647 cx,
1648 );
1649 assert_eq!(
1650 editor.selections.display_ranges(cx),
1651 &[
1652 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1653 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1654 ]
1655 );
1656 });
1657
1658 _ = editor.update(cx, |editor, window, cx| {
1659 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1660 assert_eq!(editor.display_text(cx), "ab\n de");
1661 assert_eq!(
1662 editor.selections.display_ranges(cx),
1663 &[
1664 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1665 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1666 ]
1667 );
1668 });
1669
1670 _ = editor.update(cx, |editor, window, cx| {
1671 editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx);
1672 assert_eq!(editor.display_text(cx), "\n");
1673 assert_eq!(
1674 editor.selections.display_ranges(cx),
1675 &[
1676 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1677 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1678 ]
1679 );
1680 });
1681}
1682
1683#[gpui::test]
1684fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1685 init_test(cx, |_| {});
1686 let move_to_beg = MoveToBeginningOfLine {
1687 stop_at_soft_wraps: false,
1688 };
1689
1690 let move_to_end = MoveToEndOfLine {
1691 stop_at_soft_wraps: false,
1692 };
1693
1694 let editor = cx.add_window(|window, cx| {
1695 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1696 build_editor(buffer, window, cx)
1697 });
1698
1699 _ = editor.update(cx, |editor, window, cx| {
1700 editor.set_wrap_width(Some(140.0.into()), cx);
1701
1702 // We expect the following lines after wrapping
1703 // ```
1704 // thequickbrownfox
1705 // jumpedoverthelazydo
1706 // gs
1707 // ```
1708 // The final `gs` was soft-wrapped onto a new line.
1709 assert_eq!(
1710 "thequickbrownfox\njumpedoverthelaz\nydogs",
1711 editor.display_text(cx),
1712 );
1713
1714 // First, let's assert behavior on the first line, that was not soft-wrapped.
1715 // Start the cursor at the `k` on the first line
1716 editor.change_selections(None, window, cx, |s| {
1717 s.select_display_ranges([
1718 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1719 ]);
1720 });
1721
1722 // Moving to the beginning of the line should put us at the beginning of the line.
1723 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1724 assert_eq!(
1725 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1726 editor.selections.display_ranges(cx)
1727 );
1728
1729 // Moving to the end of the line should put us at the end of the line.
1730 editor.move_to_end_of_line(&move_to_end, window, cx);
1731 assert_eq!(
1732 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1733 editor.selections.display_ranges(cx)
1734 );
1735
1736 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1737 // Start the cursor at the last line (`y` that was wrapped to a new line)
1738 editor.change_selections(None, window, cx, |s| {
1739 s.select_display_ranges([
1740 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1741 ]);
1742 });
1743
1744 // Moving to the beginning of the line should put us at the start of the second line of
1745 // display text, i.e., the `j`.
1746 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1747 assert_eq!(
1748 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1749 editor.selections.display_ranges(cx)
1750 );
1751
1752 // Moving to the beginning of the line again should be a no-op.
1753 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1754 assert_eq!(
1755 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1756 editor.selections.display_ranges(cx)
1757 );
1758
1759 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1760 // next display line.
1761 editor.move_to_end_of_line(&move_to_end, window, cx);
1762 assert_eq!(
1763 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1764 editor.selections.display_ranges(cx)
1765 );
1766
1767 // Moving to the end of the line again should be a no-op.
1768 editor.move_to_end_of_line(&move_to_end, window, cx);
1769 assert_eq!(
1770 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1771 editor.selections.display_ranges(cx)
1772 );
1773 });
1774}
1775
1776#[gpui::test]
1777fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1778 init_test(cx, |_| {});
1779
1780 let editor = cx.add_window(|window, cx| {
1781 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1782 build_editor(buffer, window, cx)
1783 });
1784 _ = editor.update(cx, |editor, window, cx| {
1785 editor.change_selections(None, window, cx, |s| {
1786 s.select_display_ranges([
1787 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1788 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1789 ])
1790 });
1791
1792 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1793 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1794
1795 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1796 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1797
1798 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1799 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1800
1801 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1802 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1803
1804 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1805 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1806
1807 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1808 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1809
1810 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1811 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1812
1813 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1814 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1815
1816 editor.move_right(&MoveRight, window, cx);
1817 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1818 assert_selection_ranges(
1819 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1820 editor,
1821 cx,
1822 );
1823
1824 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1825 assert_selection_ranges(
1826 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1827 editor,
1828 cx,
1829 );
1830
1831 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1832 assert_selection_ranges(
1833 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1834 editor,
1835 cx,
1836 );
1837 });
1838}
1839
1840#[gpui::test]
1841fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1842 init_test(cx, |_| {});
1843
1844 let editor = cx.add_window(|window, cx| {
1845 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1846 build_editor(buffer, window, cx)
1847 });
1848
1849 _ = editor.update(cx, |editor, window, cx| {
1850 editor.set_wrap_width(Some(140.0.into()), cx);
1851 assert_eq!(
1852 editor.display_text(cx),
1853 "use one::{\n two::three::\n four::five\n};"
1854 );
1855
1856 editor.change_selections(None, window, cx, |s| {
1857 s.select_display_ranges([
1858 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1859 ]);
1860 });
1861
1862 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1863 assert_eq!(
1864 editor.selections.display_ranges(cx),
1865 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1866 );
1867
1868 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1869 assert_eq!(
1870 editor.selections.display_ranges(cx),
1871 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1872 );
1873
1874 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1875 assert_eq!(
1876 editor.selections.display_ranges(cx),
1877 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1878 );
1879
1880 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1881 assert_eq!(
1882 editor.selections.display_ranges(cx),
1883 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
1884 );
1885
1886 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1887 assert_eq!(
1888 editor.selections.display_ranges(cx),
1889 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1890 );
1891
1892 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1893 assert_eq!(
1894 editor.selections.display_ranges(cx),
1895 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1896 );
1897 });
1898}
1899
1900#[gpui::test]
1901async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1902 init_test(cx, |_| {});
1903 let mut cx = EditorTestContext::new(cx).await;
1904
1905 let line_height = cx.editor(|editor, window, _| {
1906 editor
1907 .style()
1908 .unwrap()
1909 .text
1910 .line_height_in_pixels(window.rem_size())
1911 });
1912 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
1913
1914 cx.set_state(
1915 &r#"ˇone
1916 two
1917
1918 three
1919 fourˇ
1920 five
1921
1922 six"#
1923 .unindent(),
1924 );
1925
1926 cx.update_editor(|editor, window, cx| {
1927 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1928 });
1929 cx.assert_editor_state(
1930 &r#"one
1931 two
1932 ˇ
1933 three
1934 four
1935 five
1936 ˇ
1937 six"#
1938 .unindent(),
1939 );
1940
1941 cx.update_editor(|editor, window, cx| {
1942 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1943 });
1944 cx.assert_editor_state(
1945 &r#"one
1946 two
1947
1948 three
1949 four
1950 five
1951 ˇ
1952 sixˇ"#
1953 .unindent(),
1954 );
1955
1956 cx.update_editor(|editor, window, cx| {
1957 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1958 });
1959 cx.assert_editor_state(
1960 &r#"one
1961 two
1962
1963 three
1964 four
1965 five
1966
1967 sixˇ"#
1968 .unindent(),
1969 );
1970
1971 cx.update_editor(|editor, window, cx| {
1972 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
1973 });
1974 cx.assert_editor_state(
1975 &r#"one
1976 two
1977
1978 three
1979 four
1980 five
1981 ˇ
1982 six"#
1983 .unindent(),
1984 );
1985
1986 cx.update_editor(|editor, window, cx| {
1987 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
1988 });
1989 cx.assert_editor_state(
1990 &r#"one
1991 two
1992 ˇ
1993 three
1994 four
1995 five
1996
1997 six"#
1998 .unindent(),
1999 );
2000
2001 cx.update_editor(|editor, window, cx| {
2002 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2003 });
2004 cx.assert_editor_state(
2005 &r#"ˇone
2006 two
2007
2008 three
2009 four
2010 five
2011
2012 six"#
2013 .unindent(),
2014 );
2015}
2016
2017#[gpui::test]
2018async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
2019 init_test(cx, |_| {});
2020 let mut cx = EditorTestContext::new(cx).await;
2021 let line_height = cx.editor(|editor, window, _| {
2022 editor
2023 .style()
2024 .unwrap()
2025 .text
2026 .line_height_in_pixels(window.rem_size())
2027 });
2028 let window = cx.window;
2029 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2030
2031 cx.set_state(
2032 r#"ˇone
2033 two
2034 three
2035 four
2036 five
2037 six
2038 seven
2039 eight
2040 nine
2041 ten
2042 "#,
2043 );
2044
2045 cx.update_editor(|editor, window, cx| {
2046 assert_eq!(
2047 editor.snapshot(window, cx).scroll_position(),
2048 gpui::Point::new(0., 0.)
2049 );
2050 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2051 assert_eq!(
2052 editor.snapshot(window, cx).scroll_position(),
2053 gpui::Point::new(0., 3.)
2054 );
2055 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2056 assert_eq!(
2057 editor.snapshot(window, cx).scroll_position(),
2058 gpui::Point::new(0., 6.)
2059 );
2060 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2061 assert_eq!(
2062 editor.snapshot(window, cx).scroll_position(),
2063 gpui::Point::new(0., 3.)
2064 );
2065
2066 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2067 assert_eq!(
2068 editor.snapshot(window, cx).scroll_position(),
2069 gpui::Point::new(0., 1.)
2070 );
2071 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2072 assert_eq!(
2073 editor.snapshot(window, cx).scroll_position(),
2074 gpui::Point::new(0., 3.)
2075 );
2076 });
2077}
2078
2079#[gpui::test]
2080async fn test_autoscroll(cx: &mut gpui::TestAppContext) {
2081 init_test(cx, |_| {});
2082 let mut cx = EditorTestContext::new(cx).await;
2083
2084 let line_height = cx.update_editor(|editor, window, cx| {
2085 editor.set_vertical_scroll_margin(2, cx);
2086 editor
2087 .style()
2088 .unwrap()
2089 .text
2090 .line_height_in_pixels(window.rem_size())
2091 });
2092 let window = cx.window;
2093 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2094
2095 cx.set_state(
2096 r#"ˇone
2097 two
2098 three
2099 four
2100 five
2101 six
2102 seven
2103 eight
2104 nine
2105 ten
2106 "#,
2107 );
2108 cx.update_editor(|editor, window, cx| {
2109 assert_eq!(
2110 editor.snapshot(window, cx).scroll_position(),
2111 gpui::Point::new(0., 0.0)
2112 );
2113 });
2114
2115 // Add a cursor below the visible area. Since both cursors cannot fit
2116 // on screen, the editor autoscrolls to reveal the newest cursor, and
2117 // allows the vertical scroll margin below that cursor.
2118 cx.update_editor(|editor, window, cx| {
2119 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2120 selections.select_ranges([
2121 Point::new(0, 0)..Point::new(0, 0),
2122 Point::new(6, 0)..Point::new(6, 0),
2123 ]);
2124 })
2125 });
2126 cx.update_editor(|editor, window, cx| {
2127 assert_eq!(
2128 editor.snapshot(window, cx).scroll_position(),
2129 gpui::Point::new(0., 3.0)
2130 );
2131 });
2132
2133 // Move down. The editor cursor scrolls down to track the newest cursor.
2134 cx.update_editor(|editor, window, cx| {
2135 editor.move_down(&Default::default(), window, cx);
2136 });
2137 cx.update_editor(|editor, window, cx| {
2138 assert_eq!(
2139 editor.snapshot(window, cx).scroll_position(),
2140 gpui::Point::new(0., 4.0)
2141 );
2142 });
2143
2144 // Add a cursor above the visible area. Since both cursors fit on screen,
2145 // the editor scrolls to show both.
2146 cx.update_editor(|editor, window, cx| {
2147 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2148 selections.select_ranges([
2149 Point::new(1, 0)..Point::new(1, 0),
2150 Point::new(6, 0)..Point::new(6, 0),
2151 ]);
2152 })
2153 });
2154 cx.update_editor(|editor, window, cx| {
2155 assert_eq!(
2156 editor.snapshot(window, cx).scroll_position(),
2157 gpui::Point::new(0., 1.0)
2158 );
2159 });
2160}
2161
2162#[gpui::test]
2163async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
2164 init_test(cx, |_| {});
2165 let mut cx = EditorTestContext::new(cx).await;
2166
2167 let line_height = cx.editor(|editor, window, _cx| {
2168 editor
2169 .style()
2170 .unwrap()
2171 .text
2172 .line_height_in_pixels(window.rem_size())
2173 });
2174 let window = cx.window;
2175 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2176 cx.set_state(
2177 &r#"
2178 ˇone
2179 two
2180 threeˇ
2181 four
2182 five
2183 six
2184 seven
2185 eight
2186 nine
2187 ten
2188 "#
2189 .unindent(),
2190 );
2191
2192 cx.update_editor(|editor, window, cx| {
2193 editor.move_page_down(&MovePageDown::default(), window, cx)
2194 });
2195 cx.assert_editor_state(
2196 &r#"
2197 one
2198 two
2199 three
2200 ˇfour
2201 five
2202 sixˇ
2203 seven
2204 eight
2205 nine
2206 ten
2207 "#
2208 .unindent(),
2209 );
2210
2211 cx.update_editor(|editor, window, cx| {
2212 editor.move_page_down(&MovePageDown::default(), window, cx)
2213 });
2214 cx.assert_editor_state(
2215 &r#"
2216 one
2217 two
2218 three
2219 four
2220 five
2221 six
2222 ˇseven
2223 eight
2224 nineˇ
2225 ten
2226 "#
2227 .unindent(),
2228 );
2229
2230 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2231 cx.assert_editor_state(
2232 &r#"
2233 one
2234 two
2235 three
2236 ˇfour
2237 five
2238 sixˇ
2239 seven
2240 eight
2241 nine
2242 ten
2243 "#
2244 .unindent(),
2245 );
2246
2247 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2248 cx.assert_editor_state(
2249 &r#"
2250 ˇone
2251 two
2252 threeˇ
2253 four
2254 five
2255 six
2256 seven
2257 eight
2258 nine
2259 ten
2260 "#
2261 .unindent(),
2262 );
2263
2264 // Test select collapsing
2265 cx.update_editor(|editor, window, cx| {
2266 editor.move_page_down(&MovePageDown::default(), window, cx);
2267 editor.move_page_down(&MovePageDown::default(), window, cx);
2268 editor.move_page_down(&MovePageDown::default(), window, cx);
2269 });
2270 cx.assert_editor_state(
2271 &r#"
2272 one
2273 two
2274 three
2275 four
2276 five
2277 six
2278 seven
2279 eight
2280 nine
2281 ˇten
2282 ˇ"#
2283 .unindent(),
2284 );
2285}
2286
2287#[gpui::test]
2288async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
2289 init_test(cx, |_| {});
2290 let mut cx = EditorTestContext::new(cx).await;
2291 cx.set_state("one «two threeˇ» four");
2292 cx.update_editor(|editor, window, cx| {
2293 editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx);
2294 assert_eq!(editor.text(cx), " four");
2295 });
2296}
2297
2298#[gpui::test]
2299fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2300 init_test(cx, |_| {});
2301
2302 let editor = cx.add_window(|window, cx| {
2303 let buffer = MultiBuffer::build_simple("one two three four", cx);
2304 build_editor(buffer.clone(), window, cx)
2305 });
2306
2307 _ = editor.update(cx, |editor, window, cx| {
2308 editor.change_selections(None, window, cx, |s| {
2309 s.select_display_ranges([
2310 // an empty selection - the preceding word fragment is deleted
2311 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2312 // characters selected - they are deleted
2313 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2314 ])
2315 });
2316 editor.delete_to_previous_word_start(
2317 &DeleteToPreviousWordStart {
2318 ignore_newlines: false,
2319 },
2320 window,
2321 cx,
2322 );
2323 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2324 });
2325
2326 _ = editor.update(cx, |editor, window, cx| {
2327 editor.change_selections(None, window, cx, |s| {
2328 s.select_display_ranges([
2329 // an empty selection - the following word fragment is deleted
2330 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2331 // characters selected - they are deleted
2332 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2333 ])
2334 });
2335 editor.delete_to_next_word_end(
2336 &DeleteToNextWordEnd {
2337 ignore_newlines: false,
2338 },
2339 window,
2340 cx,
2341 );
2342 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2343 });
2344}
2345
2346#[gpui::test]
2347fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2348 init_test(cx, |_| {});
2349
2350 let editor = cx.add_window(|window, cx| {
2351 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2352 build_editor(buffer.clone(), window, cx)
2353 });
2354 let del_to_prev_word_start = DeleteToPreviousWordStart {
2355 ignore_newlines: false,
2356 };
2357 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2358 ignore_newlines: true,
2359 };
2360
2361 _ = editor.update(cx, |editor, window, cx| {
2362 editor.change_selections(None, window, cx, |s| {
2363 s.select_display_ranges([
2364 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2365 ])
2366 });
2367 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2368 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
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");
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\n");
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");
2375 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2376 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
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(), "");
2379 });
2380}
2381
2382#[gpui::test]
2383fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2384 init_test(cx, |_| {});
2385
2386 let editor = cx.add_window(|window, cx| {
2387 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2388 build_editor(buffer.clone(), window, cx)
2389 });
2390 let del_to_next_word_end = DeleteToNextWordEnd {
2391 ignore_newlines: false,
2392 };
2393 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2394 ignore_newlines: true,
2395 };
2396
2397 _ = editor.update(cx, |editor, window, cx| {
2398 editor.change_selections(None, window, cx, |s| {
2399 s.select_display_ranges([
2400 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2401 ])
2402 });
2403 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2404 assert_eq!(
2405 editor.buffer.read(cx).read(cx).text(),
2406 "one\n two\nthree\n four"
2407 );
2408 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2409 assert_eq!(
2410 editor.buffer.read(cx).read(cx).text(),
2411 "\n two\nthree\n four"
2412 );
2413 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2414 assert_eq!(
2415 editor.buffer.read(cx).read(cx).text(),
2416 "two\nthree\n four"
2417 );
2418 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2419 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2420 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2421 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\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(), "");
2424 });
2425}
2426
2427#[gpui::test]
2428fn test_newline(cx: &mut TestAppContext) {
2429 init_test(cx, |_| {});
2430
2431 let editor = cx.add_window(|window, cx| {
2432 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2433 build_editor(buffer.clone(), window, cx)
2434 });
2435
2436 _ = editor.update(cx, |editor, window, cx| {
2437 editor.change_selections(None, window, cx, |s| {
2438 s.select_display_ranges([
2439 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2440 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2441 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2442 ])
2443 });
2444
2445 editor.newline(&Newline, window, cx);
2446 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2447 });
2448}
2449
2450#[gpui::test]
2451fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2452 init_test(cx, |_| {});
2453
2454 let editor = cx.add_window(|window, cx| {
2455 let buffer = MultiBuffer::build_simple(
2456 "
2457 a
2458 b(
2459 X
2460 )
2461 c(
2462 X
2463 )
2464 "
2465 .unindent()
2466 .as_str(),
2467 cx,
2468 );
2469 let mut editor = build_editor(buffer.clone(), window, cx);
2470 editor.change_selections(None, window, cx, |s| {
2471 s.select_ranges([
2472 Point::new(2, 4)..Point::new(2, 5),
2473 Point::new(5, 4)..Point::new(5, 5),
2474 ])
2475 });
2476 editor
2477 });
2478
2479 _ = editor.update(cx, |editor, window, cx| {
2480 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2481 editor.buffer.update(cx, |buffer, cx| {
2482 buffer.edit(
2483 [
2484 (Point::new(1, 2)..Point::new(3, 0), ""),
2485 (Point::new(4, 2)..Point::new(6, 0), ""),
2486 ],
2487 None,
2488 cx,
2489 );
2490 assert_eq!(
2491 buffer.read(cx).text(),
2492 "
2493 a
2494 b()
2495 c()
2496 "
2497 .unindent()
2498 );
2499 });
2500 assert_eq!(
2501 editor.selections.ranges(cx),
2502 &[
2503 Point::new(1, 2)..Point::new(1, 2),
2504 Point::new(2, 2)..Point::new(2, 2),
2505 ],
2506 );
2507
2508 editor.newline(&Newline, window, cx);
2509 assert_eq!(
2510 editor.text(cx),
2511 "
2512 a
2513 b(
2514 )
2515 c(
2516 )
2517 "
2518 .unindent()
2519 );
2520
2521 // The selections are moved after the inserted newlines
2522 assert_eq!(
2523 editor.selections.ranges(cx),
2524 &[
2525 Point::new(2, 0)..Point::new(2, 0),
2526 Point::new(4, 0)..Point::new(4, 0),
2527 ],
2528 );
2529 });
2530}
2531
2532#[gpui::test]
2533async fn test_newline_above(cx: &mut gpui::TestAppContext) {
2534 init_test(cx, |settings| {
2535 settings.defaults.tab_size = NonZeroU32::new(4)
2536 });
2537
2538 let language = Arc::new(
2539 Language::new(
2540 LanguageConfig::default(),
2541 Some(tree_sitter_rust::LANGUAGE.into()),
2542 )
2543 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2544 .unwrap(),
2545 );
2546
2547 let mut cx = EditorTestContext::new(cx).await;
2548 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2549 cx.set_state(indoc! {"
2550 const a: ˇA = (
2551 (ˇ
2552 «const_functionˇ»(ˇ),
2553 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2554 )ˇ
2555 ˇ);ˇ
2556 "});
2557
2558 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2559 cx.assert_editor_state(indoc! {"
2560 ˇ
2561 const a: A = (
2562 ˇ
2563 (
2564 ˇ
2565 ˇ
2566 const_function(),
2567 ˇ
2568 ˇ
2569 ˇ
2570 ˇ
2571 something_else,
2572 ˇ
2573 )
2574 ˇ
2575 ˇ
2576 );
2577 "});
2578}
2579
2580#[gpui::test]
2581async fn test_newline_below(cx: &mut gpui::TestAppContext) {
2582 init_test(cx, |settings| {
2583 settings.defaults.tab_size = NonZeroU32::new(4)
2584 });
2585
2586 let language = Arc::new(
2587 Language::new(
2588 LanguageConfig::default(),
2589 Some(tree_sitter_rust::LANGUAGE.into()),
2590 )
2591 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2592 .unwrap(),
2593 );
2594
2595 let mut cx = EditorTestContext::new(cx).await;
2596 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2597 cx.set_state(indoc! {"
2598 const a: ˇA = (
2599 (ˇ
2600 «const_functionˇ»(ˇ),
2601 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2602 )ˇ
2603 ˇ);ˇ
2604 "});
2605
2606 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2607 cx.assert_editor_state(indoc! {"
2608 const a: A = (
2609 ˇ
2610 (
2611 ˇ
2612 const_function(),
2613 ˇ
2614 ˇ
2615 something_else,
2616 ˇ
2617 ˇ
2618 ˇ
2619 ˇ
2620 )
2621 ˇ
2622 );
2623 ˇ
2624 ˇ
2625 "});
2626}
2627
2628#[gpui::test]
2629async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
2630 init_test(cx, |settings| {
2631 settings.defaults.tab_size = NonZeroU32::new(4)
2632 });
2633
2634 let language = Arc::new(Language::new(
2635 LanguageConfig {
2636 line_comments: vec!["//".into()],
2637 ..LanguageConfig::default()
2638 },
2639 None,
2640 ));
2641 {
2642 let mut cx = EditorTestContext::new(cx).await;
2643 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2644 cx.set_state(indoc! {"
2645 // Fooˇ
2646 "});
2647
2648 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2649 cx.assert_editor_state(indoc! {"
2650 // Foo
2651 //ˇ
2652 "});
2653 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2654 cx.set_state(indoc! {"
2655 ˇ// Foo
2656 "});
2657 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2658 cx.assert_editor_state(indoc! {"
2659
2660 ˇ// Foo
2661 "});
2662 }
2663 // Ensure that comment continuations can be disabled.
2664 update_test_language_settings(cx, |settings| {
2665 settings.defaults.extend_comment_on_newline = Some(false);
2666 });
2667 let mut cx = EditorTestContext::new(cx).await;
2668 cx.set_state(indoc! {"
2669 // Fooˇ
2670 "});
2671 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2672 cx.assert_editor_state(indoc! {"
2673 // Foo
2674 ˇ
2675 "});
2676}
2677
2678#[gpui::test]
2679fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2680 init_test(cx, |_| {});
2681
2682 let editor = cx.add_window(|window, cx| {
2683 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2684 let mut editor = build_editor(buffer.clone(), window, cx);
2685 editor.change_selections(None, window, cx, |s| {
2686 s.select_ranges([3..4, 11..12, 19..20])
2687 });
2688 editor
2689 });
2690
2691 _ = editor.update(cx, |editor, window, cx| {
2692 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2693 editor.buffer.update(cx, |buffer, cx| {
2694 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2695 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2696 });
2697 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2698
2699 editor.insert("Z", window, cx);
2700 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2701
2702 // The selections are moved after the inserted characters
2703 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2704 });
2705}
2706
2707#[gpui::test]
2708async fn test_tab(cx: &mut gpui::TestAppContext) {
2709 init_test(cx, |settings| {
2710 settings.defaults.tab_size = NonZeroU32::new(3)
2711 });
2712
2713 let mut cx = EditorTestContext::new(cx).await;
2714 cx.set_state(indoc! {"
2715 ˇabˇc
2716 ˇ🏀ˇ🏀ˇefg
2717 dˇ
2718 "});
2719 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2720 cx.assert_editor_state(indoc! {"
2721 ˇab ˇc
2722 ˇ🏀 ˇ🏀 ˇefg
2723 d ˇ
2724 "});
2725
2726 cx.set_state(indoc! {"
2727 a
2728 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2729 "});
2730 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2731 cx.assert_editor_state(indoc! {"
2732 a
2733 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2734 "});
2735}
2736
2737#[gpui::test]
2738async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) {
2739 init_test(cx, |_| {});
2740
2741 let mut cx = EditorTestContext::new(cx).await;
2742 let language = Arc::new(
2743 Language::new(
2744 LanguageConfig::default(),
2745 Some(tree_sitter_rust::LANGUAGE.into()),
2746 )
2747 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2748 .unwrap(),
2749 );
2750 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2751
2752 // cursors that are already at the suggested indent level insert
2753 // a soft tab. cursors that are to the left of the suggested indent
2754 // auto-indent their line.
2755 cx.set_state(indoc! {"
2756 ˇ
2757 const a: B = (
2758 c(
2759 d(
2760 ˇ
2761 )
2762 ˇ
2763 ˇ )
2764 );
2765 "});
2766 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2767 cx.assert_editor_state(indoc! {"
2768 ˇ
2769 const a: B = (
2770 c(
2771 d(
2772 ˇ
2773 )
2774 ˇ
2775 ˇ)
2776 );
2777 "});
2778
2779 // handle auto-indent when there are multiple cursors on the same line
2780 cx.set_state(indoc! {"
2781 const a: B = (
2782 c(
2783 ˇ ˇ
2784 ˇ )
2785 );
2786 "});
2787 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2788 cx.assert_editor_state(indoc! {"
2789 const a: B = (
2790 c(
2791 ˇ
2792 ˇ)
2793 );
2794 "});
2795}
2796
2797#[gpui::test]
2798async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
2799 init_test(cx, |settings| {
2800 settings.defaults.tab_size = NonZeroU32::new(4)
2801 });
2802
2803 let language = Arc::new(
2804 Language::new(
2805 LanguageConfig::default(),
2806 Some(tree_sitter_rust::LANGUAGE.into()),
2807 )
2808 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2809 .unwrap(),
2810 );
2811
2812 let mut cx = EditorTestContext::new(cx).await;
2813 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2814 cx.set_state(indoc! {"
2815 fn a() {
2816 if b {
2817 \t ˇc
2818 }
2819 }
2820 "});
2821
2822 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2823 cx.assert_editor_state(indoc! {"
2824 fn a() {
2825 if b {
2826 ˇc
2827 }
2828 }
2829 "});
2830}
2831
2832#[gpui::test]
2833async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
2834 init_test(cx, |settings| {
2835 settings.defaults.tab_size = NonZeroU32::new(4);
2836 });
2837
2838 let mut cx = EditorTestContext::new(cx).await;
2839
2840 cx.set_state(indoc! {"
2841 «oneˇ» «twoˇ»
2842 three
2843 four
2844 "});
2845 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2846 cx.assert_editor_state(indoc! {"
2847 «oneˇ» «twoˇ»
2848 three
2849 four
2850 "});
2851
2852 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2853 cx.assert_editor_state(indoc! {"
2854 «oneˇ» «twoˇ»
2855 three
2856 four
2857 "});
2858
2859 // select across line ending
2860 cx.set_state(indoc! {"
2861 one two
2862 t«hree
2863 ˇ» four
2864 "});
2865 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2866 cx.assert_editor_state(indoc! {"
2867 one two
2868 t«hree
2869 ˇ» four
2870 "});
2871
2872 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2873 cx.assert_editor_state(indoc! {"
2874 one two
2875 t«hree
2876 ˇ» four
2877 "});
2878
2879 // Ensure that indenting/outdenting works when the cursor is at column 0.
2880 cx.set_state(indoc! {"
2881 one two
2882 ˇthree
2883 four
2884 "});
2885 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2886 cx.assert_editor_state(indoc! {"
2887 one two
2888 ˇthree
2889 four
2890 "});
2891
2892 cx.set_state(indoc! {"
2893 one two
2894 ˇ three
2895 four
2896 "});
2897 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2898 cx.assert_editor_state(indoc! {"
2899 one two
2900 ˇthree
2901 four
2902 "});
2903}
2904
2905#[gpui::test]
2906async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
2907 init_test(cx, |settings| {
2908 settings.defaults.hard_tabs = Some(true);
2909 });
2910
2911 let mut cx = EditorTestContext::new(cx).await;
2912
2913 // select two ranges on one line
2914 cx.set_state(indoc! {"
2915 «oneˇ» «twoˇ»
2916 three
2917 four
2918 "});
2919 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2920 cx.assert_editor_state(indoc! {"
2921 \t«oneˇ» «twoˇ»
2922 three
2923 four
2924 "});
2925 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2926 cx.assert_editor_state(indoc! {"
2927 \t\t«oneˇ» «twoˇ»
2928 three
2929 four
2930 "});
2931 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2932 cx.assert_editor_state(indoc! {"
2933 \t«oneˇ» «twoˇ»
2934 three
2935 four
2936 "});
2937 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2938 cx.assert_editor_state(indoc! {"
2939 «oneˇ» «twoˇ»
2940 three
2941 four
2942 "});
2943
2944 // select across a line ending
2945 cx.set_state(indoc! {"
2946 one two
2947 t«hree
2948 ˇ»four
2949 "});
2950 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2951 cx.assert_editor_state(indoc! {"
2952 one two
2953 \tt«hree
2954 ˇ»four
2955 "});
2956 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2957 cx.assert_editor_state(indoc! {"
2958 one two
2959 \t\tt«hree
2960 ˇ»four
2961 "});
2962 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2963 cx.assert_editor_state(indoc! {"
2964 one two
2965 \tt«hree
2966 ˇ»four
2967 "});
2968 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2969 cx.assert_editor_state(indoc! {"
2970 one two
2971 t«hree
2972 ˇ»four
2973 "});
2974
2975 // Ensure that indenting/outdenting works when the cursor is at column 0.
2976 cx.set_state(indoc! {"
2977 one two
2978 ˇthree
2979 four
2980 "});
2981 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2982 cx.assert_editor_state(indoc! {"
2983 one two
2984 ˇthree
2985 four
2986 "});
2987 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2988 cx.assert_editor_state(indoc! {"
2989 one two
2990 \tˇthree
2991 four
2992 "});
2993 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2994 cx.assert_editor_state(indoc! {"
2995 one two
2996 ˇthree
2997 four
2998 "});
2999}
3000
3001#[gpui::test]
3002fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3003 init_test(cx, |settings| {
3004 settings.languages.extend([
3005 (
3006 "TOML".into(),
3007 LanguageSettingsContent {
3008 tab_size: NonZeroU32::new(2),
3009 ..Default::default()
3010 },
3011 ),
3012 (
3013 "Rust".into(),
3014 LanguageSettingsContent {
3015 tab_size: NonZeroU32::new(4),
3016 ..Default::default()
3017 },
3018 ),
3019 ]);
3020 });
3021
3022 let toml_language = Arc::new(Language::new(
3023 LanguageConfig {
3024 name: "TOML".into(),
3025 ..Default::default()
3026 },
3027 None,
3028 ));
3029 let rust_language = Arc::new(Language::new(
3030 LanguageConfig {
3031 name: "Rust".into(),
3032 ..Default::default()
3033 },
3034 None,
3035 ));
3036
3037 let toml_buffer =
3038 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3039 let rust_buffer =
3040 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3041 let multibuffer = cx.new(|cx| {
3042 let mut multibuffer = MultiBuffer::new(ReadWrite);
3043 multibuffer.push_excerpts(
3044 toml_buffer.clone(),
3045 [ExcerptRange {
3046 context: Point::new(0, 0)..Point::new(2, 0),
3047 primary: None,
3048 }],
3049 cx,
3050 );
3051 multibuffer.push_excerpts(
3052 rust_buffer.clone(),
3053 [ExcerptRange {
3054 context: Point::new(0, 0)..Point::new(1, 0),
3055 primary: None,
3056 }],
3057 cx,
3058 );
3059 multibuffer
3060 });
3061
3062 cx.add_window(|window, cx| {
3063 let mut editor = build_editor(multibuffer, window, cx);
3064
3065 assert_eq!(
3066 editor.text(cx),
3067 indoc! {"
3068 a = 1
3069 b = 2
3070
3071 const c: usize = 3;
3072 "}
3073 );
3074
3075 select_ranges(
3076 &mut editor,
3077 indoc! {"
3078 «aˇ» = 1
3079 b = 2
3080
3081 «const c:ˇ» usize = 3;
3082 "},
3083 window,
3084 cx,
3085 );
3086
3087 editor.tab(&Tab, window, cx);
3088 assert_text_with_selections(
3089 &mut editor,
3090 indoc! {"
3091 «aˇ» = 1
3092 b = 2
3093
3094 «const c:ˇ» usize = 3;
3095 "},
3096 cx,
3097 );
3098 editor.tab_prev(&TabPrev, window, cx);
3099 assert_text_with_selections(
3100 &mut editor,
3101 indoc! {"
3102 «aˇ» = 1
3103 b = 2
3104
3105 «const c:ˇ» usize = 3;
3106 "},
3107 cx,
3108 );
3109
3110 editor
3111 });
3112}
3113
3114#[gpui::test]
3115async fn test_backspace(cx: &mut gpui::TestAppContext) {
3116 init_test(cx, |_| {});
3117
3118 let mut cx = EditorTestContext::new(cx).await;
3119
3120 // Basic backspace
3121 cx.set_state(indoc! {"
3122 onˇe two three
3123 fou«rˇ» five six
3124 seven «ˇeight nine
3125 »ten
3126 "});
3127 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3128 cx.assert_editor_state(indoc! {"
3129 oˇe two three
3130 fouˇ five six
3131 seven ˇten
3132 "});
3133
3134 // Test backspace inside and around indents
3135 cx.set_state(indoc! {"
3136 zero
3137 ˇone
3138 ˇtwo
3139 ˇ ˇ ˇ three
3140 ˇ ˇ four
3141 "});
3142 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3143 cx.assert_editor_state(indoc! {"
3144 zero
3145 ˇone
3146 ˇtwo
3147 ˇ threeˇ four
3148 "});
3149
3150 // Test backspace with line_mode set to true
3151 cx.update_editor(|e, _, _| e.selections.line_mode = true);
3152 cx.set_state(indoc! {"
3153 The ˇquick ˇbrown
3154 fox jumps over
3155 the lazy dog
3156 ˇThe qu«ick bˇ»rown"});
3157 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3158 cx.assert_editor_state(indoc! {"
3159 ˇfox jumps over
3160 the lazy dogˇ"});
3161}
3162
3163#[gpui::test]
3164async fn test_delete(cx: &mut gpui::TestAppContext) {
3165 init_test(cx, |_| {});
3166
3167 let mut cx = EditorTestContext::new(cx).await;
3168 cx.set_state(indoc! {"
3169 onˇe two three
3170 fou«rˇ» five six
3171 seven «ˇeight nine
3172 »ten
3173 "});
3174 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3175 cx.assert_editor_state(indoc! {"
3176 onˇ two three
3177 fouˇ five six
3178 seven ˇten
3179 "});
3180
3181 // Test backspace with line_mode set to true
3182 cx.update_editor(|e, _, _| e.selections.line_mode = true);
3183 cx.set_state(indoc! {"
3184 The ˇquick ˇbrown
3185 fox «ˇjum»ps over
3186 the lazy dog
3187 ˇThe qu«ick bˇ»rown"});
3188 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3189 cx.assert_editor_state("ˇthe lazy dogˇ");
3190}
3191
3192#[gpui::test]
3193fn test_delete_line(cx: &mut TestAppContext) {
3194 init_test(cx, |_| {});
3195
3196 let editor = cx.add_window(|window, cx| {
3197 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3198 build_editor(buffer, window, cx)
3199 });
3200 _ = editor.update(cx, |editor, window, cx| {
3201 editor.change_selections(None, window, cx, |s| {
3202 s.select_display_ranges([
3203 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3204 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3205 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3206 ])
3207 });
3208 editor.delete_line(&DeleteLine, window, cx);
3209 assert_eq!(editor.display_text(cx), "ghi");
3210 assert_eq!(
3211 editor.selections.display_ranges(cx),
3212 vec![
3213 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3214 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3215 ]
3216 );
3217 });
3218
3219 let editor = cx.add_window(|window, cx| {
3220 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3221 build_editor(buffer, window, cx)
3222 });
3223 _ = editor.update(cx, |editor, window, cx| {
3224 editor.change_selections(None, window, cx, |s| {
3225 s.select_display_ranges([
3226 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3227 ])
3228 });
3229 editor.delete_line(&DeleteLine, window, cx);
3230 assert_eq!(editor.display_text(cx), "ghi\n");
3231 assert_eq!(
3232 editor.selections.display_ranges(cx),
3233 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3234 );
3235 });
3236}
3237
3238#[gpui::test]
3239fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3240 init_test(cx, |_| {});
3241
3242 cx.add_window(|window, cx| {
3243 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3244 let mut editor = build_editor(buffer.clone(), window, cx);
3245 let buffer = buffer.read(cx).as_singleton().unwrap();
3246
3247 assert_eq!(
3248 editor.selections.ranges::<Point>(cx),
3249 &[Point::new(0, 0)..Point::new(0, 0)]
3250 );
3251
3252 // When on single line, replace newline at end by space
3253 editor.join_lines(&JoinLines, window, cx);
3254 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3255 assert_eq!(
3256 editor.selections.ranges::<Point>(cx),
3257 &[Point::new(0, 3)..Point::new(0, 3)]
3258 );
3259
3260 // When multiple lines are selected, remove newlines that are spanned by the selection
3261 editor.change_selections(None, window, cx, |s| {
3262 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3263 });
3264 editor.join_lines(&JoinLines, window, cx);
3265 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3266 assert_eq!(
3267 editor.selections.ranges::<Point>(cx),
3268 &[Point::new(0, 11)..Point::new(0, 11)]
3269 );
3270
3271 // Undo should be transactional
3272 editor.undo(&Undo, window, cx);
3273 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3274 assert_eq!(
3275 editor.selections.ranges::<Point>(cx),
3276 &[Point::new(0, 5)..Point::new(2, 2)]
3277 );
3278
3279 // When joining an empty line don't insert a space
3280 editor.change_selections(None, window, cx, |s| {
3281 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3282 });
3283 editor.join_lines(&JoinLines, window, cx);
3284 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3285 assert_eq!(
3286 editor.selections.ranges::<Point>(cx),
3287 [Point::new(2, 3)..Point::new(2, 3)]
3288 );
3289
3290 // We can remove trailing newlines
3291 editor.join_lines(&JoinLines, window, cx);
3292 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3293 assert_eq!(
3294 editor.selections.ranges::<Point>(cx),
3295 [Point::new(2, 3)..Point::new(2, 3)]
3296 );
3297
3298 // We don't blow up on the last line
3299 editor.join_lines(&JoinLines, window, cx);
3300 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3301 assert_eq!(
3302 editor.selections.ranges::<Point>(cx),
3303 [Point::new(2, 3)..Point::new(2, 3)]
3304 );
3305
3306 // reset to test indentation
3307 editor.buffer.update(cx, |buffer, cx| {
3308 buffer.edit(
3309 [
3310 (Point::new(1, 0)..Point::new(1, 2), " "),
3311 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3312 ],
3313 None,
3314 cx,
3315 )
3316 });
3317
3318 // We remove any leading spaces
3319 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3320 editor.change_selections(None, window, cx, |s| {
3321 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3322 });
3323 editor.join_lines(&JoinLines, window, cx);
3324 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3325
3326 // We don't insert a space for a line containing only spaces
3327 editor.join_lines(&JoinLines, window, cx);
3328 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3329
3330 // We ignore any leading tabs
3331 editor.join_lines(&JoinLines, window, cx);
3332 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3333
3334 editor
3335 });
3336}
3337
3338#[gpui::test]
3339fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3340 init_test(cx, |_| {});
3341
3342 cx.add_window(|window, cx| {
3343 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3344 let mut editor = build_editor(buffer.clone(), window, cx);
3345 let buffer = buffer.read(cx).as_singleton().unwrap();
3346
3347 editor.change_selections(None, window, cx, |s| {
3348 s.select_ranges([
3349 Point::new(0, 2)..Point::new(1, 1),
3350 Point::new(1, 2)..Point::new(1, 2),
3351 Point::new(3, 1)..Point::new(3, 2),
3352 ])
3353 });
3354
3355 editor.join_lines(&JoinLines, window, cx);
3356 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3357
3358 assert_eq!(
3359 editor.selections.ranges::<Point>(cx),
3360 [
3361 Point::new(0, 7)..Point::new(0, 7),
3362 Point::new(1, 3)..Point::new(1, 3)
3363 ]
3364 );
3365 editor
3366 });
3367}
3368
3369#[gpui::test]
3370async fn test_join_lines_with_git_diff_base(
3371 executor: BackgroundExecutor,
3372 cx: &mut gpui::TestAppContext,
3373) {
3374 init_test(cx, |_| {});
3375
3376 let mut cx = EditorTestContext::new(cx).await;
3377
3378 let diff_base = r#"
3379 Line 0
3380 Line 1
3381 Line 2
3382 Line 3
3383 "#
3384 .unindent();
3385
3386 cx.set_state(
3387 &r#"
3388 ˇLine 0
3389 Line 1
3390 Line 2
3391 Line 3
3392 "#
3393 .unindent(),
3394 );
3395
3396 cx.set_diff_base(&diff_base);
3397 executor.run_until_parked();
3398
3399 // Join lines
3400 cx.update_editor(|editor, window, cx| {
3401 editor.join_lines(&JoinLines, window, cx);
3402 });
3403 executor.run_until_parked();
3404
3405 cx.assert_editor_state(
3406 &r#"
3407 Line 0ˇ Line 1
3408 Line 2
3409 Line 3
3410 "#
3411 .unindent(),
3412 );
3413 // Join again
3414 cx.update_editor(|editor, window, cx| {
3415 editor.join_lines(&JoinLines, window, cx);
3416 });
3417 executor.run_until_parked();
3418
3419 cx.assert_editor_state(
3420 &r#"
3421 Line 0 Line 1ˇ Line 2
3422 Line 3
3423 "#
3424 .unindent(),
3425 );
3426}
3427
3428#[gpui::test]
3429async fn test_custom_newlines_cause_no_false_positive_diffs(
3430 executor: BackgroundExecutor,
3431 cx: &mut gpui::TestAppContext,
3432) {
3433 init_test(cx, |_| {});
3434 let mut cx = EditorTestContext::new(cx).await;
3435 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3436 cx.set_diff_base("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3437 executor.run_until_parked();
3438
3439 cx.update_editor(|editor, window, cx| {
3440 let snapshot = editor.snapshot(window, cx);
3441 assert_eq!(
3442 snapshot
3443 .buffer_snapshot
3444 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3445 .collect::<Vec<_>>(),
3446 Vec::new(),
3447 "Should not have any diffs for files with custom newlines"
3448 );
3449 });
3450}
3451
3452#[gpui::test]
3453async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3454 init_test(cx, |_| {});
3455
3456 let mut cx = EditorTestContext::new(cx).await;
3457
3458 // Test sort_lines_case_insensitive()
3459 cx.set_state(indoc! {"
3460 «z
3461 y
3462 x
3463 Z
3464 Y
3465 Xˇ»
3466 "});
3467 cx.update_editor(|e, window, cx| {
3468 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3469 });
3470 cx.assert_editor_state(indoc! {"
3471 «x
3472 X
3473 y
3474 Y
3475 z
3476 Zˇ»
3477 "});
3478
3479 // Test reverse_lines()
3480 cx.set_state(indoc! {"
3481 «5
3482 4
3483 3
3484 2
3485 1ˇ»
3486 "});
3487 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3488 cx.assert_editor_state(indoc! {"
3489 «1
3490 2
3491 3
3492 4
3493 5ˇ»
3494 "});
3495
3496 // Skip testing shuffle_line()
3497
3498 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3499 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3500
3501 // Don't manipulate when cursor is on single line, but expand the selection
3502 cx.set_state(indoc! {"
3503 ddˇdd
3504 ccc
3505 bb
3506 a
3507 "});
3508 cx.update_editor(|e, window, cx| {
3509 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3510 });
3511 cx.assert_editor_state(indoc! {"
3512 «ddddˇ»
3513 ccc
3514 bb
3515 a
3516 "});
3517
3518 // Basic manipulate case
3519 // Start selection moves to column 0
3520 // End of selection shrinks to fit shorter line
3521 cx.set_state(indoc! {"
3522 dd«d
3523 ccc
3524 bb
3525 aaaaaˇ»
3526 "});
3527 cx.update_editor(|e, window, cx| {
3528 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3529 });
3530 cx.assert_editor_state(indoc! {"
3531 «aaaaa
3532 bb
3533 ccc
3534 dddˇ»
3535 "});
3536
3537 // Manipulate case with newlines
3538 cx.set_state(indoc! {"
3539 dd«d
3540 ccc
3541
3542 bb
3543 aaaaa
3544
3545 ˇ»
3546 "});
3547 cx.update_editor(|e, window, cx| {
3548 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3549 });
3550 cx.assert_editor_state(indoc! {"
3551 «
3552
3553 aaaaa
3554 bb
3555 ccc
3556 dddˇ»
3557
3558 "});
3559
3560 // Adding new line
3561 cx.set_state(indoc! {"
3562 aa«a
3563 bbˇ»b
3564 "});
3565 cx.update_editor(|e, window, cx| {
3566 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3567 });
3568 cx.assert_editor_state(indoc! {"
3569 «aaa
3570 bbb
3571 added_lineˇ»
3572 "});
3573
3574 // Removing line
3575 cx.set_state(indoc! {"
3576 aa«a
3577 bbbˇ»
3578 "});
3579 cx.update_editor(|e, window, cx| {
3580 e.manipulate_lines(window, cx, |lines| {
3581 lines.pop();
3582 })
3583 });
3584 cx.assert_editor_state(indoc! {"
3585 «aaaˇ»
3586 "});
3587
3588 // Removing all lines
3589 cx.set_state(indoc! {"
3590 aa«a
3591 bbbˇ»
3592 "});
3593 cx.update_editor(|e, window, cx| {
3594 e.manipulate_lines(window, cx, |lines| {
3595 lines.drain(..);
3596 })
3597 });
3598 cx.assert_editor_state(indoc! {"
3599 ˇ
3600 "});
3601}
3602
3603#[gpui::test]
3604async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3605 init_test(cx, |_| {});
3606
3607 let mut cx = EditorTestContext::new(cx).await;
3608
3609 // Consider continuous selection as single selection
3610 cx.set_state(indoc! {"
3611 Aaa«aa
3612 cˇ»c«c
3613 bb
3614 aaaˇ»aa
3615 "});
3616 cx.update_editor(|e, window, cx| {
3617 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3618 });
3619 cx.assert_editor_state(indoc! {"
3620 «Aaaaa
3621 ccc
3622 bb
3623 aaaaaˇ»
3624 "});
3625
3626 cx.set_state(indoc! {"
3627 Aaa«aa
3628 cˇ»c«c
3629 bb
3630 aaaˇ»aa
3631 "});
3632 cx.update_editor(|e, window, cx| {
3633 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3634 });
3635 cx.assert_editor_state(indoc! {"
3636 «Aaaaa
3637 ccc
3638 bbˇ»
3639 "});
3640
3641 // Consider non continuous selection as distinct dedup operations
3642 cx.set_state(indoc! {"
3643 «aaaaa
3644 bb
3645 aaaaa
3646 aaaaaˇ»
3647
3648 aaa«aaˇ»
3649 "});
3650 cx.update_editor(|e, window, cx| {
3651 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3652 });
3653 cx.assert_editor_state(indoc! {"
3654 «aaaaa
3655 bbˇ»
3656
3657 «aaaaaˇ»
3658 "});
3659}
3660
3661#[gpui::test]
3662async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3663 init_test(cx, |_| {});
3664
3665 let mut cx = EditorTestContext::new(cx).await;
3666
3667 cx.set_state(indoc! {"
3668 «Aaa
3669 aAa
3670 Aaaˇ»
3671 "});
3672 cx.update_editor(|e, window, cx| {
3673 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3674 });
3675 cx.assert_editor_state(indoc! {"
3676 «Aaa
3677 aAaˇ»
3678 "});
3679
3680 cx.set_state(indoc! {"
3681 «Aaa
3682 aAa
3683 aaAˇ»
3684 "});
3685 cx.update_editor(|e, window, cx| {
3686 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3687 });
3688 cx.assert_editor_state(indoc! {"
3689 «Aaaˇ»
3690 "});
3691}
3692
3693#[gpui::test]
3694async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3695 init_test(cx, |_| {});
3696
3697 let mut cx = EditorTestContext::new(cx).await;
3698
3699 // Manipulate with multiple selections on a single line
3700 cx.set_state(indoc! {"
3701 dd«dd
3702 cˇ»c«c
3703 bb
3704 aaaˇ»aa
3705 "});
3706 cx.update_editor(|e, window, cx| {
3707 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3708 });
3709 cx.assert_editor_state(indoc! {"
3710 «aaaaa
3711 bb
3712 ccc
3713 ddddˇ»
3714 "});
3715
3716 // Manipulate with multiple disjoin selections
3717 cx.set_state(indoc! {"
3718 5«
3719 4
3720 3
3721 2
3722 1ˇ»
3723
3724 dd«dd
3725 ccc
3726 bb
3727 aaaˇ»aa
3728 "});
3729 cx.update_editor(|e, window, cx| {
3730 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3731 });
3732 cx.assert_editor_state(indoc! {"
3733 «1
3734 2
3735 3
3736 4
3737 5ˇ»
3738
3739 «aaaaa
3740 bb
3741 ccc
3742 ddddˇ»
3743 "});
3744
3745 // Adding lines on each selection
3746 cx.set_state(indoc! {"
3747 2«
3748 1ˇ»
3749
3750 bb«bb
3751 aaaˇ»aa
3752 "});
3753 cx.update_editor(|e, window, cx| {
3754 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3755 });
3756 cx.assert_editor_state(indoc! {"
3757 «2
3758 1
3759 added lineˇ»
3760
3761 «bbbb
3762 aaaaa
3763 added lineˇ»
3764 "});
3765
3766 // Removing lines on each selection
3767 cx.set_state(indoc! {"
3768 2«
3769 1ˇ»
3770
3771 bb«bb
3772 aaaˇ»aa
3773 "});
3774 cx.update_editor(|e, window, cx| {
3775 e.manipulate_lines(window, cx, |lines| {
3776 lines.pop();
3777 })
3778 });
3779 cx.assert_editor_state(indoc! {"
3780 «2ˇ»
3781
3782 «bbbbˇ»
3783 "});
3784}
3785
3786#[gpui::test]
3787async fn test_manipulate_text(cx: &mut TestAppContext) {
3788 init_test(cx, |_| {});
3789
3790 let mut cx = EditorTestContext::new(cx).await;
3791
3792 // Test convert_to_upper_case()
3793 cx.set_state(indoc! {"
3794 «hello worldˇ»
3795 "});
3796 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3797 cx.assert_editor_state(indoc! {"
3798 «HELLO WORLDˇ»
3799 "});
3800
3801 // Test convert_to_lower_case()
3802 cx.set_state(indoc! {"
3803 «HELLO WORLDˇ»
3804 "});
3805 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3806 cx.assert_editor_state(indoc! {"
3807 «hello worldˇ»
3808 "});
3809
3810 // Test multiple line, single selection case
3811 cx.set_state(indoc! {"
3812 «The quick brown
3813 fox jumps over
3814 the lazy dogˇ»
3815 "});
3816 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3817 cx.assert_editor_state(indoc! {"
3818 «The Quick Brown
3819 Fox Jumps Over
3820 The Lazy Dogˇ»
3821 "});
3822
3823 // Test multiple line, single selection case
3824 cx.set_state(indoc! {"
3825 «The quick brown
3826 fox jumps over
3827 the lazy dogˇ»
3828 "});
3829 cx.update_editor(|e, window, cx| {
3830 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
3831 });
3832 cx.assert_editor_state(indoc! {"
3833 «TheQuickBrown
3834 FoxJumpsOver
3835 TheLazyDogˇ»
3836 "});
3837
3838 // From here on out, test more complex cases of manipulate_text()
3839
3840 // Test no selection case - should affect words cursors are in
3841 // Cursor at beginning, middle, and end of word
3842 cx.set_state(indoc! {"
3843 ˇhello big beauˇtiful worldˇ
3844 "});
3845 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3846 cx.assert_editor_state(indoc! {"
3847 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
3848 "});
3849
3850 // Test multiple selections on a single line and across multiple lines
3851 cx.set_state(indoc! {"
3852 «Theˇ» quick «brown
3853 foxˇ» jumps «overˇ»
3854 the «lazyˇ» dog
3855 "});
3856 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3857 cx.assert_editor_state(indoc! {"
3858 «THEˇ» quick «BROWN
3859 FOXˇ» jumps «OVERˇ»
3860 the «LAZYˇ» dog
3861 "});
3862
3863 // Test case where text length grows
3864 cx.set_state(indoc! {"
3865 «tschüߡ»
3866 "});
3867 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3868 cx.assert_editor_state(indoc! {"
3869 «TSCHÜSSˇ»
3870 "});
3871
3872 // Test to make sure we don't crash when text shrinks
3873 cx.set_state(indoc! {"
3874 aaa_bbbˇ
3875 "});
3876 cx.update_editor(|e, window, cx| {
3877 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3878 });
3879 cx.assert_editor_state(indoc! {"
3880 «aaaBbbˇ»
3881 "});
3882
3883 // Test to make sure we all aware of the fact that each word can grow and shrink
3884 // Final selections should be aware of this fact
3885 cx.set_state(indoc! {"
3886 aaa_bˇbb bbˇb_ccc ˇccc_ddd
3887 "});
3888 cx.update_editor(|e, window, cx| {
3889 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3890 });
3891 cx.assert_editor_state(indoc! {"
3892 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
3893 "});
3894
3895 cx.set_state(indoc! {"
3896 «hElLo, WoRld!ˇ»
3897 "});
3898 cx.update_editor(|e, window, cx| {
3899 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
3900 });
3901 cx.assert_editor_state(indoc! {"
3902 «HeLlO, wOrLD!ˇ»
3903 "});
3904}
3905
3906#[gpui::test]
3907fn test_duplicate_line(cx: &mut TestAppContext) {
3908 init_test(cx, |_| {});
3909
3910 let editor = cx.add_window(|window, cx| {
3911 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3912 build_editor(buffer, window, cx)
3913 });
3914 _ = editor.update(cx, |editor, window, cx| {
3915 editor.change_selections(None, window, cx, |s| {
3916 s.select_display_ranges([
3917 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3918 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3919 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
3920 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3921 ])
3922 });
3923 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
3924 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
3925 assert_eq!(
3926 editor.selections.display_ranges(cx),
3927 vec![
3928 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3929 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3930 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3931 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
3932 ]
3933 );
3934 });
3935
3936 let editor = cx.add_window(|window, cx| {
3937 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3938 build_editor(buffer, window, cx)
3939 });
3940 _ = editor.update(cx, |editor, window, cx| {
3941 editor.change_selections(None, window, cx, |s| {
3942 s.select_display_ranges([
3943 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
3944 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
3945 ])
3946 });
3947 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
3948 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
3949 assert_eq!(
3950 editor.selections.display_ranges(cx),
3951 vec![
3952 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
3953 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
3954 ]
3955 );
3956 });
3957
3958 // With `move_upwards` the selections stay in place, except for
3959 // the lines inserted above them
3960 let editor = cx.add_window(|window, cx| {
3961 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3962 build_editor(buffer, window, cx)
3963 });
3964 _ = editor.update(cx, |editor, window, cx| {
3965 editor.change_selections(None, window, cx, |s| {
3966 s.select_display_ranges([
3967 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3968 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3969 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
3970 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3971 ])
3972 });
3973 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
3974 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
3975 assert_eq!(
3976 editor.selections.display_ranges(cx),
3977 vec![
3978 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3979 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3980 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
3981 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
3982 ]
3983 );
3984 });
3985
3986 let editor = cx.add_window(|window, cx| {
3987 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3988 build_editor(buffer, window, cx)
3989 });
3990 _ = editor.update(cx, |editor, window, cx| {
3991 editor.change_selections(None, window, cx, |s| {
3992 s.select_display_ranges([
3993 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
3994 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
3995 ])
3996 });
3997 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
3998 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
3999 assert_eq!(
4000 editor.selections.display_ranges(cx),
4001 vec![
4002 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4003 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4004 ]
4005 );
4006 });
4007
4008 let editor = cx.add_window(|window, cx| {
4009 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4010 build_editor(buffer, window, cx)
4011 });
4012 _ = editor.update(cx, |editor, window, cx| {
4013 editor.change_selections(None, window, cx, |s| {
4014 s.select_display_ranges([
4015 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4016 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4017 ])
4018 });
4019 editor.duplicate_selection(&DuplicateSelection, window, cx);
4020 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4021 assert_eq!(
4022 editor.selections.display_ranges(cx),
4023 vec![
4024 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4025 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4026 ]
4027 );
4028 });
4029}
4030
4031#[gpui::test]
4032fn test_move_line_up_down(cx: &mut TestAppContext) {
4033 init_test(cx, |_| {});
4034
4035 let editor = cx.add_window(|window, cx| {
4036 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4037 build_editor(buffer, window, cx)
4038 });
4039 _ = editor.update(cx, |editor, window, cx| {
4040 editor.fold_creases(
4041 vec![
4042 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4043 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4044 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4045 ],
4046 true,
4047 window,
4048 cx,
4049 );
4050 editor.change_selections(None, window, cx, |s| {
4051 s.select_display_ranges([
4052 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4053 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4054 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4055 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4056 ])
4057 });
4058 assert_eq!(
4059 editor.display_text(cx),
4060 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4061 );
4062
4063 editor.move_line_up(&MoveLineUp, window, cx);
4064 assert_eq!(
4065 editor.display_text(cx),
4066 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4067 );
4068 assert_eq!(
4069 editor.selections.display_ranges(cx),
4070 vec![
4071 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4072 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4073 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4074 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4075 ]
4076 );
4077 });
4078
4079 _ = editor.update(cx, |editor, window, cx| {
4080 editor.move_line_down(&MoveLineDown, window, cx);
4081 assert_eq!(
4082 editor.display_text(cx),
4083 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4084 );
4085 assert_eq!(
4086 editor.selections.display_ranges(cx),
4087 vec![
4088 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4089 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4090 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4091 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4092 ]
4093 );
4094 });
4095
4096 _ = editor.update(cx, |editor, window, cx| {
4097 editor.move_line_down(&MoveLineDown, window, cx);
4098 assert_eq!(
4099 editor.display_text(cx),
4100 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4101 );
4102 assert_eq!(
4103 editor.selections.display_ranges(cx),
4104 vec![
4105 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4106 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4107 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4108 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4109 ]
4110 );
4111 });
4112
4113 _ = editor.update(cx, |editor, window, cx| {
4114 editor.move_line_up(&MoveLineUp, window, cx);
4115 assert_eq!(
4116 editor.display_text(cx),
4117 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4118 );
4119 assert_eq!(
4120 editor.selections.display_ranges(cx),
4121 vec![
4122 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4123 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4124 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4125 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4126 ]
4127 );
4128 });
4129}
4130
4131#[gpui::test]
4132fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4133 init_test(cx, |_| {});
4134
4135 let editor = cx.add_window(|window, cx| {
4136 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4137 build_editor(buffer, window, cx)
4138 });
4139 _ = editor.update(cx, |editor, window, cx| {
4140 let snapshot = editor.buffer.read(cx).snapshot(cx);
4141 editor.insert_blocks(
4142 [BlockProperties {
4143 style: BlockStyle::Fixed,
4144 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4145 height: 1,
4146 render: Arc::new(|_| div().into_any()),
4147 priority: 0,
4148 }],
4149 Some(Autoscroll::fit()),
4150 cx,
4151 );
4152 editor.change_selections(None, window, cx, |s| {
4153 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4154 });
4155 editor.move_line_down(&MoveLineDown, window, cx);
4156 });
4157}
4158
4159#[gpui::test]
4160async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4161 init_test(cx, |_| {});
4162
4163 let mut cx = EditorTestContext::new(cx).await;
4164 cx.set_state(
4165 &"
4166 ˇzero
4167 one
4168 two
4169 three
4170 four
4171 five
4172 "
4173 .unindent(),
4174 );
4175
4176 // Create a four-line block that replaces three lines of text.
4177 cx.update_editor(|editor, window, cx| {
4178 let snapshot = editor.snapshot(window, cx);
4179 let snapshot = &snapshot.buffer_snapshot;
4180 let placement = BlockPlacement::Replace(
4181 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4182 );
4183 editor.insert_blocks(
4184 [BlockProperties {
4185 placement,
4186 height: 4,
4187 style: BlockStyle::Sticky,
4188 render: Arc::new(|_| gpui::div().into_any_element()),
4189 priority: 0,
4190 }],
4191 None,
4192 cx,
4193 );
4194 });
4195
4196 // Move down so that the cursor touches the block.
4197 cx.update_editor(|editor, window, cx| {
4198 editor.move_down(&Default::default(), window, cx);
4199 });
4200 cx.assert_editor_state(
4201 &"
4202 zero
4203 «one
4204 two
4205 threeˇ»
4206 four
4207 five
4208 "
4209 .unindent(),
4210 );
4211
4212 // Move down past the block.
4213 cx.update_editor(|editor, window, cx| {
4214 editor.move_down(&Default::default(), window, cx);
4215 });
4216 cx.assert_editor_state(
4217 &"
4218 zero
4219 one
4220 two
4221 three
4222 ˇfour
4223 five
4224 "
4225 .unindent(),
4226 );
4227}
4228
4229#[gpui::test]
4230fn test_transpose(cx: &mut TestAppContext) {
4231 init_test(cx, |_| {});
4232
4233 _ = cx.add_window(|window, cx| {
4234 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4235 editor.set_style(EditorStyle::default(), window, cx);
4236 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4237 editor.transpose(&Default::default(), window, cx);
4238 assert_eq!(editor.text(cx), "bac");
4239 assert_eq!(editor.selections.ranges(cx), [2..2]);
4240
4241 editor.transpose(&Default::default(), window, cx);
4242 assert_eq!(editor.text(cx), "bca");
4243 assert_eq!(editor.selections.ranges(cx), [3..3]);
4244
4245 editor.transpose(&Default::default(), window, cx);
4246 assert_eq!(editor.text(cx), "bac");
4247 assert_eq!(editor.selections.ranges(cx), [3..3]);
4248
4249 editor
4250 });
4251
4252 _ = cx.add_window(|window, cx| {
4253 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4254 editor.set_style(EditorStyle::default(), window, cx);
4255 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4256 editor.transpose(&Default::default(), window, cx);
4257 assert_eq!(editor.text(cx), "acb\nde");
4258 assert_eq!(editor.selections.ranges(cx), [3..3]);
4259
4260 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4261 editor.transpose(&Default::default(), window, cx);
4262 assert_eq!(editor.text(cx), "acbd\ne");
4263 assert_eq!(editor.selections.ranges(cx), [5..5]);
4264
4265 editor.transpose(&Default::default(), window, cx);
4266 assert_eq!(editor.text(cx), "acbde\n");
4267 assert_eq!(editor.selections.ranges(cx), [6..6]);
4268
4269 editor.transpose(&Default::default(), window, cx);
4270 assert_eq!(editor.text(cx), "acbd\ne");
4271 assert_eq!(editor.selections.ranges(cx), [6..6]);
4272
4273 editor
4274 });
4275
4276 _ = cx.add_window(|window, cx| {
4277 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4278 editor.set_style(EditorStyle::default(), window, cx);
4279 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4280 editor.transpose(&Default::default(), window, cx);
4281 assert_eq!(editor.text(cx), "bacd\ne");
4282 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4283
4284 editor.transpose(&Default::default(), window, cx);
4285 assert_eq!(editor.text(cx), "bcade\n");
4286 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4287
4288 editor.transpose(&Default::default(), window, cx);
4289 assert_eq!(editor.text(cx), "bcda\ne");
4290 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4291
4292 editor.transpose(&Default::default(), window, cx);
4293 assert_eq!(editor.text(cx), "bcade\n");
4294 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4295
4296 editor.transpose(&Default::default(), window, cx);
4297 assert_eq!(editor.text(cx), "bcaed\n");
4298 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4299
4300 editor
4301 });
4302
4303 _ = cx.add_window(|window, cx| {
4304 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4305 editor.set_style(EditorStyle::default(), window, cx);
4306 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4307 editor.transpose(&Default::default(), window, cx);
4308 assert_eq!(editor.text(cx), "🏀🍐✋");
4309 assert_eq!(editor.selections.ranges(cx), [8..8]);
4310
4311 editor.transpose(&Default::default(), window, cx);
4312 assert_eq!(editor.text(cx), "🏀✋🍐");
4313 assert_eq!(editor.selections.ranges(cx), [11..11]);
4314
4315 editor.transpose(&Default::default(), window, cx);
4316 assert_eq!(editor.text(cx), "🏀🍐✋");
4317 assert_eq!(editor.selections.ranges(cx), [11..11]);
4318
4319 editor
4320 });
4321}
4322
4323#[gpui::test]
4324async fn test_rewrap(cx: &mut TestAppContext) {
4325 init_test(cx, |_| {});
4326
4327 let mut cx = EditorTestContext::new(cx).await;
4328
4329 let language_with_c_comments = Arc::new(Language::new(
4330 LanguageConfig {
4331 line_comments: vec!["// ".into()],
4332 ..LanguageConfig::default()
4333 },
4334 None,
4335 ));
4336 let language_with_pound_comments = Arc::new(Language::new(
4337 LanguageConfig {
4338 line_comments: vec!["# ".into()],
4339 ..LanguageConfig::default()
4340 },
4341 None,
4342 ));
4343 let markdown_language = Arc::new(Language::new(
4344 LanguageConfig {
4345 name: "Markdown".into(),
4346 ..LanguageConfig::default()
4347 },
4348 None,
4349 ));
4350 let language_with_doc_comments = Arc::new(Language::new(
4351 LanguageConfig {
4352 line_comments: vec!["// ".into(), "/// ".into()],
4353 ..LanguageConfig::default()
4354 },
4355 Some(tree_sitter_rust::LANGUAGE.into()),
4356 ));
4357
4358 let plaintext_language = Arc::new(Language::new(
4359 LanguageConfig {
4360 name: "Plain Text".into(),
4361 ..LanguageConfig::default()
4362 },
4363 None,
4364 ));
4365
4366 assert_rewrap(
4367 indoc! {"
4368 // ˇ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.
4369 "},
4370 indoc! {"
4371 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4372 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4373 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4374 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4375 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4376 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4377 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4378 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4379 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4380 // porttitor id. Aliquam id accumsan eros.
4381 "},
4382 language_with_c_comments.clone(),
4383 &mut cx,
4384 );
4385
4386 // Test that rewrapping works inside of a selection
4387 assert_rewrap(
4388 indoc! {"
4389 «// 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.ˇ»
4390 "},
4391 indoc! {"
4392 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4393 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4394 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4395 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4396 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4397 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4398 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4399 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4400 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4401 // porttitor id. Aliquam id accumsan eros.ˇ»
4402 "},
4403 language_with_c_comments.clone(),
4404 &mut cx,
4405 );
4406
4407 // Test that cursors that expand to the same region are collapsed.
4408 assert_rewrap(
4409 indoc! {"
4410 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4411 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4412 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4413 // ˇ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.
4414 "},
4415 indoc! {"
4416 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4417 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4418 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4419 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4420 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4421 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4422 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4423 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4424 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4425 // porttitor id. Aliquam id accumsan eros.
4426 "},
4427 language_with_c_comments.clone(),
4428 &mut cx,
4429 );
4430
4431 // Test that non-contiguous selections are treated separately.
4432 assert_rewrap(
4433 indoc! {"
4434 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4435 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4436 //
4437 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4438 // ˇ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.
4439 "},
4440 indoc! {"
4441 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4442 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4443 // auctor, eu lacinia sapien scelerisque.
4444 //
4445 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4446 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4447 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4448 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4449 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4450 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4451 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4452 "},
4453 language_with_c_comments.clone(),
4454 &mut cx,
4455 );
4456
4457 // Test that different comment prefixes are supported.
4458 assert_rewrap(
4459 indoc! {"
4460 # ˇ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.
4461 "},
4462 indoc! {"
4463 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4464 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4465 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4466 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4467 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4468 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4469 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4470 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4471 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4472 # accumsan eros.
4473 "},
4474 language_with_pound_comments.clone(),
4475 &mut cx,
4476 );
4477
4478 // Test that rewrapping is ignored outside of comments in most languages.
4479 assert_rewrap(
4480 indoc! {"
4481 /// Adds two numbers.
4482 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4483 fn add(a: u32, b: u32) -> u32 {
4484 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ˇ
4485 }
4486 "},
4487 indoc! {"
4488 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4489 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4490 fn add(a: u32, b: u32) -> u32 {
4491 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ˇ
4492 }
4493 "},
4494 language_with_doc_comments.clone(),
4495 &mut cx,
4496 );
4497
4498 // Test that rewrapping works in Markdown and Plain Text languages.
4499 assert_rewrap(
4500 indoc! {"
4501 # Hello
4502
4503 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.
4504 "},
4505 indoc! {"
4506 # Hello
4507
4508 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4509 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4510 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4511 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4512 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4513 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4514 Integer sit amet scelerisque nisi.
4515 "},
4516 markdown_language,
4517 &mut cx,
4518 );
4519
4520 assert_rewrap(
4521 indoc! {"
4522 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.
4523 "},
4524 indoc! {"
4525 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4526 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4527 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4528 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4529 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4530 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4531 Integer sit amet scelerisque nisi.
4532 "},
4533 plaintext_language,
4534 &mut cx,
4535 );
4536
4537 // Test rewrapping unaligned comments in a selection.
4538 assert_rewrap(
4539 indoc! {"
4540 fn foo() {
4541 if true {
4542 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4543 // Praesent semper egestas tellus id dignissim.ˇ»
4544 do_something();
4545 } else {
4546 //
4547 }
4548 }
4549 "},
4550 indoc! {"
4551 fn foo() {
4552 if true {
4553 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4554 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4555 // egestas tellus id dignissim.ˇ»
4556 do_something();
4557 } else {
4558 //
4559 }
4560 }
4561 "},
4562 language_with_doc_comments.clone(),
4563 &mut cx,
4564 );
4565
4566 assert_rewrap(
4567 indoc! {"
4568 fn foo() {
4569 if true {
4570 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4571 // Praesent semper egestas tellus id dignissim.»
4572 do_something();
4573 } else {
4574 //
4575 }
4576
4577 }
4578 "},
4579 indoc! {"
4580 fn foo() {
4581 if true {
4582 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4583 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4584 // egestas tellus id dignissim.»
4585 do_something();
4586 } else {
4587 //
4588 }
4589
4590 }
4591 "},
4592 language_with_doc_comments.clone(),
4593 &mut cx,
4594 );
4595
4596 #[track_caller]
4597 fn assert_rewrap(
4598 unwrapped_text: &str,
4599 wrapped_text: &str,
4600 language: Arc<Language>,
4601 cx: &mut EditorTestContext,
4602 ) {
4603 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4604 cx.set_state(unwrapped_text);
4605 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4606 cx.assert_editor_state(wrapped_text);
4607 }
4608}
4609
4610#[gpui::test]
4611async fn test_clipboard(cx: &mut gpui::TestAppContext) {
4612 init_test(cx, |_| {});
4613
4614 let mut cx = EditorTestContext::new(cx).await;
4615
4616 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4617 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4618 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4619
4620 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4621 cx.set_state("two ˇfour ˇsix ˇ");
4622 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4623 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4624
4625 // Paste again but with only two cursors. Since the number of cursors doesn't
4626 // match the number of slices in the clipboard, the entire clipboard text
4627 // is pasted at each cursor.
4628 cx.set_state("ˇtwo one✅ four three six five ˇ");
4629 cx.update_editor(|e, window, cx| {
4630 e.handle_input("( ", window, cx);
4631 e.paste(&Paste, window, cx);
4632 e.handle_input(") ", window, cx);
4633 });
4634 cx.assert_editor_state(
4635 &([
4636 "( one✅ ",
4637 "three ",
4638 "five ) ˇtwo one✅ four three six five ( one✅ ",
4639 "three ",
4640 "five ) ˇ",
4641 ]
4642 .join("\n")),
4643 );
4644
4645 // Cut with three selections, one of which is full-line.
4646 cx.set_state(indoc! {"
4647 1«2ˇ»3
4648 4ˇ567
4649 «8ˇ»9"});
4650 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4651 cx.assert_editor_state(indoc! {"
4652 1ˇ3
4653 ˇ9"});
4654
4655 // Paste with three selections, noticing how the copied selection that was full-line
4656 // gets inserted before the second cursor.
4657 cx.set_state(indoc! {"
4658 1ˇ3
4659 9ˇ
4660 «oˇ»ne"});
4661 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4662 cx.assert_editor_state(indoc! {"
4663 12ˇ3
4664 4567
4665 9ˇ
4666 8ˇne"});
4667
4668 // Copy with a single cursor only, which writes the whole line into the clipboard.
4669 cx.set_state(indoc! {"
4670 The quick brown
4671 fox juˇmps over
4672 the lazy dog"});
4673 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4674 assert_eq!(
4675 cx.read_from_clipboard()
4676 .and_then(|item| item.text().as_deref().map(str::to_string)),
4677 Some("fox jumps over\n".to_string())
4678 );
4679
4680 // Paste with three selections, noticing how the copied full-line selection is inserted
4681 // before the empty selections but replaces the selection that is non-empty.
4682 cx.set_state(indoc! {"
4683 Tˇhe quick brown
4684 «foˇ»x jumps over
4685 tˇhe lazy dog"});
4686 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4687 cx.assert_editor_state(indoc! {"
4688 fox jumps over
4689 Tˇhe quick brown
4690 fox jumps over
4691 ˇx jumps over
4692 fox jumps over
4693 tˇhe lazy dog"});
4694}
4695
4696#[gpui::test]
4697async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
4698 init_test(cx, |_| {});
4699
4700 let mut cx = EditorTestContext::new(cx).await;
4701 let language = Arc::new(Language::new(
4702 LanguageConfig::default(),
4703 Some(tree_sitter_rust::LANGUAGE.into()),
4704 ));
4705 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4706
4707 // Cut an indented block, without the leading whitespace.
4708 cx.set_state(indoc! {"
4709 const a: B = (
4710 c(),
4711 «d(
4712 e,
4713 f
4714 )ˇ»
4715 );
4716 "});
4717 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4718 cx.assert_editor_state(indoc! {"
4719 const a: B = (
4720 c(),
4721 ˇ
4722 );
4723 "});
4724
4725 // Paste it at the same position.
4726 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4727 cx.assert_editor_state(indoc! {"
4728 const a: B = (
4729 c(),
4730 d(
4731 e,
4732 f
4733 )ˇ
4734 );
4735 "});
4736
4737 // Paste it at a line with a lower indent level.
4738 cx.set_state(indoc! {"
4739 ˇ
4740 const a: B = (
4741 c(),
4742 );
4743 "});
4744 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4745 cx.assert_editor_state(indoc! {"
4746 d(
4747 e,
4748 f
4749 )ˇ
4750 const a: B = (
4751 c(),
4752 );
4753 "});
4754
4755 // Cut an indented block, with the leading whitespace.
4756 cx.set_state(indoc! {"
4757 const a: B = (
4758 c(),
4759 « d(
4760 e,
4761 f
4762 )
4763 ˇ»);
4764 "});
4765 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4766 cx.assert_editor_state(indoc! {"
4767 const a: B = (
4768 c(),
4769 ˇ);
4770 "});
4771
4772 // Paste it at the same position.
4773 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4774 cx.assert_editor_state(indoc! {"
4775 const a: B = (
4776 c(),
4777 d(
4778 e,
4779 f
4780 )
4781 ˇ);
4782 "});
4783
4784 // Paste it at a line with a higher indent level.
4785 cx.set_state(indoc! {"
4786 const a: B = (
4787 c(),
4788 d(
4789 e,
4790 fˇ
4791 )
4792 );
4793 "});
4794 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4795 cx.assert_editor_state(indoc! {"
4796 const a: B = (
4797 c(),
4798 d(
4799 e,
4800 f d(
4801 e,
4802 f
4803 )
4804 ˇ
4805 )
4806 );
4807 "});
4808}
4809
4810#[gpui::test]
4811fn test_select_all(cx: &mut TestAppContext) {
4812 init_test(cx, |_| {});
4813
4814 let editor = cx.add_window(|window, cx| {
4815 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
4816 build_editor(buffer, window, cx)
4817 });
4818 _ = editor.update(cx, |editor, window, cx| {
4819 editor.select_all(&SelectAll, window, cx);
4820 assert_eq!(
4821 editor.selections.display_ranges(cx),
4822 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
4823 );
4824 });
4825}
4826
4827#[gpui::test]
4828fn test_select_line(cx: &mut TestAppContext) {
4829 init_test(cx, |_| {});
4830
4831 let editor = cx.add_window(|window, cx| {
4832 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
4833 build_editor(buffer, window, cx)
4834 });
4835 _ = editor.update(cx, |editor, window, cx| {
4836 editor.change_selections(None, window, cx, |s| {
4837 s.select_display_ranges([
4838 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4839 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4840 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4841 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
4842 ])
4843 });
4844 editor.select_line(&SelectLine, window, cx);
4845 assert_eq!(
4846 editor.selections.display_ranges(cx),
4847 vec![
4848 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
4849 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
4850 ]
4851 );
4852 });
4853
4854 _ = editor.update(cx, |editor, window, cx| {
4855 editor.select_line(&SelectLine, window, cx);
4856 assert_eq!(
4857 editor.selections.display_ranges(cx),
4858 vec![
4859 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
4860 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
4861 ]
4862 );
4863 });
4864
4865 _ = editor.update(cx, |editor, window, cx| {
4866 editor.select_line(&SelectLine, window, cx);
4867 assert_eq!(
4868 editor.selections.display_ranges(cx),
4869 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
4870 );
4871 });
4872}
4873
4874#[gpui::test]
4875fn test_split_selection_into_lines(cx: &mut TestAppContext) {
4876 init_test(cx, |_| {});
4877
4878 let editor = cx.add_window(|window, cx| {
4879 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
4880 build_editor(buffer, window, cx)
4881 });
4882 _ = editor.update(cx, |editor, window, cx| {
4883 editor.fold_creases(
4884 vec![
4885 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4886 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4887 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4888 ],
4889 true,
4890 window,
4891 cx,
4892 );
4893 editor.change_selections(None, window, cx, |s| {
4894 s.select_display_ranges([
4895 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4896 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4897 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4898 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
4899 ])
4900 });
4901 assert_eq!(
4902 editor.display_text(cx),
4903 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
4904 );
4905 });
4906
4907 _ = editor.update(cx, |editor, window, cx| {
4908 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
4909 assert_eq!(
4910 editor.display_text(cx),
4911 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
4912 );
4913 assert_eq!(
4914 editor.selections.display_ranges(cx),
4915 [
4916 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4917 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4918 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4919 DisplayPoint::new(DisplayRow(5), 4)..DisplayPoint::new(DisplayRow(5), 4)
4920 ]
4921 );
4922 });
4923
4924 _ = editor.update(cx, |editor, window, cx| {
4925 editor.change_selections(None, window, cx, |s| {
4926 s.select_display_ranges([
4927 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
4928 ])
4929 });
4930 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
4931 assert_eq!(
4932 editor.display_text(cx),
4933 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
4934 );
4935 assert_eq!(
4936 editor.selections.display_ranges(cx),
4937 [
4938 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
4939 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
4940 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
4941 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
4942 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
4943 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
4944 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5),
4945 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(7), 0)
4946 ]
4947 );
4948 });
4949}
4950
4951#[gpui::test]
4952async fn test_add_selection_above_below(cx: &mut TestAppContext) {
4953 init_test(cx, |_| {});
4954
4955 let mut cx = EditorTestContext::new(cx).await;
4956
4957 // let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx);
4958 cx.set_state(indoc!(
4959 r#"abc
4960 defˇghi
4961
4962 jk
4963 nlmo
4964 "#
4965 ));
4966
4967 cx.update_editor(|editor, window, cx| {
4968 editor.add_selection_above(&Default::default(), window, cx);
4969 });
4970
4971 cx.assert_editor_state(indoc!(
4972 r#"abcˇ
4973 defˇghi
4974
4975 jk
4976 nlmo
4977 "#
4978 ));
4979
4980 cx.update_editor(|editor, window, cx| {
4981 editor.add_selection_above(&Default::default(), window, cx);
4982 });
4983
4984 cx.assert_editor_state(indoc!(
4985 r#"abcˇ
4986 defˇghi
4987
4988 jk
4989 nlmo
4990 "#
4991 ));
4992
4993 cx.update_editor(|editor, window, cx| {
4994 editor.add_selection_below(&Default::default(), window, cx);
4995 });
4996
4997 cx.assert_editor_state(indoc!(
4998 r#"abc
4999 defˇghi
5000
5001 jk
5002 nlmo
5003 "#
5004 ));
5005
5006 cx.update_editor(|editor, window, cx| {
5007 editor.undo_selection(&Default::default(), window, cx);
5008 });
5009
5010 cx.assert_editor_state(indoc!(
5011 r#"abcˇ
5012 defˇghi
5013
5014 jk
5015 nlmo
5016 "#
5017 ));
5018
5019 cx.update_editor(|editor, window, cx| {
5020 editor.redo_selection(&Default::default(), window, cx);
5021 });
5022
5023 cx.assert_editor_state(indoc!(
5024 r#"abc
5025 defˇghi
5026
5027 jk
5028 nlmo
5029 "#
5030 ));
5031
5032 cx.update_editor(|editor, window, cx| {
5033 editor.add_selection_below(&Default::default(), window, cx);
5034 });
5035
5036 cx.assert_editor_state(indoc!(
5037 r#"abc
5038 defˇghi
5039
5040 jk
5041 nlmˇo
5042 "#
5043 ));
5044
5045 cx.update_editor(|editor, window, cx| {
5046 editor.add_selection_below(&Default::default(), window, cx);
5047 });
5048
5049 cx.assert_editor_state(indoc!(
5050 r#"abc
5051 defˇghi
5052
5053 jk
5054 nlmˇo
5055 "#
5056 ));
5057
5058 // change selections
5059 cx.set_state(indoc!(
5060 r#"abc
5061 def«ˇg»hi
5062
5063 jk
5064 nlmo
5065 "#
5066 ));
5067
5068 cx.update_editor(|editor, window, cx| {
5069 editor.add_selection_below(&Default::default(), window, cx);
5070 });
5071
5072 cx.assert_editor_state(indoc!(
5073 r#"abc
5074 def«ˇg»hi
5075
5076 jk
5077 nlm«ˇo»
5078 "#
5079 ));
5080
5081 cx.update_editor(|editor, window, cx| {
5082 editor.add_selection_below(&Default::default(), window, cx);
5083 });
5084
5085 cx.assert_editor_state(indoc!(
5086 r#"abc
5087 def«ˇg»hi
5088
5089 jk
5090 nlm«ˇo»
5091 "#
5092 ));
5093
5094 cx.update_editor(|editor, window, cx| {
5095 editor.add_selection_above(&Default::default(), window, cx);
5096 });
5097
5098 cx.assert_editor_state(indoc!(
5099 r#"abc
5100 def«ˇg»hi
5101
5102 jk
5103 nlmo
5104 "#
5105 ));
5106
5107 cx.update_editor(|editor, window, cx| {
5108 editor.add_selection_above(&Default::default(), window, cx);
5109 });
5110
5111 cx.assert_editor_state(indoc!(
5112 r#"abc
5113 def«ˇg»hi
5114
5115 jk
5116 nlmo
5117 "#
5118 ));
5119
5120 // Change selections again
5121 cx.set_state(indoc!(
5122 r#"a«bc
5123 defgˇ»hi
5124
5125 jk
5126 nlmo
5127 "#
5128 ));
5129
5130 cx.update_editor(|editor, window, cx| {
5131 editor.add_selection_below(&Default::default(), window, cx);
5132 });
5133
5134 cx.assert_editor_state(indoc!(
5135 r#"a«bcˇ»
5136 d«efgˇ»hi
5137
5138 j«kˇ»
5139 nlmo
5140 "#
5141 ));
5142
5143 cx.update_editor(|editor, window, cx| {
5144 editor.add_selection_below(&Default::default(), window, cx);
5145 });
5146 cx.assert_editor_state(indoc!(
5147 r#"a«bcˇ»
5148 d«efgˇ»hi
5149
5150 j«kˇ»
5151 n«lmoˇ»
5152 "#
5153 ));
5154 cx.update_editor(|editor, window, cx| {
5155 editor.add_selection_above(&Default::default(), window, cx);
5156 });
5157
5158 cx.assert_editor_state(indoc!(
5159 r#"a«bcˇ»
5160 d«efgˇ»hi
5161
5162 j«kˇ»
5163 nlmo
5164 "#
5165 ));
5166
5167 // Change selections again
5168 cx.set_state(indoc!(
5169 r#"abc
5170 d«ˇefghi
5171
5172 jk
5173 nlm»o
5174 "#
5175 ));
5176
5177 cx.update_editor(|editor, window, cx| {
5178 editor.add_selection_above(&Default::default(), window, cx);
5179 });
5180
5181 cx.assert_editor_state(indoc!(
5182 r#"a«ˇbc»
5183 d«ˇef»ghi
5184
5185 j«ˇk»
5186 n«ˇlm»o
5187 "#
5188 ));
5189
5190 cx.update_editor(|editor, window, cx| {
5191 editor.add_selection_below(&Default::default(), window, cx);
5192 });
5193
5194 cx.assert_editor_state(indoc!(
5195 r#"abc
5196 d«ˇef»ghi
5197
5198 j«ˇk»
5199 n«ˇlm»o
5200 "#
5201 ));
5202}
5203
5204#[gpui::test]
5205async fn test_select_next(cx: &mut gpui::TestAppContext) {
5206 init_test(cx, |_| {});
5207
5208 let mut cx = EditorTestContext::new(cx).await;
5209 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5210
5211 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5212 .unwrap();
5213 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5214
5215 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5216 .unwrap();
5217 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5218
5219 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5220 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5221
5222 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5223 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5224
5225 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5226 .unwrap();
5227 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5228
5229 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5230 .unwrap();
5231 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5232}
5233
5234#[gpui::test]
5235async fn test_select_all_matches(cx: &mut gpui::TestAppContext) {
5236 init_test(cx, |_| {});
5237
5238 let mut cx = EditorTestContext::new(cx).await;
5239
5240 // Test caret-only selections
5241 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5242
5243 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5244 .unwrap();
5245 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5246
5247 // Test left-to-right selections
5248 cx.set_state("abc\n«abcˇ»\nabc");
5249
5250 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5251 .unwrap();
5252 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5253
5254 // Test right-to-left selections
5255 cx.set_state("abc\n«ˇabc»\nabc");
5256
5257 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5258 .unwrap();
5259 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5260}
5261
5262#[gpui::test]
5263async fn test_select_next_with_multiple_carets(cx: &mut gpui::TestAppContext) {
5264 init_test(cx, |_| {});
5265
5266 let mut cx = EditorTestContext::new(cx).await;
5267 cx.set_state(
5268 r#"let foo = 2;
5269lˇet foo = 2;
5270let fooˇ = 2;
5271let foo = 2;
5272let foo = ˇ2;"#,
5273 );
5274
5275 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5276 .unwrap();
5277 cx.assert_editor_state(
5278 r#"let foo = 2;
5279«letˇ» foo = 2;
5280let «fooˇ» = 2;
5281let foo = 2;
5282let foo = «2ˇ»;"#,
5283 );
5284
5285 // noop for multiple selections with different contents
5286 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5287 .unwrap();
5288 cx.assert_editor_state(
5289 r#"let foo = 2;
5290«letˇ» foo = 2;
5291let «fooˇ» = 2;
5292let foo = 2;
5293let foo = «2ˇ»;"#,
5294 );
5295}
5296
5297#[gpui::test]
5298async fn test_select_previous_multibuffer(cx: &mut gpui::TestAppContext) {
5299 init_test(cx, |_| {});
5300
5301 let mut cx =
5302 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
5303
5304 cx.assert_editor_state(indoc! {"
5305 ˇbbb
5306 ccc
5307
5308 bbb
5309 ccc
5310 "});
5311 cx.dispatch_action(SelectPrevious::default());
5312 cx.assert_editor_state(indoc! {"
5313 «bbbˇ»
5314 ccc
5315
5316 bbb
5317 ccc
5318 "});
5319 cx.dispatch_action(SelectPrevious::default());
5320 cx.assert_editor_state(indoc! {"
5321 «bbbˇ»
5322 ccc
5323
5324 «bbbˇ»
5325 ccc
5326 "});
5327}
5328
5329#[gpui::test]
5330async fn test_select_previous_with_single_caret(cx: &mut gpui::TestAppContext) {
5331 init_test(cx, |_| {});
5332
5333 let mut cx = EditorTestContext::new(cx).await;
5334 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5335
5336 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5337 .unwrap();
5338 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5339
5340 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5341 .unwrap();
5342 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5343
5344 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5345 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5346
5347 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5348 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5349
5350 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5351 .unwrap();
5352 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
5353
5354 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5355 .unwrap();
5356 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
5357
5358 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5359 .unwrap();
5360 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5361}
5362
5363#[gpui::test]
5364async fn test_select_previous_with_multiple_carets(cx: &mut gpui::TestAppContext) {
5365 init_test(cx, |_| {});
5366
5367 let mut cx = EditorTestContext::new(cx).await;
5368 cx.set_state(
5369 r#"let foo = 2;
5370lˇet foo = 2;
5371let fooˇ = 2;
5372let foo = 2;
5373let foo = ˇ2;"#,
5374 );
5375
5376 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5377 .unwrap();
5378 cx.assert_editor_state(
5379 r#"let foo = 2;
5380«letˇ» foo = 2;
5381let «fooˇ» = 2;
5382let foo = 2;
5383let foo = «2ˇ»;"#,
5384 );
5385
5386 // noop for multiple selections with different contents
5387 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5388 .unwrap();
5389 cx.assert_editor_state(
5390 r#"let foo = 2;
5391«letˇ» foo = 2;
5392let «fooˇ» = 2;
5393let foo = 2;
5394let foo = «2ˇ»;"#,
5395 );
5396}
5397
5398#[gpui::test]
5399async fn test_select_previous_with_single_selection(cx: &mut gpui::TestAppContext) {
5400 init_test(cx, |_| {});
5401
5402 let mut cx = EditorTestContext::new(cx).await;
5403 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5404
5405 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5406 .unwrap();
5407 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5408
5409 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5410 .unwrap();
5411 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5412
5413 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5414 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5415
5416 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5417 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5418
5419 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5420 .unwrap();
5421 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
5422
5423 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5424 .unwrap();
5425 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5426}
5427
5428#[gpui::test]
5429async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
5430 init_test(cx, |_| {});
5431
5432 let language = Arc::new(Language::new(
5433 LanguageConfig::default(),
5434 Some(tree_sitter_rust::LANGUAGE.into()),
5435 ));
5436
5437 let text = r#"
5438 use mod1::mod2::{mod3, mod4};
5439
5440 fn fn_1(param1: bool, param2: &str) {
5441 let var1 = "text";
5442 }
5443 "#
5444 .unindent();
5445
5446 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5447 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5448 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5449
5450 editor
5451 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5452 .await;
5453
5454 editor.update_in(cx, |editor, window, cx| {
5455 editor.change_selections(None, window, cx, |s| {
5456 s.select_display_ranges([
5457 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
5458 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
5459 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
5460 ]);
5461 });
5462 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5463 });
5464 editor.update(cx, |editor, cx| {
5465 assert_text_with_selections(
5466 editor,
5467 indoc! {r#"
5468 use mod1::mod2::{mod3, «mod4ˇ»};
5469
5470 fn fn_1«ˇ(param1: bool, param2: &str)» {
5471 let var1 = "«textˇ»";
5472 }
5473 "#},
5474 cx,
5475 );
5476 });
5477
5478 editor.update_in(cx, |editor, window, cx| {
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 assert_eq!(
5499 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
5500 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
5501 );
5502
5503 // Trying to expand the selected syntax node one more time has no effect.
5504 editor.update_in(cx, |editor, window, cx| {
5505 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5506 });
5507 assert_eq!(
5508 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
5509 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
5510 );
5511
5512 editor.update_in(cx, |editor, window, cx| {
5513 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5514 });
5515 editor.update(cx, |editor, cx| {
5516 assert_text_with_selections(
5517 editor,
5518 indoc! {r#"
5519 use mod1::mod2::«{mod3, mod4}ˇ»;
5520
5521 «ˇfn fn_1(param1: bool, param2: &str) {
5522 let var1 = "text";
5523 }»
5524 "#},
5525 cx,
5526 );
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, mo«ˇ»d4};
5554
5555 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
5556 let var1 = "te«ˇ»xt";
5557 }
5558 "#},
5559 cx,
5560 );
5561 });
5562
5563 // Trying to shrink the selected syntax node one more time has no effect.
5564 editor.update_in(cx, |editor, window, cx| {
5565 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5566 });
5567 editor.update_in(cx, |editor, _, cx| {
5568 assert_text_with_selections(
5569 editor,
5570 indoc! {r#"
5571 use mod1::mod2::{mod3, mo«ˇ»d4};
5572
5573 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
5574 let var1 = "te«ˇ»xt";
5575 }
5576 "#},
5577 cx,
5578 );
5579 });
5580
5581 // Ensure that we keep expanding the selection if the larger selection starts or ends within
5582 // a fold.
5583 editor.update_in(cx, |editor, window, cx| {
5584 editor.fold_creases(
5585 vec![
5586 Crease::simple(
5587 Point::new(0, 21)..Point::new(0, 24),
5588 FoldPlaceholder::test(),
5589 ),
5590 Crease::simple(
5591 Point::new(3, 20)..Point::new(3, 22),
5592 FoldPlaceholder::test(),
5593 ),
5594 ],
5595 true,
5596 window,
5597 cx,
5598 );
5599 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5600 });
5601 editor.update(cx, |editor, cx| {
5602 assert_text_with_selections(
5603 editor,
5604 indoc! {r#"
5605 use mod1::mod2::«{mod3, mod4}ˇ»;
5606
5607 fn fn_1«ˇ(param1: bool, param2: &str)» {
5608 «let var1 = "text";ˇ»
5609 }
5610 "#},
5611 cx,
5612 );
5613 });
5614}
5615
5616#[gpui::test]
5617async fn test_fold_function_bodies(cx: &mut gpui::TestAppContext) {
5618 init_test(cx, |_| {});
5619
5620 let base_text = r#"
5621 impl A {
5622 // this is an uncommitted comment
5623
5624 fn b() {
5625 c();
5626 }
5627
5628 // this is another uncommitted comment
5629
5630 fn d() {
5631 // e
5632 // f
5633 }
5634 }
5635
5636 fn g() {
5637 // h
5638 }
5639 "#
5640 .unindent();
5641
5642 let text = r#"
5643 ˇimpl A {
5644
5645 fn b() {
5646 c();
5647 }
5648
5649 fn d() {
5650 // e
5651 // f
5652 }
5653 }
5654
5655 fn g() {
5656 // h
5657 }
5658 "#
5659 .unindent();
5660
5661 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
5662 cx.set_state(&text);
5663 cx.set_diff_base(&base_text);
5664 cx.update_editor(|editor, window, cx| {
5665 editor.expand_all_diff_hunks(&Default::default(), window, cx);
5666 });
5667
5668 cx.assert_state_with_diff(
5669 "
5670 ˇimpl A {
5671 - // this is an uncommitted comment
5672
5673 fn b() {
5674 c();
5675 }
5676
5677 - // this is another uncommitted comment
5678 -
5679 fn d() {
5680 // e
5681 // f
5682 }
5683 }
5684
5685 fn g() {
5686 // h
5687 }
5688 "
5689 .unindent(),
5690 );
5691
5692 let expected_display_text = "
5693 impl A {
5694 // this is an uncommitted comment
5695
5696 fn b() {
5697 ⋯
5698 }
5699
5700 // this is another uncommitted comment
5701
5702 fn d() {
5703 ⋯
5704 }
5705 }
5706
5707 fn g() {
5708 ⋯
5709 }
5710 "
5711 .unindent();
5712
5713 cx.update_editor(|editor, window, cx| {
5714 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
5715 assert_eq!(editor.display_text(cx), expected_display_text);
5716 });
5717}
5718
5719#[gpui::test]
5720async fn test_autoindent(cx: &mut gpui::TestAppContext) {
5721 init_test(cx, |_| {});
5722
5723 let language = Arc::new(
5724 Language::new(
5725 LanguageConfig {
5726 brackets: BracketPairConfig {
5727 pairs: vec![
5728 BracketPair {
5729 start: "{".to_string(),
5730 end: "}".to_string(),
5731 close: false,
5732 surround: false,
5733 newline: true,
5734 },
5735 BracketPair {
5736 start: "(".to_string(),
5737 end: ")".to_string(),
5738 close: false,
5739 surround: false,
5740 newline: true,
5741 },
5742 ],
5743 ..Default::default()
5744 },
5745 ..Default::default()
5746 },
5747 Some(tree_sitter_rust::LANGUAGE.into()),
5748 )
5749 .with_indents_query(
5750 r#"
5751 (_ "(" ")" @end) @indent
5752 (_ "{" "}" @end) @indent
5753 "#,
5754 )
5755 .unwrap(),
5756 );
5757
5758 let text = "fn a() {}";
5759
5760 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5761 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5762 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5763 editor
5764 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5765 .await;
5766
5767 editor.update_in(cx, |editor, window, cx| {
5768 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
5769 editor.newline(&Newline, window, cx);
5770 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
5771 assert_eq!(
5772 editor.selections.ranges(cx),
5773 &[
5774 Point::new(1, 4)..Point::new(1, 4),
5775 Point::new(3, 4)..Point::new(3, 4),
5776 Point::new(5, 0)..Point::new(5, 0)
5777 ]
5778 );
5779 });
5780}
5781
5782#[gpui::test]
5783async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
5784 init_test(cx, |_| {});
5785
5786 {
5787 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
5788 cx.set_state(indoc! {"
5789 impl A {
5790
5791 fn b() {}
5792
5793 «fn c() {
5794
5795 }ˇ»
5796 }
5797 "});
5798
5799 cx.update_editor(|editor, window, cx| {
5800 editor.autoindent(&Default::default(), window, cx);
5801 });
5802
5803 cx.assert_editor_state(indoc! {"
5804 impl A {
5805
5806 fn b() {}
5807
5808 «fn c() {
5809
5810 }ˇ»
5811 }
5812 "});
5813 }
5814
5815 {
5816 let mut cx = EditorTestContext::new_multibuffer(
5817 cx,
5818 [indoc! { "
5819 impl A {
5820 «
5821 // a
5822 fn b(){}
5823 »
5824 «
5825 }
5826 fn c(){}
5827 »
5828 "}],
5829 );
5830
5831 let buffer = cx.update_editor(|editor, _, cx| {
5832 let buffer = editor.buffer().update(cx, |buffer, _| {
5833 buffer.all_buffers().iter().next().unwrap().clone()
5834 });
5835 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5836 buffer
5837 });
5838
5839 cx.run_until_parked();
5840 cx.update_editor(|editor, window, cx| {
5841 editor.select_all(&Default::default(), window, cx);
5842 editor.autoindent(&Default::default(), window, cx)
5843 });
5844 cx.run_until_parked();
5845
5846 cx.update(|_, cx| {
5847 pretty_assertions::assert_eq!(
5848 buffer.read(cx).text(),
5849 indoc! { "
5850 impl A {
5851
5852 // a
5853 fn b(){}
5854
5855
5856 }
5857 fn c(){}
5858
5859 " }
5860 )
5861 });
5862 }
5863}
5864
5865#[gpui::test]
5866async fn test_autoclose_and_auto_surround_pairs(cx: &mut gpui::TestAppContext) {
5867 init_test(cx, |_| {});
5868
5869 let mut cx = EditorTestContext::new(cx).await;
5870
5871 let language = Arc::new(Language::new(
5872 LanguageConfig {
5873 brackets: BracketPairConfig {
5874 pairs: vec![
5875 BracketPair {
5876 start: "{".to_string(),
5877 end: "}".to_string(),
5878 close: true,
5879 surround: true,
5880 newline: true,
5881 },
5882 BracketPair {
5883 start: "(".to_string(),
5884 end: ")".to_string(),
5885 close: true,
5886 surround: true,
5887 newline: true,
5888 },
5889 BracketPair {
5890 start: "/*".to_string(),
5891 end: " */".to_string(),
5892 close: true,
5893 surround: true,
5894 newline: true,
5895 },
5896 BracketPair {
5897 start: "[".to_string(),
5898 end: "]".to_string(),
5899 close: false,
5900 surround: false,
5901 newline: true,
5902 },
5903 BracketPair {
5904 start: "\"".to_string(),
5905 end: "\"".to_string(),
5906 close: true,
5907 surround: true,
5908 newline: false,
5909 },
5910 BracketPair {
5911 start: "<".to_string(),
5912 end: ">".to_string(),
5913 close: false,
5914 surround: true,
5915 newline: true,
5916 },
5917 ],
5918 ..Default::default()
5919 },
5920 autoclose_before: "})]".to_string(),
5921 ..Default::default()
5922 },
5923 Some(tree_sitter_rust::LANGUAGE.into()),
5924 ));
5925
5926 cx.language_registry().add(language.clone());
5927 cx.update_buffer(|buffer, cx| {
5928 buffer.set_language(Some(language), cx);
5929 });
5930
5931 cx.set_state(
5932 &r#"
5933 🏀ˇ
5934 εˇ
5935 ❤️ˇ
5936 "#
5937 .unindent(),
5938 );
5939
5940 // autoclose multiple nested brackets at multiple cursors
5941 cx.update_editor(|editor, window, cx| {
5942 editor.handle_input("{", window, cx);
5943 editor.handle_input("{", window, cx);
5944 editor.handle_input("{", window, cx);
5945 });
5946 cx.assert_editor_state(
5947 &"
5948 🏀{{{ˇ}}}
5949 ε{{{ˇ}}}
5950 ❤️{{{ˇ}}}
5951 "
5952 .unindent(),
5953 );
5954
5955 // insert a different closing bracket
5956 cx.update_editor(|editor, window, cx| {
5957 editor.handle_input(")", window, cx);
5958 });
5959 cx.assert_editor_state(
5960 &"
5961 🏀{{{)ˇ}}}
5962 ε{{{)ˇ}}}
5963 ❤️{{{)ˇ}}}
5964 "
5965 .unindent(),
5966 );
5967
5968 // skip over the auto-closed brackets when typing a closing bracket
5969 cx.update_editor(|editor, window, cx| {
5970 editor.move_right(&MoveRight, window, cx);
5971 editor.handle_input("}", window, cx);
5972 editor.handle_input("}", window, cx);
5973 editor.handle_input("}", window, cx);
5974 });
5975 cx.assert_editor_state(
5976 &"
5977 🏀{{{)}}}}ˇ
5978 ε{{{)}}}}ˇ
5979 ❤️{{{)}}}}ˇ
5980 "
5981 .unindent(),
5982 );
5983
5984 // autoclose multi-character pairs
5985 cx.set_state(
5986 &"
5987 ˇ
5988 ˇ
5989 "
5990 .unindent(),
5991 );
5992 cx.update_editor(|editor, window, cx| {
5993 editor.handle_input("/", window, cx);
5994 editor.handle_input("*", window, cx);
5995 });
5996 cx.assert_editor_state(
5997 &"
5998 /*ˇ */
5999 /*ˇ */
6000 "
6001 .unindent(),
6002 );
6003
6004 // one cursor autocloses a multi-character pair, one cursor
6005 // does not autoclose.
6006 cx.set_state(
6007 &"
6008 /ˇ
6009 ˇ
6010 "
6011 .unindent(),
6012 );
6013 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6014 cx.assert_editor_state(
6015 &"
6016 /*ˇ */
6017 *ˇ
6018 "
6019 .unindent(),
6020 );
6021
6022 // Don't autoclose if the next character isn't whitespace and isn't
6023 // listed in the language's "autoclose_before" section.
6024 cx.set_state("ˇa b");
6025 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6026 cx.assert_editor_state("{ˇa b");
6027
6028 // Don't autoclose if `close` is false for the bracket pair
6029 cx.set_state("ˇ");
6030 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6031 cx.assert_editor_state("[ˇ");
6032
6033 // Surround with brackets if text is selected
6034 cx.set_state("«aˇ» b");
6035 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6036 cx.assert_editor_state("{«aˇ»} b");
6037
6038 // Autclose pair where the start and end characters are the same
6039 cx.set_state("aˇ");
6040 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6041 cx.assert_editor_state("a\"ˇ\"");
6042 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6043 cx.assert_editor_state("a\"\"ˇ");
6044
6045 // Don't autoclose pair if autoclose is disabled
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 and auto_surround is enabled, even if autoclose is disabled
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
6056#[gpui::test]
6057async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut gpui::TestAppContext) {
6058 init_test(cx, |settings| {
6059 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6060 });
6061
6062 let mut cx = EditorTestContext::new(cx).await;
6063
6064 let language = Arc::new(Language::new(
6065 LanguageConfig {
6066 brackets: BracketPairConfig {
6067 pairs: vec![
6068 BracketPair {
6069 start: "{".to_string(),
6070 end: "}".to_string(),
6071 close: true,
6072 surround: true,
6073 newline: true,
6074 },
6075 BracketPair {
6076 start: "(".to_string(),
6077 end: ")".to_string(),
6078 close: true,
6079 surround: true,
6080 newline: true,
6081 },
6082 BracketPair {
6083 start: "[".to_string(),
6084 end: "]".to_string(),
6085 close: false,
6086 surround: false,
6087 newline: true,
6088 },
6089 ],
6090 ..Default::default()
6091 },
6092 autoclose_before: "})]".to_string(),
6093 ..Default::default()
6094 },
6095 Some(tree_sitter_rust::LANGUAGE.into()),
6096 ));
6097
6098 cx.language_registry().add(language.clone());
6099 cx.update_buffer(|buffer, cx| {
6100 buffer.set_language(Some(language), cx);
6101 });
6102
6103 cx.set_state(
6104 &"
6105 ˇ
6106 ˇ
6107 ˇ
6108 "
6109 .unindent(),
6110 );
6111
6112 // ensure only matching closing brackets are skipped over
6113 cx.update_editor(|editor, window, cx| {
6114 editor.handle_input("}", window, cx);
6115 editor.move_left(&MoveLeft, window, cx);
6116 editor.handle_input(")", window, cx);
6117 editor.move_left(&MoveLeft, window, cx);
6118 });
6119 cx.assert_editor_state(
6120 &"
6121 ˇ)}
6122 ˇ)}
6123 ˇ)}
6124 "
6125 .unindent(),
6126 );
6127
6128 // skip-over closing brackets at multiple cursors
6129 cx.update_editor(|editor, window, cx| {
6130 editor.handle_input(")", window, cx);
6131 editor.handle_input("}", window, cx);
6132 });
6133 cx.assert_editor_state(
6134 &"
6135 )}ˇ
6136 )}ˇ
6137 )}ˇ
6138 "
6139 .unindent(),
6140 );
6141
6142 // ignore non-close brackets
6143 cx.update_editor(|editor, window, cx| {
6144 editor.handle_input("]", window, cx);
6145 editor.move_left(&MoveLeft, window, cx);
6146 editor.handle_input("]", window, cx);
6147 });
6148 cx.assert_editor_state(
6149 &"
6150 )}]ˇ]
6151 )}]ˇ]
6152 )}]ˇ]
6153 "
6154 .unindent(),
6155 );
6156}
6157
6158#[gpui::test]
6159async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
6160 init_test(cx, |_| {});
6161
6162 let mut cx = EditorTestContext::new(cx).await;
6163
6164 let html_language = Arc::new(
6165 Language::new(
6166 LanguageConfig {
6167 name: "HTML".into(),
6168 brackets: BracketPairConfig {
6169 pairs: vec![
6170 BracketPair {
6171 start: "<".into(),
6172 end: ">".into(),
6173 close: true,
6174 ..Default::default()
6175 },
6176 BracketPair {
6177 start: "{".into(),
6178 end: "}".into(),
6179 close: true,
6180 ..Default::default()
6181 },
6182 BracketPair {
6183 start: "(".into(),
6184 end: ")".into(),
6185 close: true,
6186 ..Default::default()
6187 },
6188 ],
6189 ..Default::default()
6190 },
6191 autoclose_before: "})]>".into(),
6192 ..Default::default()
6193 },
6194 Some(tree_sitter_html::language()),
6195 )
6196 .with_injection_query(
6197 r#"
6198 (script_element
6199 (raw_text) @injection.content
6200 (#set! injection.language "javascript"))
6201 "#,
6202 )
6203 .unwrap(),
6204 );
6205
6206 let javascript_language = Arc::new(Language::new(
6207 LanguageConfig {
6208 name: "JavaScript".into(),
6209 brackets: BracketPairConfig {
6210 pairs: vec![
6211 BracketPair {
6212 start: "/*".into(),
6213 end: " */".into(),
6214 close: true,
6215 ..Default::default()
6216 },
6217 BracketPair {
6218 start: "{".into(),
6219 end: "}".into(),
6220 close: true,
6221 ..Default::default()
6222 },
6223 BracketPair {
6224 start: "(".into(),
6225 end: ")".into(),
6226 close: true,
6227 ..Default::default()
6228 },
6229 ],
6230 ..Default::default()
6231 },
6232 autoclose_before: "})]>".into(),
6233 ..Default::default()
6234 },
6235 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6236 ));
6237
6238 cx.language_registry().add(html_language.clone());
6239 cx.language_registry().add(javascript_language.clone());
6240
6241 cx.update_buffer(|buffer, cx| {
6242 buffer.set_language(Some(html_language), cx);
6243 });
6244
6245 cx.set_state(
6246 &r#"
6247 <body>ˇ
6248 <script>
6249 var x = 1;ˇ
6250 </script>
6251 </body>ˇ
6252 "#
6253 .unindent(),
6254 );
6255
6256 // Precondition: different languages are active at different locations.
6257 cx.update_editor(|editor, window, cx| {
6258 let snapshot = editor.snapshot(window, cx);
6259 let cursors = editor.selections.ranges::<usize>(cx);
6260 let languages = cursors
6261 .iter()
6262 .map(|c| snapshot.language_at(c.start).unwrap().name())
6263 .collect::<Vec<_>>();
6264 assert_eq!(
6265 languages,
6266 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6267 );
6268 });
6269
6270 // Angle brackets autoclose in HTML, but not JavaScript.
6271 cx.update_editor(|editor, window, cx| {
6272 editor.handle_input("<", window, cx);
6273 editor.handle_input("a", window, cx);
6274 });
6275 cx.assert_editor_state(
6276 &r#"
6277 <body><aˇ>
6278 <script>
6279 var x = 1;<aˇ
6280 </script>
6281 </body><aˇ>
6282 "#
6283 .unindent(),
6284 );
6285
6286 // Curly braces and parens autoclose in both HTML and JavaScript.
6287 cx.update_editor(|editor, window, cx| {
6288 editor.handle_input(" b=", window, cx);
6289 editor.handle_input("{", window, cx);
6290 editor.handle_input("c", window, cx);
6291 editor.handle_input("(", window, cx);
6292 });
6293 cx.assert_editor_state(
6294 &r#"
6295 <body><a b={c(ˇ)}>
6296 <script>
6297 var x = 1;<a b={c(ˇ)}
6298 </script>
6299 </body><a b={c(ˇ)}>
6300 "#
6301 .unindent(),
6302 );
6303
6304 // Brackets that were already autoclosed are skipped.
6305 cx.update_editor(|editor, window, cx| {
6306 editor.handle_input(")", window, cx);
6307 editor.handle_input("d", window, cx);
6308 editor.handle_input("}", window, cx);
6309 });
6310 cx.assert_editor_state(
6311 &r#"
6312 <body><a b={c()d}ˇ>
6313 <script>
6314 var x = 1;<a b={c()d}ˇ
6315 </script>
6316 </body><a b={c()d}ˇ>
6317 "#
6318 .unindent(),
6319 );
6320 cx.update_editor(|editor, window, cx| {
6321 editor.handle_input(">", window, cx);
6322 });
6323 cx.assert_editor_state(
6324 &r#"
6325 <body><a b={c()d}>ˇ
6326 <script>
6327 var x = 1;<a b={c()d}>ˇ
6328 </script>
6329 </body><a b={c()d}>ˇ
6330 "#
6331 .unindent(),
6332 );
6333
6334 // Reset
6335 cx.set_state(
6336 &r#"
6337 <body>ˇ
6338 <script>
6339 var x = 1;ˇ
6340 </script>
6341 </body>ˇ
6342 "#
6343 .unindent(),
6344 );
6345
6346 cx.update_editor(|editor, window, cx| {
6347 editor.handle_input("<", window, cx);
6348 });
6349 cx.assert_editor_state(
6350 &r#"
6351 <body><ˇ>
6352 <script>
6353 var x = 1;<ˇ
6354 </script>
6355 </body><ˇ>
6356 "#
6357 .unindent(),
6358 );
6359
6360 // When backspacing, the closing angle brackets are removed.
6361 cx.update_editor(|editor, window, cx| {
6362 editor.backspace(&Backspace, window, cx);
6363 });
6364 cx.assert_editor_state(
6365 &r#"
6366 <body>ˇ
6367 <script>
6368 var x = 1;ˇ
6369 </script>
6370 </body>ˇ
6371 "#
6372 .unindent(),
6373 );
6374
6375 // Block comments autoclose in JavaScript, but not HTML.
6376 cx.update_editor(|editor, window, cx| {
6377 editor.handle_input("/", window, cx);
6378 editor.handle_input("*", window, cx);
6379 });
6380 cx.assert_editor_state(
6381 &r#"
6382 <body>/*ˇ
6383 <script>
6384 var x = 1;/*ˇ */
6385 </script>
6386 </body>/*ˇ
6387 "#
6388 .unindent(),
6389 );
6390}
6391
6392#[gpui::test]
6393async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
6394 init_test(cx, |_| {});
6395
6396 let mut cx = EditorTestContext::new(cx).await;
6397
6398 let rust_language = Arc::new(
6399 Language::new(
6400 LanguageConfig {
6401 name: "Rust".into(),
6402 brackets: serde_json::from_value(json!([
6403 { "start": "{", "end": "}", "close": true, "newline": true },
6404 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
6405 ]))
6406 .unwrap(),
6407 autoclose_before: "})]>".into(),
6408 ..Default::default()
6409 },
6410 Some(tree_sitter_rust::LANGUAGE.into()),
6411 )
6412 .with_override_query("(string_literal) @string")
6413 .unwrap(),
6414 );
6415
6416 cx.language_registry().add(rust_language.clone());
6417 cx.update_buffer(|buffer, cx| {
6418 buffer.set_language(Some(rust_language), cx);
6419 });
6420
6421 cx.set_state(
6422 &r#"
6423 let x = ˇ
6424 "#
6425 .unindent(),
6426 );
6427
6428 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
6429 cx.update_editor(|editor, window, cx| {
6430 editor.handle_input("\"", window, cx);
6431 });
6432 cx.assert_editor_state(
6433 &r#"
6434 let x = "ˇ"
6435 "#
6436 .unindent(),
6437 );
6438
6439 // Inserting another quotation mark. The cursor moves across the existing
6440 // automatically-inserted quotation mark.
6441 cx.update_editor(|editor, window, cx| {
6442 editor.handle_input("\"", window, cx);
6443 });
6444 cx.assert_editor_state(
6445 &r#"
6446 let x = ""ˇ
6447 "#
6448 .unindent(),
6449 );
6450
6451 // Reset
6452 cx.set_state(
6453 &r#"
6454 let x = ˇ
6455 "#
6456 .unindent(),
6457 );
6458
6459 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
6460 cx.update_editor(|editor, window, cx| {
6461 editor.handle_input("\"", window, cx);
6462 editor.handle_input(" ", window, cx);
6463 editor.move_left(&Default::default(), window, cx);
6464 editor.handle_input("\\", window, cx);
6465 editor.handle_input("\"", window, cx);
6466 });
6467 cx.assert_editor_state(
6468 &r#"
6469 let x = "\"ˇ "
6470 "#
6471 .unindent(),
6472 );
6473
6474 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
6475 // mark. Nothing is inserted.
6476 cx.update_editor(|editor, window, cx| {
6477 editor.move_right(&Default::default(), window, cx);
6478 editor.handle_input("\"", window, cx);
6479 });
6480 cx.assert_editor_state(
6481 &r#"
6482 let x = "\" "ˇ
6483 "#
6484 .unindent(),
6485 );
6486}
6487
6488#[gpui::test]
6489async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
6490 init_test(cx, |_| {});
6491
6492 let language = Arc::new(Language::new(
6493 LanguageConfig {
6494 brackets: BracketPairConfig {
6495 pairs: vec![
6496 BracketPair {
6497 start: "{".to_string(),
6498 end: "}".to_string(),
6499 close: true,
6500 surround: true,
6501 newline: true,
6502 },
6503 BracketPair {
6504 start: "/* ".to_string(),
6505 end: "*/".to_string(),
6506 close: true,
6507 surround: true,
6508 ..Default::default()
6509 },
6510 ],
6511 ..Default::default()
6512 },
6513 ..Default::default()
6514 },
6515 Some(tree_sitter_rust::LANGUAGE.into()),
6516 ));
6517
6518 let text = r#"
6519 a
6520 b
6521 c
6522 "#
6523 .unindent();
6524
6525 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6526 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6527 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6528 editor
6529 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6530 .await;
6531
6532 editor.update_in(cx, |editor, window, cx| {
6533 editor.change_selections(None, window, cx, |s| {
6534 s.select_display_ranges([
6535 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6536 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6537 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
6538 ])
6539 });
6540
6541 editor.handle_input("{", window, cx);
6542 editor.handle_input("{", window, cx);
6543 editor.handle_input("{", window, cx);
6544 assert_eq!(
6545 editor.text(cx),
6546 "
6547 {{{a}}}
6548 {{{b}}}
6549 {{{c}}}
6550 "
6551 .unindent()
6552 );
6553 assert_eq!(
6554 editor.selections.display_ranges(cx),
6555 [
6556 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
6557 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
6558 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
6559 ]
6560 );
6561
6562 editor.undo(&Undo, window, cx);
6563 editor.undo(&Undo, window, cx);
6564 editor.undo(&Undo, window, cx);
6565 assert_eq!(
6566 editor.text(cx),
6567 "
6568 a
6569 b
6570 c
6571 "
6572 .unindent()
6573 );
6574 assert_eq!(
6575 editor.selections.display_ranges(cx),
6576 [
6577 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6578 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6579 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
6580 ]
6581 );
6582
6583 // Ensure inserting the first character of a multi-byte bracket pair
6584 // doesn't surround the selections with the bracket.
6585 editor.handle_input("/", window, cx);
6586 assert_eq!(
6587 editor.text(cx),
6588 "
6589 /
6590 /
6591 /
6592 "
6593 .unindent()
6594 );
6595 assert_eq!(
6596 editor.selections.display_ranges(cx),
6597 [
6598 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6599 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6600 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
6601 ]
6602 );
6603
6604 editor.undo(&Undo, window, cx);
6605 assert_eq!(
6606 editor.text(cx),
6607 "
6608 a
6609 b
6610 c
6611 "
6612 .unindent()
6613 );
6614 assert_eq!(
6615 editor.selections.display_ranges(cx),
6616 [
6617 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6618 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6619 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
6620 ]
6621 );
6622
6623 // Ensure inserting the last character of a multi-byte bracket pair
6624 // doesn't surround the selections with the bracket.
6625 editor.handle_input("*", window, cx);
6626 assert_eq!(
6627 editor.text(cx),
6628 "
6629 *
6630 *
6631 *
6632 "
6633 .unindent()
6634 );
6635 assert_eq!(
6636 editor.selections.display_ranges(cx),
6637 [
6638 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6639 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6640 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
6641 ]
6642 );
6643 });
6644}
6645
6646#[gpui::test]
6647async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
6648 init_test(cx, |_| {});
6649
6650 let language = Arc::new(Language::new(
6651 LanguageConfig {
6652 brackets: BracketPairConfig {
6653 pairs: vec![BracketPair {
6654 start: "{".to_string(),
6655 end: "}".to_string(),
6656 close: true,
6657 surround: true,
6658 newline: true,
6659 }],
6660 ..Default::default()
6661 },
6662 autoclose_before: "}".to_string(),
6663 ..Default::default()
6664 },
6665 Some(tree_sitter_rust::LANGUAGE.into()),
6666 ));
6667
6668 let text = r#"
6669 a
6670 b
6671 c
6672 "#
6673 .unindent();
6674
6675 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6676 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6677 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6678 editor
6679 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6680 .await;
6681
6682 editor.update_in(cx, |editor, window, cx| {
6683 editor.change_selections(None, window, cx, |s| {
6684 s.select_ranges([
6685 Point::new(0, 1)..Point::new(0, 1),
6686 Point::new(1, 1)..Point::new(1, 1),
6687 Point::new(2, 1)..Point::new(2, 1),
6688 ])
6689 });
6690
6691 editor.handle_input("{", window, cx);
6692 editor.handle_input("{", window, cx);
6693 editor.handle_input("_", window, cx);
6694 assert_eq!(
6695 editor.text(cx),
6696 "
6697 a{{_}}
6698 b{{_}}
6699 c{{_}}
6700 "
6701 .unindent()
6702 );
6703 assert_eq!(
6704 editor.selections.ranges::<Point>(cx),
6705 [
6706 Point::new(0, 4)..Point::new(0, 4),
6707 Point::new(1, 4)..Point::new(1, 4),
6708 Point::new(2, 4)..Point::new(2, 4)
6709 ]
6710 );
6711
6712 editor.backspace(&Default::default(), window, cx);
6713 editor.backspace(&Default::default(), window, cx);
6714 assert_eq!(
6715 editor.text(cx),
6716 "
6717 a{}
6718 b{}
6719 c{}
6720 "
6721 .unindent()
6722 );
6723 assert_eq!(
6724 editor.selections.ranges::<Point>(cx),
6725 [
6726 Point::new(0, 2)..Point::new(0, 2),
6727 Point::new(1, 2)..Point::new(1, 2),
6728 Point::new(2, 2)..Point::new(2, 2)
6729 ]
6730 );
6731
6732 editor.delete_to_previous_word_start(&Default::default(), window, cx);
6733 assert_eq!(
6734 editor.text(cx),
6735 "
6736 a
6737 b
6738 c
6739 "
6740 .unindent()
6741 );
6742 assert_eq!(
6743 editor.selections.ranges::<Point>(cx),
6744 [
6745 Point::new(0, 1)..Point::new(0, 1),
6746 Point::new(1, 1)..Point::new(1, 1),
6747 Point::new(2, 1)..Point::new(2, 1)
6748 ]
6749 );
6750 });
6751}
6752
6753#[gpui::test]
6754async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut gpui::TestAppContext) {
6755 init_test(cx, |settings| {
6756 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6757 });
6758
6759 let mut cx = EditorTestContext::new(cx).await;
6760
6761 let language = Arc::new(Language::new(
6762 LanguageConfig {
6763 brackets: BracketPairConfig {
6764 pairs: vec![
6765 BracketPair {
6766 start: "{".to_string(),
6767 end: "}".to_string(),
6768 close: true,
6769 surround: true,
6770 newline: true,
6771 },
6772 BracketPair {
6773 start: "(".to_string(),
6774 end: ")".to_string(),
6775 close: true,
6776 surround: true,
6777 newline: true,
6778 },
6779 BracketPair {
6780 start: "[".to_string(),
6781 end: "]".to_string(),
6782 close: false,
6783 surround: true,
6784 newline: true,
6785 },
6786 ],
6787 ..Default::default()
6788 },
6789 autoclose_before: "})]".to_string(),
6790 ..Default::default()
6791 },
6792 Some(tree_sitter_rust::LANGUAGE.into()),
6793 ));
6794
6795 cx.language_registry().add(language.clone());
6796 cx.update_buffer(|buffer, cx| {
6797 buffer.set_language(Some(language), cx);
6798 });
6799
6800 cx.set_state(
6801 &"
6802 {(ˇ)}
6803 [[ˇ]]
6804 {(ˇ)}
6805 "
6806 .unindent(),
6807 );
6808
6809 cx.update_editor(|editor, window, cx| {
6810 editor.backspace(&Default::default(), window, cx);
6811 editor.backspace(&Default::default(), window, cx);
6812 });
6813
6814 cx.assert_editor_state(
6815 &"
6816 ˇ
6817 ˇ]]
6818 ˇ
6819 "
6820 .unindent(),
6821 );
6822
6823 cx.update_editor(|editor, window, cx| {
6824 editor.handle_input("{", window, cx);
6825 editor.handle_input("{", window, cx);
6826 editor.move_right(&MoveRight, window, cx);
6827 editor.move_right(&MoveRight, window, cx);
6828 editor.move_left(&MoveLeft, window, cx);
6829 editor.move_left(&MoveLeft, window, cx);
6830 editor.backspace(&Default::default(), window, cx);
6831 });
6832
6833 cx.assert_editor_state(
6834 &"
6835 {ˇ}
6836 {ˇ}]]
6837 {ˇ}
6838 "
6839 .unindent(),
6840 );
6841
6842 cx.update_editor(|editor, window, cx| {
6843 editor.backspace(&Default::default(), window, cx);
6844 });
6845
6846 cx.assert_editor_state(
6847 &"
6848 ˇ
6849 ˇ]]
6850 ˇ
6851 "
6852 .unindent(),
6853 );
6854}
6855
6856#[gpui::test]
6857async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) {
6858 init_test(cx, |_| {});
6859
6860 let language = Arc::new(Language::new(
6861 LanguageConfig::default(),
6862 Some(tree_sitter_rust::LANGUAGE.into()),
6863 ));
6864
6865 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
6866 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6867 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6868 editor
6869 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6870 .await;
6871
6872 editor.update_in(cx, |editor, window, cx| {
6873 editor.set_auto_replace_emoji_shortcode(true);
6874
6875 editor.handle_input("Hello ", window, cx);
6876 editor.handle_input(":wave", window, cx);
6877 assert_eq!(editor.text(cx), "Hello :wave".unindent());
6878
6879 editor.handle_input(":", window, cx);
6880 assert_eq!(editor.text(cx), "Hello 👋".unindent());
6881
6882 editor.handle_input(" :smile", window, cx);
6883 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
6884
6885 editor.handle_input(":", window, cx);
6886 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
6887
6888 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
6889 editor.handle_input(":wave", window, cx);
6890 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
6891
6892 editor.handle_input(":", window, cx);
6893 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
6894
6895 editor.handle_input(":1", window, cx);
6896 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
6897
6898 editor.handle_input(":", window, cx);
6899 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
6900
6901 // Ensure shortcode does not get replaced when it is part of a word
6902 editor.handle_input(" Test:wave", window, cx);
6903 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
6904
6905 editor.handle_input(":", window, cx);
6906 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
6907
6908 editor.set_auto_replace_emoji_shortcode(false);
6909
6910 // Ensure shortcode does not get replaced when auto replace is off
6911 editor.handle_input(" :wave", window, cx);
6912 assert_eq!(
6913 editor.text(cx),
6914 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
6915 );
6916
6917 editor.handle_input(":", window, cx);
6918 assert_eq!(
6919 editor.text(cx),
6920 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
6921 );
6922 });
6923}
6924
6925#[gpui::test]
6926async fn test_snippet_placeholder_choices(cx: &mut gpui::TestAppContext) {
6927 init_test(cx, |_| {});
6928
6929 let (text, insertion_ranges) = marked_text_ranges(
6930 indoc! {"
6931 ˇ
6932 "},
6933 false,
6934 );
6935
6936 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
6937 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6938
6939 _ = editor.update_in(cx, |editor, window, cx| {
6940 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
6941
6942 editor
6943 .insert_snippet(&insertion_ranges, snippet, window, cx)
6944 .unwrap();
6945
6946 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
6947 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
6948 assert_eq!(editor.text(cx), expected_text);
6949 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
6950 }
6951
6952 assert(
6953 editor,
6954 cx,
6955 indoc! {"
6956 type «» =•
6957 "},
6958 );
6959
6960 assert!(editor.context_menu_visible(), "There should be a matches");
6961 });
6962}
6963
6964#[gpui::test]
6965async fn test_snippets(cx: &mut gpui::TestAppContext) {
6966 init_test(cx, |_| {});
6967
6968 let (text, insertion_ranges) = marked_text_ranges(
6969 indoc! {"
6970 a.ˇ b
6971 a.ˇ b
6972 a.ˇ b
6973 "},
6974 false,
6975 );
6976
6977 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
6978 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6979
6980 editor.update_in(cx, |editor, window, cx| {
6981 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
6982
6983 editor
6984 .insert_snippet(&insertion_ranges, snippet, window, cx)
6985 .unwrap();
6986
6987 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
6988 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
6989 assert_eq!(editor.text(cx), expected_text);
6990 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
6991 }
6992
6993 assert(
6994 editor,
6995 cx,
6996 indoc! {"
6997 a.f(«one», two, «three») b
6998 a.f(«one», two, «three») b
6999 a.f(«one», two, «three») b
7000 "},
7001 );
7002
7003 // Can't move earlier than the first tab stop
7004 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7005 assert(
7006 editor,
7007 cx,
7008 indoc! {"
7009 a.f(«one», two, «three») b
7010 a.f(«one», two, «three») b
7011 a.f(«one», two, «three») b
7012 "},
7013 );
7014
7015 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7016 assert(
7017 editor,
7018 cx,
7019 indoc! {"
7020 a.f(one, «two», three) b
7021 a.f(one, «two», three) b
7022 a.f(one, «two», three) b
7023 "},
7024 );
7025
7026 editor.move_to_prev_snippet_tabstop(window, cx);
7027 assert(
7028 editor,
7029 cx,
7030 indoc! {"
7031 a.f(«one», two, «three») b
7032 a.f(«one», two, «three») b
7033 a.f(«one», two, «three») b
7034 "},
7035 );
7036
7037 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7038 assert(
7039 editor,
7040 cx,
7041 indoc! {"
7042 a.f(one, «two», three) b
7043 a.f(one, «two», three) b
7044 a.f(one, «two», three) b
7045 "},
7046 );
7047 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7048 assert(
7049 editor,
7050 cx,
7051 indoc! {"
7052 a.f(one, two, three)ˇ b
7053 a.f(one, two, three)ˇ b
7054 a.f(one, two, three)ˇ b
7055 "},
7056 );
7057
7058 // As soon as the last tab stop is reached, snippet state is gone
7059 editor.move_to_prev_snippet_tabstop(window, cx);
7060 assert(
7061 editor,
7062 cx,
7063 indoc! {"
7064 a.f(one, two, three)ˇ b
7065 a.f(one, two, three)ˇ b
7066 a.f(one, two, three)ˇ b
7067 "},
7068 );
7069 });
7070}
7071
7072#[gpui::test]
7073async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
7074 init_test(cx, |_| {});
7075
7076 let fs = FakeFs::new(cx.executor());
7077 fs.insert_file("/file.rs", Default::default()).await;
7078
7079 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
7080
7081 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7082 language_registry.add(rust_lang());
7083 let mut fake_servers = language_registry.register_fake_lsp(
7084 "Rust",
7085 FakeLspAdapter {
7086 capabilities: lsp::ServerCapabilities {
7087 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7088 ..Default::default()
7089 },
7090 ..Default::default()
7091 },
7092 );
7093
7094 let buffer = project
7095 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
7096 .await
7097 .unwrap();
7098
7099 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7100 let (editor, cx) = cx.add_window_view(|window, cx| {
7101 build_editor_with_project(project.clone(), buffer, window, cx)
7102 });
7103 editor.update_in(cx, |editor, window, cx| {
7104 editor.set_text("one\ntwo\nthree\n", window, cx)
7105 });
7106 assert!(cx.read(|cx| editor.is_dirty(cx)));
7107
7108 cx.executor().start_waiting();
7109 let fake_server = fake_servers.next().await.unwrap();
7110
7111 let save = editor
7112 .update_in(cx, |editor, window, cx| {
7113 editor.save(true, project.clone(), window, cx)
7114 })
7115 .unwrap();
7116 fake_server
7117 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7118 assert_eq!(
7119 params.text_document.uri,
7120 lsp::Url::from_file_path("/file.rs").unwrap()
7121 );
7122 assert_eq!(params.options.tab_size, 4);
7123 Ok(Some(vec![lsp::TextEdit::new(
7124 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7125 ", ".to_string(),
7126 )]))
7127 })
7128 .next()
7129 .await;
7130 cx.executor().start_waiting();
7131 save.await;
7132
7133 assert_eq!(
7134 editor.update(cx, |editor, cx| editor.text(cx)),
7135 "one, two\nthree\n"
7136 );
7137 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7138
7139 editor.update_in(cx, |editor, window, cx| {
7140 editor.set_text("one\ntwo\nthree\n", window, cx)
7141 });
7142 assert!(cx.read(|cx| editor.is_dirty(cx)));
7143
7144 // Ensure we can still save even if formatting hangs.
7145 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7146 assert_eq!(
7147 params.text_document.uri,
7148 lsp::Url::from_file_path("/file.rs").unwrap()
7149 );
7150 futures::future::pending::<()>().await;
7151 unreachable!()
7152 });
7153 let save = editor
7154 .update_in(cx, |editor, window, cx| {
7155 editor.save(true, project.clone(), window, cx)
7156 })
7157 .unwrap();
7158 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7159 cx.executor().start_waiting();
7160 save.await;
7161 assert_eq!(
7162 editor.update(cx, |editor, cx| editor.text(cx)),
7163 "one\ntwo\nthree\n"
7164 );
7165 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7166
7167 // For non-dirty buffer, no formatting request should be sent
7168 let save = editor
7169 .update_in(cx, |editor, window, cx| {
7170 editor.save(true, project.clone(), window, cx)
7171 })
7172 .unwrap();
7173 let _pending_format_request = fake_server
7174 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7175 panic!("Should not be invoked on non-dirty buffer");
7176 })
7177 .next();
7178 cx.executor().start_waiting();
7179 save.await;
7180
7181 // Set rust language override and assert overridden tabsize is sent to language server
7182 update_test_language_settings(cx, |settings| {
7183 settings.languages.insert(
7184 "Rust".into(),
7185 LanguageSettingsContent {
7186 tab_size: NonZeroU32::new(8),
7187 ..Default::default()
7188 },
7189 );
7190 });
7191
7192 editor.update_in(cx, |editor, window, cx| {
7193 editor.set_text("somehting_new\n", window, cx)
7194 });
7195 assert!(cx.read(|cx| editor.is_dirty(cx)));
7196 let save = editor
7197 .update_in(cx, |editor, window, cx| {
7198 editor.save(true, project.clone(), window, cx)
7199 })
7200 .unwrap();
7201 fake_server
7202 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7203 assert_eq!(
7204 params.text_document.uri,
7205 lsp::Url::from_file_path("/file.rs").unwrap()
7206 );
7207 assert_eq!(params.options.tab_size, 8);
7208 Ok(Some(vec![]))
7209 })
7210 .next()
7211 .await;
7212 cx.executor().start_waiting();
7213 save.await;
7214}
7215
7216#[gpui::test]
7217async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) {
7218 init_test(cx, |_| {});
7219
7220 let cols = 4;
7221 let rows = 10;
7222 let sample_text_1 = sample_text(rows, cols, 'a');
7223 assert_eq!(
7224 sample_text_1,
7225 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7226 );
7227 let sample_text_2 = sample_text(rows, cols, 'l');
7228 assert_eq!(
7229 sample_text_2,
7230 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7231 );
7232 let sample_text_3 = sample_text(rows, cols, 'v');
7233 assert_eq!(
7234 sample_text_3,
7235 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7236 );
7237
7238 let fs = FakeFs::new(cx.executor());
7239 fs.insert_tree(
7240 "/a",
7241 json!({
7242 "main.rs": sample_text_1,
7243 "other.rs": sample_text_2,
7244 "lib.rs": sample_text_3,
7245 }),
7246 )
7247 .await;
7248
7249 let project = Project::test(fs, ["/a".as_ref()], cx).await;
7250 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7251 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7252
7253 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7254 language_registry.add(rust_lang());
7255 let mut fake_servers = language_registry.register_fake_lsp(
7256 "Rust",
7257 FakeLspAdapter {
7258 capabilities: lsp::ServerCapabilities {
7259 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7260 ..Default::default()
7261 },
7262 ..Default::default()
7263 },
7264 );
7265
7266 let worktree = project.update(cx, |project, cx| {
7267 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7268 assert_eq!(worktrees.len(), 1);
7269 worktrees.pop().unwrap()
7270 });
7271 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
7272
7273 let buffer_1 = project
7274 .update(cx, |project, cx| {
7275 project.open_buffer((worktree_id, "main.rs"), cx)
7276 })
7277 .await
7278 .unwrap();
7279 let buffer_2 = project
7280 .update(cx, |project, cx| {
7281 project.open_buffer((worktree_id, "other.rs"), cx)
7282 })
7283 .await
7284 .unwrap();
7285 let buffer_3 = project
7286 .update(cx, |project, cx| {
7287 project.open_buffer((worktree_id, "lib.rs"), cx)
7288 })
7289 .await
7290 .unwrap();
7291
7292 let multi_buffer = cx.new(|cx| {
7293 let mut multi_buffer = MultiBuffer::new(ReadWrite);
7294 multi_buffer.push_excerpts(
7295 buffer_1.clone(),
7296 [
7297 ExcerptRange {
7298 context: Point::new(0, 0)..Point::new(3, 0),
7299 primary: None,
7300 },
7301 ExcerptRange {
7302 context: Point::new(5, 0)..Point::new(7, 0),
7303 primary: None,
7304 },
7305 ExcerptRange {
7306 context: Point::new(9, 0)..Point::new(10, 4),
7307 primary: None,
7308 },
7309 ],
7310 cx,
7311 );
7312 multi_buffer.push_excerpts(
7313 buffer_2.clone(),
7314 [
7315 ExcerptRange {
7316 context: Point::new(0, 0)..Point::new(3, 0),
7317 primary: None,
7318 },
7319 ExcerptRange {
7320 context: Point::new(5, 0)..Point::new(7, 0),
7321 primary: None,
7322 },
7323 ExcerptRange {
7324 context: Point::new(9, 0)..Point::new(10, 4),
7325 primary: None,
7326 },
7327 ],
7328 cx,
7329 );
7330 multi_buffer.push_excerpts(
7331 buffer_3.clone(),
7332 [
7333 ExcerptRange {
7334 context: Point::new(0, 0)..Point::new(3, 0),
7335 primary: None,
7336 },
7337 ExcerptRange {
7338 context: Point::new(5, 0)..Point::new(7, 0),
7339 primary: None,
7340 },
7341 ExcerptRange {
7342 context: Point::new(9, 0)..Point::new(10, 4),
7343 primary: None,
7344 },
7345 ],
7346 cx,
7347 );
7348 multi_buffer
7349 });
7350 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
7351 Editor::new(
7352 EditorMode::Full,
7353 multi_buffer,
7354 Some(project.clone()),
7355 true,
7356 window,
7357 cx,
7358 )
7359 });
7360
7361 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7362 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7363 s.select_ranges(Some(1..2))
7364 });
7365 editor.insert("|one|two|three|", window, cx);
7366 });
7367 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7368 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7369 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7370 s.select_ranges(Some(60..70))
7371 });
7372 editor.insert("|four|five|six|", window, cx);
7373 });
7374 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7375
7376 // First two buffers should be edited, but not the third one.
7377 assert_eq!(
7378 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7379 "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}",
7380 );
7381 buffer_1.update(cx, |buffer, _| {
7382 assert!(buffer.is_dirty());
7383 assert_eq!(
7384 buffer.text(),
7385 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
7386 )
7387 });
7388 buffer_2.update(cx, |buffer, _| {
7389 assert!(buffer.is_dirty());
7390 assert_eq!(
7391 buffer.text(),
7392 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
7393 )
7394 });
7395 buffer_3.update(cx, |buffer, _| {
7396 assert!(!buffer.is_dirty());
7397 assert_eq!(buffer.text(), sample_text_3,)
7398 });
7399 cx.executor().run_until_parked();
7400
7401 cx.executor().start_waiting();
7402 let save = multi_buffer_editor
7403 .update_in(cx, |editor, window, cx| {
7404 editor.save(true, project.clone(), window, cx)
7405 })
7406 .unwrap();
7407
7408 let fake_server = fake_servers.next().await.unwrap();
7409 fake_server
7410 .server
7411 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7412 Ok(Some(vec![lsp::TextEdit::new(
7413 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7414 format!("[{} formatted]", params.text_document.uri),
7415 )]))
7416 })
7417 .detach();
7418 save.await;
7419
7420 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
7421 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
7422 assert_eq!(
7423 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7424 "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}",
7425 );
7426 buffer_1.update(cx, |buffer, _| {
7427 assert!(!buffer.is_dirty());
7428 assert_eq!(
7429 buffer.text(),
7430 "a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n",
7431 )
7432 });
7433 buffer_2.update(cx, |buffer, _| {
7434 assert!(!buffer.is_dirty());
7435 assert_eq!(
7436 buffer.text(),
7437 "lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n",
7438 )
7439 });
7440 buffer_3.update(cx, |buffer, _| {
7441 assert!(!buffer.is_dirty());
7442 assert_eq!(buffer.text(), sample_text_3,)
7443 });
7444}
7445
7446#[gpui::test]
7447async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
7448 init_test(cx, |_| {});
7449
7450 let fs = FakeFs::new(cx.executor());
7451 fs.insert_file("/file.rs", Default::default()).await;
7452
7453 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
7454
7455 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7456 language_registry.add(rust_lang());
7457 let mut fake_servers = language_registry.register_fake_lsp(
7458 "Rust",
7459 FakeLspAdapter {
7460 capabilities: lsp::ServerCapabilities {
7461 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
7462 ..Default::default()
7463 },
7464 ..Default::default()
7465 },
7466 );
7467
7468 let buffer = project
7469 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
7470 .await
7471 .unwrap();
7472
7473 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7474 let (editor, cx) = cx.add_window_view(|window, cx| {
7475 build_editor_with_project(project.clone(), buffer, window, cx)
7476 });
7477 editor.update_in(cx, |editor, window, cx| {
7478 editor.set_text("one\ntwo\nthree\n", window, cx)
7479 });
7480 assert!(cx.read(|cx| editor.is_dirty(cx)));
7481
7482 cx.executor().start_waiting();
7483 let fake_server = fake_servers.next().await.unwrap();
7484
7485 let save = editor
7486 .update_in(cx, |editor, window, cx| {
7487 editor.save(true, project.clone(), window, cx)
7488 })
7489 .unwrap();
7490 fake_server
7491 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
7492 assert_eq!(
7493 params.text_document.uri,
7494 lsp::Url::from_file_path("/file.rs").unwrap()
7495 );
7496 assert_eq!(params.options.tab_size, 4);
7497 Ok(Some(vec![lsp::TextEdit::new(
7498 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7499 ", ".to_string(),
7500 )]))
7501 })
7502 .next()
7503 .await;
7504 cx.executor().start_waiting();
7505 save.await;
7506 assert_eq!(
7507 editor.update(cx, |editor, cx| editor.text(cx)),
7508 "one, two\nthree\n"
7509 );
7510 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7511
7512 editor.update_in(cx, |editor, window, cx| {
7513 editor.set_text("one\ntwo\nthree\n", window, cx)
7514 });
7515 assert!(cx.read(|cx| editor.is_dirty(cx)));
7516
7517 // Ensure we can still save even if formatting hangs.
7518 fake_server.handle_request::<lsp::request::RangeFormatting, _, _>(
7519 move |params, _| async move {
7520 assert_eq!(
7521 params.text_document.uri,
7522 lsp::Url::from_file_path("/file.rs").unwrap()
7523 );
7524 futures::future::pending::<()>().await;
7525 unreachable!()
7526 },
7527 );
7528 let save = editor
7529 .update_in(cx, |editor, window, cx| {
7530 editor.save(true, project.clone(), window, cx)
7531 })
7532 .unwrap();
7533 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7534 cx.executor().start_waiting();
7535 save.await;
7536 assert_eq!(
7537 editor.update(cx, |editor, cx| editor.text(cx)),
7538 "one\ntwo\nthree\n"
7539 );
7540 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7541
7542 // For non-dirty buffer, no formatting request should be sent
7543 let save = editor
7544 .update_in(cx, |editor, window, cx| {
7545 editor.save(true, project.clone(), window, cx)
7546 })
7547 .unwrap();
7548 let _pending_format_request = fake_server
7549 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7550 panic!("Should not be invoked on non-dirty buffer");
7551 })
7552 .next();
7553 cx.executor().start_waiting();
7554 save.await;
7555
7556 // Set Rust language override and assert overridden tabsize is sent to language server
7557 update_test_language_settings(cx, |settings| {
7558 settings.languages.insert(
7559 "Rust".into(),
7560 LanguageSettingsContent {
7561 tab_size: NonZeroU32::new(8),
7562 ..Default::default()
7563 },
7564 );
7565 });
7566
7567 editor.update_in(cx, |editor, window, cx| {
7568 editor.set_text("somehting_new\n", window, cx)
7569 });
7570 assert!(cx.read(|cx| editor.is_dirty(cx)));
7571 let save = editor
7572 .update_in(cx, |editor, window, cx| {
7573 editor.save(true, project.clone(), window, cx)
7574 })
7575 .unwrap();
7576 fake_server
7577 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
7578 assert_eq!(
7579 params.text_document.uri,
7580 lsp::Url::from_file_path("/file.rs").unwrap()
7581 );
7582 assert_eq!(params.options.tab_size, 8);
7583 Ok(Some(vec![]))
7584 })
7585 .next()
7586 .await;
7587 cx.executor().start_waiting();
7588 save.await;
7589}
7590
7591#[gpui::test]
7592async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
7593 init_test(cx, |settings| {
7594 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
7595 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
7596 ))
7597 });
7598
7599 let fs = FakeFs::new(cx.executor());
7600 fs.insert_file("/file.rs", Default::default()).await;
7601
7602 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
7603
7604 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7605 language_registry.add(Arc::new(Language::new(
7606 LanguageConfig {
7607 name: "Rust".into(),
7608 matcher: LanguageMatcher {
7609 path_suffixes: vec!["rs".to_string()],
7610 ..Default::default()
7611 },
7612 ..LanguageConfig::default()
7613 },
7614 Some(tree_sitter_rust::LANGUAGE.into()),
7615 )));
7616 update_test_language_settings(cx, |settings| {
7617 // Enable Prettier formatting for the same buffer, and ensure
7618 // LSP is called instead of Prettier.
7619 settings.defaults.prettier = Some(PrettierSettings {
7620 allowed: true,
7621 ..PrettierSettings::default()
7622 });
7623 });
7624 let mut fake_servers = language_registry.register_fake_lsp(
7625 "Rust",
7626 FakeLspAdapter {
7627 capabilities: lsp::ServerCapabilities {
7628 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7629 ..Default::default()
7630 },
7631 ..Default::default()
7632 },
7633 );
7634
7635 let buffer = project
7636 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
7637 .await
7638 .unwrap();
7639
7640 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7641 let (editor, cx) = cx.add_window_view(|window, cx| {
7642 build_editor_with_project(project.clone(), buffer, window, cx)
7643 });
7644 editor.update_in(cx, |editor, window, cx| {
7645 editor.set_text("one\ntwo\nthree\n", window, cx)
7646 });
7647
7648 cx.executor().start_waiting();
7649 let fake_server = fake_servers.next().await.unwrap();
7650
7651 let format = editor
7652 .update_in(cx, |editor, window, cx| {
7653 editor.perform_format(
7654 project.clone(),
7655 FormatTrigger::Manual,
7656 FormatTarget::Buffers,
7657 window,
7658 cx,
7659 )
7660 })
7661 .unwrap();
7662 fake_server
7663 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7664 assert_eq!(
7665 params.text_document.uri,
7666 lsp::Url::from_file_path("/file.rs").unwrap()
7667 );
7668 assert_eq!(params.options.tab_size, 4);
7669 Ok(Some(vec![lsp::TextEdit::new(
7670 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7671 ", ".to_string(),
7672 )]))
7673 })
7674 .next()
7675 .await;
7676 cx.executor().start_waiting();
7677 format.await;
7678 assert_eq!(
7679 editor.update(cx, |editor, cx| editor.text(cx)),
7680 "one, two\nthree\n"
7681 );
7682
7683 editor.update_in(cx, |editor, window, cx| {
7684 editor.set_text("one\ntwo\nthree\n", window, cx)
7685 });
7686 // Ensure we don't lock if formatting hangs.
7687 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7688 assert_eq!(
7689 params.text_document.uri,
7690 lsp::Url::from_file_path("/file.rs").unwrap()
7691 );
7692 futures::future::pending::<()>().await;
7693 unreachable!()
7694 });
7695 let format = editor
7696 .update_in(cx, |editor, window, cx| {
7697 editor.perform_format(
7698 project,
7699 FormatTrigger::Manual,
7700 FormatTarget::Buffers,
7701 window,
7702 cx,
7703 )
7704 })
7705 .unwrap();
7706 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7707 cx.executor().start_waiting();
7708 format.await;
7709 assert_eq!(
7710 editor.update(cx, |editor, cx| editor.text(cx)),
7711 "one\ntwo\nthree\n"
7712 );
7713}
7714
7715#[gpui::test]
7716async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
7717 init_test(cx, |_| {});
7718
7719 let mut cx = EditorLspTestContext::new_rust(
7720 lsp::ServerCapabilities {
7721 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7722 ..Default::default()
7723 },
7724 cx,
7725 )
7726 .await;
7727
7728 cx.set_state(indoc! {"
7729 one.twoˇ
7730 "});
7731
7732 // The format request takes a long time. When it completes, it inserts
7733 // a newline and an indent before the `.`
7734 cx.lsp
7735 .handle_request::<lsp::request::Formatting, _, _>(move |_, cx| {
7736 let executor = cx.background_executor().clone();
7737 async move {
7738 executor.timer(Duration::from_millis(100)).await;
7739 Ok(Some(vec![lsp::TextEdit {
7740 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
7741 new_text: "\n ".into(),
7742 }]))
7743 }
7744 });
7745
7746 // Submit a format request.
7747 let format_1 = cx
7748 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7749 .unwrap();
7750 cx.executor().run_until_parked();
7751
7752 // Submit a second format request.
7753 let format_2 = cx
7754 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7755 .unwrap();
7756 cx.executor().run_until_parked();
7757
7758 // Wait for both format requests to complete
7759 cx.executor().advance_clock(Duration::from_millis(200));
7760 cx.executor().start_waiting();
7761 format_1.await.unwrap();
7762 cx.executor().start_waiting();
7763 format_2.await.unwrap();
7764
7765 // The formatting edits only happens once.
7766 cx.assert_editor_state(indoc! {"
7767 one
7768 .twoˇ
7769 "});
7770}
7771
7772#[gpui::test]
7773async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
7774 init_test(cx, |settings| {
7775 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
7776 });
7777
7778 let mut cx = EditorLspTestContext::new_rust(
7779 lsp::ServerCapabilities {
7780 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7781 ..Default::default()
7782 },
7783 cx,
7784 )
7785 .await;
7786
7787 // Set up a buffer white some trailing whitespace and no trailing newline.
7788 cx.set_state(
7789 &[
7790 "one ", //
7791 "twoˇ", //
7792 "three ", //
7793 "four", //
7794 ]
7795 .join("\n"),
7796 );
7797
7798 // Submit a format request.
7799 let format = cx
7800 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7801 .unwrap();
7802
7803 // Record which buffer changes have been sent to the language server
7804 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
7805 cx.lsp
7806 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
7807 let buffer_changes = buffer_changes.clone();
7808 move |params, _| {
7809 buffer_changes.lock().extend(
7810 params
7811 .content_changes
7812 .into_iter()
7813 .map(|e| (e.range.unwrap(), e.text)),
7814 );
7815 }
7816 });
7817
7818 // Handle formatting requests to the language server.
7819 cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
7820 let buffer_changes = buffer_changes.clone();
7821 move |_, _| {
7822 // When formatting is requested, trailing whitespace has already been stripped,
7823 // and the trailing newline has already been added.
7824 assert_eq!(
7825 &buffer_changes.lock()[1..],
7826 &[
7827 (
7828 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
7829 "".into()
7830 ),
7831 (
7832 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
7833 "".into()
7834 ),
7835 (
7836 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
7837 "\n".into()
7838 ),
7839 ]
7840 );
7841
7842 // Insert blank lines between each line of the buffer.
7843 async move {
7844 Ok(Some(vec![
7845 lsp::TextEdit {
7846 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
7847 new_text: "\n".into(),
7848 },
7849 lsp::TextEdit {
7850 range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
7851 new_text: "\n".into(),
7852 },
7853 ]))
7854 }
7855 }
7856 });
7857
7858 // After formatting the buffer, the trailing whitespace is stripped,
7859 // a newline is appended, and the edits provided by the language server
7860 // have been applied.
7861 format.await.unwrap();
7862 cx.assert_editor_state(
7863 &[
7864 "one", //
7865 "", //
7866 "twoˇ", //
7867 "", //
7868 "three", //
7869 "four", //
7870 "", //
7871 ]
7872 .join("\n"),
7873 );
7874
7875 // Undoing the formatting undoes the trailing whitespace removal, the
7876 // trailing newline, and the LSP edits.
7877 cx.update_buffer(|buffer, cx| buffer.undo(cx));
7878 cx.assert_editor_state(
7879 &[
7880 "one ", //
7881 "twoˇ", //
7882 "three ", //
7883 "four", //
7884 ]
7885 .join("\n"),
7886 );
7887}
7888
7889#[gpui::test]
7890async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
7891 cx: &mut gpui::TestAppContext,
7892) {
7893 init_test(cx, |_| {});
7894
7895 cx.update(|cx| {
7896 cx.update_global::<SettingsStore, _>(|settings, cx| {
7897 settings.update_user_settings::<EditorSettings>(cx, |settings| {
7898 settings.auto_signature_help = Some(true);
7899 });
7900 });
7901 });
7902
7903 let mut cx = EditorLspTestContext::new_rust(
7904 lsp::ServerCapabilities {
7905 signature_help_provider: Some(lsp::SignatureHelpOptions {
7906 ..Default::default()
7907 }),
7908 ..Default::default()
7909 },
7910 cx,
7911 )
7912 .await;
7913
7914 let language = Language::new(
7915 LanguageConfig {
7916 name: "Rust".into(),
7917 brackets: BracketPairConfig {
7918 pairs: vec![
7919 BracketPair {
7920 start: "{".to_string(),
7921 end: "}".to_string(),
7922 close: true,
7923 surround: true,
7924 newline: true,
7925 },
7926 BracketPair {
7927 start: "(".to_string(),
7928 end: ")".to_string(),
7929 close: true,
7930 surround: true,
7931 newline: true,
7932 },
7933 BracketPair {
7934 start: "/*".to_string(),
7935 end: " */".to_string(),
7936 close: true,
7937 surround: true,
7938 newline: true,
7939 },
7940 BracketPair {
7941 start: "[".to_string(),
7942 end: "]".to_string(),
7943 close: false,
7944 surround: false,
7945 newline: true,
7946 },
7947 BracketPair {
7948 start: "\"".to_string(),
7949 end: "\"".to_string(),
7950 close: true,
7951 surround: true,
7952 newline: false,
7953 },
7954 BracketPair {
7955 start: "<".to_string(),
7956 end: ">".to_string(),
7957 close: false,
7958 surround: true,
7959 newline: true,
7960 },
7961 ],
7962 ..Default::default()
7963 },
7964 autoclose_before: "})]".to_string(),
7965 ..Default::default()
7966 },
7967 Some(tree_sitter_rust::LANGUAGE.into()),
7968 );
7969 let language = Arc::new(language);
7970
7971 cx.language_registry().add(language.clone());
7972 cx.update_buffer(|buffer, cx| {
7973 buffer.set_language(Some(language), cx);
7974 });
7975
7976 cx.set_state(
7977 &r#"
7978 fn main() {
7979 sampleˇ
7980 }
7981 "#
7982 .unindent(),
7983 );
7984
7985 cx.update_editor(|editor, window, cx| {
7986 editor.handle_input("(", window, cx);
7987 });
7988 cx.assert_editor_state(
7989 &"
7990 fn main() {
7991 sample(ˇ)
7992 }
7993 "
7994 .unindent(),
7995 );
7996
7997 let mocked_response = lsp::SignatureHelp {
7998 signatures: vec![lsp::SignatureInformation {
7999 label: "fn sample(param1: u8, param2: u8)".to_string(),
8000 documentation: None,
8001 parameters: Some(vec![
8002 lsp::ParameterInformation {
8003 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8004 documentation: None,
8005 },
8006 lsp::ParameterInformation {
8007 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8008 documentation: None,
8009 },
8010 ]),
8011 active_parameter: None,
8012 }],
8013 active_signature: Some(0),
8014 active_parameter: Some(0),
8015 };
8016 handle_signature_help_request(&mut cx, mocked_response).await;
8017
8018 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8019 .await;
8020
8021 cx.editor(|editor, _, _| {
8022 let signature_help_state = editor.signature_help_state.popover().cloned();
8023 assert!(signature_help_state.is_some());
8024 let ParsedMarkdown {
8025 text, highlights, ..
8026 } = signature_help_state.unwrap().parsed_content;
8027 assert_eq!(text, "param1: u8, param2: u8");
8028 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8029 });
8030}
8031
8032#[gpui::test]
8033async fn test_handle_input_with_different_show_signature_settings(cx: &mut gpui::TestAppContext) {
8034 init_test(cx, |_| {});
8035
8036 cx.update(|cx| {
8037 cx.update_global::<SettingsStore, _>(|settings, cx| {
8038 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8039 settings.auto_signature_help = Some(false);
8040 settings.show_signature_help_after_edits = Some(false);
8041 });
8042 });
8043 });
8044
8045 let mut cx = EditorLspTestContext::new_rust(
8046 lsp::ServerCapabilities {
8047 signature_help_provider: Some(lsp::SignatureHelpOptions {
8048 ..Default::default()
8049 }),
8050 ..Default::default()
8051 },
8052 cx,
8053 )
8054 .await;
8055
8056 let language = Language::new(
8057 LanguageConfig {
8058 name: "Rust".into(),
8059 brackets: BracketPairConfig {
8060 pairs: vec![
8061 BracketPair {
8062 start: "{".to_string(),
8063 end: "}".to_string(),
8064 close: true,
8065 surround: true,
8066 newline: true,
8067 },
8068 BracketPair {
8069 start: "(".to_string(),
8070 end: ")".to_string(),
8071 close: true,
8072 surround: true,
8073 newline: true,
8074 },
8075 BracketPair {
8076 start: "/*".to_string(),
8077 end: " */".to_string(),
8078 close: true,
8079 surround: true,
8080 newline: true,
8081 },
8082 BracketPair {
8083 start: "[".to_string(),
8084 end: "]".to_string(),
8085 close: false,
8086 surround: false,
8087 newline: true,
8088 },
8089 BracketPair {
8090 start: "\"".to_string(),
8091 end: "\"".to_string(),
8092 close: true,
8093 surround: true,
8094 newline: false,
8095 },
8096 BracketPair {
8097 start: "<".to_string(),
8098 end: ">".to_string(),
8099 close: false,
8100 surround: true,
8101 newline: true,
8102 },
8103 ],
8104 ..Default::default()
8105 },
8106 autoclose_before: "})]".to_string(),
8107 ..Default::default()
8108 },
8109 Some(tree_sitter_rust::LANGUAGE.into()),
8110 );
8111 let language = Arc::new(language);
8112
8113 cx.language_registry().add(language.clone());
8114 cx.update_buffer(|buffer, cx| {
8115 buffer.set_language(Some(language), cx);
8116 });
8117
8118 // Ensure that signature_help is not called when no signature help is enabled.
8119 cx.set_state(
8120 &r#"
8121 fn main() {
8122 sampleˇ
8123 }
8124 "#
8125 .unindent(),
8126 );
8127 cx.update_editor(|editor, window, cx| {
8128 editor.handle_input("(", window, cx);
8129 });
8130 cx.assert_editor_state(
8131 &"
8132 fn main() {
8133 sample(ˇ)
8134 }
8135 "
8136 .unindent(),
8137 );
8138 cx.editor(|editor, _, _| {
8139 assert!(editor.signature_help_state.task().is_none());
8140 });
8141
8142 let mocked_response = lsp::SignatureHelp {
8143 signatures: vec![lsp::SignatureInformation {
8144 label: "fn sample(param1: u8, param2: u8)".to_string(),
8145 documentation: None,
8146 parameters: Some(vec![
8147 lsp::ParameterInformation {
8148 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8149 documentation: None,
8150 },
8151 lsp::ParameterInformation {
8152 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8153 documentation: None,
8154 },
8155 ]),
8156 active_parameter: None,
8157 }],
8158 active_signature: Some(0),
8159 active_parameter: Some(0),
8160 };
8161
8162 // Ensure that signature_help is called when enabled afte edits
8163 cx.update(|_, cx| {
8164 cx.update_global::<SettingsStore, _>(|settings, cx| {
8165 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8166 settings.auto_signature_help = Some(false);
8167 settings.show_signature_help_after_edits = Some(true);
8168 });
8169 });
8170 });
8171 cx.set_state(
8172 &r#"
8173 fn main() {
8174 sampleˇ
8175 }
8176 "#
8177 .unindent(),
8178 );
8179 cx.update_editor(|editor, window, cx| {
8180 editor.handle_input("(", window, cx);
8181 });
8182 cx.assert_editor_state(
8183 &"
8184 fn main() {
8185 sample(ˇ)
8186 }
8187 "
8188 .unindent(),
8189 );
8190 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8191 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8192 .await;
8193 cx.update_editor(|editor, _, _| {
8194 let signature_help_state = editor.signature_help_state.popover().cloned();
8195 assert!(signature_help_state.is_some());
8196 let ParsedMarkdown {
8197 text, highlights, ..
8198 } = signature_help_state.unwrap().parsed_content;
8199 assert_eq!(text, "param1: u8, param2: u8");
8200 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8201 editor.signature_help_state = SignatureHelpState::default();
8202 });
8203
8204 // Ensure that signature_help is called when auto signature help override is enabled
8205 cx.update(|_, cx| {
8206 cx.update_global::<SettingsStore, _>(|settings, cx| {
8207 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8208 settings.auto_signature_help = Some(true);
8209 settings.show_signature_help_after_edits = Some(false);
8210 });
8211 });
8212 });
8213 cx.set_state(
8214 &r#"
8215 fn main() {
8216 sampleˇ
8217 }
8218 "#
8219 .unindent(),
8220 );
8221 cx.update_editor(|editor, window, cx| {
8222 editor.handle_input("(", window, cx);
8223 });
8224 cx.assert_editor_state(
8225 &"
8226 fn main() {
8227 sample(ˇ)
8228 }
8229 "
8230 .unindent(),
8231 );
8232 handle_signature_help_request(&mut cx, mocked_response).await;
8233 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8234 .await;
8235 cx.editor(|editor, _, _| {
8236 let signature_help_state = editor.signature_help_state.popover().cloned();
8237 assert!(signature_help_state.is_some());
8238 let ParsedMarkdown {
8239 text, highlights, ..
8240 } = signature_help_state.unwrap().parsed_content;
8241 assert_eq!(text, "param1: u8, param2: u8");
8242 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8243 });
8244}
8245
8246#[gpui::test]
8247async fn test_signature_help(cx: &mut gpui::TestAppContext) {
8248 init_test(cx, |_| {});
8249 cx.update(|cx| {
8250 cx.update_global::<SettingsStore, _>(|settings, cx| {
8251 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8252 settings.auto_signature_help = Some(true);
8253 });
8254 });
8255 });
8256
8257 let mut cx = EditorLspTestContext::new_rust(
8258 lsp::ServerCapabilities {
8259 signature_help_provider: Some(lsp::SignatureHelpOptions {
8260 ..Default::default()
8261 }),
8262 ..Default::default()
8263 },
8264 cx,
8265 )
8266 .await;
8267
8268 // A test that directly calls `show_signature_help`
8269 cx.update_editor(|editor, window, cx| {
8270 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8271 });
8272
8273 let mocked_response = lsp::SignatureHelp {
8274 signatures: vec![lsp::SignatureInformation {
8275 label: "fn sample(param1: u8, param2: u8)".to_string(),
8276 documentation: None,
8277 parameters: Some(vec![
8278 lsp::ParameterInformation {
8279 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8280 documentation: None,
8281 },
8282 lsp::ParameterInformation {
8283 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8284 documentation: None,
8285 },
8286 ]),
8287 active_parameter: None,
8288 }],
8289 active_signature: Some(0),
8290 active_parameter: Some(0),
8291 };
8292 handle_signature_help_request(&mut cx, mocked_response).await;
8293
8294 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8295 .await;
8296
8297 cx.editor(|editor, _, _| {
8298 let signature_help_state = editor.signature_help_state.popover().cloned();
8299 assert!(signature_help_state.is_some());
8300 let ParsedMarkdown {
8301 text, highlights, ..
8302 } = signature_help_state.unwrap().parsed_content;
8303 assert_eq!(text, "param1: u8, param2: u8");
8304 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8305 });
8306
8307 // When exiting outside from inside the brackets, `signature_help` is closed.
8308 cx.set_state(indoc! {"
8309 fn main() {
8310 sample(ˇ);
8311 }
8312
8313 fn sample(param1: u8, param2: u8) {}
8314 "});
8315
8316 cx.update_editor(|editor, window, cx| {
8317 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
8318 });
8319
8320 let mocked_response = lsp::SignatureHelp {
8321 signatures: Vec::new(),
8322 active_signature: None,
8323 active_parameter: None,
8324 };
8325 handle_signature_help_request(&mut cx, mocked_response).await;
8326
8327 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
8328 .await;
8329
8330 cx.editor(|editor, _, _| {
8331 assert!(!editor.signature_help_state.is_shown());
8332 });
8333
8334 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
8335 cx.set_state(indoc! {"
8336 fn main() {
8337 sample(ˇ);
8338 }
8339
8340 fn sample(param1: u8, param2: u8) {}
8341 "});
8342
8343 let mocked_response = lsp::SignatureHelp {
8344 signatures: vec![lsp::SignatureInformation {
8345 label: "fn sample(param1: u8, param2: u8)".to_string(),
8346 documentation: None,
8347 parameters: Some(vec![
8348 lsp::ParameterInformation {
8349 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8350 documentation: None,
8351 },
8352 lsp::ParameterInformation {
8353 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8354 documentation: None,
8355 },
8356 ]),
8357 active_parameter: None,
8358 }],
8359 active_signature: Some(0),
8360 active_parameter: Some(0),
8361 };
8362 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8363 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8364 .await;
8365 cx.editor(|editor, _, _| {
8366 assert!(editor.signature_help_state.is_shown());
8367 });
8368
8369 // Restore the popover with more parameter input
8370 cx.set_state(indoc! {"
8371 fn main() {
8372 sample(param1, param2ˇ);
8373 }
8374
8375 fn sample(param1: u8, param2: u8) {}
8376 "});
8377
8378 let mocked_response = lsp::SignatureHelp {
8379 signatures: vec![lsp::SignatureInformation {
8380 label: "fn sample(param1: u8, param2: u8)".to_string(),
8381 documentation: None,
8382 parameters: Some(vec![
8383 lsp::ParameterInformation {
8384 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8385 documentation: None,
8386 },
8387 lsp::ParameterInformation {
8388 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8389 documentation: None,
8390 },
8391 ]),
8392 active_parameter: None,
8393 }],
8394 active_signature: Some(0),
8395 active_parameter: Some(1),
8396 };
8397 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8398 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8399 .await;
8400
8401 // When selecting a range, the popover is gone.
8402 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
8403 cx.update_editor(|editor, window, cx| {
8404 editor.change_selections(None, window, cx, |s| {
8405 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
8406 })
8407 });
8408 cx.assert_editor_state(indoc! {"
8409 fn main() {
8410 sample(param1, «ˇparam2»);
8411 }
8412
8413 fn sample(param1: u8, param2: u8) {}
8414 "});
8415 cx.editor(|editor, _, _| {
8416 assert!(!editor.signature_help_state.is_shown());
8417 });
8418
8419 // When unselecting again, the popover is back if within the brackets.
8420 cx.update_editor(|editor, window, cx| {
8421 editor.change_selections(None, window, cx, |s| {
8422 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8423 })
8424 });
8425 cx.assert_editor_state(indoc! {"
8426 fn main() {
8427 sample(param1, ˇparam2);
8428 }
8429
8430 fn sample(param1: u8, param2: u8) {}
8431 "});
8432 handle_signature_help_request(&mut cx, mocked_response).await;
8433 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8434 .await;
8435 cx.editor(|editor, _, _| {
8436 assert!(editor.signature_help_state.is_shown());
8437 });
8438
8439 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
8440 cx.update_editor(|editor, window, cx| {
8441 editor.change_selections(None, window, cx, |s| {
8442 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
8443 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8444 })
8445 });
8446 cx.assert_editor_state(indoc! {"
8447 fn main() {
8448 sample(param1, ˇparam2);
8449 }
8450
8451 fn sample(param1: u8, param2: u8) {}
8452 "});
8453
8454 let mocked_response = lsp::SignatureHelp {
8455 signatures: vec![lsp::SignatureInformation {
8456 label: "fn sample(param1: u8, param2: u8)".to_string(),
8457 documentation: None,
8458 parameters: Some(vec![
8459 lsp::ParameterInformation {
8460 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8461 documentation: None,
8462 },
8463 lsp::ParameterInformation {
8464 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8465 documentation: None,
8466 },
8467 ]),
8468 active_parameter: None,
8469 }],
8470 active_signature: Some(0),
8471 active_parameter: Some(1),
8472 };
8473 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8474 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8475 .await;
8476 cx.update_editor(|editor, _, cx| {
8477 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
8478 });
8479 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
8480 .await;
8481 cx.update_editor(|editor, window, cx| {
8482 editor.change_selections(None, window, cx, |s| {
8483 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
8484 })
8485 });
8486 cx.assert_editor_state(indoc! {"
8487 fn main() {
8488 sample(param1, «ˇparam2»);
8489 }
8490
8491 fn sample(param1: u8, param2: u8) {}
8492 "});
8493 cx.update_editor(|editor, window, cx| {
8494 editor.change_selections(None, window, cx, |s| {
8495 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8496 })
8497 });
8498 cx.assert_editor_state(indoc! {"
8499 fn main() {
8500 sample(param1, ˇparam2);
8501 }
8502
8503 fn sample(param1: u8, param2: u8) {}
8504 "});
8505 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
8506 .await;
8507}
8508
8509#[gpui::test]
8510async fn test_completion(cx: &mut gpui::TestAppContext) {
8511 init_test(cx, |_| {});
8512
8513 let mut cx = EditorLspTestContext::new_rust(
8514 lsp::ServerCapabilities {
8515 completion_provider: Some(lsp::CompletionOptions {
8516 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
8517 resolve_provider: Some(true),
8518 ..Default::default()
8519 }),
8520 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
8521 ..Default::default()
8522 },
8523 cx,
8524 )
8525 .await;
8526 let counter = Arc::new(AtomicUsize::new(0));
8527
8528 cx.set_state(indoc! {"
8529 oneˇ
8530 two
8531 three
8532 "});
8533 cx.simulate_keystroke(".");
8534 handle_completion_request(
8535 &mut cx,
8536 indoc! {"
8537 one.|<>
8538 two
8539 three
8540 "},
8541 vec!["first_completion", "second_completion"],
8542 counter.clone(),
8543 )
8544 .await;
8545 cx.condition(|editor, _| editor.context_menu_visible())
8546 .await;
8547 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
8548
8549 let _handler = handle_signature_help_request(
8550 &mut cx,
8551 lsp::SignatureHelp {
8552 signatures: vec![lsp::SignatureInformation {
8553 label: "test signature".to_string(),
8554 documentation: None,
8555 parameters: Some(vec![lsp::ParameterInformation {
8556 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
8557 documentation: None,
8558 }]),
8559 active_parameter: None,
8560 }],
8561 active_signature: None,
8562 active_parameter: None,
8563 },
8564 );
8565 cx.update_editor(|editor, window, cx| {
8566 assert!(
8567 !editor.signature_help_state.is_shown(),
8568 "No signature help was called for"
8569 );
8570 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8571 });
8572 cx.run_until_parked();
8573 cx.update_editor(|editor, _, _| {
8574 assert!(
8575 !editor.signature_help_state.is_shown(),
8576 "No signature help should be shown when completions menu is open"
8577 );
8578 });
8579
8580 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8581 editor.context_menu_next(&Default::default(), window, cx);
8582 editor
8583 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8584 .unwrap()
8585 });
8586 cx.assert_editor_state(indoc! {"
8587 one.second_completionˇ
8588 two
8589 three
8590 "});
8591
8592 handle_resolve_completion_request(
8593 &mut cx,
8594 Some(vec![
8595 (
8596 //This overlaps with the primary completion edit which is
8597 //misbehavior from the LSP spec, test that we filter it out
8598 indoc! {"
8599 one.second_ˇcompletion
8600 two
8601 threeˇ
8602 "},
8603 "overlapping additional edit",
8604 ),
8605 (
8606 indoc! {"
8607 one.second_completion
8608 two
8609 threeˇ
8610 "},
8611 "\nadditional edit",
8612 ),
8613 ]),
8614 )
8615 .await;
8616 apply_additional_edits.await.unwrap();
8617 cx.assert_editor_state(indoc! {"
8618 one.second_completionˇ
8619 two
8620 three
8621 additional edit
8622 "});
8623
8624 cx.set_state(indoc! {"
8625 one.second_completion
8626 twoˇ
8627 threeˇ
8628 additional edit
8629 "});
8630 cx.simulate_keystroke(" ");
8631 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8632 cx.simulate_keystroke("s");
8633 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8634
8635 cx.assert_editor_state(indoc! {"
8636 one.second_completion
8637 two sˇ
8638 three sˇ
8639 additional edit
8640 "});
8641 handle_completion_request(
8642 &mut cx,
8643 indoc! {"
8644 one.second_completion
8645 two s
8646 three <s|>
8647 additional edit
8648 "},
8649 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
8650 counter.clone(),
8651 )
8652 .await;
8653 cx.condition(|editor, _| editor.context_menu_visible())
8654 .await;
8655 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
8656
8657 cx.simulate_keystroke("i");
8658
8659 handle_completion_request(
8660 &mut cx,
8661 indoc! {"
8662 one.second_completion
8663 two si
8664 three <si|>
8665 additional edit
8666 "},
8667 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
8668 counter.clone(),
8669 )
8670 .await;
8671 cx.condition(|editor, _| editor.context_menu_visible())
8672 .await;
8673 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
8674
8675 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8676 editor
8677 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8678 .unwrap()
8679 });
8680 cx.assert_editor_state(indoc! {"
8681 one.second_completion
8682 two sixth_completionˇ
8683 three sixth_completionˇ
8684 additional edit
8685 "});
8686
8687 apply_additional_edits.await.unwrap();
8688
8689 update_test_language_settings(&mut cx, |settings| {
8690 settings.defaults.show_completions_on_input = Some(false);
8691 });
8692 cx.set_state("editorˇ");
8693 cx.simulate_keystroke(".");
8694 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8695 cx.simulate_keystroke("c");
8696 cx.simulate_keystroke("l");
8697 cx.simulate_keystroke("o");
8698 cx.assert_editor_state("editor.cloˇ");
8699 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8700 cx.update_editor(|editor, window, cx| {
8701 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
8702 });
8703 handle_completion_request(
8704 &mut cx,
8705 "editor.<clo|>",
8706 vec!["close", "clobber"],
8707 counter.clone(),
8708 )
8709 .await;
8710 cx.condition(|editor, _| editor.context_menu_visible())
8711 .await;
8712 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
8713
8714 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8715 editor
8716 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8717 .unwrap()
8718 });
8719 cx.assert_editor_state("editor.closeˇ");
8720 handle_resolve_completion_request(&mut cx, None).await;
8721 apply_additional_edits.await.unwrap();
8722}
8723
8724#[gpui::test]
8725async fn test_multiline_completion(cx: &mut gpui::TestAppContext) {
8726 init_test(cx, |_| {});
8727
8728 let fs = FakeFs::new(cx.executor());
8729 fs.insert_tree(
8730 "/a",
8731 json!({
8732 "main.ts": "a",
8733 }),
8734 )
8735 .await;
8736
8737 let project = Project::test(fs, ["/a".as_ref()], cx).await;
8738 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8739 let typescript_language = Arc::new(Language::new(
8740 LanguageConfig {
8741 name: "TypeScript".into(),
8742 matcher: LanguageMatcher {
8743 path_suffixes: vec!["ts".to_string()],
8744 ..LanguageMatcher::default()
8745 },
8746 line_comments: vec!["// ".into()],
8747 ..LanguageConfig::default()
8748 },
8749 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8750 ));
8751 language_registry.add(typescript_language.clone());
8752 let mut fake_servers = language_registry.register_fake_lsp(
8753 "TypeScript",
8754 FakeLspAdapter {
8755 capabilities: lsp::ServerCapabilities {
8756 completion_provider: Some(lsp::CompletionOptions {
8757 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
8758 ..lsp::CompletionOptions::default()
8759 }),
8760 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
8761 ..lsp::ServerCapabilities::default()
8762 },
8763 // Emulate vtsls label generation
8764 label_for_completion: Some(Box::new(|item, _| {
8765 let text = if let Some(description) = item
8766 .label_details
8767 .as_ref()
8768 .and_then(|label_details| label_details.description.as_ref())
8769 {
8770 format!("{} {}", item.label, description)
8771 } else if let Some(detail) = &item.detail {
8772 format!("{} {}", item.label, detail)
8773 } else {
8774 item.label.clone()
8775 };
8776 let len = text.len();
8777 Some(language::CodeLabel {
8778 text,
8779 runs: Vec::new(),
8780 filter_range: 0..len,
8781 })
8782 })),
8783 ..FakeLspAdapter::default()
8784 },
8785 );
8786 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8787 let cx = &mut VisualTestContext::from_window(*workspace, cx);
8788 let worktree_id = workspace
8789 .update(cx, |workspace, _window, cx| {
8790 workspace.project().update(cx, |project, cx| {
8791 project.worktrees(cx).next().unwrap().read(cx).id()
8792 })
8793 })
8794 .unwrap();
8795 let _buffer = project
8796 .update(cx, |project, cx| {
8797 project.open_local_buffer_with_lsp("/a/main.ts", cx)
8798 })
8799 .await
8800 .unwrap();
8801 let editor = workspace
8802 .update(cx, |workspace, window, cx| {
8803 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
8804 })
8805 .unwrap()
8806 .await
8807 .unwrap()
8808 .downcast::<Editor>()
8809 .unwrap();
8810 let fake_server = fake_servers.next().await.unwrap();
8811
8812 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
8813 let multiline_label_2 = "a\nb\nc\n";
8814 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
8815 let multiline_description = "d\ne\nf\n";
8816 let multiline_detail_2 = "g\nh\ni\n";
8817
8818 let mut completion_handle =
8819 fake_server.handle_request::<lsp::request::Completion, _, _>(move |params, _| async move {
8820 Ok(Some(lsp::CompletionResponse::Array(vec![
8821 lsp::CompletionItem {
8822 label: multiline_label.to_string(),
8823 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8824 range: lsp::Range {
8825 start: lsp::Position {
8826 line: params.text_document_position.position.line,
8827 character: params.text_document_position.position.character,
8828 },
8829 end: lsp::Position {
8830 line: params.text_document_position.position.line,
8831 character: params.text_document_position.position.character,
8832 },
8833 },
8834 new_text: "new_text_1".to_string(),
8835 })),
8836 ..lsp::CompletionItem::default()
8837 },
8838 lsp::CompletionItem {
8839 label: "single line label 1".to_string(),
8840 detail: Some(multiline_detail.to_string()),
8841 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8842 range: lsp::Range {
8843 start: lsp::Position {
8844 line: params.text_document_position.position.line,
8845 character: params.text_document_position.position.character,
8846 },
8847 end: lsp::Position {
8848 line: params.text_document_position.position.line,
8849 character: params.text_document_position.position.character,
8850 },
8851 },
8852 new_text: "new_text_2".to_string(),
8853 })),
8854 ..lsp::CompletionItem::default()
8855 },
8856 lsp::CompletionItem {
8857 label: "single line label 2".to_string(),
8858 label_details: Some(lsp::CompletionItemLabelDetails {
8859 description: Some(multiline_description.to_string()),
8860 detail: None,
8861 }),
8862 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8863 range: lsp::Range {
8864 start: lsp::Position {
8865 line: params.text_document_position.position.line,
8866 character: params.text_document_position.position.character,
8867 },
8868 end: lsp::Position {
8869 line: params.text_document_position.position.line,
8870 character: params.text_document_position.position.character,
8871 },
8872 },
8873 new_text: "new_text_2".to_string(),
8874 })),
8875 ..lsp::CompletionItem::default()
8876 },
8877 lsp::CompletionItem {
8878 label: multiline_label_2.to_string(),
8879 detail: Some(multiline_detail_2.to_string()),
8880 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8881 range: lsp::Range {
8882 start: lsp::Position {
8883 line: params.text_document_position.position.line,
8884 character: params.text_document_position.position.character,
8885 },
8886 end: lsp::Position {
8887 line: params.text_document_position.position.line,
8888 character: params.text_document_position.position.character,
8889 },
8890 },
8891 new_text: "new_text_3".to_string(),
8892 })),
8893 ..lsp::CompletionItem::default()
8894 },
8895 lsp::CompletionItem {
8896 label: "Label with many spaces and \t but without newlines".to_string(),
8897 detail: Some(
8898 "Details with many spaces and \t but without newlines".to_string(),
8899 ),
8900 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8901 range: lsp::Range {
8902 start: lsp::Position {
8903 line: params.text_document_position.position.line,
8904 character: params.text_document_position.position.character,
8905 },
8906 end: lsp::Position {
8907 line: params.text_document_position.position.line,
8908 character: params.text_document_position.position.character,
8909 },
8910 },
8911 new_text: "new_text_4".to_string(),
8912 })),
8913 ..lsp::CompletionItem::default()
8914 },
8915 ])))
8916 });
8917
8918 editor.update_in(cx, |editor, window, cx| {
8919 cx.focus_self(window);
8920 editor.move_to_end(&MoveToEnd, window, cx);
8921 editor.handle_input(".", window, cx);
8922 });
8923 cx.run_until_parked();
8924 completion_handle.next().await.unwrap();
8925
8926 editor.update(cx, |editor, _| {
8927 assert!(editor.context_menu_visible());
8928 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
8929 {
8930 let completion_labels = menu
8931 .completions
8932 .borrow()
8933 .iter()
8934 .map(|c| c.label.text.clone())
8935 .collect::<Vec<_>>();
8936 assert_eq!(
8937 completion_labels,
8938 &[
8939 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
8940 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
8941 "single line label 2 d e f ",
8942 "a b c g h i ",
8943 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
8944 ],
8945 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
8946 );
8947
8948 for completion in menu
8949 .completions
8950 .borrow()
8951 .iter() {
8952 assert_eq!(
8953 completion.label.filter_range,
8954 0..completion.label.text.len(),
8955 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
8956 );
8957 }
8958
8959 } else {
8960 panic!("expected completion menu to be open");
8961 }
8962 });
8963}
8964
8965#[gpui::test]
8966async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
8967 init_test(cx, |_| {});
8968 let mut cx = EditorLspTestContext::new_rust(
8969 lsp::ServerCapabilities {
8970 completion_provider: Some(lsp::CompletionOptions {
8971 trigger_characters: Some(vec![".".to_string()]),
8972 ..Default::default()
8973 }),
8974 ..Default::default()
8975 },
8976 cx,
8977 )
8978 .await;
8979 cx.lsp
8980 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
8981 Ok(Some(lsp::CompletionResponse::Array(vec![
8982 lsp::CompletionItem {
8983 label: "first".into(),
8984 ..Default::default()
8985 },
8986 lsp::CompletionItem {
8987 label: "last".into(),
8988 ..Default::default()
8989 },
8990 ])))
8991 });
8992 cx.set_state("variableˇ");
8993 cx.simulate_keystroke(".");
8994 cx.executor().run_until_parked();
8995
8996 cx.update_editor(|editor, _, _| {
8997 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
8998 {
8999 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
9000 } else {
9001 panic!("expected completion menu to be open");
9002 }
9003 });
9004
9005 cx.update_editor(|editor, window, cx| {
9006 editor.move_page_down(&MovePageDown::default(), window, cx);
9007 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9008 {
9009 assert!(
9010 menu.selected_item == 1,
9011 "expected PageDown to select the last item from the context menu"
9012 );
9013 } else {
9014 panic!("expected completion menu to stay open after PageDown");
9015 }
9016 });
9017
9018 cx.update_editor(|editor, window, cx| {
9019 editor.move_page_up(&MovePageUp::default(), window, cx);
9020 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9021 {
9022 assert!(
9023 menu.selected_item == 0,
9024 "expected PageUp to select the first item from the context menu"
9025 );
9026 } else {
9027 panic!("expected completion menu to stay open after PageUp");
9028 }
9029 });
9030}
9031
9032#[gpui::test]
9033async fn test_completion_sort(cx: &mut gpui::TestAppContext) {
9034 init_test(cx, |_| {});
9035 let mut cx = EditorLspTestContext::new_rust(
9036 lsp::ServerCapabilities {
9037 completion_provider: Some(lsp::CompletionOptions {
9038 trigger_characters: Some(vec![".".to_string()]),
9039 ..Default::default()
9040 }),
9041 ..Default::default()
9042 },
9043 cx,
9044 )
9045 .await;
9046 cx.lsp
9047 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9048 Ok(Some(lsp::CompletionResponse::Array(vec![
9049 lsp::CompletionItem {
9050 label: "Range".into(),
9051 sort_text: Some("a".into()),
9052 ..Default::default()
9053 },
9054 lsp::CompletionItem {
9055 label: "r".into(),
9056 sort_text: Some("b".into()),
9057 ..Default::default()
9058 },
9059 lsp::CompletionItem {
9060 label: "ret".into(),
9061 sort_text: Some("c".into()),
9062 ..Default::default()
9063 },
9064 lsp::CompletionItem {
9065 label: "return".into(),
9066 sort_text: Some("d".into()),
9067 ..Default::default()
9068 },
9069 lsp::CompletionItem {
9070 label: "slice".into(),
9071 sort_text: Some("d".into()),
9072 ..Default::default()
9073 },
9074 ])))
9075 });
9076 cx.set_state("rˇ");
9077 cx.executor().run_until_parked();
9078 cx.update_editor(|editor, window, cx| {
9079 editor.show_completions(
9080 &ShowCompletions {
9081 trigger: Some("r".into()),
9082 },
9083 window,
9084 cx,
9085 );
9086 });
9087 cx.executor().run_until_parked();
9088
9089 cx.update_editor(|editor, _, _| {
9090 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9091 {
9092 assert_eq!(
9093 completion_menu_entries(&menu),
9094 &["r", "ret", "Range", "return"]
9095 );
9096 } else {
9097 panic!("expected completion menu to be open");
9098 }
9099 });
9100}
9101
9102#[gpui::test]
9103async fn test_no_duplicated_completion_requests(cx: &mut gpui::TestAppContext) {
9104 init_test(cx, |_| {});
9105
9106 let mut cx = EditorLspTestContext::new_rust(
9107 lsp::ServerCapabilities {
9108 completion_provider: Some(lsp::CompletionOptions {
9109 trigger_characters: Some(vec![".".to_string()]),
9110 resolve_provider: Some(true),
9111 ..Default::default()
9112 }),
9113 ..Default::default()
9114 },
9115 cx,
9116 )
9117 .await;
9118
9119 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
9120 cx.simulate_keystroke(".");
9121 let completion_item = lsp::CompletionItem {
9122 label: "Some".into(),
9123 kind: Some(lsp::CompletionItemKind::SNIPPET),
9124 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
9125 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
9126 kind: lsp::MarkupKind::Markdown,
9127 value: "```rust\nSome(2)\n```".to_string(),
9128 })),
9129 deprecated: Some(false),
9130 sort_text: Some("Some".to_string()),
9131 filter_text: Some("Some".to_string()),
9132 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
9133 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9134 range: lsp::Range {
9135 start: lsp::Position {
9136 line: 0,
9137 character: 22,
9138 },
9139 end: lsp::Position {
9140 line: 0,
9141 character: 22,
9142 },
9143 },
9144 new_text: "Some(2)".to_string(),
9145 })),
9146 additional_text_edits: Some(vec![lsp::TextEdit {
9147 range: lsp::Range {
9148 start: lsp::Position {
9149 line: 0,
9150 character: 20,
9151 },
9152 end: lsp::Position {
9153 line: 0,
9154 character: 22,
9155 },
9156 },
9157 new_text: "".to_string(),
9158 }]),
9159 ..Default::default()
9160 };
9161
9162 let closure_completion_item = completion_item.clone();
9163 let counter = Arc::new(AtomicUsize::new(0));
9164 let counter_clone = counter.clone();
9165 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
9166 let task_completion_item = closure_completion_item.clone();
9167 counter_clone.fetch_add(1, atomic::Ordering::Release);
9168 async move {
9169 Ok(Some(lsp::CompletionResponse::Array(vec![
9170 task_completion_item,
9171 ])))
9172 }
9173 });
9174
9175 cx.condition(|editor, _| editor.context_menu_visible())
9176 .await;
9177 cx.assert_editor_state(indoc! {"fn main() { let a = 2.ˇ; }"});
9178 assert!(request.next().await.is_some());
9179 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9180
9181 cx.simulate_keystroke("S");
9182 cx.simulate_keystroke("o");
9183 cx.simulate_keystroke("m");
9184 cx.condition(|editor, _| editor.context_menu_visible())
9185 .await;
9186 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Somˇ; }"});
9187 assert!(request.next().await.is_some());
9188 assert!(request.next().await.is_some());
9189 assert!(request.next().await.is_some());
9190 request.close();
9191 assert!(request.next().await.is_none());
9192 assert_eq!(
9193 counter.load(atomic::Ordering::Acquire),
9194 4,
9195 "With the completions menu open, only one LSP request should happen per input"
9196 );
9197}
9198
9199#[gpui::test]
9200async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
9201 init_test(cx, |_| {});
9202 let mut cx = EditorTestContext::new(cx).await;
9203 let language = Arc::new(Language::new(
9204 LanguageConfig {
9205 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
9206 ..Default::default()
9207 },
9208 Some(tree_sitter_rust::LANGUAGE.into()),
9209 ));
9210 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
9211
9212 // If multiple selections intersect a line, the line is only toggled once.
9213 cx.set_state(indoc! {"
9214 fn a() {
9215 «//b();
9216 ˇ»// «c();
9217 //ˇ» d();
9218 }
9219 "});
9220
9221 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9222
9223 cx.assert_editor_state(indoc! {"
9224 fn a() {
9225 «b();
9226 c();
9227 ˇ» d();
9228 }
9229 "});
9230
9231 // The comment prefix is inserted at the same column for every line in a
9232 // selection.
9233 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9234
9235 cx.assert_editor_state(indoc! {"
9236 fn a() {
9237 // «b();
9238 // c();
9239 ˇ»// d();
9240 }
9241 "});
9242
9243 // If a selection ends at the beginning of a line, that line is not toggled.
9244 cx.set_selections_state(indoc! {"
9245 fn a() {
9246 // b();
9247 «// c();
9248 ˇ» // d();
9249 }
9250 "});
9251
9252 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9253
9254 cx.assert_editor_state(indoc! {"
9255 fn a() {
9256 // b();
9257 «c();
9258 ˇ» // d();
9259 }
9260 "});
9261
9262 // If a selection span a single line and is empty, the line is toggled.
9263 cx.set_state(indoc! {"
9264 fn a() {
9265 a();
9266 b();
9267 ˇ
9268 }
9269 "});
9270
9271 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9272
9273 cx.assert_editor_state(indoc! {"
9274 fn a() {
9275 a();
9276 b();
9277 //•ˇ
9278 }
9279 "});
9280
9281 // If a selection span multiple lines, empty lines are not toggled.
9282 cx.set_state(indoc! {"
9283 fn a() {
9284 «a();
9285
9286 c();ˇ»
9287 }
9288 "});
9289
9290 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9291
9292 cx.assert_editor_state(indoc! {"
9293 fn a() {
9294 // «a();
9295
9296 // c();ˇ»
9297 }
9298 "});
9299
9300 // If a selection includes multiple comment prefixes, all lines are uncommented.
9301 cx.set_state(indoc! {"
9302 fn a() {
9303 «// a();
9304 /// b();
9305 //! c();ˇ»
9306 }
9307 "});
9308
9309 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9310
9311 cx.assert_editor_state(indoc! {"
9312 fn a() {
9313 «a();
9314 b();
9315 c();ˇ»
9316 }
9317 "});
9318}
9319
9320#[gpui::test]
9321async fn test_toggle_comment_ignore_indent(cx: &mut gpui::TestAppContext) {
9322 init_test(cx, |_| {});
9323 let mut cx = EditorTestContext::new(cx).await;
9324 let language = Arc::new(Language::new(
9325 LanguageConfig {
9326 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
9327 ..Default::default()
9328 },
9329 Some(tree_sitter_rust::LANGUAGE.into()),
9330 ));
9331 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
9332
9333 let toggle_comments = &ToggleComments {
9334 advance_downwards: false,
9335 ignore_indent: true,
9336 };
9337
9338 // If multiple selections intersect a line, the line is only toggled once.
9339 cx.set_state(indoc! {"
9340 fn a() {
9341 // «b();
9342 // c();
9343 // ˇ» d();
9344 }
9345 "});
9346
9347 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9348
9349 cx.assert_editor_state(indoc! {"
9350 fn a() {
9351 «b();
9352 c();
9353 ˇ» d();
9354 }
9355 "});
9356
9357 // The comment prefix is inserted at the beginning of each line
9358 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9359
9360 cx.assert_editor_state(indoc! {"
9361 fn a() {
9362 // «b();
9363 // c();
9364 // ˇ» d();
9365 }
9366 "});
9367
9368 // If a selection ends at the beginning of a line, that line is not toggled.
9369 cx.set_selections_state(indoc! {"
9370 fn a() {
9371 // b();
9372 // «c();
9373 ˇ»// d();
9374 }
9375 "});
9376
9377 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9378
9379 cx.assert_editor_state(indoc! {"
9380 fn a() {
9381 // b();
9382 «c();
9383 ˇ»// d();
9384 }
9385 "});
9386
9387 // If a selection span a single line and is empty, the line is toggled.
9388 cx.set_state(indoc! {"
9389 fn a() {
9390 a();
9391 b();
9392 ˇ
9393 }
9394 "});
9395
9396 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9397
9398 cx.assert_editor_state(indoc! {"
9399 fn a() {
9400 a();
9401 b();
9402 //ˇ
9403 }
9404 "});
9405
9406 // If a selection span multiple lines, empty lines are not toggled.
9407 cx.set_state(indoc! {"
9408 fn a() {
9409 «a();
9410
9411 c();ˇ»
9412 }
9413 "});
9414
9415 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9416
9417 cx.assert_editor_state(indoc! {"
9418 fn a() {
9419 // «a();
9420
9421 // c();ˇ»
9422 }
9423 "});
9424
9425 // If a selection includes multiple comment prefixes, all lines are uncommented.
9426 cx.set_state(indoc! {"
9427 fn a() {
9428 // «a();
9429 /// b();
9430 //! c();ˇ»
9431 }
9432 "});
9433
9434 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9435
9436 cx.assert_editor_state(indoc! {"
9437 fn a() {
9438 «a();
9439 b();
9440 c();ˇ»
9441 }
9442 "});
9443}
9444
9445#[gpui::test]
9446async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
9447 init_test(cx, |_| {});
9448
9449 let language = Arc::new(Language::new(
9450 LanguageConfig {
9451 line_comments: vec!["// ".into()],
9452 ..Default::default()
9453 },
9454 Some(tree_sitter_rust::LANGUAGE.into()),
9455 ));
9456
9457 let mut cx = EditorTestContext::new(cx).await;
9458
9459 cx.language_registry().add(language.clone());
9460 cx.update_buffer(|buffer, cx| {
9461 buffer.set_language(Some(language), cx);
9462 });
9463
9464 let toggle_comments = &ToggleComments {
9465 advance_downwards: true,
9466 ignore_indent: false,
9467 };
9468
9469 // Single cursor on one line -> advance
9470 // Cursor moves horizontally 3 characters as well on non-blank line
9471 cx.set_state(indoc!(
9472 "fn a() {
9473 ˇdog();
9474 cat();
9475 }"
9476 ));
9477 cx.update_editor(|editor, window, cx| {
9478 editor.toggle_comments(toggle_comments, window, cx);
9479 });
9480 cx.assert_editor_state(indoc!(
9481 "fn a() {
9482 // dog();
9483 catˇ();
9484 }"
9485 ));
9486
9487 // Single selection on one line -> don't advance
9488 cx.set_state(indoc!(
9489 "fn a() {
9490 «dog()ˇ»;
9491 cat();
9492 }"
9493 ));
9494 cx.update_editor(|editor, window, cx| {
9495 editor.toggle_comments(toggle_comments, window, cx);
9496 });
9497 cx.assert_editor_state(indoc!(
9498 "fn a() {
9499 // «dog()ˇ»;
9500 cat();
9501 }"
9502 ));
9503
9504 // Multiple cursors on one line -> advance
9505 cx.set_state(indoc!(
9506 "fn a() {
9507 ˇdˇog();
9508 cat();
9509 }"
9510 ));
9511 cx.update_editor(|editor, window, cx| {
9512 editor.toggle_comments(toggle_comments, window, cx);
9513 });
9514 cx.assert_editor_state(indoc!(
9515 "fn a() {
9516 // dog();
9517 catˇ(ˇ);
9518 }"
9519 ));
9520
9521 // Multiple cursors on one line, with selection -> don't advance
9522 cx.set_state(indoc!(
9523 "fn a() {
9524 ˇdˇog«()ˇ»;
9525 cat();
9526 }"
9527 ));
9528 cx.update_editor(|editor, window, cx| {
9529 editor.toggle_comments(toggle_comments, window, cx);
9530 });
9531 cx.assert_editor_state(indoc!(
9532 "fn a() {
9533 // ˇdˇog«()ˇ»;
9534 cat();
9535 }"
9536 ));
9537
9538 // Single cursor on one line -> advance
9539 // Cursor moves to column 0 on blank line
9540 cx.set_state(indoc!(
9541 "fn a() {
9542 ˇdog();
9543
9544 cat();
9545 }"
9546 ));
9547 cx.update_editor(|editor, window, cx| {
9548 editor.toggle_comments(toggle_comments, window, cx);
9549 });
9550 cx.assert_editor_state(indoc!(
9551 "fn a() {
9552 // dog();
9553 ˇ
9554 cat();
9555 }"
9556 ));
9557
9558 // Single cursor on one line -> advance
9559 // Cursor starts and ends at column 0
9560 cx.set_state(indoc!(
9561 "fn a() {
9562 ˇ dog();
9563 cat();
9564 }"
9565 ));
9566 cx.update_editor(|editor, window, cx| {
9567 editor.toggle_comments(toggle_comments, window, cx);
9568 });
9569 cx.assert_editor_state(indoc!(
9570 "fn a() {
9571 // dog();
9572 ˇ cat();
9573 }"
9574 ));
9575}
9576
9577#[gpui::test]
9578async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
9579 init_test(cx, |_| {});
9580
9581 let mut cx = EditorTestContext::new(cx).await;
9582
9583 let html_language = Arc::new(
9584 Language::new(
9585 LanguageConfig {
9586 name: "HTML".into(),
9587 block_comment: Some(("<!-- ".into(), " -->".into())),
9588 ..Default::default()
9589 },
9590 Some(tree_sitter_html::language()),
9591 )
9592 .with_injection_query(
9593 r#"
9594 (script_element
9595 (raw_text) @injection.content
9596 (#set! injection.language "javascript"))
9597 "#,
9598 )
9599 .unwrap(),
9600 );
9601
9602 let javascript_language = Arc::new(Language::new(
9603 LanguageConfig {
9604 name: "JavaScript".into(),
9605 line_comments: vec!["// ".into()],
9606 ..Default::default()
9607 },
9608 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9609 ));
9610
9611 cx.language_registry().add(html_language.clone());
9612 cx.language_registry().add(javascript_language.clone());
9613 cx.update_buffer(|buffer, cx| {
9614 buffer.set_language(Some(html_language), cx);
9615 });
9616
9617 // Toggle comments for empty selections
9618 cx.set_state(
9619 &r#"
9620 <p>A</p>ˇ
9621 <p>B</p>ˇ
9622 <p>C</p>ˇ
9623 "#
9624 .unindent(),
9625 );
9626 cx.update_editor(|editor, window, cx| {
9627 editor.toggle_comments(&ToggleComments::default(), window, cx)
9628 });
9629 cx.assert_editor_state(
9630 &r#"
9631 <!-- <p>A</p>ˇ -->
9632 <!-- <p>B</p>ˇ -->
9633 <!-- <p>C</p>ˇ -->
9634 "#
9635 .unindent(),
9636 );
9637 cx.update_editor(|editor, window, cx| {
9638 editor.toggle_comments(&ToggleComments::default(), window, cx)
9639 });
9640 cx.assert_editor_state(
9641 &r#"
9642 <p>A</p>ˇ
9643 <p>B</p>ˇ
9644 <p>C</p>ˇ
9645 "#
9646 .unindent(),
9647 );
9648
9649 // Toggle comments for mixture of empty and non-empty selections, where
9650 // multiple selections occupy a given line.
9651 cx.set_state(
9652 &r#"
9653 <p>A«</p>
9654 <p>ˇ»B</p>ˇ
9655 <p>C«</p>
9656 <p>ˇ»D</p>ˇ
9657 "#
9658 .unindent(),
9659 );
9660
9661 cx.update_editor(|editor, window, cx| {
9662 editor.toggle_comments(&ToggleComments::default(), window, cx)
9663 });
9664 cx.assert_editor_state(
9665 &r#"
9666 <!-- <p>A«</p>
9667 <p>ˇ»B</p>ˇ -->
9668 <!-- <p>C«</p>
9669 <p>ˇ»D</p>ˇ -->
9670 "#
9671 .unindent(),
9672 );
9673 cx.update_editor(|editor, window, cx| {
9674 editor.toggle_comments(&ToggleComments::default(), window, cx)
9675 });
9676 cx.assert_editor_state(
9677 &r#"
9678 <p>A«</p>
9679 <p>ˇ»B</p>ˇ
9680 <p>C«</p>
9681 <p>ˇ»D</p>ˇ
9682 "#
9683 .unindent(),
9684 );
9685
9686 // Toggle comments when different languages are active for different
9687 // selections.
9688 cx.set_state(
9689 &r#"
9690 ˇ<script>
9691 ˇvar x = new Y();
9692 ˇ</script>
9693 "#
9694 .unindent(),
9695 );
9696 cx.executor().run_until_parked();
9697 cx.update_editor(|editor, window, cx| {
9698 editor.toggle_comments(&ToggleComments::default(), window, cx)
9699 });
9700 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
9701 // Uncommenting and commenting from this position brings in even more wrong artifacts.
9702 cx.assert_editor_state(
9703 &r#"
9704 <!-- ˇ<script> -->
9705 // ˇvar x = new Y();
9706 // ˇ</script>
9707 "#
9708 .unindent(),
9709 );
9710}
9711
9712#[gpui::test]
9713fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
9714 init_test(cx, |_| {});
9715
9716 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9717 let multibuffer = cx.new(|cx| {
9718 let mut multibuffer = MultiBuffer::new(ReadWrite);
9719 multibuffer.push_excerpts(
9720 buffer.clone(),
9721 [
9722 ExcerptRange {
9723 context: Point::new(0, 0)..Point::new(0, 4),
9724 primary: None,
9725 },
9726 ExcerptRange {
9727 context: Point::new(1, 0)..Point::new(1, 4),
9728 primary: None,
9729 },
9730 ],
9731 cx,
9732 );
9733 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
9734 multibuffer
9735 });
9736
9737 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
9738 editor.update_in(cx, |editor, window, cx| {
9739 assert_eq!(editor.text(cx), "aaaa\nbbbb");
9740 editor.change_selections(None, window, cx, |s| {
9741 s.select_ranges([
9742 Point::new(0, 0)..Point::new(0, 0),
9743 Point::new(1, 0)..Point::new(1, 0),
9744 ])
9745 });
9746
9747 editor.handle_input("X", window, cx);
9748 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
9749 assert_eq!(
9750 editor.selections.ranges(cx),
9751 [
9752 Point::new(0, 1)..Point::new(0, 1),
9753 Point::new(1, 1)..Point::new(1, 1),
9754 ]
9755 );
9756
9757 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
9758 editor.change_selections(None, window, cx, |s| {
9759 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
9760 });
9761 editor.backspace(&Default::default(), window, cx);
9762 assert_eq!(editor.text(cx), "Xa\nbbb");
9763 assert_eq!(
9764 editor.selections.ranges(cx),
9765 [Point::new(1, 0)..Point::new(1, 0)]
9766 );
9767
9768 editor.change_selections(None, window, cx, |s| {
9769 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
9770 });
9771 editor.backspace(&Default::default(), window, cx);
9772 assert_eq!(editor.text(cx), "X\nbb");
9773 assert_eq!(
9774 editor.selections.ranges(cx),
9775 [Point::new(0, 1)..Point::new(0, 1)]
9776 );
9777 });
9778}
9779
9780#[gpui::test]
9781fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
9782 init_test(cx, |_| {});
9783
9784 let markers = vec![('[', ']').into(), ('(', ')').into()];
9785 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
9786 indoc! {"
9787 [aaaa
9788 (bbbb]
9789 cccc)",
9790 },
9791 markers.clone(),
9792 );
9793 let excerpt_ranges = markers.into_iter().map(|marker| {
9794 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
9795 ExcerptRange {
9796 context,
9797 primary: None,
9798 }
9799 });
9800 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
9801 let multibuffer = cx.new(|cx| {
9802 let mut multibuffer = MultiBuffer::new(ReadWrite);
9803 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
9804 multibuffer
9805 });
9806
9807 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
9808 editor.update_in(cx, |editor, window, cx| {
9809 let (expected_text, selection_ranges) = marked_text_ranges(
9810 indoc! {"
9811 aaaa
9812 bˇbbb
9813 bˇbbˇb
9814 cccc"
9815 },
9816 true,
9817 );
9818 assert_eq!(editor.text(cx), expected_text);
9819 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
9820
9821 editor.handle_input("X", window, cx);
9822
9823 let (expected_text, expected_selections) = marked_text_ranges(
9824 indoc! {"
9825 aaaa
9826 bXˇbbXb
9827 bXˇbbXˇb
9828 cccc"
9829 },
9830 false,
9831 );
9832 assert_eq!(editor.text(cx), expected_text);
9833 assert_eq!(editor.selections.ranges(cx), expected_selections);
9834
9835 editor.newline(&Newline, window, cx);
9836 let (expected_text, expected_selections) = marked_text_ranges(
9837 indoc! {"
9838 aaaa
9839 bX
9840 ˇbbX
9841 b
9842 bX
9843 ˇbbX
9844 ˇb
9845 cccc"
9846 },
9847 false,
9848 );
9849 assert_eq!(editor.text(cx), expected_text);
9850 assert_eq!(editor.selections.ranges(cx), expected_selections);
9851 });
9852}
9853
9854#[gpui::test]
9855fn test_refresh_selections(cx: &mut TestAppContext) {
9856 init_test(cx, |_| {});
9857
9858 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9859 let mut excerpt1_id = None;
9860 let multibuffer = cx.new(|cx| {
9861 let mut multibuffer = MultiBuffer::new(ReadWrite);
9862 excerpt1_id = multibuffer
9863 .push_excerpts(
9864 buffer.clone(),
9865 [
9866 ExcerptRange {
9867 context: Point::new(0, 0)..Point::new(1, 4),
9868 primary: None,
9869 },
9870 ExcerptRange {
9871 context: Point::new(1, 0)..Point::new(2, 4),
9872 primary: None,
9873 },
9874 ],
9875 cx,
9876 )
9877 .into_iter()
9878 .next();
9879 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
9880 multibuffer
9881 });
9882
9883 let editor = cx.add_window(|window, cx| {
9884 let mut editor = build_editor(multibuffer.clone(), window, cx);
9885 let snapshot = editor.snapshot(window, cx);
9886 editor.change_selections(None, window, cx, |s| {
9887 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
9888 });
9889 editor.begin_selection(
9890 Point::new(2, 1).to_display_point(&snapshot),
9891 true,
9892 1,
9893 window,
9894 cx,
9895 );
9896 assert_eq!(
9897 editor.selections.ranges(cx),
9898 [
9899 Point::new(1, 3)..Point::new(1, 3),
9900 Point::new(2, 1)..Point::new(2, 1),
9901 ]
9902 );
9903 editor
9904 });
9905
9906 // Refreshing selections is a no-op when excerpts haven't changed.
9907 _ = editor.update(cx, |editor, window, cx| {
9908 editor.change_selections(None, window, cx, |s| s.refresh());
9909 assert_eq!(
9910 editor.selections.ranges(cx),
9911 [
9912 Point::new(1, 3)..Point::new(1, 3),
9913 Point::new(2, 1)..Point::new(2, 1),
9914 ]
9915 );
9916 });
9917
9918 multibuffer.update(cx, |multibuffer, cx| {
9919 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
9920 });
9921 _ = editor.update(cx, |editor, window, cx| {
9922 // Removing an excerpt causes the first selection to become degenerate.
9923 assert_eq!(
9924 editor.selections.ranges(cx),
9925 [
9926 Point::new(0, 0)..Point::new(0, 0),
9927 Point::new(0, 1)..Point::new(0, 1)
9928 ]
9929 );
9930
9931 // Refreshing selections will relocate the first selection to the original buffer
9932 // location.
9933 editor.change_selections(None, window, cx, |s| s.refresh());
9934 assert_eq!(
9935 editor.selections.ranges(cx),
9936 [
9937 Point::new(0, 1)..Point::new(0, 1),
9938 Point::new(0, 3)..Point::new(0, 3)
9939 ]
9940 );
9941 assert!(editor.selections.pending_anchor().is_some());
9942 });
9943}
9944
9945#[gpui::test]
9946fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
9947 init_test(cx, |_| {});
9948
9949 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9950 let mut excerpt1_id = None;
9951 let multibuffer = cx.new(|cx| {
9952 let mut multibuffer = MultiBuffer::new(ReadWrite);
9953 excerpt1_id = multibuffer
9954 .push_excerpts(
9955 buffer.clone(),
9956 [
9957 ExcerptRange {
9958 context: Point::new(0, 0)..Point::new(1, 4),
9959 primary: None,
9960 },
9961 ExcerptRange {
9962 context: Point::new(1, 0)..Point::new(2, 4),
9963 primary: None,
9964 },
9965 ],
9966 cx,
9967 )
9968 .into_iter()
9969 .next();
9970 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
9971 multibuffer
9972 });
9973
9974 let editor = cx.add_window(|window, cx| {
9975 let mut editor = build_editor(multibuffer.clone(), window, cx);
9976 let snapshot = editor.snapshot(window, cx);
9977 editor.begin_selection(
9978 Point::new(1, 3).to_display_point(&snapshot),
9979 false,
9980 1,
9981 window,
9982 cx,
9983 );
9984 assert_eq!(
9985 editor.selections.ranges(cx),
9986 [Point::new(1, 3)..Point::new(1, 3)]
9987 );
9988 editor
9989 });
9990
9991 multibuffer.update(cx, |multibuffer, cx| {
9992 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
9993 });
9994 _ = editor.update(cx, |editor, window, cx| {
9995 assert_eq!(
9996 editor.selections.ranges(cx),
9997 [Point::new(0, 0)..Point::new(0, 0)]
9998 );
9999
10000 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
10001 editor.change_selections(None, window, cx, |s| s.refresh());
10002 assert_eq!(
10003 editor.selections.ranges(cx),
10004 [Point::new(0, 3)..Point::new(0, 3)]
10005 );
10006 assert!(editor.selections.pending_anchor().is_some());
10007 });
10008}
10009
10010#[gpui::test]
10011async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
10012 init_test(cx, |_| {});
10013
10014 let language = Arc::new(
10015 Language::new(
10016 LanguageConfig {
10017 brackets: BracketPairConfig {
10018 pairs: vec![
10019 BracketPair {
10020 start: "{".to_string(),
10021 end: "}".to_string(),
10022 close: true,
10023 surround: true,
10024 newline: true,
10025 },
10026 BracketPair {
10027 start: "/* ".to_string(),
10028 end: " */".to_string(),
10029 close: true,
10030 surround: true,
10031 newline: true,
10032 },
10033 ],
10034 ..Default::default()
10035 },
10036 ..Default::default()
10037 },
10038 Some(tree_sitter_rust::LANGUAGE.into()),
10039 )
10040 .with_indents_query("")
10041 .unwrap(),
10042 );
10043
10044 let text = concat!(
10045 "{ }\n", //
10046 " x\n", //
10047 " /* */\n", //
10048 "x\n", //
10049 "{{} }\n", //
10050 );
10051
10052 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10053 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10054 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10055 editor
10056 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10057 .await;
10058
10059 editor.update_in(cx, |editor, window, cx| {
10060 editor.change_selections(None, window, cx, |s| {
10061 s.select_display_ranges([
10062 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
10063 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
10064 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
10065 ])
10066 });
10067 editor.newline(&Newline, window, cx);
10068
10069 assert_eq!(
10070 editor.buffer().read(cx).read(cx).text(),
10071 concat!(
10072 "{ \n", // Suppress rustfmt
10073 "\n", //
10074 "}\n", //
10075 " x\n", //
10076 " /* \n", //
10077 " \n", //
10078 " */\n", //
10079 "x\n", //
10080 "{{} \n", //
10081 "}\n", //
10082 )
10083 );
10084 });
10085}
10086
10087#[gpui::test]
10088fn test_highlighted_ranges(cx: &mut TestAppContext) {
10089 init_test(cx, |_| {});
10090
10091 let editor = cx.add_window(|window, cx| {
10092 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
10093 build_editor(buffer.clone(), window, cx)
10094 });
10095
10096 _ = editor.update(cx, |editor, window, cx| {
10097 struct Type1;
10098 struct Type2;
10099
10100 let buffer = editor.buffer.read(cx).snapshot(cx);
10101
10102 let anchor_range =
10103 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
10104
10105 editor.highlight_background::<Type1>(
10106 &[
10107 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
10108 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
10109 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
10110 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
10111 ],
10112 |_| Hsla::red(),
10113 cx,
10114 );
10115 editor.highlight_background::<Type2>(
10116 &[
10117 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
10118 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
10119 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
10120 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
10121 ],
10122 |_| Hsla::green(),
10123 cx,
10124 );
10125
10126 let snapshot = editor.snapshot(window, cx);
10127 let mut highlighted_ranges = editor.background_highlights_in_range(
10128 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
10129 &snapshot,
10130 cx.theme().colors(),
10131 );
10132 // Enforce a consistent ordering based on color without relying on the ordering of the
10133 // highlight's `TypeId` which is non-executor.
10134 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
10135 assert_eq!(
10136 highlighted_ranges,
10137 &[
10138 (
10139 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
10140 Hsla::red(),
10141 ),
10142 (
10143 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
10144 Hsla::red(),
10145 ),
10146 (
10147 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
10148 Hsla::green(),
10149 ),
10150 (
10151 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
10152 Hsla::green(),
10153 ),
10154 ]
10155 );
10156 assert_eq!(
10157 editor.background_highlights_in_range(
10158 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
10159 &snapshot,
10160 cx.theme().colors(),
10161 ),
10162 &[(
10163 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
10164 Hsla::red(),
10165 )]
10166 );
10167 });
10168}
10169
10170#[gpui::test]
10171async fn test_following(cx: &mut gpui::TestAppContext) {
10172 init_test(cx, |_| {});
10173
10174 let fs = FakeFs::new(cx.executor());
10175 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
10176
10177 let buffer = project.update(cx, |project, cx| {
10178 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
10179 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
10180 });
10181 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
10182 let follower = cx.update(|cx| {
10183 cx.open_window(
10184 WindowOptions {
10185 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
10186 gpui::Point::new(px(0.), px(0.)),
10187 gpui::Point::new(px(10.), px(80.)),
10188 ))),
10189 ..Default::default()
10190 },
10191 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
10192 )
10193 .unwrap()
10194 });
10195
10196 let is_still_following = Rc::new(RefCell::new(true));
10197 let follower_edit_event_count = Rc::new(RefCell::new(0));
10198 let pending_update = Rc::new(RefCell::new(None));
10199 let leader_entity = leader.root(cx).unwrap();
10200 let follower_entity = follower.root(cx).unwrap();
10201 _ = follower.update(cx, {
10202 let update = pending_update.clone();
10203 let is_still_following = is_still_following.clone();
10204 let follower_edit_event_count = follower_edit_event_count.clone();
10205 |_, window, cx| {
10206 cx.subscribe_in(
10207 &leader_entity,
10208 window,
10209 move |_, leader, event, window, cx| {
10210 leader.read(cx).add_event_to_update_proto(
10211 event,
10212 &mut update.borrow_mut(),
10213 window,
10214 cx,
10215 );
10216 },
10217 )
10218 .detach();
10219
10220 cx.subscribe_in(
10221 &follower_entity,
10222 window,
10223 move |_, _, event: &EditorEvent, _window, _cx| {
10224 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
10225 *is_still_following.borrow_mut() = false;
10226 }
10227
10228 if let EditorEvent::BufferEdited = event {
10229 *follower_edit_event_count.borrow_mut() += 1;
10230 }
10231 },
10232 )
10233 .detach();
10234 }
10235 });
10236
10237 // Update the selections only
10238 _ = leader.update(cx, |leader, window, cx| {
10239 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
10240 });
10241 follower
10242 .update(cx, |follower, window, cx| {
10243 follower.apply_update_proto(
10244 &project,
10245 pending_update.borrow_mut().take().unwrap(),
10246 window,
10247 cx,
10248 )
10249 })
10250 .unwrap()
10251 .await
10252 .unwrap();
10253 _ = follower.update(cx, |follower, _, cx| {
10254 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
10255 });
10256 assert!(*is_still_following.borrow());
10257 assert_eq!(*follower_edit_event_count.borrow(), 0);
10258
10259 // Update the scroll position only
10260 _ = leader.update(cx, |leader, window, cx| {
10261 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
10262 });
10263 follower
10264 .update(cx, |follower, window, cx| {
10265 follower.apply_update_proto(
10266 &project,
10267 pending_update.borrow_mut().take().unwrap(),
10268 window,
10269 cx,
10270 )
10271 })
10272 .unwrap()
10273 .await
10274 .unwrap();
10275 assert_eq!(
10276 follower
10277 .update(cx, |follower, _, cx| follower.scroll_position(cx))
10278 .unwrap(),
10279 gpui::Point::new(1.5, 3.5)
10280 );
10281 assert!(*is_still_following.borrow());
10282 assert_eq!(*follower_edit_event_count.borrow(), 0);
10283
10284 // Update the selections and scroll position. The follower's scroll position is updated
10285 // via autoscroll, not via the leader's exact scroll position.
10286 _ = leader.update(cx, |leader, window, cx| {
10287 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10288 leader.request_autoscroll(Autoscroll::newest(), cx);
10289 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
10290 });
10291 follower
10292 .update(cx, |follower, window, cx| {
10293 follower.apply_update_proto(
10294 &project,
10295 pending_update.borrow_mut().take().unwrap(),
10296 window,
10297 cx,
10298 )
10299 })
10300 .unwrap()
10301 .await
10302 .unwrap();
10303 _ = follower.update(cx, |follower, _, cx| {
10304 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
10305 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
10306 });
10307 assert!(*is_still_following.borrow());
10308
10309 // Creating a pending selection that precedes another selection
10310 _ = leader.update(cx, |leader, window, cx| {
10311 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
10312 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, 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.selections.ranges(cx), vec![0..0, 1..1]);
10328 });
10329 assert!(*is_still_following.borrow());
10330
10331 // Extend the pending selection so that it surrounds another selection
10332 _ = leader.update(cx, |leader, window, cx| {
10333 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
10334 });
10335 follower
10336 .update(cx, |follower, window, cx| {
10337 follower.apply_update_proto(
10338 &project,
10339 pending_update.borrow_mut().take().unwrap(),
10340 window,
10341 cx,
10342 )
10343 })
10344 .unwrap()
10345 .await
10346 .unwrap();
10347 _ = follower.update(cx, |follower, _, cx| {
10348 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
10349 });
10350
10351 // Scrolling locally breaks the follow
10352 _ = follower.update(cx, |follower, window, cx| {
10353 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
10354 follower.set_scroll_anchor(
10355 ScrollAnchor {
10356 anchor: top_anchor,
10357 offset: gpui::Point::new(0.0, 0.5),
10358 },
10359 window,
10360 cx,
10361 );
10362 });
10363 assert!(!(*is_still_following.borrow()));
10364}
10365
10366#[gpui::test]
10367async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
10368 init_test(cx, |_| {});
10369
10370 let fs = FakeFs::new(cx.executor());
10371 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
10372 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10373 let pane = workspace
10374 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10375 .unwrap();
10376
10377 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10378
10379 let leader = pane.update_in(cx, |_, window, cx| {
10380 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
10381 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
10382 });
10383
10384 // Start following the editor when it has no excerpts.
10385 let mut state_message =
10386 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
10387 let workspace_entity = workspace.root(cx).unwrap();
10388 let follower_1 = cx
10389 .update_window(*workspace.deref(), |_, window, cx| {
10390 Editor::from_state_proto(
10391 workspace_entity,
10392 ViewId {
10393 creator: Default::default(),
10394 id: 0,
10395 },
10396 &mut state_message,
10397 window,
10398 cx,
10399 )
10400 })
10401 .unwrap()
10402 .unwrap()
10403 .await
10404 .unwrap();
10405
10406 let update_message = Rc::new(RefCell::new(None));
10407 follower_1.update_in(cx, {
10408 let update = update_message.clone();
10409 |_, window, cx| {
10410 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
10411 leader.read(cx).add_event_to_update_proto(
10412 event,
10413 &mut update.borrow_mut(),
10414 window,
10415 cx,
10416 );
10417 })
10418 .detach();
10419 }
10420 });
10421
10422 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
10423 (
10424 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
10425 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
10426 )
10427 });
10428
10429 // Insert some excerpts.
10430 leader.update(cx, |leader, cx| {
10431 leader.buffer.update(cx, |multibuffer, cx| {
10432 let excerpt_ids = multibuffer.push_excerpts(
10433 buffer_1.clone(),
10434 [
10435 ExcerptRange {
10436 context: 1..6,
10437 primary: None,
10438 },
10439 ExcerptRange {
10440 context: 12..15,
10441 primary: None,
10442 },
10443 ExcerptRange {
10444 context: 0..3,
10445 primary: None,
10446 },
10447 ],
10448 cx,
10449 );
10450 multibuffer.insert_excerpts_after(
10451 excerpt_ids[0],
10452 buffer_2.clone(),
10453 [
10454 ExcerptRange {
10455 context: 8..12,
10456 primary: None,
10457 },
10458 ExcerptRange {
10459 context: 0..6,
10460 primary: None,
10461 },
10462 ],
10463 cx,
10464 );
10465 });
10466 });
10467
10468 // Apply the update of adding the excerpts.
10469 follower_1
10470 .update_in(cx, |follower, window, cx| {
10471 follower.apply_update_proto(
10472 &project,
10473 update_message.borrow().clone().unwrap(),
10474 window,
10475 cx,
10476 )
10477 })
10478 .await
10479 .unwrap();
10480 assert_eq!(
10481 follower_1.update(cx, |editor, cx| editor.text(cx)),
10482 leader.update(cx, |editor, cx| editor.text(cx))
10483 );
10484 update_message.borrow_mut().take();
10485
10486 // Start following separately after it already has excerpts.
10487 let mut state_message =
10488 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
10489 let workspace_entity = workspace.root(cx).unwrap();
10490 let follower_2 = cx
10491 .update_window(*workspace.deref(), |_, window, cx| {
10492 Editor::from_state_proto(
10493 workspace_entity,
10494 ViewId {
10495 creator: Default::default(),
10496 id: 0,
10497 },
10498 &mut state_message,
10499 window,
10500 cx,
10501 )
10502 })
10503 .unwrap()
10504 .unwrap()
10505 .await
10506 .unwrap();
10507 assert_eq!(
10508 follower_2.update(cx, |editor, cx| editor.text(cx)),
10509 leader.update(cx, |editor, cx| editor.text(cx))
10510 );
10511
10512 // Remove some excerpts.
10513 leader.update(cx, |leader, cx| {
10514 leader.buffer.update(cx, |multibuffer, cx| {
10515 let excerpt_ids = multibuffer.excerpt_ids();
10516 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
10517 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
10518 });
10519 });
10520
10521 // Apply the update of removing the excerpts.
10522 follower_1
10523 .update_in(cx, |follower, window, cx| {
10524 follower.apply_update_proto(
10525 &project,
10526 update_message.borrow().clone().unwrap(),
10527 window,
10528 cx,
10529 )
10530 })
10531 .await
10532 .unwrap();
10533 follower_2
10534 .update_in(cx, |follower, window, cx| {
10535 follower.apply_update_proto(
10536 &project,
10537 update_message.borrow().clone().unwrap(),
10538 window,
10539 cx,
10540 )
10541 })
10542 .await
10543 .unwrap();
10544 update_message.borrow_mut().take();
10545 assert_eq!(
10546 follower_1.update(cx, |editor, cx| editor.text(cx)),
10547 leader.update(cx, |editor, cx| editor.text(cx))
10548 );
10549}
10550
10551#[gpui::test]
10552async fn go_to_prev_overlapping_diagnostic(
10553 executor: BackgroundExecutor,
10554 cx: &mut gpui::TestAppContext,
10555) {
10556 init_test(cx, |_| {});
10557
10558 let mut cx = EditorTestContext::new(cx).await;
10559 let lsp_store =
10560 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10561
10562 cx.set_state(indoc! {"
10563 ˇfn func(abc def: i32) -> u32 {
10564 }
10565 "});
10566
10567 cx.update(|_, cx| {
10568 lsp_store.update(cx, |lsp_store, cx| {
10569 lsp_store
10570 .update_diagnostics(
10571 LanguageServerId(0),
10572 lsp::PublishDiagnosticsParams {
10573 uri: lsp::Url::from_file_path("/root/file").unwrap(),
10574 version: None,
10575 diagnostics: vec![
10576 lsp::Diagnostic {
10577 range: lsp::Range::new(
10578 lsp::Position::new(0, 11),
10579 lsp::Position::new(0, 12),
10580 ),
10581 severity: Some(lsp::DiagnosticSeverity::ERROR),
10582 ..Default::default()
10583 },
10584 lsp::Diagnostic {
10585 range: lsp::Range::new(
10586 lsp::Position::new(0, 12),
10587 lsp::Position::new(0, 15),
10588 ),
10589 severity: Some(lsp::DiagnosticSeverity::ERROR),
10590 ..Default::default()
10591 },
10592 lsp::Diagnostic {
10593 range: lsp::Range::new(
10594 lsp::Position::new(0, 25),
10595 lsp::Position::new(0, 28),
10596 ),
10597 severity: Some(lsp::DiagnosticSeverity::ERROR),
10598 ..Default::default()
10599 },
10600 ],
10601 },
10602 &[],
10603 cx,
10604 )
10605 .unwrap()
10606 });
10607 });
10608
10609 executor.run_until_parked();
10610
10611 cx.update_editor(|editor, window, cx| {
10612 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10613 });
10614
10615 cx.assert_editor_state(indoc! {"
10616 fn func(abc def: i32) -> ˇu32 {
10617 }
10618 "});
10619
10620 cx.update_editor(|editor, window, cx| {
10621 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10622 });
10623
10624 cx.assert_editor_state(indoc! {"
10625 fn func(abc ˇdef: i32) -> u32 {
10626 }
10627 "});
10628
10629 cx.update_editor(|editor, window, cx| {
10630 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10631 });
10632
10633 cx.assert_editor_state(indoc! {"
10634 fn func(abcˇ def: i32) -> u32 {
10635 }
10636 "});
10637
10638 cx.update_editor(|editor, window, cx| {
10639 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10640 });
10641
10642 cx.assert_editor_state(indoc! {"
10643 fn func(abc def: i32) -> ˇu32 {
10644 }
10645 "});
10646}
10647
10648#[gpui::test]
10649async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
10650 init_test(cx, |_| {});
10651
10652 let mut cx = EditorTestContext::new(cx).await;
10653
10654 cx.set_state(indoc! {"
10655 fn func(abˇc def: i32) -> u32 {
10656 }
10657 "});
10658 let lsp_store =
10659 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10660
10661 cx.update(|_, cx| {
10662 lsp_store.update(cx, |lsp_store, cx| {
10663 lsp_store.update_diagnostics(
10664 LanguageServerId(0),
10665 lsp::PublishDiagnosticsParams {
10666 uri: lsp::Url::from_file_path("/root/file").unwrap(),
10667 version: None,
10668 diagnostics: vec![lsp::Diagnostic {
10669 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
10670 severity: Some(lsp::DiagnosticSeverity::ERROR),
10671 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
10672 ..Default::default()
10673 }],
10674 },
10675 &[],
10676 cx,
10677 )
10678 })
10679 }).unwrap();
10680 cx.run_until_parked();
10681 cx.update_editor(|editor, window, cx| {
10682 hover_popover::hover(editor, &Default::default(), window, cx)
10683 });
10684 cx.run_until_parked();
10685 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
10686}
10687
10688#[gpui::test]
10689async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
10690 init_test(cx, |_| {});
10691
10692 let mut cx = EditorTestContext::new(cx).await;
10693
10694 let diff_base = r#"
10695 use some::mod;
10696
10697 const A: u32 = 42;
10698
10699 fn main() {
10700 println!("hello");
10701
10702 println!("world");
10703 }
10704 "#
10705 .unindent();
10706
10707 // Edits are modified, removed, modified, added
10708 cx.set_state(
10709 &r#"
10710 use some::modified;
10711
10712 ˇ
10713 fn main() {
10714 println!("hello there");
10715
10716 println!("around the");
10717 println!("world");
10718 }
10719 "#
10720 .unindent(),
10721 );
10722
10723 cx.set_diff_base(&diff_base);
10724 executor.run_until_parked();
10725
10726 cx.update_editor(|editor, window, cx| {
10727 //Wrap around the bottom of the buffer
10728 for _ in 0..3 {
10729 editor.go_to_next_hunk(&GoToHunk, window, cx);
10730 }
10731 });
10732
10733 cx.assert_editor_state(
10734 &r#"
10735 ˇuse some::modified;
10736
10737
10738 fn main() {
10739 println!("hello there");
10740
10741 println!("around the");
10742 println!("world");
10743 }
10744 "#
10745 .unindent(),
10746 );
10747
10748 cx.update_editor(|editor, window, cx| {
10749 //Wrap around the top of the buffer
10750 for _ in 0..2 {
10751 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10752 }
10753 });
10754
10755 cx.assert_editor_state(
10756 &r#"
10757 use some::modified;
10758
10759
10760 fn main() {
10761 ˇ println!("hello there");
10762
10763 println!("around the");
10764 println!("world");
10765 }
10766 "#
10767 .unindent(),
10768 );
10769
10770 cx.update_editor(|editor, window, cx| {
10771 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10772 });
10773
10774 cx.assert_editor_state(
10775 &r#"
10776 use some::modified;
10777
10778 ˇ
10779 fn main() {
10780 println!("hello there");
10781
10782 println!("around the");
10783 println!("world");
10784 }
10785 "#
10786 .unindent(),
10787 );
10788
10789 cx.update_editor(|editor, window, cx| {
10790 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10791 });
10792
10793 cx.assert_editor_state(
10794 &r#"
10795 ˇuse some::modified;
10796
10797
10798 fn main() {
10799 println!("hello there");
10800
10801 println!("around the");
10802 println!("world");
10803 }
10804 "#
10805 .unindent(),
10806 );
10807
10808 cx.update_editor(|editor, window, cx| {
10809 for _ in 0..2 {
10810 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10811 }
10812 });
10813
10814 cx.assert_editor_state(
10815 &r#"
10816 use some::modified;
10817
10818
10819 fn main() {
10820 ˇ println!("hello there");
10821
10822 println!("around the");
10823 println!("world");
10824 }
10825 "#
10826 .unindent(),
10827 );
10828
10829 cx.update_editor(|editor, window, cx| {
10830 editor.fold(&Fold, window, cx);
10831 });
10832
10833 cx.update_editor(|editor, window, cx| {
10834 editor.go_to_next_hunk(&GoToHunk, window, cx);
10835 });
10836
10837 cx.assert_editor_state(
10838 &r#"
10839 ˇuse some::modified;
10840
10841
10842 fn main() {
10843 println!("hello there");
10844
10845 println!("around the");
10846 println!("world");
10847 }
10848 "#
10849 .unindent(),
10850 );
10851}
10852
10853#[test]
10854fn test_split_words() {
10855 fn split(text: &str) -> Vec<&str> {
10856 split_words(text).collect()
10857 }
10858
10859 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
10860 assert_eq!(split("hello_world"), &["hello_", "world"]);
10861 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
10862 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
10863 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
10864 assert_eq!(split("helloworld"), &["helloworld"]);
10865
10866 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
10867}
10868
10869#[gpui::test]
10870async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
10871 init_test(cx, |_| {});
10872
10873 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
10874 let mut assert = |before, after| {
10875 let _state_context = cx.set_state(before);
10876 cx.run_until_parked();
10877 cx.update_editor(|editor, window, cx| {
10878 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
10879 });
10880 cx.assert_editor_state(after);
10881 };
10882
10883 // Outside bracket jumps to outside of matching bracket
10884 assert("console.logˇ(var);", "console.log(var)ˇ;");
10885 assert("console.log(var)ˇ;", "console.logˇ(var);");
10886
10887 // Inside bracket jumps to inside of matching bracket
10888 assert("console.log(ˇvar);", "console.log(varˇ);");
10889 assert("console.log(varˇ);", "console.log(ˇvar);");
10890
10891 // When outside a bracket and inside, favor jumping to the inside bracket
10892 assert(
10893 "console.log('foo', [1, 2, 3]ˇ);",
10894 "console.log(ˇ'foo', [1, 2, 3]);",
10895 );
10896 assert(
10897 "console.log(ˇ'foo', [1, 2, 3]);",
10898 "console.log('foo', [1, 2, 3]ˇ);",
10899 );
10900
10901 // Bias forward if two options are equally likely
10902 assert(
10903 "let result = curried_fun()ˇ();",
10904 "let result = curried_fun()()ˇ;",
10905 );
10906
10907 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
10908 assert(
10909 indoc! {"
10910 function test() {
10911 console.log('test')ˇ
10912 }"},
10913 indoc! {"
10914 function test() {
10915 console.logˇ('test')
10916 }"},
10917 );
10918}
10919
10920#[gpui::test]
10921async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
10922 init_test(cx, |_| {});
10923
10924 let fs = FakeFs::new(cx.executor());
10925 fs.insert_tree(
10926 "/a",
10927 json!({
10928 "main.rs": "fn main() { let a = 5; }",
10929 "other.rs": "// Test file",
10930 }),
10931 )
10932 .await;
10933 let project = Project::test(fs, ["/a".as_ref()], cx).await;
10934
10935 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10936 language_registry.add(Arc::new(Language::new(
10937 LanguageConfig {
10938 name: "Rust".into(),
10939 matcher: LanguageMatcher {
10940 path_suffixes: vec!["rs".to_string()],
10941 ..Default::default()
10942 },
10943 brackets: BracketPairConfig {
10944 pairs: vec![BracketPair {
10945 start: "{".to_string(),
10946 end: "}".to_string(),
10947 close: true,
10948 surround: true,
10949 newline: true,
10950 }],
10951 disabled_scopes_by_bracket_ix: Vec::new(),
10952 },
10953 ..Default::default()
10954 },
10955 Some(tree_sitter_rust::LANGUAGE.into()),
10956 )));
10957 let mut fake_servers = language_registry.register_fake_lsp(
10958 "Rust",
10959 FakeLspAdapter {
10960 capabilities: lsp::ServerCapabilities {
10961 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
10962 first_trigger_character: "{".to_string(),
10963 more_trigger_character: None,
10964 }),
10965 ..Default::default()
10966 },
10967 ..Default::default()
10968 },
10969 );
10970
10971 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10972
10973 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10974
10975 let worktree_id = workspace
10976 .update(cx, |workspace, _, cx| {
10977 workspace.project().update(cx, |project, cx| {
10978 project.worktrees(cx).next().unwrap().read(cx).id()
10979 })
10980 })
10981 .unwrap();
10982
10983 let buffer = project
10984 .update(cx, |project, cx| {
10985 project.open_local_buffer("/a/main.rs", cx)
10986 })
10987 .await
10988 .unwrap();
10989 let editor_handle = workspace
10990 .update(cx, |workspace, window, cx| {
10991 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
10992 })
10993 .unwrap()
10994 .await
10995 .unwrap()
10996 .downcast::<Editor>()
10997 .unwrap();
10998
10999 cx.executor().start_waiting();
11000 let fake_server = fake_servers.next().await.unwrap();
11001
11002 fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
11003 assert_eq!(
11004 params.text_document_position.text_document.uri,
11005 lsp::Url::from_file_path("/a/main.rs").unwrap(),
11006 );
11007 assert_eq!(
11008 params.text_document_position.position,
11009 lsp::Position::new(0, 21),
11010 );
11011
11012 Ok(Some(vec![lsp::TextEdit {
11013 new_text: "]".to_string(),
11014 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11015 }]))
11016 });
11017
11018 editor_handle.update_in(cx, |editor, window, cx| {
11019 window.focus(&editor.focus_handle(cx));
11020 editor.change_selections(None, window, cx, |s| {
11021 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
11022 });
11023 editor.handle_input("{", window, cx);
11024 });
11025
11026 cx.executor().run_until_parked();
11027
11028 buffer.update(cx, |buffer, _| {
11029 assert_eq!(
11030 buffer.text(),
11031 "fn main() { let a = {5}; }",
11032 "No extra braces from on type formatting should appear in the buffer"
11033 )
11034 });
11035}
11036
11037#[gpui::test]
11038async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) {
11039 init_test(cx, |_| {});
11040
11041 let fs = FakeFs::new(cx.executor());
11042 fs.insert_tree(
11043 "/a",
11044 json!({
11045 "main.rs": "fn main() { let a = 5; }",
11046 "other.rs": "// Test file",
11047 }),
11048 )
11049 .await;
11050
11051 let project = Project::test(fs, ["/a".as_ref()], cx).await;
11052
11053 let server_restarts = Arc::new(AtomicUsize::new(0));
11054 let closure_restarts = Arc::clone(&server_restarts);
11055 let language_server_name = "test language server";
11056 let language_name: LanguageName = "Rust".into();
11057
11058 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11059 language_registry.add(Arc::new(Language::new(
11060 LanguageConfig {
11061 name: language_name.clone(),
11062 matcher: LanguageMatcher {
11063 path_suffixes: vec!["rs".to_string()],
11064 ..Default::default()
11065 },
11066 ..Default::default()
11067 },
11068 Some(tree_sitter_rust::LANGUAGE.into()),
11069 )));
11070 let mut fake_servers = language_registry.register_fake_lsp(
11071 "Rust",
11072 FakeLspAdapter {
11073 name: language_server_name,
11074 initialization_options: Some(json!({
11075 "testOptionValue": true
11076 })),
11077 initializer: Some(Box::new(move |fake_server| {
11078 let task_restarts = Arc::clone(&closure_restarts);
11079 fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
11080 task_restarts.fetch_add(1, atomic::Ordering::Release);
11081 futures::future::ready(Ok(()))
11082 });
11083 })),
11084 ..Default::default()
11085 },
11086 );
11087
11088 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11089 let _buffer = project
11090 .update(cx, |project, cx| {
11091 project.open_local_buffer_with_lsp("/a/main.rs", cx)
11092 })
11093 .await
11094 .unwrap();
11095 let _fake_server = fake_servers.next().await.unwrap();
11096 update_test_language_settings(cx, |language_settings| {
11097 language_settings.languages.insert(
11098 language_name.clone(),
11099 LanguageSettingsContent {
11100 tab_size: NonZeroU32::new(8),
11101 ..Default::default()
11102 },
11103 );
11104 });
11105 cx.executor().run_until_parked();
11106 assert_eq!(
11107 server_restarts.load(atomic::Ordering::Acquire),
11108 0,
11109 "Should not restart LSP server on an unrelated change"
11110 );
11111
11112 update_test_project_settings(cx, |project_settings| {
11113 project_settings.lsp.insert(
11114 "Some other server name".into(),
11115 LspSettings {
11116 binary: None,
11117 settings: None,
11118 initialization_options: Some(json!({
11119 "some other init value": false
11120 })),
11121 },
11122 );
11123 });
11124 cx.executor().run_until_parked();
11125 assert_eq!(
11126 server_restarts.load(atomic::Ordering::Acquire),
11127 0,
11128 "Should not restart LSP server on an unrelated LSP settings change"
11129 );
11130
11131 update_test_project_settings(cx, |project_settings| {
11132 project_settings.lsp.insert(
11133 language_server_name.into(),
11134 LspSettings {
11135 binary: None,
11136 settings: None,
11137 initialization_options: Some(json!({
11138 "anotherInitValue": false
11139 })),
11140 },
11141 );
11142 });
11143 cx.executor().run_until_parked();
11144 assert_eq!(
11145 server_restarts.load(atomic::Ordering::Acquire),
11146 1,
11147 "Should restart LSP server on a related LSP settings change"
11148 );
11149
11150 update_test_project_settings(cx, |project_settings| {
11151 project_settings.lsp.insert(
11152 language_server_name.into(),
11153 LspSettings {
11154 binary: None,
11155 settings: None,
11156 initialization_options: Some(json!({
11157 "anotherInitValue": false
11158 })),
11159 },
11160 );
11161 });
11162 cx.executor().run_until_parked();
11163 assert_eq!(
11164 server_restarts.load(atomic::Ordering::Acquire),
11165 1,
11166 "Should not restart LSP server on a related LSP settings change that is the same"
11167 );
11168
11169 update_test_project_settings(cx, |project_settings| {
11170 project_settings.lsp.insert(
11171 language_server_name.into(),
11172 LspSettings {
11173 binary: None,
11174 settings: None,
11175 initialization_options: None,
11176 },
11177 );
11178 });
11179 cx.executor().run_until_parked();
11180 assert_eq!(
11181 server_restarts.load(atomic::Ordering::Acquire),
11182 2,
11183 "Should restart LSP server on another related LSP settings change"
11184 );
11185}
11186
11187#[gpui::test]
11188async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
11189 init_test(cx, |_| {});
11190
11191 let mut cx = EditorLspTestContext::new_rust(
11192 lsp::ServerCapabilities {
11193 completion_provider: Some(lsp::CompletionOptions {
11194 trigger_characters: Some(vec![".".to_string()]),
11195 resolve_provider: Some(true),
11196 ..Default::default()
11197 }),
11198 ..Default::default()
11199 },
11200 cx,
11201 )
11202 .await;
11203
11204 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11205 cx.simulate_keystroke(".");
11206 let completion_item = lsp::CompletionItem {
11207 label: "some".into(),
11208 kind: Some(lsp::CompletionItemKind::SNIPPET),
11209 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11210 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11211 kind: lsp::MarkupKind::Markdown,
11212 value: "```rust\nSome(2)\n```".to_string(),
11213 })),
11214 deprecated: Some(false),
11215 sort_text: Some("fffffff2".to_string()),
11216 filter_text: Some("some".to_string()),
11217 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11218 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11219 range: lsp::Range {
11220 start: lsp::Position {
11221 line: 0,
11222 character: 22,
11223 },
11224 end: lsp::Position {
11225 line: 0,
11226 character: 22,
11227 },
11228 },
11229 new_text: "Some(2)".to_string(),
11230 })),
11231 additional_text_edits: Some(vec![lsp::TextEdit {
11232 range: lsp::Range {
11233 start: lsp::Position {
11234 line: 0,
11235 character: 20,
11236 },
11237 end: lsp::Position {
11238 line: 0,
11239 character: 22,
11240 },
11241 },
11242 new_text: "".to_string(),
11243 }]),
11244 ..Default::default()
11245 };
11246
11247 let closure_completion_item = completion_item.clone();
11248 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11249 let task_completion_item = closure_completion_item.clone();
11250 async move {
11251 Ok(Some(lsp::CompletionResponse::Array(vec![
11252 task_completion_item,
11253 ])))
11254 }
11255 });
11256
11257 request.next().await;
11258
11259 cx.condition(|editor, _| editor.context_menu_visible())
11260 .await;
11261 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11262 editor
11263 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11264 .unwrap()
11265 });
11266 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
11267
11268 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
11269 let task_completion_item = completion_item.clone();
11270 async move { Ok(task_completion_item) }
11271 })
11272 .next()
11273 .await
11274 .unwrap();
11275 apply_additional_edits.await.unwrap();
11276 cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
11277}
11278
11279#[gpui::test]
11280async fn test_completions_resolve_updates_labels_if_filter_text_matches(
11281 cx: &mut gpui::TestAppContext,
11282) {
11283 init_test(cx, |_| {});
11284
11285 let mut cx = EditorLspTestContext::new_rust(
11286 lsp::ServerCapabilities {
11287 completion_provider: Some(lsp::CompletionOptions {
11288 trigger_characters: Some(vec![".".to_string()]),
11289 resolve_provider: Some(true),
11290 ..Default::default()
11291 }),
11292 ..Default::default()
11293 },
11294 cx,
11295 )
11296 .await;
11297
11298 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11299 cx.simulate_keystroke(".");
11300
11301 let item1 = lsp::CompletionItem {
11302 label: "method id()".to_string(),
11303 filter_text: Some("id".to_string()),
11304 detail: None,
11305 documentation: None,
11306 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11307 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11308 new_text: ".id".to_string(),
11309 })),
11310 ..lsp::CompletionItem::default()
11311 };
11312
11313 let item2 = lsp::CompletionItem {
11314 label: "other".to_string(),
11315 filter_text: Some("other".to_string()),
11316 detail: None,
11317 documentation: None,
11318 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11319 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11320 new_text: ".other".to_string(),
11321 })),
11322 ..lsp::CompletionItem::default()
11323 };
11324
11325 let item1 = item1.clone();
11326 cx.handle_request::<lsp::request::Completion, _, _>({
11327 let item1 = item1.clone();
11328 move |_, _, _| {
11329 let item1 = item1.clone();
11330 let item2 = item2.clone();
11331 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
11332 }
11333 })
11334 .next()
11335 .await;
11336
11337 cx.condition(|editor, _| editor.context_menu_visible())
11338 .await;
11339 cx.update_editor(|editor, _, _| {
11340 let context_menu = editor.context_menu.borrow_mut();
11341 let context_menu = context_menu
11342 .as_ref()
11343 .expect("Should have the context menu deployed");
11344 match context_menu {
11345 CodeContextMenu::Completions(completions_menu) => {
11346 let completions = completions_menu.completions.borrow_mut();
11347 assert_eq!(
11348 completions
11349 .iter()
11350 .map(|completion| &completion.label.text)
11351 .collect::<Vec<_>>(),
11352 vec!["method id()", "other"]
11353 )
11354 }
11355 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11356 }
11357 });
11358
11359 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>({
11360 let item1 = item1.clone();
11361 move |_, item_to_resolve, _| {
11362 let item1 = item1.clone();
11363 async move {
11364 if item1 == item_to_resolve {
11365 Ok(lsp::CompletionItem {
11366 label: "method id()".to_string(),
11367 filter_text: Some("id".to_string()),
11368 detail: Some("Now resolved!".to_string()),
11369 documentation: Some(lsp::Documentation::String("Docs".to_string())),
11370 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11371 range: lsp::Range::new(
11372 lsp::Position::new(0, 22),
11373 lsp::Position::new(0, 22),
11374 ),
11375 new_text: ".id".to_string(),
11376 })),
11377 ..lsp::CompletionItem::default()
11378 })
11379 } else {
11380 Ok(item_to_resolve)
11381 }
11382 }
11383 }
11384 })
11385 .next()
11386 .await
11387 .unwrap();
11388 cx.run_until_parked();
11389
11390 cx.update_editor(|editor, window, cx| {
11391 editor.context_menu_next(&Default::default(), window, cx);
11392 });
11393
11394 cx.update_editor(|editor, _, _| {
11395 let context_menu = editor.context_menu.borrow_mut();
11396 let context_menu = context_menu
11397 .as_ref()
11398 .expect("Should have the context menu deployed");
11399 match context_menu {
11400 CodeContextMenu::Completions(completions_menu) => {
11401 let completions = completions_menu.completions.borrow_mut();
11402 assert_eq!(
11403 completions
11404 .iter()
11405 .map(|completion| &completion.label.text)
11406 .collect::<Vec<_>>(),
11407 vec!["method id() Now resolved!", "other"],
11408 "Should update first completion label, but not second as the filter text did not match."
11409 );
11410 }
11411 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11412 }
11413 });
11414}
11415
11416#[gpui::test]
11417async fn test_completions_resolve_happens_once(cx: &mut gpui::TestAppContext) {
11418 init_test(cx, |_| {});
11419
11420 let mut cx = EditorLspTestContext::new_rust(
11421 lsp::ServerCapabilities {
11422 completion_provider: Some(lsp::CompletionOptions {
11423 trigger_characters: Some(vec![".".to_string()]),
11424 resolve_provider: Some(true),
11425 ..Default::default()
11426 }),
11427 ..Default::default()
11428 },
11429 cx,
11430 )
11431 .await;
11432
11433 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11434 cx.simulate_keystroke(".");
11435
11436 let unresolved_item_1 = lsp::CompletionItem {
11437 label: "id".to_string(),
11438 filter_text: Some("id".to_string()),
11439 detail: None,
11440 documentation: None,
11441 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11442 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11443 new_text: ".id".to_string(),
11444 })),
11445 ..lsp::CompletionItem::default()
11446 };
11447 let resolved_item_1 = lsp::CompletionItem {
11448 additional_text_edits: Some(vec![lsp::TextEdit {
11449 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
11450 new_text: "!!".to_string(),
11451 }]),
11452 ..unresolved_item_1.clone()
11453 };
11454 let unresolved_item_2 = lsp::CompletionItem {
11455 label: "other".to_string(),
11456 filter_text: Some("other".to_string()),
11457 detail: None,
11458 documentation: None,
11459 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11460 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11461 new_text: ".other".to_string(),
11462 })),
11463 ..lsp::CompletionItem::default()
11464 };
11465 let resolved_item_2 = lsp::CompletionItem {
11466 additional_text_edits: Some(vec![lsp::TextEdit {
11467 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
11468 new_text: "??".to_string(),
11469 }]),
11470 ..unresolved_item_2.clone()
11471 };
11472
11473 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
11474 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
11475 cx.lsp
11476 .server
11477 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
11478 let unresolved_item_1 = unresolved_item_1.clone();
11479 let resolved_item_1 = resolved_item_1.clone();
11480 let unresolved_item_2 = unresolved_item_2.clone();
11481 let resolved_item_2 = resolved_item_2.clone();
11482 let resolve_requests_1 = resolve_requests_1.clone();
11483 let resolve_requests_2 = resolve_requests_2.clone();
11484 move |unresolved_request, _| {
11485 let unresolved_item_1 = unresolved_item_1.clone();
11486 let resolved_item_1 = resolved_item_1.clone();
11487 let unresolved_item_2 = unresolved_item_2.clone();
11488 let resolved_item_2 = resolved_item_2.clone();
11489 let resolve_requests_1 = resolve_requests_1.clone();
11490 let resolve_requests_2 = resolve_requests_2.clone();
11491 async move {
11492 if unresolved_request == unresolved_item_1 {
11493 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
11494 Ok(resolved_item_1.clone())
11495 } else if unresolved_request == unresolved_item_2 {
11496 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
11497 Ok(resolved_item_2.clone())
11498 } else {
11499 panic!("Unexpected completion item {unresolved_request:?}")
11500 }
11501 }
11502 }
11503 })
11504 .detach();
11505
11506 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11507 let unresolved_item_1 = unresolved_item_1.clone();
11508 let unresolved_item_2 = unresolved_item_2.clone();
11509 async move {
11510 Ok(Some(lsp::CompletionResponse::Array(vec![
11511 unresolved_item_1,
11512 unresolved_item_2,
11513 ])))
11514 }
11515 })
11516 .next()
11517 .await;
11518
11519 cx.condition(|editor, _| editor.context_menu_visible())
11520 .await;
11521 cx.update_editor(|editor, _, _| {
11522 let context_menu = editor.context_menu.borrow_mut();
11523 let context_menu = context_menu
11524 .as_ref()
11525 .expect("Should have the context menu deployed");
11526 match context_menu {
11527 CodeContextMenu::Completions(completions_menu) => {
11528 let completions = completions_menu.completions.borrow_mut();
11529 assert_eq!(
11530 completions
11531 .iter()
11532 .map(|completion| &completion.label.text)
11533 .collect::<Vec<_>>(),
11534 vec!["id", "other"]
11535 )
11536 }
11537 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11538 }
11539 });
11540 cx.run_until_parked();
11541
11542 cx.update_editor(|editor, window, cx| {
11543 editor.context_menu_next(&ContextMenuNext, window, cx);
11544 });
11545 cx.run_until_parked();
11546 cx.update_editor(|editor, window, cx| {
11547 editor.context_menu_prev(&ContextMenuPrev, window, cx);
11548 });
11549 cx.run_until_parked();
11550 cx.update_editor(|editor, window, cx| {
11551 editor.context_menu_next(&ContextMenuNext, window, cx);
11552 });
11553 cx.run_until_parked();
11554 cx.update_editor(|editor, window, cx| {
11555 editor
11556 .compose_completion(&ComposeCompletion::default(), window, cx)
11557 .expect("No task returned")
11558 })
11559 .await
11560 .expect("Completion failed");
11561 cx.run_until_parked();
11562
11563 cx.update_editor(|editor, _, cx| {
11564 assert_eq!(
11565 resolve_requests_1.load(atomic::Ordering::Acquire),
11566 1,
11567 "Should always resolve once despite multiple selections"
11568 );
11569 assert_eq!(
11570 resolve_requests_2.load(atomic::Ordering::Acquire),
11571 1,
11572 "Should always resolve once after multiple selections and applying the completion"
11573 );
11574 assert_eq!(
11575 editor.text(cx),
11576 "fn main() { let a = ??.other; }",
11577 "Should use resolved data when applying the completion"
11578 );
11579 });
11580}
11581
11582#[gpui::test]
11583async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
11584 init_test(cx, |_| {});
11585
11586 let item_0 = lsp::CompletionItem {
11587 label: "abs".into(),
11588 insert_text: Some("abs".into()),
11589 data: Some(json!({ "very": "special"})),
11590 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
11591 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11592 lsp::InsertReplaceEdit {
11593 new_text: "abs".to_string(),
11594 insert: lsp::Range::default(),
11595 replace: lsp::Range::default(),
11596 },
11597 )),
11598 ..lsp::CompletionItem::default()
11599 };
11600 let items = iter::once(item_0.clone())
11601 .chain((11..51).map(|i| lsp::CompletionItem {
11602 label: format!("item_{}", i),
11603 insert_text: Some(format!("item_{}", i)),
11604 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
11605 ..lsp::CompletionItem::default()
11606 }))
11607 .collect::<Vec<_>>();
11608
11609 let default_commit_characters = vec!["?".to_string()];
11610 let default_data = json!({ "default": "data"});
11611 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
11612 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
11613 let default_edit_range = lsp::Range {
11614 start: lsp::Position {
11615 line: 0,
11616 character: 5,
11617 },
11618 end: lsp::Position {
11619 line: 0,
11620 character: 5,
11621 },
11622 };
11623
11624 let item_0_out = lsp::CompletionItem {
11625 commit_characters: Some(default_commit_characters.clone()),
11626 insert_text_format: Some(default_insert_text_format),
11627 ..item_0
11628 };
11629 let items_out = iter::once(item_0_out)
11630 .chain(items[1..].iter().map(|item| lsp::CompletionItem {
11631 commit_characters: Some(default_commit_characters.clone()),
11632 data: Some(default_data.clone()),
11633 insert_text_mode: Some(default_insert_text_mode),
11634 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11635 range: default_edit_range,
11636 new_text: item.label.clone(),
11637 })),
11638 ..item.clone()
11639 }))
11640 .collect::<Vec<lsp::CompletionItem>>();
11641
11642 let mut cx = EditorLspTestContext::new_rust(
11643 lsp::ServerCapabilities {
11644 completion_provider: Some(lsp::CompletionOptions {
11645 trigger_characters: Some(vec![".".to_string()]),
11646 resolve_provider: Some(true),
11647 ..Default::default()
11648 }),
11649 ..Default::default()
11650 },
11651 cx,
11652 )
11653 .await;
11654
11655 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11656 cx.simulate_keystroke(".");
11657
11658 let completion_data = default_data.clone();
11659 let completion_characters = default_commit_characters.clone();
11660 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11661 let default_data = completion_data.clone();
11662 let default_commit_characters = completion_characters.clone();
11663 let items = items.clone();
11664 async move {
11665 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
11666 items,
11667 item_defaults: Some(lsp::CompletionListItemDefaults {
11668 data: Some(default_data.clone()),
11669 commit_characters: Some(default_commit_characters.clone()),
11670 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
11671 default_edit_range,
11672 )),
11673 insert_text_format: Some(default_insert_text_format),
11674 insert_text_mode: Some(default_insert_text_mode),
11675 }),
11676 ..lsp::CompletionList::default()
11677 })))
11678 }
11679 })
11680 .next()
11681 .await;
11682
11683 let resolved_items = Arc::new(Mutex::new(Vec::new()));
11684 cx.lsp
11685 .server
11686 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
11687 let closure_resolved_items = resolved_items.clone();
11688 move |item_to_resolve, _| {
11689 let closure_resolved_items = closure_resolved_items.clone();
11690 async move {
11691 closure_resolved_items.lock().push(item_to_resolve.clone());
11692 Ok(item_to_resolve)
11693 }
11694 }
11695 })
11696 .detach();
11697
11698 cx.condition(|editor, _| editor.context_menu_visible())
11699 .await;
11700 cx.run_until_parked();
11701 cx.update_editor(|editor, _, _| {
11702 let menu = editor.context_menu.borrow_mut();
11703 match menu.as_ref().expect("should have the completions menu") {
11704 CodeContextMenu::Completions(completions_menu) => {
11705 assert_eq!(
11706 completions_menu
11707 .entries
11708 .borrow()
11709 .iter()
11710 .map(|mat| mat.string.clone())
11711 .collect::<Vec<String>>(),
11712 items_out
11713 .iter()
11714 .map(|completion| completion.label.clone())
11715 .collect::<Vec<String>>()
11716 );
11717 }
11718 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
11719 }
11720 });
11721 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
11722 // with 4 from the end.
11723 assert_eq!(
11724 *resolved_items.lock(),
11725 [
11726 &items_out[0..16],
11727 &items_out[items_out.len() - 4..items_out.len()]
11728 ]
11729 .concat()
11730 .iter()
11731 .cloned()
11732 .collect::<Vec<lsp::CompletionItem>>()
11733 );
11734 resolved_items.lock().clear();
11735
11736 cx.update_editor(|editor, window, cx| {
11737 editor.context_menu_prev(&ContextMenuPrev, window, cx);
11738 });
11739 cx.run_until_parked();
11740 // Completions that have already been resolved are skipped.
11741 assert_eq!(
11742 *resolved_items.lock(),
11743 items_out[items_out.len() - 16..items_out.len() - 4]
11744 .iter()
11745 .cloned()
11746 .collect::<Vec<lsp::CompletionItem>>()
11747 );
11748 resolved_items.lock().clear();
11749}
11750
11751#[gpui::test]
11752async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
11753 init_test(cx, |_| {});
11754
11755 let mut cx = EditorLspTestContext::new(
11756 Language::new(
11757 LanguageConfig {
11758 matcher: LanguageMatcher {
11759 path_suffixes: vec!["jsx".into()],
11760 ..Default::default()
11761 },
11762 overrides: [(
11763 "element".into(),
11764 LanguageConfigOverride {
11765 word_characters: Override::Set(['-'].into_iter().collect()),
11766 ..Default::default()
11767 },
11768 )]
11769 .into_iter()
11770 .collect(),
11771 ..Default::default()
11772 },
11773 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
11774 )
11775 .with_override_query("(jsx_self_closing_element) @element")
11776 .unwrap(),
11777 lsp::ServerCapabilities {
11778 completion_provider: Some(lsp::CompletionOptions {
11779 trigger_characters: Some(vec![":".to_string()]),
11780 ..Default::default()
11781 }),
11782 ..Default::default()
11783 },
11784 cx,
11785 )
11786 .await;
11787
11788 cx.lsp
11789 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11790 Ok(Some(lsp::CompletionResponse::Array(vec![
11791 lsp::CompletionItem {
11792 label: "bg-blue".into(),
11793 ..Default::default()
11794 },
11795 lsp::CompletionItem {
11796 label: "bg-red".into(),
11797 ..Default::default()
11798 },
11799 lsp::CompletionItem {
11800 label: "bg-yellow".into(),
11801 ..Default::default()
11802 },
11803 ])))
11804 });
11805
11806 cx.set_state(r#"<p class="bgˇ" />"#);
11807
11808 // Trigger completion when typing a dash, because the dash is an extra
11809 // word character in the 'element' scope, which contains the cursor.
11810 cx.simulate_keystroke("-");
11811 cx.executor().run_until_parked();
11812 cx.update_editor(|editor, _, _| {
11813 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11814 {
11815 assert_eq!(
11816 completion_menu_entries(&menu),
11817 &["bg-red", "bg-blue", "bg-yellow"]
11818 );
11819 } else {
11820 panic!("expected completion menu to be open");
11821 }
11822 });
11823
11824 cx.simulate_keystroke("l");
11825 cx.executor().run_until_parked();
11826 cx.update_editor(|editor, _, _| {
11827 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11828 {
11829 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
11830 } else {
11831 panic!("expected completion menu to be open");
11832 }
11833 });
11834
11835 // When filtering completions, consider the character after the '-' to
11836 // be the start of a subword.
11837 cx.set_state(r#"<p class="yelˇ" />"#);
11838 cx.simulate_keystroke("l");
11839 cx.executor().run_until_parked();
11840 cx.update_editor(|editor, _, _| {
11841 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11842 {
11843 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
11844 } else {
11845 panic!("expected completion menu to be open");
11846 }
11847 });
11848}
11849
11850fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
11851 let entries = menu.entries.borrow();
11852 entries.iter().map(|mat| mat.string.clone()).collect()
11853}
11854
11855#[gpui::test]
11856async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
11857 init_test(cx, |settings| {
11858 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
11859 FormatterList(vec![Formatter::Prettier].into()),
11860 ))
11861 });
11862
11863 let fs = FakeFs::new(cx.executor());
11864 fs.insert_file("/file.ts", Default::default()).await;
11865
11866 let project = Project::test(fs, ["/file.ts".as_ref()], cx).await;
11867 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11868
11869 language_registry.add(Arc::new(Language::new(
11870 LanguageConfig {
11871 name: "TypeScript".into(),
11872 matcher: LanguageMatcher {
11873 path_suffixes: vec!["ts".to_string()],
11874 ..Default::default()
11875 },
11876 ..Default::default()
11877 },
11878 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11879 )));
11880 update_test_language_settings(cx, |settings| {
11881 settings.defaults.prettier = Some(PrettierSettings {
11882 allowed: true,
11883 ..PrettierSettings::default()
11884 });
11885 });
11886
11887 let test_plugin = "test_plugin";
11888 let _ = language_registry.register_fake_lsp(
11889 "TypeScript",
11890 FakeLspAdapter {
11891 prettier_plugins: vec![test_plugin],
11892 ..Default::default()
11893 },
11894 );
11895
11896 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
11897 let buffer = project
11898 .update(cx, |project, cx| project.open_local_buffer("/file.ts", cx))
11899 .await
11900 .unwrap();
11901
11902 let buffer_text = "one\ntwo\nthree\n";
11903 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11904 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11905 editor.update_in(cx, |editor, window, cx| {
11906 editor.set_text(buffer_text, window, cx)
11907 });
11908
11909 editor
11910 .update_in(cx, |editor, window, cx| {
11911 editor.perform_format(
11912 project.clone(),
11913 FormatTrigger::Manual,
11914 FormatTarget::Buffers,
11915 window,
11916 cx,
11917 )
11918 })
11919 .unwrap()
11920 .await;
11921 assert_eq!(
11922 editor.update(cx, |editor, cx| editor.text(cx)),
11923 buffer_text.to_string() + prettier_format_suffix,
11924 "Test prettier formatting was not applied to the original buffer text",
11925 );
11926
11927 update_test_language_settings(cx, |settings| {
11928 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
11929 });
11930 let format = editor.update_in(cx, |editor, window, cx| {
11931 editor.perform_format(
11932 project.clone(),
11933 FormatTrigger::Manual,
11934 FormatTarget::Buffers,
11935 window,
11936 cx,
11937 )
11938 });
11939 format.await.unwrap();
11940 assert_eq!(
11941 editor.update(cx, |editor, cx| editor.text(cx)),
11942 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
11943 "Autoformatting (via test prettier) was not applied to the original buffer text",
11944 );
11945}
11946
11947#[gpui::test]
11948async fn test_addition_reverts(cx: &mut gpui::TestAppContext) {
11949 init_test(cx, |_| {});
11950 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11951 let base_text = indoc! {r#"
11952 struct Row;
11953 struct Row1;
11954 struct Row2;
11955
11956 struct Row4;
11957 struct Row5;
11958 struct Row6;
11959
11960 struct Row8;
11961 struct Row9;
11962 struct Row10;"#};
11963
11964 // When addition hunks are not adjacent to carets, no hunk revert is performed
11965 assert_hunk_revert(
11966 indoc! {r#"struct Row;
11967 struct Row1;
11968 struct Row1.1;
11969 struct Row1.2;
11970 struct Row2;ˇ
11971
11972 struct Row4;
11973 struct Row5;
11974 struct Row6;
11975
11976 struct Row8;
11977 ˇstruct Row9;
11978 struct Row9.1;
11979 struct Row9.2;
11980 struct Row9.3;
11981 struct Row10;"#},
11982 vec![DiffHunkStatus::Added, DiffHunkStatus::Added],
11983 indoc! {r#"struct Row;
11984 struct Row1;
11985 struct Row1.1;
11986 struct Row1.2;
11987 struct Row2;ˇ
11988
11989 struct Row4;
11990 struct Row5;
11991 struct Row6;
11992
11993 struct Row8;
11994 ˇstruct Row9;
11995 struct Row9.1;
11996 struct Row9.2;
11997 struct Row9.3;
11998 struct Row10;"#},
11999 base_text,
12000 &mut cx,
12001 );
12002 // Same for selections
12003 assert_hunk_revert(
12004 indoc! {r#"struct Row;
12005 struct Row1;
12006 struct Row2;
12007 struct Row2.1;
12008 struct Row2.2;
12009 «ˇ
12010 struct Row4;
12011 struct» Row5;
12012 «struct Row6;
12013 ˇ»
12014 struct Row9.1;
12015 struct Row9.2;
12016 struct Row9.3;
12017 struct Row8;
12018 struct Row9;
12019 struct Row10;"#},
12020 vec![DiffHunkStatus::Added, DiffHunkStatus::Added],
12021 indoc! {r#"struct Row;
12022 struct Row1;
12023 struct Row2;
12024 struct Row2.1;
12025 struct Row2.2;
12026 «ˇ
12027 struct Row4;
12028 struct» Row5;
12029 «struct Row6;
12030 ˇ»
12031 struct Row9.1;
12032 struct Row9.2;
12033 struct Row9.3;
12034 struct Row8;
12035 struct Row9;
12036 struct Row10;"#},
12037 base_text,
12038 &mut cx,
12039 );
12040
12041 // When carets and selections intersect the addition hunks, those are reverted.
12042 // Adjacent carets got merged.
12043 assert_hunk_revert(
12044 indoc! {r#"struct Row;
12045 ˇ// something on the top
12046 struct Row1;
12047 struct Row2;
12048 struct Roˇw3.1;
12049 struct Row2.2;
12050 struct Row2.3;ˇ
12051
12052 struct Row4;
12053 struct ˇRow5.1;
12054 struct Row5.2;
12055 struct «Rowˇ»5.3;
12056 struct Row5;
12057 struct Row6;
12058 ˇ
12059 struct Row9.1;
12060 struct «Rowˇ»9.2;
12061 struct «ˇRow»9.3;
12062 struct Row8;
12063 struct Row9;
12064 «ˇ// something on bottom»
12065 struct Row10;"#},
12066 vec![
12067 DiffHunkStatus::Added,
12068 DiffHunkStatus::Added,
12069 DiffHunkStatus::Added,
12070 DiffHunkStatus::Added,
12071 DiffHunkStatus::Added,
12072 ],
12073 indoc! {r#"struct Row;
12074 ˇstruct Row1;
12075 struct Row2;
12076 ˇ
12077 struct Row4;
12078 ˇstruct Row5;
12079 struct Row6;
12080 ˇ
12081 ˇstruct Row8;
12082 struct Row9;
12083 ˇstruct Row10;"#},
12084 base_text,
12085 &mut cx,
12086 );
12087}
12088
12089#[gpui::test]
12090async fn test_modification_reverts(cx: &mut gpui::TestAppContext) {
12091 init_test(cx, |_| {});
12092 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12093 let base_text = indoc! {r#"
12094 struct Row;
12095 struct Row1;
12096 struct Row2;
12097
12098 struct Row4;
12099 struct Row5;
12100 struct Row6;
12101
12102 struct Row8;
12103 struct Row9;
12104 struct Row10;"#};
12105
12106 // Modification hunks behave the same as the addition ones.
12107 assert_hunk_revert(
12108 indoc! {r#"struct Row;
12109 struct Row1;
12110 struct Row33;
12111 ˇ
12112 struct Row4;
12113 struct Row5;
12114 struct Row6;
12115 ˇ
12116 struct Row99;
12117 struct Row9;
12118 struct Row10;"#},
12119 vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified],
12120 indoc! {r#"struct Row;
12121 struct Row1;
12122 struct Row33;
12123 ˇ
12124 struct Row4;
12125 struct Row5;
12126 struct Row6;
12127 ˇ
12128 struct Row99;
12129 struct Row9;
12130 struct Row10;"#},
12131 base_text,
12132 &mut cx,
12133 );
12134 assert_hunk_revert(
12135 indoc! {r#"struct Row;
12136 struct Row1;
12137 struct Row33;
12138 «ˇ
12139 struct Row4;
12140 struct» Row5;
12141 «struct Row6;
12142 ˇ»
12143 struct Row99;
12144 struct Row9;
12145 struct Row10;"#},
12146 vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified],
12147 indoc! {r#"struct Row;
12148 struct Row1;
12149 struct Row33;
12150 «ˇ
12151 struct Row4;
12152 struct» Row5;
12153 «struct Row6;
12154 ˇ»
12155 struct Row99;
12156 struct Row9;
12157 struct Row10;"#},
12158 base_text,
12159 &mut cx,
12160 );
12161
12162 assert_hunk_revert(
12163 indoc! {r#"ˇstruct Row1.1;
12164 struct Row1;
12165 «ˇstr»uct Row22;
12166
12167 struct ˇRow44;
12168 struct Row5;
12169 struct «Rˇ»ow66;ˇ
12170
12171 «struˇ»ct Row88;
12172 struct Row9;
12173 struct Row1011;ˇ"#},
12174 vec![
12175 DiffHunkStatus::Modified,
12176 DiffHunkStatus::Modified,
12177 DiffHunkStatus::Modified,
12178 DiffHunkStatus::Modified,
12179 DiffHunkStatus::Modified,
12180 DiffHunkStatus::Modified,
12181 ],
12182 indoc! {r#"struct Row;
12183 ˇstruct Row1;
12184 struct Row2;
12185 ˇ
12186 struct Row4;
12187 ˇstruct Row5;
12188 struct Row6;
12189 ˇ
12190 struct Row8;
12191 ˇstruct Row9;
12192 struct Row10;ˇ"#},
12193 base_text,
12194 &mut cx,
12195 );
12196}
12197
12198#[gpui::test]
12199async fn test_deleting_over_diff_hunk(cx: &mut gpui::TestAppContext) {
12200 init_test(cx, |_| {});
12201 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12202 let base_text = indoc! {r#"
12203 one
12204
12205 two
12206 three
12207 "#};
12208
12209 cx.set_diff_base(base_text);
12210 cx.set_state("\nˇ\n");
12211 cx.executor().run_until_parked();
12212 cx.update_editor(|editor, _window, cx| {
12213 editor.expand_selected_diff_hunks(cx);
12214 });
12215 cx.executor().run_until_parked();
12216 cx.update_editor(|editor, window, cx| {
12217 editor.backspace(&Default::default(), window, cx);
12218 });
12219 cx.run_until_parked();
12220 cx.assert_state_with_diff(
12221 indoc! {r#"
12222
12223 - two
12224 - threeˇ
12225 +
12226 "#}
12227 .to_string(),
12228 );
12229}
12230
12231#[gpui::test]
12232async fn test_deletion_reverts(cx: &mut gpui::TestAppContext) {
12233 init_test(cx, |_| {});
12234 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12235 let base_text = indoc! {r#"struct Row;
12236struct Row1;
12237struct Row2;
12238
12239struct Row4;
12240struct Row5;
12241struct Row6;
12242
12243struct Row8;
12244struct Row9;
12245struct Row10;"#};
12246
12247 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
12248 assert_hunk_revert(
12249 indoc! {r#"struct Row;
12250 struct Row2;
12251
12252 ˇstruct Row4;
12253 struct Row5;
12254 struct Row6;
12255 ˇ
12256 struct Row8;
12257 struct Row10;"#},
12258 vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
12259 indoc! {r#"struct Row;
12260 struct Row2;
12261
12262 ˇstruct Row4;
12263 struct Row5;
12264 struct Row6;
12265 ˇ
12266 struct Row8;
12267 struct Row10;"#},
12268 base_text,
12269 &mut cx,
12270 );
12271 assert_hunk_revert(
12272 indoc! {r#"struct Row;
12273 struct Row2;
12274
12275 «ˇstruct Row4;
12276 struct» Row5;
12277 «struct Row6;
12278 ˇ»
12279 struct Row8;
12280 struct Row10;"#},
12281 vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
12282 indoc! {r#"struct Row;
12283 struct Row2;
12284
12285 «ˇstruct Row4;
12286 struct» Row5;
12287 «struct Row6;
12288 ˇ»
12289 struct Row8;
12290 struct Row10;"#},
12291 base_text,
12292 &mut cx,
12293 );
12294
12295 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
12296 assert_hunk_revert(
12297 indoc! {r#"struct Row;
12298 ˇstruct Row2;
12299
12300 struct Row4;
12301 struct Row5;
12302 struct Row6;
12303
12304 struct Row8;ˇ
12305 struct Row10;"#},
12306 vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
12307 indoc! {r#"struct Row;
12308 struct Row1;
12309 ˇstruct Row2;
12310
12311 struct Row4;
12312 struct Row5;
12313 struct Row6;
12314
12315 struct Row8;ˇ
12316 struct Row9;
12317 struct Row10;"#},
12318 base_text,
12319 &mut cx,
12320 );
12321 assert_hunk_revert(
12322 indoc! {r#"struct Row;
12323 struct Row2«ˇ;
12324 struct Row4;
12325 struct» Row5;
12326 «struct Row6;
12327
12328 struct Row8;ˇ»
12329 struct Row10;"#},
12330 vec![
12331 DiffHunkStatus::Removed,
12332 DiffHunkStatus::Removed,
12333 DiffHunkStatus::Removed,
12334 ],
12335 indoc! {r#"struct Row;
12336 struct Row1;
12337 struct Row2«ˇ;
12338
12339 struct Row4;
12340 struct» Row5;
12341 «struct Row6;
12342
12343 struct Row8;ˇ»
12344 struct Row9;
12345 struct Row10;"#},
12346 base_text,
12347 &mut cx,
12348 );
12349}
12350
12351#[gpui::test]
12352async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
12353 init_test(cx, |_| {});
12354
12355 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
12356 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
12357 let base_text_3 =
12358 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
12359
12360 let text_1 = edit_first_char_of_every_line(base_text_1);
12361 let text_2 = edit_first_char_of_every_line(base_text_2);
12362 let text_3 = edit_first_char_of_every_line(base_text_3);
12363
12364 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
12365 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
12366 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
12367
12368 let multibuffer = cx.new(|cx| {
12369 let mut multibuffer = MultiBuffer::new(ReadWrite);
12370 multibuffer.push_excerpts(
12371 buffer_1.clone(),
12372 [
12373 ExcerptRange {
12374 context: Point::new(0, 0)..Point::new(3, 0),
12375 primary: None,
12376 },
12377 ExcerptRange {
12378 context: Point::new(5, 0)..Point::new(7, 0),
12379 primary: None,
12380 },
12381 ExcerptRange {
12382 context: Point::new(9, 0)..Point::new(10, 4),
12383 primary: None,
12384 },
12385 ],
12386 cx,
12387 );
12388 multibuffer.push_excerpts(
12389 buffer_2.clone(),
12390 [
12391 ExcerptRange {
12392 context: Point::new(0, 0)..Point::new(3, 0),
12393 primary: None,
12394 },
12395 ExcerptRange {
12396 context: Point::new(5, 0)..Point::new(7, 0),
12397 primary: None,
12398 },
12399 ExcerptRange {
12400 context: Point::new(9, 0)..Point::new(10, 4),
12401 primary: None,
12402 },
12403 ],
12404 cx,
12405 );
12406 multibuffer.push_excerpts(
12407 buffer_3.clone(),
12408 [
12409 ExcerptRange {
12410 context: Point::new(0, 0)..Point::new(3, 0),
12411 primary: None,
12412 },
12413 ExcerptRange {
12414 context: Point::new(5, 0)..Point::new(7, 0),
12415 primary: None,
12416 },
12417 ExcerptRange {
12418 context: Point::new(9, 0)..Point::new(10, 4),
12419 primary: None,
12420 },
12421 ],
12422 cx,
12423 );
12424 multibuffer
12425 });
12426
12427 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12428 editor.update_in(cx, |editor, _window, cx| {
12429 for (buffer, diff_base) in [
12430 (buffer_1.clone(), base_text_1),
12431 (buffer_2.clone(), base_text_2),
12432 (buffer_3.clone(), base_text_3),
12433 ] {
12434 let change_set =
12435 cx.new(|cx| BufferChangeSet::new_with_base_text(&diff_base, &buffer, cx));
12436 editor
12437 .buffer
12438 .update(cx, |buffer, cx| buffer.add_change_set(change_set, cx));
12439 }
12440 });
12441 cx.executor().run_until_parked();
12442
12443 editor.update_in(cx, |editor, window, cx| {
12444 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}");
12445 editor.select_all(&SelectAll, window, cx);
12446 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
12447 });
12448 cx.executor().run_until_parked();
12449
12450 // When all ranges are selected, all buffer hunks are reverted.
12451 editor.update(cx, |editor, cx| {
12452 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");
12453 });
12454 buffer_1.update(cx, |buffer, _| {
12455 assert_eq!(buffer.text(), base_text_1);
12456 });
12457 buffer_2.update(cx, |buffer, _| {
12458 assert_eq!(buffer.text(), base_text_2);
12459 });
12460 buffer_3.update(cx, |buffer, _| {
12461 assert_eq!(buffer.text(), base_text_3);
12462 });
12463
12464 editor.update_in(cx, |editor, window, cx| {
12465 editor.undo(&Default::default(), window, cx);
12466 });
12467
12468 editor.update_in(cx, |editor, window, cx| {
12469 editor.change_selections(None, window, cx, |s| {
12470 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
12471 });
12472 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
12473 });
12474
12475 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
12476 // but not affect buffer_2 and its related excerpts.
12477 editor.update(cx, |editor, cx| {
12478 assert_eq!(
12479 editor.text(cx),
12480 "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}"
12481 );
12482 });
12483 buffer_1.update(cx, |buffer, _| {
12484 assert_eq!(buffer.text(), base_text_1);
12485 });
12486 buffer_2.update(cx, |buffer, _| {
12487 assert_eq!(
12488 buffer.text(),
12489 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
12490 );
12491 });
12492 buffer_3.update(cx, |buffer, _| {
12493 assert_eq!(
12494 buffer.text(),
12495 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
12496 );
12497 });
12498
12499 fn edit_first_char_of_every_line(text: &str) -> String {
12500 text.split('\n')
12501 .map(|line| format!("X{}", &line[1..]))
12502 .collect::<Vec<_>>()
12503 .join("\n")
12504 }
12505}
12506
12507#[gpui::test]
12508async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
12509 init_test(cx, |_| {});
12510
12511 let cols = 4;
12512 let rows = 10;
12513 let sample_text_1 = sample_text(rows, cols, 'a');
12514 assert_eq!(
12515 sample_text_1,
12516 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
12517 );
12518 let sample_text_2 = sample_text(rows, cols, 'l');
12519 assert_eq!(
12520 sample_text_2,
12521 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
12522 );
12523 let sample_text_3 = sample_text(rows, cols, 'v');
12524 assert_eq!(
12525 sample_text_3,
12526 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
12527 );
12528
12529 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
12530 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
12531 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
12532
12533 let multi_buffer = cx.new(|cx| {
12534 let mut multibuffer = MultiBuffer::new(ReadWrite);
12535 multibuffer.push_excerpts(
12536 buffer_1.clone(),
12537 [
12538 ExcerptRange {
12539 context: Point::new(0, 0)..Point::new(3, 0),
12540 primary: None,
12541 },
12542 ExcerptRange {
12543 context: Point::new(5, 0)..Point::new(7, 0),
12544 primary: None,
12545 },
12546 ExcerptRange {
12547 context: Point::new(9, 0)..Point::new(10, 4),
12548 primary: None,
12549 },
12550 ],
12551 cx,
12552 );
12553 multibuffer.push_excerpts(
12554 buffer_2.clone(),
12555 [
12556 ExcerptRange {
12557 context: Point::new(0, 0)..Point::new(3, 0),
12558 primary: None,
12559 },
12560 ExcerptRange {
12561 context: Point::new(5, 0)..Point::new(7, 0),
12562 primary: None,
12563 },
12564 ExcerptRange {
12565 context: Point::new(9, 0)..Point::new(10, 4),
12566 primary: None,
12567 },
12568 ],
12569 cx,
12570 );
12571 multibuffer.push_excerpts(
12572 buffer_3.clone(),
12573 [
12574 ExcerptRange {
12575 context: Point::new(0, 0)..Point::new(3, 0),
12576 primary: None,
12577 },
12578 ExcerptRange {
12579 context: Point::new(5, 0)..Point::new(7, 0),
12580 primary: None,
12581 },
12582 ExcerptRange {
12583 context: Point::new(9, 0)..Point::new(10, 4),
12584 primary: None,
12585 },
12586 ],
12587 cx,
12588 );
12589 multibuffer
12590 });
12591
12592 let fs = FakeFs::new(cx.executor());
12593 fs.insert_tree(
12594 "/a",
12595 json!({
12596 "main.rs": sample_text_1,
12597 "other.rs": sample_text_2,
12598 "lib.rs": sample_text_3,
12599 }),
12600 )
12601 .await;
12602 let project = Project::test(fs, ["/a".as_ref()], cx).await;
12603 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12604 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12605 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
12606 Editor::new(
12607 EditorMode::Full,
12608 multi_buffer,
12609 Some(project.clone()),
12610 true,
12611 window,
12612 cx,
12613 )
12614 });
12615 let multibuffer_item_id = workspace
12616 .update(cx, |workspace, window, cx| {
12617 assert!(
12618 workspace.active_item(cx).is_none(),
12619 "active item should be None before the first item is added"
12620 );
12621 workspace.add_item_to_active_pane(
12622 Box::new(multi_buffer_editor.clone()),
12623 None,
12624 true,
12625 window,
12626 cx,
12627 );
12628 let active_item = workspace
12629 .active_item(cx)
12630 .expect("should have an active item after adding the multi buffer");
12631 assert!(
12632 !active_item.is_singleton(cx),
12633 "A multi buffer was expected to active after adding"
12634 );
12635 active_item.item_id()
12636 })
12637 .unwrap();
12638 cx.executor().run_until_parked();
12639
12640 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12641 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12642 s.select_ranges(Some(1..2))
12643 });
12644 editor.open_excerpts(&OpenExcerpts, window, cx);
12645 });
12646 cx.executor().run_until_parked();
12647 let first_item_id = workspace
12648 .update(cx, |workspace, window, cx| {
12649 let active_item = workspace
12650 .active_item(cx)
12651 .expect("should have an active item after navigating into the 1st buffer");
12652 let first_item_id = active_item.item_id();
12653 assert_ne!(
12654 first_item_id, multibuffer_item_id,
12655 "Should navigate into the 1st buffer and activate it"
12656 );
12657 assert!(
12658 active_item.is_singleton(cx),
12659 "New active item should be a singleton buffer"
12660 );
12661 assert_eq!(
12662 active_item
12663 .act_as::<Editor>(cx)
12664 .expect("should have navigated into an editor for the 1st buffer")
12665 .read(cx)
12666 .text(cx),
12667 sample_text_1
12668 );
12669
12670 workspace
12671 .go_back(workspace.active_pane().downgrade(), window, cx)
12672 .detach_and_log_err(cx);
12673
12674 first_item_id
12675 })
12676 .unwrap();
12677 cx.executor().run_until_parked();
12678 workspace
12679 .update(cx, |workspace, _, cx| {
12680 let active_item = workspace
12681 .active_item(cx)
12682 .expect("should have an active item after navigating back");
12683 assert_eq!(
12684 active_item.item_id(),
12685 multibuffer_item_id,
12686 "Should navigate back to the multi buffer"
12687 );
12688 assert!(!active_item.is_singleton(cx));
12689 })
12690 .unwrap();
12691
12692 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12693 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12694 s.select_ranges(Some(39..40))
12695 });
12696 editor.open_excerpts(&OpenExcerpts, window, cx);
12697 });
12698 cx.executor().run_until_parked();
12699 let second_item_id = workspace
12700 .update(cx, |workspace, window, cx| {
12701 let active_item = workspace
12702 .active_item(cx)
12703 .expect("should have an active item after navigating into the 2nd buffer");
12704 let second_item_id = active_item.item_id();
12705 assert_ne!(
12706 second_item_id, multibuffer_item_id,
12707 "Should navigate away from the multibuffer"
12708 );
12709 assert_ne!(
12710 second_item_id, first_item_id,
12711 "Should navigate into the 2nd buffer and activate it"
12712 );
12713 assert!(
12714 active_item.is_singleton(cx),
12715 "New active item should be a singleton buffer"
12716 );
12717 assert_eq!(
12718 active_item
12719 .act_as::<Editor>(cx)
12720 .expect("should have navigated into an editor")
12721 .read(cx)
12722 .text(cx),
12723 sample_text_2
12724 );
12725
12726 workspace
12727 .go_back(workspace.active_pane().downgrade(), window, cx)
12728 .detach_and_log_err(cx);
12729
12730 second_item_id
12731 })
12732 .unwrap();
12733 cx.executor().run_until_parked();
12734 workspace
12735 .update(cx, |workspace, _, cx| {
12736 let active_item = workspace
12737 .active_item(cx)
12738 .expect("should have an active item after navigating back from the 2nd buffer");
12739 assert_eq!(
12740 active_item.item_id(),
12741 multibuffer_item_id,
12742 "Should navigate back from the 2nd buffer to the multi buffer"
12743 );
12744 assert!(!active_item.is_singleton(cx));
12745 })
12746 .unwrap();
12747
12748 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12749 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12750 s.select_ranges(Some(70..70))
12751 });
12752 editor.open_excerpts(&OpenExcerpts, window, cx);
12753 });
12754 cx.executor().run_until_parked();
12755 workspace
12756 .update(cx, |workspace, window, cx| {
12757 let active_item = workspace
12758 .active_item(cx)
12759 .expect("should have an active item after navigating into the 3rd buffer");
12760 let third_item_id = active_item.item_id();
12761 assert_ne!(
12762 third_item_id, multibuffer_item_id,
12763 "Should navigate into the 3rd buffer and activate it"
12764 );
12765 assert_ne!(third_item_id, first_item_id);
12766 assert_ne!(third_item_id, second_item_id);
12767 assert!(
12768 active_item.is_singleton(cx),
12769 "New active item should be a singleton buffer"
12770 );
12771 assert_eq!(
12772 active_item
12773 .act_as::<Editor>(cx)
12774 .expect("should have navigated into an editor")
12775 .read(cx)
12776 .text(cx),
12777 sample_text_3
12778 );
12779
12780 workspace
12781 .go_back(workspace.active_pane().downgrade(), window, cx)
12782 .detach_and_log_err(cx);
12783 })
12784 .unwrap();
12785 cx.executor().run_until_parked();
12786 workspace
12787 .update(cx, |workspace, _, cx| {
12788 let active_item = workspace
12789 .active_item(cx)
12790 .expect("should have an active item after navigating back from the 3rd buffer");
12791 assert_eq!(
12792 active_item.item_id(),
12793 multibuffer_item_id,
12794 "Should navigate back from the 3rd buffer to the multi buffer"
12795 );
12796 assert!(!active_item.is_singleton(cx));
12797 })
12798 .unwrap();
12799}
12800
12801#[gpui::test]
12802async fn test_toggle_selected_diff_hunks(
12803 executor: BackgroundExecutor,
12804 cx: &mut gpui::TestAppContext,
12805) {
12806 init_test(cx, |_| {});
12807
12808 let mut cx = EditorTestContext::new(cx).await;
12809
12810 let diff_base = r#"
12811 use some::mod;
12812
12813 const A: u32 = 42;
12814
12815 fn main() {
12816 println!("hello");
12817
12818 println!("world");
12819 }
12820 "#
12821 .unindent();
12822
12823 cx.set_state(
12824 &r#"
12825 use some::modified;
12826
12827 ˇ
12828 fn main() {
12829 println!("hello there");
12830
12831 println!("around the");
12832 println!("world");
12833 }
12834 "#
12835 .unindent(),
12836 );
12837
12838 cx.set_diff_base(&diff_base);
12839 executor.run_until_parked();
12840
12841 cx.update_editor(|editor, window, cx| {
12842 editor.go_to_next_hunk(&GoToHunk, window, cx);
12843 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
12844 });
12845 executor.run_until_parked();
12846 cx.assert_state_with_diff(
12847 r#"
12848 use some::modified;
12849
12850
12851 fn main() {
12852 - println!("hello");
12853 + ˇ println!("hello there");
12854
12855 println!("around the");
12856 println!("world");
12857 }
12858 "#
12859 .unindent(),
12860 );
12861
12862 cx.update_editor(|editor, window, cx| {
12863 for _ in 0..2 {
12864 editor.go_to_next_hunk(&GoToHunk, window, cx);
12865 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
12866 }
12867 });
12868 executor.run_until_parked();
12869 cx.assert_state_with_diff(
12870 r#"
12871 - use some::mod;
12872 + ˇuse some::modified;
12873
12874
12875 fn main() {
12876 - println!("hello");
12877 + println!("hello there");
12878
12879 + println!("around the");
12880 println!("world");
12881 }
12882 "#
12883 .unindent(),
12884 );
12885
12886 cx.update_editor(|editor, window, cx| {
12887 editor.go_to_next_hunk(&GoToHunk, window, cx);
12888 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
12889 });
12890 executor.run_until_parked();
12891 cx.assert_state_with_diff(
12892 r#"
12893 - use some::mod;
12894 + use some::modified;
12895
12896 - const A: u32 = 42;
12897 ˇ
12898 fn main() {
12899 - println!("hello");
12900 + println!("hello there");
12901
12902 + println!("around the");
12903 println!("world");
12904 }
12905 "#
12906 .unindent(),
12907 );
12908
12909 cx.update_editor(|editor, window, cx| {
12910 editor.cancel(&Cancel, window, cx);
12911 });
12912
12913 cx.assert_state_with_diff(
12914 r#"
12915 use some::modified;
12916
12917 ˇ
12918 fn main() {
12919 println!("hello there");
12920
12921 println!("around the");
12922 println!("world");
12923 }
12924 "#
12925 .unindent(),
12926 );
12927}
12928
12929#[gpui::test]
12930async fn test_diff_base_change_with_expanded_diff_hunks(
12931 executor: BackgroundExecutor,
12932 cx: &mut gpui::TestAppContext,
12933) {
12934 init_test(cx, |_| {});
12935
12936 let mut cx = EditorTestContext::new(cx).await;
12937
12938 let diff_base = r#"
12939 use some::mod1;
12940 use some::mod2;
12941
12942 const A: u32 = 42;
12943 const B: u32 = 42;
12944 const C: u32 = 42;
12945
12946 fn main() {
12947 println!("hello");
12948
12949 println!("world");
12950 }
12951 "#
12952 .unindent();
12953
12954 cx.set_state(
12955 &r#"
12956 use some::mod2;
12957
12958 const A: u32 = 42;
12959 const C: u32 = 42;
12960
12961 fn main(ˇ) {
12962 //println!("hello");
12963
12964 println!("world");
12965 //
12966 //
12967 }
12968 "#
12969 .unindent(),
12970 );
12971
12972 cx.set_diff_base(&diff_base);
12973 executor.run_until_parked();
12974
12975 cx.update_editor(|editor, window, cx| {
12976 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
12977 });
12978 executor.run_until_parked();
12979 cx.assert_state_with_diff(
12980 r#"
12981 - use some::mod1;
12982 use some::mod2;
12983
12984 const A: u32 = 42;
12985 - const B: u32 = 42;
12986 const C: u32 = 42;
12987
12988 fn main(ˇ) {
12989 - println!("hello");
12990 + //println!("hello");
12991
12992 println!("world");
12993 + //
12994 + //
12995 }
12996 "#
12997 .unindent(),
12998 );
12999
13000 cx.set_diff_base("new diff base!");
13001 executor.run_until_parked();
13002 cx.assert_state_with_diff(
13003 r#"
13004 use some::mod2;
13005
13006 const A: u32 = 42;
13007 const C: u32 = 42;
13008
13009 fn main(ˇ) {
13010 //println!("hello");
13011
13012 println!("world");
13013 //
13014 //
13015 }
13016 "#
13017 .unindent(),
13018 );
13019
13020 cx.update_editor(|editor, window, cx| {
13021 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13022 });
13023 executor.run_until_parked();
13024 cx.assert_state_with_diff(
13025 r#"
13026 - new diff base!
13027 + use some::mod2;
13028 +
13029 + const A: u32 = 42;
13030 + const C: u32 = 42;
13031 +
13032 + fn main(ˇ) {
13033 + //println!("hello");
13034 +
13035 + println!("world");
13036 + //
13037 + //
13038 + }
13039 "#
13040 .unindent(),
13041 );
13042}
13043
13044#[gpui::test]
13045async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) {
13046 init_test(cx, |_| {});
13047
13048 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
13049 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
13050 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
13051 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
13052 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
13053 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
13054
13055 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
13056 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
13057 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
13058
13059 let multi_buffer = cx.new(|cx| {
13060 let mut multibuffer = MultiBuffer::new(ReadWrite);
13061 multibuffer.push_excerpts(
13062 buffer_1.clone(),
13063 [
13064 ExcerptRange {
13065 context: Point::new(0, 0)..Point::new(3, 0),
13066 primary: None,
13067 },
13068 ExcerptRange {
13069 context: Point::new(5, 0)..Point::new(7, 0),
13070 primary: None,
13071 },
13072 ExcerptRange {
13073 context: Point::new(9, 0)..Point::new(10, 3),
13074 primary: None,
13075 },
13076 ],
13077 cx,
13078 );
13079 multibuffer.push_excerpts(
13080 buffer_2.clone(),
13081 [
13082 ExcerptRange {
13083 context: Point::new(0, 0)..Point::new(3, 0),
13084 primary: None,
13085 },
13086 ExcerptRange {
13087 context: Point::new(5, 0)..Point::new(7, 0),
13088 primary: None,
13089 },
13090 ExcerptRange {
13091 context: Point::new(9, 0)..Point::new(10, 3),
13092 primary: None,
13093 },
13094 ],
13095 cx,
13096 );
13097 multibuffer.push_excerpts(
13098 buffer_3.clone(),
13099 [
13100 ExcerptRange {
13101 context: Point::new(0, 0)..Point::new(3, 0),
13102 primary: None,
13103 },
13104 ExcerptRange {
13105 context: Point::new(5, 0)..Point::new(7, 0),
13106 primary: None,
13107 },
13108 ExcerptRange {
13109 context: Point::new(9, 0)..Point::new(10, 3),
13110 primary: None,
13111 },
13112 ],
13113 cx,
13114 );
13115 multibuffer
13116 });
13117
13118 let editor = cx.add_window(|window, cx| {
13119 Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
13120 });
13121 editor
13122 .update(cx, |editor, _window, cx| {
13123 for (buffer, diff_base) in [
13124 (buffer_1.clone(), file_1_old),
13125 (buffer_2.clone(), file_2_old),
13126 (buffer_3.clone(), file_3_old),
13127 ] {
13128 let change_set =
13129 cx.new(|cx| BufferChangeSet::new_with_base_text(&diff_base, &buffer, cx));
13130 editor
13131 .buffer
13132 .update(cx, |buffer, cx| buffer.add_change_set(change_set, cx));
13133 }
13134 })
13135 .unwrap();
13136
13137 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13138 cx.run_until_parked();
13139
13140 cx.assert_editor_state(
13141 &"
13142 ˇaaa
13143 ccc
13144 ddd
13145
13146 ggg
13147 hhh
13148
13149
13150 lll
13151 mmm
13152 NNN
13153
13154 qqq
13155 rrr
13156
13157 uuu
13158 111
13159 222
13160 333
13161
13162 666
13163 777
13164
13165 000
13166 !!!"
13167 .unindent(),
13168 );
13169
13170 cx.update_editor(|editor, window, cx| {
13171 editor.select_all(&SelectAll, window, cx);
13172 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13173 });
13174 cx.executor().run_until_parked();
13175
13176 cx.assert_state_with_diff(
13177 "
13178 «aaa
13179 - bbb
13180 ccc
13181 ddd
13182
13183 ggg
13184 hhh
13185
13186
13187 lll
13188 mmm
13189 - nnn
13190 + NNN
13191
13192 qqq
13193 rrr
13194
13195 uuu
13196 111
13197 222
13198 333
13199
13200 + 666
13201 777
13202
13203 000
13204 !!!ˇ»"
13205 .unindent(),
13206 );
13207}
13208
13209#[gpui::test]
13210async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext) {
13211 init_test(cx, |_| {});
13212
13213 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
13214 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
13215
13216 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
13217 let multi_buffer = cx.new(|cx| {
13218 let mut multibuffer = MultiBuffer::new(ReadWrite);
13219 multibuffer.push_excerpts(
13220 buffer.clone(),
13221 [
13222 ExcerptRange {
13223 context: Point::new(0, 0)..Point::new(2, 0),
13224 primary: None,
13225 },
13226 ExcerptRange {
13227 context: Point::new(4, 0)..Point::new(7, 0),
13228 primary: None,
13229 },
13230 ExcerptRange {
13231 context: Point::new(9, 0)..Point::new(10, 0),
13232 primary: None,
13233 },
13234 ],
13235 cx,
13236 );
13237 multibuffer
13238 });
13239
13240 let editor = cx.add_window(|window, cx| {
13241 Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
13242 });
13243 editor
13244 .update(cx, |editor, _window, cx| {
13245 let change_set = cx.new(|cx| BufferChangeSet::new_with_base_text(base, &buffer, cx));
13246 editor
13247 .buffer
13248 .update(cx, |buffer, cx| buffer.add_change_set(change_set, cx))
13249 })
13250 .unwrap();
13251
13252 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13253 cx.run_until_parked();
13254
13255 cx.update_editor(|editor, window, cx| {
13256 editor.expand_all_diff_hunks(&Default::default(), window, cx)
13257 });
13258 cx.executor().run_until_parked();
13259
13260 // When the start of a hunk coincides with the start of its excerpt,
13261 // the hunk is expanded. When the start of a a hunk is earlier than
13262 // the start of its excerpt, the hunk is not expanded.
13263 cx.assert_state_with_diff(
13264 "
13265 ˇaaa
13266 - bbb
13267 + BBB
13268
13269 - ddd
13270 - eee
13271 + DDD
13272 + EEE
13273 fff
13274
13275 iii
13276 "
13277 .unindent(),
13278 );
13279}
13280
13281#[gpui::test]
13282async fn test_edits_around_expanded_insertion_hunks(
13283 executor: BackgroundExecutor,
13284 cx: &mut gpui::TestAppContext,
13285) {
13286 init_test(cx, |_| {});
13287
13288 let mut cx = EditorTestContext::new(cx).await;
13289
13290 let diff_base = r#"
13291 use some::mod1;
13292 use some::mod2;
13293
13294 const A: u32 = 42;
13295
13296 fn main() {
13297 println!("hello");
13298
13299 println!("world");
13300 }
13301 "#
13302 .unindent();
13303 executor.run_until_parked();
13304 cx.set_state(
13305 &r#"
13306 use some::mod1;
13307 use some::mod2;
13308
13309 const A: u32 = 42;
13310 const B: u32 = 42;
13311 const C: u32 = 42;
13312 ˇ
13313
13314 fn main() {
13315 println!("hello");
13316
13317 println!("world");
13318 }
13319 "#
13320 .unindent(),
13321 );
13322
13323 cx.set_diff_base(&diff_base);
13324 executor.run_until_parked();
13325
13326 cx.update_editor(|editor, window, cx| {
13327 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13328 });
13329 executor.run_until_parked();
13330
13331 cx.assert_state_with_diff(
13332 r#"
13333 use some::mod1;
13334 use some::mod2;
13335
13336 const A: u32 = 42;
13337 + const B: u32 = 42;
13338 + const C: u32 = 42;
13339 + ˇ
13340
13341 fn main() {
13342 println!("hello");
13343
13344 println!("world");
13345 }
13346 "#
13347 .unindent(),
13348 );
13349
13350 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
13351 executor.run_until_parked();
13352
13353 cx.assert_state_with_diff(
13354 r#"
13355 use some::mod1;
13356 use some::mod2;
13357
13358 const A: u32 = 42;
13359 + const B: u32 = 42;
13360 + const C: u32 = 42;
13361 + const D: u32 = 42;
13362 + ˇ
13363
13364 fn main() {
13365 println!("hello");
13366
13367 println!("world");
13368 }
13369 "#
13370 .unindent(),
13371 );
13372
13373 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
13374 executor.run_until_parked();
13375
13376 cx.assert_state_with_diff(
13377 r#"
13378 use some::mod1;
13379 use some::mod2;
13380
13381 const A: u32 = 42;
13382 + const B: u32 = 42;
13383 + const C: u32 = 42;
13384 + const D: u32 = 42;
13385 + const E: u32 = 42;
13386 + ˇ
13387
13388 fn main() {
13389 println!("hello");
13390
13391 println!("world");
13392 }
13393 "#
13394 .unindent(),
13395 );
13396
13397 cx.update_editor(|editor, window, cx| {
13398 editor.delete_line(&DeleteLine, window, cx);
13399 });
13400 executor.run_until_parked();
13401
13402 cx.assert_state_with_diff(
13403 r#"
13404 use some::mod1;
13405 use some::mod2;
13406
13407 const A: u32 = 42;
13408 + const B: u32 = 42;
13409 + const C: u32 = 42;
13410 + const D: u32 = 42;
13411 + const E: u32 = 42;
13412 ˇ
13413 fn main() {
13414 println!("hello");
13415
13416 println!("world");
13417 }
13418 "#
13419 .unindent(),
13420 );
13421
13422 cx.update_editor(|editor, window, cx| {
13423 editor.move_up(&MoveUp, window, cx);
13424 editor.delete_line(&DeleteLine, window, cx);
13425 editor.move_up(&MoveUp, window, cx);
13426 editor.delete_line(&DeleteLine, window, cx);
13427 editor.move_up(&MoveUp, window, cx);
13428 editor.delete_line(&DeleteLine, window, cx);
13429 });
13430 executor.run_until_parked();
13431 cx.assert_state_with_diff(
13432 r#"
13433 use some::mod1;
13434 use some::mod2;
13435
13436 const A: u32 = 42;
13437 + const B: u32 = 42;
13438 ˇ
13439 fn main() {
13440 println!("hello");
13441
13442 println!("world");
13443 }
13444 "#
13445 .unindent(),
13446 );
13447
13448 cx.update_editor(|editor, window, cx| {
13449 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
13450 editor.delete_line(&DeleteLine, window, cx);
13451 });
13452 executor.run_until_parked();
13453 cx.assert_state_with_diff(
13454 r#"
13455 ˇ
13456 fn main() {
13457 println!("hello");
13458
13459 println!("world");
13460 }
13461 "#
13462 .unindent(),
13463 );
13464}
13465
13466#[gpui::test]
13467async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
13468 init_test(cx, |_| {});
13469
13470 let mut cx = EditorTestContext::new(cx).await;
13471 cx.set_diff_base(indoc! { "
13472 one
13473 two
13474 three
13475 four
13476 five
13477 "
13478 });
13479 cx.set_state(indoc! { "
13480 one
13481 ˇthree
13482 five
13483 "});
13484 cx.run_until_parked();
13485 cx.update_editor(|editor, window, cx| {
13486 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13487 });
13488 cx.assert_state_with_diff(
13489 indoc! { "
13490 one
13491 - two
13492 ˇthree
13493 - four
13494 five
13495 "}
13496 .to_string(),
13497 );
13498 cx.update_editor(|editor, window, cx| {
13499 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13500 });
13501
13502 cx.assert_state_with_diff(
13503 indoc! { "
13504 one
13505 ˇthree
13506 five
13507 "}
13508 .to_string(),
13509 );
13510
13511 cx.set_state(indoc! { "
13512 one
13513 ˇTWO
13514 three
13515 four
13516 five
13517 "});
13518 cx.run_until_parked();
13519 cx.update_editor(|editor, window, cx| {
13520 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13521 });
13522
13523 cx.assert_state_with_diff(
13524 indoc! { "
13525 one
13526 - two
13527 + ˇTWO
13528 three
13529 four
13530 five
13531 "}
13532 .to_string(),
13533 );
13534 cx.update_editor(|editor, window, cx| {
13535 editor.move_up(&Default::default(), window, cx);
13536 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13537 });
13538 cx.assert_state_with_diff(
13539 indoc! { "
13540 one
13541 ˇTWO
13542 three
13543 four
13544 five
13545 "}
13546 .to_string(),
13547 );
13548}
13549
13550#[gpui::test]
13551async fn test_edits_around_expanded_deletion_hunks(
13552 executor: BackgroundExecutor,
13553 cx: &mut gpui::TestAppContext,
13554) {
13555 init_test(cx, |_| {});
13556
13557 let mut cx = EditorTestContext::new(cx).await;
13558
13559 let diff_base = r#"
13560 use some::mod1;
13561 use some::mod2;
13562
13563 const A: u32 = 42;
13564 const B: u32 = 42;
13565 const C: u32 = 42;
13566
13567
13568 fn main() {
13569 println!("hello");
13570
13571 println!("world");
13572 }
13573 "#
13574 .unindent();
13575 executor.run_until_parked();
13576 cx.set_state(
13577 &r#"
13578 use some::mod1;
13579 use some::mod2;
13580
13581 ˇconst B: u32 = 42;
13582 const C: u32 = 42;
13583
13584
13585 fn main() {
13586 println!("hello");
13587
13588 println!("world");
13589 }
13590 "#
13591 .unindent(),
13592 );
13593
13594 cx.set_diff_base(&diff_base);
13595 executor.run_until_parked();
13596
13597 cx.update_editor(|editor, window, cx| {
13598 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13599 });
13600 executor.run_until_parked();
13601
13602 cx.assert_state_with_diff(
13603 r#"
13604 use some::mod1;
13605 use some::mod2;
13606
13607 - const A: u32 = 42;
13608 ˇconst B: u32 = 42;
13609 const C: u32 = 42;
13610
13611
13612 fn main() {
13613 println!("hello");
13614
13615 println!("world");
13616 }
13617 "#
13618 .unindent(),
13619 );
13620
13621 cx.update_editor(|editor, window, cx| {
13622 editor.delete_line(&DeleteLine, window, cx);
13623 });
13624 executor.run_until_parked();
13625 cx.assert_state_with_diff(
13626 r#"
13627 use some::mod1;
13628 use some::mod2;
13629
13630 - const A: u32 = 42;
13631 - const B: u32 = 42;
13632 ˇconst C: u32 = 42;
13633
13634
13635 fn main() {
13636 println!("hello");
13637
13638 println!("world");
13639 }
13640 "#
13641 .unindent(),
13642 );
13643
13644 cx.update_editor(|editor, window, cx| {
13645 editor.delete_line(&DeleteLine, window, cx);
13646 });
13647 executor.run_until_parked();
13648 cx.assert_state_with_diff(
13649 r#"
13650 use some::mod1;
13651 use some::mod2;
13652
13653 - const A: u32 = 42;
13654 - const B: u32 = 42;
13655 - const C: u32 = 42;
13656 ˇ
13657
13658 fn main() {
13659 println!("hello");
13660
13661 println!("world");
13662 }
13663 "#
13664 .unindent(),
13665 );
13666
13667 cx.update_editor(|editor, window, cx| {
13668 editor.handle_input("replacement", window, cx);
13669 });
13670 executor.run_until_parked();
13671 cx.assert_state_with_diff(
13672 r#"
13673 use some::mod1;
13674 use some::mod2;
13675
13676 - const A: u32 = 42;
13677 - const B: u32 = 42;
13678 - const C: u32 = 42;
13679 -
13680 + replacementˇ
13681
13682 fn main() {
13683 println!("hello");
13684
13685 println!("world");
13686 }
13687 "#
13688 .unindent(),
13689 );
13690}
13691
13692#[gpui::test]
13693async fn test_backspace_after_deletion_hunk(
13694 executor: BackgroundExecutor,
13695 cx: &mut gpui::TestAppContext,
13696) {
13697 init_test(cx, |_| {});
13698
13699 let mut cx = EditorTestContext::new(cx).await;
13700
13701 let base_text = r#"
13702 one
13703 two
13704 three
13705 four
13706 five
13707 "#
13708 .unindent();
13709 executor.run_until_parked();
13710 cx.set_state(
13711 &r#"
13712 one
13713 two
13714 fˇour
13715 five
13716 "#
13717 .unindent(),
13718 );
13719
13720 cx.set_diff_base(&base_text);
13721 executor.run_until_parked();
13722
13723 cx.update_editor(|editor, window, cx| {
13724 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13725 });
13726 executor.run_until_parked();
13727
13728 cx.assert_state_with_diff(
13729 r#"
13730 one
13731 two
13732 - three
13733 fˇour
13734 five
13735 "#
13736 .unindent(),
13737 );
13738
13739 cx.update_editor(|editor, window, cx| {
13740 editor.backspace(&Backspace, window, cx);
13741 editor.backspace(&Backspace, window, cx);
13742 });
13743 executor.run_until_parked();
13744 cx.assert_state_with_diff(
13745 r#"
13746 one
13747 two
13748 - threeˇ
13749 - four
13750 + our
13751 five
13752 "#
13753 .unindent(),
13754 );
13755}
13756
13757#[gpui::test]
13758async fn test_edit_after_expanded_modification_hunk(
13759 executor: BackgroundExecutor,
13760 cx: &mut gpui::TestAppContext,
13761) {
13762 init_test(cx, |_| {});
13763
13764 let mut cx = EditorTestContext::new(cx).await;
13765
13766 let diff_base = r#"
13767 use some::mod1;
13768 use some::mod2;
13769
13770 const A: u32 = 42;
13771 const B: u32 = 42;
13772 const C: u32 = 42;
13773 const D: u32 = 42;
13774
13775
13776 fn main() {
13777 println!("hello");
13778
13779 println!("world");
13780 }"#
13781 .unindent();
13782
13783 cx.set_state(
13784 &r#"
13785 use some::mod1;
13786 use some::mod2;
13787
13788 const A: u32 = 42;
13789 const B: u32 = 42;
13790 const C: u32 = 43ˇ
13791 const D: u32 = 42;
13792
13793
13794 fn main() {
13795 println!("hello");
13796
13797 println!("world");
13798 }"#
13799 .unindent(),
13800 );
13801
13802 cx.set_diff_base(&diff_base);
13803 executor.run_until_parked();
13804 cx.update_editor(|editor, window, cx| {
13805 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13806 });
13807 executor.run_until_parked();
13808
13809 cx.assert_state_with_diff(
13810 r#"
13811 use some::mod1;
13812 use some::mod2;
13813
13814 const A: u32 = 42;
13815 const B: u32 = 42;
13816 - const C: u32 = 42;
13817 + const C: u32 = 43ˇ
13818 const D: u32 = 42;
13819
13820
13821 fn main() {
13822 println!("hello");
13823
13824 println!("world");
13825 }"#
13826 .unindent(),
13827 );
13828
13829 cx.update_editor(|editor, window, cx| {
13830 editor.handle_input("\nnew_line\n", window, cx);
13831 });
13832 executor.run_until_parked();
13833
13834 cx.assert_state_with_diff(
13835 r#"
13836 use some::mod1;
13837 use some::mod2;
13838
13839 const A: u32 = 42;
13840 const B: u32 = 42;
13841 - const C: u32 = 42;
13842 + const C: u32 = 43
13843 + new_line
13844 + ˇ
13845 const D: u32 = 42;
13846
13847
13848 fn main() {
13849 println!("hello");
13850
13851 println!("world");
13852 }"#
13853 .unindent(),
13854 );
13855}
13856
13857async fn setup_indent_guides_editor(
13858 text: &str,
13859 cx: &mut gpui::TestAppContext,
13860) -> (BufferId, EditorTestContext) {
13861 init_test(cx, |_| {});
13862
13863 let mut cx = EditorTestContext::new(cx).await;
13864
13865 let buffer_id = cx.update_editor(|editor, window, cx| {
13866 editor.set_text(text, window, cx);
13867 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
13868
13869 buffer_ids[0]
13870 });
13871
13872 (buffer_id, cx)
13873}
13874
13875fn assert_indent_guides(
13876 range: Range<u32>,
13877 expected: Vec<IndentGuide>,
13878 active_indices: Option<Vec<usize>>,
13879 cx: &mut EditorTestContext,
13880) {
13881 let indent_guides = cx.update_editor(|editor, window, cx| {
13882 let snapshot = editor.snapshot(window, cx).display_snapshot;
13883 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
13884 editor,
13885 MultiBufferRow(range.start)..MultiBufferRow(range.end),
13886 true,
13887 &snapshot,
13888 cx,
13889 );
13890
13891 indent_guides.sort_by(|a, b| {
13892 a.depth.cmp(&b.depth).then(
13893 a.start_row
13894 .cmp(&b.start_row)
13895 .then(a.end_row.cmp(&b.end_row)),
13896 )
13897 });
13898 indent_guides
13899 });
13900
13901 if let Some(expected) = active_indices {
13902 let active_indices = cx.update_editor(|editor, window, cx| {
13903 let snapshot = editor.snapshot(window, cx).display_snapshot;
13904 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
13905 });
13906
13907 assert_eq!(
13908 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
13909 expected,
13910 "Active indent guide indices do not match"
13911 );
13912 }
13913
13914 assert_eq!(indent_guides, expected, "Indent guides do not match");
13915}
13916
13917fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
13918 IndentGuide {
13919 buffer_id,
13920 start_row: MultiBufferRow(start_row),
13921 end_row: MultiBufferRow(end_row),
13922 depth,
13923 tab_size: 4,
13924 settings: IndentGuideSettings {
13925 enabled: true,
13926 line_width: 1,
13927 active_line_width: 1,
13928 ..Default::default()
13929 },
13930 }
13931}
13932
13933#[gpui::test]
13934async fn test_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
13935 let (buffer_id, mut cx) = setup_indent_guides_editor(
13936 &"
13937 fn main() {
13938 let a = 1;
13939 }"
13940 .unindent(),
13941 cx,
13942 )
13943 .await;
13944
13945 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
13946}
13947
13948#[gpui::test]
13949async fn test_indent_guide_simple_block(cx: &mut gpui::TestAppContext) {
13950 let (buffer_id, mut cx) = setup_indent_guides_editor(
13951 &"
13952 fn main() {
13953 let a = 1;
13954 let b = 2;
13955 }"
13956 .unindent(),
13957 cx,
13958 )
13959 .await;
13960
13961 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
13962}
13963
13964#[gpui::test]
13965async fn test_indent_guide_nested(cx: &mut gpui::TestAppContext) {
13966 let (buffer_id, mut cx) = setup_indent_guides_editor(
13967 &"
13968 fn main() {
13969 let a = 1;
13970 if a == 3 {
13971 let b = 2;
13972 } else {
13973 let c = 3;
13974 }
13975 }"
13976 .unindent(),
13977 cx,
13978 )
13979 .await;
13980
13981 assert_indent_guides(
13982 0..8,
13983 vec![
13984 indent_guide(buffer_id, 1, 6, 0),
13985 indent_guide(buffer_id, 3, 3, 1),
13986 indent_guide(buffer_id, 5, 5, 1),
13987 ],
13988 None,
13989 &mut cx,
13990 );
13991}
13992
13993#[gpui::test]
13994async fn test_indent_guide_tab(cx: &mut gpui::TestAppContext) {
13995 let (buffer_id, mut cx) = setup_indent_guides_editor(
13996 &"
13997 fn main() {
13998 let a = 1;
13999 let b = 2;
14000 let c = 3;
14001 }"
14002 .unindent(),
14003 cx,
14004 )
14005 .await;
14006
14007 assert_indent_guides(
14008 0..5,
14009 vec![
14010 indent_guide(buffer_id, 1, 3, 0),
14011 indent_guide(buffer_id, 2, 2, 1),
14012 ],
14013 None,
14014 &mut cx,
14015 );
14016}
14017
14018#[gpui::test]
14019async fn test_indent_guide_continues_on_empty_line(cx: &mut gpui::TestAppContext) {
14020 let (buffer_id, mut cx) = setup_indent_guides_editor(
14021 &"
14022 fn main() {
14023 let a = 1;
14024
14025 let c = 3;
14026 }"
14027 .unindent(),
14028 cx,
14029 )
14030 .await;
14031
14032 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
14033}
14034
14035#[gpui::test]
14036async fn test_indent_guide_complex(cx: &mut gpui::TestAppContext) {
14037 let (buffer_id, mut cx) = setup_indent_guides_editor(
14038 &"
14039 fn main() {
14040 let a = 1;
14041
14042 let c = 3;
14043
14044 if a == 3 {
14045 let b = 2;
14046 } else {
14047 let c = 3;
14048 }
14049 }"
14050 .unindent(),
14051 cx,
14052 )
14053 .await;
14054
14055 assert_indent_guides(
14056 0..11,
14057 vec![
14058 indent_guide(buffer_id, 1, 9, 0),
14059 indent_guide(buffer_id, 6, 6, 1),
14060 indent_guide(buffer_id, 8, 8, 1),
14061 ],
14062 None,
14063 &mut cx,
14064 );
14065}
14066
14067#[gpui::test]
14068async fn test_indent_guide_starts_off_screen(cx: &mut gpui::TestAppContext) {
14069 let (buffer_id, mut cx) = setup_indent_guides_editor(
14070 &"
14071 fn main() {
14072 let a = 1;
14073
14074 let c = 3;
14075
14076 if a == 3 {
14077 let b = 2;
14078 } else {
14079 let c = 3;
14080 }
14081 }"
14082 .unindent(),
14083 cx,
14084 )
14085 .await;
14086
14087 assert_indent_guides(
14088 1..11,
14089 vec![
14090 indent_guide(buffer_id, 1, 9, 0),
14091 indent_guide(buffer_id, 6, 6, 1),
14092 indent_guide(buffer_id, 8, 8, 1),
14093 ],
14094 None,
14095 &mut cx,
14096 );
14097}
14098
14099#[gpui::test]
14100async fn test_indent_guide_ends_off_screen(cx: &mut gpui::TestAppContext) {
14101 let (buffer_id, mut cx) = setup_indent_guides_editor(
14102 &"
14103 fn main() {
14104 let a = 1;
14105
14106 let c = 3;
14107
14108 if a == 3 {
14109 let b = 2;
14110 } else {
14111 let c = 3;
14112 }
14113 }"
14114 .unindent(),
14115 cx,
14116 )
14117 .await;
14118
14119 assert_indent_guides(
14120 1..10,
14121 vec![
14122 indent_guide(buffer_id, 1, 9, 0),
14123 indent_guide(buffer_id, 6, 6, 1),
14124 indent_guide(buffer_id, 8, 8, 1),
14125 ],
14126 None,
14127 &mut cx,
14128 );
14129}
14130
14131#[gpui::test]
14132async fn test_indent_guide_without_brackets(cx: &mut gpui::TestAppContext) {
14133 let (buffer_id, mut cx) = setup_indent_guides_editor(
14134 &"
14135 block1
14136 block2
14137 block3
14138 block4
14139 block2
14140 block1
14141 block1"
14142 .unindent(),
14143 cx,
14144 )
14145 .await;
14146
14147 assert_indent_guides(
14148 1..10,
14149 vec![
14150 indent_guide(buffer_id, 1, 4, 0),
14151 indent_guide(buffer_id, 2, 3, 1),
14152 indent_guide(buffer_id, 3, 3, 2),
14153 ],
14154 None,
14155 &mut cx,
14156 );
14157}
14158
14159#[gpui::test]
14160async fn test_indent_guide_ends_before_empty_line(cx: &mut gpui::TestAppContext) {
14161 let (buffer_id, mut cx) = setup_indent_guides_editor(
14162 &"
14163 block1
14164 block2
14165 block3
14166
14167 block1
14168 block1"
14169 .unindent(),
14170 cx,
14171 )
14172 .await;
14173
14174 assert_indent_guides(
14175 0..6,
14176 vec![
14177 indent_guide(buffer_id, 1, 2, 0),
14178 indent_guide(buffer_id, 2, 2, 1),
14179 ],
14180 None,
14181 &mut cx,
14182 );
14183}
14184
14185#[gpui::test]
14186async fn test_indent_guide_continuing_off_screen(cx: &mut gpui::TestAppContext) {
14187 let (buffer_id, mut cx) = setup_indent_guides_editor(
14188 &"
14189 block1
14190
14191
14192
14193 block2
14194 "
14195 .unindent(),
14196 cx,
14197 )
14198 .await;
14199
14200 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
14201}
14202
14203#[gpui::test]
14204async fn test_indent_guide_tabs(cx: &mut gpui::TestAppContext) {
14205 let (buffer_id, mut cx) = setup_indent_guides_editor(
14206 &"
14207 def a:
14208 \tb = 3
14209 \tif True:
14210 \t\tc = 4
14211 \t\td = 5
14212 \tprint(b)
14213 "
14214 .unindent(),
14215 cx,
14216 )
14217 .await;
14218
14219 assert_indent_guides(
14220 0..6,
14221 vec![
14222 indent_guide(buffer_id, 1, 6, 0),
14223 indent_guide(buffer_id, 3, 4, 1),
14224 ],
14225 None,
14226 &mut cx,
14227 );
14228}
14229
14230#[gpui::test]
14231async fn test_active_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
14232 let (buffer_id, mut cx) = setup_indent_guides_editor(
14233 &"
14234 fn main() {
14235 let a = 1;
14236 }"
14237 .unindent(),
14238 cx,
14239 )
14240 .await;
14241
14242 cx.update_editor(|editor, window, cx| {
14243 editor.change_selections(None, window, cx, |s| {
14244 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14245 });
14246 });
14247
14248 assert_indent_guides(
14249 0..3,
14250 vec![indent_guide(buffer_id, 1, 1, 0)],
14251 Some(vec![0]),
14252 &mut cx,
14253 );
14254}
14255
14256#[gpui::test]
14257async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppContext) {
14258 let (buffer_id, mut cx) = setup_indent_guides_editor(
14259 &"
14260 fn main() {
14261 if 1 == 2 {
14262 let a = 1;
14263 }
14264 }"
14265 .unindent(),
14266 cx,
14267 )
14268 .await;
14269
14270 cx.update_editor(|editor, window, cx| {
14271 editor.change_selections(None, window, cx, |s| {
14272 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14273 });
14274 });
14275
14276 assert_indent_guides(
14277 0..4,
14278 vec![
14279 indent_guide(buffer_id, 1, 3, 0),
14280 indent_guide(buffer_id, 2, 2, 1),
14281 ],
14282 Some(vec![1]),
14283 &mut cx,
14284 );
14285
14286 cx.update_editor(|editor, window, cx| {
14287 editor.change_selections(None, window, cx, |s| {
14288 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
14289 });
14290 });
14291
14292 assert_indent_guides(
14293 0..4,
14294 vec![
14295 indent_guide(buffer_id, 1, 3, 0),
14296 indent_guide(buffer_id, 2, 2, 1),
14297 ],
14298 Some(vec![1]),
14299 &mut cx,
14300 );
14301
14302 cx.update_editor(|editor, window, cx| {
14303 editor.change_selections(None, window, cx, |s| {
14304 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
14305 });
14306 });
14307
14308 assert_indent_guides(
14309 0..4,
14310 vec![
14311 indent_guide(buffer_id, 1, 3, 0),
14312 indent_guide(buffer_id, 2, 2, 1),
14313 ],
14314 Some(vec![0]),
14315 &mut cx,
14316 );
14317}
14318
14319#[gpui::test]
14320async fn test_active_indent_guide_empty_line(cx: &mut gpui::TestAppContext) {
14321 let (buffer_id, mut cx) = setup_indent_guides_editor(
14322 &"
14323 fn main() {
14324 let a = 1;
14325
14326 let b = 2;
14327 }"
14328 .unindent(),
14329 cx,
14330 )
14331 .await;
14332
14333 cx.update_editor(|editor, window, cx| {
14334 editor.change_selections(None, window, cx, |s| {
14335 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
14336 });
14337 });
14338
14339 assert_indent_guides(
14340 0..5,
14341 vec![indent_guide(buffer_id, 1, 3, 0)],
14342 Some(vec![0]),
14343 &mut cx,
14344 );
14345}
14346
14347#[gpui::test]
14348async fn test_active_indent_guide_non_matching_indent(cx: &mut gpui::TestAppContext) {
14349 let (buffer_id, mut cx) = setup_indent_guides_editor(
14350 &"
14351 def m:
14352 a = 1
14353 pass"
14354 .unindent(),
14355 cx,
14356 )
14357 .await;
14358
14359 cx.update_editor(|editor, window, cx| {
14360 editor.change_selections(None, window, cx, |s| {
14361 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14362 });
14363 });
14364
14365 assert_indent_guides(
14366 0..3,
14367 vec![indent_guide(buffer_id, 1, 2, 0)],
14368 Some(vec![0]),
14369 &mut cx,
14370 );
14371}
14372
14373#[gpui::test]
14374async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut gpui::TestAppContext) {
14375 init_test(cx, |_| {});
14376 let mut cx = EditorTestContext::new(cx).await;
14377 let text = indoc! {
14378 "
14379 impl A {
14380 fn b() {
14381 0;
14382 3;
14383 5;
14384 6;
14385 7;
14386 }
14387 }
14388 "
14389 };
14390 let base_text = indoc! {
14391 "
14392 impl A {
14393 fn b() {
14394 0;
14395 1;
14396 2;
14397 3;
14398 4;
14399 }
14400 fn c() {
14401 5;
14402 6;
14403 7;
14404 }
14405 }
14406 "
14407 };
14408
14409 cx.update_editor(|editor, window, cx| {
14410 editor.set_text(text, window, cx);
14411
14412 editor.buffer().update(cx, |multibuffer, cx| {
14413 let buffer = multibuffer.as_singleton().unwrap();
14414 let change_set =
14415 cx.new(|cx| BufferChangeSet::new_with_base_text(base_text, &buffer, cx));
14416
14417 multibuffer.set_all_diff_hunks_expanded(cx);
14418 multibuffer.add_change_set(change_set, cx);
14419
14420 buffer.read(cx).remote_id()
14421 })
14422 });
14423 cx.run_until_parked();
14424
14425 cx.assert_state_with_diff(
14426 indoc! { "
14427 impl A {
14428 fn b() {
14429 0;
14430 - 1;
14431 - 2;
14432 3;
14433 - 4;
14434 - }
14435 - fn c() {
14436 5;
14437 6;
14438 7;
14439 }
14440 }
14441 ˇ"
14442 }
14443 .to_string(),
14444 );
14445
14446 let mut actual_guides = cx.update_editor(|editor, window, cx| {
14447 editor
14448 .snapshot(window, cx)
14449 .buffer_snapshot
14450 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
14451 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
14452 .collect::<Vec<_>>()
14453 });
14454 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
14455 assert_eq!(
14456 actual_guides,
14457 vec![
14458 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
14459 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
14460 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
14461 ]
14462 );
14463}
14464
14465#[gpui::test]
14466fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
14467 init_test(cx, |_| {});
14468
14469 let editor = cx.add_window(|window, cx| {
14470 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
14471 build_editor(buffer, window, cx)
14472 });
14473
14474 let render_args = Arc::new(Mutex::new(None));
14475 let snapshot = editor
14476 .update(cx, |editor, window, cx| {
14477 let snapshot = editor.buffer().read(cx).snapshot(cx);
14478 let range =
14479 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
14480
14481 struct RenderArgs {
14482 row: MultiBufferRow,
14483 folded: bool,
14484 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
14485 }
14486
14487 let crease = Crease::inline(
14488 range,
14489 FoldPlaceholder::test(),
14490 {
14491 let toggle_callback = render_args.clone();
14492 move |row, folded, callback, _window, _cx| {
14493 *toggle_callback.lock() = Some(RenderArgs {
14494 row,
14495 folded,
14496 callback,
14497 });
14498 div()
14499 }
14500 },
14501 |_row, _folded, _window, _cx| div(),
14502 );
14503
14504 editor.insert_creases(Some(crease), cx);
14505 let snapshot = editor.snapshot(window, cx);
14506 let _div = snapshot.render_crease_toggle(
14507 MultiBufferRow(1),
14508 false,
14509 cx.entity().clone(),
14510 window,
14511 cx,
14512 );
14513 snapshot
14514 })
14515 .unwrap();
14516
14517 let render_args = render_args.lock().take().unwrap();
14518 assert_eq!(render_args.row, MultiBufferRow(1));
14519 assert!(!render_args.folded);
14520 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
14521
14522 cx.update_window(*editor, |_, window, cx| {
14523 (render_args.callback)(true, window, cx)
14524 })
14525 .unwrap();
14526 let snapshot = editor
14527 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
14528 .unwrap();
14529 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
14530
14531 cx.update_window(*editor, |_, window, cx| {
14532 (render_args.callback)(false, window, cx)
14533 })
14534 .unwrap();
14535 let snapshot = editor
14536 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
14537 .unwrap();
14538 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
14539}
14540
14541#[gpui::test]
14542async fn test_input_text(cx: &mut gpui::TestAppContext) {
14543 init_test(cx, |_| {});
14544 let mut cx = EditorTestContext::new(cx).await;
14545
14546 cx.set_state(
14547 &r#"ˇone
14548 two
14549
14550 three
14551 fourˇ
14552 five
14553
14554 siˇx"#
14555 .unindent(),
14556 );
14557
14558 cx.dispatch_action(HandleInput(String::new()));
14559 cx.assert_editor_state(
14560 &r#"ˇone
14561 two
14562
14563 three
14564 fourˇ
14565 five
14566
14567 siˇx"#
14568 .unindent(),
14569 );
14570
14571 cx.dispatch_action(HandleInput("AAAA".to_string()));
14572 cx.assert_editor_state(
14573 &r#"AAAAˇone
14574 two
14575
14576 three
14577 fourAAAAˇ
14578 five
14579
14580 siAAAAˇx"#
14581 .unindent(),
14582 );
14583}
14584
14585#[gpui::test]
14586async fn test_scroll_cursor_center_top_bottom(cx: &mut gpui::TestAppContext) {
14587 init_test(cx, |_| {});
14588
14589 let mut cx = EditorTestContext::new(cx).await;
14590 cx.set_state(
14591 r#"let foo = 1;
14592let foo = 2;
14593let foo = 3;
14594let fooˇ = 4;
14595let foo = 5;
14596let foo = 6;
14597let foo = 7;
14598let foo = 8;
14599let foo = 9;
14600let foo = 10;
14601let foo = 11;
14602let foo = 12;
14603let foo = 13;
14604let foo = 14;
14605let foo = 15;"#,
14606 );
14607
14608 cx.update_editor(|e, window, cx| {
14609 assert_eq!(
14610 e.next_scroll_position,
14611 NextScrollCursorCenterTopBottom::Center,
14612 "Default next scroll direction is center",
14613 );
14614
14615 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14616 assert_eq!(
14617 e.next_scroll_position,
14618 NextScrollCursorCenterTopBottom::Top,
14619 "After center, next scroll direction should be top",
14620 );
14621
14622 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14623 assert_eq!(
14624 e.next_scroll_position,
14625 NextScrollCursorCenterTopBottom::Bottom,
14626 "After top, next scroll direction should be bottom",
14627 );
14628
14629 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14630 assert_eq!(
14631 e.next_scroll_position,
14632 NextScrollCursorCenterTopBottom::Center,
14633 "After bottom, scrolling should start over",
14634 );
14635
14636 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14637 assert_eq!(
14638 e.next_scroll_position,
14639 NextScrollCursorCenterTopBottom::Top,
14640 "Scrolling continues if retriggered fast enough"
14641 );
14642 });
14643
14644 cx.executor()
14645 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
14646 cx.executor().run_until_parked();
14647 cx.update_editor(|e, _, _| {
14648 assert_eq!(
14649 e.next_scroll_position,
14650 NextScrollCursorCenterTopBottom::Center,
14651 "If scrolling is not triggered fast enough, it should reset"
14652 );
14653 });
14654}
14655
14656#[gpui::test]
14657async fn test_goto_definition_with_find_all_references_fallback(cx: &mut gpui::TestAppContext) {
14658 init_test(cx, |_| {});
14659 let mut cx = EditorLspTestContext::new_rust(
14660 lsp::ServerCapabilities {
14661 definition_provider: Some(lsp::OneOf::Left(true)),
14662 references_provider: Some(lsp::OneOf::Left(true)),
14663 ..lsp::ServerCapabilities::default()
14664 },
14665 cx,
14666 )
14667 .await;
14668
14669 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
14670 let go_to_definition = cx.lsp.handle_request::<lsp::request::GotoDefinition, _, _>(
14671 move |params, _| async move {
14672 if empty_go_to_definition {
14673 Ok(None)
14674 } else {
14675 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
14676 uri: params.text_document_position_params.text_document.uri,
14677 range: lsp::Range::new(lsp::Position::new(4, 3), lsp::Position::new(4, 6)),
14678 })))
14679 }
14680 },
14681 );
14682 let references =
14683 cx.lsp
14684 .handle_request::<lsp::request::References, _, _>(move |params, _| async move {
14685 Ok(Some(vec![lsp::Location {
14686 uri: params.text_document_position.text_document.uri,
14687 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
14688 }]))
14689 });
14690 (go_to_definition, references)
14691 };
14692
14693 cx.set_state(
14694 &r#"fn one() {
14695 let mut a = ˇtwo();
14696 }
14697
14698 fn two() {}"#
14699 .unindent(),
14700 );
14701 set_up_lsp_handlers(false, &mut cx);
14702 let navigated = cx
14703 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
14704 .await
14705 .expect("Failed to navigate to definition");
14706 assert_eq!(
14707 navigated,
14708 Navigated::Yes,
14709 "Should have navigated to definition from the GetDefinition response"
14710 );
14711 cx.assert_editor_state(
14712 &r#"fn one() {
14713 let mut a = two();
14714 }
14715
14716 fn «twoˇ»() {}"#
14717 .unindent(),
14718 );
14719
14720 let editors = cx.update_workspace(|workspace, _, cx| {
14721 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
14722 });
14723 cx.update_editor(|_, _, test_editor_cx| {
14724 assert_eq!(
14725 editors.len(),
14726 1,
14727 "Initially, only one, test, editor should be open in the workspace"
14728 );
14729 assert_eq!(
14730 test_editor_cx.entity(),
14731 editors.last().expect("Asserted len is 1").clone()
14732 );
14733 });
14734
14735 set_up_lsp_handlers(true, &mut cx);
14736 let navigated = cx
14737 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
14738 .await
14739 .expect("Failed to navigate to lookup references");
14740 assert_eq!(
14741 navigated,
14742 Navigated::Yes,
14743 "Should have navigated to references as a fallback after empty GoToDefinition response"
14744 );
14745 // We should not change the selections in the existing file,
14746 // if opening another milti buffer with the references
14747 cx.assert_editor_state(
14748 &r#"fn one() {
14749 let mut a = two();
14750 }
14751
14752 fn «twoˇ»() {}"#
14753 .unindent(),
14754 );
14755 let editors = cx.update_workspace(|workspace, _, cx| {
14756 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
14757 });
14758 cx.update_editor(|_, _, test_editor_cx| {
14759 assert_eq!(
14760 editors.len(),
14761 2,
14762 "After falling back to references search, we open a new editor with the results"
14763 );
14764 let references_fallback_text = editors
14765 .into_iter()
14766 .find(|new_editor| *new_editor != test_editor_cx.entity())
14767 .expect("Should have one non-test editor now")
14768 .read(test_editor_cx)
14769 .text(test_editor_cx);
14770 assert_eq!(
14771 references_fallback_text, "fn one() {\n let mut a = two();\n}",
14772 "Should use the range from the references response and not the GoToDefinition one"
14773 );
14774 });
14775}
14776
14777#[gpui::test]
14778async fn test_find_enclosing_node_with_task(cx: &mut gpui::TestAppContext) {
14779 init_test(cx, |_| {});
14780
14781 let language = Arc::new(Language::new(
14782 LanguageConfig::default(),
14783 Some(tree_sitter_rust::LANGUAGE.into()),
14784 ));
14785
14786 let text = r#"
14787 #[cfg(test)]
14788 mod tests() {
14789 #[test]
14790 fn runnable_1() {
14791 let a = 1;
14792 }
14793
14794 #[test]
14795 fn runnable_2() {
14796 let a = 1;
14797 let b = 2;
14798 }
14799 }
14800 "#
14801 .unindent();
14802
14803 let fs = FakeFs::new(cx.executor());
14804 fs.insert_file("/file.rs", Default::default()).await;
14805
14806 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14807 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14808 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14809 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14810 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
14811
14812 let editor = cx.new_window_entity(|window, cx| {
14813 Editor::new(
14814 EditorMode::Full,
14815 multi_buffer,
14816 Some(project.clone()),
14817 true,
14818 window,
14819 cx,
14820 )
14821 });
14822
14823 editor.update_in(cx, |editor, window, cx| {
14824 editor.tasks.insert(
14825 (buffer.read(cx).remote_id(), 3),
14826 RunnableTasks {
14827 templates: vec![],
14828 offset: MultiBufferOffset(43),
14829 column: 0,
14830 extra_variables: HashMap::default(),
14831 context_range: BufferOffset(43)..BufferOffset(85),
14832 },
14833 );
14834 editor.tasks.insert(
14835 (buffer.read(cx).remote_id(), 8),
14836 RunnableTasks {
14837 templates: vec![],
14838 offset: MultiBufferOffset(86),
14839 column: 0,
14840 extra_variables: HashMap::default(),
14841 context_range: BufferOffset(86)..BufferOffset(191),
14842 },
14843 );
14844
14845 // Test finding task when cursor is inside function body
14846 editor.change_selections(None, window, cx, |s| {
14847 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
14848 });
14849 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
14850 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
14851
14852 // Test finding task when cursor is on function name
14853 editor.change_selections(None, window, cx, |s| {
14854 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
14855 });
14856 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
14857 assert_eq!(row, 8, "Should find task when cursor is on function name");
14858 });
14859}
14860
14861#[gpui::test]
14862async fn test_multi_buffer_folding(cx: &mut gpui::TestAppContext) {
14863 init_test(cx, |_| {});
14864
14865 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
14866 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
14867 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
14868
14869 let fs = FakeFs::new(cx.executor());
14870 fs.insert_tree(
14871 "/a",
14872 json!({
14873 "first.rs": sample_text_1,
14874 "second.rs": sample_text_2,
14875 "third.rs": sample_text_3,
14876 }),
14877 )
14878 .await;
14879 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14880 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14881 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14882 let worktree = project.update(cx, |project, cx| {
14883 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
14884 assert_eq!(worktrees.len(), 1);
14885 worktrees.pop().unwrap()
14886 });
14887 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
14888
14889 let buffer_1 = project
14890 .update(cx, |project, cx| {
14891 project.open_buffer((worktree_id, "first.rs"), cx)
14892 })
14893 .await
14894 .unwrap();
14895 let buffer_2 = project
14896 .update(cx, |project, cx| {
14897 project.open_buffer((worktree_id, "second.rs"), cx)
14898 })
14899 .await
14900 .unwrap();
14901 let buffer_3 = project
14902 .update(cx, |project, cx| {
14903 project.open_buffer((worktree_id, "third.rs"), cx)
14904 })
14905 .await
14906 .unwrap();
14907
14908 let multi_buffer = cx.new(|cx| {
14909 let mut multi_buffer = MultiBuffer::new(ReadWrite);
14910 multi_buffer.push_excerpts(
14911 buffer_1.clone(),
14912 [
14913 ExcerptRange {
14914 context: Point::new(0, 0)..Point::new(3, 0),
14915 primary: None,
14916 },
14917 ExcerptRange {
14918 context: Point::new(5, 0)..Point::new(7, 0),
14919 primary: None,
14920 },
14921 ExcerptRange {
14922 context: Point::new(9, 0)..Point::new(10, 4),
14923 primary: None,
14924 },
14925 ],
14926 cx,
14927 );
14928 multi_buffer.push_excerpts(
14929 buffer_2.clone(),
14930 [
14931 ExcerptRange {
14932 context: Point::new(0, 0)..Point::new(3, 0),
14933 primary: None,
14934 },
14935 ExcerptRange {
14936 context: Point::new(5, 0)..Point::new(7, 0),
14937 primary: None,
14938 },
14939 ExcerptRange {
14940 context: Point::new(9, 0)..Point::new(10, 4),
14941 primary: None,
14942 },
14943 ],
14944 cx,
14945 );
14946 multi_buffer.push_excerpts(
14947 buffer_3.clone(),
14948 [
14949 ExcerptRange {
14950 context: Point::new(0, 0)..Point::new(3, 0),
14951 primary: None,
14952 },
14953 ExcerptRange {
14954 context: Point::new(5, 0)..Point::new(7, 0),
14955 primary: None,
14956 },
14957 ExcerptRange {
14958 context: Point::new(9, 0)..Point::new(10, 4),
14959 primary: None,
14960 },
14961 ],
14962 cx,
14963 );
14964 multi_buffer
14965 });
14966 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
14967 Editor::new(
14968 EditorMode::Full,
14969 multi_buffer,
14970 Some(project.clone()),
14971 true,
14972 window,
14973 cx,
14974 )
14975 });
14976
14977 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";
14978 assert_eq!(
14979 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
14980 full_text,
14981 );
14982
14983 multi_buffer_editor.update(cx, |editor, cx| {
14984 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
14985 });
14986 assert_eq!(
14987 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
14988 "\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",
14989 "After folding the first buffer, its text should not be displayed"
14990 );
14991
14992 multi_buffer_editor.update(cx, |editor, cx| {
14993 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
14994 });
14995 assert_eq!(
14996 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
14997 "\n\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
14998 "After folding the second buffer, its text should not be displayed"
14999 );
15000
15001 multi_buffer_editor.update(cx, |editor, cx| {
15002 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
15003 });
15004 assert_eq!(
15005 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15006 "\n\n\n\n\n",
15007 "After folding the third buffer, its text should not be displayed"
15008 );
15009
15010 // Emulate selection inside the fold logic, that should work
15011 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15012 editor
15013 .snapshot(window, cx)
15014 .next_line_boundary(Point::new(0, 4));
15015 });
15016
15017 multi_buffer_editor.update(cx, |editor, cx| {
15018 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
15019 });
15020 assert_eq!(
15021 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15022 "\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
15023 "After unfolding the second buffer, its text should be displayed"
15024 );
15025
15026 multi_buffer_editor.update(cx, |editor, cx| {
15027 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
15028 });
15029 assert_eq!(
15030 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15031 "\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",
15032 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
15033 );
15034
15035 multi_buffer_editor.update(cx, |editor, cx| {
15036 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
15037 });
15038 assert_eq!(
15039 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15040 full_text,
15041 "After unfolding the all buffers, all original text should be displayed"
15042 );
15043}
15044
15045#[gpui::test]
15046async fn test_multi_buffer_single_excerpts_folding(cx: &mut gpui::TestAppContext) {
15047 init_test(cx, |_| {});
15048
15049 let sample_text_1 = "1111\n2222\n3333".to_string();
15050 let sample_text_2 = "4444\n5555\n6666".to_string();
15051 let sample_text_3 = "7777\n8888\n9999".to_string();
15052
15053 let fs = FakeFs::new(cx.executor());
15054 fs.insert_tree(
15055 "/a",
15056 json!({
15057 "first.rs": sample_text_1,
15058 "second.rs": sample_text_2,
15059 "third.rs": sample_text_3,
15060 }),
15061 )
15062 .await;
15063 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15064 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15065 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15066 let worktree = project.update(cx, |project, cx| {
15067 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15068 assert_eq!(worktrees.len(), 1);
15069 worktrees.pop().unwrap()
15070 });
15071 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15072
15073 let buffer_1 = project
15074 .update(cx, |project, cx| {
15075 project.open_buffer((worktree_id, "first.rs"), cx)
15076 })
15077 .await
15078 .unwrap();
15079 let buffer_2 = project
15080 .update(cx, |project, cx| {
15081 project.open_buffer((worktree_id, "second.rs"), cx)
15082 })
15083 .await
15084 .unwrap();
15085 let buffer_3 = project
15086 .update(cx, |project, cx| {
15087 project.open_buffer((worktree_id, "third.rs"), cx)
15088 })
15089 .await
15090 .unwrap();
15091
15092 let multi_buffer = cx.new(|cx| {
15093 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15094 multi_buffer.push_excerpts(
15095 buffer_1.clone(),
15096 [ExcerptRange {
15097 context: Point::new(0, 0)..Point::new(3, 0),
15098 primary: None,
15099 }],
15100 cx,
15101 );
15102 multi_buffer.push_excerpts(
15103 buffer_2.clone(),
15104 [ExcerptRange {
15105 context: Point::new(0, 0)..Point::new(3, 0),
15106 primary: None,
15107 }],
15108 cx,
15109 );
15110 multi_buffer.push_excerpts(
15111 buffer_3.clone(),
15112 [ExcerptRange {
15113 context: Point::new(0, 0)..Point::new(3, 0),
15114 primary: None,
15115 }],
15116 cx,
15117 );
15118 multi_buffer
15119 });
15120
15121 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15122 Editor::new(
15123 EditorMode::Full,
15124 multi_buffer,
15125 Some(project.clone()),
15126 true,
15127 window,
15128 cx,
15129 )
15130 });
15131
15132 let full_text = "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n";
15133 assert_eq!(
15134 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15135 full_text,
15136 );
15137
15138 multi_buffer_editor.update(cx, |editor, cx| {
15139 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
15140 });
15141 assert_eq!(
15142 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15143 "\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n",
15144 "After folding the first buffer, its text should not be displayed"
15145 );
15146
15147 multi_buffer_editor.update(cx, |editor, cx| {
15148 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
15149 });
15150
15151 assert_eq!(
15152 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15153 "\n\n\n\n\n\n\n7777\n8888\n9999\n",
15154 "After folding the second buffer, its text should not be displayed"
15155 );
15156
15157 multi_buffer_editor.update(cx, |editor, cx| {
15158 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
15159 });
15160 assert_eq!(
15161 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15162 "\n\n\n\n\n",
15163 "After folding the third buffer, its text should not be displayed"
15164 );
15165
15166 multi_buffer_editor.update(cx, |editor, cx| {
15167 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
15168 });
15169 assert_eq!(
15170 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15171 "\n\n\n\n\n4444\n5555\n6666\n\n\n",
15172 "After unfolding the second buffer, its text should be displayed"
15173 );
15174
15175 multi_buffer_editor.update(cx, |editor, cx| {
15176 editor.unfold_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\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n",
15181 "After unfolding the first buffer, its text should be displayed"
15182 );
15183
15184 multi_buffer_editor.update(cx, |editor, cx| {
15185 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
15186 });
15187 assert_eq!(
15188 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15189 full_text,
15190 "After unfolding all buffers, all original text should be displayed"
15191 );
15192}
15193
15194#[gpui::test]
15195async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppContext) {
15196 init_test(cx, |_| {});
15197
15198 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
15199
15200 let fs = FakeFs::new(cx.executor());
15201 fs.insert_tree(
15202 "/a",
15203 json!({
15204 "main.rs": sample_text,
15205 }),
15206 )
15207 .await;
15208 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15209 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15210 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15211 let worktree = project.update(cx, |project, cx| {
15212 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15213 assert_eq!(worktrees.len(), 1);
15214 worktrees.pop().unwrap()
15215 });
15216 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15217
15218 let buffer_1 = project
15219 .update(cx, |project, cx| {
15220 project.open_buffer((worktree_id, "main.rs"), cx)
15221 })
15222 .await
15223 .unwrap();
15224
15225 let multi_buffer = cx.new(|cx| {
15226 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15227 multi_buffer.push_excerpts(
15228 buffer_1.clone(),
15229 [ExcerptRange {
15230 context: Point::new(0, 0)
15231 ..Point::new(
15232 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
15233 0,
15234 ),
15235 primary: None,
15236 }],
15237 cx,
15238 );
15239 multi_buffer
15240 });
15241 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15242 Editor::new(
15243 EditorMode::Full,
15244 multi_buffer,
15245 Some(project.clone()),
15246 true,
15247 window,
15248 cx,
15249 )
15250 });
15251
15252 let selection_range = Point::new(1, 0)..Point::new(2, 0);
15253 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15254 enum TestHighlight {}
15255 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
15256 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
15257 editor.highlight_text::<TestHighlight>(
15258 vec![highlight_range.clone()],
15259 HighlightStyle::color(Hsla::green()),
15260 cx,
15261 );
15262 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
15263 });
15264
15265 let full_text = format!("\n\n\n{sample_text}\n");
15266 assert_eq!(
15267 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15268 full_text,
15269 );
15270}
15271
15272#[gpui::test]
15273async fn test_inline_completion_text(cx: &mut TestAppContext) {
15274 init_test(cx, |_| {});
15275
15276 // Simple insertion
15277 assert_highlighted_edits(
15278 "Hello, world!",
15279 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
15280 true,
15281 cx,
15282 |highlighted_edits, cx| {
15283 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
15284 assert_eq!(highlighted_edits.highlights.len(), 1);
15285 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
15286 assert_eq!(
15287 highlighted_edits.highlights[0].1.background_color,
15288 Some(cx.theme().status().created_background)
15289 );
15290 },
15291 )
15292 .await;
15293
15294 // Replacement
15295 assert_highlighted_edits(
15296 "This is a test.",
15297 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
15298 false,
15299 cx,
15300 |highlighted_edits, cx| {
15301 assert_eq!(highlighted_edits.text, "That is a test.");
15302 assert_eq!(highlighted_edits.highlights.len(), 1);
15303 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
15304 assert_eq!(
15305 highlighted_edits.highlights[0].1.background_color,
15306 Some(cx.theme().status().created_background)
15307 );
15308 },
15309 )
15310 .await;
15311
15312 // Multiple edits
15313 assert_highlighted_edits(
15314 "Hello, world!",
15315 vec![
15316 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
15317 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
15318 ],
15319 false,
15320 cx,
15321 |highlighted_edits, cx| {
15322 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
15323 assert_eq!(highlighted_edits.highlights.len(), 2);
15324 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
15325 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
15326 assert_eq!(
15327 highlighted_edits.highlights[0].1.background_color,
15328 Some(cx.theme().status().created_background)
15329 );
15330 assert_eq!(
15331 highlighted_edits.highlights[1].1.background_color,
15332 Some(cx.theme().status().created_background)
15333 );
15334 },
15335 )
15336 .await;
15337
15338 // Multiple lines with edits
15339 assert_highlighted_edits(
15340 "First line\nSecond line\nThird line\nFourth line",
15341 vec![
15342 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
15343 (
15344 Point::new(2, 0)..Point::new(2, 10),
15345 "New third line".to_string(),
15346 ),
15347 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
15348 ],
15349 false,
15350 cx,
15351 |highlighted_edits, cx| {
15352 assert_eq!(
15353 highlighted_edits.text,
15354 "Second modified\nNew third line\nFourth updated line"
15355 );
15356 assert_eq!(highlighted_edits.highlights.len(), 3);
15357 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
15358 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
15359 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
15360 for highlight in &highlighted_edits.highlights {
15361 assert_eq!(
15362 highlight.1.background_color,
15363 Some(cx.theme().status().created_background)
15364 );
15365 }
15366 },
15367 )
15368 .await;
15369}
15370
15371#[gpui::test]
15372async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
15373 init_test(cx, |_| {});
15374
15375 // Deletion
15376 assert_highlighted_edits(
15377 "Hello, world!",
15378 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
15379 true,
15380 cx,
15381 |highlighted_edits, cx| {
15382 assert_eq!(highlighted_edits.text, "Hello, world!");
15383 assert_eq!(highlighted_edits.highlights.len(), 1);
15384 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
15385 assert_eq!(
15386 highlighted_edits.highlights[0].1.background_color,
15387 Some(cx.theme().status().deleted_background)
15388 );
15389 },
15390 )
15391 .await;
15392
15393 // Insertion
15394 assert_highlighted_edits(
15395 "Hello, world!",
15396 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
15397 true,
15398 cx,
15399 |highlighted_edits, cx| {
15400 assert_eq!(highlighted_edits.highlights.len(), 1);
15401 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
15402 assert_eq!(
15403 highlighted_edits.highlights[0].1.background_color,
15404 Some(cx.theme().status().created_background)
15405 );
15406 },
15407 )
15408 .await;
15409}
15410
15411async fn assert_highlighted_edits(
15412 text: &str,
15413 edits: Vec<(Range<Point>, String)>,
15414 include_deletions: bool,
15415 cx: &mut TestAppContext,
15416 assertion_fn: impl Fn(HighlightedText, &App),
15417) {
15418 let window = cx.add_window(|window, cx| {
15419 let buffer = MultiBuffer::build_simple(text, cx);
15420 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
15421 });
15422 let cx = &mut VisualTestContext::from_window(*window, cx);
15423
15424 let (buffer, snapshot) = window
15425 .update(cx, |editor, _window, cx| {
15426 (
15427 editor.buffer().clone(),
15428 editor.buffer().read(cx).snapshot(cx),
15429 )
15430 })
15431 .unwrap();
15432
15433 let edits = edits
15434 .into_iter()
15435 .map(|(range, edit)| {
15436 (
15437 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
15438 edit,
15439 )
15440 })
15441 .collect::<Vec<_>>();
15442
15443 let text_anchor_edits = edits
15444 .clone()
15445 .into_iter()
15446 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
15447 .collect::<Vec<_>>();
15448
15449 let edit_preview = window
15450 .update(cx, |_, _window, cx| {
15451 buffer
15452 .read(cx)
15453 .as_singleton()
15454 .unwrap()
15455 .read(cx)
15456 .preview_edits(text_anchor_edits.into(), cx)
15457 })
15458 .unwrap()
15459 .await;
15460
15461 cx.update(|_window, cx| {
15462 let highlighted_edits = inline_completion_edit_text(
15463 &snapshot.as_singleton().unwrap().2,
15464 &edits,
15465 &edit_preview,
15466 include_deletions,
15467 cx,
15468 );
15469 assertion_fn(highlighted_edits, cx)
15470 });
15471}
15472
15473#[gpui::test]
15474async fn test_rename_with_duplicate_edits(cx: &mut gpui::TestAppContext) {
15475 init_test(cx, |_| {});
15476 let capabilities = lsp::ServerCapabilities {
15477 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
15478 prepare_provider: Some(true),
15479 work_done_progress_options: Default::default(),
15480 })),
15481 ..Default::default()
15482 };
15483 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
15484
15485 cx.set_state(indoc! {"
15486 struct Fˇoo {}
15487 "});
15488
15489 cx.update_editor(|editor, _, cx| {
15490 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
15491 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
15492 editor.highlight_background::<DocumentHighlightRead>(
15493 &[highlight_range],
15494 |c| c.editor_document_highlight_read_background,
15495 cx,
15496 );
15497 });
15498
15499 let mut prepare_rename_handler =
15500 cx.handle_request::<lsp::request::PrepareRenameRequest, _, _>(move |_, _, _| async move {
15501 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
15502 start: lsp::Position {
15503 line: 0,
15504 character: 7,
15505 },
15506 end: lsp::Position {
15507 line: 0,
15508 character: 10,
15509 },
15510 })))
15511 });
15512 let prepare_rename_task = cx
15513 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
15514 .expect("Prepare rename was not started");
15515 prepare_rename_handler.next().await.unwrap();
15516 prepare_rename_task.await.expect("Prepare rename failed");
15517
15518 let mut rename_handler =
15519 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
15520 let edit = lsp::TextEdit {
15521 range: lsp::Range {
15522 start: lsp::Position {
15523 line: 0,
15524 character: 7,
15525 },
15526 end: lsp::Position {
15527 line: 0,
15528 character: 10,
15529 },
15530 },
15531 new_text: "FooRenamed".to_string(),
15532 };
15533 Ok(Some(lsp::WorkspaceEdit::new(
15534 // Specify the same edit twice
15535 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
15536 )))
15537 });
15538 let rename_task = cx
15539 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
15540 .expect("Confirm rename was not started");
15541 rename_handler.next().await.unwrap();
15542 rename_task.await.expect("Confirm rename failed");
15543 cx.run_until_parked();
15544
15545 // Despite two edits, only one is actually applied as those are identical
15546 cx.assert_editor_state(indoc! {"
15547 struct FooRenamedˇ {}
15548 "});
15549}
15550
15551#[gpui::test]
15552async fn test_rename_without_prepare(cx: &mut gpui::TestAppContext) {
15553 init_test(cx, |_| {});
15554 // These capabilities indicate that the server does not support prepare rename.
15555 let capabilities = lsp::ServerCapabilities {
15556 rename_provider: Some(lsp::OneOf::Left(true)),
15557 ..Default::default()
15558 };
15559 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
15560
15561 cx.set_state(indoc! {"
15562 struct Fˇoo {}
15563 "});
15564
15565 cx.update_editor(|editor, _window, cx| {
15566 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
15567 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
15568 editor.highlight_background::<DocumentHighlightRead>(
15569 &[highlight_range],
15570 |c| c.editor_document_highlight_read_background,
15571 cx,
15572 );
15573 });
15574
15575 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
15576 .expect("Prepare rename was not started")
15577 .await
15578 .expect("Prepare rename failed");
15579
15580 let mut rename_handler =
15581 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
15582 let edit = lsp::TextEdit {
15583 range: lsp::Range {
15584 start: lsp::Position {
15585 line: 0,
15586 character: 7,
15587 },
15588 end: lsp::Position {
15589 line: 0,
15590 character: 10,
15591 },
15592 },
15593 new_text: "FooRenamed".to_string(),
15594 };
15595 Ok(Some(lsp::WorkspaceEdit::new(
15596 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
15597 )))
15598 });
15599 let rename_task = cx
15600 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
15601 .expect("Confirm rename was not started");
15602 rename_handler.next().await.unwrap();
15603 rename_task.await.expect("Confirm rename failed");
15604 cx.run_until_parked();
15605
15606 // Correct range is renamed, as `surrounding_word` is used to find it.
15607 cx.assert_editor_state(indoc! {"
15608 struct FooRenamedˇ {}
15609 "});
15610}
15611
15612fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
15613 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
15614 point..point
15615}
15616
15617fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
15618 let (text, ranges) = marked_text_ranges(marked_text, true);
15619 assert_eq!(editor.text(cx), text);
15620 assert_eq!(
15621 editor.selections.ranges(cx),
15622 ranges,
15623 "Assert selections are {}",
15624 marked_text
15625 );
15626}
15627
15628pub fn handle_signature_help_request(
15629 cx: &mut EditorLspTestContext,
15630 mocked_response: lsp::SignatureHelp,
15631) -> impl Future<Output = ()> {
15632 let mut request =
15633 cx.handle_request::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
15634 let mocked_response = mocked_response.clone();
15635 async move { Ok(Some(mocked_response)) }
15636 });
15637
15638 async move {
15639 request.next().await;
15640 }
15641}
15642
15643/// Handle completion request passing a marked string specifying where the completion
15644/// should be triggered from using '|' character, what range should be replaced, and what completions
15645/// should be returned using '<' and '>' to delimit the range
15646pub fn handle_completion_request(
15647 cx: &mut EditorLspTestContext,
15648 marked_string: &str,
15649 completions: Vec<&'static str>,
15650 counter: Arc<AtomicUsize>,
15651) -> impl Future<Output = ()> {
15652 let complete_from_marker: TextRangeMarker = '|'.into();
15653 let replace_range_marker: TextRangeMarker = ('<', '>').into();
15654 let (_, mut marked_ranges) = marked_text_ranges_by(
15655 marked_string,
15656 vec![complete_from_marker.clone(), replace_range_marker.clone()],
15657 );
15658
15659 let complete_from_position =
15660 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
15661 let replace_range =
15662 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
15663
15664 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
15665 let completions = completions.clone();
15666 counter.fetch_add(1, atomic::Ordering::Release);
15667 async move {
15668 assert_eq!(params.text_document_position.text_document.uri, url.clone());
15669 assert_eq!(
15670 params.text_document_position.position,
15671 complete_from_position
15672 );
15673 Ok(Some(lsp::CompletionResponse::Array(
15674 completions
15675 .iter()
15676 .map(|completion_text| lsp::CompletionItem {
15677 label: completion_text.to_string(),
15678 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15679 range: replace_range,
15680 new_text: completion_text.to_string(),
15681 })),
15682 ..Default::default()
15683 })
15684 .collect(),
15685 )))
15686 }
15687 });
15688
15689 async move {
15690 request.next().await;
15691 }
15692}
15693
15694fn handle_resolve_completion_request(
15695 cx: &mut EditorLspTestContext,
15696 edits: Option<Vec<(&'static str, &'static str)>>,
15697) -> impl Future<Output = ()> {
15698 let edits = edits.map(|edits| {
15699 edits
15700 .iter()
15701 .map(|(marked_string, new_text)| {
15702 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
15703 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
15704 lsp::TextEdit::new(replace_range, new_text.to_string())
15705 })
15706 .collect::<Vec<_>>()
15707 });
15708
15709 let mut request =
15710 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15711 let edits = edits.clone();
15712 async move {
15713 Ok(lsp::CompletionItem {
15714 additional_text_edits: edits,
15715 ..Default::default()
15716 })
15717 }
15718 });
15719
15720 async move {
15721 request.next().await;
15722 }
15723}
15724
15725pub(crate) fn update_test_language_settings(
15726 cx: &mut TestAppContext,
15727 f: impl Fn(&mut AllLanguageSettingsContent),
15728) {
15729 cx.update(|cx| {
15730 SettingsStore::update_global(cx, |store, cx| {
15731 store.update_user_settings::<AllLanguageSettings>(cx, f);
15732 });
15733 });
15734}
15735
15736pub(crate) fn update_test_project_settings(
15737 cx: &mut TestAppContext,
15738 f: impl Fn(&mut ProjectSettings),
15739) {
15740 cx.update(|cx| {
15741 SettingsStore::update_global(cx, |store, cx| {
15742 store.update_user_settings::<ProjectSettings>(cx, f);
15743 });
15744 });
15745}
15746
15747pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
15748 cx.update(|cx| {
15749 assets::Assets.load_test_fonts(cx);
15750 let store = SettingsStore::test(cx);
15751 cx.set_global(store);
15752 theme::init(theme::LoadThemes::JustBase, cx);
15753 release_channel::init(SemanticVersion::default(), cx);
15754 client::init_settings(cx);
15755 language::init(cx);
15756 Project::init_settings(cx);
15757 workspace::init_settings(cx);
15758 crate::init(cx);
15759 });
15760
15761 update_test_language_settings(cx, f);
15762}
15763
15764#[track_caller]
15765fn assert_hunk_revert(
15766 not_reverted_text_with_selections: &str,
15767 expected_hunk_statuses_before: Vec<DiffHunkStatus>,
15768 expected_reverted_text_with_selections: &str,
15769 base_text: &str,
15770 cx: &mut EditorLspTestContext,
15771) {
15772 cx.set_state(not_reverted_text_with_selections);
15773 cx.set_diff_base(base_text);
15774 cx.executor().run_until_parked();
15775
15776 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
15777 let snapshot = editor.snapshot(window, cx);
15778 let reverted_hunk_statuses = snapshot
15779 .buffer_snapshot
15780 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
15781 .map(|hunk| hunk.status())
15782 .collect::<Vec<_>>();
15783
15784 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
15785 reverted_hunk_statuses
15786 });
15787 cx.executor().run_until_parked();
15788 cx.assert_editor_state(expected_reverted_text_with_selections);
15789 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
15790}