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 model = cx.entity().clone();
66 cx.subscribe_in(
67 &model,
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 unstaged comment
5623
5624 fn b() {
5625 c();
5626 }
5627
5628 // this is another unstaged 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 unstaged comment
5672
5673 fn b() {
5674 c();
5675 }
5676
5677 - // this is another unstaged 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 unstaged comment
5695
5696 fn b() {
5697 ⋯
5698 }
5699
5700 // this is another unstaged 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_model = leader.root(cx).unwrap();
10200 let follower_model = 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_model,
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_model,
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_model = workspace.root(cx).unwrap();
10388 let follower_1 = cx
10389 .update_window(*workspace.deref(), |_, window, cx| {
10390 Editor::from_state_proto(
10391 workspace_model,
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_model = workspace.root(cx).unwrap();
10490 let follower_2 = cx
10491 .update_window(*workspace.deref(), |_, window, cx| {
10492 Editor::from_state_proto(
10493 workspace_model,
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 = cx
12435 .new(|cx| BufferChangeSet::new_with_base_text(diff_base.to_string(), &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 = cx.new(|cx| {
13129 BufferChangeSet::new_with_base_text(diff_base.to_string(), &buffer, cx)
13130 });
13131 editor
13132 .buffer
13133 .update(cx, |buffer, cx| buffer.add_change_set(change_set, cx));
13134 }
13135 })
13136 .unwrap();
13137
13138 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13139 cx.run_until_parked();
13140
13141 cx.assert_editor_state(
13142 &"
13143 ˇaaa
13144 ccc
13145 ddd
13146
13147 ggg
13148 hhh
13149
13150
13151 lll
13152 mmm
13153 NNN
13154
13155 qqq
13156 rrr
13157
13158 uuu
13159 111
13160 222
13161 333
13162
13163 666
13164 777
13165
13166 000
13167 !!!"
13168 .unindent(),
13169 );
13170
13171 cx.update_editor(|editor, window, cx| {
13172 editor.select_all(&SelectAll, window, cx);
13173 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13174 });
13175 cx.executor().run_until_parked();
13176
13177 cx.assert_state_with_diff(
13178 "
13179 «aaa
13180 - bbb
13181 ccc
13182 ddd
13183
13184 ggg
13185 hhh
13186
13187
13188 lll
13189 mmm
13190 - nnn
13191 + NNN
13192
13193 qqq
13194 rrr
13195
13196 uuu
13197 111
13198 222
13199 333
13200
13201 + 666
13202 777
13203
13204 000
13205 !!!ˇ»"
13206 .unindent(),
13207 );
13208}
13209
13210#[gpui::test]
13211async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext) {
13212 init_test(cx, |_| {});
13213
13214 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
13215 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\n";
13216
13217 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
13218 let multi_buffer = cx.new(|cx| {
13219 let mut multibuffer = MultiBuffer::new(ReadWrite);
13220 multibuffer.push_excerpts(
13221 buffer.clone(),
13222 [
13223 ExcerptRange {
13224 context: Point::new(0, 0)..Point::new(2, 0),
13225 primary: None,
13226 },
13227 ExcerptRange {
13228 context: Point::new(5, 0)..Point::new(7, 0),
13229 primary: None,
13230 },
13231 ],
13232 cx,
13233 );
13234 multibuffer
13235 });
13236
13237 let editor = cx.add_window(|window, cx| {
13238 Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
13239 });
13240 editor
13241 .update(cx, |editor, _window, cx| {
13242 let change_set =
13243 cx.new(|cx| BufferChangeSet::new_with_base_text(base.to_string(), &buffer, cx));
13244 editor
13245 .buffer
13246 .update(cx, |buffer, cx| buffer.add_change_set(change_set, cx))
13247 })
13248 .unwrap();
13249
13250 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13251 cx.run_until_parked();
13252
13253 cx.update_editor(|editor, window, cx| {
13254 editor.expand_all_diff_hunks(&Default::default(), window, cx)
13255 });
13256 cx.executor().run_until_parked();
13257
13258 cx.assert_state_with_diff(
13259 "
13260 ˇaaa
13261 - bbb
13262 + BBB
13263
13264 + EEE
13265 fff
13266 "
13267 .unindent(),
13268 );
13269}
13270
13271#[gpui::test]
13272async fn test_edits_around_expanded_insertion_hunks(
13273 executor: BackgroundExecutor,
13274 cx: &mut gpui::TestAppContext,
13275) {
13276 init_test(cx, |_| {});
13277
13278 let mut cx = EditorTestContext::new(cx).await;
13279
13280 let diff_base = r#"
13281 use some::mod1;
13282 use some::mod2;
13283
13284 const A: u32 = 42;
13285
13286 fn main() {
13287 println!("hello");
13288
13289 println!("world");
13290 }
13291 "#
13292 .unindent();
13293 executor.run_until_parked();
13294 cx.set_state(
13295 &r#"
13296 use some::mod1;
13297 use some::mod2;
13298
13299 const A: u32 = 42;
13300 const B: u32 = 42;
13301 const C: u32 = 42;
13302 ˇ
13303
13304 fn main() {
13305 println!("hello");
13306
13307 println!("world");
13308 }
13309 "#
13310 .unindent(),
13311 );
13312
13313 cx.set_diff_base(&diff_base);
13314 executor.run_until_parked();
13315
13316 cx.update_editor(|editor, window, cx| {
13317 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13318 });
13319 executor.run_until_parked();
13320
13321 cx.assert_state_with_diff(
13322 r#"
13323 use some::mod1;
13324 use some::mod2;
13325
13326 const A: u32 = 42;
13327 + const B: u32 = 42;
13328 + const C: u32 = 42;
13329 + ˇ
13330
13331 fn main() {
13332 println!("hello");
13333
13334 println!("world");
13335 }
13336 "#
13337 .unindent(),
13338 );
13339
13340 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
13341 executor.run_until_parked();
13342
13343 cx.assert_state_with_diff(
13344 r#"
13345 use some::mod1;
13346 use some::mod2;
13347
13348 const A: u32 = 42;
13349 + const B: u32 = 42;
13350 + const C: u32 = 42;
13351 + const D: u32 = 42;
13352 + ˇ
13353
13354 fn main() {
13355 println!("hello");
13356
13357 println!("world");
13358 }
13359 "#
13360 .unindent(),
13361 );
13362
13363 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
13364 executor.run_until_parked();
13365
13366 cx.assert_state_with_diff(
13367 r#"
13368 use some::mod1;
13369 use some::mod2;
13370
13371 const A: u32 = 42;
13372 + const B: u32 = 42;
13373 + const C: u32 = 42;
13374 + const D: u32 = 42;
13375 + const E: u32 = 42;
13376 + ˇ
13377
13378 fn main() {
13379 println!("hello");
13380
13381 println!("world");
13382 }
13383 "#
13384 .unindent(),
13385 );
13386
13387 cx.update_editor(|editor, window, cx| {
13388 editor.delete_line(&DeleteLine, window, cx);
13389 });
13390 executor.run_until_parked();
13391
13392 cx.assert_state_with_diff(
13393 r#"
13394 use some::mod1;
13395 use some::mod2;
13396
13397 const A: u32 = 42;
13398 + const B: u32 = 42;
13399 + const C: u32 = 42;
13400 + const D: u32 = 42;
13401 + const E: u32 = 42;
13402 ˇ
13403 fn main() {
13404 println!("hello");
13405
13406 println!("world");
13407 }
13408 "#
13409 .unindent(),
13410 );
13411
13412 cx.update_editor(|editor, window, cx| {
13413 editor.move_up(&MoveUp, window, cx);
13414 editor.delete_line(&DeleteLine, window, cx);
13415 editor.move_up(&MoveUp, window, cx);
13416 editor.delete_line(&DeleteLine, window, cx);
13417 editor.move_up(&MoveUp, window, cx);
13418 editor.delete_line(&DeleteLine, window, cx);
13419 });
13420 executor.run_until_parked();
13421 cx.assert_state_with_diff(
13422 r#"
13423 use some::mod1;
13424 use some::mod2;
13425
13426 const A: u32 = 42;
13427 + const B: u32 = 42;
13428 ˇ
13429 fn main() {
13430 println!("hello");
13431
13432 println!("world");
13433 }
13434 "#
13435 .unindent(),
13436 );
13437
13438 cx.update_editor(|editor, window, cx| {
13439 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
13440 editor.delete_line(&DeleteLine, window, cx);
13441 });
13442 executor.run_until_parked();
13443 cx.assert_state_with_diff(
13444 r#"
13445 ˇ
13446 fn main() {
13447 println!("hello");
13448
13449 println!("world");
13450 }
13451 "#
13452 .unindent(),
13453 );
13454}
13455
13456#[gpui::test]
13457async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
13458 init_test(cx, |_| {});
13459
13460 let mut cx = EditorTestContext::new(cx).await;
13461 cx.set_diff_base(indoc! { "
13462 one
13463 two
13464 three
13465 four
13466 five
13467 "
13468 });
13469 cx.set_state(indoc! { "
13470 one
13471 ˇthree
13472 five
13473 "});
13474 cx.run_until_parked();
13475 cx.update_editor(|editor, window, cx| {
13476 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13477 });
13478 cx.assert_state_with_diff(
13479 indoc! { "
13480 one
13481 - two
13482 ˇthree
13483 - four
13484 five
13485 "}
13486 .to_string(),
13487 );
13488 cx.update_editor(|editor, window, cx| {
13489 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13490 });
13491
13492 cx.assert_state_with_diff(
13493 indoc! { "
13494 one
13495 ˇthree
13496 five
13497 "}
13498 .to_string(),
13499 );
13500
13501 cx.set_state(indoc! { "
13502 one
13503 TWO
13504 ˇthree
13505 four
13506 five
13507 "});
13508 cx.run_until_parked();
13509 cx.update_editor(|editor, window, cx| {
13510 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13511 });
13512
13513 cx.assert_state_with_diff(
13514 indoc! { "
13515 one
13516 - two
13517 + TWO
13518 ˇthree
13519 four
13520 five
13521 "}
13522 .to_string(),
13523 );
13524 cx.update_editor(|editor, window, cx| {
13525 editor.move_up(&Default::default(), window, cx);
13526 editor.move_up(&Default::default(), window, cx);
13527 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13528 });
13529 cx.assert_state_with_diff(
13530 indoc! { "
13531 one
13532 ˇTWO
13533 three
13534 four
13535 five
13536 "}
13537 .to_string(),
13538 );
13539}
13540
13541#[gpui::test]
13542async fn test_edits_around_expanded_deletion_hunks(
13543 executor: BackgroundExecutor,
13544 cx: &mut gpui::TestAppContext,
13545) {
13546 init_test(cx, |_| {});
13547
13548 let mut cx = EditorTestContext::new(cx).await;
13549
13550 let diff_base = r#"
13551 use some::mod1;
13552 use some::mod2;
13553
13554 const A: u32 = 42;
13555 const B: u32 = 42;
13556 const C: u32 = 42;
13557
13558
13559 fn main() {
13560 println!("hello");
13561
13562 println!("world");
13563 }
13564 "#
13565 .unindent();
13566 executor.run_until_parked();
13567 cx.set_state(
13568 &r#"
13569 use some::mod1;
13570 use some::mod2;
13571
13572 ˇconst B: u32 = 42;
13573 const C: u32 = 42;
13574
13575
13576 fn main() {
13577 println!("hello");
13578
13579 println!("world");
13580 }
13581 "#
13582 .unindent(),
13583 );
13584
13585 cx.set_diff_base(&diff_base);
13586 executor.run_until_parked();
13587
13588 cx.update_editor(|editor, window, cx| {
13589 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13590 });
13591 executor.run_until_parked();
13592
13593 cx.assert_state_with_diff(
13594 r#"
13595 use some::mod1;
13596 use some::mod2;
13597
13598 - const A: u32 = 42;
13599 ˇconst B: u32 = 42;
13600 const C: u32 = 42;
13601
13602
13603 fn main() {
13604 println!("hello");
13605
13606 println!("world");
13607 }
13608 "#
13609 .unindent(),
13610 );
13611
13612 cx.update_editor(|editor, window, cx| {
13613 editor.delete_line(&DeleteLine, window, cx);
13614 });
13615 executor.run_until_parked();
13616 cx.assert_state_with_diff(
13617 r#"
13618 use some::mod1;
13619 use some::mod2;
13620
13621 - const A: u32 = 42;
13622 - const B: u32 = 42;
13623 ˇconst C: u32 = 42;
13624
13625
13626 fn main() {
13627 println!("hello");
13628
13629 println!("world");
13630 }
13631 "#
13632 .unindent(),
13633 );
13634
13635 cx.update_editor(|editor, window, cx| {
13636 editor.delete_line(&DeleteLine, window, cx);
13637 });
13638 executor.run_until_parked();
13639 cx.assert_state_with_diff(
13640 r#"
13641 use some::mod1;
13642 use some::mod2;
13643
13644 - const A: u32 = 42;
13645 - const B: u32 = 42;
13646 - const C: u32 = 42;
13647 ˇ
13648
13649 fn main() {
13650 println!("hello");
13651
13652 println!("world");
13653 }
13654 "#
13655 .unindent(),
13656 );
13657
13658 cx.update_editor(|editor, window, cx| {
13659 editor.handle_input("replacement", window, cx);
13660 });
13661 executor.run_until_parked();
13662 cx.assert_state_with_diff(
13663 r#"
13664 use some::mod1;
13665 use some::mod2;
13666
13667 - const A: u32 = 42;
13668 - const B: u32 = 42;
13669 - const C: u32 = 42;
13670 -
13671 + replacementˇ
13672
13673 fn main() {
13674 println!("hello");
13675
13676 println!("world");
13677 }
13678 "#
13679 .unindent(),
13680 );
13681}
13682
13683#[gpui::test]
13684async fn test_backspace_after_deletion_hunk(
13685 executor: BackgroundExecutor,
13686 cx: &mut gpui::TestAppContext,
13687) {
13688 init_test(cx, |_| {});
13689
13690 let mut cx = EditorTestContext::new(cx).await;
13691
13692 let base_text = r#"
13693 one
13694 two
13695 three
13696 four
13697 five
13698 "#
13699 .unindent();
13700 executor.run_until_parked();
13701 cx.set_state(
13702 &r#"
13703 one
13704 two
13705 fˇour
13706 five
13707 "#
13708 .unindent(),
13709 );
13710
13711 cx.set_diff_base(&base_text);
13712 executor.run_until_parked();
13713
13714 cx.update_editor(|editor, window, cx| {
13715 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13716 });
13717 executor.run_until_parked();
13718
13719 cx.assert_state_with_diff(
13720 r#"
13721 one
13722 two
13723 - three
13724 fˇour
13725 five
13726 "#
13727 .unindent(),
13728 );
13729
13730 cx.update_editor(|editor, window, cx| {
13731 editor.backspace(&Backspace, window, cx);
13732 editor.backspace(&Backspace, window, cx);
13733 });
13734 executor.run_until_parked();
13735 cx.assert_state_with_diff(
13736 r#"
13737 one
13738 two
13739 - threeˇ
13740 - four
13741 + our
13742 five
13743 "#
13744 .unindent(),
13745 );
13746}
13747
13748#[gpui::test]
13749async fn test_edit_after_expanded_modification_hunk(
13750 executor: BackgroundExecutor,
13751 cx: &mut gpui::TestAppContext,
13752) {
13753 init_test(cx, |_| {});
13754
13755 let mut cx = EditorTestContext::new(cx).await;
13756
13757 let diff_base = r#"
13758 use some::mod1;
13759 use some::mod2;
13760
13761 const A: u32 = 42;
13762 const B: u32 = 42;
13763 const C: u32 = 42;
13764 const D: u32 = 42;
13765
13766
13767 fn main() {
13768 println!("hello");
13769
13770 println!("world");
13771 }"#
13772 .unindent();
13773
13774 cx.set_state(
13775 &r#"
13776 use some::mod1;
13777 use some::mod2;
13778
13779 const A: u32 = 42;
13780 const B: u32 = 42;
13781 const C: u32 = 43ˇ
13782 const D: u32 = 42;
13783
13784
13785 fn main() {
13786 println!("hello");
13787
13788 println!("world");
13789 }"#
13790 .unindent(),
13791 );
13792
13793 cx.set_diff_base(&diff_base);
13794 executor.run_until_parked();
13795 cx.update_editor(|editor, window, cx| {
13796 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13797 });
13798 executor.run_until_parked();
13799
13800 cx.assert_state_with_diff(
13801 r#"
13802 use some::mod1;
13803 use some::mod2;
13804
13805 const A: u32 = 42;
13806 const B: u32 = 42;
13807 - const C: u32 = 42;
13808 + const C: u32 = 43ˇ
13809 const D: u32 = 42;
13810
13811
13812 fn main() {
13813 println!("hello");
13814
13815 println!("world");
13816 }"#
13817 .unindent(),
13818 );
13819
13820 cx.update_editor(|editor, window, cx| {
13821 editor.handle_input("\nnew_line\n", window, cx);
13822 });
13823 executor.run_until_parked();
13824
13825 cx.assert_state_with_diff(
13826 r#"
13827 use some::mod1;
13828 use some::mod2;
13829
13830 const A: u32 = 42;
13831 const B: u32 = 42;
13832 - const C: u32 = 42;
13833 + const C: u32 = 43
13834 + new_line
13835 + ˇ
13836 const D: u32 = 42;
13837
13838
13839 fn main() {
13840 println!("hello");
13841
13842 println!("world");
13843 }"#
13844 .unindent(),
13845 );
13846}
13847
13848async fn setup_indent_guides_editor(
13849 text: &str,
13850 cx: &mut gpui::TestAppContext,
13851) -> (BufferId, EditorTestContext) {
13852 init_test(cx, |_| {});
13853
13854 let mut cx = EditorTestContext::new(cx).await;
13855
13856 let buffer_id = cx.update_editor(|editor, window, cx| {
13857 editor.set_text(text, window, cx);
13858 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
13859
13860 buffer_ids[0]
13861 });
13862
13863 (buffer_id, cx)
13864}
13865
13866fn assert_indent_guides(
13867 range: Range<u32>,
13868 expected: Vec<IndentGuide>,
13869 active_indices: Option<Vec<usize>>,
13870 cx: &mut EditorTestContext,
13871) {
13872 let indent_guides = cx.update_editor(|editor, window, cx| {
13873 let snapshot = editor.snapshot(window, cx).display_snapshot;
13874 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
13875 editor,
13876 MultiBufferRow(range.start)..MultiBufferRow(range.end),
13877 true,
13878 &snapshot,
13879 cx,
13880 );
13881
13882 indent_guides.sort_by(|a, b| {
13883 a.depth.cmp(&b.depth).then(
13884 a.start_row
13885 .cmp(&b.start_row)
13886 .then(a.end_row.cmp(&b.end_row)),
13887 )
13888 });
13889 indent_guides
13890 });
13891
13892 if let Some(expected) = active_indices {
13893 let active_indices = cx.update_editor(|editor, window, cx| {
13894 let snapshot = editor.snapshot(window, cx).display_snapshot;
13895 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
13896 });
13897
13898 assert_eq!(
13899 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
13900 expected,
13901 "Active indent guide indices do not match"
13902 );
13903 }
13904
13905 assert_eq!(indent_guides, expected, "Indent guides do not match");
13906}
13907
13908fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
13909 IndentGuide {
13910 buffer_id,
13911 start_row: MultiBufferRow(start_row),
13912 end_row: MultiBufferRow(end_row),
13913 depth,
13914 tab_size: 4,
13915 settings: IndentGuideSettings {
13916 enabled: true,
13917 line_width: 1,
13918 active_line_width: 1,
13919 ..Default::default()
13920 },
13921 }
13922}
13923
13924#[gpui::test]
13925async fn test_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
13926 let (buffer_id, mut cx) = setup_indent_guides_editor(
13927 &"
13928 fn main() {
13929 let a = 1;
13930 }"
13931 .unindent(),
13932 cx,
13933 )
13934 .await;
13935
13936 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
13937}
13938
13939#[gpui::test]
13940async fn test_indent_guide_simple_block(cx: &mut gpui::TestAppContext) {
13941 let (buffer_id, mut cx) = setup_indent_guides_editor(
13942 &"
13943 fn main() {
13944 let a = 1;
13945 let b = 2;
13946 }"
13947 .unindent(),
13948 cx,
13949 )
13950 .await;
13951
13952 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
13953}
13954
13955#[gpui::test]
13956async fn test_indent_guide_nested(cx: &mut gpui::TestAppContext) {
13957 let (buffer_id, mut cx) = setup_indent_guides_editor(
13958 &"
13959 fn main() {
13960 let a = 1;
13961 if a == 3 {
13962 let b = 2;
13963 } else {
13964 let c = 3;
13965 }
13966 }"
13967 .unindent(),
13968 cx,
13969 )
13970 .await;
13971
13972 assert_indent_guides(
13973 0..8,
13974 vec![
13975 indent_guide(buffer_id, 1, 6, 0),
13976 indent_guide(buffer_id, 3, 3, 1),
13977 indent_guide(buffer_id, 5, 5, 1),
13978 ],
13979 None,
13980 &mut cx,
13981 );
13982}
13983
13984#[gpui::test]
13985async fn test_indent_guide_tab(cx: &mut gpui::TestAppContext) {
13986 let (buffer_id, mut cx) = setup_indent_guides_editor(
13987 &"
13988 fn main() {
13989 let a = 1;
13990 let b = 2;
13991 let c = 3;
13992 }"
13993 .unindent(),
13994 cx,
13995 )
13996 .await;
13997
13998 assert_indent_guides(
13999 0..5,
14000 vec![
14001 indent_guide(buffer_id, 1, 3, 0),
14002 indent_guide(buffer_id, 2, 2, 1),
14003 ],
14004 None,
14005 &mut cx,
14006 );
14007}
14008
14009#[gpui::test]
14010async fn test_indent_guide_continues_on_empty_line(cx: &mut gpui::TestAppContext) {
14011 let (buffer_id, mut cx) = setup_indent_guides_editor(
14012 &"
14013 fn main() {
14014 let a = 1;
14015
14016 let c = 3;
14017 }"
14018 .unindent(),
14019 cx,
14020 )
14021 .await;
14022
14023 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
14024}
14025
14026#[gpui::test]
14027async fn test_indent_guide_complex(cx: &mut gpui::TestAppContext) {
14028 let (buffer_id, mut cx) = setup_indent_guides_editor(
14029 &"
14030 fn main() {
14031 let a = 1;
14032
14033 let c = 3;
14034
14035 if a == 3 {
14036 let b = 2;
14037 } else {
14038 let c = 3;
14039 }
14040 }"
14041 .unindent(),
14042 cx,
14043 )
14044 .await;
14045
14046 assert_indent_guides(
14047 0..11,
14048 vec![
14049 indent_guide(buffer_id, 1, 9, 0),
14050 indent_guide(buffer_id, 6, 6, 1),
14051 indent_guide(buffer_id, 8, 8, 1),
14052 ],
14053 None,
14054 &mut cx,
14055 );
14056}
14057
14058#[gpui::test]
14059async fn test_indent_guide_starts_off_screen(cx: &mut gpui::TestAppContext) {
14060 let (buffer_id, mut cx) = setup_indent_guides_editor(
14061 &"
14062 fn main() {
14063 let a = 1;
14064
14065 let c = 3;
14066
14067 if a == 3 {
14068 let b = 2;
14069 } else {
14070 let c = 3;
14071 }
14072 }"
14073 .unindent(),
14074 cx,
14075 )
14076 .await;
14077
14078 assert_indent_guides(
14079 1..11,
14080 vec![
14081 indent_guide(buffer_id, 1, 9, 0),
14082 indent_guide(buffer_id, 6, 6, 1),
14083 indent_guide(buffer_id, 8, 8, 1),
14084 ],
14085 None,
14086 &mut cx,
14087 );
14088}
14089
14090#[gpui::test]
14091async fn test_indent_guide_ends_off_screen(cx: &mut gpui::TestAppContext) {
14092 let (buffer_id, mut cx) = setup_indent_guides_editor(
14093 &"
14094 fn main() {
14095 let a = 1;
14096
14097 let c = 3;
14098
14099 if a == 3 {
14100 let b = 2;
14101 } else {
14102 let c = 3;
14103 }
14104 }"
14105 .unindent(),
14106 cx,
14107 )
14108 .await;
14109
14110 assert_indent_guides(
14111 1..10,
14112 vec![
14113 indent_guide(buffer_id, 1, 9, 0),
14114 indent_guide(buffer_id, 6, 6, 1),
14115 indent_guide(buffer_id, 8, 8, 1),
14116 ],
14117 None,
14118 &mut cx,
14119 );
14120}
14121
14122#[gpui::test]
14123async fn test_indent_guide_without_brackets(cx: &mut gpui::TestAppContext) {
14124 let (buffer_id, mut cx) = setup_indent_guides_editor(
14125 &"
14126 block1
14127 block2
14128 block3
14129 block4
14130 block2
14131 block1
14132 block1"
14133 .unindent(),
14134 cx,
14135 )
14136 .await;
14137
14138 assert_indent_guides(
14139 1..10,
14140 vec![
14141 indent_guide(buffer_id, 1, 4, 0),
14142 indent_guide(buffer_id, 2, 3, 1),
14143 indent_guide(buffer_id, 3, 3, 2),
14144 ],
14145 None,
14146 &mut cx,
14147 );
14148}
14149
14150#[gpui::test]
14151async fn test_indent_guide_ends_before_empty_line(cx: &mut gpui::TestAppContext) {
14152 let (buffer_id, mut cx) = setup_indent_guides_editor(
14153 &"
14154 block1
14155 block2
14156 block3
14157
14158 block1
14159 block1"
14160 .unindent(),
14161 cx,
14162 )
14163 .await;
14164
14165 assert_indent_guides(
14166 0..6,
14167 vec![
14168 indent_guide(buffer_id, 1, 2, 0),
14169 indent_guide(buffer_id, 2, 2, 1),
14170 ],
14171 None,
14172 &mut cx,
14173 );
14174}
14175
14176#[gpui::test]
14177async fn test_indent_guide_continuing_off_screen(cx: &mut gpui::TestAppContext) {
14178 let (buffer_id, mut cx) = setup_indent_guides_editor(
14179 &"
14180 block1
14181
14182
14183
14184 block2
14185 "
14186 .unindent(),
14187 cx,
14188 )
14189 .await;
14190
14191 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
14192}
14193
14194#[gpui::test]
14195async fn test_indent_guide_tabs(cx: &mut gpui::TestAppContext) {
14196 let (buffer_id, mut cx) = setup_indent_guides_editor(
14197 &"
14198 def a:
14199 \tb = 3
14200 \tif True:
14201 \t\tc = 4
14202 \t\td = 5
14203 \tprint(b)
14204 "
14205 .unindent(),
14206 cx,
14207 )
14208 .await;
14209
14210 assert_indent_guides(
14211 0..6,
14212 vec![
14213 indent_guide(buffer_id, 1, 6, 0),
14214 indent_guide(buffer_id, 3, 4, 1),
14215 ],
14216 None,
14217 &mut cx,
14218 );
14219}
14220
14221#[gpui::test]
14222async fn test_active_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
14223 let (buffer_id, mut cx) = setup_indent_guides_editor(
14224 &"
14225 fn main() {
14226 let a = 1;
14227 }"
14228 .unindent(),
14229 cx,
14230 )
14231 .await;
14232
14233 cx.update_editor(|editor, window, cx| {
14234 editor.change_selections(None, window, cx, |s| {
14235 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14236 });
14237 });
14238
14239 assert_indent_guides(
14240 0..3,
14241 vec![indent_guide(buffer_id, 1, 1, 0)],
14242 Some(vec![0]),
14243 &mut cx,
14244 );
14245}
14246
14247#[gpui::test]
14248async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppContext) {
14249 let (buffer_id, mut cx) = setup_indent_guides_editor(
14250 &"
14251 fn main() {
14252 if 1 == 2 {
14253 let a = 1;
14254 }
14255 }"
14256 .unindent(),
14257 cx,
14258 )
14259 .await;
14260
14261 cx.update_editor(|editor, window, cx| {
14262 editor.change_selections(None, window, cx, |s| {
14263 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14264 });
14265 });
14266
14267 assert_indent_guides(
14268 0..4,
14269 vec![
14270 indent_guide(buffer_id, 1, 3, 0),
14271 indent_guide(buffer_id, 2, 2, 1),
14272 ],
14273 Some(vec![1]),
14274 &mut cx,
14275 );
14276
14277 cx.update_editor(|editor, window, cx| {
14278 editor.change_selections(None, window, cx, |s| {
14279 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
14280 });
14281 });
14282
14283 assert_indent_guides(
14284 0..4,
14285 vec![
14286 indent_guide(buffer_id, 1, 3, 0),
14287 indent_guide(buffer_id, 2, 2, 1),
14288 ],
14289 Some(vec![1]),
14290 &mut cx,
14291 );
14292
14293 cx.update_editor(|editor, window, cx| {
14294 editor.change_selections(None, window, cx, |s| {
14295 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
14296 });
14297 });
14298
14299 assert_indent_guides(
14300 0..4,
14301 vec![
14302 indent_guide(buffer_id, 1, 3, 0),
14303 indent_guide(buffer_id, 2, 2, 1),
14304 ],
14305 Some(vec![0]),
14306 &mut cx,
14307 );
14308}
14309
14310#[gpui::test]
14311async fn test_active_indent_guide_empty_line(cx: &mut gpui::TestAppContext) {
14312 let (buffer_id, mut cx) = setup_indent_guides_editor(
14313 &"
14314 fn main() {
14315 let a = 1;
14316
14317 let b = 2;
14318 }"
14319 .unindent(),
14320 cx,
14321 )
14322 .await;
14323
14324 cx.update_editor(|editor, window, cx| {
14325 editor.change_selections(None, window, cx, |s| {
14326 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
14327 });
14328 });
14329
14330 assert_indent_guides(
14331 0..5,
14332 vec![indent_guide(buffer_id, 1, 3, 0)],
14333 Some(vec![0]),
14334 &mut cx,
14335 );
14336}
14337
14338#[gpui::test]
14339async fn test_active_indent_guide_non_matching_indent(cx: &mut gpui::TestAppContext) {
14340 let (buffer_id, mut cx) = setup_indent_guides_editor(
14341 &"
14342 def m:
14343 a = 1
14344 pass"
14345 .unindent(),
14346 cx,
14347 )
14348 .await;
14349
14350 cx.update_editor(|editor, window, cx| {
14351 editor.change_selections(None, window, cx, |s| {
14352 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14353 });
14354 });
14355
14356 assert_indent_guides(
14357 0..3,
14358 vec![indent_guide(buffer_id, 1, 2, 0)],
14359 Some(vec![0]),
14360 &mut cx,
14361 );
14362}
14363
14364#[gpui::test]
14365async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut gpui::TestAppContext) {
14366 init_test(cx, |_| {});
14367 let mut cx = EditorTestContext::new(cx).await;
14368 let text = indoc! {
14369 "
14370 impl A {
14371 fn b() {
14372 0;
14373 3;
14374 5;
14375 6;
14376 7;
14377 }
14378 }
14379 "
14380 };
14381 let base_text = indoc! {
14382 "
14383 impl A {
14384 fn b() {
14385 0;
14386 1;
14387 2;
14388 3;
14389 4;
14390 }
14391 fn c() {
14392 5;
14393 6;
14394 7;
14395 }
14396 }
14397 "
14398 };
14399
14400 cx.update_editor(|editor, window, cx| {
14401 editor.set_text(text, window, cx);
14402
14403 editor.buffer().update(cx, |multibuffer, cx| {
14404 let buffer = multibuffer.as_singleton().unwrap();
14405 let change_set = cx.new(|cx| {
14406 let mut change_set = BufferChangeSet::new(&buffer, cx);
14407 let _ =
14408 change_set.set_base_text(base_text.into(), buffer.read(cx).text_snapshot(), cx);
14409 change_set
14410 });
14411
14412 multibuffer.set_all_diff_hunks_expanded(cx);
14413 multibuffer.add_change_set(change_set, cx);
14414
14415 buffer.read(cx).remote_id()
14416 })
14417 });
14418 cx.run_until_parked();
14419
14420 cx.assert_state_with_diff(
14421 indoc! { "
14422 impl A {
14423 fn b() {
14424 0;
14425 - 1;
14426 - 2;
14427 3;
14428 - 4;
14429 - }
14430 - fn c() {
14431 5;
14432 6;
14433 7;
14434 }
14435 }
14436 ˇ"
14437 }
14438 .to_string(),
14439 );
14440
14441 let mut actual_guides = cx.update_editor(|editor, window, cx| {
14442 editor
14443 .snapshot(window, cx)
14444 .buffer_snapshot
14445 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
14446 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
14447 .collect::<Vec<_>>()
14448 });
14449 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
14450 assert_eq!(
14451 actual_guides,
14452 vec![
14453 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
14454 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
14455 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
14456 ]
14457 );
14458}
14459
14460#[gpui::test]
14461fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
14462 init_test(cx, |_| {});
14463
14464 let editor = cx.add_window(|window, cx| {
14465 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
14466 build_editor(buffer, window, cx)
14467 });
14468
14469 let render_args = Arc::new(Mutex::new(None));
14470 let snapshot = editor
14471 .update(cx, |editor, window, cx| {
14472 let snapshot = editor.buffer().read(cx).snapshot(cx);
14473 let range =
14474 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
14475
14476 struct RenderArgs {
14477 row: MultiBufferRow,
14478 folded: bool,
14479 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
14480 }
14481
14482 let crease = Crease::inline(
14483 range,
14484 FoldPlaceholder::test(),
14485 {
14486 let toggle_callback = render_args.clone();
14487 move |row, folded, callback, _window, _cx| {
14488 *toggle_callback.lock() = Some(RenderArgs {
14489 row,
14490 folded,
14491 callback,
14492 });
14493 div()
14494 }
14495 },
14496 |_row, _folded, _window, _cx| div(),
14497 );
14498
14499 editor.insert_creases(Some(crease), cx);
14500 let snapshot = editor.snapshot(window, cx);
14501 let _div = snapshot.render_crease_toggle(
14502 MultiBufferRow(1),
14503 false,
14504 cx.entity().clone(),
14505 window,
14506 cx,
14507 );
14508 snapshot
14509 })
14510 .unwrap();
14511
14512 let render_args = render_args.lock().take().unwrap();
14513 assert_eq!(render_args.row, MultiBufferRow(1));
14514 assert!(!render_args.folded);
14515 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
14516
14517 cx.update_window(*editor, |_, window, cx| {
14518 (render_args.callback)(true, window, cx)
14519 })
14520 .unwrap();
14521 let snapshot = editor
14522 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
14523 .unwrap();
14524 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
14525
14526 cx.update_window(*editor, |_, window, cx| {
14527 (render_args.callback)(false, window, cx)
14528 })
14529 .unwrap();
14530 let snapshot = editor
14531 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
14532 .unwrap();
14533 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
14534}
14535
14536#[gpui::test]
14537async fn test_input_text(cx: &mut gpui::TestAppContext) {
14538 init_test(cx, |_| {});
14539 let mut cx = EditorTestContext::new(cx).await;
14540
14541 cx.set_state(
14542 &r#"ˇone
14543 two
14544
14545 three
14546 fourˇ
14547 five
14548
14549 siˇx"#
14550 .unindent(),
14551 );
14552
14553 cx.dispatch_action(HandleInput(String::new()));
14554 cx.assert_editor_state(
14555 &r#"ˇone
14556 two
14557
14558 three
14559 fourˇ
14560 five
14561
14562 siˇx"#
14563 .unindent(),
14564 );
14565
14566 cx.dispatch_action(HandleInput("AAAA".to_string()));
14567 cx.assert_editor_state(
14568 &r#"AAAAˇone
14569 two
14570
14571 three
14572 fourAAAAˇ
14573 five
14574
14575 siAAAAˇx"#
14576 .unindent(),
14577 );
14578}
14579
14580#[gpui::test]
14581async fn test_scroll_cursor_center_top_bottom(cx: &mut gpui::TestAppContext) {
14582 init_test(cx, |_| {});
14583
14584 let mut cx = EditorTestContext::new(cx).await;
14585 cx.set_state(
14586 r#"let foo = 1;
14587let foo = 2;
14588let foo = 3;
14589let fooˇ = 4;
14590let foo = 5;
14591let foo = 6;
14592let foo = 7;
14593let foo = 8;
14594let foo = 9;
14595let foo = 10;
14596let foo = 11;
14597let foo = 12;
14598let foo = 13;
14599let foo = 14;
14600let foo = 15;"#,
14601 );
14602
14603 cx.update_editor(|e, window, cx| {
14604 assert_eq!(
14605 e.next_scroll_position,
14606 NextScrollCursorCenterTopBottom::Center,
14607 "Default next scroll direction is center",
14608 );
14609
14610 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14611 assert_eq!(
14612 e.next_scroll_position,
14613 NextScrollCursorCenterTopBottom::Top,
14614 "After center, next scroll direction should be top",
14615 );
14616
14617 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14618 assert_eq!(
14619 e.next_scroll_position,
14620 NextScrollCursorCenterTopBottom::Bottom,
14621 "After top, next scroll direction should be bottom",
14622 );
14623
14624 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14625 assert_eq!(
14626 e.next_scroll_position,
14627 NextScrollCursorCenterTopBottom::Center,
14628 "After bottom, scrolling should start over",
14629 );
14630
14631 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14632 assert_eq!(
14633 e.next_scroll_position,
14634 NextScrollCursorCenterTopBottom::Top,
14635 "Scrolling continues if retriggered fast enough"
14636 );
14637 });
14638
14639 cx.executor()
14640 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
14641 cx.executor().run_until_parked();
14642 cx.update_editor(|e, _, _| {
14643 assert_eq!(
14644 e.next_scroll_position,
14645 NextScrollCursorCenterTopBottom::Center,
14646 "If scrolling is not triggered fast enough, it should reset"
14647 );
14648 });
14649}
14650
14651#[gpui::test]
14652async fn test_goto_definition_with_find_all_references_fallback(cx: &mut gpui::TestAppContext) {
14653 init_test(cx, |_| {});
14654 let mut cx = EditorLspTestContext::new_rust(
14655 lsp::ServerCapabilities {
14656 definition_provider: Some(lsp::OneOf::Left(true)),
14657 references_provider: Some(lsp::OneOf::Left(true)),
14658 ..lsp::ServerCapabilities::default()
14659 },
14660 cx,
14661 )
14662 .await;
14663
14664 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
14665 let go_to_definition = cx.lsp.handle_request::<lsp::request::GotoDefinition, _, _>(
14666 move |params, _| async move {
14667 if empty_go_to_definition {
14668 Ok(None)
14669 } else {
14670 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
14671 uri: params.text_document_position_params.text_document.uri,
14672 range: lsp::Range::new(lsp::Position::new(4, 3), lsp::Position::new(4, 6)),
14673 })))
14674 }
14675 },
14676 );
14677 let references =
14678 cx.lsp
14679 .handle_request::<lsp::request::References, _, _>(move |params, _| async move {
14680 Ok(Some(vec![lsp::Location {
14681 uri: params.text_document_position.text_document.uri,
14682 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
14683 }]))
14684 });
14685 (go_to_definition, references)
14686 };
14687
14688 cx.set_state(
14689 &r#"fn one() {
14690 let mut a = ˇtwo();
14691 }
14692
14693 fn two() {}"#
14694 .unindent(),
14695 );
14696 set_up_lsp_handlers(false, &mut cx);
14697 let navigated = cx
14698 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
14699 .await
14700 .expect("Failed to navigate to definition");
14701 assert_eq!(
14702 navigated,
14703 Navigated::Yes,
14704 "Should have navigated to definition from the GetDefinition response"
14705 );
14706 cx.assert_editor_state(
14707 &r#"fn one() {
14708 let mut a = two();
14709 }
14710
14711 fn «twoˇ»() {}"#
14712 .unindent(),
14713 );
14714
14715 let editors = cx.update_workspace(|workspace, _, cx| {
14716 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
14717 });
14718 cx.update_editor(|_, _, test_editor_cx| {
14719 assert_eq!(
14720 editors.len(),
14721 1,
14722 "Initially, only one, test, editor should be open in the workspace"
14723 );
14724 assert_eq!(
14725 test_editor_cx.entity(),
14726 editors.last().expect("Asserted len is 1").clone()
14727 );
14728 });
14729
14730 set_up_lsp_handlers(true, &mut cx);
14731 let navigated = cx
14732 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
14733 .await
14734 .expect("Failed to navigate to lookup references");
14735 assert_eq!(
14736 navigated,
14737 Navigated::Yes,
14738 "Should have navigated to references as a fallback after empty GoToDefinition response"
14739 );
14740 // We should not change the selections in the existing file,
14741 // if opening another milti buffer with the references
14742 cx.assert_editor_state(
14743 &r#"fn one() {
14744 let mut a = two();
14745 }
14746
14747 fn «twoˇ»() {}"#
14748 .unindent(),
14749 );
14750 let editors = cx.update_workspace(|workspace, _, cx| {
14751 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
14752 });
14753 cx.update_editor(|_, _, test_editor_cx| {
14754 assert_eq!(
14755 editors.len(),
14756 2,
14757 "After falling back to references search, we open a new editor with the results"
14758 );
14759 let references_fallback_text = editors
14760 .into_iter()
14761 .find(|new_editor| *new_editor != test_editor_cx.entity())
14762 .expect("Should have one non-test editor now")
14763 .read(test_editor_cx)
14764 .text(test_editor_cx);
14765 assert_eq!(
14766 references_fallback_text, "fn one() {\n let mut a = two();\n}",
14767 "Should use the range from the references response and not the GoToDefinition one"
14768 );
14769 });
14770}
14771
14772#[gpui::test]
14773async fn test_find_enclosing_node_with_task(cx: &mut gpui::TestAppContext) {
14774 init_test(cx, |_| {});
14775
14776 let language = Arc::new(Language::new(
14777 LanguageConfig::default(),
14778 Some(tree_sitter_rust::LANGUAGE.into()),
14779 ));
14780
14781 let text = r#"
14782 #[cfg(test)]
14783 mod tests() {
14784 #[test]
14785 fn runnable_1() {
14786 let a = 1;
14787 }
14788
14789 #[test]
14790 fn runnable_2() {
14791 let a = 1;
14792 let b = 2;
14793 }
14794 }
14795 "#
14796 .unindent();
14797
14798 let fs = FakeFs::new(cx.executor());
14799 fs.insert_file("/file.rs", Default::default()).await;
14800
14801 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14802 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14803 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14804 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14805 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
14806
14807 let editor = cx.new_window_entity(|window, cx| {
14808 Editor::new(
14809 EditorMode::Full,
14810 multi_buffer,
14811 Some(project.clone()),
14812 true,
14813 window,
14814 cx,
14815 )
14816 });
14817
14818 editor.update_in(cx, |editor, window, cx| {
14819 editor.tasks.insert(
14820 (buffer.read(cx).remote_id(), 3),
14821 RunnableTasks {
14822 templates: vec![],
14823 offset: MultiBufferOffset(43),
14824 column: 0,
14825 extra_variables: HashMap::default(),
14826 context_range: BufferOffset(43)..BufferOffset(85),
14827 },
14828 );
14829 editor.tasks.insert(
14830 (buffer.read(cx).remote_id(), 8),
14831 RunnableTasks {
14832 templates: vec![],
14833 offset: MultiBufferOffset(86),
14834 column: 0,
14835 extra_variables: HashMap::default(),
14836 context_range: BufferOffset(86)..BufferOffset(191),
14837 },
14838 );
14839
14840 // Test finding task when cursor is inside function body
14841 editor.change_selections(None, window, cx, |s| {
14842 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
14843 });
14844 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
14845 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
14846
14847 // Test finding task when cursor is on function name
14848 editor.change_selections(None, window, cx, |s| {
14849 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
14850 });
14851 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
14852 assert_eq!(row, 8, "Should find task when cursor is on function name");
14853 });
14854}
14855
14856#[gpui::test]
14857async fn test_multi_buffer_folding(cx: &mut gpui::TestAppContext) {
14858 init_test(cx, |_| {});
14859
14860 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
14861 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
14862 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
14863
14864 let fs = FakeFs::new(cx.executor());
14865 fs.insert_tree(
14866 "/a",
14867 json!({
14868 "first.rs": sample_text_1,
14869 "second.rs": sample_text_2,
14870 "third.rs": sample_text_3,
14871 }),
14872 )
14873 .await;
14874 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14875 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14876 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14877 let worktree = project.update(cx, |project, cx| {
14878 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
14879 assert_eq!(worktrees.len(), 1);
14880 worktrees.pop().unwrap()
14881 });
14882 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
14883
14884 let buffer_1 = project
14885 .update(cx, |project, cx| {
14886 project.open_buffer((worktree_id, "first.rs"), cx)
14887 })
14888 .await
14889 .unwrap();
14890 let buffer_2 = project
14891 .update(cx, |project, cx| {
14892 project.open_buffer((worktree_id, "second.rs"), cx)
14893 })
14894 .await
14895 .unwrap();
14896 let buffer_3 = project
14897 .update(cx, |project, cx| {
14898 project.open_buffer((worktree_id, "third.rs"), cx)
14899 })
14900 .await
14901 .unwrap();
14902
14903 let multi_buffer = cx.new(|cx| {
14904 let mut multi_buffer = MultiBuffer::new(ReadWrite);
14905 multi_buffer.push_excerpts(
14906 buffer_1.clone(),
14907 [
14908 ExcerptRange {
14909 context: Point::new(0, 0)..Point::new(3, 0),
14910 primary: None,
14911 },
14912 ExcerptRange {
14913 context: Point::new(5, 0)..Point::new(7, 0),
14914 primary: None,
14915 },
14916 ExcerptRange {
14917 context: Point::new(9, 0)..Point::new(10, 4),
14918 primary: None,
14919 },
14920 ],
14921 cx,
14922 );
14923 multi_buffer.push_excerpts(
14924 buffer_2.clone(),
14925 [
14926 ExcerptRange {
14927 context: Point::new(0, 0)..Point::new(3, 0),
14928 primary: None,
14929 },
14930 ExcerptRange {
14931 context: Point::new(5, 0)..Point::new(7, 0),
14932 primary: None,
14933 },
14934 ExcerptRange {
14935 context: Point::new(9, 0)..Point::new(10, 4),
14936 primary: None,
14937 },
14938 ],
14939 cx,
14940 );
14941 multi_buffer.push_excerpts(
14942 buffer_3.clone(),
14943 [
14944 ExcerptRange {
14945 context: Point::new(0, 0)..Point::new(3, 0),
14946 primary: None,
14947 },
14948 ExcerptRange {
14949 context: Point::new(5, 0)..Point::new(7, 0),
14950 primary: None,
14951 },
14952 ExcerptRange {
14953 context: Point::new(9, 0)..Point::new(10, 4),
14954 primary: None,
14955 },
14956 ],
14957 cx,
14958 );
14959 multi_buffer
14960 });
14961 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
14962 Editor::new(
14963 EditorMode::Full,
14964 multi_buffer,
14965 Some(project.clone()),
14966 true,
14967 window,
14968 cx,
14969 )
14970 });
14971
14972 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";
14973 assert_eq!(
14974 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
14975 full_text,
14976 );
14977
14978 multi_buffer_editor.update(cx, |editor, cx| {
14979 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
14980 });
14981 assert_eq!(
14982 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
14983 "\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",
14984 "After folding the first buffer, its text should not be displayed"
14985 );
14986
14987 multi_buffer_editor.update(cx, |editor, cx| {
14988 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
14989 });
14990 assert_eq!(
14991 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
14992 "\n\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
14993 "After folding the second buffer, its text should not be displayed"
14994 );
14995
14996 multi_buffer_editor.update(cx, |editor, cx| {
14997 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
14998 });
14999 assert_eq!(
15000 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15001 "\n\n\n\n\n",
15002 "After folding the third buffer, its text should not be displayed"
15003 );
15004
15005 // Emulate selection inside the fold logic, that should work
15006 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15007 editor
15008 .snapshot(window, cx)
15009 .next_line_boundary(Point::new(0, 4));
15010 });
15011
15012 multi_buffer_editor.update(cx, |editor, cx| {
15013 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
15014 });
15015 assert_eq!(
15016 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15017 "\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
15018 "After unfolding the second buffer, its text should be displayed"
15019 );
15020
15021 multi_buffer_editor.update(cx, |editor, cx| {
15022 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
15023 });
15024 assert_eq!(
15025 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15026 "\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",
15027 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
15028 );
15029
15030 multi_buffer_editor.update(cx, |editor, cx| {
15031 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
15032 });
15033 assert_eq!(
15034 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15035 full_text,
15036 "After unfolding the all buffers, all original text should be displayed"
15037 );
15038}
15039
15040#[gpui::test]
15041async fn test_multi_buffer_single_excerpts_folding(cx: &mut gpui::TestAppContext) {
15042 init_test(cx, |_| {});
15043
15044 let sample_text_1 = "1111\n2222\n3333".to_string();
15045 let sample_text_2 = "4444\n5555\n6666".to_string();
15046 let sample_text_3 = "7777\n8888\n9999".to_string();
15047
15048 let fs = FakeFs::new(cx.executor());
15049 fs.insert_tree(
15050 "/a",
15051 json!({
15052 "first.rs": sample_text_1,
15053 "second.rs": sample_text_2,
15054 "third.rs": sample_text_3,
15055 }),
15056 )
15057 .await;
15058 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15059 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15060 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15061 let worktree = project.update(cx, |project, cx| {
15062 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15063 assert_eq!(worktrees.len(), 1);
15064 worktrees.pop().unwrap()
15065 });
15066 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15067
15068 let buffer_1 = project
15069 .update(cx, |project, cx| {
15070 project.open_buffer((worktree_id, "first.rs"), cx)
15071 })
15072 .await
15073 .unwrap();
15074 let buffer_2 = project
15075 .update(cx, |project, cx| {
15076 project.open_buffer((worktree_id, "second.rs"), cx)
15077 })
15078 .await
15079 .unwrap();
15080 let buffer_3 = project
15081 .update(cx, |project, cx| {
15082 project.open_buffer((worktree_id, "third.rs"), cx)
15083 })
15084 .await
15085 .unwrap();
15086
15087 let multi_buffer = cx.new(|cx| {
15088 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15089 multi_buffer.push_excerpts(
15090 buffer_1.clone(),
15091 [ExcerptRange {
15092 context: Point::new(0, 0)..Point::new(3, 0),
15093 primary: None,
15094 }],
15095 cx,
15096 );
15097 multi_buffer.push_excerpts(
15098 buffer_2.clone(),
15099 [ExcerptRange {
15100 context: Point::new(0, 0)..Point::new(3, 0),
15101 primary: None,
15102 }],
15103 cx,
15104 );
15105 multi_buffer.push_excerpts(
15106 buffer_3.clone(),
15107 [ExcerptRange {
15108 context: Point::new(0, 0)..Point::new(3, 0),
15109 primary: None,
15110 }],
15111 cx,
15112 );
15113 multi_buffer
15114 });
15115
15116 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15117 Editor::new(
15118 EditorMode::Full,
15119 multi_buffer,
15120 Some(project.clone()),
15121 true,
15122 window,
15123 cx,
15124 )
15125 });
15126
15127 let full_text = "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n";
15128 assert_eq!(
15129 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15130 full_text,
15131 );
15132
15133 multi_buffer_editor.update(cx, |editor, cx| {
15134 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
15135 });
15136 assert_eq!(
15137 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15138 "\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n",
15139 "After folding the first buffer, its text should not be displayed"
15140 );
15141
15142 multi_buffer_editor.update(cx, |editor, cx| {
15143 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
15144 });
15145
15146 assert_eq!(
15147 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15148 "\n\n\n\n\n\n\n7777\n8888\n9999\n",
15149 "After folding the second buffer, its text should not be displayed"
15150 );
15151
15152 multi_buffer_editor.update(cx, |editor, cx| {
15153 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
15154 });
15155 assert_eq!(
15156 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15157 "\n\n\n\n\n",
15158 "After folding the third buffer, its text should not be displayed"
15159 );
15160
15161 multi_buffer_editor.update(cx, |editor, cx| {
15162 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
15163 });
15164 assert_eq!(
15165 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15166 "\n\n\n\n\n4444\n5555\n6666\n\n\n",
15167 "After unfolding the second buffer, its text should be displayed"
15168 );
15169
15170 multi_buffer_editor.update(cx, |editor, cx| {
15171 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
15172 });
15173 assert_eq!(
15174 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15175 "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n",
15176 "After unfolding the first buffer, its text should be displayed"
15177 );
15178
15179 multi_buffer_editor.update(cx, |editor, cx| {
15180 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
15181 });
15182 assert_eq!(
15183 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15184 full_text,
15185 "After unfolding all buffers, all original text should be displayed"
15186 );
15187}
15188
15189#[gpui::test]
15190async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppContext) {
15191 init_test(cx, |_| {});
15192
15193 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
15194
15195 let fs = FakeFs::new(cx.executor());
15196 fs.insert_tree(
15197 "/a",
15198 json!({
15199 "main.rs": sample_text,
15200 }),
15201 )
15202 .await;
15203 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15204 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15205 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15206 let worktree = project.update(cx, |project, cx| {
15207 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15208 assert_eq!(worktrees.len(), 1);
15209 worktrees.pop().unwrap()
15210 });
15211 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15212
15213 let buffer_1 = project
15214 .update(cx, |project, cx| {
15215 project.open_buffer((worktree_id, "main.rs"), cx)
15216 })
15217 .await
15218 .unwrap();
15219
15220 let multi_buffer = cx.new(|cx| {
15221 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15222 multi_buffer.push_excerpts(
15223 buffer_1.clone(),
15224 [ExcerptRange {
15225 context: Point::new(0, 0)
15226 ..Point::new(
15227 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
15228 0,
15229 ),
15230 primary: None,
15231 }],
15232 cx,
15233 );
15234 multi_buffer
15235 });
15236 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15237 Editor::new(
15238 EditorMode::Full,
15239 multi_buffer,
15240 Some(project.clone()),
15241 true,
15242 window,
15243 cx,
15244 )
15245 });
15246
15247 let selection_range = Point::new(1, 0)..Point::new(2, 0);
15248 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15249 enum TestHighlight {}
15250 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
15251 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
15252 editor.highlight_text::<TestHighlight>(
15253 vec![highlight_range.clone()],
15254 HighlightStyle::color(Hsla::green()),
15255 cx,
15256 );
15257 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
15258 });
15259
15260 let full_text = format!("\n\n\n{sample_text}\n");
15261 assert_eq!(
15262 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15263 full_text,
15264 );
15265}
15266
15267#[gpui::test]
15268async fn test_inline_completion_text(cx: &mut TestAppContext) {
15269 init_test(cx, |_| {});
15270
15271 // Simple insertion
15272 assert_highlighted_edits(
15273 "Hello, world!",
15274 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
15275 true,
15276 cx,
15277 |highlighted_edits, cx| {
15278 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
15279 assert_eq!(highlighted_edits.highlights.len(), 1);
15280 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
15281 assert_eq!(
15282 highlighted_edits.highlights[0].1.background_color,
15283 Some(cx.theme().status().created_background)
15284 );
15285 },
15286 )
15287 .await;
15288
15289 // Replacement
15290 assert_highlighted_edits(
15291 "This is a test.",
15292 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
15293 false,
15294 cx,
15295 |highlighted_edits, cx| {
15296 assert_eq!(highlighted_edits.text, "That is a test.");
15297 assert_eq!(highlighted_edits.highlights.len(), 1);
15298 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
15299 assert_eq!(
15300 highlighted_edits.highlights[0].1.background_color,
15301 Some(cx.theme().status().created_background)
15302 );
15303 },
15304 )
15305 .await;
15306
15307 // Multiple edits
15308 assert_highlighted_edits(
15309 "Hello, world!",
15310 vec![
15311 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
15312 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
15313 ],
15314 false,
15315 cx,
15316 |highlighted_edits, cx| {
15317 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
15318 assert_eq!(highlighted_edits.highlights.len(), 2);
15319 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
15320 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
15321 assert_eq!(
15322 highlighted_edits.highlights[0].1.background_color,
15323 Some(cx.theme().status().created_background)
15324 );
15325 assert_eq!(
15326 highlighted_edits.highlights[1].1.background_color,
15327 Some(cx.theme().status().created_background)
15328 );
15329 },
15330 )
15331 .await;
15332
15333 // Multiple lines with edits
15334 assert_highlighted_edits(
15335 "First line\nSecond line\nThird line\nFourth line",
15336 vec![
15337 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
15338 (
15339 Point::new(2, 0)..Point::new(2, 10),
15340 "New third line".to_string(),
15341 ),
15342 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
15343 ],
15344 false,
15345 cx,
15346 |highlighted_edits, cx| {
15347 assert_eq!(
15348 highlighted_edits.text,
15349 "Second modified\nNew third line\nFourth updated line"
15350 );
15351 assert_eq!(highlighted_edits.highlights.len(), 3);
15352 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
15353 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
15354 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
15355 for highlight in &highlighted_edits.highlights {
15356 assert_eq!(
15357 highlight.1.background_color,
15358 Some(cx.theme().status().created_background)
15359 );
15360 }
15361 },
15362 )
15363 .await;
15364}
15365
15366#[gpui::test]
15367async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
15368 init_test(cx, |_| {});
15369
15370 // Deletion
15371 assert_highlighted_edits(
15372 "Hello, world!",
15373 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
15374 true,
15375 cx,
15376 |highlighted_edits, cx| {
15377 assert_eq!(highlighted_edits.text, "Hello, world!");
15378 assert_eq!(highlighted_edits.highlights.len(), 1);
15379 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
15380 assert_eq!(
15381 highlighted_edits.highlights[0].1.background_color,
15382 Some(cx.theme().status().deleted_background)
15383 );
15384 },
15385 )
15386 .await;
15387
15388 // Insertion
15389 assert_highlighted_edits(
15390 "Hello, world!",
15391 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
15392 true,
15393 cx,
15394 |highlighted_edits, cx| {
15395 assert_eq!(highlighted_edits.highlights.len(), 1);
15396 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
15397 assert_eq!(
15398 highlighted_edits.highlights[0].1.background_color,
15399 Some(cx.theme().status().created_background)
15400 );
15401 },
15402 )
15403 .await;
15404}
15405
15406async fn assert_highlighted_edits(
15407 text: &str,
15408 edits: Vec<(Range<Point>, String)>,
15409 include_deletions: bool,
15410 cx: &mut TestAppContext,
15411 assertion_fn: impl Fn(HighlightedText, &App),
15412) {
15413 let window = cx.add_window(|window, cx| {
15414 let buffer = MultiBuffer::build_simple(text, cx);
15415 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
15416 });
15417 let cx = &mut VisualTestContext::from_window(*window, cx);
15418
15419 let (buffer, snapshot) = window
15420 .update(cx, |editor, _window, cx| {
15421 (
15422 editor.buffer().clone(),
15423 editor.buffer().read(cx).snapshot(cx),
15424 )
15425 })
15426 .unwrap();
15427
15428 let edits = edits
15429 .into_iter()
15430 .map(|(range, edit)| {
15431 (
15432 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
15433 edit,
15434 )
15435 })
15436 .collect::<Vec<_>>();
15437
15438 let text_anchor_edits = edits
15439 .clone()
15440 .into_iter()
15441 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
15442 .collect::<Vec<_>>();
15443
15444 let edit_preview = window
15445 .update(cx, |_, _window, cx| {
15446 buffer
15447 .read(cx)
15448 .as_singleton()
15449 .unwrap()
15450 .read(cx)
15451 .preview_edits(text_anchor_edits.into(), cx)
15452 })
15453 .unwrap()
15454 .await;
15455
15456 cx.update(|_window, cx| {
15457 let highlighted_edits = inline_completion_edit_text(
15458 &snapshot.as_singleton().unwrap().2,
15459 &edits,
15460 &edit_preview,
15461 include_deletions,
15462 cx,
15463 );
15464 assertion_fn(highlighted_edits, cx)
15465 });
15466}
15467
15468#[gpui::test]
15469async fn test_rename_with_duplicate_edits(cx: &mut gpui::TestAppContext) {
15470 init_test(cx, |_| {});
15471 let capabilities = lsp::ServerCapabilities {
15472 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
15473 prepare_provider: Some(true),
15474 work_done_progress_options: Default::default(),
15475 })),
15476 ..Default::default()
15477 };
15478 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
15479
15480 cx.set_state(indoc! {"
15481 struct Fˇoo {}
15482 "});
15483
15484 cx.update_editor(|editor, _, cx| {
15485 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
15486 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
15487 editor.highlight_background::<DocumentHighlightRead>(
15488 &[highlight_range],
15489 |c| c.editor_document_highlight_read_background,
15490 cx,
15491 );
15492 });
15493
15494 let mut prepare_rename_handler =
15495 cx.handle_request::<lsp::request::PrepareRenameRequest, _, _>(move |_, _, _| async move {
15496 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
15497 start: lsp::Position {
15498 line: 0,
15499 character: 7,
15500 },
15501 end: lsp::Position {
15502 line: 0,
15503 character: 10,
15504 },
15505 })))
15506 });
15507 let prepare_rename_task = cx
15508 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
15509 .expect("Prepare rename was not started");
15510 prepare_rename_handler.next().await.unwrap();
15511 prepare_rename_task.await.expect("Prepare rename failed");
15512
15513 let mut rename_handler =
15514 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
15515 let edit = lsp::TextEdit {
15516 range: lsp::Range {
15517 start: lsp::Position {
15518 line: 0,
15519 character: 7,
15520 },
15521 end: lsp::Position {
15522 line: 0,
15523 character: 10,
15524 },
15525 },
15526 new_text: "FooRenamed".to_string(),
15527 };
15528 Ok(Some(lsp::WorkspaceEdit::new(
15529 // Specify the same edit twice
15530 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
15531 )))
15532 });
15533 let rename_task = cx
15534 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
15535 .expect("Confirm rename was not started");
15536 rename_handler.next().await.unwrap();
15537 rename_task.await.expect("Confirm rename failed");
15538 cx.run_until_parked();
15539
15540 // Despite two edits, only one is actually applied as those are identical
15541 cx.assert_editor_state(indoc! {"
15542 struct FooRenamedˇ {}
15543 "});
15544}
15545
15546#[gpui::test]
15547async fn test_rename_without_prepare(cx: &mut gpui::TestAppContext) {
15548 init_test(cx, |_| {});
15549 // These capabilities indicate that the server does not support prepare rename.
15550 let capabilities = lsp::ServerCapabilities {
15551 rename_provider: Some(lsp::OneOf::Left(true)),
15552 ..Default::default()
15553 };
15554 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
15555
15556 cx.set_state(indoc! {"
15557 struct Fˇoo {}
15558 "});
15559
15560 cx.update_editor(|editor, _window, cx| {
15561 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
15562 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
15563 editor.highlight_background::<DocumentHighlightRead>(
15564 &[highlight_range],
15565 |c| c.editor_document_highlight_read_background,
15566 cx,
15567 );
15568 });
15569
15570 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
15571 .expect("Prepare rename was not started")
15572 .await
15573 .expect("Prepare rename failed");
15574
15575 let mut rename_handler =
15576 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
15577 let edit = lsp::TextEdit {
15578 range: lsp::Range {
15579 start: lsp::Position {
15580 line: 0,
15581 character: 7,
15582 },
15583 end: lsp::Position {
15584 line: 0,
15585 character: 10,
15586 },
15587 },
15588 new_text: "FooRenamed".to_string(),
15589 };
15590 Ok(Some(lsp::WorkspaceEdit::new(
15591 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
15592 )))
15593 });
15594 let rename_task = cx
15595 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
15596 .expect("Confirm rename was not started");
15597 rename_handler.next().await.unwrap();
15598 rename_task.await.expect("Confirm rename failed");
15599 cx.run_until_parked();
15600
15601 // Correct range is renamed, as `surrounding_word` is used to find it.
15602 cx.assert_editor_state(indoc! {"
15603 struct FooRenamedˇ {}
15604 "});
15605}
15606
15607fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
15608 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
15609 point..point
15610}
15611
15612fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
15613 let (text, ranges) = marked_text_ranges(marked_text, true);
15614 assert_eq!(editor.text(cx), text);
15615 assert_eq!(
15616 editor.selections.ranges(cx),
15617 ranges,
15618 "Assert selections are {}",
15619 marked_text
15620 );
15621}
15622
15623pub fn handle_signature_help_request(
15624 cx: &mut EditorLspTestContext,
15625 mocked_response: lsp::SignatureHelp,
15626) -> impl Future<Output = ()> {
15627 let mut request =
15628 cx.handle_request::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
15629 let mocked_response = mocked_response.clone();
15630 async move { Ok(Some(mocked_response)) }
15631 });
15632
15633 async move {
15634 request.next().await;
15635 }
15636}
15637
15638/// Handle completion request passing a marked string specifying where the completion
15639/// should be triggered from using '|' character, what range should be replaced, and what completions
15640/// should be returned using '<' and '>' to delimit the range
15641pub fn handle_completion_request(
15642 cx: &mut EditorLspTestContext,
15643 marked_string: &str,
15644 completions: Vec<&'static str>,
15645 counter: Arc<AtomicUsize>,
15646) -> impl Future<Output = ()> {
15647 let complete_from_marker: TextRangeMarker = '|'.into();
15648 let replace_range_marker: TextRangeMarker = ('<', '>').into();
15649 let (_, mut marked_ranges) = marked_text_ranges_by(
15650 marked_string,
15651 vec![complete_from_marker.clone(), replace_range_marker.clone()],
15652 );
15653
15654 let complete_from_position =
15655 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
15656 let replace_range =
15657 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
15658
15659 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
15660 let completions = completions.clone();
15661 counter.fetch_add(1, atomic::Ordering::Release);
15662 async move {
15663 assert_eq!(params.text_document_position.text_document.uri, url.clone());
15664 assert_eq!(
15665 params.text_document_position.position,
15666 complete_from_position
15667 );
15668 Ok(Some(lsp::CompletionResponse::Array(
15669 completions
15670 .iter()
15671 .map(|completion_text| lsp::CompletionItem {
15672 label: completion_text.to_string(),
15673 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15674 range: replace_range,
15675 new_text: completion_text.to_string(),
15676 })),
15677 ..Default::default()
15678 })
15679 .collect(),
15680 )))
15681 }
15682 });
15683
15684 async move {
15685 request.next().await;
15686 }
15687}
15688
15689fn handle_resolve_completion_request(
15690 cx: &mut EditorLspTestContext,
15691 edits: Option<Vec<(&'static str, &'static str)>>,
15692) -> impl Future<Output = ()> {
15693 let edits = edits.map(|edits| {
15694 edits
15695 .iter()
15696 .map(|(marked_string, new_text)| {
15697 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
15698 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
15699 lsp::TextEdit::new(replace_range, new_text.to_string())
15700 })
15701 .collect::<Vec<_>>()
15702 });
15703
15704 let mut request =
15705 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15706 let edits = edits.clone();
15707 async move {
15708 Ok(lsp::CompletionItem {
15709 additional_text_edits: edits,
15710 ..Default::default()
15711 })
15712 }
15713 });
15714
15715 async move {
15716 request.next().await;
15717 }
15718}
15719
15720pub(crate) fn update_test_language_settings(
15721 cx: &mut TestAppContext,
15722 f: impl Fn(&mut AllLanguageSettingsContent),
15723) {
15724 cx.update(|cx| {
15725 SettingsStore::update_global(cx, |store, cx| {
15726 store.update_user_settings::<AllLanguageSettings>(cx, f);
15727 });
15728 });
15729}
15730
15731pub(crate) fn update_test_project_settings(
15732 cx: &mut TestAppContext,
15733 f: impl Fn(&mut ProjectSettings),
15734) {
15735 cx.update(|cx| {
15736 SettingsStore::update_global(cx, |store, cx| {
15737 store.update_user_settings::<ProjectSettings>(cx, f);
15738 });
15739 });
15740}
15741
15742pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
15743 cx.update(|cx| {
15744 assets::Assets.load_test_fonts(cx);
15745 let store = SettingsStore::test(cx);
15746 cx.set_global(store);
15747 theme::init(theme::LoadThemes::JustBase, cx);
15748 release_channel::init(SemanticVersion::default(), cx);
15749 client::init_settings(cx);
15750 language::init(cx);
15751 Project::init_settings(cx);
15752 workspace::init_settings(cx);
15753 crate::init(cx);
15754 });
15755
15756 update_test_language_settings(cx, f);
15757}
15758
15759#[track_caller]
15760fn assert_hunk_revert(
15761 not_reverted_text_with_selections: &str,
15762 expected_hunk_statuses_before: Vec<DiffHunkStatus>,
15763 expected_reverted_text_with_selections: &str,
15764 base_text: &str,
15765 cx: &mut EditorLspTestContext,
15766) {
15767 cx.set_state(not_reverted_text_with_selections);
15768 cx.set_diff_base(base_text);
15769 cx.executor().run_until_parked();
15770
15771 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
15772 let snapshot = editor.snapshot(window, cx);
15773 let reverted_hunk_statuses = snapshot
15774 .buffer_snapshot
15775 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
15776 .map(|hunk| hunk.status())
15777 .collect::<Vec<_>>();
15778
15779 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
15780 reverted_hunk_statuses
15781 });
15782 cx.executor().run_until_parked();
15783 cx.assert_editor_state(expected_reverted_text_with_selections);
15784 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
15785}