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 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5240
5241 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5242 .unwrap();
5243 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5244}
5245
5246#[gpui::test]
5247async fn test_select_next_with_multiple_carets(cx: &mut gpui::TestAppContext) {
5248 init_test(cx, |_| {});
5249
5250 let mut cx = EditorTestContext::new(cx).await;
5251 cx.set_state(
5252 r#"let foo = 2;
5253lˇet foo = 2;
5254let fooˇ = 2;
5255let foo = 2;
5256let foo = ˇ2;"#,
5257 );
5258
5259 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5260 .unwrap();
5261 cx.assert_editor_state(
5262 r#"let foo = 2;
5263«letˇ» foo = 2;
5264let «fooˇ» = 2;
5265let foo = 2;
5266let foo = «2ˇ»;"#,
5267 );
5268
5269 // noop for multiple selections with different contents
5270 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5271 .unwrap();
5272 cx.assert_editor_state(
5273 r#"let foo = 2;
5274«letˇ» foo = 2;
5275let «fooˇ» = 2;
5276let foo = 2;
5277let foo = «2ˇ»;"#,
5278 );
5279}
5280
5281#[gpui::test]
5282async fn test_select_previous_multibuffer(cx: &mut gpui::TestAppContext) {
5283 init_test(cx, |_| {});
5284
5285 let mut cx =
5286 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
5287
5288 cx.assert_editor_state(indoc! {"
5289 ˇbbb
5290 ccc
5291
5292 bbb
5293 ccc
5294 "});
5295 cx.dispatch_action(SelectPrevious::default());
5296 cx.assert_editor_state(indoc! {"
5297 «bbbˇ»
5298 ccc
5299
5300 bbb
5301 ccc
5302 "});
5303 cx.dispatch_action(SelectPrevious::default());
5304 cx.assert_editor_state(indoc! {"
5305 «bbbˇ»
5306 ccc
5307
5308 «bbbˇ»
5309 ccc
5310 "});
5311}
5312
5313#[gpui::test]
5314async fn test_select_previous_with_single_caret(cx: &mut gpui::TestAppContext) {
5315 init_test(cx, |_| {});
5316
5317 let mut cx = EditorTestContext::new(cx).await;
5318 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5319
5320 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5321 .unwrap();
5322 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5323
5324 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5325 .unwrap();
5326 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5327
5328 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5329 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5330
5331 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5332 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5333
5334 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5335 .unwrap();
5336 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
5337
5338 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5339 .unwrap();
5340 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
5341
5342 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5343 .unwrap();
5344 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5345}
5346
5347#[gpui::test]
5348async fn test_select_previous_with_multiple_carets(cx: &mut gpui::TestAppContext) {
5349 init_test(cx, |_| {});
5350
5351 let mut cx = EditorTestContext::new(cx).await;
5352 cx.set_state(
5353 r#"let foo = 2;
5354lˇet foo = 2;
5355let fooˇ = 2;
5356let foo = 2;
5357let foo = ˇ2;"#,
5358 );
5359
5360 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5361 .unwrap();
5362 cx.assert_editor_state(
5363 r#"let foo = 2;
5364«letˇ» foo = 2;
5365let «fooˇ» = 2;
5366let foo = 2;
5367let foo = «2ˇ»;"#,
5368 );
5369
5370 // noop for multiple selections with different contents
5371 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5372 .unwrap();
5373 cx.assert_editor_state(
5374 r#"let foo = 2;
5375«letˇ» foo = 2;
5376let «fooˇ» = 2;
5377let foo = 2;
5378let foo = «2ˇ»;"#,
5379 );
5380}
5381
5382#[gpui::test]
5383async fn test_select_previous_with_single_selection(cx: &mut gpui::TestAppContext) {
5384 init_test(cx, |_| {});
5385
5386 let mut cx = EditorTestContext::new(cx).await;
5387 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5388
5389 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5390 .unwrap();
5391 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5392
5393 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5394 .unwrap();
5395 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5396
5397 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5398 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5399
5400 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5401 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5402
5403 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5404 .unwrap();
5405 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
5406
5407 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5408 .unwrap();
5409 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5410}
5411
5412#[gpui::test]
5413async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
5414 init_test(cx, |_| {});
5415
5416 let language = Arc::new(Language::new(
5417 LanguageConfig::default(),
5418 Some(tree_sitter_rust::LANGUAGE.into()),
5419 ));
5420
5421 let text = r#"
5422 use mod1::mod2::{mod3, mod4};
5423
5424 fn fn_1(param1: bool, param2: &str) {
5425 let var1 = "text";
5426 }
5427 "#
5428 .unindent();
5429
5430 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5431 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5432 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5433
5434 editor
5435 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5436 .await;
5437
5438 editor.update_in(cx, |editor, window, cx| {
5439 editor.change_selections(None, window, cx, |s| {
5440 s.select_display_ranges([
5441 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
5442 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
5443 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
5444 ]);
5445 });
5446 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5447 });
5448 editor.update(cx, |editor, cx| {
5449 assert_text_with_selections(
5450 editor,
5451 indoc! {r#"
5452 use mod1::mod2::{mod3, «mod4ˇ»};
5453
5454 fn fn_1«ˇ(param1: bool, param2: &str)» {
5455 let var1 = "«textˇ»";
5456 }
5457 "#},
5458 cx,
5459 );
5460 });
5461
5462 editor.update_in(cx, |editor, window, cx| {
5463 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5464 });
5465 editor.update(cx, |editor, cx| {
5466 assert_text_with_selections(
5467 editor,
5468 indoc! {r#"
5469 use mod1::mod2::«{mod3, mod4}ˇ»;
5470
5471 «ˇfn fn_1(param1: bool, param2: &str) {
5472 let var1 = "text";
5473 }»
5474 "#},
5475 cx,
5476 );
5477 });
5478
5479 editor.update_in(cx, |editor, window, cx| {
5480 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5481 });
5482 assert_eq!(
5483 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
5484 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
5485 );
5486
5487 // Trying to expand the selected syntax node one more time has no effect.
5488 editor.update_in(cx, |editor, window, cx| {
5489 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5490 });
5491 assert_eq!(
5492 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
5493 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
5494 );
5495
5496 editor.update_in(cx, |editor, window, cx| {
5497 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5498 });
5499 editor.update(cx, |editor, cx| {
5500 assert_text_with_selections(
5501 editor,
5502 indoc! {r#"
5503 use mod1::mod2::«{mod3, mod4}ˇ»;
5504
5505 «ˇfn fn_1(param1: bool, param2: &str) {
5506 let var1 = "text";
5507 }»
5508 "#},
5509 cx,
5510 );
5511 });
5512
5513 editor.update_in(cx, |editor, window, cx| {
5514 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5515 });
5516 editor.update(cx, |editor, cx| {
5517 assert_text_with_selections(
5518 editor,
5519 indoc! {r#"
5520 use mod1::mod2::{mod3, «mod4ˇ»};
5521
5522 fn fn_1«ˇ(param1: bool, param2: &str)» {
5523 let var1 = "«textˇ»";
5524 }
5525 "#},
5526 cx,
5527 );
5528 });
5529
5530 editor.update_in(cx, |editor, window, cx| {
5531 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5532 });
5533 editor.update(cx, |editor, cx| {
5534 assert_text_with_selections(
5535 editor,
5536 indoc! {r#"
5537 use mod1::mod2::{mod3, mo«ˇ»d4};
5538
5539 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
5540 let var1 = "te«ˇ»xt";
5541 }
5542 "#},
5543 cx,
5544 );
5545 });
5546
5547 // Trying to shrink the selected syntax node one more time has no effect.
5548 editor.update_in(cx, |editor, window, cx| {
5549 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5550 });
5551 editor.update_in(cx, |editor, _, cx| {
5552 assert_text_with_selections(
5553 editor,
5554 indoc! {r#"
5555 use mod1::mod2::{mod3, mo«ˇ»d4};
5556
5557 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
5558 let var1 = "te«ˇ»xt";
5559 }
5560 "#},
5561 cx,
5562 );
5563 });
5564
5565 // Ensure that we keep expanding the selection if the larger selection starts or ends within
5566 // a fold.
5567 editor.update_in(cx, |editor, window, cx| {
5568 editor.fold_creases(
5569 vec![
5570 Crease::simple(
5571 Point::new(0, 21)..Point::new(0, 24),
5572 FoldPlaceholder::test(),
5573 ),
5574 Crease::simple(
5575 Point::new(3, 20)..Point::new(3, 22),
5576 FoldPlaceholder::test(),
5577 ),
5578 ],
5579 true,
5580 window,
5581 cx,
5582 );
5583 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5584 });
5585 editor.update(cx, |editor, cx| {
5586 assert_text_with_selections(
5587 editor,
5588 indoc! {r#"
5589 use mod1::mod2::«{mod3, mod4}ˇ»;
5590
5591 fn fn_1«ˇ(param1: bool, param2: &str)» {
5592 «let var1 = "text";ˇ»
5593 }
5594 "#},
5595 cx,
5596 );
5597 });
5598}
5599
5600#[gpui::test]
5601async fn test_fold_function_bodies(cx: &mut gpui::TestAppContext) {
5602 init_test(cx, |_| {});
5603
5604 let base_text = r#"
5605 impl A {
5606 // this is an unstaged comment
5607
5608 fn b() {
5609 c();
5610 }
5611
5612 // this is another unstaged comment
5613
5614 fn d() {
5615 // e
5616 // f
5617 }
5618 }
5619
5620 fn g() {
5621 // h
5622 }
5623 "#
5624 .unindent();
5625
5626 let text = r#"
5627 ˇimpl A {
5628
5629 fn b() {
5630 c();
5631 }
5632
5633 fn d() {
5634 // e
5635 // f
5636 }
5637 }
5638
5639 fn g() {
5640 // h
5641 }
5642 "#
5643 .unindent();
5644
5645 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
5646 cx.set_state(&text);
5647 cx.set_diff_base(&base_text);
5648 cx.update_editor(|editor, window, cx| {
5649 editor.expand_all_diff_hunks(&Default::default(), window, cx);
5650 });
5651
5652 cx.assert_state_with_diff(
5653 "
5654 ˇimpl A {
5655 - // this is an unstaged comment
5656
5657 fn b() {
5658 c();
5659 }
5660
5661 - // this is another unstaged comment
5662 -
5663 fn d() {
5664 // e
5665 // f
5666 }
5667 }
5668
5669 fn g() {
5670 // h
5671 }
5672 "
5673 .unindent(),
5674 );
5675
5676 let expected_display_text = "
5677 impl A {
5678 // this is an unstaged comment
5679
5680 fn b() {
5681 ⋯
5682 }
5683
5684 // this is another unstaged comment
5685
5686 fn d() {
5687 ⋯
5688 }
5689 }
5690
5691 fn g() {
5692 ⋯
5693 }
5694 "
5695 .unindent();
5696
5697 cx.update_editor(|editor, window, cx| {
5698 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
5699 assert_eq!(editor.display_text(cx), expected_display_text);
5700 });
5701}
5702
5703#[gpui::test]
5704async fn test_autoindent(cx: &mut gpui::TestAppContext) {
5705 init_test(cx, |_| {});
5706
5707 let language = Arc::new(
5708 Language::new(
5709 LanguageConfig {
5710 brackets: BracketPairConfig {
5711 pairs: vec![
5712 BracketPair {
5713 start: "{".to_string(),
5714 end: "}".to_string(),
5715 close: false,
5716 surround: false,
5717 newline: true,
5718 },
5719 BracketPair {
5720 start: "(".to_string(),
5721 end: ")".to_string(),
5722 close: false,
5723 surround: false,
5724 newline: true,
5725 },
5726 ],
5727 ..Default::default()
5728 },
5729 ..Default::default()
5730 },
5731 Some(tree_sitter_rust::LANGUAGE.into()),
5732 )
5733 .with_indents_query(
5734 r#"
5735 (_ "(" ")" @end) @indent
5736 (_ "{" "}" @end) @indent
5737 "#,
5738 )
5739 .unwrap(),
5740 );
5741
5742 let text = "fn a() {}";
5743
5744 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5745 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5746 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5747 editor
5748 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5749 .await;
5750
5751 editor.update_in(cx, |editor, window, cx| {
5752 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
5753 editor.newline(&Newline, window, cx);
5754 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
5755 assert_eq!(
5756 editor.selections.ranges(cx),
5757 &[
5758 Point::new(1, 4)..Point::new(1, 4),
5759 Point::new(3, 4)..Point::new(3, 4),
5760 Point::new(5, 0)..Point::new(5, 0)
5761 ]
5762 );
5763 });
5764}
5765
5766#[gpui::test]
5767async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
5768 init_test(cx, |_| {});
5769
5770 {
5771 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
5772 cx.set_state(indoc! {"
5773 impl A {
5774
5775 fn b() {}
5776
5777 «fn c() {
5778
5779 }ˇ»
5780 }
5781 "});
5782
5783 cx.update_editor(|editor, window, cx| {
5784 editor.autoindent(&Default::default(), window, cx);
5785 });
5786
5787 cx.assert_editor_state(indoc! {"
5788 impl A {
5789
5790 fn b() {}
5791
5792 «fn c() {
5793
5794 }ˇ»
5795 }
5796 "});
5797 }
5798
5799 {
5800 let mut cx = EditorTestContext::new_multibuffer(
5801 cx,
5802 [indoc! { "
5803 impl A {
5804 «
5805 // a
5806 fn b(){}
5807 »
5808 «
5809 }
5810 fn c(){}
5811 »
5812 "}],
5813 );
5814
5815 let buffer = cx.update_editor(|editor, _, cx| {
5816 let buffer = editor.buffer().update(cx, |buffer, _| {
5817 buffer.all_buffers().iter().next().unwrap().clone()
5818 });
5819 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5820 buffer
5821 });
5822
5823 cx.run_until_parked();
5824 cx.update_editor(|editor, window, cx| {
5825 editor.select_all(&Default::default(), window, cx);
5826 editor.autoindent(&Default::default(), window, cx)
5827 });
5828 cx.run_until_parked();
5829
5830 cx.update(|_, cx| {
5831 pretty_assertions::assert_eq!(
5832 buffer.read(cx).text(),
5833 indoc! { "
5834 impl A {
5835
5836 // a
5837 fn b(){}
5838
5839
5840 }
5841 fn c(){}
5842
5843 " }
5844 )
5845 });
5846 }
5847}
5848
5849#[gpui::test]
5850async fn test_autoclose_and_auto_surround_pairs(cx: &mut gpui::TestAppContext) {
5851 init_test(cx, |_| {});
5852
5853 let mut cx = EditorTestContext::new(cx).await;
5854
5855 let language = Arc::new(Language::new(
5856 LanguageConfig {
5857 brackets: BracketPairConfig {
5858 pairs: vec![
5859 BracketPair {
5860 start: "{".to_string(),
5861 end: "}".to_string(),
5862 close: true,
5863 surround: true,
5864 newline: true,
5865 },
5866 BracketPair {
5867 start: "(".to_string(),
5868 end: ")".to_string(),
5869 close: true,
5870 surround: true,
5871 newline: true,
5872 },
5873 BracketPair {
5874 start: "/*".to_string(),
5875 end: " */".to_string(),
5876 close: true,
5877 surround: true,
5878 newline: true,
5879 },
5880 BracketPair {
5881 start: "[".to_string(),
5882 end: "]".to_string(),
5883 close: false,
5884 surround: false,
5885 newline: true,
5886 },
5887 BracketPair {
5888 start: "\"".to_string(),
5889 end: "\"".to_string(),
5890 close: true,
5891 surround: true,
5892 newline: false,
5893 },
5894 BracketPair {
5895 start: "<".to_string(),
5896 end: ">".to_string(),
5897 close: false,
5898 surround: true,
5899 newline: true,
5900 },
5901 ],
5902 ..Default::default()
5903 },
5904 autoclose_before: "})]".to_string(),
5905 ..Default::default()
5906 },
5907 Some(tree_sitter_rust::LANGUAGE.into()),
5908 ));
5909
5910 cx.language_registry().add(language.clone());
5911 cx.update_buffer(|buffer, cx| {
5912 buffer.set_language(Some(language), cx);
5913 });
5914
5915 cx.set_state(
5916 &r#"
5917 🏀ˇ
5918 εˇ
5919 ❤️ˇ
5920 "#
5921 .unindent(),
5922 );
5923
5924 // autoclose multiple nested brackets at multiple cursors
5925 cx.update_editor(|editor, window, cx| {
5926 editor.handle_input("{", window, cx);
5927 editor.handle_input("{", window, cx);
5928 editor.handle_input("{", window, cx);
5929 });
5930 cx.assert_editor_state(
5931 &"
5932 🏀{{{ˇ}}}
5933 ε{{{ˇ}}}
5934 ❤️{{{ˇ}}}
5935 "
5936 .unindent(),
5937 );
5938
5939 // insert a different closing bracket
5940 cx.update_editor(|editor, window, cx| {
5941 editor.handle_input(")", window, cx);
5942 });
5943 cx.assert_editor_state(
5944 &"
5945 🏀{{{)ˇ}}}
5946 ε{{{)ˇ}}}
5947 ❤️{{{)ˇ}}}
5948 "
5949 .unindent(),
5950 );
5951
5952 // skip over the auto-closed brackets when typing a closing bracket
5953 cx.update_editor(|editor, window, cx| {
5954 editor.move_right(&MoveRight, window, cx);
5955 editor.handle_input("}", window, cx);
5956 editor.handle_input("}", window, cx);
5957 editor.handle_input("}", window, cx);
5958 });
5959 cx.assert_editor_state(
5960 &"
5961 🏀{{{)}}}}ˇ
5962 ε{{{)}}}}ˇ
5963 ❤️{{{)}}}}ˇ
5964 "
5965 .unindent(),
5966 );
5967
5968 // autoclose multi-character pairs
5969 cx.set_state(
5970 &"
5971 ˇ
5972 ˇ
5973 "
5974 .unindent(),
5975 );
5976 cx.update_editor(|editor, window, cx| {
5977 editor.handle_input("/", window, cx);
5978 editor.handle_input("*", window, cx);
5979 });
5980 cx.assert_editor_state(
5981 &"
5982 /*ˇ */
5983 /*ˇ */
5984 "
5985 .unindent(),
5986 );
5987
5988 // one cursor autocloses a multi-character pair, one cursor
5989 // does not autoclose.
5990 cx.set_state(
5991 &"
5992 /ˇ
5993 ˇ
5994 "
5995 .unindent(),
5996 );
5997 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
5998 cx.assert_editor_state(
5999 &"
6000 /*ˇ */
6001 *ˇ
6002 "
6003 .unindent(),
6004 );
6005
6006 // Don't autoclose if the next character isn't whitespace and isn't
6007 // listed in the language's "autoclose_before" section.
6008 cx.set_state("ˇa b");
6009 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6010 cx.assert_editor_state("{ˇa b");
6011
6012 // Don't autoclose if `close` is false for the bracket pair
6013 cx.set_state("ˇ");
6014 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6015 cx.assert_editor_state("[ˇ");
6016
6017 // Surround with brackets if text is selected
6018 cx.set_state("«aˇ» b");
6019 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6020 cx.assert_editor_state("{«aˇ»} b");
6021
6022 // Autclose pair where the start and end characters are the same
6023 cx.set_state("aˇ");
6024 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6025 cx.assert_editor_state("a\"ˇ\"");
6026 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6027 cx.assert_editor_state("a\"\"ˇ");
6028
6029 // Don't autoclose pair if autoclose is disabled
6030 cx.set_state("ˇ");
6031 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6032 cx.assert_editor_state("<ˇ");
6033
6034 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6035 cx.set_state("«aˇ» b");
6036 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6037 cx.assert_editor_state("<«aˇ»> b");
6038}
6039
6040#[gpui::test]
6041async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut gpui::TestAppContext) {
6042 init_test(cx, |settings| {
6043 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6044 });
6045
6046 let mut cx = EditorTestContext::new(cx).await;
6047
6048 let language = Arc::new(Language::new(
6049 LanguageConfig {
6050 brackets: BracketPairConfig {
6051 pairs: vec![
6052 BracketPair {
6053 start: "{".to_string(),
6054 end: "}".to_string(),
6055 close: true,
6056 surround: true,
6057 newline: true,
6058 },
6059 BracketPair {
6060 start: "(".to_string(),
6061 end: ")".to_string(),
6062 close: true,
6063 surround: true,
6064 newline: true,
6065 },
6066 BracketPair {
6067 start: "[".to_string(),
6068 end: "]".to_string(),
6069 close: false,
6070 surround: false,
6071 newline: true,
6072 },
6073 ],
6074 ..Default::default()
6075 },
6076 autoclose_before: "})]".to_string(),
6077 ..Default::default()
6078 },
6079 Some(tree_sitter_rust::LANGUAGE.into()),
6080 ));
6081
6082 cx.language_registry().add(language.clone());
6083 cx.update_buffer(|buffer, cx| {
6084 buffer.set_language(Some(language), cx);
6085 });
6086
6087 cx.set_state(
6088 &"
6089 ˇ
6090 ˇ
6091 ˇ
6092 "
6093 .unindent(),
6094 );
6095
6096 // ensure only matching closing brackets are skipped over
6097 cx.update_editor(|editor, window, cx| {
6098 editor.handle_input("}", window, cx);
6099 editor.move_left(&MoveLeft, window, cx);
6100 editor.handle_input(")", window, cx);
6101 editor.move_left(&MoveLeft, window, cx);
6102 });
6103 cx.assert_editor_state(
6104 &"
6105 ˇ)}
6106 ˇ)}
6107 ˇ)}
6108 "
6109 .unindent(),
6110 );
6111
6112 // skip-over closing brackets at multiple cursors
6113 cx.update_editor(|editor, window, cx| {
6114 editor.handle_input(")", window, cx);
6115 editor.handle_input("}", window, cx);
6116 });
6117 cx.assert_editor_state(
6118 &"
6119 )}ˇ
6120 )}ˇ
6121 )}ˇ
6122 "
6123 .unindent(),
6124 );
6125
6126 // ignore non-close brackets
6127 cx.update_editor(|editor, window, cx| {
6128 editor.handle_input("]", window, cx);
6129 editor.move_left(&MoveLeft, window, cx);
6130 editor.handle_input("]", window, cx);
6131 });
6132 cx.assert_editor_state(
6133 &"
6134 )}]ˇ]
6135 )}]ˇ]
6136 )}]ˇ]
6137 "
6138 .unindent(),
6139 );
6140}
6141
6142#[gpui::test]
6143async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
6144 init_test(cx, |_| {});
6145
6146 let mut cx = EditorTestContext::new(cx).await;
6147
6148 let html_language = Arc::new(
6149 Language::new(
6150 LanguageConfig {
6151 name: "HTML".into(),
6152 brackets: BracketPairConfig {
6153 pairs: vec![
6154 BracketPair {
6155 start: "<".into(),
6156 end: ">".into(),
6157 close: true,
6158 ..Default::default()
6159 },
6160 BracketPair {
6161 start: "{".into(),
6162 end: "}".into(),
6163 close: true,
6164 ..Default::default()
6165 },
6166 BracketPair {
6167 start: "(".into(),
6168 end: ")".into(),
6169 close: true,
6170 ..Default::default()
6171 },
6172 ],
6173 ..Default::default()
6174 },
6175 autoclose_before: "})]>".into(),
6176 ..Default::default()
6177 },
6178 Some(tree_sitter_html::language()),
6179 )
6180 .with_injection_query(
6181 r#"
6182 (script_element
6183 (raw_text) @injection.content
6184 (#set! injection.language "javascript"))
6185 "#,
6186 )
6187 .unwrap(),
6188 );
6189
6190 let javascript_language = Arc::new(Language::new(
6191 LanguageConfig {
6192 name: "JavaScript".into(),
6193 brackets: BracketPairConfig {
6194 pairs: vec![
6195 BracketPair {
6196 start: "/*".into(),
6197 end: " */".into(),
6198 close: true,
6199 ..Default::default()
6200 },
6201 BracketPair {
6202 start: "{".into(),
6203 end: "}".into(),
6204 close: true,
6205 ..Default::default()
6206 },
6207 BracketPair {
6208 start: "(".into(),
6209 end: ")".into(),
6210 close: true,
6211 ..Default::default()
6212 },
6213 ],
6214 ..Default::default()
6215 },
6216 autoclose_before: "})]>".into(),
6217 ..Default::default()
6218 },
6219 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6220 ));
6221
6222 cx.language_registry().add(html_language.clone());
6223 cx.language_registry().add(javascript_language.clone());
6224
6225 cx.update_buffer(|buffer, cx| {
6226 buffer.set_language(Some(html_language), cx);
6227 });
6228
6229 cx.set_state(
6230 &r#"
6231 <body>ˇ
6232 <script>
6233 var x = 1;ˇ
6234 </script>
6235 </body>ˇ
6236 "#
6237 .unindent(),
6238 );
6239
6240 // Precondition: different languages are active at different locations.
6241 cx.update_editor(|editor, window, cx| {
6242 let snapshot = editor.snapshot(window, cx);
6243 let cursors = editor.selections.ranges::<usize>(cx);
6244 let languages = cursors
6245 .iter()
6246 .map(|c| snapshot.language_at(c.start).unwrap().name())
6247 .collect::<Vec<_>>();
6248 assert_eq!(
6249 languages,
6250 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6251 );
6252 });
6253
6254 // Angle brackets autoclose in HTML, but not JavaScript.
6255 cx.update_editor(|editor, window, cx| {
6256 editor.handle_input("<", window, cx);
6257 editor.handle_input("a", window, cx);
6258 });
6259 cx.assert_editor_state(
6260 &r#"
6261 <body><aˇ>
6262 <script>
6263 var x = 1;<aˇ
6264 </script>
6265 </body><aˇ>
6266 "#
6267 .unindent(),
6268 );
6269
6270 // Curly braces and parens autoclose in both HTML and JavaScript.
6271 cx.update_editor(|editor, window, cx| {
6272 editor.handle_input(" b=", window, cx);
6273 editor.handle_input("{", window, cx);
6274 editor.handle_input("c", window, cx);
6275 editor.handle_input("(", window, cx);
6276 });
6277 cx.assert_editor_state(
6278 &r#"
6279 <body><a b={c(ˇ)}>
6280 <script>
6281 var x = 1;<a b={c(ˇ)}
6282 </script>
6283 </body><a b={c(ˇ)}>
6284 "#
6285 .unindent(),
6286 );
6287
6288 // Brackets that were already autoclosed are skipped.
6289 cx.update_editor(|editor, window, cx| {
6290 editor.handle_input(")", window, cx);
6291 editor.handle_input("d", window, cx);
6292 editor.handle_input("}", window, cx);
6293 });
6294 cx.assert_editor_state(
6295 &r#"
6296 <body><a b={c()d}ˇ>
6297 <script>
6298 var x = 1;<a b={c()d}ˇ
6299 </script>
6300 </body><a b={c()d}ˇ>
6301 "#
6302 .unindent(),
6303 );
6304 cx.update_editor(|editor, window, cx| {
6305 editor.handle_input(">", window, cx);
6306 });
6307 cx.assert_editor_state(
6308 &r#"
6309 <body><a b={c()d}>ˇ
6310 <script>
6311 var x = 1;<a b={c()d}>ˇ
6312 </script>
6313 </body><a b={c()d}>ˇ
6314 "#
6315 .unindent(),
6316 );
6317
6318 // Reset
6319 cx.set_state(
6320 &r#"
6321 <body>ˇ
6322 <script>
6323 var x = 1;ˇ
6324 </script>
6325 </body>ˇ
6326 "#
6327 .unindent(),
6328 );
6329
6330 cx.update_editor(|editor, window, cx| {
6331 editor.handle_input("<", window, cx);
6332 });
6333 cx.assert_editor_state(
6334 &r#"
6335 <body><ˇ>
6336 <script>
6337 var x = 1;<ˇ
6338 </script>
6339 </body><ˇ>
6340 "#
6341 .unindent(),
6342 );
6343
6344 // When backspacing, the closing angle brackets are removed.
6345 cx.update_editor(|editor, window, cx| {
6346 editor.backspace(&Backspace, window, cx);
6347 });
6348 cx.assert_editor_state(
6349 &r#"
6350 <body>ˇ
6351 <script>
6352 var x = 1;ˇ
6353 </script>
6354 </body>ˇ
6355 "#
6356 .unindent(),
6357 );
6358
6359 // Block comments autoclose in JavaScript, but not HTML.
6360 cx.update_editor(|editor, window, cx| {
6361 editor.handle_input("/", window, cx);
6362 editor.handle_input("*", 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
6376#[gpui::test]
6377async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
6378 init_test(cx, |_| {});
6379
6380 let mut cx = EditorTestContext::new(cx).await;
6381
6382 let rust_language = Arc::new(
6383 Language::new(
6384 LanguageConfig {
6385 name: "Rust".into(),
6386 brackets: serde_json::from_value(json!([
6387 { "start": "{", "end": "}", "close": true, "newline": true },
6388 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
6389 ]))
6390 .unwrap(),
6391 autoclose_before: "})]>".into(),
6392 ..Default::default()
6393 },
6394 Some(tree_sitter_rust::LANGUAGE.into()),
6395 )
6396 .with_override_query("(string_literal) @string")
6397 .unwrap(),
6398 );
6399
6400 cx.language_registry().add(rust_language.clone());
6401 cx.update_buffer(|buffer, cx| {
6402 buffer.set_language(Some(rust_language), cx);
6403 });
6404
6405 cx.set_state(
6406 &r#"
6407 let x = ˇ
6408 "#
6409 .unindent(),
6410 );
6411
6412 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
6413 cx.update_editor(|editor, window, cx| {
6414 editor.handle_input("\"", window, cx);
6415 });
6416 cx.assert_editor_state(
6417 &r#"
6418 let x = "ˇ"
6419 "#
6420 .unindent(),
6421 );
6422
6423 // Inserting another quotation mark. The cursor moves across the existing
6424 // automatically-inserted quotation mark.
6425 cx.update_editor(|editor, window, cx| {
6426 editor.handle_input("\"", window, cx);
6427 });
6428 cx.assert_editor_state(
6429 &r#"
6430 let x = ""ˇ
6431 "#
6432 .unindent(),
6433 );
6434
6435 // Reset
6436 cx.set_state(
6437 &r#"
6438 let x = ˇ
6439 "#
6440 .unindent(),
6441 );
6442
6443 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
6444 cx.update_editor(|editor, window, cx| {
6445 editor.handle_input("\"", window, cx);
6446 editor.handle_input(" ", window, cx);
6447 editor.move_left(&Default::default(), window, cx);
6448 editor.handle_input("\\", window, cx);
6449 editor.handle_input("\"", window, cx);
6450 });
6451 cx.assert_editor_state(
6452 &r#"
6453 let x = "\"ˇ "
6454 "#
6455 .unindent(),
6456 );
6457
6458 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
6459 // mark. Nothing is inserted.
6460 cx.update_editor(|editor, window, cx| {
6461 editor.move_right(&Default::default(), window, cx);
6462 editor.handle_input("\"", window, cx);
6463 });
6464 cx.assert_editor_state(
6465 &r#"
6466 let x = "\" "ˇ
6467 "#
6468 .unindent(),
6469 );
6470}
6471
6472#[gpui::test]
6473async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
6474 init_test(cx, |_| {});
6475
6476 let language = Arc::new(Language::new(
6477 LanguageConfig {
6478 brackets: BracketPairConfig {
6479 pairs: vec![
6480 BracketPair {
6481 start: "{".to_string(),
6482 end: "}".to_string(),
6483 close: true,
6484 surround: true,
6485 newline: true,
6486 },
6487 BracketPair {
6488 start: "/* ".to_string(),
6489 end: "*/".to_string(),
6490 close: true,
6491 surround: true,
6492 ..Default::default()
6493 },
6494 ],
6495 ..Default::default()
6496 },
6497 ..Default::default()
6498 },
6499 Some(tree_sitter_rust::LANGUAGE.into()),
6500 ));
6501
6502 let text = r#"
6503 a
6504 b
6505 c
6506 "#
6507 .unindent();
6508
6509 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6510 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6511 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6512 editor
6513 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6514 .await;
6515
6516 editor.update_in(cx, |editor, window, cx| {
6517 editor.change_selections(None, window, cx, |s| {
6518 s.select_display_ranges([
6519 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6520 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6521 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
6522 ])
6523 });
6524
6525 editor.handle_input("{", window, cx);
6526 editor.handle_input("{", window, cx);
6527 editor.handle_input("{", window, cx);
6528 assert_eq!(
6529 editor.text(cx),
6530 "
6531 {{{a}}}
6532 {{{b}}}
6533 {{{c}}}
6534 "
6535 .unindent()
6536 );
6537 assert_eq!(
6538 editor.selections.display_ranges(cx),
6539 [
6540 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
6541 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
6542 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
6543 ]
6544 );
6545
6546 editor.undo(&Undo, window, cx);
6547 editor.undo(&Undo, window, cx);
6548 editor.undo(&Undo, window, cx);
6549 assert_eq!(
6550 editor.text(cx),
6551 "
6552 a
6553 b
6554 c
6555 "
6556 .unindent()
6557 );
6558 assert_eq!(
6559 editor.selections.display_ranges(cx),
6560 [
6561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6562 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6563 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
6564 ]
6565 );
6566
6567 // Ensure inserting the first character of a multi-byte bracket pair
6568 // doesn't surround the selections with the bracket.
6569 editor.handle_input("/", window, cx);
6570 assert_eq!(
6571 editor.text(cx),
6572 "
6573 /
6574 /
6575 /
6576 "
6577 .unindent()
6578 );
6579 assert_eq!(
6580 editor.selections.display_ranges(cx),
6581 [
6582 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6583 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6584 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
6585 ]
6586 );
6587
6588 editor.undo(&Undo, window, cx);
6589 assert_eq!(
6590 editor.text(cx),
6591 "
6592 a
6593 b
6594 c
6595 "
6596 .unindent()
6597 );
6598 assert_eq!(
6599 editor.selections.display_ranges(cx),
6600 [
6601 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6602 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6603 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
6604 ]
6605 );
6606
6607 // Ensure inserting the last character of a multi-byte bracket pair
6608 // doesn't surround the selections with the bracket.
6609 editor.handle_input("*", window, cx);
6610 assert_eq!(
6611 editor.text(cx),
6612 "
6613 *
6614 *
6615 *
6616 "
6617 .unindent()
6618 );
6619 assert_eq!(
6620 editor.selections.display_ranges(cx),
6621 [
6622 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6623 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6624 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
6625 ]
6626 );
6627 });
6628}
6629
6630#[gpui::test]
6631async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
6632 init_test(cx, |_| {});
6633
6634 let language = Arc::new(Language::new(
6635 LanguageConfig {
6636 brackets: BracketPairConfig {
6637 pairs: vec![BracketPair {
6638 start: "{".to_string(),
6639 end: "}".to_string(),
6640 close: true,
6641 surround: true,
6642 newline: true,
6643 }],
6644 ..Default::default()
6645 },
6646 autoclose_before: "}".to_string(),
6647 ..Default::default()
6648 },
6649 Some(tree_sitter_rust::LANGUAGE.into()),
6650 ));
6651
6652 let text = r#"
6653 a
6654 b
6655 c
6656 "#
6657 .unindent();
6658
6659 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6660 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6661 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6662 editor
6663 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6664 .await;
6665
6666 editor.update_in(cx, |editor, window, cx| {
6667 editor.change_selections(None, window, cx, |s| {
6668 s.select_ranges([
6669 Point::new(0, 1)..Point::new(0, 1),
6670 Point::new(1, 1)..Point::new(1, 1),
6671 Point::new(2, 1)..Point::new(2, 1),
6672 ])
6673 });
6674
6675 editor.handle_input("{", window, cx);
6676 editor.handle_input("{", window, cx);
6677 editor.handle_input("_", window, cx);
6678 assert_eq!(
6679 editor.text(cx),
6680 "
6681 a{{_}}
6682 b{{_}}
6683 c{{_}}
6684 "
6685 .unindent()
6686 );
6687 assert_eq!(
6688 editor.selections.ranges::<Point>(cx),
6689 [
6690 Point::new(0, 4)..Point::new(0, 4),
6691 Point::new(1, 4)..Point::new(1, 4),
6692 Point::new(2, 4)..Point::new(2, 4)
6693 ]
6694 );
6695
6696 editor.backspace(&Default::default(), window, cx);
6697 editor.backspace(&Default::default(), window, cx);
6698 assert_eq!(
6699 editor.text(cx),
6700 "
6701 a{}
6702 b{}
6703 c{}
6704 "
6705 .unindent()
6706 );
6707 assert_eq!(
6708 editor.selections.ranges::<Point>(cx),
6709 [
6710 Point::new(0, 2)..Point::new(0, 2),
6711 Point::new(1, 2)..Point::new(1, 2),
6712 Point::new(2, 2)..Point::new(2, 2)
6713 ]
6714 );
6715
6716 editor.delete_to_previous_word_start(&Default::default(), window, cx);
6717 assert_eq!(
6718 editor.text(cx),
6719 "
6720 a
6721 b
6722 c
6723 "
6724 .unindent()
6725 );
6726 assert_eq!(
6727 editor.selections.ranges::<Point>(cx),
6728 [
6729 Point::new(0, 1)..Point::new(0, 1),
6730 Point::new(1, 1)..Point::new(1, 1),
6731 Point::new(2, 1)..Point::new(2, 1)
6732 ]
6733 );
6734 });
6735}
6736
6737#[gpui::test]
6738async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut gpui::TestAppContext) {
6739 init_test(cx, |settings| {
6740 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6741 });
6742
6743 let mut cx = EditorTestContext::new(cx).await;
6744
6745 let language = Arc::new(Language::new(
6746 LanguageConfig {
6747 brackets: BracketPairConfig {
6748 pairs: vec![
6749 BracketPair {
6750 start: "{".to_string(),
6751 end: "}".to_string(),
6752 close: true,
6753 surround: true,
6754 newline: true,
6755 },
6756 BracketPair {
6757 start: "(".to_string(),
6758 end: ")".to_string(),
6759 close: true,
6760 surround: true,
6761 newline: true,
6762 },
6763 BracketPair {
6764 start: "[".to_string(),
6765 end: "]".to_string(),
6766 close: false,
6767 surround: true,
6768 newline: true,
6769 },
6770 ],
6771 ..Default::default()
6772 },
6773 autoclose_before: "})]".to_string(),
6774 ..Default::default()
6775 },
6776 Some(tree_sitter_rust::LANGUAGE.into()),
6777 ));
6778
6779 cx.language_registry().add(language.clone());
6780 cx.update_buffer(|buffer, cx| {
6781 buffer.set_language(Some(language), cx);
6782 });
6783
6784 cx.set_state(
6785 &"
6786 {(ˇ)}
6787 [[ˇ]]
6788 {(ˇ)}
6789 "
6790 .unindent(),
6791 );
6792
6793 cx.update_editor(|editor, window, cx| {
6794 editor.backspace(&Default::default(), window, cx);
6795 editor.backspace(&Default::default(), window, cx);
6796 });
6797
6798 cx.assert_editor_state(
6799 &"
6800 ˇ
6801 ˇ]]
6802 ˇ
6803 "
6804 .unindent(),
6805 );
6806
6807 cx.update_editor(|editor, window, cx| {
6808 editor.handle_input("{", window, cx);
6809 editor.handle_input("{", window, cx);
6810 editor.move_right(&MoveRight, window, cx);
6811 editor.move_right(&MoveRight, window, cx);
6812 editor.move_left(&MoveLeft, window, cx);
6813 editor.move_left(&MoveLeft, window, cx);
6814 editor.backspace(&Default::default(), window, cx);
6815 });
6816
6817 cx.assert_editor_state(
6818 &"
6819 {ˇ}
6820 {ˇ}]]
6821 {ˇ}
6822 "
6823 .unindent(),
6824 );
6825
6826 cx.update_editor(|editor, window, cx| {
6827 editor.backspace(&Default::default(), window, cx);
6828 });
6829
6830 cx.assert_editor_state(
6831 &"
6832 ˇ
6833 ˇ]]
6834 ˇ
6835 "
6836 .unindent(),
6837 );
6838}
6839
6840#[gpui::test]
6841async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) {
6842 init_test(cx, |_| {});
6843
6844 let language = Arc::new(Language::new(
6845 LanguageConfig::default(),
6846 Some(tree_sitter_rust::LANGUAGE.into()),
6847 ));
6848
6849 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
6850 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6851 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6852 editor
6853 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6854 .await;
6855
6856 editor.update_in(cx, |editor, window, cx| {
6857 editor.set_auto_replace_emoji_shortcode(true);
6858
6859 editor.handle_input("Hello ", window, cx);
6860 editor.handle_input(":wave", window, cx);
6861 assert_eq!(editor.text(cx), "Hello :wave".unindent());
6862
6863 editor.handle_input(":", window, cx);
6864 assert_eq!(editor.text(cx), "Hello 👋".unindent());
6865
6866 editor.handle_input(" :smile", window, cx);
6867 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
6868
6869 editor.handle_input(":", window, cx);
6870 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
6871
6872 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
6873 editor.handle_input(":wave", window, cx);
6874 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
6875
6876 editor.handle_input(":", window, cx);
6877 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
6878
6879 editor.handle_input(":1", window, cx);
6880 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
6881
6882 editor.handle_input(":", window, cx);
6883 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
6884
6885 // Ensure shortcode does not get replaced when it is part of a word
6886 editor.handle_input(" Test:wave", window, cx);
6887 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
6888
6889 editor.handle_input(":", window, cx);
6890 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
6891
6892 editor.set_auto_replace_emoji_shortcode(false);
6893
6894 // Ensure shortcode does not get replaced when auto replace is off
6895 editor.handle_input(" :wave", window, cx);
6896 assert_eq!(
6897 editor.text(cx),
6898 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
6899 );
6900
6901 editor.handle_input(":", window, cx);
6902 assert_eq!(
6903 editor.text(cx),
6904 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
6905 );
6906 });
6907}
6908
6909#[gpui::test]
6910async fn test_snippet_placeholder_choices(cx: &mut gpui::TestAppContext) {
6911 init_test(cx, |_| {});
6912
6913 let (text, insertion_ranges) = marked_text_ranges(
6914 indoc! {"
6915 ˇ
6916 "},
6917 false,
6918 );
6919
6920 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
6921 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6922
6923 _ = editor.update_in(cx, |editor, window, cx| {
6924 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
6925
6926 editor
6927 .insert_snippet(&insertion_ranges, snippet, window, cx)
6928 .unwrap();
6929
6930 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
6931 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
6932 assert_eq!(editor.text(cx), expected_text);
6933 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
6934 }
6935
6936 assert(
6937 editor,
6938 cx,
6939 indoc! {"
6940 type «» =•
6941 "},
6942 );
6943
6944 assert!(editor.context_menu_visible(), "There should be a matches");
6945 });
6946}
6947
6948#[gpui::test]
6949async fn test_snippets(cx: &mut gpui::TestAppContext) {
6950 init_test(cx, |_| {});
6951
6952 let (text, insertion_ranges) = marked_text_ranges(
6953 indoc! {"
6954 a.ˇ b
6955 a.ˇ b
6956 a.ˇ b
6957 "},
6958 false,
6959 );
6960
6961 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
6962 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6963
6964 editor.update_in(cx, |editor, window, cx| {
6965 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
6966
6967 editor
6968 .insert_snippet(&insertion_ranges, snippet, window, cx)
6969 .unwrap();
6970
6971 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
6972 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
6973 assert_eq!(editor.text(cx), expected_text);
6974 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
6975 }
6976
6977 assert(
6978 editor,
6979 cx,
6980 indoc! {"
6981 a.f(«one», two, «three») b
6982 a.f(«one», two, «three») b
6983 a.f(«one», two, «three») b
6984 "},
6985 );
6986
6987 // Can't move earlier than the first tab stop
6988 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
6989 assert(
6990 editor,
6991 cx,
6992 indoc! {"
6993 a.f(«one», two, «three») b
6994 a.f(«one», two, «three») b
6995 a.f(«one», two, «three») b
6996 "},
6997 );
6998
6999 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7000 assert(
7001 editor,
7002 cx,
7003 indoc! {"
7004 a.f(one, «two», three) b
7005 a.f(one, «two», three) b
7006 a.f(one, «two», three) b
7007 "},
7008 );
7009
7010 editor.move_to_prev_snippet_tabstop(window, cx);
7011 assert(
7012 editor,
7013 cx,
7014 indoc! {"
7015 a.f(«one», two, «three») b
7016 a.f(«one», two, «three») b
7017 a.f(«one», two, «three») b
7018 "},
7019 );
7020
7021 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7022 assert(
7023 editor,
7024 cx,
7025 indoc! {"
7026 a.f(one, «two», three) b
7027 a.f(one, «two», three) b
7028 a.f(one, «two», three) b
7029 "},
7030 );
7031 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7032 assert(
7033 editor,
7034 cx,
7035 indoc! {"
7036 a.f(one, two, three)ˇ b
7037 a.f(one, two, three)ˇ b
7038 a.f(one, two, three)ˇ b
7039 "},
7040 );
7041
7042 // As soon as the last tab stop is reached, snippet state is gone
7043 editor.move_to_prev_snippet_tabstop(window, cx);
7044 assert(
7045 editor,
7046 cx,
7047 indoc! {"
7048 a.f(one, two, three)ˇ b
7049 a.f(one, two, three)ˇ b
7050 a.f(one, two, three)ˇ b
7051 "},
7052 );
7053 });
7054}
7055
7056#[gpui::test]
7057async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
7058 init_test(cx, |_| {});
7059
7060 let fs = FakeFs::new(cx.executor());
7061 fs.insert_file("/file.rs", Default::default()).await;
7062
7063 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
7064
7065 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7066 language_registry.add(rust_lang());
7067 let mut fake_servers = language_registry.register_fake_lsp(
7068 "Rust",
7069 FakeLspAdapter {
7070 capabilities: lsp::ServerCapabilities {
7071 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7072 ..Default::default()
7073 },
7074 ..Default::default()
7075 },
7076 );
7077
7078 let buffer = project
7079 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
7080 .await
7081 .unwrap();
7082
7083 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7084 let (editor, cx) = cx.add_window_view(|window, cx| {
7085 build_editor_with_project(project.clone(), buffer, window, cx)
7086 });
7087 editor.update_in(cx, |editor, window, cx| {
7088 editor.set_text("one\ntwo\nthree\n", window, cx)
7089 });
7090 assert!(cx.read(|cx| editor.is_dirty(cx)));
7091
7092 cx.executor().start_waiting();
7093 let fake_server = fake_servers.next().await.unwrap();
7094
7095 let save = editor
7096 .update_in(cx, |editor, window, cx| {
7097 editor.save(true, project.clone(), window, cx)
7098 })
7099 .unwrap();
7100 fake_server
7101 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7102 assert_eq!(
7103 params.text_document.uri,
7104 lsp::Url::from_file_path("/file.rs").unwrap()
7105 );
7106 assert_eq!(params.options.tab_size, 4);
7107 Ok(Some(vec![lsp::TextEdit::new(
7108 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7109 ", ".to_string(),
7110 )]))
7111 })
7112 .next()
7113 .await;
7114 cx.executor().start_waiting();
7115 save.await;
7116
7117 assert_eq!(
7118 editor.update(cx, |editor, cx| editor.text(cx)),
7119 "one, two\nthree\n"
7120 );
7121 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7122
7123 editor.update_in(cx, |editor, window, cx| {
7124 editor.set_text("one\ntwo\nthree\n", window, cx)
7125 });
7126 assert!(cx.read(|cx| editor.is_dirty(cx)));
7127
7128 // Ensure we can still save even if formatting hangs.
7129 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7130 assert_eq!(
7131 params.text_document.uri,
7132 lsp::Url::from_file_path("/file.rs").unwrap()
7133 );
7134 futures::future::pending::<()>().await;
7135 unreachable!()
7136 });
7137 let save = editor
7138 .update_in(cx, |editor, window, cx| {
7139 editor.save(true, project.clone(), window, cx)
7140 })
7141 .unwrap();
7142 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7143 cx.executor().start_waiting();
7144 save.await;
7145 assert_eq!(
7146 editor.update(cx, |editor, cx| editor.text(cx)),
7147 "one\ntwo\nthree\n"
7148 );
7149 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7150
7151 // For non-dirty buffer, no formatting request should be sent
7152 let save = editor
7153 .update_in(cx, |editor, window, cx| {
7154 editor.save(true, project.clone(), window, cx)
7155 })
7156 .unwrap();
7157 let _pending_format_request = fake_server
7158 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7159 panic!("Should not be invoked on non-dirty buffer");
7160 })
7161 .next();
7162 cx.executor().start_waiting();
7163 save.await;
7164
7165 // Set rust language override and assert overridden tabsize is sent to language server
7166 update_test_language_settings(cx, |settings| {
7167 settings.languages.insert(
7168 "Rust".into(),
7169 LanguageSettingsContent {
7170 tab_size: NonZeroU32::new(8),
7171 ..Default::default()
7172 },
7173 );
7174 });
7175
7176 editor.update_in(cx, |editor, window, cx| {
7177 editor.set_text("somehting_new\n", window, cx)
7178 });
7179 assert!(cx.read(|cx| editor.is_dirty(cx)));
7180 let save = editor
7181 .update_in(cx, |editor, window, cx| {
7182 editor.save(true, project.clone(), window, cx)
7183 })
7184 .unwrap();
7185 fake_server
7186 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7187 assert_eq!(
7188 params.text_document.uri,
7189 lsp::Url::from_file_path("/file.rs").unwrap()
7190 );
7191 assert_eq!(params.options.tab_size, 8);
7192 Ok(Some(vec![]))
7193 })
7194 .next()
7195 .await;
7196 cx.executor().start_waiting();
7197 save.await;
7198}
7199
7200#[gpui::test]
7201async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) {
7202 init_test(cx, |_| {});
7203
7204 let cols = 4;
7205 let rows = 10;
7206 let sample_text_1 = sample_text(rows, cols, 'a');
7207 assert_eq!(
7208 sample_text_1,
7209 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7210 );
7211 let sample_text_2 = sample_text(rows, cols, 'l');
7212 assert_eq!(
7213 sample_text_2,
7214 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7215 );
7216 let sample_text_3 = sample_text(rows, cols, 'v');
7217 assert_eq!(
7218 sample_text_3,
7219 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7220 );
7221
7222 let fs = FakeFs::new(cx.executor());
7223 fs.insert_tree(
7224 "/a",
7225 json!({
7226 "main.rs": sample_text_1,
7227 "other.rs": sample_text_2,
7228 "lib.rs": sample_text_3,
7229 }),
7230 )
7231 .await;
7232
7233 let project = Project::test(fs, ["/a".as_ref()], cx).await;
7234 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7235 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7236
7237 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7238 language_registry.add(rust_lang());
7239 let mut fake_servers = language_registry.register_fake_lsp(
7240 "Rust",
7241 FakeLspAdapter {
7242 capabilities: lsp::ServerCapabilities {
7243 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7244 ..Default::default()
7245 },
7246 ..Default::default()
7247 },
7248 );
7249
7250 let worktree = project.update(cx, |project, cx| {
7251 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7252 assert_eq!(worktrees.len(), 1);
7253 worktrees.pop().unwrap()
7254 });
7255 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
7256
7257 let buffer_1 = project
7258 .update(cx, |project, cx| {
7259 project.open_buffer((worktree_id, "main.rs"), cx)
7260 })
7261 .await
7262 .unwrap();
7263 let buffer_2 = project
7264 .update(cx, |project, cx| {
7265 project.open_buffer((worktree_id, "other.rs"), cx)
7266 })
7267 .await
7268 .unwrap();
7269 let buffer_3 = project
7270 .update(cx, |project, cx| {
7271 project.open_buffer((worktree_id, "lib.rs"), cx)
7272 })
7273 .await
7274 .unwrap();
7275
7276 let multi_buffer = cx.new(|cx| {
7277 let mut multi_buffer = MultiBuffer::new(ReadWrite);
7278 multi_buffer.push_excerpts(
7279 buffer_1.clone(),
7280 [
7281 ExcerptRange {
7282 context: Point::new(0, 0)..Point::new(3, 0),
7283 primary: None,
7284 },
7285 ExcerptRange {
7286 context: Point::new(5, 0)..Point::new(7, 0),
7287 primary: None,
7288 },
7289 ExcerptRange {
7290 context: Point::new(9, 0)..Point::new(10, 4),
7291 primary: None,
7292 },
7293 ],
7294 cx,
7295 );
7296 multi_buffer.push_excerpts(
7297 buffer_2.clone(),
7298 [
7299 ExcerptRange {
7300 context: Point::new(0, 0)..Point::new(3, 0),
7301 primary: None,
7302 },
7303 ExcerptRange {
7304 context: Point::new(5, 0)..Point::new(7, 0),
7305 primary: None,
7306 },
7307 ExcerptRange {
7308 context: Point::new(9, 0)..Point::new(10, 4),
7309 primary: None,
7310 },
7311 ],
7312 cx,
7313 );
7314 multi_buffer.push_excerpts(
7315 buffer_3.clone(),
7316 [
7317 ExcerptRange {
7318 context: Point::new(0, 0)..Point::new(3, 0),
7319 primary: None,
7320 },
7321 ExcerptRange {
7322 context: Point::new(5, 0)..Point::new(7, 0),
7323 primary: None,
7324 },
7325 ExcerptRange {
7326 context: Point::new(9, 0)..Point::new(10, 4),
7327 primary: None,
7328 },
7329 ],
7330 cx,
7331 );
7332 multi_buffer
7333 });
7334 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
7335 Editor::new(
7336 EditorMode::Full,
7337 multi_buffer,
7338 Some(project.clone()),
7339 true,
7340 window,
7341 cx,
7342 )
7343 });
7344
7345 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7346 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7347 s.select_ranges(Some(1..2))
7348 });
7349 editor.insert("|one|two|three|", window, cx);
7350 });
7351 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7352 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7353 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7354 s.select_ranges(Some(60..70))
7355 });
7356 editor.insert("|four|five|six|", window, cx);
7357 });
7358 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7359
7360 // First two buffers should be edited, but not the third one.
7361 assert_eq!(
7362 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7363 "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}",
7364 );
7365 buffer_1.update(cx, |buffer, _| {
7366 assert!(buffer.is_dirty());
7367 assert_eq!(
7368 buffer.text(),
7369 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
7370 )
7371 });
7372 buffer_2.update(cx, |buffer, _| {
7373 assert!(buffer.is_dirty());
7374 assert_eq!(
7375 buffer.text(),
7376 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
7377 )
7378 });
7379 buffer_3.update(cx, |buffer, _| {
7380 assert!(!buffer.is_dirty());
7381 assert_eq!(buffer.text(), sample_text_3,)
7382 });
7383 cx.executor().run_until_parked();
7384
7385 cx.executor().start_waiting();
7386 let save = multi_buffer_editor
7387 .update_in(cx, |editor, window, cx| {
7388 editor.save(true, project.clone(), window, cx)
7389 })
7390 .unwrap();
7391
7392 let fake_server = fake_servers.next().await.unwrap();
7393 fake_server
7394 .server
7395 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7396 Ok(Some(vec![lsp::TextEdit::new(
7397 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7398 format!("[{} formatted]", params.text_document.uri),
7399 )]))
7400 })
7401 .detach();
7402 save.await;
7403
7404 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
7405 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
7406 assert_eq!(
7407 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7408 "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}",
7409 );
7410 buffer_1.update(cx, |buffer, _| {
7411 assert!(!buffer.is_dirty());
7412 assert_eq!(
7413 buffer.text(),
7414 "a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n",
7415 )
7416 });
7417 buffer_2.update(cx, |buffer, _| {
7418 assert!(!buffer.is_dirty());
7419 assert_eq!(
7420 buffer.text(),
7421 "lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n",
7422 )
7423 });
7424 buffer_3.update(cx, |buffer, _| {
7425 assert!(!buffer.is_dirty());
7426 assert_eq!(buffer.text(), sample_text_3,)
7427 });
7428}
7429
7430#[gpui::test]
7431async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
7432 init_test(cx, |_| {});
7433
7434 let fs = FakeFs::new(cx.executor());
7435 fs.insert_file("/file.rs", Default::default()).await;
7436
7437 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
7438
7439 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7440 language_registry.add(rust_lang());
7441 let mut fake_servers = language_registry.register_fake_lsp(
7442 "Rust",
7443 FakeLspAdapter {
7444 capabilities: lsp::ServerCapabilities {
7445 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
7446 ..Default::default()
7447 },
7448 ..Default::default()
7449 },
7450 );
7451
7452 let buffer = project
7453 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
7454 .await
7455 .unwrap();
7456
7457 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7458 let (editor, cx) = cx.add_window_view(|window, cx| {
7459 build_editor_with_project(project.clone(), buffer, window, cx)
7460 });
7461 editor.update_in(cx, |editor, window, cx| {
7462 editor.set_text("one\ntwo\nthree\n", window, cx)
7463 });
7464 assert!(cx.read(|cx| editor.is_dirty(cx)));
7465
7466 cx.executor().start_waiting();
7467 let fake_server = fake_servers.next().await.unwrap();
7468
7469 let save = editor
7470 .update_in(cx, |editor, window, cx| {
7471 editor.save(true, project.clone(), window, cx)
7472 })
7473 .unwrap();
7474 fake_server
7475 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
7476 assert_eq!(
7477 params.text_document.uri,
7478 lsp::Url::from_file_path("/file.rs").unwrap()
7479 );
7480 assert_eq!(params.options.tab_size, 4);
7481 Ok(Some(vec![lsp::TextEdit::new(
7482 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7483 ", ".to_string(),
7484 )]))
7485 })
7486 .next()
7487 .await;
7488 cx.executor().start_waiting();
7489 save.await;
7490 assert_eq!(
7491 editor.update(cx, |editor, cx| editor.text(cx)),
7492 "one, two\nthree\n"
7493 );
7494 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7495
7496 editor.update_in(cx, |editor, window, cx| {
7497 editor.set_text("one\ntwo\nthree\n", window, cx)
7498 });
7499 assert!(cx.read(|cx| editor.is_dirty(cx)));
7500
7501 // Ensure we can still save even if formatting hangs.
7502 fake_server.handle_request::<lsp::request::RangeFormatting, _, _>(
7503 move |params, _| async move {
7504 assert_eq!(
7505 params.text_document.uri,
7506 lsp::Url::from_file_path("/file.rs").unwrap()
7507 );
7508 futures::future::pending::<()>().await;
7509 unreachable!()
7510 },
7511 );
7512 let save = editor
7513 .update_in(cx, |editor, window, cx| {
7514 editor.save(true, project.clone(), window, cx)
7515 })
7516 .unwrap();
7517 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7518 cx.executor().start_waiting();
7519 save.await;
7520 assert_eq!(
7521 editor.update(cx, |editor, cx| editor.text(cx)),
7522 "one\ntwo\nthree\n"
7523 );
7524 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7525
7526 // For non-dirty buffer, no formatting request should be sent
7527 let save = editor
7528 .update_in(cx, |editor, window, cx| {
7529 editor.save(true, project.clone(), window, cx)
7530 })
7531 .unwrap();
7532 let _pending_format_request = fake_server
7533 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7534 panic!("Should not be invoked on non-dirty buffer");
7535 })
7536 .next();
7537 cx.executor().start_waiting();
7538 save.await;
7539
7540 // Set Rust language override and assert overridden tabsize is sent to language server
7541 update_test_language_settings(cx, |settings| {
7542 settings.languages.insert(
7543 "Rust".into(),
7544 LanguageSettingsContent {
7545 tab_size: NonZeroU32::new(8),
7546 ..Default::default()
7547 },
7548 );
7549 });
7550
7551 editor.update_in(cx, |editor, window, cx| {
7552 editor.set_text("somehting_new\n", window, cx)
7553 });
7554 assert!(cx.read(|cx| editor.is_dirty(cx)));
7555 let save = editor
7556 .update_in(cx, |editor, window, cx| {
7557 editor.save(true, project.clone(), window, cx)
7558 })
7559 .unwrap();
7560 fake_server
7561 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
7562 assert_eq!(
7563 params.text_document.uri,
7564 lsp::Url::from_file_path("/file.rs").unwrap()
7565 );
7566 assert_eq!(params.options.tab_size, 8);
7567 Ok(Some(vec![]))
7568 })
7569 .next()
7570 .await;
7571 cx.executor().start_waiting();
7572 save.await;
7573}
7574
7575#[gpui::test]
7576async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
7577 init_test(cx, |settings| {
7578 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
7579 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
7580 ))
7581 });
7582
7583 let fs = FakeFs::new(cx.executor());
7584 fs.insert_file("/file.rs", Default::default()).await;
7585
7586 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
7587
7588 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7589 language_registry.add(Arc::new(Language::new(
7590 LanguageConfig {
7591 name: "Rust".into(),
7592 matcher: LanguageMatcher {
7593 path_suffixes: vec!["rs".to_string()],
7594 ..Default::default()
7595 },
7596 ..LanguageConfig::default()
7597 },
7598 Some(tree_sitter_rust::LANGUAGE.into()),
7599 )));
7600 update_test_language_settings(cx, |settings| {
7601 // Enable Prettier formatting for the same buffer, and ensure
7602 // LSP is called instead of Prettier.
7603 settings.defaults.prettier = Some(PrettierSettings {
7604 allowed: true,
7605 ..PrettierSettings::default()
7606 });
7607 });
7608 let mut fake_servers = language_registry.register_fake_lsp(
7609 "Rust",
7610 FakeLspAdapter {
7611 capabilities: lsp::ServerCapabilities {
7612 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7613 ..Default::default()
7614 },
7615 ..Default::default()
7616 },
7617 );
7618
7619 let buffer = project
7620 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
7621 .await
7622 .unwrap();
7623
7624 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7625 let (editor, cx) = cx.add_window_view(|window, cx| {
7626 build_editor_with_project(project.clone(), buffer, window, cx)
7627 });
7628 editor.update_in(cx, |editor, window, cx| {
7629 editor.set_text("one\ntwo\nthree\n", window, cx)
7630 });
7631
7632 cx.executor().start_waiting();
7633 let fake_server = fake_servers.next().await.unwrap();
7634
7635 let format = editor
7636 .update_in(cx, |editor, window, cx| {
7637 editor.perform_format(
7638 project.clone(),
7639 FormatTrigger::Manual,
7640 FormatTarget::Buffers,
7641 window,
7642 cx,
7643 )
7644 })
7645 .unwrap();
7646 fake_server
7647 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7648 assert_eq!(
7649 params.text_document.uri,
7650 lsp::Url::from_file_path("/file.rs").unwrap()
7651 );
7652 assert_eq!(params.options.tab_size, 4);
7653 Ok(Some(vec![lsp::TextEdit::new(
7654 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7655 ", ".to_string(),
7656 )]))
7657 })
7658 .next()
7659 .await;
7660 cx.executor().start_waiting();
7661 format.await;
7662 assert_eq!(
7663 editor.update(cx, |editor, cx| editor.text(cx)),
7664 "one, two\nthree\n"
7665 );
7666
7667 editor.update_in(cx, |editor, window, cx| {
7668 editor.set_text("one\ntwo\nthree\n", window, cx)
7669 });
7670 // Ensure we don't lock if formatting hangs.
7671 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7672 assert_eq!(
7673 params.text_document.uri,
7674 lsp::Url::from_file_path("/file.rs").unwrap()
7675 );
7676 futures::future::pending::<()>().await;
7677 unreachable!()
7678 });
7679 let format = editor
7680 .update_in(cx, |editor, window, cx| {
7681 editor.perform_format(
7682 project,
7683 FormatTrigger::Manual,
7684 FormatTarget::Buffers,
7685 window,
7686 cx,
7687 )
7688 })
7689 .unwrap();
7690 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7691 cx.executor().start_waiting();
7692 format.await;
7693 assert_eq!(
7694 editor.update(cx, |editor, cx| editor.text(cx)),
7695 "one\ntwo\nthree\n"
7696 );
7697}
7698
7699#[gpui::test]
7700async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
7701 init_test(cx, |_| {});
7702
7703 let mut cx = EditorLspTestContext::new_rust(
7704 lsp::ServerCapabilities {
7705 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7706 ..Default::default()
7707 },
7708 cx,
7709 )
7710 .await;
7711
7712 cx.set_state(indoc! {"
7713 one.twoˇ
7714 "});
7715
7716 // The format request takes a long time. When it completes, it inserts
7717 // a newline and an indent before the `.`
7718 cx.lsp
7719 .handle_request::<lsp::request::Formatting, _, _>(move |_, cx| {
7720 let executor = cx.background_executor().clone();
7721 async move {
7722 executor.timer(Duration::from_millis(100)).await;
7723 Ok(Some(vec![lsp::TextEdit {
7724 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
7725 new_text: "\n ".into(),
7726 }]))
7727 }
7728 });
7729
7730 // Submit a format request.
7731 let format_1 = cx
7732 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7733 .unwrap();
7734 cx.executor().run_until_parked();
7735
7736 // Submit a second format request.
7737 let format_2 = cx
7738 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7739 .unwrap();
7740 cx.executor().run_until_parked();
7741
7742 // Wait for both format requests to complete
7743 cx.executor().advance_clock(Duration::from_millis(200));
7744 cx.executor().start_waiting();
7745 format_1.await.unwrap();
7746 cx.executor().start_waiting();
7747 format_2.await.unwrap();
7748
7749 // The formatting edits only happens once.
7750 cx.assert_editor_state(indoc! {"
7751 one
7752 .twoˇ
7753 "});
7754}
7755
7756#[gpui::test]
7757async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
7758 init_test(cx, |settings| {
7759 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
7760 });
7761
7762 let mut cx = EditorLspTestContext::new_rust(
7763 lsp::ServerCapabilities {
7764 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7765 ..Default::default()
7766 },
7767 cx,
7768 )
7769 .await;
7770
7771 // Set up a buffer white some trailing whitespace and no trailing newline.
7772 cx.set_state(
7773 &[
7774 "one ", //
7775 "twoˇ", //
7776 "three ", //
7777 "four", //
7778 ]
7779 .join("\n"),
7780 );
7781
7782 // Submit a format request.
7783 let format = cx
7784 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7785 .unwrap();
7786
7787 // Record which buffer changes have been sent to the language server
7788 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
7789 cx.lsp
7790 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
7791 let buffer_changes = buffer_changes.clone();
7792 move |params, _| {
7793 buffer_changes.lock().extend(
7794 params
7795 .content_changes
7796 .into_iter()
7797 .map(|e| (e.range.unwrap(), e.text)),
7798 );
7799 }
7800 });
7801
7802 // Handle formatting requests to the language server.
7803 cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
7804 let buffer_changes = buffer_changes.clone();
7805 move |_, _| {
7806 // When formatting is requested, trailing whitespace has already been stripped,
7807 // and the trailing newline has already been added.
7808 assert_eq!(
7809 &buffer_changes.lock()[1..],
7810 &[
7811 (
7812 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
7813 "".into()
7814 ),
7815 (
7816 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
7817 "".into()
7818 ),
7819 (
7820 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
7821 "\n".into()
7822 ),
7823 ]
7824 );
7825
7826 // Insert blank lines between each line of the buffer.
7827 async move {
7828 Ok(Some(vec![
7829 lsp::TextEdit {
7830 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
7831 new_text: "\n".into(),
7832 },
7833 lsp::TextEdit {
7834 range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
7835 new_text: "\n".into(),
7836 },
7837 ]))
7838 }
7839 }
7840 });
7841
7842 // After formatting the buffer, the trailing whitespace is stripped,
7843 // a newline is appended, and the edits provided by the language server
7844 // have been applied.
7845 format.await.unwrap();
7846 cx.assert_editor_state(
7847 &[
7848 "one", //
7849 "", //
7850 "twoˇ", //
7851 "", //
7852 "three", //
7853 "four", //
7854 "", //
7855 ]
7856 .join("\n"),
7857 );
7858
7859 // Undoing the formatting undoes the trailing whitespace removal, the
7860 // trailing newline, and the LSP edits.
7861 cx.update_buffer(|buffer, cx| buffer.undo(cx));
7862 cx.assert_editor_state(
7863 &[
7864 "one ", //
7865 "twoˇ", //
7866 "three ", //
7867 "four", //
7868 ]
7869 .join("\n"),
7870 );
7871}
7872
7873#[gpui::test]
7874async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
7875 cx: &mut gpui::TestAppContext,
7876) {
7877 init_test(cx, |_| {});
7878
7879 cx.update(|cx| {
7880 cx.update_global::<SettingsStore, _>(|settings, cx| {
7881 settings.update_user_settings::<EditorSettings>(cx, |settings| {
7882 settings.auto_signature_help = Some(true);
7883 });
7884 });
7885 });
7886
7887 let mut cx = EditorLspTestContext::new_rust(
7888 lsp::ServerCapabilities {
7889 signature_help_provider: Some(lsp::SignatureHelpOptions {
7890 ..Default::default()
7891 }),
7892 ..Default::default()
7893 },
7894 cx,
7895 )
7896 .await;
7897
7898 let language = Language::new(
7899 LanguageConfig {
7900 name: "Rust".into(),
7901 brackets: BracketPairConfig {
7902 pairs: vec![
7903 BracketPair {
7904 start: "{".to_string(),
7905 end: "}".to_string(),
7906 close: true,
7907 surround: true,
7908 newline: true,
7909 },
7910 BracketPair {
7911 start: "(".to_string(),
7912 end: ")".to_string(),
7913 close: true,
7914 surround: true,
7915 newline: true,
7916 },
7917 BracketPair {
7918 start: "/*".to_string(),
7919 end: " */".to_string(),
7920 close: true,
7921 surround: true,
7922 newline: true,
7923 },
7924 BracketPair {
7925 start: "[".to_string(),
7926 end: "]".to_string(),
7927 close: false,
7928 surround: false,
7929 newline: true,
7930 },
7931 BracketPair {
7932 start: "\"".to_string(),
7933 end: "\"".to_string(),
7934 close: true,
7935 surround: true,
7936 newline: false,
7937 },
7938 BracketPair {
7939 start: "<".to_string(),
7940 end: ">".to_string(),
7941 close: false,
7942 surround: true,
7943 newline: true,
7944 },
7945 ],
7946 ..Default::default()
7947 },
7948 autoclose_before: "})]".to_string(),
7949 ..Default::default()
7950 },
7951 Some(tree_sitter_rust::LANGUAGE.into()),
7952 );
7953 let language = Arc::new(language);
7954
7955 cx.language_registry().add(language.clone());
7956 cx.update_buffer(|buffer, cx| {
7957 buffer.set_language(Some(language), cx);
7958 });
7959
7960 cx.set_state(
7961 &r#"
7962 fn main() {
7963 sampleˇ
7964 }
7965 "#
7966 .unindent(),
7967 );
7968
7969 cx.update_editor(|editor, window, cx| {
7970 editor.handle_input("(", window, cx);
7971 });
7972 cx.assert_editor_state(
7973 &"
7974 fn main() {
7975 sample(ˇ)
7976 }
7977 "
7978 .unindent(),
7979 );
7980
7981 let mocked_response = lsp::SignatureHelp {
7982 signatures: vec![lsp::SignatureInformation {
7983 label: "fn sample(param1: u8, param2: u8)".to_string(),
7984 documentation: None,
7985 parameters: Some(vec![
7986 lsp::ParameterInformation {
7987 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
7988 documentation: None,
7989 },
7990 lsp::ParameterInformation {
7991 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
7992 documentation: None,
7993 },
7994 ]),
7995 active_parameter: None,
7996 }],
7997 active_signature: Some(0),
7998 active_parameter: Some(0),
7999 };
8000 handle_signature_help_request(&mut cx, mocked_response).await;
8001
8002 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8003 .await;
8004
8005 cx.editor(|editor, _, _| {
8006 let signature_help_state = editor.signature_help_state.popover().cloned();
8007 assert!(signature_help_state.is_some());
8008 let ParsedMarkdown {
8009 text, highlights, ..
8010 } = signature_help_state.unwrap().parsed_content;
8011 assert_eq!(text, "param1: u8, param2: u8");
8012 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8013 });
8014}
8015
8016#[gpui::test]
8017async fn test_handle_input_with_different_show_signature_settings(cx: &mut gpui::TestAppContext) {
8018 init_test(cx, |_| {});
8019
8020 cx.update(|cx| {
8021 cx.update_global::<SettingsStore, _>(|settings, cx| {
8022 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8023 settings.auto_signature_help = Some(false);
8024 settings.show_signature_help_after_edits = Some(false);
8025 });
8026 });
8027 });
8028
8029 let mut cx = EditorLspTestContext::new_rust(
8030 lsp::ServerCapabilities {
8031 signature_help_provider: Some(lsp::SignatureHelpOptions {
8032 ..Default::default()
8033 }),
8034 ..Default::default()
8035 },
8036 cx,
8037 )
8038 .await;
8039
8040 let language = Language::new(
8041 LanguageConfig {
8042 name: "Rust".into(),
8043 brackets: BracketPairConfig {
8044 pairs: vec![
8045 BracketPair {
8046 start: "{".to_string(),
8047 end: "}".to_string(),
8048 close: true,
8049 surround: true,
8050 newline: true,
8051 },
8052 BracketPair {
8053 start: "(".to_string(),
8054 end: ")".to_string(),
8055 close: true,
8056 surround: true,
8057 newline: true,
8058 },
8059 BracketPair {
8060 start: "/*".to_string(),
8061 end: " */".to_string(),
8062 close: true,
8063 surround: true,
8064 newline: true,
8065 },
8066 BracketPair {
8067 start: "[".to_string(),
8068 end: "]".to_string(),
8069 close: false,
8070 surround: false,
8071 newline: true,
8072 },
8073 BracketPair {
8074 start: "\"".to_string(),
8075 end: "\"".to_string(),
8076 close: true,
8077 surround: true,
8078 newline: false,
8079 },
8080 BracketPair {
8081 start: "<".to_string(),
8082 end: ">".to_string(),
8083 close: false,
8084 surround: true,
8085 newline: true,
8086 },
8087 ],
8088 ..Default::default()
8089 },
8090 autoclose_before: "})]".to_string(),
8091 ..Default::default()
8092 },
8093 Some(tree_sitter_rust::LANGUAGE.into()),
8094 );
8095 let language = Arc::new(language);
8096
8097 cx.language_registry().add(language.clone());
8098 cx.update_buffer(|buffer, cx| {
8099 buffer.set_language(Some(language), cx);
8100 });
8101
8102 // Ensure that signature_help is not called when no signature help is enabled.
8103 cx.set_state(
8104 &r#"
8105 fn main() {
8106 sampleˇ
8107 }
8108 "#
8109 .unindent(),
8110 );
8111 cx.update_editor(|editor, window, cx| {
8112 editor.handle_input("(", window, cx);
8113 });
8114 cx.assert_editor_state(
8115 &"
8116 fn main() {
8117 sample(ˇ)
8118 }
8119 "
8120 .unindent(),
8121 );
8122 cx.editor(|editor, _, _| {
8123 assert!(editor.signature_help_state.task().is_none());
8124 });
8125
8126 let mocked_response = lsp::SignatureHelp {
8127 signatures: vec![lsp::SignatureInformation {
8128 label: "fn sample(param1: u8, param2: u8)".to_string(),
8129 documentation: None,
8130 parameters: Some(vec![
8131 lsp::ParameterInformation {
8132 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8133 documentation: None,
8134 },
8135 lsp::ParameterInformation {
8136 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8137 documentation: None,
8138 },
8139 ]),
8140 active_parameter: None,
8141 }],
8142 active_signature: Some(0),
8143 active_parameter: Some(0),
8144 };
8145
8146 // Ensure that signature_help is called when enabled afte edits
8147 cx.update(|_, cx| {
8148 cx.update_global::<SettingsStore, _>(|settings, cx| {
8149 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8150 settings.auto_signature_help = Some(false);
8151 settings.show_signature_help_after_edits = Some(true);
8152 });
8153 });
8154 });
8155 cx.set_state(
8156 &r#"
8157 fn main() {
8158 sampleˇ
8159 }
8160 "#
8161 .unindent(),
8162 );
8163 cx.update_editor(|editor, window, cx| {
8164 editor.handle_input("(", window, cx);
8165 });
8166 cx.assert_editor_state(
8167 &"
8168 fn main() {
8169 sample(ˇ)
8170 }
8171 "
8172 .unindent(),
8173 );
8174 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8175 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8176 .await;
8177 cx.update_editor(|editor, _, _| {
8178 let signature_help_state = editor.signature_help_state.popover().cloned();
8179 assert!(signature_help_state.is_some());
8180 let ParsedMarkdown {
8181 text, highlights, ..
8182 } = signature_help_state.unwrap().parsed_content;
8183 assert_eq!(text, "param1: u8, param2: u8");
8184 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8185 editor.signature_help_state = SignatureHelpState::default();
8186 });
8187
8188 // Ensure that signature_help is called when auto signature help override is enabled
8189 cx.update(|_, cx| {
8190 cx.update_global::<SettingsStore, _>(|settings, cx| {
8191 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8192 settings.auto_signature_help = Some(true);
8193 settings.show_signature_help_after_edits = Some(false);
8194 });
8195 });
8196 });
8197 cx.set_state(
8198 &r#"
8199 fn main() {
8200 sampleˇ
8201 }
8202 "#
8203 .unindent(),
8204 );
8205 cx.update_editor(|editor, window, cx| {
8206 editor.handle_input("(", window, cx);
8207 });
8208 cx.assert_editor_state(
8209 &"
8210 fn main() {
8211 sample(ˇ)
8212 }
8213 "
8214 .unindent(),
8215 );
8216 handle_signature_help_request(&mut cx, mocked_response).await;
8217 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8218 .await;
8219 cx.editor(|editor, _, _| {
8220 let signature_help_state = editor.signature_help_state.popover().cloned();
8221 assert!(signature_help_state.is_some());
8222 let ParsedMarkdown {
8223 text, highlights, ..
8224 } = signature_help_state.unwrap().parsed_content;
8225 assert_eq!(text, "param1: u8, param2: u8");
8226 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8227 });
8228}
8229
8230#[gpui::test]
8231async fn test_signature_help(cx: &mut gpui::TestAppContext) {
8232 init_test(cx, |_| {});
8233 cx.update(|cx| {
8234 cx.update_global::<SettingsStore, _>(|settings, cx| {
8235 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8236 settings.auto_signature_help = Some(true);
8237 });
8238 });
8239 });
8240
8241 let mut cx = EditorLspTestContext::new_rust(
8242 lsp::ServerCapabilities {
8243 signature_help_provider: Some(lsp::SignatureHelpOptions {
8244 ..Default::default()
8245 }),
8246 ..Default::default()
8247 },
8248 cx,
8249 )
8250 .await;
8251
8252 // A test that directly calls `show_signature_help`
8253 cx.update_editor(|editor, window, cx| {
8254 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8255 });
8256
8257 let mocked_response = lsp::SignatureHelp {
8258 signatures: vec![lsp::SignatureInformation {
8259 label: "fn sample(param1: u8, param2: u8)".to_string(),
8260 documentation: None,
8261 parameters: Some(vec![
8262 lsp::ParameterInformation {
8263 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8264 documentation: None,
8265 },
8266 lsp::ParameterInformation {
8267 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8268 documentation: None,
8269 },
8270 ]),
8271 active_parameter: None,
8272 }],
8273 active_signature: Some(0),
8274 active_parameter: Some(0),
8275 };
8276 handle_signature_help_request(&mut cx, mocked_response).await;
8277
8278 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8279 .await;
8280
8281 cx.editor(|editor, _, _| {
8282 let signature_help_state = editor.signature_help_state.popover().cloned();
8283 assert!(signature_help_state.is_some());
8284 let ParsedMarkdown {
8285 text, highlights, ..
8286 } = signature_help_state.unwrap().parsed_content;
8287 assert_eq!(text, "param1: u8, param2: u8");
8288 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8289 });
8290
8291 // When exiting outside from inside the brackets, `signature_help` is closed.
8292 cx.set_state(indoc! {"
8293 fn main() {
8294 sample(ˇ);
8295 }
8296
8297 fn sample(param1: u8, param2: u8) {}
8298 "});
8299
8300 cx.update_editor(|editor, window, cx| {
8301 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
8302 });
8303
8304 let mocked_response = lsp::SignatureHelp {
8305 signatures: Vec::new(),
8306 active_signature: None,
8307 active_parameter: None,
8308 };
8309 handle_signature_help_request(&mut cx, mocked_response).await;
8310
8311 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
8312 .await;
8313
8314 cx.editor(|editor, _, _| {
8315 assert!(!editor.signature_help_state.is_shown());
8316 });
8317
8318 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
8319 cx.set_state(indoc! {"
8320 fn main() {
8321 sample(ˇ);
8322 }
8323
8324 fn sample(param1: u8, param2: u8) {}
8325 "});
8326
8327 let mocked_response = lsp::SignatureHelp {
8328 signatures: vec![lsp::SignatureInformation {
8329 label: "fn sample(param1: u8, param2: u8)".to_string(),
8330 documentation: None,
8331 parameters: Some(vec![
8332 lsp::ParameterInformation {
8333 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8334 documentation: None,
8335 },
8336 lsp::ParameterInformation {
8337 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8338 documentation: None,
8339 },
8340 ]),
8341 active_parameter: None,
8342 }],
8343 active_signature: Some(0),
8344 active_parameter: Some(0),
8345 };
8346 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8347 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8348 .await;
8349 cx.editor(|editor, _, _| {
8350 assert!(editor.signature_help_state.is_shown());
8351 });
8352
8353 // Restore the popover with more parameter input
8354 cx.set_state(indoc! {"
8355 fn main() {
8356 sample(param1, param2ˇ);
8357 }
8358
8359 fn sample(param1: u8, param2: u8) {}
8360 "});
8361
8362 let mocked_response = lsp::SignatureHelp {
8363 signatures: vec![lsp::SignatureInformation {
8364 label: "fn sample(param1: u8, param2: u8)".to_string(),
8365 documentation: None,
8366 parameters: Some(vec![
8367 lsp::ParameterInformation {
8368 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8369 documentation: None,
8370 },
8371 lsp::ParameterInformation {
8372 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8373 documentation: None,
8374 },
8375 ]),
8376 active_parameter: None,
8377 }],
8378 active_signature: Some(0),
8379 active_parameter: Some(1),
8380 };
8381 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8382 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8383 .await;
8384
8385 // When selecting a range, the popover is gone.
8386 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
8387 cx.update_editor(|editor, window, cx| {
8388 editor.change_selections(None, window, cx, |s| {
8389 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
8390 })
8391 });
8392 cx.assert_editor_state(indoc! {"
8393 fn main() {
8394 sample(param1, «ˇparam2»);
8395 }
8396
8397 fn sample(param1: u8, param2: u8) {}
8398 "});
8399 cx.editor(|editor, _, _| {
8400 assert!(!editor.signature_help_state.is_shown());
8401 });
8402
8403 // When unselecting again, the popover is back if within the brackets.
8404 cx.update_editor(|editor, window, cx| {
8405 editor.change_selections(None, window, cx, |s| {
8406 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8407 })
8408 });
8409 cx.assert_editor_state(indoc! {"
8410 fn main() {
8411 sample(param1, ˇparam2);
8412 }
8413
8414 fn sample(param1: u8, param2: u8) {}
8415 "});
8416 handle_signature_help_request(&mut cx, mocked_response).await;
8417 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8418 .await;
8419 cx.editor(|editor, _, _| {
8420 assert!(editor.signature_help_state.is_shown());
8421 });
8422
8423 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
8424 cx.update_editor(|editor, window, cx| {
8425 editor.change_selections(None, window, cx, |s| {
8426 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
8427 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8428 })
8429 });
8430 cx.assert_editor_state(indoc! {"
8431 fn main() {
8432 sample(param1, ˇparam2);
8433 }
8434
8435 fn sample(param1: u8, param2: u8) {}
8436 "});
8437
8438 let mocked_response = lsp::SignatureHelp {
8439 signatures: vec![lsp::SignatureInformation {
8440 label: "fn sample(param1: u8, param2: u8)".to_string(),
8441 documentation: None,
8442 parameters: Some(vec![
8443 lsp::ParameterInformation {
8444 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8445 documentation: None,
8446 },
8447 lsp::ParameterInformation {
8448 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8449 documentation: None,
8450 },
8451 ]),
8452 active_parameter: None,
8453 }],
8454 active_signature: Some(0),
8455 active_parameter: Some(1),
8456 };
8457 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8458 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8459 .await;
8460 cx.update_editor(|editor, _, cx| {
8461 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
8462 });
8463 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
8464 .await;
8465 cx.update_editor(|editor, window, cx| {
8466 editor.change_selections(None, window, cx, |s| {
8467 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
8468 })
8469 });
8470 cx.assert_editor_state(indoc! {"
8471 fn main() {
8472 sample(param1, «ˇparam2»);
8473 }
8474
8475 fn sample(param1: u8, param2: u8) {}
8476 "});
8477 cx.update_editor(|editor, window, cx| {
8478 editor.change_selections(None, window, cx, |s| {
8479 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8480 })
8481 });
8482 cx.assert_editor_state(indoc! {"
8483 fn main() {
8484 sample(param1, ˇparam2);
8485 }
8486
8487 fn sample(param1: u8, param2: u8) {}
8488 "});
8489 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
8490 .await;
8491}
8492
8493#[gpui::test]
8494async fn test_completion(cx: &mut gpui::TestAppContext) {
8495 init_test(cx, |_| {});
8496
8497 let mut cx = EditorLspTestContext::new_rust(
8498 lsp::ServerCapabilities {
8499 completion_provider: Some(lsp::CompletionOptions {
8500 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
8501 resolve_provider: Some(true),
8502 ..Default::default()
8503 }),
8504 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
8505 ..Default::default()
8506 },
8507 cx,
8508 )
8509 .await;
8510 let counter = Arc::new(AtomicUsize::new(0));
8511
8512 cx.set_state(indoc! {"
8513 oneˇ
8514 two
8515 three
8516 "});
8517 cx.simulate_keystroke(".");
8518 handle_completion_request(
8519 &mut cx,
8520 indoc! {"
8521 one.|<>
8522 two
8523 three
8524 "},
8525 vec!["first_completion", "second_completion"],
8526 counter.clone(),
8527 )
8528 .await;
8529 cx.condition(|editor, _| editor.context_menu_visible())
8530 .await;
8531 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
8532
8533 let _handler = handle_signature_help_request(
8534 &mut cx,
8535 lsp::SignatureHelp {
8536 signatures: vec![lsp::SignatureInformation {
8537 label: "test signature".to_string(),
8538 documentation: None,
8539 parameters: Some(vec![lsp::ParameterInformation {
8540 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
8541 documentation: None,
8542 }]),
8543 active_parameter: None,
8544 }],
8545 active_signature: None,
8546 active_parameter: None,
8547 },
8548 );
8549 cx.update_editor(|editor, window, cx| {
8550 assert!(
8551 !editor.signature_help_state.is_shown(),
8552 "No signature help was called for"
8553 );
8554 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8555 });
8556 cx.run_until_parked();
8557 cx.update_editor(|editor, _, _| {
8558 assert!(
8559 !editor.signature_help_state.is_shown(),
8560 "No signature help should be shown when completions menu is open"
8561 );
8562 });
8563
8564 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8565 editor.context_menu_next(&Default::default(), window, cx);
8566 editor
8567 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8568 .unwrap()
8569 });
8570 cx.assert_editor_state(indoc! {"
8571 one.second_completionˇ
8572 two
8573 three
8574 "});
8575
8576 handle_resolve_completion_request(
8577 &mut cx,
8578 Some(vec![
8579 (
8580 //This overlaps with the primary completion edit which is
8581 //misbehavior from the LSP spec, test that we filter it out
8582 indoc! {"
8583 one.second_ˇcompletion
8584 two
8585 threeˇ
8586 "},
8587 "overlapping additional edit",
8588 ),
8589 (
8590 indoc! {"
8591 one.second_completion
8592 two
8593 threeˇ
8594 "},
8595 "\nadditional edit",
8596 ),
8597 ]),
8598 )
8599 .await;
8600 apply_additional_edits.await.unwrap();
8601 cx.assert_editor_state(indoc! {"
8602 one.second_completionˇ
8603 two
8604 three
8605 additional edit
8606 "});
8607
8608 cx.set_state(indoc! {"
8609 one.second_completion
8610 twoˇ
8611 threeˇ
8612 additional edit
8613 "});
8614 cx.simulate_keystroke(" ");
8615 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8616 cx.simulate_keystroke("s");
8617 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8618
8619 cx.assert_editor_state(indoc! {"
8620 one.second_completion
8621 two sˇ
8622 three sˇ
8623 additional edit
8624 "});
8625 handle_completion_request(
8626 &mut cx,
8627 indoc! {"
8628 one.second_completion
8629 two s
8630 three <s|>
8631 additional edit
8632 "},
8633 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
8634 counter.clone(),
8635 )
8636 .await;
8637 cx.condition(|editor, _| editor.context_menu_visible())
8638 .await;
8639 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
8640
8641 cx.simulate_keystroke("i");
8642
8643 handle_completion_request(
8644 &mut cx,
8645 indoc! {"
8646 one.second_completion
8647 two si
8648 three <si|>
8649 additional edit
8650 "},
8651 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
8652 counter.clone(),
8653 )
8654 .await;
8655 cx.condition(|editor, _| editor.context_menu_visible())
8656 .await;
8657 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
8658
8659 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8660 editor
8661 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8662 .unwrap()
8663 });
8664 cx.assert_editor_state(indoc! {"
8665 one.second_completion
8666 two sixth_completionˇ
8667 three sixth_completionˇ
8668 additional edit
8669 "});
8670
8671 apply_additional_edits.await.unwrap();
8672
8673 update_test_language_settings(&mut cx, |settings| {
8674 settings.defaults.show_completions_on_input = Some(false);
8675 });
8676 cx.set_state("editorˇ");
8677 cx.simulate_keystroke(".");
8678 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8679 cx.simulate_keystroke("c");
8680 cx.simulate_keystroke("l");
8681 cx.simulate_keystroke("o");
8682 cx.assert_editor_state("editor.cloˇ");
8683 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8684 cx.update_editor(|editor, window, cx| {
8685 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
8686 });
8687 handle_completion_request(
8688 &mut cx,
8689 "editor.<clo|>",
8690 vec!["close", "clobber"],
8691 counter.clone(),
8692 )
8693 .await;
8694 cx.condition(|editor, _| editor.context_menu_visible())
8695 .await;
8696 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
8697
8698 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8699 editor
8700 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8701 .unwrap()
8702 });
8703 cx.assert_editor_state("editor.closeˇ");
8704 handle_resolve_completion_request(&mut cx, None).await;
8705 apply_additional_edits.await.unwrap();
8706}
8707
8708#[gpui::test]
8709async fn test_multiline_completion(cx: &mut gpui::TestAppContext) {
8710 init_test(cx, |_| {});
8711
8712 let fs = FakeFs::new(cx.executor());
8713 fs.insert_tree(
8714 "/a",
8715 json!({
8716 "main.ts": "a",
8717 }),
8718 )
8719 .await;
8720
8721 let project = Project::test(fs, ["/a".as_ref()], cx).await;
8722 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8723 let typescript_language = Arc::new(Language::new(
8724 LanguageConfig {
8725 name: "TypeScript".into(),
8726 matcher: LanguageMatcher {
8727 path_suffixes: vec!["ts".to_string()],
8728 ..LanguageMatcher::default()
8729 },
8730 line_comments: vec!["// ".into()],
8731 ..LanguageConfig::default()
8732 },
8733 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8734 ));
8735 language_registry.add(typescript_language.clone());
8736 let mut fake_servers = language_registry.register_fake_lsp(
8737 "TypeScript",
8738 FakeLspAdapter {
8739 capabilities: lsp::ServerCapabilities {
8740 completion_provider: Some(lsp::CompletionOptions {
8741 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
8742 ..lsp::CompletionOptions::default()
8743 }),
8744 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
8745 ..lsp::ServerCapabilities::default()
8746 },
8747 // Emulate vtsls label generation
8748 label_for_completion: Some(Box::new(|item, _| {
8749 let text = if let Some(description) = item
8750 .label_details
8751 .as_ref()
8752 .and_then(|label_details| label_details.description.as_ref())
8753 {
8754 format!("{} {}", item.label, description)
8755 } else if let Some(detail) = &item.detail {
8756 format!("{} {}", item.label, detail)
8757 } else {
8758 item.label.clone()
8759 };
8760 let len = text.len();
8761 Some(language::CodeLabel {
8762 text,
8763 runs: Vec::new(),
8764 filter_range: 0..len,
8765 })
8766 })),
8767 ..FakeLspAdapter::default()
8768 },
8769 );
8770 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8771 let cx = &mut VisualTestContext::from_window(*workspace, cx);
8772 let worktree_id = workspace
8773 .update(cx, |workspace, _window, cx| {
8774 workspace.project().update(cx, |project, cx| {
8775 project.worktrees(cx).next().unwrap().read(cx).id()
8776 })
8777 })
8778 .unwrap();
8779 let _buffer = project
8780 .update(cx, |project, cx| {
8781 project.open_local_buffer_with_lsp("/a/main.ts", cx)
8782 })
8783 .await
8784 .unwrap();
8785 let editor = workspace
8786 .update(cx, |workspace, window, cx| {
8787 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
8788 })
8789 .unwrap()
8790 .await
8791 .unwrap()
8792 .downcast::<Editor>()
8793 .unwrap();
8794 let fake_server = fake_servers.next().await.unwrap();
8795
8796 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
8797 let multiline_label_2 = "a\nb\nc\n";
8798 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
8799 let multiline_description = "d\ne\nf\n";
8800 let multiline_detail_2 = "g\nh\ni\n";
8801
8802 let mut completion_handle =
8803 fake_server.handle_request::<lsp::request::Completion, _, _>(move |params, _| async move {
8804 Ok(Some(lsp::CompletionResponse::Array(vec![
8805 lsp::CompletionItem {
8806 label: multiline_label.to_string(),
8807 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8808 range: lsp::Range {
8809 start: lsp::Position {
8810 line: params.text_document_position.position.line,
8811 character: params.text_document_position.position.character,
8812 },
8813 end: lsp::Position {
8814 line: params.text_document_position.position.line,
8815 character: params.text_document_position.position.character,
8816 },
8817 },
8818 new_text: "new_text_1".to_string(),
8819 })),
8820 ..lsp::CompletionItem::default()
8821 },
8822 lsp::CompletionItem {
8823 label: "single line label 1".to_string(),
8824 detail: Some(multiline_detail.to_string()),
8825 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8826 range: lsp::Range {
8827 start: lsp::Position {
8828 line: params.text_document_position.position.line,
8829 character: params.text_document_position.position.character,
8830 },
8831 end: lsp::Position {
8832 line: params.text_document_position.position.line,
8833 character: params.text_document_position.position.character,
8834 },
8835 },
8836 new_text: "new_text_2".to_string(),
8837 })),
8838 ..lsp::CompletionItem::default()
8839 },
8840 lsp::CompletionItem {
8841 label: "single line label 2".to_string(),
8842 label_details: Some(lsp::CompletionItemLabelDetails {
8843 description: Some(multiline_description.to_string()),
8844 detail: None,
8845 }),
8846 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8847 range: lsp::Range {
8848 start: lsp::Position {
8849 line: params.text_document_position.position.line,
8850 character: params.text_document_position.position.character,
8851 },
8852 end: lsp::Position {
8853 line: params.text_document_position.position.line,
8854 character: params.text_document_position.position.character,
8855 },
8856 },
8857 new_text: "new_text_2".to_string(),
8858 })),
8859 ..lsp::CompletionItem::default()
8860 },
8861 lsp::CompletionItem {
8862 label: multiline_label_2.to_string(),
8863 detail: Some(multiline_detail_2.to_string()),
8864 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8865 range: lsp::Range {
8866 start: lsp::Position {
8867 line: params.text_document_position.position.line,
8868 character: params.text_document_position.position.character,
8869 },
8870 end: lsp::Position {
8871 line: params.text_document_position.position.line,
8872 character: params.text_document_position.position.character,
8873 },
8874 },
8875 new_text: "new_text_3".to_string(),
8876 })),
8877 ..lsp::CompletionItem::default()
8878 },
8879 lsp::CompletionItem {
8880 label: "Label with many spaces and \t but without newlines".to_string(),
8881 detail: Some(
8882 "Details with many spaces and \t but without newlines".to_string(),
8883 ),
8884 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8885 range: lsp::Range {
8886 start: lsp::Position {
8887 line: params.text_document_position.position.line,
8888 character: params.text_document_position.position.character,
8889 },
8890 end: lsp::Position {
8891 line: params.text_document_position.position.line,
8892 character: params.text_document_position.position.character,
8893 },
8894 },
8895 new_text: "new_text_4".to_string(),
8896 })),
8897 ..lsp::CompletionItem::default()
8898 },
8899 ])))
8900 });
8901
8902 editor.update_in(cx, |editor, window, cx| {
8903 cx.focus_self(window);
8904 editor.move_to_end(&MoveToEnd, window, cx);
8905 editor.handle_input(".", window, cx);
8906 });
8907 cx.run_until_parked();
8908 completion_handle.next().await.unwrap();
8909
8910 editor.update(cx, |editor, _| {
8911 assert!(editor.context_menu_visible());
8912 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
8913 {
8914 let completion_labels = menu
8915 .completions
8916 .borrow()
8917 .iter()
8918 .map(|c| c.label.text.clone())
8919 .collect::<Vec<_>>();
8920 assert_eq!(
8921 completion_labels,
8922 &[
8923 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
8924 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
8925 "single line label 2 d e f ",
8926 "a b c g h i ",
8927 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
8928 ],
8929 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
8930 );
8931
8932 for completion in menu
8933 .completions
8934 .borrow()
8935 .iter() {
8936 assert_eq!(
8937 completion.label.filter_range,
8938 0..completion.label.text.len(),
8939 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
8940 );
8941 }
8942
8943 } else {
8944 panic!("expected completion menu to be open");
8945 }
8946 });
8947}
8948
8949#[gpui::test]
8950async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
8951 init_test(cx, |_| {});
8952 let mut cx = EditorLspTestContext::new_rust(
8953 lsp::ServerCapabilities {
8954 completion_provider: Some(lsp::CompletionOptions {
8955 trigger_characters: Some(vec![".".to_string()]),
8956 ..Default::default()
8957 }),
8958 ..Default::default()
8959 },
8960 cx,
8961 )
8962 .await;
8963 cx.lsp
8964 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
8965 Ok(Some(lsp::CompletionResponse::Array(vec![
8966 lsp::CompletionItem {
8967 label: "first".into(),
8968 ..Default::default()
8969 },
8970 lsp::CompletionItem {
8971 label: "last".into(),
8972 ..Default::default()
8973 },
8974 ])))
8975 });
8976 cx.set_state("variableˇ");
8977 cx.simulate_keystroke(".");
8978 cx.executor().run_until_parked();
8979
8980 cx.update_editor(|editor, _, _| {
8981 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
8982 {
8983 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
8984 } else {
8985 panic!("expected completion menu to be open");
8986 }
8987 });
8988
8989 cx.update_editor(|editor, window, cx| {
8990 editor.move_page_down(&MovePageDown::default(), window, cx);
8991 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
8992 {
8993 assert!(
8994 menu.selected_item == 1,
8995 "expected PageDown to select the last item from the context menu"
8996 );
8997 } else {
8998 panic!("expected completion menu to stay open after PageDown");
8999 }
9000 });
9001
9002 cx.update_editor(|editor, window, cx| {
9003 editor.move_page_up(&MovePageUp::default(), window, cx);
9004 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9005 {
9006 assert!(
9007 menu.selected_item == 0,
9008 "expected PageUp to select the first item from the context menu"
9009 );
9010 } else {
9011 panic!("expected completion menu to stay open after PageUp");
9012 }
9013 });
9014}
9015
9016#[gpui::test]
9017async fn test_completion_sort(cx: &mut gpui::TestAppContext) {
9018 init_test(cx, |_| {});
9019 let mut cx = EditorLspTestContext::new_rust(
9020 lsp::ServerCapabilities {
9021 completion_provider: Some(lsp::CompletionOptions {
9022 trigger_characters: Some(vec![".".to_string()]),
9023 ..Default::default()
9024 }),
9025 ..Default::default()
9026 },
9027 cx,
9028 )
9029 .await;
9030 cx.lsp
9031 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9032 Ok(Some(lsp::CompletionResponse::Array(vec![
9033 lsp::CompletionItem {
9034 label: "Range".into(),
9035 sort_text: Some("a".into()),
9036 ..Default::default()
9037 },
9038 lsp::CompletionItem {
9039 label: "r".into(),
9040 sort_text: Some("b".into()),
9041 ..Default::default()
9042 },
9043 lsp::CompletionItem {
9044 label: "ret".into(),
9045 sort_text: Some("c".into()),
9046 ..Default::default()
9047 },
9048 lsp::CompletionItem {
9049 label: "return".into(),
9050 sort_text: Some("d".into()),
9051 ..Default::default()
9052 },
9053 lsp::CompletionItem {
9054 label: "slice".into(),
9055 sort_text: Some("d".into()),
9056 ..Default::default()
9057 },
9058 ])))
9059 });
9060 cx.set_state("rˇ");
9061 cx.executor().run_until_parked();
9062 cx.update_editor(|editor, window, cx| {
9063 editor.show_completions(
9064 &ShowCompletions {
9065 trigger: Some("r".into()),
9066 },
9067 window,
9068 cx,
9069 );
9070 });
9071 cx.executor().run_until_parked();
9072
9073 cx.update_editor(|editor, _, _| {
9074 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9075 {
9076 assert_eq!(
9077 completion_menu_entries(&menu),
9078 &["r", "ret", "Range", "return"]
9079 );
9080 } else {
9081 panic!("expected completion menu to be open");
9082 }
9083 });
9084}
9085
9086#[gpui::test]
9087async fn test_no_duplicated_completion_requests(cx: &mut gpui::TestAppContext) {
9088 init_test(cx, |_| {});
9089
9090 let mut cx = EditorLspTestContext::new_rust(
9091 lsp::ServerCapabilities {
9092 completion_provider: Some(lsp::CompletionOptions {
9093 trigger_characters: Some(vec![".".to_string()]),
9094 resolve_provider: Some(true),
9095 ..Default::default()
9096 }),
9097 ..Default::default()
9098 },
9099 cx,
9100 )
9101 .await;
9102
9103 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
9104 cx.simulate_keystroke(".");
9105 let completion_item = lsp::CompletionItem {
9106 label: "Some".into(),
9107 kind: Some(lsp::CompletionItemKind::SNIPPET),
9108 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
9109 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
9110 kind: lsp::MarkupKind::Markdown,
9111 value: "```rust\nSome(2)\n```".to_string(),
9112 })),
9113 deprecated: Some(false),
9114 sort_text: Some("Some".to_string()),
9115 filter_text: Some("Some".to_string()),
9116 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
9117 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9118 range: lsp::Range {
9119 start: lsp::Position {
9120 line: 0,
9121 character: 22,
9122 },
9123 end: lsp::Position {
9124 line: 0,
9125 character: 22,
9126 },
9127 },
9128 new_text: "Some(2)".to_string(),
9129 })),
9130 additional_text_edits: Some(vec![lsp::TextEdit {
9131 range: lsp::Range {
9132 start: lsp::Position {
9133 line: 0,
9134 character: 20,
9135 },
9136 end: lsp::Position {
9137 line: 0,
9138 character: 22,
9139 },
9140 },
9141 new_text: "".to_string(),
9142 }]),
9143 ..Default::default()
9144 };
9145
9146 let closure_completion_item = completion_item.clone();
9147 let counter = Arc::new(AtomicUsize::new(0));
9148 let counter_clone = counter.clone();
9149 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
9150 let task_completion_item = closure_completion_item.clone();
9151 counter_clone.fetch_add(1, atomic::Ordering::Release);
9152 async move {
9153 Ok(Some(lsp::CompletionResponse::Array(vec![
9154 task_completion_item,
9155 ])))
9156 }
9157 });
9158
9159 cx.condition(|editor, _| editor.context_menu_visible())
9160 .await;
9161 cx.assert_editor_state(indoc! {"fn main() { let a = 2.ˇ; }"});
9162 assert!(request.next().await.is_some());
9163 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9164
9165 cx.simulate_keystroke("S");
9166 cx.simulate_keystroke("o");
9167 cx.simulate_keystroke("m");
9168 cx.condition(|editor, _| editor.context_menu_visible())
9169 .await;
9170 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Somˇ; }"});
9171 assert!(request.next().await.is_some());
9172 assert!(request.next().await.is_some());
9173 assert!(request.next().await.is_some());
9174 request.close();
9175 assert!(request.next().await.is_none());
9176 assert_eq!(
9177 counter.load(atomic::Ordering::Acquire),
9178 4,
9179 "With the completions menu open, only one LSP request should happen per input"
9180 );
9181}
9182
9183#[gpui::test]
9184async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
9185 init_test(cx, |_| {});
9186 let mut cx = EditorTestContext::new(cx).await;
9187 let language = Arc::new(Language::new(
9188 LanguageConfig {
9189 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
9190 ..Default::default()
9191 },
9192 Some(tree_sitter_rust::LANGUAGE.into()),
9193 ));
9194 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
9195
9196 // If multiple selections intersect a line, the line is only toggled once.
9197 cx.set_state(indoc! {"
9198 fn a() {
9199 «//b();
9200 ˇ»// «c();
9201 //ˇ» d();
9202 }
9203 "});
9204
9205 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9206
9207 cx.assert_editor_state(indoc! {"
9208 fn a() {
9209 «b();
9210 c();
9211 ˇ» d();
9212 }
9213 "});
9214
9215 // The comment prefix is inserted at the same column for every line in a
9216 // selection.
9217 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9218
9219 cx.assert_editor_state(indoc! {"
9220 fn a() {
9221 // «b();
9222 // c();
9223 ˇ»// d();
9224 }
9225 "});
9226
9227 // If a selection ends at the beginning of a line, that line is not toggled.
9228 cx.set_selections_state(indoc! {"
9229 fn a() {
9230 // b();
9231 «// c();
9232 ˇ» // d();
9233 }
9234 "});
9235
9236 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9237
9238 cx.assert_editor_state(indoc! {"
9239 fn a() {
9240 // b();
9241 «c();
9242 ˇ» // d();
9243 }
9244 "});
9245
9246 // If a selection span a single line and is empty, the line is toggled.
9247 cx.set_state(indoc! {"
9248 fn a() {
9249 a();
9250 b();
9251 ˇ
9252 }
9253 "});
9254
9255 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9256
9257 cx.assert_editor_state(indoc! {"
9258 fn a() {
9259 a();
9260 b();
9261 //•ˇ
9262 }
9263 "});
9264
9265 // If a selection span multiple lines, empty lines are not toggled.
9266 cx.set_state(indoc! {"
9267 fn a() {
9268 «a();
9269
9270 c();ˇ»
9271 }
9272 "});
9273
9274 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9275
9276 cx.assert_editor_state(indoc! {"
9277 fn a() {
9278 // «a();
9279
9280 // c();ˇ»
9281 }
9282 "});
9283
9284 // If a selection includes multiple comment prefixes, all lines are uncommented.
9285 cx.set_state(indoc! {"
9286 fn a() {
9287 «// a();
9288 /// b();
9289 //! c();ˇ»
9290 }
9291 "});
9292
9293 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9294
9295 cx.assert_editor_state(indoc! {"
9296 fn a() {
9297 «a();
9298 b();
9299 c();ˇ»
9300 }
9301 "});
9302}
9303
9304#[gpui::test]
9305async fn test_toggle_comment_ignore_indent(cx: &mut gpui::TestAppContext) {
9306 init_test(cx, |_| {});
9307 let mut cx = EditorTestContext::new(cx).await;
9308 let language = Arc::new(Language::new(
9309 LanguageConfig {
9310 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
9311 ..Default::default()
9312 },
9313 Some(tree_sitter_rust::LANGUAGE.into()),
9314 ));
9315 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
9316
9317 let toggle_comments = &ToggleComments {
9318 advance_downwards: false,
9319 ignore_indent: true,
9320 };
9321
9322 // If multiple selections intersect a line, the line is only toggled once.
9323 cx.set_state(indoc! {"
9324 fn a() {
9325 // «b();
9326 // c();
9327 // ˇ» d();
9328 }
9329 "});
9330
9331 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9332
9333 cx.assert_editor_state(indoc! {"
9334 fn a() {
9335 «b();
9336 c();
9337 ˇ» d();
9338 }
9339 "});
9340
9341 // The comment prefix is inserted at the beginning of each line
9342 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9343
9344 cx.assert_editor_state(indoc! {"
9345 fn a() {
9346 // «b();
9347 // c();
9348 // ˇ» d();
9349 }
9350 "});
9351
9352 // If a selection ends at the beginning of a line, that line is not toggled.
9353 cx.set_selections_state(indoc! {"
9354 fn a() {
9355 // b();
9356 // «c();
9357 ˇ»// d();
9358 }
9359 "});
9360
9361 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9362
9363 cx.assert_editor_state(indoc! {"
9364 fn a() {
9365 // b();
9366 «c();
9367 ˇ»// d();
9368 }
9369 "});
9370
9371 // If a selection span a single line and is empty, the line is toggled.
9372 cx.set_state(indoc! {"
9373 fn a() {
9374 a();
9375 b();
9376 ˇ
9377 }
9378 "});
9379
9380 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9381
9382 cx.assert_editor_state(indoc! {"
9383 fn a() {
9384 a();
9385 b();
9386 //ˇ
9387 }
9388 "});
9389
9390 // If a selection span multiple lines, empty lines are not toggled.
9391 cx.set_state(indoc! {"
9392 fn a() {
9393 «a();
9394
9395 c();ˇ»
9396 }
9397 "});
9398
9399 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9400
9401 cx.assert_editor_state(indoc! {"
9402 fn a() {
9403 // «a();
9404
9405 // c();ˇ»
9406 }
9407 "});
9408
9409 // If a selection includes multiple comment prefixes, all lines are uncommented.
9410 cx.set_state(indoc! {"
9411 fn a() {
9412 // «a();
9413 /// b();
9414 //! c();ˇ»
9415 }
9416 "});
9417
9418 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9419
9420 cx.assert_editor_state(indoc! {"
9421 fn a() {
9422 «a();
9423 b();
9424 c();ˇ»
9425 }
9426 "});
9427}
9428
9429#[gpui::test]
9430async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
9431 init_test(cx, |_| {});
9432
9433 let language = Arc::new(Language::new(
9434 LanguageConfig {
9435 line_comments: vec!["// ".into()],
9436 ..Default::default()
9437 },
9438 Some(tree_sitter_rust::LANGUAGE.into()),
9439 ));
9440
9441 let mut cx = EditorTestContext::new(cx).await;
9442
9443 cx.language_registry().add(language.clone());
9444 cx.update_buffer(|buffer, cx| {
9445 buffer.set_language(Some(language), cx);
9446 });
9447
9448 let toggle_comments = &ToggleComments {
9449 advance_downwards: true,
9450 ignore_indent: false,
9451 };
9452
9453 // Single cursor on one line -> advance
9454 // Cursor moves horizontally 3 characters as well on non-blank line
9455 cx.set_state(indoc!(
9456 "fn a() {
9457 ˇdog();
9458 cat();
9459 }"
9460 ));
9461 cx.update_editor(|editor, window, cx| {
9462 editor.toggle_comments(toggle_comments, window, cx);
9463 });
9464 cx.assert_editor_state(indoc!(
9465 "fn a() {
9466 // dog();
9467 catˇ();
9468 }"
9469 ));
9470
9471 // Single selection on one line -> don't advance
9472 cx.set_state(indoc!(
9473 "fn a() {
9474 «dog()ˇ»;
9475 cat();
9476 }"
9477 ));
9478 cx.update_editor(|editor, window, cx| {
9479 editor.toggle_comments(toggle_comments, window, cx);
9480 });
9481 cx.assert_editor_state(indoc!(
9482 "fn a() {
9483 // «dog()ˇ»;
9484 cat();
9485 }"
9486 ));
9487
9488 // Multiple cursors on one line -> advance
9489 cx.set_state(indoc!(
9490 "fn a() {
9491 ˇdˇog();
9492 cat();
9493 }"
9494 ));
9495 cx.update_editor(|editor, window, cx| {
9496 editor.toggle_comments(toggle_comments, window, cx);
9497 });
9498 cx.assert_editor_state(indoc!(
9499 "fn a() {
9500 // dog();
9501 catˇ(ˇ);
9502 }"
9503 ));
9504
9505 // Multiple cursors on one line, with selection -> don't advance
9506 cx.set_state(indoc!(
9507 "fn a() {
9508 ˇdˇog«()ˇ»;
9509 cat();
9510 }"
9511 ));
9512 cx.update_editor(|editor, window, cx| {
9513 editor.toggle_comments(toggle_comments, window, cx);
9514 });
9515 cx.assert_editor_state(indoc!(
9516 "fn a() {
9517 // ˇdˇog«()ˇ»;
9518 cat();
9519 }"
9520 ));
9521
9522 // Single cursor on one line -> advance
9523 // Cursor moves to column 0 on blank line
9524 cx.set_state(indoc!(
9525 "fn a() {
9526 ˇdog();
9527
9528 cat();
9529 }"
9530 ));
9531 cx.update_editor(|editor, window, cx| {
9532 editor.toggle_comments(toggle_comments, window, cx);
9533 });
9534 cx.assert_editor_state(indoc!(
9535 "fn a() {
9536 // dog();
9537 ˇ
9538 cat();
9539 }"
9540 ));
9541
9542 // Single cursor on one line -> advance
9543 // Cursor starts and ends at column 0
9544 cx.set_state(indoc!(
9545 "fn a() {
9546 ˇ dog();
9547 cat();
9548 }"
9549 ));
9550 cx.update_editor(|editor, window, cx| {
9551 editor.toggle_comments(toggle_comments, window, cx);
9552 });
9553 cx.assert_editor_state(indoc!(
9554 "fn a() {
9555 // dog();
9556 ˇ cat();
9557 }"
9558 ));
9559}
9560
9561#[gpui::test]
9562async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
9563 init_test(cx, |_| {});
9564
9565 let mut cx = EditorTestContext::new(cx).await;
9566
9567 let html_language = Arc::new(
9568 Language::new(
9569 LanguageConfig {
9570 name: "HTML".into(),
9571 block_comment: Some(("<!-- ".into(), " -->".into())),
9572 ..Default::default()
9573 },
9574 Some(tree_sitter_html::language()),
9575 )
9576 .with_injection_query(
9577 r#"
9578 (script_element
9579 (raw_text) @injection.content
9580 (#set! injection.language "javascript"))
9581 "#,
9582 )
9583 .unwrap(),
9584 );
9585
9586 let javascript_language = Arc::new(Language::new(
9587 LanguageConfig {
9588 name: "JavaScript".into(),
9589 line_comments: vec!["// ".into()],
9590 ..Default::default()
9591 },
9592 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9593 ));
9594
9595 cx.language_registry().add(html_language.clone());
9596 cx.language_registry().add(javascript_language.clone());
9597 cx.update_buffer(|buffer, cx| {
9598 buffer.set_language(Some(html_language), cx);
9599 });
9600
9601 // Toggle comments for empty selections
9602 cx.set_state(
9603 &r#"
9604 <p>A</p>ˇ
9605 <p>B</p>ˇ
9606 <p>C</p>ˇ
9607 "#
9608 .unindent(),
9609 );
9610 cx.update_editor(|editor, window, cx| {
9611 editor.toggle_comments(&ToggleComments::default(), window, cx)
9612 });
9613 cx.assert_editor_state(
9614 &r#"
9615 <!-- <p>A</p>ˇ -->
9616 <!-- <p>B</p>ˇ -->
9617 <!-- <p>C</p>ˇ -->
9618 "#
9619 .unindent(),
9620 );
9621 cx.update_editor(|editor, window, cx| {
9622 editor.toggle_comments(&ToggleComments::default(), window, cx)
9623 });
9624 cx.assert_editor_state(
9625 &r#"
9626 <p>A</p>ˇ
9627 <p>B</p>ˇ
9628 <p>C</p>ˇ
9629 "#
9630 .unindent(),
9631 );
9632
9633 // Toggle comments for mixture of empty and non-empty selections, where
9634 // multiple selections occupy a given line.
9635 cx.set_state(
9636 &r#"
9637 <p>A«</p>
9638 <p>ˇ»B</p>ˇ
9639 <p>C«</p>
9640 <p>ˇ»D</p>ˇ
9641 "#
9642 .unindent(),
9643 );
9644
9645 cx.update_editor(|editor, window, cx| {
9646 editor.toggle_comments(&ToggleComments::default(), window, cx)
9647 });
9648 cx.assert_editor_state(
9649 &r#"
9650 <!-- <p>A«</p>
9651 <p>ˇ»B</p>ˇ -->
9652 <!-- <p>C«</p>
9653 <p>ˇ»D</p>ˇ -->
9654 "#
9655 .unindent(),
9656 );
9657 cx.update_editor(|editor, window, cx| {
9658 editor.toggle_comments(&ToggleComments::default(), window, cx)
9659 });
9660 cx.assert_editor_state(
9661 &r#"
9662 <p>A«</p>
9663 <p>ˇ»B</p>ˇ
9664 <p>C«</p>
9665 <p>ˇ»D</p>ˇ
9666 "#
9667 .unindent(),
9668 );
9669
9670 // Toggle comments when different languages are active for different
9671 // selections.
9672 cx.set_state(
9673 &r#"
9674 ˇ<script>
9675 ˇvar x = new Y();
9676 ˇ</script>
9677 "#
9678 .unindent(),
9679 );
9680 cx.executor().run_until_parked();
9681 cx.update_editor(|editor, window, cx| {
9682 editor.toggle_comments(&ToggleComments::default(), window, cx)
9683 });
9684 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
9685 // Uncommenting and commenting from this position brings in even more wrong artifacts.
9686 cx.assert_editor_state(
9687 &r#"
9688 <!-- ˇ<script> -->
9689 // ˇvar x = new Y();
9690 // ˇ</script>
9691 "#
9692 .unindent(),
9693 );
9694}
9695
9696#[gpui::test]
9697fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
9698 init_test(cx, |_| {});
9699
9700 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9701 let multibuffer = cx.new(|cx| {
9702 let mut multibuffer = MultiBuffer::new(ReadWrite);
9703 multibuffer.push_excerpts(
9704 buffer.clone(),
9705 [
9706 ExcerptRange {
9707 context: Point::new(0, 0)..Point::new(0, 4),
9708 primary: None,
9709 },
9710 ExcerptRange {
9711 context: Point::new(1, 0)..Point::new(1, 4),
9712 primary: None,
9713 },
9714 ],
9715 cx,
9716 );
9717 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
9718 multibuffer
9719 });
9720
9721 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
9722 editor.update_in(cx, |editor, window, cx| {
9723 assert_eq!(editor.text(cx), "aaaa\nbbbb");
9724 editor.change_selections(None, window, cx, |s| {
9725 s.select_ranges([
9726 Point::new(0, 0)..Point::new(0, 0),
9727 Point::new(1, 0)..Point::new(1, 0),
9728 ])
9729 });
9730
9731 editor.handle_input("X", window, cx);
9732 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
9733 assert_eq!(
9734 editor.selections.ranges(cx),
9735 [
9736 Point::new(0, 1)..Point::new(0, 1),
9737 Point::new(1, 1)..Point::new(1, 1),
9738 ]
9739 );
9740
9741 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
9742 editor.change_selections(None, window, cx, |s| {
9743 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
9744 });
9745 editor.backspace(&Default::default(), window, cx);
9746 assert_eq!(editor.text(cx), "Xa\nbbb");
9747 assert_eq!(
9748 editor.selections.ranges(cx),
9749 [Point::new(1, 0)..Point::new(1, 0)]
9750 );
9751
9752 editor.change_selections(None, window, cx, |s| {
9753 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
9754 });
9755 editor.backspace(&Default::default(), window, cx);
9756 assert_eq!(editor.text(cx), "X\nbb");
9757 assert_eq!(
9758 editor.selections.ranges(cx),
9759 [Point::new(0, 1)..Point::new(0, 1)]
9760 );
9761 });
9762}
9763
9764#[gpui::test]
9765fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
9766 init_test(cx, |_| {});
9767
9768 let markers = vec![('[', ']').into(), ('(', ')').into()];
9769 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
9770 indoc! {"
9771 [aaaa
9772 (bbbb]
9773 cccc)",
9774 },
9775 markers.clone(),
9776 );
9777 let excerpt_ranges = markers.into_iter().map(|marker| {
9778 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
9779 ExcerptRange {
9780 context,
9781 primary: None,
9782 }
9783 });
9784 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
9785 let multibuffer = cx.new(|cx| {
9786 let mut multibuffer = MultiBuffer::new(ReadWrite);
9787 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
9788 multibuffer
9789 });
9790
9791 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
9792 editor.update_in(cx, |editor, window, cx| {
9793 let (expected_text, selection_ranges) = marked_text_ranges(
9794 indoc! {"
9795 aaaa
9796 bˇbbb
9797 bˇbbˇb
9798 cccc"
9799 },
9800 true,
9801 );
9802 assert_eq!(editor.text(cx), expected_text);
9803 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
9804
9805 editor.handle_input("X", window, cx);
9806
9807 let (expected_text, expected_selections) = marked_text_ranges(
9808 indoc! {"
9809 aaaa
9810 bXˇbbXb
9811 bXˇbbXˇb
9812 cccc"
9813 },
9814 false,
9815 );
9816 assert_eq!(editor.text(cx), expected_text);
9817 assert_eq!(editor.selections.ranges(cx), expected_selections);
9818
9819 editor.newline(&Newline, window, cx);
9820 let (expected_text, expected_selections) = marked_text_ranges(
9821 indoc! {"
9822 aaaa
9823 bX
9824 ˇbbX
9825 b
9826 bX
9827 ˇbbX
9828 ˇb
9829 cccc"
9830 },
9831 false,
9832 );
9833 assert_eq!(editor.text(cx), expected_text);
9834 assert_eq!(editor.selections.ranges(cx), expected_selections);
9835 });
9836}
9837
9838#[gpui::test]
9839fn test_refresh_selections(cx: &mut TestAppContext) {
9840 init_test(cx, |_| {});
9841
9842 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9843 let mut excerpt1_id = None;
9844 let multibuffer = cx.new(|cx| {
9845 let mut multibuffer = MultiBuffer::new(ReadWrite);
9846 excerpt1_id = multibuffer
9847 .push_excerpts(
9848 buffer.clone(),
9849 [
9850 ExcerptRange {
9851 context: Point::new(0, 0)..Point::new(1, 4),
9852 primary: None,
9853 },
9854 ExcerptRange {
9855 context: Point::new(1, 0)..Point::new(2, 4),
9856 primary: None,
9857 },
9858 ],
9859 cx,
9860 )
9861 .into_iter()
9862 .next();
9863 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
9864 multibuffer
9865 });
9866
9867 let editor = cx.add_window(|window, cx| {
9868 let mut editor = build_editor(multibuffer.clone(), window, cx);
9869 let snapshot = editor.snapshot(window, cx);
9870 editor.change_selections(None, window, cx, |s| {
9871 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
9872 });
9873 editor.begin_selection(
9874 Point::new(2, 1).to_display_point(&snapshot),
9875 true,
9876 1,
9877 window,
9878 cx,
9879 );
9880 assert_eq!(
9881 editor.selections.ranges(cx),
9882 [
9883 Point::new(1, 3)..Point::new(1, 3),
9884 Point::new(2, 1)..Point::new(2, 1),
9885 ]
9886 );
9887 editor
9888 });
9889
9890 // Refreshing selections is a no-op when excerpts haven't changed.
9891 _ = editor.update(cx, |editor, window, cx| {
9892 editor.change_selections(None, window, cx, |s| s.refresh());
9893 assert_eq!(
9894 editor.selections.ranges(cx),
9895 [
9896 Point::new(1, 3)..Point::new(1, 3),
9897 Point::new(2, 1)..Point::new(2, 1),
9898 ]
9899 );
9900 });
9901
9902 multibuffer.update(cx, |multibuffer, cx| {
9903 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
9904 });
9905 _ = editor.update(cx, |editor, window, cx| {
9906 // Removing an excerpt causes the first selection to become degenerate.
9907 assert_eq!(
9908 editor.selections.ranges(cx),
9909 [
9910 Point::new(0, 0)..Point::new(0, 0),
9911 Point::new(0, 1)..Point::new(0, 1)
9912 ]
9913 );
9914
9915 // Refreshing selections will relocate the first selection to the original buffer
9916 // location.
9917 editor.change_selections(None, window, cx, |s| s.refresh());
9918 assert_eq!(
9919 editor.selections.ranges(cx),
9920 [
9921 Point::new(0, 1)..Point::new(0, 1),
9922 Point::new(0, 3)..Point::new(0, 3)
9923 ]
9924 );
9925 assert!(editor.selections.pending_anchor().is_some());
9926 });
9927}
9928
9929#[gpui::test]
9930fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
9931 init_test(cx, |_| {});
9932
9933 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9934 let mut excerpt1_id = None;
9935 let multibuffer = cx.new(|cx| {
9936 let mut multibuffer = MultiBuffer::new(ReadWrite);
9937 excerpt1_id = multibuffer
9938 .push_excerpts(
9939 buffer.clone(),
9940 [
9941 ExcerptRange {
9942 context: Point::new(0, 0)..Point::new(1, 4),
9943 primary: None,
9944 },
9945 ExcerptRange {
9946 context: Point::new(1, 0)..Point::new(2, 4),
9947 primary: None,
9948 },
9949 ],
9950 cx,
9951 )
9952 .into_iter()
9953 .next();
9954 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
9955 multibuffer
9956 });
9957
9958 let editor = cx.add_window(|window, cx| {
9959 let mut editor = build_editor(multibuffer.clone(), window, cx);
9960 let snapshot = editor.snapshot(window, cx);
9961 editor.begin_selection(
9962 Point::new(1, 3).to_display_point(&snapshot),
9963 false,
9964 1,
9965 window,
9966 cx,
9967 );
9968 assert_eq!(
9969 editor.selections.ranges(cx),
9970 [Point::new(1, 3)..Point::new(1, 3)]
9971 );
9972 editor
9973 });
9974
9975 multibuffer.update(cx, |multibuffer, cx| {
9976 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
9977 });
9978 _ = editor.update(cx, |editor, window, cx| {
9979 assert_eq!(
9980 editor.selections.ranges(cx),
9981 [Point::new(0, 0)..Point::new(0, 0)]
9982 );
9983
9984 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
9985 editor.change_selections(None, window, cx, |s| s.refresh());
9986 assert_eq!(
9987 editor.selections.ranges(cx),
9988 [Point::new(0, 3)..Point::new(0, 3)]
9989 );
9990 assert!(editor.selections.pending_anchor().is_some());
9991 });
9992}
9993
9994#[gpui::test]
9995async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
9996 init_test(cx, |_| {});
9997
9998 let language = Arc::new(
9999 Language::new(
10000 LanguageConfig {
10001 brackets: BracketPairConfig {
10002 pairs: vec![
10003 BracketPair {
10004 start: "{".to_string(),
10005 end: "}".to_string(),
10006 close: true,
10007 surround: true,
10008 newline: true,
10009 },
10010 BracketPair {
10011 start: "/* ".to_string(),
10012 end: " */".to_string(),
10013 close: true,
10014 surround: true,
10015 newline: true,
10016 },
10017 ],
10018 ..Default::default()
10019 },
10020 ..Default::default()
10021 },
10022 Some(tree_sitter_rust::LANGUAGE.into()),
10023 )
10024 .with_indents_query("")
10025 .unwrap(),
10026 );
10027
10028 let text = concat!(
10029 "{ }\n", //
10030 " x\n", //
10031 " /* */\n", //
10032 "x\n", //
10033 "{{} }\n", //
10034 );
10035
10036 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10037 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10038 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10039 editor
10040 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10041 .await;
10042
10043 editor.update_in(cx, |editor, window, cx| {
10044 editor.change_selections(None, window, cx, |s| {
10045 s.select_display_ranges([
10046 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
10047 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
10048 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
10049 ])
10050 });
10051 editor.newline(&Newline, window, cx);
10052
10053 assert_eq!(
10054 editor.buffer().read(cx).read(cx).text(),
10055 concat!(
10056 "{ \n", // Suppress rustfmt
10057 "\n", //
10058 "}\n", //
10059 " x\n", //
10060 " /* \n", //
10061 " \n", //
10062 " */\n", //
10063 "x\n", //
10064 "{{} \n", //
10065 "}\n", //
10066 )
10067 );
10068 });
10069}
10070
10071#[gpui::test]
10072fn test_highlighted_ranges(cx: &mut TestAppContext) {
10073 init_test(cx, |_| {});
10074
10075 let editor = cx.add_window(|window, cx| {
10076 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
10077 build_editor(buffer.clone(), window, cx)
10078 });
10079
10080 _ = editor.update(cx, |editor, window, cx| {
10081 struct Type1;
10082 struct Type2;
10083
10084 let buffer = editor.buffer.read(cx).snapshot(cx);
10085
10086 let anchor_range =
10087 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
10088
10089 editor.highlight_background::<Type1>(
10090 &[
10091 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
10092 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
10093 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
10094 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
10095 ],
10096 |_| Hsla::red(),
10097 cx,
10098 );
10099 editor.highlight_background::<Type2>(
10100 &[
10101 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
10102 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
10103 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
10104 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
10105 ],
10106 |_| Hsla::green(),
10107 cx,
10108 );
10109
10110 let snapshot = editor.snapshot(window, cx);
10111 let mut highlighted_ranges = editor.background_highlights_in_range(
10112 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
10113 &snapshot,
10114 cx.theme().colors(),
10115 );
10116 // Enforce a consistent ordering based on color without relying on the ordering of the
10117 // highlight's `TypeId` which is non-executor.
10118 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
10119 assert_eq!(
10120 highlighted_ranges,
10121 &[
10122 (
10123 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
10124 Hsla::red(),
10125 ),
10126 (
10127 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
10128 Hsla::red(),
10129 ),
10130 (
10131 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
10132 Hsla::green(),
10133 ),
10134 (
10135 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
10136 Hsla::green(),
10137 ),
10138 ]
10139 );
10140 assert_eq!(
10141 editor.background_highlights_in_range(
10142 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
10143 &snapshot,
10144 cx.theme().colors(),
10145 ),
10146 &[(
10147 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
10148 Hsla::red(),
10149 )]
10150 );
10151 });
10152}
10153
10154#[gpui::test]
10155async fn test_following(cx: &mut gpui::TestAppContext) {
10156 init_test(cx, |_| {});
10157
10158 let fs = FakeFs::new(cx.executor());
10159 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
10160
10161 let buffer = project.update(cx, |project, cx| {
10162 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
10163 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
10164 });
10165 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
10166 let follower = cx.update(|cx| {
10167 cx.open_window(
10168 WindowOptions {
10169 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
10170 gpui::Point::new(px(0.), px(0.)),
10171 gpui::Point::new(px(10.), px(80.)),
10172 ))),
10173 ..Default::default()
10174 },
10175 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
10176 )
10177 .unwrap()
10178 });
10179
10180 let is_still_following = Rc::new(RefCell::new(true));
10181 let follower_edit_event_count = Rc::new(RefCell::new(0));
10182 let pending_update = Rc::new(RefCell::new(None));
10183 let leader_model = leader.root(cx).unwrap();
10184 let follower_model = follower.root(cx).unwrap();
10185 _ = follower.update(cx, {
10186 let update = pending_update.clone();
10187 let is_still_following = is_still_following.clone();
10188 let follower_edit_event_count = follower_edit_event_count.clone();
10189 |_, window, cx| {
10190 cx.subscribe_in(
10191 &leader_model,
10192 window,
10193 move |_, leader, event, window, cx| {
10194 leader.read(cx).add_event_to_update_proto(
10195 event,
10196 &mut update.borrow_mut(),
10197 window,
10198 cx,
10199 );
10200 },
10201 )
10202 .detach();
10203
10204 cx.subscribe_in(
10205 &follower_model,
10206 window,
10207 move |_, _, event: &EditorEvent, _window, _cx| {
10208 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
10209 *is_still_following.borrow_mut() = false;
10210 }
10211
10212 if let EditorEvent::BufferEdited = event {
10213 *follower_edit_event_count.borrow_mut() += 1;
10214 }
10215 },
10216 )
10217 .detach();
10218 }
10219 });
10220
10221 // Update the selections only
10222 _ = leader.update(cx, |leader, window, cx| {
10223 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
10224 });
10225 follower
10226 .update(cx, |follower, window, cx| {
10227 follower.apply_update_proto(
10228 &project,
10229 pending_update.borrow_mut().take().unwrap(),
10230 window,
10231 cx,
10232 )
10233 })
10234 .unwrap()
10235 .await
10236 .unwrap();
10237 _ = follower.update(cx, |follower, _, cx| {
10238 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
10239 });
10240 assert!(*is_still_following.borrow());
10241 assert_eq!(*follower_edit_event_count.borrow(), 0);
10242
10243 // Update the scroll position only
10244 _ = leader.update(cx, |leader, window, cx| {
10245 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
10246 });
10247 follower
10248 .update(cx, |follower, window, cx| {
10249 follower.apply_update_proto(
10250 &project,
10251 pending_update.borrow_mut().take().unwrap(),
10252 window,
10253 cx,
10254 )
10255 })
10256 .unwrap()
10257 .await
10258 .unwrap();
10259 assert_eq!(
10260 follower
10261 .update(cx, |follower, _, cx| follower.scroll_position(cx))
10262 .unwrap(),
10263 gpui::Point::new(1.5, 3.5)
10264 );
10265 assert!(*is_still_following.borrow());
10266 assert_eq!(*follower_edit_event_count.borrow(), 0);
10267
10268 // Update the selections and scroll position. The follower's scroll position is updated
10269 // via autoscroll, not via the leader's exact scroll position.
10270 _ = leader.update(cx, |leader, window, cx| {
10271 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10272 leader.request_autoscroll(Autoscroll::newest(), cx);
10273 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
10274 });
10275 follower
10276 .update(cx, |follower, window, cx| {
10277 follower.apply_update_proto(
10278 &project,
10279 pending_update.borrow_mut().take().unwrap(),
10280 window,
10281 cx,
10282 )
10283 })
10284 .unwrap()
10285 .await
10286 .unwrap();
10287 _ = follower.update(cx, |follower, _, cx| {
10288 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
10289 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
10290 });
10291 assert!(*is_still_following.borrow());
10292
10293 // Creating a pending selection that precedes another selection
10294 _ = leader.update(cx, |leader, window, cx| {
10295 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
10296 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
10297 });
10298 follower
10299 .update(cx, |follower, window, cx| {
10300 follower.apply_update_proto(
10301 &project,
10302 pending_update.borrow_mut().take().unwrap(),
10303 window,
10304 cx,
10305 )
10306 })
10307 .unwrap()
10308 .await
10309 .unwrap();
10310 _ = follower.update(cx, |follower, _, cx| {
10311 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
10312 });
10313 assert!(*is_still_following.borrow());
10314
10315 // Extend the pending selection so that it surrounds another selection
10316 _ = leader.update(cx, |leader, window, cx| {
10317 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
10318 });
10319 follower
10320 .update(cx, |follower, window, cx| {
10321 follower.apply_update_proto(
10322 &project,
10323 pending_update.borrow_mut().take().unwrap(),
10324 window,
10325 cx,
10326 )
10327 })
10328 .unwrap()
10329 .await
10330 .unwrap();
10331 _ = follower.update(cx, |follower, _, cx| {
10332 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
10333 });
10334
10335 // Scrolling locally breaks the follow
10336 _ = follower.update(cx, |follower, window, cx| {
10337 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
10338 follower.set_scroll_anchor(
10339 ScrollAnchor {
10340 anchor: top_anchor,
10341 offset: gpui::Point::new(0.0, 0.5),
10342 },
10343 window,
10344 cx,
10345 );
10346 });
10347 assert!(!(*is_still_following.borrow()));
10348}
10349
10350#[gpui::test]
10351async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
10352 init_test(cx, |_| {});
10353
10354 let fs = FakeFs::new(cx.executor());
10355 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
10356 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10357 let pane = workspace
10358 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10359 .unwrap();
10360
10361 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10362
10363 let leader = pane.update_in(cx, |_, window, cx| {
10364 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
10365 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
10366 });
10367
10368 // Start following the editor when it has no excerpts.
10369 let mut state_message =
10370 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
10371 let workspace_model = workspace.root(cx).unwrap();
10372 let follower_1 = cx
10373 .update_window(*workspace.deref(), |_, window, cx| {
10374 Editor::from_state_proto(
10375 workspace_model,
10376 ViewId {
10377 creator: Default::default(),
10378 id: 0,
10379 },
10380 &mut state_message,
10381 window,
10382 cx,
10383 )
10384 })
10385 .unwrap()
10386 .unwrap()
10387 .await
10388 .unwrap();
10389
10390 let update_message = Rc::new(RefCell::new(None));
10391 follower_1.update_in(cx, {
10392 let update = update_message.clone();
10393 |_, window, cx| {
10394 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
10395 leader.read(cx).add_event_to_update_proto(
10396 event,
10397 &mut update.borrow_mut(),
10398 window,
10399 cx,
10400 );
10401 })
10402 .detach();
10403 }
10404 });
10405
10406 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
10407 (
10408 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
10409 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
10410 )
10411 });
10412
10413 // Insert some excerpts.
10414 leader.update(cx, |leader, cx| {
10415 leader.buffer.update(cx, |multibuffer, cx| {
10416 let excerpt_ids = multibuffer.push_excerpts(
10417 buffer_1.clone(),
10418 [
10419 ExcerptRange {
10420 context: 1..6,
10421 primary: None,
10422 },
10423 ExcerptRange {
10424 context: 12..15,
10425 primary: None,
10426 },
10427 ExcerptRange {
10428 context: 0..3,
10429 primary: None,
10430 },
10431 ],
10432 cx,
10433 );
10434 multibuffer.insert_excerpts_after(
10435 excerpt_ids[0],
10436 buffer_2.clone(),
10437 [
10438 ExcerptRange {
10439 context: 8..12,
10440 primary: None,
10441 },
10442 ExcerptRange {
10443 context: 0..6,
10444 primary: None,
10445 },
10446 ],
10447 cx,
10448 );
10449 });
10450 });
10451
10452 // Apply the update of adding the excerpts.
10453 follower_1
10454 .update_in(cx, |follower, window, cx| {
10455 follower.apply_update_proto(
10456 &project,
10457 update_message.borrow().clone().unwrap(),
10458 window,
10459 cx,
10460 )
10461 })
10462 .await
10463 .unwrap();
10464 assert_eq!(
10465 follower_1.update(cx, |editor, cx| editor.text(cx)),
10466 leader.update(cx, |editor, cx| editor.text(cx))
10467 );
10468 update_message.borrow_mut().take();
10469
10470 // Start following separately after it already has excerpts.
10471 let mut state_message =
10472 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
10473 let workspace_model = workspace.root(cx).unwrap();
10474 let follower_2 = cx
10475 .update_window(*workspace.deref(), |_, window, cx| {
10476 Editor::from_state_proto(
10477 workspace_model,
10478 ViewId {
10479 creator: Default::default(),
10480 id: 0,
10481 },
10482 &mut state_message,
10483 window,
10484 cx,
10485 )
10486 })
10487 .unwrap()
10488 .unwrap()
10489 .await
10490 .unwrap();
10491 assert_eq!(
10492 follower_2.update(cx, |editor, cx| editor.text(cx)),
10493 leader.update(cx, |editor, cx| editor.text(cx))
10494 );
10495
10496 // Remove some excerpts.
10497 leader.update(cx, |leader, cx| {
10498 leader.buffer.update(cx, |multibuffer, cx| {
10499 let excerpt_ids = multibuffer.excerpt_ids();
10500 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
10501 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
10502 });
10503 });
10504
10505 // Apply the update of removing the excerpts.
10506 follower_1
10507 .update_in(cx, |follower, window, cx| {
10508 follower.apply_update_proto(
10509 &project,
10510 update_message.borrow().clone().unwrap(),
10511 window,
10512 cx,
10513 )
10514 })
10515 .await
10516 .unwrap();
10517 follower_2
10518 .update_in(cx, |follower, window, cx| {
10519 follower.apply_update_proto(
10520 &project,
10521 update_message.borrow().clone().unwrap(),
10522 window,
10523 cx,
10524 )
10525 })
10526 .await
10527 .unwrap();
10528 update_message.borrow_mut().take();
10529 assert_eq!(
10530 follower_1.update(cx, |editor, cx| editor.text(cx)),
10531 leader.update(cx, |editor, cx| editor.text(cx))
10532 );
10533}
10534
10535#[gpui::test]
10536async fn go_to_prev_overlapping_diagnostic(
10537 executor: BackgroundExecutor,
10538 cx: &mut gpui::TestAppContext,
10539) {
10540 init_test(cx, |_| {});
10541
10542 let mut cx = EditorTestContext::new(cx).await;
10543 let lsp_store =
10544 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10545
10546 cx.set_state(indoc! {"
10547 ˇfn func(abc def: i32) -> u32 {
10548 }
10549 "});
10550
10551 cx.update(|_, cx| {
10552 lsp_store.update(cx, |lsp_store, cx| {
10553 lsp_store
10554 .update_diagnostics(
10555 LanguageServerId(0),
10556 lsp::PublishDiagnosticsParams {
10557 uri: lsp::Url::from_file_path("/root/file").unwrap(),
10558 version: None,
10559 diagnostics: vec![
10560 lsp::Diagnostic {
10561 range: lsp::Range::new(
10562 lsp::Position::new(0, 11),
10563 lsp::Position::new(0, 12),
10564 ),
10565 severity: Some(lsp::DiagnosticSeverity::ERROR),
10566 ..Default::default()
10567 },
10568 lsp::Diagnostic {
10569 range: lsp::Range::new(
10570 lsp::Position::new(0, 12),
10571 lsp::Position::new(0, 15),
10572 ),
10573 severity: Some(lsp::DiagnosticSeverity::ERROR),
10574 ..Default::default()
10575 },
10576 lsp::Diagnostic {
10577 range: lsp::Range::new(
10578 lsp::Position::new(0, 25),
10579 lsp::Position::new(0, 28),
10580 ),
10581 severity: Some(lsp::DiagnosticSeverity::ERROR),
10582 ..Default::default()
10583 },
10584 ],
10585 },
10586 &[],
10587 cx,
10588 )
10589 .unwrap()
10590 });
10591 });
10592
10593 executor.run_until_parked();
10594
10595 cx.update_editor(|editor, window, cx| {
10596 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10597 });
10598
10599 cx.assert_editor_state(indoc! {"
10600 fn func(abc def: i32) -> ˇu32 {
10601 }
10602 "});
10603
10604 cx.update_editor(|editor, window, cx| {
10605 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10606 });
10607
10608 cx.assert_editor_state(indoc! {"
10609 fn func(abc ˇdef: i32) -> u32 {
10610 }
10611 "});
10612
10613 cx.update_editor(|editor, window, cx| {
10614 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10615 });
10616
10617 cx.assert_editor_state(indoc! {"
10618 fn func(abcˇ def: i32) -> u32 {
10619 }
10620 "});
10621
10622 cx.update_editor(|editor, window, cx| {
10623 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10624 });
10625
10626 cx.assert_editor_state(indoc! {"
10627 fn func(abc def: i32) -> ˇu32 {
10628 }
10629 "});
10630}
10631
10632#[gpui::test]
10633async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
10634 init_test(cx, |_| {});
10635
10636 let mut cx = EditorTestContext::new(cx).await;
10637
10638 cx.set_state(indoc! {"
10639 fn func(abˇc def: i32) -> u32 {
10640 }
10641 "});
10642 let lsp_store =
10643 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10644
10645 cx.update(|_, cx| {
10646 lsp_store.update(cx, |lsp_store, cx| {
10647 lsp_store.update_diagnostics(
10648 LanguageServerId(0),
10649 lsp::PublishDiagnosticsParams {
10650 uri: lsp::Url::from_file_path("/root/file").unwrap(),
10651 version: None,
10652 diagnostics: vec![lsp::Diagnostic {
10653 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
10654 severity: Some(lsp::DiagnosticSeverity::ERROR),
10655 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
10656 ..Default::default()
10657 }],
10658 },
10659 &[],
10660 cx,
10661 )
10662 })
10663 }).unwrap();
10664 cx.run_until_parked();
10665 cx.update_editor(|editor, window, cx| {
10666 hover_popover::hover(editor, &Default::default(), window, cx)
10667 });
10668 cx.run_until_parked();
10669 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
10670}
10671
10672#[gpui::test]
10673async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
10674 init_test(cx, |_| {});
10675
10676 let mut cx = EditorTestContext::new(cx).await;
10677
10678 let diff_base = r#"
10679 use some::mod;
10680
10681 const A: u32 = 42;
10682
10683 fn main() {
10684 println!("hello");
10685
10686 println!("world");
10687 }
10688 "#
10689 .unindent();
10690
10691 // Edits are modified, removed, modified, added
10692 cx.set_state(
10693 &r#"
10694 use some::modified;
10695
10696 ˇ
10697 fn main() {
10698 println!("hello there");
10699
10700 println!("around the");
10701 println!("world");
10702 }
10703 "#
10704 .unindent(),
10705 );
10706
10707 cx.set_diff_base(&diff_base);
10708 executor.run_until_parked();
10709
10710 cx.update_editor(|editor, window, cx| {
10711 //Wrap around the bottom of the buffer
10712 for _ in 0..3 {
10713 editor.go_to_next_hunk(&GoToHunk, window, cx);
10714 }
10715 });
10716
10717 cx.assert_editor_state(
10718 &r#"
10719 ˇuse some::modified;
10720
10721
10722 fn main() {
10723 println!("hello there");
10724
10725 println!("around the");
10726 println!("world");
10727 }
10728 "#
10729 .unindent(),
10730 );
10731
10732 cx.update_editor(|editor, window, cx| {
10733 //Wrap around the top of the buffer
10734 for _ in 0..2 {
10735 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10736 }
10737 });
10738
10739 cx.assert_editor_state(
10740 &r#"
10741 use some::modified;
10742
10743
10744 fn main() {
10745 ˇ println!("hello there");
10746
10747 println!("around the");
10748 println!("world");
10749 }
10750 "#
10751 .unindent(),
10752 );
10753
10754 cx.update_editor(|editor, window, cx| {
10755 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10756 });
10757
10758 cx.assert_editor_state(
10759 &r#"
10760 use some::modified;
10761
10762 ˇ
10763 fn main() {
10764 println!("hello there");
10765
10766 println!("around the");
10767 println!("world");
10768 }
10769 "#
10770 .unindent(),
10771 );
10772
10773 cx.update_editor(|editor, window, cx| {
10774 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10775 });
10776
10777 cx.assert_editor_state(
10778 &r#"
10779 ˇuse some::modified;
10780
10781
10782 fn main() {
10783 println!("hello there");
10784
10785 println!("around the");
10786 println!("world");
10787 }
10788 "#
10789 .unindent(),
10790 );
10791
10792 cx.update_editor(|editor, window, cx| {
10793 for _ in 0..2 {
10794 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10795 }
10796 });
10797
10798 cx.assert_editor_state(
10799 &r#"
10800 use some::modified;
10801
10802
10803 fn main() {
10804 ˇ println!("hello there");
10805
10806 println!("around the");
10807 println!("world");
10808 }
10809 "#
10810 .unindent(),
10811 );
10812
10813 cx.update_editor(|editor, window, cx| {
10814 editor.fold(&Fold, window, cx);
10815 });
10816
10817 cx.update_editor(|editor, window, cx| {
10818 editor.go_to_next_hunk(&GoToHunk, window, cx);
10819 });
10820
10821 cx.assert_editor_state(
10822 &r#"
10823 ˇuse some::modified;
10824
10825
10826 fn main() {
10827 println!("hello there");
10828
10829 println!("around the");
10830 println!("world");
10831 }
10832 "#
10833 .unindent(),
10834 );
10835}
10836
10837#[test]
10838fn test_split_words() {
10839 fn split(text: &str) -> Vec<&str> {
10840 split_words(text).collect()
10841 }
10842
10843 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
10844 assert_eq!(split("hello_world"), &["hello_", "world"]);
10845 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
10846 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
10847 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
10848 assert_eq!(split("helloworld"), &["helloworld"]);
10849
10850 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
10851}
10852
10853#[gpui::test]
10854async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
10855 init_test(cx, |_| {});
10856
10857 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
10858 let mut assert = |before, after| {
10859 let _state_context = cx.set_state(before);
10860 cx.run_until_parked();
10861 cx.update_editor(|editor, window, cx| {
10862 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
10863 });
10864 cx.assert_editor_state(after);
10865 };
10866
10867 // Outside bracket jumps to outside of matching bracket
10868 assert("console.logˇ(var);", "console.log(var)ˇ;");
10869 assert("console.log(var)ˇ;", "console.logˇ(var);");
10870
10871 // Inside bracket jumps to inside of matching bracket
10872 assert("console.log(ˇvar);", "console.log(varˇ);");
10873 assert("console.log(varˇ);", "console.log(ˇvar);");
10874
10875 // When outside a bracket and inside, favor jumping to the inside bracket
10876 assert(
10877 "console.log('foo', [1, 2, 3]ˇ);",
10878 "console.log(ˇ'foo', [1, 2, 3]);",
10879 );
10880 assert(
10881 "console.log(ˇ'foo', [1, 2, 3]);",
10882 "console.log('foo', [1, 2, 3]ˇ);",
10883 );
10884
10885 // Bias forward if two options are equally likely
10886 assert(
10887 "let result = curried_fun()ˇ();",
10888 "let result = curried_fun()()ˇ;",
10889 );
10890
10891 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
10892 assert(
10893 indoc! {"
10894 function test() {
10895 console.log('test')ˇ
10896 }"},
10897 indoc! {"
10898 function test() {
10899 console.logˇ('test')
10900 }"},
10901 );
10902}
10903
10904#[gpui::test]
10905async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
10906 init_test(cx, |_| {});
10907
10908 let fs = FakeFs::new(cx.executor());
10909 fs.insert_tree(
10910 "/a",
10911 json!({
10912 "main.rs": "fn main() { let a = 5; }",
10913 "other.rs": "// Test file",
10914 }),
10915 )
10916 .await;
10917 let project = Project::test(fs, ["/a".as_ref()], cx).await;
10918
10919 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10920 language_registry.add(Arc::new(Language::new(
10921 LanguageConfig {
10922 name: "Rust".into(),
10923 matcher: LanguageMatcher {
10924 path_suffixes: vec!["rs".to_string()],
10925 ..Default::default()
10926 },
10927 brackets: BracketPairConfig {
10928 pairs: vec![BracketPair {
10929 start: "{".to_string(),
10930 end: "}".to_string(),
10931 close: true,
10932 surround: true,
10933 newline: true,
10934 }],
10935 disabled_scopes_by_bracket_ix: Vec::new(),
10936 },
10937 ..Default::default()
10938 },
10939 Some(tree_sitter_rust::LANGUAGE.into()),
10940 )));
10941 let mut fake_servers = language_registry.register_fake_lsp(
10942 "Rust",
10943 FakeLspAdapter {
10944 capabilities: lsp::ServerCapabilities {
10945 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
10946 first_trigger_character: "{".to_string(),
10947 more_trigger_character: None,
10948 }),
10949 ..Default::default()
10950 },
10951 ..Default::default()
10952 },
10953 );
10954
10955 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10956
10957 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10958
10959 let worktree_id = workspace
10960 .update(cx, |workspace, _, cx| {
10961 workspace.project().update(cx, |project, cx| {
10962 project.worktrees(cx).next().unwrap().read(cx).id()
10963 })
10964 })
10965 .unwrap();
10966
10967 let buffer = project
10968 .update(cx, |project, cx| {
10969 project.open_local_buffer("/a/main.rs", cx)
10970 })
10971 .await
10972 .unwrap();
10973 let editor_handle = workspace
10974 .update(cx, |workspace, window, cx| {
10975 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
10976 })
10977 .unwrap()
10978 .await
10979 .unwrap()
10980 .downcast::<Editor>()
10981 .unwrap();
10982
10983 cx.executor().start_waiting();
10984 let fake_server = fake_servers.next().await.unwrap();
10985
10986 fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
10987 assert_eq!(
10988 params.text_document_position.text_document.uri,
10989 lsp::Url::from_file_path("/a/main.rs").unwrap(),
10990 );
10991 assert_eq!(
10992 params.text_document_position.position,
10993 lsp::Position::new(0, 21),
10994 );
10995
10996 Ok(Some(vec![lsp::TextEdit {
10997 new_text: "]".to_string(),
10998 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
10999 }]))
11000 });
11001
11002 editor_handle.update_in(cx, |editor, window, cx| {
11003 window.focus(&editor.focus_handle(cx));
11004 editor.change_selections(None, window, cx, |s| {
11005 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
11006 });
11007 editor.handle_input("{", window, cx);
11008 });
11009
11010 cx.executor().run_until_parked();
11011
11012 buffer.update(cx, |buffer, _| {
11013 assert_eq!(
11014 buffer.text(),
11015 "fn main() { let a = {5}; }",
11016 "No extra braces from on type formatting should appear in the buffer"
11017 )
11018 });
11019}
11020
11021#[gpui::test]
11022async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) {
11023 init_test(cx, |_| {});
11024
11025 let fs = FakeFs::new(cx.executor());
11026 fs.insert_tree(
11027 "/a",
11028 json!({
11029 "main.rs": "fn main() { let a = 5; }",
11030 "other.rs": "// Test file",
11031 }),
11032 )
11033 .await;
11034
11035 let project = Project::test(fs, ["/a".as_ref()], cx).await;
11036
11037 let server_restarts = Arc::new(AtomicUsize::new(0));
11038 let closure_restarts = Arc::clone(&server_restarts);
11039 let language_server_name = "test language server";
11040 let language_name: LanguageName = "Rust".into();
11041
11042 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11043 language_registry.add(Arc::new(Language::new(
11044 LanguageConfig {
11045 name: language_name.clone(),
11046 matcher: LanguageMatcher {
11047 path_suffixes: vec!["rs".to_string()],
11048 ..Default::default()
11049 },
11050 ..Default::default()
11051 },
11052 Some(tree_sitter_rust::LANGUAGE.into()),
11053 )));
11054 let mut fake_servers = language_registry.register_fake_lsp(
11055 "Rust",
11056 FakeLspAdapter {
11057 name: language_server_name,
11058 initialization_options: Some(json!({
11059 "testOptionValue": true
11060 })),
11061 initializer: Some(Box::new(move |fake_server| {
11062 let task_restarts = Arc::clone(&closure_restarts);
11063 fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
11064 task_restarts.fetch_add(1, atomic::Ordering::Release);
11065 futures::future::ready(Ok(()))
11066 });
11067 })),
11068 ..Default::default()
11069 },
11070 );
11071
11072 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11073 let _buffer = project
11074 .update(cx, |project, cx| {
11075 project.open_local_buffer_with_lsp("/a/main.rs", cx)
11076 })
11077 .await
11078 .unwrap();
11079 let _fake_server = fake_servers.next().await.unwrap();
11080 update_test_language_settings(cx, |language_settings| {
11081 language_settings.languages.insert(
11082 language_name.clone(),
11083 LanguageSettingsContent {
11084 tab_size: NonZeroU32::new(8),
11085 ..Default::default()
11086 },
11087 );
11088 });
11089 cx.executor().run_until_parked();
11090 assert_eq!(
11091 server_restarts.load(atomic::Ordering::Acquire),
11092 0,
11093 "Should not restart LSP server on an unrelated change"
11094 );
11095
11096 update_test_project_settings(cx, |project_settings| {
11097 project_settings.lsp.insert(
11098 "Some other server name".into(),
11099 LspSettings {
11100 binary: None,
11101 settings: None,
11102 initialization_options: Some(json!({
11103 "some other init value": false
11104 })),
11105 },
11106 );
11107 });
11108 cx.executor().run_until_parked();
11109 assert_eq!(
11110 server_restarts.load(atomic::Ordering::Acquire),
11111 0,
11112 "Should not restart LSP server on an unrelated LSP settings change"
11113 );
11114 update_test_project_settings(cx, |project_settings| {
11115 project_settings.lsp.insert(
11116 language_server_name.into(),
11117 LspSettings {
11118 binary: None,
11119 settings: None,
11120 initialization_options: Some(json!({
11121 "anotherInitValue": false
11122 })),
11123 },
11124 );
11125 });
11126 cx.executor().run_until_parked();
11127 assert_eq!(
11128 server_restarts.load(atomic::Ordering::Acquire),
11129 1,
11130 "Should restart LSP server on a related LSP settings change"
11131 );
11132
11133 update_test_project_settings(cx, |project_settings| {
11134 project_settings.lsp.insert(
11135 language_server_name.into(),
11136 LspSettings {
11137 binary: None,
11138 settings: None,
11139 initialization_options: Some(json!({
11140 "anotherInitValue": false
11141 })),
11142 },
11143 );
11144 });
11145 cx.executor().run_until_parked();
11146 assert_eq!(
11147 server_restarts.load(atomic::Ordering::Acquire),
11148 1,
11149 "Should not restart LSP server on a related LSP settings change that is the same"
11150 );
11151
11152 update_test_project_settings(cx, |project_settings| {
11153 project_settings.lsp.insert(
11154 language_server_name.into(),
11155 LspSettings {
11156 binary: None,
11157 settings: None,
11158 initialization_options: None,
11159 },
11160 );
11161 });
11162 cx.executor().run_until_parked();
11163 assert_eq!(
11164 server_restarts.load(atomic::Ordering::Acquire),
11165 2,
11166 "Should restart LSP server on another related LSP settings change"
11167 );
11168}
11169
11170#[gpui::test]
11171async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
11172 init_test(cx, |_| {});
11173
11174 let mut cx = EditorLspTestContext::new_rust(
11175 lsp::ServerCapabilities {
11176 completion_provider: Some(lsp::CompletionOptions {
11177 trigger_characters: Some(vec![".".to_string()]),
11178 resolve_provider: Some(true),
11179 ..Default::default()
11180 }),
11181 ..Default::default()
11182 },
11183 cx,
11184 )
11185 .await;
11186
11187 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11188 cx.simulate_keystroke(".");
11189 let completion_item = lsp::CompletionItem {
11190 label: "some".into(),
11191 kind: Some(lsp::CompletionItemKind::SNIPPET),
11192 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11193 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11194 kind: lsp::MarkupKind::Markdown,
11195 value: "```rust\nSome(2)\n```".to_string(),
11196 })),
11197 deprecated: Some(false),
11198 sort_text: Some("fffffff2".to_string()),
11199 filter_text: Some("some".to_string()),
11200 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11201 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11202 range: lsp::Range {
11203 start: lsp::Position {
11204 line: 0,
11205 character: 22,
11206 },
11207 end: lsp::Position {
11208 line: 0,
11209 character: 22,
11210 },
11211 },
11212 new_text: "Some(2)".to_string(),
11213 })),
11214 additional_text_edits: Some(vec![lsp::TextEdit {
11215 range: lsp::Range {
11216 start: lsp::Position {
11217 line: 0,
11218 character: 20,
11219 },
11220 end: lsp::Position {
11221 line: 0,
11222 character: 22,
11223 },
11224 },
11225 new_text: "".to_string(),
11226 }]),
11227 ..Default::default()
11228 };
11229
11230 let closure_completion_item = completion_item.clone();
11231 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11232 let task_completion_item = closure_completion_item.clone();
11233 async move {
11234 Ok(Some(lsp::CompletionResponse::Array(vec![
11235 task_completion_item,
11236 ])))
11237 }
11238 });
11239
11240 request.next().await;
11241
11242 cx.condition(|editor, _| editor.context_menu_visible())
11243 .await;
11244 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11245 editor
11246 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11247 .unwrap()
11248 });
11249 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
11250
11251 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
11252 let task_completion_item = completion_item.clone();
11253 async move { Ok(task_completion_item) }
11254 })
11255 .next()
11256 .await
11257 .unwrap();
11258 apply_additional_edits.await.unwrap();
11259 cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
11260}
11261
11262#[gpui::test]
11263async fn test_completions_resolve_updates_labels_if_filter_text_matches(
11264 cx: &mut gpui::TestAppContext,
11265) {
11266 init_test(cx, |_| {});
11267
11268 let mut cx = EditorLspTestContext::new_rust(
11269 lsp::ServerCapabilities {
11270 completion_provider: Some(lsp::CompletionOptions {
11271 trigger_characters: Some(vec![".".to_string()]),
11272 resolve_provider: Some(true),
11273 ..Default::default()
11274 }),
11275 ..Default::default()
11276 },
11277 cx,
11278 )
11279 .await;
11280
11281 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11282 cx.simulate_keystroke(".");
11283
11284 let item1 = lsp::CompletionItem {
11285 label: "method id()".to_string(),
11286 filter_text: Some("id".to_string()),
11287 detail: None,
11288 documentation: None,
11289 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11290 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11291 new_text: ".id".to_string(),
11292 })),
11293 ..lsp::CompletionItem::default()
11294 };
11295
11296 let item2 = lsp::CompletionItem {
11297 label: "other".to_string(),
11298 filter_text: Some("other".to_string()),
11299 detail: None,
11300 documentation: None,
11301 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11302 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11303 new_text: ".other".to_string(),
11304 })),
11305 ..lsp::CompletionItem::default()
11306 };
11307
11308 let item1 = item1.clone();
11309 cx.handle_request::<lsp::request::Completion, _, _>({
11310 let item1 = item1.clone();
11311 move |_, _, _| {
11312 let item1 = item1.clone();
11313 let item2 = item2.clone();
11314 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
11315 }
11316 })
11317 .next()
11318 .await;
11319
11320 cx.condition(|editor, _| editor.context_menu_visible())
11321 .await;
11322 cx.update_editor(|editor, _, _| {
11323 let context_menu = editor.context_menu.borrow_mut();
11324 let context_menu = context_menu
11325 .as_ref()
11326 .expect("Should have the context menu deployed");
11327 match context_menu {
11328 CodeContextMenu::Completions(completions_menu) => {
11329 let completions = completions_menu.completions.borrow_mut();
11330 assert_eq!(
11331 completions
11332 .iter()
11333 .map(|completion| &completion.label.text)
11334 .collect::<Vec<_>>(),
11335 vec!["method id()", "other"]
11336 )
11337 }
11338 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11339 }
11340 });
11341
11342 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>({
11343 let item1 = item1.clone();
11344 move |_, item_to_resolve, _| {
11345 let item1 = item1.clone();
11346 async move {
11347 if item1 == item_to_resolve {
11348 Ok(lsp::CompletionItem {
11349 label: "method id()".to_string(),
11350 filter_text: Some("id".to_string()),
11351 detail: Some("Now resolved!".to_string()),
11352 documentation: Some(lsp::Documentation::String("Docs".to_string())),
11353 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11354 range: lsp::Range::new(
11355 lsp::Position::new(0, 22),
11356 lsp::Position::new(0, 22),
11357 ),
11358 new_text: ".id".to_string(),
11359 })),
11360 ..lsp::CompletionItem::default()
11361 })
11362 } else {
11363 Ok(item_to_resolve)
11364 }
11365 }
11366 }
11367 })
11368 .next()
11369 .await
11370 .unwrap();
11371 cx.run_until_parked();
11372
11373 cx.update_editor(|editor, window, cx| {
11374 editor.context_menu_next(&Default::default(), window, cx);
11375 });
11376
11377 cx.update_editor(|editor, _, _| {
11378 let context_menu = editor.context_menu.borrow_mut();
11379 let context_menu = context_menu
11380 .as_ref()
11381 .expect("Should have the context menu deployed");
11382 match context_menu {
11383 CodeContextMenu::Completions(completions_menu) => {
11384 let completions = completions_menu.completions.borrow_mut();
11385 assert_eq!(
11386 completions
11387 .iter()
11388 .map(|completion| &completion.label.text)
11389 .collect::<Vec<_>>(),
11390 vec!["method id() Now resolved!", "other"],
11391 "Should update first completion label, but not second as the filter text did not match."
11392 );
11393 }
11394 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11395 }
11396 });
11397}
11398
11399#[gpui::test]
11400async fn test_completions_resolve_happens_once(cx: &mut gpui::TestAppContext) {
11401 init_test(cx, |_| {});
11402
11403 let mut cx = EditorLspTestContext::new_rust(
11404 lsp::ServerCapabilities {
11405 completion_provider: Some(lsp::CompletionOptions {
11406 trigger_characters: Some(vec![".".to_string()]),
11407 resolve_provider: Some(true),
11408 ..Default::default()
11409 }),
11410 ..Default::default()
11411 },
11412 cx,
11413 )
11414 .await;
11415
11416 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11417 cx.simulate_keystroke(".");
11418
11419 let unresolved_item_1 = lsp::CompletionItem {
11420 label: "id".to_string(),
11421 filter_text: Some("id".to_string()),
11422 detail: None,
11423 documentation: None,
11424 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11425 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11426 new_text: ".id".to_string(),
11427 })),
11428 ..lsp::CompletionItem::default()
11429 };
11430 let resolved_item_1 = lsp::CompletionItem {
11431 additional_text_edits: Some(vec![lsp::TextEdit {
11432 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
11433 new_text: "!!".to_string(),
11434 }]),
11435 ..unresolved_item_1.clone()
11436 };
11437 let unresolved_item_2 = lsp::CompletionItem {
11438 label: "other".to_string(),
11439 filter_text: Some("other".to_string()),
11440 detail: None,
11441 documentation: None,
11442 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11443 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11444 new_text: ".other".to_string(),
11445 })),
11446 ..lsp::CompletionItem::default()
11447 };
11448 let resolved_item_2 = lsp::CompletionItem {
11449 additional_text_edits: Some(vec![lsp::TextEdit {
11450 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
11451 new_text: "??".to_string(),
11452 }]),
11453 ..unresolved_item_2.clone()
11454 };
11455
11456 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
11457 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
11458 cx.lsp
11459 .server
11460 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
11461 let unresolved_item_1 = unresolved_item_1.clone();
11462 let resolved_item_1 = resolved_item_1.clone();
11463 let unresolved_item_2 = unresolved_item_2.clone();
11464 let resolved_item_2 = resolved_item_2.clone();
11465 let resolve_requests_1 = resolve_requests_1.clone();
11466 let resolve_requests_2 = resolve_requests_2.clone();
11467 move |unresolved_request, _| {
11468 let unresolved_item_1 = unresolved_item_1.clone();
11469 let resolved_item_1 = resolved_item_1.clone();
11470 let unresolved_item_2 = unresolved_item_2.clone();
11471 let resolved_item_2 = resolved_item_2.clone();
11472 let resolve_requests_1 = resolve_requests_1.clone();
11473 let resolve_requests_2 = resolve_requests_2.clone();
11474 async move {
11475 if unresolved_request == unresolved_item_1 {
11476 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
11477 Ok(resolved_item_1.clone())
11478 } else if unresolved_request == unresolved_item_2 {
11479 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
11480 Ok(resolved_item_2.clone())
11481 } else {
11482 panic!("Unexpected completion item {unresolved_request:?}")
11483 }
11484 }
11485 }
11486 })
11487 .detach();
11488
11489 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11490 let unresolved_item_1 = unresolved_item_1.clone();
11491 let unresolved_item_2 = unresolved_item_2.clone();
11492 async move {
11493 Ok(Some(lsp::CompletionResponse::Array(vec![
11494 unresolved_item_1,
11495 unresolved_item_2,
11496 ])))
11497 }
11498 })
11499 .next()
11500 .await;
11501
11502 cx.condition(|editor, _| editor.context_menu_visible())
11503 .await;
11504 cx.update_editor(|editor, _, _| {
11505 let context_menu = editor.context_menu.borrow_mut();
11506 let context_menu = context_menu
11507 .as_ref()
11508 .expect("Should have the context menu deployed");
11509 match context_menu {
11510 CodeContextMenu::Completions(completions_menu) => {
11511 let completions = completions_menu.completions.borrow_mut();
11512 assert_eq!(
11513 completions
11514 .iter()
11515 .map(|completion| &completion.label.text)
11516 .collect::<Vec<_>>(),
11517 vec!["id", "other"]
11518 )
11519 }
11520 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11521 }
11522 });
11523 cx.run_until_parked();
11524
11525 cx.update_editor(|editor, window, cx| {
11526 editor.context_menu_next(&ContextMenuNext, window, cx);
11527 });
11528 cx.run_until_parked();
11529 cx.update_editor(|editor, window, cx| {
11530 editor.context_menu_prev(&ContextMenuPrev, window, cx);
11531 });
11532 cx.run_until_parked();
11533 cx.update_editor(|editor, window, cx| {
11534 editor.context_menu_next(&ContextMenuNext, window, cx);
11535 });
11536 cx.run_until_parked();
11537 cx.update_editor(|editor, window, cx| {
11538 editor
11539 .compose_completion(&ComposeCompletion::default(), window, cx)
11540 .expect("No task returned")
11541 })
11542 .await
11543 .expect("Completion failed");
11544 cx.run_until_parked();
11545
11546 cx.update_editor(|editor, _, cx| {
11547 assert_eq!(
11548 resolve_requests_1.load(atomic::Ordering::Acquire),
11549 1,
11550 "Should always resolve once despite multiple selections"
11551 );
11552 assert_eq!(
11553 resolve_requests_2.load(atomic::Ordering::Acquire),
11554 1,
11555 "Should always resolve once after multiple selections and applying the completion"
11556 );
11557 assert_eq!(
11558 editor.text(cx),
11559 "fn main() { let a = ??.other; }",
11560 "Should use resolved data when applying the completion"
11561 );
11562 });
11563}
11564
11565#[gpui::test]
11566async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
11567 init_test(cx, |_| {});
11568
11569 let item_0 = lsp::CompletionItem {
11570 label: "abs".into(),
11571 insert_text: Some("abs".into()),
11572 data: Some(json!({ "very": "special"})),
11573 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
11574 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11575 lsp::InsertReplaceEdit {
11576 new_text: "abs".to_string(),
11577 insert: lsp::Range::default(),
11578 replace: lsp::Range::default(),
11579 },
11580 )),
11581 ..lsp::CompletionItem::default()
11582 };
11583 let items = iter::once(item_0.clone())
11584 .chain((11..51).map(|i| lsp::CompletionItem {
11585 label: format!("item_{}", i),
11586 insert_text: Some(format!("item_{}", i)),
11587 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
11588 ..lsp::CompletionItem::default()
11589 }))
11590 .collect::<Vec<_>>();
11591
11592 let default_commit_characters = vec!["?".to_string()];
11593 let default_data = json!({ "default": "data"});
11594 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
11595 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
11596 let default_edit_range = lsp::Range {
11597 start: lsp::Position {
11598 line: 0,
11599 character: 5,
11600 },
11601 end: lsp::Position {
11602 line: 0,
11603 character: 5,
11604 },
11605 };
11606
11607 let item_0_out = lsp::CompletionItem {
11608 commit_characters: Some(default_commit_characters.clone()),
11609 insert_text_format: Some(default_insert_text_format),
11610 ..item_0
11611 };
11612 let items_out = iter::once(item_0_out)
11613 .chain(items[1..].iter().map(|item| lsp::CompletionItem {
11614 commit_characters: Some(default_commit_characters.clone()),
11615 data: Some(default_data.clone()),
11616 insert_text_mode: Some(default_insert_text_mode),
11617 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11618 range: default_edit_range,
11619 new_text: item.label.clone(),
11620 })),
11621 ..item.clone()
11622 }))
11623 .collect::<Vec<lsp::CompletionItem>>();
11624
11625 let mut cx = EditorLspTestContext::new_rust(
11626 lsp::ServerCapabilities {
11627 completion_provider: Some(lsp::CompletionOptions {
11628 trigger_characters: Some(vec![".".to_string()]),
11629 resolve_provider: Some(true),
11630 ..Default::default()
11631 }),
11632 ..Default::default()
11633 },
11634 cx,
11635 )
11636 .await;
11637
11638 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11639 cx.simulate_keystroke(".");
11640
11641 let completion_data = default_data.clone();
11642 let completion_characters = default_commit_characters.clone();
11643 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11644 let default_data = completion_data.clone();
11645 let default_commit_characters = completion_characters.clone();
11646 let items = items.clone();
11647 async move {
11648 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
11649 items,
11650 item_defaults: Some(lsp::CompletionListItemDefaults {
11651 data: Some(default_data.clone()),
11652 commit_characters: Some(default_commit_characters.clone()),
11653 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
11654 default_edit_range,
11655 )),
11656 insert_text_format: Some(default_insert_text_format),
11657 insert_text_mode: Some(default_insert_text_mode),
11658 }),
11659 ..lsp::CompletionList::default()
11660 })))
11661 }
11662 })
11663 .next()
11664 .await;
11665
11666 let resolved_items = Arc::new(Mutex::new(Vec::new()));
11667 cx.lsp
11668 .server
11669 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
11670 let closure_resolved_items = resolved_items.clone();
11671 move |item_to_resolve, _| {
11672 let closure_resolved_items = closure_resolved_items.clone();
11673 async move {
11674 closure_resolved_items.lock().push(item_to_resolve.clone());
11675 Ok(item_to_resolve)
11676 }
11677 }
11678 })
11679 .detach();
11680
11681 cx.condition(|editor, _| editor.context_menu_visible())
11682 .await;
11683 cx.run_until_parked();
11684 cx.update_editor(|editor, _, _| {
11685 let menu = editor.context_menu.borrow_mut();
11686 match menu.as_ref().expect("should have the completions menu") {
11687 CodeContextMenu::Completions(completions_menu) => {
11688 assert_eq!(
11689 completions_menu
11690 .entries
11691 .borrow()
11692 .iter()
11693 .flat_map(|c| match c {
11694 CompletionEntry::Match(mat) => Some(mat.string.clone()),
11695 _ => None,
11696 })
11697 .collect::<Vec<String>>(),
11698 items_out
11699 .iter()
11700 .map(|completion| completion.label.clone())
11701 .collect::<Vec<String>>()
11702 );
11703 }
11704 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
11705 }
11706 });
11707 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
11708 // with 4 from the end.
11709 assert_eq!(
11710 *resolved_items.lock(),
11711 [
11712 &items_out[0..16],
11713 &items_out[items_out.len() - 4..items_out.len()]
11714 ]
11715 .concat()
11716 .iter()
11717 .cloned()
11718 .collect::<Vec<lsp::CompletionItem>>()
11719 );
11720 resolved_items.lock().clear();
11721
11722 cx.update_editor(|editor, window, cx| {
11723 editor.context_menu_prev(&ContextMenuPrev, window, cx);
11724 });
11725 cx.run_until_parked();
11726 // Completions that have already been resolved are skipped.
11727 assert_eq!(
11728 *resolved_items.lock(),
11729 items_out[items_out.len() - 16..items_out.len() - 4]
11730 .iter()
11731 .cloned()
11732 .collect::<Vec<lsp::CompletionItem>>()
11733 );
11734 resolved_items.lock().clear();
11735}
11736
11737#[gpui::test]
11738async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
11739 init_test(cx, |_| {});
11740
11741 let mut cx = EditorLspTestContext::new(
11742 Language::new(
11743 LanguageConfig {
11744 matcher: LanguageMatcher {
11745 path_suffixes: vec!["jsx".into()],
11746 ..Default::default()
11747 },
11748 overrides: [(
11749 "element".into(),
11750 LanguageConfigOverride {
11751 word_characters: Override::Set(['-'].into_iter().collect()),
11752 ..Default::default()
11753 },
11754 )]
11755 .into_iter()
11756 .collect(),
11757 ..Default::default()
11758 },
11759 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
11760 )
11761 .with_override_query("(jsx_self_closing_element) @element")
11762 .unwrap(),
11763 lsp::ServerCapabilities {
11764 completion_provider: Some(lsp::CompletionOptions {
11765 trigger_characters: Some(vec![":".to_string()]),
11766 ..Default::default()
11767 }),
11768 ..Default::default()
11769 },
11770 cx,
11771 )
11772 .await;
11773
11774 cx.lsp
11775 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11776 Ok(Some(lsp::CompletionResponse::Array(vec![
11777 lsp::CompletionItem {
11778 label: "bg-blue".into(),
11779 ..Default::default()
11780 },
11781 lsp::CompletionItem {
11782 label: "bg-red".into(),
11783 ..Default::default()
11784 },
11785 lsp::CompletionItem {
11786 label: "bg-yellow".into(),
11787 ..Default::default()
11788 },
11789 ])))
11790 });
11791
11792 cx.set_state(r#"<p class="bgˇ" />"#);
11793
11794 // Trigger completion when typing a dash, because the dash is an extra
11795 // word character in the 'element' scope, which contains the cursor.
11796 cx.simulate_keystroke("-");
11797 cx.executor().run_until_parked();
11798 cx.update_editor(|editor, _, _| {
11799 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11800 {
11801 assert_eq!(
11802 completion_menu_entries(&menu),
11803 &["bg-red", "bg-blue", "bg-yellow"]
11804 );
11805 } else {
11806 panic!("expected completion menu to be open");
11807 }
11808 });
11809
11810 cx.simulate_keystroke("l");
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!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
11816 } else {
11817 panic!("expected completion menu to be open");
11818 }
11819 });
11820
11821 // When filtering completions, consider the character after the '-' to
11822 // be the start of a subword.
11823 cx.set_state(r#"<p class="yelˇ" />"#);
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-yellow"]);
11830 } else {
11831 panic!("expected completion menu to be open");
11832 }
11833 });
11834}
11835
11836fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
11837 let entries = menu.entries.borrow();
11838 entries
11839 .iter()
11840 .flat_map(|e| match e {
11841 CompletionEntry::Match(mat) => Some(mat.string.clone()),
11842 _ => None,
11843 })
11844 .collect()
11845}
11846
11847#[gpui::test]
11848async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
11849 init_test(cx, |settings| {
11850 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
11851 FormatterList(vec![Formatter::Prettier].into()),
11852 ))
11853 });
11854
11855 let fs = FakeFs::new(cx.executor());
11856 fs.insert_file("/file.ts", Default::default()).await;
11857
11858 let project = Project::test(fs, ["/file.ts".as_ref()], cx).await;
11859 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11860
11861 language_registry.add(Arc::new(Language::new(
11862 LanguageConfig {
11863 name: "TypeScript".into(),
11864 matcher: LanguageMatcher {
11865 path_suffixes: vec!["ts".to_string()],
11866 ..Default::default()
11867 },
11868 ..Default::default()
11869 },
11870 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11871 )));
11872 update_test_language_settings(cx, |settings| {
11873 settings.defaults.prettier = Some(PrettierSettings {
11874 allowed: true,
11875 ..PrettierSettings::default()
11876 });
11877 });
11878
11879 let test_plugin = "test_plugin";
11880 let _ = language_registry.register_fake_lsp(
11881 "TypeScript",
11882 FakeLspAdapter {
11883 prettier_plugins: vec![test_plugin],
11884 ..Default::default()
11885 },
11886 );
11887
11888 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
11889 let buffer = project
11890 .update(cx, |project, cx| project.open_local_buffer("/file.ts", cx))
11891 .await
11892 .unwrap();
11893
11894 let buffer_text = "one\ntwo\nthree\n";
11895 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11896 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11897 editor.update_in(cx, |editor, window, cx| {
11898 editor.set_text(buffer_text, window, cx)
11899 });
11900
11901 editor
11902 .update_in(cx, |editor, window, cx| {
11903 editor.perform_format(
11904 project.clone(),
11905 FormatTrigger::Manual,
11906 FormatTarget::Buffers,
11907 window,
11908 cx,
11909 )
11910 })
11911 .unwrap()
11912 .await;
11913 assert_eq!(
11914 editor.update(cx, |editor, cx| editor.text(cx)),
11915 buffer_text.to_string() + prettier_format_suffix,
11916 "Test prettier formatting was not applied to the original buffer text",
11917 );
11918
11919 update_test_language_settings(cx, |settings| {
11920 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
11921 });
11922 let format = editor.update_in(cx, |editor, window, cx| {
11923 editor.perform_format(
11924 project.clone(),
11925 FormatTrigger::Manual,
11926 FormatTarget::Buffers,
11927 window,
11928 cx,
11929 )
11930 });
11931 format.await.unwrap();
11932 assert_eq!(
11933 editor.update(cx, |editor, cx| editor.text(cx)),
11934 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
11935 "Autoformatting (via test prettier) was not applied to the original buffer text",
11936 );
11937}
11938
11939#[gpui::test]
11940async fn test_addition_reverts(cx: &mut gpui::TestAppContext) {
11941 init_test(cx, |_| {});
11942 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11943 let base_text = indoc! {r#"
11944 struct Row;
11945 struct Row1;
11946 struct Row2;
11947
11948 struct Row4;
11949 struct Row5;
11950 struct Row6;
11951
11952 struct Row8;
11953 struct Row9;
11954 struct Row10;"#};
11955
11956 // When addition hunks are not adjacent to carets, no hunk revert is performed
11957 assert_hunk_revert(
11958 indoc! {r#"struct Row;
11959 struct Row1;
11960 struct Row1.1;
11961 struct Row1.2;
11962 struct Row2;ˇ
11963
11964 struct Row4;
11965 struct Row5;
11966 struct Row6;
11967
11968 struct Row8;
11969 ˇstruct Row9;
11970 struct Row9.1;
11971 struct Row9.2;
11972 struct Row9.3;
11973 struct Row10;"#},
11974 vec![DiffHunkStatus::Added, DiffHunkStatus::Added],
11975 indoc! {r#"struct Row;
11976 struct Row1;
11977 struct Row1.1;
11978 struct Row1.2;
11979 struct Row2;ˇ
11980
11981 struct Row4;
11982 struct Row5;
11983 struct Row6;
11984
11985 struct Row8;
11986 ˇstruct Row9;
11987 struct Row9.1;
11988 struct Row9.2;
11989 struct Row9.3;
11990 struct Row10;"#},
11991 base_text,
11992 &mut cx,
11993 );
11994 // Same for selections
11995 assert_hunk_revert(
11996 indoc! {r#"struct Row;
11997 struct Row1;
11998 struct Row2;
11999 struct Row2.1;
12000 struct Row2.2;
12001 «ˇ
12002 struct Row4;
12003 struct» Row5;
12004 «struct Row6;
12005 ˇ»
12006 struct Row9.1;
12007 struct Row9.2;
12008 struct Row9.3;
12009 struct Row8;
12010 struct Row9;
12011 struct Row10;"#},
12012 vec![DiffHunkStatus::Added, DiffHunkStatus::Added],
12013 indoc! {r#"struct Row;
12014 struct Row1;
12015 struct Row2;
12016 struct Row2.1;
12017 struct Row2.2;
12018 «ˇ
12019 struct Row4;
12020 struct» Row5;
12021 «struct Row6;
12022 ˇ»
12023 struct Row9.1;
12024 struct Row9.2;
12025 struct Row9.3;
12026 struct Row8;
12027 struct Row9;
12028 struct Row10;"#},
12029 base_text,
12030 &mut cx,
12031 );
12032
12033 // When carets and selections intersect the addition hunks, those are reverted.
12034 // Adjacent carets got merged.
12035 assert_hunk_revert(
12036 indoc! {r#"struct Row;
12037 ˇ// something on the top
12038 struct Row1;
12039 struct Row2;
12040 struct Roˇw3.1;
12041 struct Row2.2;
12042 struct Row2.3;ˇ
12043
12044 struct Row4;
12045 struct ˇRow5.1;
12046 struct Row5.2;
12047 struct «Rowˇ»5.3;
12048 struct Row5;
12049 struct Row6;
12050 ˇ
12051 struct Row9.1;
12052 struct «Rowˇ»9.2;
12053 struct «ˇRow»9.3;
12054 struct Row8;
12055 struct Row9;
12056 «ˇ// something on bottom»
12057 struct Row10;"#},
12058 vec![
12059 DiffHunkStatus::Added,
12060 DiffHunkStatus::Added,
12061 DiffHunkStatus::Added,
12062 DiffHunkStatus::Added,
12063 DiffHunkStatus::Added,
12064 ],
12065 indoc! {r#"struct Row;
12066 ˇstruct Row1;
12067 struct Row2;
12068 ˇ
12069 struct Row4;
12070 ˇstruct Row5;
12071 struct Row6;
12072 ˇ
12073 ˇstruct Row8;
12074 struct Row9;
12075 ˇstruct Row10;"#},
12076 base_text,
12077 &mut cx,
12078 );
12079}
12080
12081#[gpui::test]
12082async fn test_modification_reverts(cx: &mut gpui::TestAppContext) {
12083 init_test(cx, |_| {});
12084 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12085 let base_text = indoc! {r#"
12086 struct Row;
12087 struct Row1;
12088 struct Row2;
12089
12090 struct Row4;
12091 struct Row5;
12092 struct Row6;
12093
12094 struct Row8;
12095 struct Row9;
12096 struct Row10;"#};
12097
12098 // Modification hunks behave the same as the addition ones.
12099 assert_hunk_revert(
12100 indoc! {r#"struct Row;
12101 struct Row1;
12102 struct Row33;
12103 ˇ
12104 struct Row4;
12105 struct Row5;
12106 struct Row6;
12107 ˇ
12108 struct Row99;
12109 struct Row9;
12110 struct Row10;"#},
12111 vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified],
12112 indoc! {r#"struct Row;
12113 struct Row1;
12114 struct Row33;
12115 ˇ
12116 struct Row4;
12117 struct Row5;
12118 struct Row6;
12119 ˇ
12120 struct Row99;
12121 struct Row9;
12122 struct Row10;"#},
12123 base_text,
12124 &mut cx,
12125 );
12126 assert_hunk_revert(
12127 indoc! {r#"struct Row;
12128 struct Row1;
12129 struct Row33;
12130 «ˇ
12131 struct Row4;
12132 struct» Row5;
12133 «struct Row6;
12134 ˇ»
12135 struct Row99;
12136 struct Row9;
12137 struct Row10;"#},
12138 vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified],
12139 indoc! {r#"struct Row;
12140 struct Row1;
12141 struct Row33;
12142 «ˇ
12143 struct Row4;
12144 struct» Row5;
12145 «struct Row6;
12146 ˇ»
12147 struct Row99;
12148 struct Row9;
12149 struct Row10;"#},
12150 base_text,
12151 &mut cx,
12152 );
12153
12154 assert_hunk_revert(
12155 indoc! {r#"ˇstruct Row1.1;
12156 struct Row1;
12157 «ˇstr»uct Row22;
12158
12159 struct ˇRow44;
12160 struct Row5;
12161 struct «Rˇ»ow66;ˇ
12162
12163 «struˇ»ct Row88;
12164 struct Row9;
12165 struct Row1011;ˇ"#},
12166 vec![
12167 DiffHunkStatus::Modified,
12168 DiffHunkStatus::Modified,
12169 DiffHunkStatus::Modified,
12170 DiffHunkStatus::Modified,
12171 DiffHunkStatus::Modified,
12172 DiffHunkStatus::Modified,
12173 ],
12174 indoc! {r#"struct Row;
12175 ˇstruct Row1;
12176 struct Row2;
12177 ˇ
12178 struct Row4;
12179 ˇstruct Row5;
12180 struct Row6;
12181 ˇ
12182 struct Row8;
12183 ˇstruct Row9;
12184 struct Row10;ˇ"#},
12185 base_text,
12186 &mut cx,
12187 );
12188}
12189
12190#[gpui::test]
12191async fn test_deleting_over_diff_hunk(cx: &mut gpui::TestAppContext) {
12192 init_test(cx, |_| {});
12193 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12194 let base_text = indoc! {r#"
12195 one
12196
12197 two
12198 three
12199 "#};
12200
12201 cx.set_diff_base(base_text);
12202 cx.set_state("\nˇ\n");
12203 cx.executor().run_until_parked();
12204 cx.update_editor(|editor, _window, cx| {
12205 editor.expand_selected_diff_hunks(cx);
12206 });
12207 cx.executor().run_until_parked();
12208 cx.update_editor(|editor, window, cx| {
12209 editor.backspace(&Default::default(), window, cx);
12210 });
12211 cx.run_until_parked();
12212 cx.assert_state_with_diff(
12213 indoc! {r#"
12214
12215 - two
12216 - threeˇ
12217 +
12218 "#}
12219 .to_string(),
12220 );
12221}
12222
12223#[gpui::test]
12224async fn test_deletion_reverts(cx: &mut gpui::TestAppContext) {
12225 init_test(cx, |_| {});
12226 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12227 let base_text = indoc! {r#"struct Row;
12228struct Row1;
12229struct Row2;
12230
12231struct Row4;
12232struct Row5;
12233struct Row6;
12234
12235struct Row8;
12236struct Row9;
12237struct Row10;"#};
12238
12239 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
12240 assert_hunk_revert(
12241 indoc! {r#"struct Row;
12242 struct Row2;
12243
12244 ˇstruct Row4;
12245 struct Row5;
12246 struct Row6;
12247 ˇ
12248 struct Row8;
12249 struct Row10;"#},
12250 vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
12251 indoc! {r#"struct Row;
12252 struct Row2;
12253
12254 ˇstruct Row4;
12255 struct Row5;
12256 struct Row6;
12257 ˇ
12258 struct Row8;
12259 struct Row10;"#},
12260 base_text,
12261 &mut cx,
12262 );
12263 assert_hunk_revert(
12264 indoc! {r#"struct Row;
12265 struct Row2;
12266
12267 «ˇstruct Row4;
12268 struct» Row5;
12269 «struct Row6;
12270 ˇ»
12271 struct Row8;
12272 struct Row10;"#},
12273 vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
12274 indoc! {r#"struct Row;
12275 struct Row2;
12276
12277 «ˇstruct Row4;
12278 struct» Row5;
12279 «struct Row6;
12280 ˇ»
12281 struct Row8;
12282 struct Row10;"#},
12283 base_text,
12284 &mut cx,
12285 );
12286
12287 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
12288 assert_hunk_revert(
12289 indoc! {r#"struct Row;
12290 ˇstruct Row2;
12291
12292 struct Row4;
12293 struct Row5;
12294 struct Row6;
12295
12296 struct Row8;ˇ
12297 struct Row10;"#},
12298 vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
12299 indoc! {r#"struct Row;
12300 struct Row1;
12301 ˇstruct Row2;
12302
12303 struct Row4;
12304 struct Row5;
12305 struct Row6;
12306
12307 struct Row8;ˇ
12308 struct Row9;
12309 struct Row10;"#},
12310 base_text,
12311 &mut cx,
12312 );
12313 assert_hunk_revert(
12314 indoc! {r#"struct Row;
12315 struct Row2«ˇ;
12316 struct Row4;
12317 struct» Row5;
12318 «struct Row6;
12319
12320 struct Row8;ˇ»
12321 struct Row10;"#},
12322 vec![
12323 DiffHunkStatus::Removed,
12324 DiffHunkStatus::Removed,
12325 DiffHunkStatus::Removed,
12326 ],
12327 indoc! {r#"struct Row;
12328 struct Row1;
12329 struct Row2«ˇ;
12330
12331 struct Row4;
12332 struct» Row5;
12333 «struct Row6;
12334
12335 struct Row8;ˇ»
12336 struct Row9;
12337 struct Row10;"#},
12338 base_text,
12339 &mut cx,
12340 );
12341}
12342
12343#[gpui::test]
12344async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
12345 init_test(cx, |_| {});
12346
12347 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
12348 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
12349 let base_text_3 =
12350 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
12351
12352 let text_1 = edit_first_char_of_every_line(base_text_1);
12353 let text_2 = edit_first_char_of_every_line(base_text_2);
12354 let text_3 = edit_first_char_of_every_line(base_text_3);
12355
12356 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
12357 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
12358 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
12359
12360 let multibuffer = cx.new(|cx| {
12361 let mut multibuffer = MultiBuffer::new(ReadWrite);
12362 multibuffer.push_excerpts(
12363 buffer_1.clone(),
12364 [
12365 ExcerptRange {
12366 context: Point::new(0, 0)..Point::new(3, 0),
12367 primary: None,
12368 },
12369 ExcerptRange {
12370 context: Point::new(5, 0)..Point::new(7, 0),
12371 primary: None,
12372 },
12373 ExcerptRange {
12374 context: Point::new(9, 0)..Point::new(10, 4),
12375 primary: None,
12376 },
12377 ],
12378 cx,
12379 );
12380 multibuffer.push_excerpts(
12381 buffer_2.clone(),
12382 [
12383 ExcerptRange {
12384 context: Point::new(0, 0)..Point::new(3, 0),
12385 primary: None,
12386 },
12387 ExcerptRange {
12388 context: Point::new(5, 0)..Point::new(7, 0),
12389 primary: None,
12390 },
12391 ExcerptRange {
12392 context: Point::new(9, 0)..Point::new(10, 4),
12393 primary: None,
12394 },
12395 ],
12396 cx,
12397 );
12398 multibuffer.push_excerpts(
12399 buffer_3.clone(),
12400 [
12401 ExcerptRange {
12402 context: Point::new(0, 0)..Point::new(3, 0),
12403 primary: None,
12404 },
12405 ExcerptRange {
12406 context: Point::new(5, 0)..Point::new(7, 0),
12407 primary: None,
12408 },
12409 ExcerptRange {
12410 context: Point::new(9, 0)..Point::new(10, 4),
12411 primary: None,
12412 },
12413 ],
12414 cx,
12415 );
12416 multibuffer
12417 });
12418
12419 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12420 editor.update_in(cx, |editor, _window, cx| {
12421 for (buffer, diff_base) in [
12422 (buffer_1.clone(), base_text_1),
12423 (buffer_2.clone(), base_text_2),
12424 (buffer_3.clone(), base_text_3),
12425 ] {
12426 let change_set = cx
12427 .new(|cx| BufferChangeSet::new_with_base_text(diff_base.to_string(), &buffer, cx));
12428 editor
12429 .buffer
12430 .update(cx, |buffer, cx| buffer.add_change_set(change_set, cx));
12431 }
12432 });
12433 cx.executor().run_until_parked();
12434
12435 editor.update_in(cx, |editor, window, cx| {
12436 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}");
12437 editor.select_all(&SelectAll, window, cx);
12438 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
12439 });
12440 cx.executor().run_until_parked();
12441
12442 // When all ranges are selected, all buffer hunks are reverted.
12443 editor.update(cx, |editor, cx| {
12444 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");
12445 });
12446 buffer_1.update(cx, |buffer, _| {
12447 assert_eq!(buffer.text(), base_text_1);
12448 });
12449 buffer_2.update(cx, |buffer, _| {
12450 assert_eq!(buffer.text(), base_text_2);
12451 });
12452 buffer_3.update(cx, |buffer, _| {
12453 assert_eq!(buffer.text(), base_text_3);
12454 });
12455
12456 editor.update_in(cx, |editor, window, cx| {
12457 editor.undo(&Default::default(), window, cx);
12458 });
12459
12460 editor.update_in(cx, |editor, window, cx| {
12461 editor.change_selections(None, window, cx, |s| {
12462 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
12463 });
12464 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
12465 });
12466
12467 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
12468 // but not affect buffer_2 and its related excerpts.
12469 editor.update(cx, |editor, cx| {
12470 assert_eq!(
12471 editor.text(cx),
12472 "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}"
12473 );
12474 });
12475 buffer_1.update(cx, |buffer, _| {
12476 assert_eq!(buffer.text(), base_text_1);
12477 });
12478 buffer_2.update(cx, |buffer, _| {
12479 assert_eq!(
12480 buffer.text(),
12481 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
12482 );
12483 });
12484 buffer_3.update(cx, |buffer, _| {
12485 assert_eq!(
12486 buffer.text(),
12487 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
12488 );
12489 });
12490
12491 fn edit_first_char_of_every_line(text: &str) -> String {
12492 text.split('\n')
12493 .map(|line| format!("X{}", &line[1..]))
12494 .collect::<Vec<_>>()
12495 .join("\n")
12496 }
12497}
12498
12499#[gpui::test]
12500async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
12501 init_test(cx, |_| {});
12502
12503 let cols = 4;
12504 let rows = 10;
12505 let sample_text_1 = sample_text(rows, cols, 'a');
12506 assert_eq!(
12507 sample_text_1,
12508 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
12509 );
12510 let sample_text_2 = sample_text(rows, cols, 'l');
12511 assert_eq!(
12512 sample_text_2,
12513 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
12514 );
12515 let sample_text_3 = sample_text(rows, cols, 'v');
12516 assert_eq!(
12517 sample_text_3,
12518 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
12519 );
12520
12521 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
12522 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
12523 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
12524
12525 let multi_buffer = cx.new(|cx| {
12526 let mut multibuffer = MultiBuffer::new(ReadWrite);
12527 multibuffer.push_excerpts(
12528 buffer_1.clone(),
12529 [
12530 ExcerptRange {
12531 context: Point::new(0, 0)..Point::new(3, 0),
12532 primary: None,
12533 },
12534 ExcerptRange {
12535 context: Point::new(5, 0)..Point::new(7, 0),
12536 primary: None,
12537 },
12538 ExcerptRange {
12539 context: Point::new(9, 0)..Point::new(10, 4),
12540 primary: None,
12541 },
12542 ],
12543 cx,
12544 );
12545 multibuffer.push_excerpts(
12546 buffer_2.clone(),
12547 [
12548 ExcerptRange {
12549 context: Point::new(0, 0)..Point::new(3, 0),
12550 primary: None,
12551 },
12552 ExcerptRange {
12553 context: Point::new(5, 0)..Point::new(7, 0),
12554 primary: None,
12555 },
12556 ExcerptRange {
12557 context: Point::new(9, 0)..Point::new(10, 4),
12558 primary: None,
12559 },
12560 ],
12561 cx,
12562 );
12563 multibuffer.push_excerpts(
12564 buffer_3.clone(),
12565 [
12566 ExcerptRange {
12567 context: Point::new(0, 0)..Point::new(3, 0),
12568 primary: None,
12569 },
12570 ExcerptRange {
12571 context: Point::new(5, 0)..Point::new(7, 0),
12572 primary: None,
12573 },
12574 ExcerptRange {
12575 context: Point::new(9, 0)..Point::new(10, 4),
12576 primary: None,
12577 },
12578 ],
12579 cx,
12580 );
12581 multibuffer
12582 });
12583
12584 let fs = FakeFs::new(cx.executor());
12585 fs.insert_tree(
12586 "/a",
12587 json!({
12588 "main.rs": sample_text_1,
12589 "other.rs": sample_text_2,
12590 "lib.rs": sample_text_3,
12591 }),
12592 )
12593 .await;
12594 let project = Project::test(fs, ["/a".as_ref()], cx).await;
12595 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12596 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12597 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
12598 Editor::new(
12599 EditorMode::Full,
12600 multi_buffer,
12601 Some(project.clone()),
12602 true,
12603 window,
12604 cx,
12605 )
12606 });
12607 let multibuffer_item_id = workspace
12608 .update(cx, |workspace, window, cx| {
12609 assert!(
12610 workspace.active_item(cx).is_none(),
12611 "active item should be None before the first item is added"
12612 );
12613 workspace.add_item_to_active_pane(
12614 Box::new(multi_buffer_editor.clone()),
12615 None,
12616 true,
12617 window,
12618 cx,
12619 );
12620 let active_item = workspace
12621 .active_item(cx)
12622 .expect("should have an active item after adding the multi buffer");
12623 assert!(
12624 !active_item.is_singleton(cx),
12625 "A multi buffer was expected to active after adding"
12626 );
12627 active_item.item_id()
12628 })
12629 .unwrap();
12630 cx.executor().run_until_parked();
12631
12632 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12633 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12634 s.select_ranges(Some(1..2))
12635 });
12636 editor.open_excerpts(&OpenExcerpts, window, cx);
12637 });
12638 cx.executor().run_until_parked();
12639 let first_item_id = workspace
12640 .update(cx, |workspace, window, cx| {
12641 let active_item = workspace
12642 .active_item(cx)
12643 .expect("should have an active item after navigating into the 1st buffer");
12644 let first_item_id = active_item.item_id();
12645 assert_ne!(
12646 first_item_id, multibuffer_item_id,
12647 "Should navigate into the 1st buffer and activate it"
12648 );
12649 assert!(
12650 active_item.is_singleton(cx),
12651 "New active item should be a singleton buffer"
12652 );
12653 assert_eq!(
12654 active_item
12655 .act_as::<Editor>(cx)
12656 .expect("should have navigated into an editor for the 1st buffer")
12657 .read(cx)
12658 .text(cx),
12659 sample_text_1
12660 );
12661
12662 workspace
12663 .go_back(workspace.active_pane().downgrade(), window, cx)
12664 .detach_and_log_err(cx);
12665
12666 first_item_id
12667 })
12668 .unwrap();
12669 cx.executor().run_until_parked();
12670 workspace
12671 .update(cx, |workspace, _, cx| {
12672 let active_item = workspace
12673 .active_item(cx)
12674 .expect("should have an active item after navigating back");
12675 assert_eq!(
12676 active_item.item_id(),
12677 multibuffer_item_id,
12678 "Should navigate back to the multi buffer"
12679 );
12680 assert!(!active_item.is_singleton(cx));
12681 })
12682 .unwrap();
12683
12684 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12685 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12686 s.select_ranges(Some(39..40))
12687 });
12688 editor.open_excerpts(&OpenExcerpts, window, cx);
12689 });
12690 cx.executor().run_until_parked();
12691 let second_item_id = workspace
12692 .update(cx, |workspace, window, cx| {
12693 let active_item = workspace
12694 .active_item(cx)
12695 .expect("should have an active item after navigating into the 2nd buffer");
12696 let second_item_id = active_item.item_id();
12697 assert_ne!(
12698 second_item_id, multibuffer_item_id,
12699 "Should navigate away from the multibuffer"
12700 );
12701 assert_ne!(
12702 second_item_id, first_item_id,
12703 "Should navigate into the 2nd buffer and activate it"
12704 );
12705 assert!(
12706 active_item.is_singleton(cx),
12707 "New active item should be a singleton buffer"
12708 );
12709 assert_eq!(
12710 active_item
12711 .act_as::<Editor>(cx)
12712 .expect("should have navigated into an editor")
12713 .read(cx)
12714 .text(cx),
12715 sample_text_2
12716 );
12717
12718 workspace
12719 .go_back(workspace.active_pane().downgrade(), window, cx)
12720 .detach_and_log_err(cx);
12721
12722 second_item_id
12723 })
12724 .unwrap();
12725 cx.executor().run_until_parked();
12726 workspace
12727 .update(cx, |workspace, _, cx| {
12728 let active_item = workspace
12729 .active_item(cx)
12730 .expect("should have an active item after navigating back from the 2nd buffer");
12731 assert_eq!(
12732 active_item.item_id(),
12733 multibuffer_item_id,
12734 "Should navigate back from the 2nd buffer to the multi buffer"
12735 );
12736 assert!(!active_item.is_singleton(cx));
12737 })
12738 .unwrap();
12739
12740 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12741 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12742 s.select_ranges(Some(70..70))
12743 });
12744 editor.open_excerpts(&OpenExcerpts, window, cx);
12745 });
12746 cx.executor().run_until_parked();
12747 workspace
12748 .update(cx, |workspace, window, cx| {
12749 let active_item = workspace
12750 .active_item(cx)
12751 .expect("should have an active item after navigating into the 3rd buffer");
12752 let third_item_id = active_item.item_id();
12753 assert_ne!(
12754 third_item_id, multibuffer_item_id,
12755 "Should navigate into the 3rd buffer and activate it"
12756 );
12757 assert_ne!(third_item_id, first_item_id);
12758 assert_ne!(third_item_id, second_item_id);
12759 assert!(
12760 active_item.is_singleton(cx),
12761 "New active item should be a singleton buffer"
12762 );
12763 assert_eq!(
12764 active_item
12765 .act_as::<Editor>(cx)
12766 .expect("should have navigated into an editor")
12767 .read(cx)
12768 .text(cx),
12769 sample_text_3
12770 );
12771
12772 workspace
12773 .go_back(workspace.active_pane().downgrade(), window, cx)
12774 .detach_and_log_err(cx);
12775 })
12776 .unwrap();
12777 cx.executor().run_until_parked();
12778 workspace
12779 .update(cx, |workspace, _, cx| {
12780 let active_item = workspace
12781 .active_item(cx)
12782 .expect("should have an active item after navigating back from the 3rd buffer");
12783 assert_eq!(
12784 active_item.item_id(),
12785 multibuffer_item_id,
12786 "Should navigate back from the 3rd buffer to the multi buffer"
12787 );
12788 assert!(!active_item.is_singleton(cx));
12789 })
12790 .unwrap();
12791}
12792
12793#[gpui::test]
12794async fn test_toggle_selected_diff_hunks(
12795 executor: BackgroundExecutor,
12796 cx: &mut gpui::TestAppContext,
12797) {
12798 init_test(cx, |_| {});
12799
12800 let mut cx = EditorTestContext::new(cx).await;
12801
12802 let diff_base = r#"
12803 use some::mod;
12804
12805 const A: u32 = 42;
12806
12807 fn main() {
12808 println!("hello");
12809
12810 println!("world");
12811 }
12812 "#
12813 .unindent();
12814
12815 cx.set_state(
12816 &r#"
12817 use some::modified;
12818
12819 ˇ
12820 fn main() {
12821 println!("hello there");
12822
12823 println!("around the");
12824 println!("world");
12825 }
12826 "#
12827 .unindent(),
12828 );
12829
12830 cx.set_diff_base(&diff_base);
12831 executor.run_until_parked();
12832
12833 cx.update_editor(|editor, window, cx| {
12834 editor.go_to_next_hunk(&GoToHunk, window, cx);
12835 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
12836 });
12837 executor.run_until_parked();
12838 cx.assert_state_with_diff(
12839 r#"
12840 use some::modified;
12841
12842
12843 fn main() {
12844 - println!("hello");
12845 + ˇ println!("hello there");
12846
12847 println!("around the");
12848 println!("world");
12849 }
12850 "#
12851 .unindent(),
12852 );
12853
12854 cx.update_editor(|editor, window, cx| {
12855 for _ in 0..2 {
12856 editor.go_to_next_hunk(&GoToHunk, window, cx);
12857 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
12858 }
12859 });
12860 executor.run_until_parked();
12861 cx.assert_state_with_diff(
12862 r#"
12863 - use some::mod;
12864 + ˇuse some::modified;
12865
12866
12867 fn main() {
12868 - println!("hello");
12869 + println!("hello there");
12870
12871 + println!("around the");
12872 println!("world");
12873 }
12874 "#
12875 .unindent(),
12876 );
12877
12878 cx.update_editor(|editor, window, cx| {
12879 editor.go_to_next_hunk(&GoToHunk, window, cx);
12880 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
12881 });
12882 executor.run_until_parked();
12883 cx.assert_state_with_diff(
12884 r#"
12885 - use some::mod;
12886 + use some::modified;
12887
12888 - const A: u32 = 42;
12889 ˇ
12890 fn main() {
12891 - println!("hello");
12892 + println!("hello there");
12893
12894 + println!("around the");
12895 println!("world");
12896 }
12897 "#
12898 .unindent(),
12899 );
12900
12901 cx.update_editor(|editor, window, cx| {
12902 editor.cancel(&Cancel, window, cx);
12903 });
12904
12905 cx.assert_state_with_diff(
12906 r#"
12907 use some::modified;
12908
12909 ˇ
12910 fn main() {
12911 println!("hello there");
12912
12913 println!("around the");
12914 println!("world");
12915 }
12916 "#
12917 .unindent(),
12918 );
12919}
12920
12921#[gpui::test]
12922async fn test_diff_base_change_with_expanded_diff_hunks(
12923 executor: BackgroundExecutor,
12924 cx: &mut gpui::TestAppContext,
12925) {
12926 init_test(cx, |_| {});
12927
12928 let mut cx = EditorTestContext::new(cx).await;
12929
12930 let diff_base = r#"
12931 use some::mod1;
12932 use some::mod2;
12933
12934 const A: u32 = 42;
12935 const B: u32 = 42;
12936 const C: u32 = 42;
12937
12938 fn main() {
12939 println!("hello");
12940
12941 println!("world");
12942 }
12943 "#
12944 .unindent();
12945
12946 cx.set_state(
12947 &r#"
12948 use some::mod2;
12949
12950 const A: u32 = 42;
12951 const C: u32 = 42;
12952
12953 fn main(ˇ) {
12954 //println!("hello");
12955
12956 println!("world");
12957 //
12958 //
12959 }
12960 "#
12961 .unindent(),
12962 );
12963
12964 cx.set_diff_base(&diff_base);
12965 executor.run_until_parked();
12966
12967 cx.update_editor(|editor, window, cx| {
12968 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
12969 });
12970 executor.run_until_parked();
12971 cx.assert_state_with_diff(
12972 r#"
12973 - use some::mod1;
12974 use some::mod2;
12975
12976 const A: u32 = 42;
12977 - const B: u32 = 42;
12978 const C: u32 = 42;
12979
12980 fn main(ˇ) {
12981 - println!("hello");
12982 + //println!("hello");
12983
12984 println!("world");
12985 + //
12986 + //
12987 }
12988 "#
12989 .unindent(),
12990 );
12991
12992 cx.set_diff_base("new diff base!");
12993 executor.run_until_parked();
12994 cx.assert_state_with_diff(
12995 r#"
12996 use some::mod2;
12997
12998 const A: u32 = 42;
12999 const C: u32 = 42;
13000
13001 fn main(ˇ) {
13002 //println!("hello");
13003
13004 println!("world");
13005 //
13006 //
13007 }
13008 "#
13009 .unindent(),
13010 );
13011
13012 cx.update_editor(|editor, window, cx| {
13013 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13014 });
13015 executor.run_until_parked();
13016 cx.assert_state_with_diff(
13017 r#"
13018 - new diff base!
13019 + use some::mod2;
13020 +
13021 + const A: u32 = 42;
13022 + const C: u32 = 42;
13023 +
13024 + fn main(ˇ) {
13025 + //println!("hello");
13026 +
13027 + println!("world");
13028 + //
13029 + //
13030 + }
13031 "#
13032 .unindent(),
13033 );
13034}
13035
13036#[gpui::test]
13037async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) {
13038 init_test(cx, |_| {});
13039
13040 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
13041 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
13042 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
13043 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
13044 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
13045 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
13046
13047 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
13048 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
13049 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
13050
13051 let multi_buffer = cx.new(|cx| {
13052 let mut multibuffer = MultiBuffer::new(ReadWrite);
13053 multibuffer.push_excerpts(
13054 buffer_1.clone(),
13055 [
13056 ExcerptRange {
13057 context: Point::new(0, 0)..Point::new(3, 0),
13058 primary: None,
13059 },
13060 ExcerptRange {
13061 context: Point::new(5, 0)..Point::new(7, 0),
13062 primary: None,
13063 },
13064 ExcerptRange {
13065 context: Point::new(9, 0)..Point::new(10, 3),
13066 primary: None,
13067 },
13068 ],
13069 cx,
13070 );
13071 multibuffer.push_excerpts(
13072 buffer_2.clone(),
13073 [
13074 ExcerptRange {
13075 context: Point::new(0, 0)..Point::new(3, 0),
13076 primary: None,
13077 },
13078 ExcerptRange {
13079 context: Point::new(5, 0)..Point::new(7, 0),
13080 primary: None,
13081 },
13082 ExcerptRange {
13083 context: Point::new(9, 0)..Point::new(10, 3),
13084 primary: None,
13085 },
13086 ],
13087 cx,
13088 );
13089 multibuffer.push_excerpts(
13090 buffer_3.clone(),
13091 [
13092 ExcerptRange {
13093 context: Point::new(0, 0)..Point::new(3, 0),
13094 primary: None,
13095 },
13096 ExcerptRange {
13097 context: Point::new(5, 0)..Point::new(7, 0),
13098 primary: None,
13099 },
13100 ExcerptRange {
13101 context: Point::new(9, 0)..Point::new(10, 3),
13102 primary: None,
13103 },
13104 ],
13105 cx,
13106 );
13107 multibuffer
13108 });
13109
13110 let editor = cx.add_window(|window, cx| {
13111 Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
13112 });
13113 editor
13114 .update(cx, |editor, _window, cx| {
13115 for (buffer, diff_base) in [
13116 (buffer_1.clone(), file_1_old),
13117 (buffer_2.clone(), file_2_old),
13118 (buffer_3.clone(), file_3_old),
13119 ] {
13120 let change_set = cx.new(|cx| {
13121 BufferChangeSet::new_with_base_text(diff_base.to_string(), &buffer, cx)
13122 });
13123 editor
13124 .buffer
13125 .update(cx, |buffer, cx| buffer.add_change_set(change_set, cx));
13126 }
13127 })
13128 .unwrap();
13129
13130 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13131 cx.run_until_parked();
13132
13133 cx.assert_editor_state(
13134 &"
13135 ˇaaa
13136 ccc
13137 ddd
13138
13139 ggg
13140 hhh
13141
13142
13143 lll
13144 mmm
13145 NNN
13146
13147 qqq
13148 rrr
13149
13150 uuu
13151 111
13152 222
13153 333
13154
13155 666
13156 777
13157
13158 000
13159 !!!"
13160 .unindent(),
13161 );
13162
13163 cx.update_editor(|editor, window, cx| {
13164 editor.select_all(&SelectAll, window, cx);
13165 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13166 });
13167 cx.executor().run_until_parked();
13168
13169 cx.assert_state_with_diff(
13170 "
13171 «aaa
13172 - bbb
13173 ccc
13174 ddd
13175
13176 ggg
13177 hhh
13178
13179
13180 lll
13181 mmm
13182 - nnn
13183 + NNN
13184
13185 qqq
13186 rrr
13187
13188 uuu
13189 111
13190 222
13191 333
13192
13193 + 666
13194 777
13195
13196 000
13197 !!!ˇ»"
13198 .unindent(),
13199 );
13200}
13201
13202#[gpui::test]
13203async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext) {
13204 init_test(cx, |_| {});
13205
13206 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
13207 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\n";
13208
13209 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
13210 let multi_buffer = cx.new(|cx| {
13211 let mut multibuffer = MultiBuffer::new(ReadWrite);
13212 multibuffer.push_excerpts(
13213 buffer.clone(),
13214 [
13215 ExcerptRange {
13216 context: Point::new(0, 0)..Point::new(2, 0),
13217 primary: None,
13218 },
13219 ExcerptRange {
13220 context: Point::new(5, 0)..Point::new(7, 0),
13221 primary: None,
13222 },
13223 ],
13224 cx,
13225 );
13226 multibuffer
13227 });
13228
13229 let editor = cx.add_window(|window, cx| {
13230 Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
13231 });
13232 editor
13233 .update(cx, |editor, _window, cx| {
13234 let change_set =
13235 cx.new(|cx| BufferChangeSet::new_with_base_text(base.to_string(), &buffer, cx));
13236 editor
13237 .buffer
13238 .update(cx, |buffer, cx| buffer.add_change_set(change_set, cx))
13239 })
13240 .unwrap();
13241
13242 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13243 cx.run_until_parked();
13244
13245 cx.update_editor(|editor, window, cx| {
13246 editor.expand_all_diff_hunks(&Default::default(), window, cx)
13247 });
13248 cx.executor().run_until_parked();
13249
13250 cx.assert_state_with_diff(
13251 "
13252 ˇaaa
13253 - bbb
13254 + BBB
13255
13256 + EEE
13257 fff
13258 "
13259 .unindent(),
13260 );
13261}
13262
13263#[gpui::test]
13264async fn test_edits_around_expanded_insertion_hunks(
13265 executor: BackgroundExecutor,
13266 cx: &mut gpui::TestAppContext,
13267) {
13268 init_test(cx, |_| {});
13269
13270 let mut cx = EditorTestContext::new(cx).await;
13271
13272 let diff_base = r#"
13273 use some::mod1;
13274 use some::mod2;
13275
13276 const A: u32 = 42;
13277
13278 fn main() {
13279 println!("hello");
13280
13281 println!("world");
13282 }
13283 "#
13284 .unindent();
13285 executor.run_until_parked();
13286 cx.set_state(
13287 &r#"
13288 use some::mod1;
13289 use some::mod2;
13290
13291 const A: u32 = 42;
13292 const B: u32 = 42;
13293 const C: u32 = 42;
13294 ˇ
13295
13296 fn main() {
13297 println!("hello");
13298
13299 println!("world");
13300 }
13301 "#
13302 .unindent(),
13303 );
13304
13305 cx.set_diff_base(&diff_base);
13306 executor.run_until_parked();
13307
13308 cx.update_editor(|editor, window, cx| {
13309 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13310 });
13311 executor.run_until_parked();
13312
13313 cx.assert_state_with_diff(
13314 r#"
13315 use some::mod1;
13316 use some::mod2;
13317
13318 const A: u32 = 42;
13319 + const B: u32 = 42;
13320 + const C: u32 = 42;
13321 + ˇ
13322
13323 fn main() {
13324 println!("hello");
13325
13326 println!("world");
13327 }
13328 "#
13329 .unindent(),
13330 );
13331
13332 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
13333 executor.run_until_parked();
13334
13335 cx.assert_state_with_diff(
13336 r#"
13337 use some::mod1;
13338 use some::mod2;
13339
13340 const A: u32 = 42;
13341 + const B: u32 = 42;
13342 + const C: u32 = 42;
13343 + const D: u32 = 42;
13344 + ˇ
13345
13346 fn main() {
13347 println!("hello");
13348
13349 println!("world");
13350 }
13351 "#
13352 .unindent(),
13353 );
13354
13355 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
13356 executor.run_until_parked();
13357
13358 cx.assert_state_with_diff(
13359 r#"
13360 use some::mod1;
13361 use some::mod2;
13362
13363 const A: u32 = 42;
13364 + const B: u32 = 42;
13365 + const C: u32 = 42;
13366 + const D: u32 = 42;
13367 + const E: u32 = 42;
13368 + ˇ
13369
13370 fn main() {
13371 println!("hello");
13372
13373 println!("world");
13374 }
13375 "#
13376 .unindent(),
13377 );
13378
13379 cx.update_editor(|editor, window, cx| {
13380 editor.delete_line(&DeleteLine, window, cx);
13381 });
13382 executor.run_until_parked();
13383
13384 cx.assert_state_with_diff(
13385 r#"
13386 use some::mod1;
13387 use some::mod2;
13388
13389 const A: u32 = 42;
13390 + const B: u32 = 42;
13391 + const C: u32 = 42;
13392 + const D: u32 = 42;
13393 + const E: u32 = 42;
13394 ˇ
13395 fn main() {
13396 println!("hello");
13397
13398 println!("world");
13399 }
13400 "#
13401 .unindent(),
13402 );
13403
13404 cx.update_editor(|editor, window, cx| {
13405 editor.move_up(&MoveUp, window, cx);
13406 editor.delete_line(&DeleteLine, window, cx);
13407 editor.move_up(&MoveUp, window, cx);
13408 editor.delete_line(&DeleteLine, window, cx);
13409 editor.move_up(&MoveUp, window, cx);
13410 editor.delete_line(&DeleteLine, window, cx);
13411 });
13412 executor.run_until_parked();
13413 cx.assert_state_with_diff(
13414 r#"
13415 use some::mod1;
13416 use some::mod2;
13417
13418 const A: u32 = 42;
13419 + const B: u32 = 42;
13420 ˇ
13421 fn main() {
13422 println!("hello");
13423
13424 println!("world");
13425 }
13426 "#
13427 .unindent(),
13428 );
13429
13430 cx.update_editor(|editor, window, cx| {
13431 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
13432 editor.delete_line(&DeleteLine, window, cx);
13433 });
13434 executor.run_until_parked();
13435 cx.assert_state_with_diff(
13436 r#"
13437 ˇ
13438 fn main() {
13439 println!("hello");
13440
13441 println!("world");
13442 }
13443 "#
13444 .unindent(),
13445 );
13446}
13447
13448#[gpui::test]
13449async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
13450 init_test(cx, |_| {});
13451
13452 let mut cx = EditorTestContext::new(cx).await;
13453 cx.set_diff_base(indoc! { "
13454 one
13455 two
13456 three
13457 four
13458 five
13459 "
13460 });
13461 cx.set_state(indoc! { "
13462 one
13463 ˇthree
13464 five
13465 "});
13466 cx.run_until_parked();
13467 cx.update_editor(|editor, window, cx| {
13468 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13469 });
13470 cx.assert_state_with_diff(
13471 indoc! { "
13472 one
13473 - two
13474 ˇthree
13475 - four
13476 five
13477 "}
13478 .to_string(),
13479 );
13480 cx.update_editor(|editor, window, cx| {
13481 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13482 });
13483
13484 cx.assert_state_with_diff(
13485 indoc! { "
13486 one
13487 ˇthree
13488 five
13489 "}
13490 .to_string(),
13491 );
13492
13493 cx.set_state(indoc! { "
13494 one
13495 TWO
13496 ˇthree
13497 four
13498 five
13499 "});
13500 cx.run_until_parked();
13501 cx.update_editor(|editor, window, cx| {
13502 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13503 });
13504
13505 cx.assert_state_with_diff(
13506 indoc! { "
13507 one
13508 - two
13509 + TWO
13510 ˇthree
13511 four
13512 five
13513 "}
13514 .to_string(),
13515 );
13516 cx.update_editor(|editor, window, cx| {
13517 editor.move_up(&Default::default(), window, cx);
13518 editor.move_up(&Default::default(), window, cx);
13519 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13520 });
13521 cx.assert_state_with_diff(
13522 indoc! { "
13523 one
13524 ˇTWO
13525 three
13526 four
13527 five
13528 "}
13529 .to_string(),
13530 );
13531}
13532
13533#[gpui::test]
13534async fn test_edits_around_expanded_deletion_hunks(
13535 executor: BackgroundExecutor,
13536 cx: &mut gpui::TestAppContext,
13537) {
13538 init_test(cx, |_| {});
13539
13540 let mut cx = EditorTestContext::new(cx).await;
13541
13542 let diff_base = r#"
13543 use some::mod1;
13544 use some::mod2;
13545
13546 const A: u32 = 42;
13547 const B: u32 = 42;
13548 const C: u32 = 42;
13549
13550
13551 fn main() {
13552 println!("hello");
13553
13554 println!("world");
13555 }
13556 "#
13557 .unindent();
13558 executor.run_until_parked();
13559 cx.set_state(
13560 &r#"
13561 use some::mod1;
13562 use some::mod2;
13563
13564 ˇconst B: u32 = 42;
13565 const C: u32 = 42;
13566
13567
13568 fn main() {
13569 println!("hello");
13570
13571 println!("world");
13572 }
13573 "#
13574 .unindent(),
13575 );
13576
13577 cx.set_diff_base(&diff_base);
13578 executor.run_until_parked();
13579
13580 cx.update_editor(|editor, window, cx| {
13581 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13582 });
13583 executor.run_until_parked();
13584
13585 cx.assert_state_with_diff(
13586 r#"
13587 use some::mod1;
13588 use some::mod2;
13589
13590 - const A: u32 = 42;
13591 ˇconst B: u32 = 42;
13592 const C: u32 = 42;
13593
13594
13595 fn main() {
13596 println!("hello");
13597
13598 println!("world");
13599 }
13600 "#
13601 .unindent(),
13602 );
13603
13604 cx.update_editor(|editor, window, cx| {
13605 editor.delete_line(&DeleteLine, window, cx);
13606 });
13607 executor.run_until_parked();
13608 cx.assert_state_with_diff(
13609 r#"
13610 use some::mod1;
13611 use some::mod2;
13612
13613 - const A: u32 = 42;
13614 - const B: u32 = 42;
13615 ˇconst C: u32 = 42;
13616
13617
13618 fn main() {
13619 println!("hello");
13620
13621 println!("world");
13622 }
13623 "#
13624 .unindent(),
13625 );
13626
13627 cx.update_editor(|editor, window, cx| {
13628 editor.delete_line(&DeleteLine, window, cx);
13629 });
13630 executor.run_until_parked();
13631 cx.assert_state_with_diff(
13632 r#"
13633 use some::mod1;
13634 use some::mod2;
13635
13636 - const A: u32 = 42;
13637 - const B: u32 = 42;
13638 - const C: u32 = 42;
13639 ˇ
13640
13641 fn main() {
13642 println!("hello");
13643
13644 println!("world");
13645 }
13646 "#
13647 .unindent(),
13648 );
13649
13650 cx.update_editor(|editor, window, cx| {
13651 editor.handle_input("replacement", window, cx);
13652 });
13653 executor.run_until_parked();
13654 cx.assert_state_with_diff(
13655 r#"
13656 use some::mod1;
13657 use some::mod2;
13658
13659 - const A: u32 = 42;
13660 - const B: u32 = 42;
13661 - const C: u32 = 42;
13662 -
13663 + replacementˇ
13664
13665 fn main() {
13666 println!("hello");
13667
13668 println!("world");
13669 }
13670 "#
13671 .unindent(),
13672 );
13673}
13674
13675#[gpui::test]
13676async fn test_backspace_after_deletion_hunk(
13677 executor: BackgroundExecutor,
13678 cx: &mut gpui::TestAppContext,
13679) {
13680 init_test(cx, |_| {});
13681
13682 let mut cx = EditorTestContext::new(cx).await;
13683
13684 let base_text = r#"
13685 one
13686 two
13687 three
13688 four
13689 five
13690 "#
13691 .unindent();
13692 executor.run_until_parked();
13693 cx.set_state(
13694 &r#"
13695 one
13696 two
13697 fˇour
13698 five
13699 "#
13700 .unindent(),
13701 );
13702
13703 cx.set_diff_base(&base_text);
13704 executor.run_until_parked();
13705
13706 cx.update_editor(|editor, window, cx| {
13707 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13708 });
13709 executor.run_until_parked();
13710
13711 cx.assert_state_with_diff(
13712 r#"
13713 one
13714 two
13715 - three
13716 fˇour
13717 five
13718 "#
13719 .unindent(),
13720 );
13721
13722 cx.update_editor(|editor, window, cx| {
13723 editor.backspace(&Backspace, window, cx);
13724 editor.backspace(&Backspace, window, cx);
13725 });
13726 executor.run_until_parked();
13727 cx.assert_state_with_diff(
13728 r#"
13729 one
13730 two
13731 - threeˇ
13732 - four
13733 + our
13734 five
13735 "#
13736 .unindent(),
13737 );
13738}
13739
13740#[gpui::test]
13741async fn test_edit_after_expanded_modification_hunk(
13742 executor: BackgroundExecutor,
13743 cx: &mut gpui::TestAppContext,
13744) {
13745 init_test(cx, |_| {});
13746
13747 let mut cx = EditorTestContext::new(cx).await;
13748
13749 let diff_base = r#"
13750 use some::mod1;
13751 use some::mod2;
13752
13753 const A: u32 = 42;
13754 const B: u32 = 42;
13755 const C: u32 = 42;
13756 const D: u32 = 42;
13757
13758
13759 fn main() {
13760 println!("hello");
13761
13762 println!("world");
13763 }"#
13764 .unindent();
13765
13766 cx.set_state(
13767 &r#"
13768 use some::mod1;
13769 use some::mod2;
13770
13771 const A: u32 = 42;
13772 const B: u32 = 42;
13773 const C: u32 = 43ˇ
13774 const D: u32 = 42;
13775
13776
13777 fn main() {
13778 println!("hello");
13779
13780 println!("world");
13781 }"#
13782 .unindent(),
13783 );
13784
13785 cx.set_diff_base(&diff_base);
13786 executor.run_until_parked();
13787 cx.update_editor(|editor, window, cx| {
13788 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13789 });
13790 executor.run_until_parked();
13791
13792 cx.assert_state_with_diff(
13793 r#"
13794 use some::mod1;
13795 use some::mod2;
13796
13797 const A: u32 = 42;
13798 const B: u32 = 42;
13799 - const C: u32 = 42;
13800 + const C: u32 = 43ˇ
13801 const D: u32 = 42;
13802
13803
13804 fn main() {
13805 println!("hello");
13806
13807 println!("world");
13808 }"#
13809 .unindent(),
13810 );
13811
13812 cx.update_editor(|editor, window, cx| {
13813 editor.handle_input("\nnew_line\n", window, cx);
13814 });
13815 executor.run_until_parked();
13816
13817 cx.assert_state_with_diff(
13818 r#"
13819 use some::mod1;
13820 use some::mod2;
13821
13822 const A: u32 = 42;
13823 const B: u32 = 42;
13824 - const C: u32 = 42;
13825 + const C: u32 = 43
13826 + new_line
13827 + ˇ
13828 const D: u32 = 42;
13829
13830
13831 fn main() {
13832 println!("hello");
13833
13834 println!("world");
13835 }"#
13836 .unindent(),
13837 );
13838}
13839
13840async fn setup_indent_guides_editor(
13841 text: &str,
13842 cx: &mut gpui::TestAppContext,
13843) -> (BufferId, EditorTestContext) {
13844 init_test(cx, |_| {});
13845
13846 let mut cx = EditorTestContext::new(cx).await;
13847
13848 let buffer_id = cx.update_editor(|editor, window, cx| {
13849 editor.set_text(text, window, cx);
13850 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
13851
13852 buffer_ids[0]
13853 });
13854
13855 (buffer_id, cx)
13856}
13857
13858fn assert_indent_guides(
13859 range: Range<u32>,
13860 expected: Vec<IndentGuide>,
13861 active_indices: Option<Vec<usize>>,
13862 cx: &mut EditorTestContext,
13863) {
13864 let indent_guides = cx.update_editor(|editor, window, cx| {
13865 let snapshot = editor.snapshot(window, cx).display_snapshot;
13866 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
13867 editor,
13868 MultiBufferRow(range.start)..MultiBufferRow(range.end),
13869 true,
13870 &snapshot,
13871 cx,
13872 );
13873
13874 indent_guides.sort_by(|a, b| {
13875 a.depth.cmp(&b.depth).then(
13876 a.start_row
13877 .cmp(&b.start_row)
13878 .then(a.end_row.cmp(&b.end_row)),
13879 )
13880 });
13881 indent_guides
13882 });
13883
13884 if let Some(expected) = active_indices {
13885 let active_indices = cx.update_editor(|editor, window, cx| {
13886 let snapshot = editor.snapshot(window, cx).display_snapshot;
13887 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
13888 });
13889
13890 assert_eq!(
13891 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
13892 expected,
13893 "Active indent guide indices do not match"
13894 );
13895 }
13896
13897 assert_eq!(indent_guides, expected, "Indent guides do not match");
13898}
13899
13900fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
13901 IndentGuide {
13902 buffer_id,
13903 start_row: MultiBufferRow(start_row),
13904 end_row: MultiBufferRow(end_row),
13905 depth,
13906 tab_size: 4,
13907 settings: IndentGuideSettings {
13908 enabled: true,
13909 line_width: 1,
13910 active_line_width: 1,
13911 ..Default::default()
13912 },
13913 }
13914}
13915
13916#[gpui::test]
13917async fn test_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
13918 let (buffer_id, mut cx) = setup_indent_guides_editor(
13919 &"
13920 fn main() {
13921 let a = 1;
13922 }"
13923 .unindent(),
13924 cx,
13925 )
13926 .await;
13927
13928 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
13929}
13930
13931#[gpui::test]
13932async fn test_indent_guide_simple_block(cx: &mut gpui::TestAppContext) {
13933 let (buffer_id, mut cx) = setup_indent_guides_editor(
13934 &"
13935 fn main() {
13936 let a = 1;
13937 let b = 2;
13938 }"
13939 .unindent(),
13940 cx,
13941 )
13942 .await;
13943
13944 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
13945}
13946
13947#[gpui::test]
13948async fn test_indent_guide_nested(cx: &mut gpui::TestAppContext) {
13949 let (buffer_id, mut cx) = setup_indent_guides_editor(
13950 &"
13951 fn main() {
13952 let a = 1;
13953 if a == 3 {
13954 let b = 2;
13955 } else {
13956 let c = 3;
13957 }
13958 }"
13959 .unindent(),
13960 cx,
13961 )
13962 .await;
13963
13964 assert_indent_guides(
13965 0..8,
13966 vec![
13967 indent_guide(buffer_id, 1, 6, 0),
13968 indent_guide(buffer_id, 3, 3, 1),
13969 indent_guide(buffer_id, 5, 5, 1),
13970 ],
13971 None,
13972 &mut cx,
13973 );
13974}
13975
13976#[gpui::test]
13977async fn test_indent_guide_tab(cx: &mut gpui::TestAppContext) {
13978 let (buffer_id, mut cx) = setup_indent_guides_editor(
13979 &"
13980 fn main() {
13981 let a = 1;
13982 let b = 2;
13983 let c = 3;
13984 }"
13985 .unindent(),
13986 cx,
13987 )
13988 .await;
13989
13990 assert_indent_guides(
13991 0..5,
13992 vec![
13993 indent_guide(buffer_id, 1, 3, 0),
13994 indent_guide(buffer_id, 2, 2, 1),
13995 ],
13996 None,
13997 &mut cx,
13998 );
13999}
14000
14001#[gpui::test]
14002async fn test_indent_guide_continues_on_empty_line(cx: &mut gpui::TestAppContext) {
14003 let (buffer_id, mut cx) = setup_indent_guides_editor(
14004 &"
14005 fn main() {
14006 let a = 1;
14007
14008 let c = 3;
14009 }"
14010 .unindent(),
14011 cx,
14012 )
14013 .await;
14014
14015 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
14016}
14017
14018#[gpui::test]
14019async fn test_indent_guide_complex(cx: &mut gpui::TestAppContext) {
14020 let (buffer_id, mut cx) = setup_indent_guides_editor(
14021 &"
14022 fn main() {
14023 let a = 1;
14024
14025 let c = 3;
14026
14027 if a == 3 {
14028 let b = 2;
14029 } else {
14030 let c = 3;
14031 }
14032 }"
14033 .unindent(),
14034 cx,
14035 )
14036 .await;
14037
14038 assert_indent_guides(
14039 0..11,
14040 vec![
14041 indent_guide(buffer_id, 1, 9, 0),
14042 indent_guide(buffer_id, 6, 6, 1),
14043 indent_guide(buffer_id, 8, 8, 1),
14044 ],
14045 None,
14046 &mut cx,
14047 );
14048}
14049
14050#[gpui::test]
14051async fn test_indent_guide_starts_off_screen(cx: &mut gpui::TestAppContext) {
14052 let (buffer_id, mut cx) = setup_indent_guides_editor(
14053 &"
14054 fn main() {
14055 let a = 1;
14056
14057 let c = 3;
14058
14059 if a == 3 {
14060 let b = 2;
14061 } else {
14062 let c = 3;
14063 }
14064 }"
14065 .unindent(),
14066 cx,
14067 )
14068 .await;
14069
14070 assert_indent_guides(
14071 1..11,
14072 vec![
14073 indent_guide(buffer_id, 1, 9, 0),
14074 indent_guide(buffer_id, 6, 6, 1),
14075 indent_guide(buffer_id, 8, 8, 1),
14076 ],
14077 None,
14078 &mut cx,
14079 );
14080}
14081
14082#[gpui::test]
14083async fn test_indent_guide_ends_off_screen(cx: &mut gpui::TestAppContext) {
14084 let (buffer_id, mut cx) = setup_indent_guides_editor(
14085 &"
14086 fn main() {
14087 let a = 1;
14088
14089 let c = 3;
14090
14091 if a == 3 {
14092 let b = 2;
14093 } else {
14094 let c = 3;
14095 }
14096 }"
14097 .unindent(),
14098 cx,
14099 )
14100 .await;
14101
14102 assert_indent_guides(
14103 1..10,
14104 vec![
14105 indent_guide(buffer_id, 1, 9, 0),
14106 indent_guide(buffer_id, 6, 6, 1),
14107 indent_guide(buffer_id, 8, 8, 1),
14108 ],
14109 None,
14110 &mut cx,
14111 );
14112}
14113
14114#[gpui::test]
14115async fn test_indent_guide_without_brackets(cx: &mut gpui::TestAppContext) {
14116 let (buffer_id, mut cx) = setup_indent_guides_editor(
14117 &"
14118 block1
14119 block2
14120 block3
14121 block4
14122 block2
14123 block1
14124 block1"
14125 .unindent(),
14126 cx,
14127 )
14128 .await;
14129
14130 assert_indent_guides(
14131 1..10,
14132 vec![
14133 indent_guide(buffer_id, 1, 4, 0),
14134 indent_guide(buffer_id, 2, 3, 1),
14135 indent_guide(buffer_id, 3, 3, 2),
14136 ],
14137 None,
14138 &mut cx,
14139 );
14140}
14141
14142#[gpui::test]
14143async fn test_indent_guide_ends_before_empty_line(cx: &mut gpui::TestAppContext) {
14144 let (buffer_id, mut cx) = setup_indent_guides_editor(
14145 &"
14146 block1
14147 block2
14148 block3
14149
14150 block1
14151 block1"
14152 .unindent(),
14153 cx,
14154 )
14155 .await;
14156
14157 assert_indent_guides(
14158 0..6,
14159 vec![
14160 indent_guide(buffer_id, 1, 2, 0),
14161 indent_guide(buffer_id, 2, 2, 1),
14162 ],
14163 None,
14164 &mut cx,
14165 );
14166}
14167
14168#[gpui::test]
14169async fn test_indent_guide_continuing_off_screen(cx: &mut gpui::TestAppContext) {
14170 let (buffer_id, mut cx) = setup_indent_guides_editor(
14171 &"
14172 block1
14173
14174
14175
14176 block2
14177 "
14178 .unindent(),
14179 cx,
14180 )
14181 .await;
14182
14183 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
14184}
14185
14186#[gpui::test]
14187async fn test_indent_guide_tabs(cx: &mut gpui::TestAppContext) {
14188 let (buffer_id, mut cx) = setup_indent_guides_editor(
14189 &"
14190 def a:
14191 \tb = 3
14192 \tif True:
14193 \t\tc = 4
14194 \t\td = 5
14195 \tprint(b)
14196 "
14197 .unindent(),
14198 cx,
14199 )
14200 .await;
14201
14202 assert_indent_guides(
14203 0..6,
14204 vec![
14205 indent_guide(buffer_id, 1, 6, 0),
14206 indent_guide(buffer_id, 3, 4, 1),
14207 ],
14208 None,
14209 &mut cx,
14210 );
14211}
14212
14213#[gpui::test]
14214async fn test_active_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
14215 let (buffer_id, mut cx) = setup_indent_guides_editor(
14216 &"
14217 fn main() {
14218 let a = 1;
14219 }"
14220 .unindent(),
14221 cx,
14222 )
14223 .await;
14224
14225 cx.update_editor(|editor, window, cx| {
14226 editor.change_selections(None, window, cx, |s| {
14227 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14228 });
14229 });
14230
14231 assert_indent_guides(
14232 0..3,
14233 vec![indent_guide(buffer_id, 1, 1, 0)],
14234 Some(vec![0]),
14235 &mut cx,
14236 );
14237}
14238
14239#[gpui::test]
14240async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppContext) {
14241 let (buffer_id, mut cx) = setup_indent_guides_editor(
14242 &"
14243 fn main() {
14244 if 1 == 2 {
14245 let a = 1;
14246 }
14247 }"
14248 .unindent(),
14249 cx,
14250 )
14251 .await;
14252
14253 cx.update_editor(|editor, window, cx| {
14254 editor.change_selections(None, window, cx, |s| {
14255 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14256 });
14257 });
14258
14259 assert_indent_guides(
14260 0..4,
14261 vec![
14262 indent_guide(buffer_id, 1, 3, 0),
14263 indent_guide(buffer_id, 2, 2, 1),
14264 ],
14265 Some(vec![1]),
14266 &mut cx,
14267 );
14268
14269 cx.update_editor(|editor, window, cx| {
14270 editor.change_selections(None, window, cx, |s| {
14271 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
14272 });
14273 });
14274
14275 assert_indent_guides(
14276 0..4,
14277 vec![
14278 indent_guide(buffer_id, 1, 3, 0),
14279 indent_guide(buffer_id, 2, 2, 1),
14280 ],
14281 Some(vec![1]),
14282 &mut cx,
14283 );
14284
14285 cx.update_editor(|editor, window, cx| {
14286 editor.change_selections(None, window, cx, |s| {
14287 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
14288 });
14289 });
14290
14291 assert_indent_guides(
14292 0..4,
14293 vec![
14294 indent_guide(buffer_id, 1, 3, 0),
14295 indent_guide(buffer_id, 2, 2, 1),
14296 ],
14297 Some(vec![0]),
14298 &mut cx,
14299 );
14300}
14301
14302#[gpui::test]
14303async fn test_active_indent_guide_empty_line(cx: &mut gpui::TestAppContext) {
14304 let (buffer_id, mut cx) = setup_indent_guides_editor(
14305 &"
14306 fn main() {
14307 let a = 1;
14308
14309 let b = 2;
14310 }"
14311 .unindent(),
14312 cx,
14313 )
14314 .await;
14315
14316 cx.update_editor(|editor, window, cx| {
14317 editor.change_selections(None, window, cx, |s| {
14318 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
14319 });
14320 });
14321
14322 assert_indent_guides(
14323 0..5,
14324 vec![indent_guide(buffer_id, 1, 3, 0)],
14325 Some(vec![0]),
14326 &mut cx,
14327 );
14328}
14329
14330#[gpui::test]
14331async fn test_active_indent_guide_non_matching_indent(cx: &mut gpui::TestAppContext) {
14332 let (buffer_id, mut cx) = setup_indent_guides_editor(
14333 &"
14334 def m:
14335 a = 1
14336 pass"
14337 .unindent(),
14338 cx,
14339 )
14340 .await;
14341
14342 cx.update_editor(|editor, window, cx| {
14343 editor.change_selections(None, window, cx, |s| {
14344 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14345 });
14346 });
14347
14348 assert_indent_guides(
14349 0..3,
14350 vec![indent_guide(buffer_id, 1, 2, 0)],
14351 Some(vec![0]),
14352 &mut cx,
14353 );
14354}
14355
14356#[gpui::test]
14357async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut gpui::TestAppContext) {
14358 init_test(cx, |_| {});
14359 let mut cx = EditorTestContext::new(cx).await;
14360 let text = indoc! {
14361 "
14362 impl A {
14363 fn b() {
14364 0;
14365 3;
14366 5;
14367 6;
14368 7;
14369 }
14370 }
14371 "
14372 };
14373 let base_text = indoc! {
14374 "
14375 impl A {
14376 fn b() {
14377 0;
14378 1;
14379 2;
14380 3;
14381 4;
14382 }
14383 fn c() {
14384 5;
14385 6;
14386 7;
14387 }
14388 }
14389 "
14390 };
14391
14392 cx.update_editor(|editor, window, cx| {
14393 editor.set_text(text, window, cx);
14394
14395 editor.buffer().update(cx, |multibuffer, cx| {
14396 let buffer = multibuffer.as_singleton().unwrap();
14397 let change_set = cx.new(|cx| {
14398 let mut change_set = BufferChangeSet::new(&buffer, cx);
14399 let _ =
14400 change_set.set_base_text(base_text.into(), buffer.read(cx).text_snapshot(), cx);
14401 change_set
14402 });
14403
14404 multibuffer.set_all_diff_hunks_expanded(cx);
14405 multibuffer.add_change_set(change_set, cx);
14406
14407 buffer.read(cx).remote_id()
14408 })
14409 });
14410 cx.run_until_parked();
14411
14412 cx.assert_state_with_diff(
14413 indoc! { "
14414 impl A {
14415 fn b() {
14416 0;
14417 - 1;
14418 - 2;
14419 3;
14420 - 4;
14421 - }
14422 - fn c() {
14423 5;
14424 6;
14425 7;
14426 }
14427 }
14428 ˇ"
14429 }
14430 .to_string(),
14431 );
14432
14433 let mut actual_guides = cx.update_editor(|editor, window, cx| {
14434 editor
14435 .snapshot(window, cx)
14436 .buffer_snapshot
14437 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
14438 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
14439 .collect::<Vec<_>>()
14440 });
14441 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
14442 assert_eq!(
14443 actual_guides,
14444 vec![
14445 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
14446 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
14447 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
14448 ]
14449 );
14450}
14451
14452#[gpui::test]
14453fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
14454 init_test(cx, |_| {});
14455
14456 let editor = cx.add_window(|window, cx| {
14457 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
14458 build_editor(buffer, window, cx)
14459 });
14460
14461 let render_args = Arc::new(Mutex::new(None));
14462 let snapshot = editor
14463 .update(cx, |editor, window, cx| {
14464 let snapshot = editor.buffer().read(cx).snapshot(cx);
14465 let range =
14466 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
14467
14468 struct RenderArgs {
14469 row: MultiBufferRow,
14470 folded: bool,
14471 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
14472 }
14473
14474 let crease = Crease::inline(
14475 range,
14476 FoldPlaceholder::test(),
14477 {
14478 let toggle_callback = render_args.clone();
14479 move |row, folded, callback, _window, _cx| {
14480 *toggle_callback.lock() = Some(RenderArgs {
14481 row,
14482 folded,
14483 callback,
14484 });
14485 div()
14486 }
14487 },
14488 |_row, _folded, _window, _cx| div(),
14489 );
14490
14491 editor.insert_creases(Some(crease), cx);
14492 let snapshot = editor.snapshot(window, cx);
14493 let _div = snapshot.render_crease_toggle(
14494 MultiBufferRow(1),
14495 false,
14496 cx.entity().clone(),
14497 window,
14498 cx,
14499 );
14500 snapshot
14501 })
14502 .unwrap();
14503
14504 let render_args = render_args.lock().take().unwrap();
14505 assert_eq!(render_args.row, MultiBufferRow(1));
14506 assert!(!render_args.folded);
14507 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
14508
14509 cx.update_window(*editor, |_, window, cx| {
14510 (render_args.callback)(true, window, cx)
14511 })
14512 .unwrap();
14513 let snapshot = editor
14514 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
14515 .unwrap();
14516 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
14517
14518 cx.update_window(*editor, |_, window, cx| {
14519 (render_args.callback)(false, window, cx)
14520 })
14521 .unwrap();
14522 let snapshot = editor
14523 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
14524 .unwrap();
14525 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
14526}
14527
14528#[gpui::test]
14529async fn test_input_text(cx: &mut gpui::TestAppContext) {
14530 init_test(cx, |_| {});
14531 let mut cx = EditorTestContext::new(cx).await;
14532
14533 cx.set_state(
14534 &r#"ˇone
14535 two
14536
14537 three
14538 fourˇ
14539 five
14540
14541 siˇx"#
14542 .unindent(),
14543 );
14544
14545 cx.dispatch_action(HandleInput(String::new()));
14546 cx.assert_editor_state(
14547 &r#"ˇone
14548 two
14549
14550 three
14551 fourˇ
14552 five
14553
14554 siˇx"#
14555 .unindent(),
14556 );
14557
14558 cx.dispatch_action(HandleInput("AAAA".to_string()));
14559 cx.assert_editor_state(
14560 &r#"AAAAˇone
14561 two
14562
14563 three
14564 fourAAAAˇ
14565 five
14566
14567 siAAAAˇx"#
14568 .unindent(),
14569 );
14570}
14571
14572#[gpui::test]
14573async fn test_scroll_cursor_center_top_bottom(cx: &mut gpui::TestAppContext) {
14574 init_test(cx, |_| {});
14575
14576 let mut cx = EditorTestContext::new(cx).await;
14577 cx.set_state(
14578 r#"let foo = 1;
14579let foo = 2;
14580let foo = 3;
14581let fooˇ = 4;
14582let foo = 5;
14583let foo = 6;
14584let foo = 7;
14585let foo = 8;
14586let foo = 9;
14587let foo = 10;
14588let foo = 11;
14589let foo = 12;
14590let foo = 13;
14591let foo = 14;
14592let foo = 15;"#,
14593 );
14594
14595 cx.update_editor(|e, window, cx| {
14596 assert_eq!(
14597 e.next_scroll_position,
14598 NextScrollCursorCenterTopBottom::Center,
14599 "Default next scroll direction is center",
14600 );
14601
14602 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14603 assert_eq!(
14604 e.next_scroll_position,
14605 NextScrollCursorCenterTopBottom::Top,
14606 "After center, next scroll direction should be top",
14607 );
14608
14609 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14610 assert_eq!(
14611 e.next_scroll_position,
14612 NextScrollCursorCenterTopBottom::Bottom,
14613 "After top, next scroll direction should be bottom",
14614 );
14615
14616 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14617 assert_eq!(
14618 e.next_scroll_position,
14619 NextScrollCursorCenterTopBottom::Center,
14620 "After bottom, scrolling should start over",
14621 );
14622
14623 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14624 assert_eq!(
14625 e.next_scroll_position,
14626 NextScrollCursorCenterTopBottom::Top,
14627 "Scrolling continues if retriggered fast enough"
14628 );
14629 });
14630
14631 cx.executor()
14632 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
14633 cx.executor().run_until_parked();
14634 cx.update_editor(|e, _, _| {
14635 assert_eq!(
14636 e.next_scroll_position,
14637 NextScrollCursorCenterTopBottom::Center,
14638 "If scrolling is not triggered fast enough, it should reset"
14639 );
14640 });
14641}
14642
14643#[gpui::test]
14644async fn test_goto_definition_with_find_all_references_fallback(cx: &mut gpui::TestAppContext) {
14645 init_test(cx, |_| {});
14646 let mut cx = EditorLspTestContext::new_rust(
14647 lsp::ServerCapabilities {
14648 definition_provider: Some(lsp::OneOf::Left(true)),
14649 references_provider: Some(lsp::OneOf::Left(true)),
14650 ..lsp::ServerCapabilities::default()
14651 },
14652 cx,
14653 )
14654 .await;
14655
14656 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
14657 let go_to_definition = cx.lsp.handle_request::<lsp::request::GotoDefinition, _, _>(
14658 move |params, _| async move {
14659 if empty_go_to_definition {
14660 Ok(None)
14661 } else {
14662 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
14663 uri: params.text_document_position_params.text_document.uri,
14664 range: lsp::Range::new(lsp::Position::new(4, 3), lsp::Position::new(4, 6)),
14665 })))
14666 }
14667 },
14668 );
14669 let references =
14670 cx.lsp
14671 .handle_request::<lsp::request::References, _, _>(move |params, _| async move {
14672 Ok(Some(vec![lsp::Location {
14673 uri: params.text_document_position.text_document.uri,
14674 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
14675 }]))
14676 });
14677 (go_to_definition, references)
14678 };
14679
14680 cx.set_state(
14681 &r#"fn one() {
14682 let mut a = ˇtwo();
14683 }
14684
14685 fn two() {}"#
14686 .unindent(),
14687 );
14688 set_up_lsp_handlers(false, &mut cx);
14689 let navigated = cx
14690 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
14691 .await
14692 .expect("Failed to navigate to definition");
14693 assert_eq!(
14694 navigated,
14695 Navigated::Yes,
14696 "Should have navigated to definition from the GetDefinition response"
14697 );
14698 cx.assert_editor_state(
14699 &r#"fn one() {
14700 let mut a = two();
14701 }
14702
14703 fn «twoˇ»() {}"#
14704 .unindent(),
14705 );
14706
14707 let editors = cx.update_workspace(|workspace, _, cx| {
14708 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
14709 });
14710 cx.update_editor(|_, _, test_editor_cx| {
14711 assert_eq!(
14712 editors.len(),
14713 1,
14714 "Initially, only one, test, editor should be open in the workspace"
14715 );
14716 assert_eq!(
14717 test_editor_cx.entity(),
14718 editors.last().expect("Asserted len is 1").clone()
14719 );
14720 });
14721
14722 set_up_lsp_handlers(true, &mut cx);
14723 let navigated = cx
14724 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
14725 .await
14726 .expect("Failed to navigate to lookup references");
14727 assert_eq!(
14728 navigated,
14729 Navigated::Yes,
14730 "Should have navigated to references as a fallback after empty GoToDefinition response"
14731 );
14732 // We should not change the selections in the existing file,
14733 // if opening another milti buffer with the references
14734 cx.assert_editor_state(
14735 &r#"fn one() {
14736 let mut a = two();
14737 }
14738
14739 fn «twoˇ»() {}"#
14740 .unindent(),
14741 );
14742 let editors = cx.update_workspace(|workspace, _, cx| {
14743 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
14744 });
14745 cx.update_editor(|_, _, test_editor_cx| {
14746 assert_eq!(
14747 editors.len(),
14748 2,
14749 "After falling back to references search, we open a new editor with the results"
14750 );
14751 let references_fallback_text = editors
14752 .into_iter()
14753 .find(|new_editor| *new_editor != test_editor_cx.entity())
14754 .expect("Should have one non-test editor now")
14755 .read(test_editor_cx)
14756 .text(test_editor_cx);
14757 assert_eq!(
14758 references_fallback_text, "fn one() {\n let mut a = two();\n}",
14759 "Should use the range from the references response and not the GoToDefinition one"
14760 );
14761 });
14762}
14763
14764#[gpui::test]
14765async fn test_find_enclosing_node_with_task(cx: &mut gpui::TestAppContext) {
14766 init_test(cx, |_| {});
14767
14768 let language = Arc::new(Language::new(
14769 LanguageConfig::default(),
14770 Some(tree_sitter_rust::LANGUAGE.into()),
14771 ));
14772
14773 let text = r#"
14774 #[cfg(test)]
14775 mod tests() {
14776 #[test]
14777 fn runnable_1() {
14778 let a = 1;
14779 }
14780
14781 #[test]
14782 fn runnable_2() {
14783 let a = 1;
14784 let b = 2;
14785 }
14786 }
14787 "#
14788 .unindent();
14789
14790 let fs = FakeFs::new(cx.executor());
14791 fs.insert_file("/file.rs", Default::default()).await;
14792
14793 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14794 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14795 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14796 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14797 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
14798
14799 let editor = cx.new_window_entity(|window, cx| {
14800 Editor::new(
14801 EditorMode::Full,
14802 multi_buffer,
14803 Some(project.clone()),
14804 true,
14805 window,
14806 cx,
14807 )
14808 });
14809
14810 editor.update_in(cx, |editor, window, cx| {
14811 editor.tasks.insert(
14812 (buffer.read(cx).remote_id(), 3),
14813 RunnableTasks {
14814 templates: vec![],
14815 offset: MultiBufferOffset(43),
14816 column: 0,
14817 extra_variables: HashMap::default(),
14818 context_range: BufferOffset(43)..BufferOffset(85),
14819 },
14820 );
14821 editor.tasks.insert(
14822 (buffer.read(cx).remote_id(), 8),
14823 RunnableTasks {
14824 templates: vec![],
14825 offset: MultiBufferOffset(86),
14826 column: 0,
14827 extra_variables: HashMap::default(),
14828 context_range: BufferOffset(86)..BufferOffset(191),
14829 },
14830 );
14831
14832 // Test finding task when cursor is inside function body
14833 editor.change_selections(None, window, cx, |s| {
14834 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
14835 });
14836 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
14837 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
14838
14839 // Test finding task when cursor is on function name
14840 editor.change_selections(None, window, cx, |s| {
14841 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
14842 });
14843 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
14844 assert_eq!(row, 8, "Should find task when cursor is on function name");
14845 });
14846}
14847
14848#[gpui::test]
14849async fn test_multi_buffer_folding(cx: &mut gpui::TestAppContext) {
14850 init_test(cx, |_| {});
14851
14852 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
14853 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
14854 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
14855
14856 let fs = FakeFs::new(cx.executor());
14857 fs.insert_tree(
14858 "/a",
14859 json!({
14860 "first.rs": sample_text_1,
14861 "second.rs": sample_text_2,
14862 "third.rs": sample_text_3,
14863 }),
14864 )
14865 .await;
14866 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14867 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14868 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14869 let worktree = project.update(cx, |project, cx| {
14870 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
14871 assert_eq!(worktrees.len(), 1);
14872 worktrees.pop().unwrap()
14873 });
14874 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
14875
14876 let buffer_1 = project
14877 .update(cx, |project, cx| {
14878 project.open_buffer((worktree_id, "first.rs"), cx)
14879 })
14880 .await
14881 .unwrap();
14882 let buffer_2 = project
14883 .update(cx, |project, cx| {
14884 project.open_buffer((worktree_id, "second.rs"), cx)
14885 })
14886 .await
14887 .unwrap();
14888 let buffer_3 = project
14889 .update(cx, |project, cx| {
14890 project.open_buffer((worktree_id, "third.rs"), cx)
14891 })
14892 .await
14893 .unwrap();
14894
14895 let multi_buffer = cx.new(|cx| {
14896 let mut multi_buffer = MultiBuffer::new(ReadWrite);
14897 multi_buffer.push_excerpts(
14898 buffer_1.clone(),
14899 [
14900 ExcerptRange {
14901 context: Point::new(0, 0)..Point::new(3, 0),
14902 primary: None,
14903 },
14904 ExcerptRange {
14905 context: Point::new(5, 0)..Point::new(7, 0),
14906 primary: None,
14907 },
14908 ExcerptRange {
14909 context: Point::new(9, 0)..Point::new(10, 4),
14910 primary: None,
14911 },
14912 ],
14913 cx,
14914 );
14915 multi_buffer.push_excerpts(
14916 buffer_2.clone(),
14917 [
14918 ExcerptRange {
14919 context: Point::new(0, 0)..Point::new(3, 0),
14920 primary: None,
14921 },
14922 ExcerptRange {
14923 context: Point::new(5, 0)..Point::new(7, 0),
14924 primary: None,
14925 },
14926 ExcerptRange {
14927 context: Point::new(9, 0)..Point::new(10, 4),
14928 primary: None,
14929 },
14930 ],
14931 cx,
14932 );
14933 multi_buffer.push_excerpts(
14934 buffer_3.clone(),
14935 [
14936 ExcerptRange {
14937 context: Point::new(0, 0)..Point::new(3, 0),
14938 primary: None,
14939 },
14940 ExcerptRange {
14941 context: Point::new(5, 0)..Point::new(7, 0),
14942 primary: None,
14943 },
14944 ExcerptRange {
14945 context: Point::new(9, 0)..Point::new(10, 4),
14946 primary: None,
14947 },
14948 ],
14949 cx,
14950 );
14951 multi_buffer
14952 });
14953 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
14954 Editor::new(
14955 EditorMode::Full,
14956 multi_buffer,
14957 Some(project.clone()),
14958 true,
14959 window,
14960 cx,
14961 )
14962 });
14963
14964 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";
14965 assert_eq!(
14966 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
14967 full_text,
14968 );
14969
14970 multi_buffer_editor.update(cx, |editor, cx| {
14971 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
14972 });
14973 assert_eq!(
14974 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
14975 "\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",
14976 "After folding the first buffer, its text should not be displayed"
14977 );
14978
14979 multi_buffer_editor.update(cx, |editor, cx| {
14980 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
14981 });
14982 assert_eq!(
14983 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
14984 "\n\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
14985 "After folding the second buffer, its text should not be displayed"
14986 );
14987
14988 multi_buffer_editor.update(cx, |editor, cx| {
14989 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
14990 });
14991 assert_eq!(
14992 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
14993 "\n\n\n\n\n",
14994 "After folding the third buffer, its text should not be displayed"
14995 );
14996
14997 // Emulate selection inside the fold logic, that should work
14998 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14999 editor
15000 .snapshot(window, cx)
15001 .next_line_boundary(Point::new(0, 4));
15002 });
15003
15004 multi_buffer_editor.update(cx, |editor, cx| {
15005 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
15006 });
15007 assert_eq!(
15008 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15009 "\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
15010 "After unfolding the second buffer, its text should be displayed"
15011 );
15012
15013 multi_buffer_editor.update(cx, |editor, cx| {
15014 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
15015 });
15016 assert_eq!(
15017 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15018 "\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",
15019 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
15020 );
15021
15022 multi_buffer_editor.update(cx, |editor, cx| {
15023 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
15024 });
15025 assert_eq!(
15026 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15027 full_text,
15028 "After unfolding the all buffers, all original text should be displayed"
15029 );
15030}
15031
15032#[gpui::test]
15033async fn test_multi_buffer_single_excerpts_folding(cx: &mut gpui::TestAppContext) {
15034 init_test(cx, |_| {});
15035
15036 let sample_text_1 = "1111\n2222\n3333".to_string();
15037 let sample_text_2 = "4444\n5555\n6666".to_string();
15038 let sample_text_3 = "7777\n8888\n9999".to_string();
15039
15040 let fs = FakeFs::new(cx.executor());
15041 fs.insert_tree(
15042 "/a",
15043 json!({
15044 "first.rs": sample_text_1,
15045 "second.rs": sample_text_2,
15046 "third.rs": sample_text_3,
15047 }),
15048 )
15049 .await;
15050 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15051 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15052 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15053 let worktree = project.update(cx, |project, cx| {
15054 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15055 assert_eq!(worktrees.len(), 1);
15056 worktrees.pop().unwrap()
15057 });
15058 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15059
15060 let buffer_1 = project
15061 .update(cx, |project, cx| {
15062 project.open_buffer((worktree_id, "first.rs"), cx)
15063 })
15064 .await
15065 .unwrap();
15066 let buffer_2 = project
15067 .update(cx, |project, cx| {
15068 project.open_buffer((worktree_id, "second.rs"), cx)
15069 })
15070 .await
15071 .unwrap();
15072 let buffer_3 = project
15073 .update(cx, |project, cx| {
15074 project.open_buffer((worktree_id, "third.rs"), cx)
15075 })
15076 .await
15077 .unwrap();
15078
15079 let multi_buffer = cx.new(|cx| {
15080 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15081 multi_buffer.push_excerpts(
15082 buffer_1.clone(),
15083 [ExcerptRange {
15084 context: Point::new(0, 0)..Point::new(3, 0),
15085 primary: None,
15086 }],
15087 cx,
15088 );
15089 multi_buffer.push_excerpts(
15090 buffer_2.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_3.clone(),
15099 [ExcerptRange {
15100 context: Point::new(0, 0)..Point::new(3, 0),
15101 primary: None,
15102 }],
15103 cx,
15104 );
15105 multi_buffer
15106 });
15107
15108 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15109 Editor::new(
15110 EditorMode::Full,
15111 multi_buffer,
15112 Some(project.clone()),
15113 true,
15114 window,
15115 cx,
15116 )
15117 });
15118
15119 let full_text = "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n";
15120 assert_eq!(
15121 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15122 full_text,
15123 );
15124
15125 multi_buffer_editor.update(cx, |editor, cx| {
15126 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
15127 });
15128 assert_eq!(
15129 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15130 "\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n",
15131 "After folding the first buffer, its text should not be displayed"
15132 );
15133
15134 multi_buffer_editor.update(cx, |editor, cx| {
15135 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
15136 });
15137
15138 assert_eq!(
15139 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15140 "\n\n\n\n\n\n\n7777\n8888\n9999\n",
15141 "After folding the second buffer, its text should not be displayed"
15142 );
15143
15144 multi_buffer_editor.update(cx, |editor, cx| {
15145 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
15146 });
15147 assert_eq!(
15148 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15149 "\n\n\n\n\n",
15150 "After folding the third buffer, its text should not be displayed"
15151 );
15152
15153 multi_buffer_editor.update(cx, |editor, cx| {
15154 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
15155 });
15156 assert_eq!(
15157 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15158 "\n\n\n\n\n4444\n5555\n6666\n\n\n",
15159 "After unfolding the second buffer, its text should be displayed"
15160 );
15161
15162 multi_buffer_editor.update(cx, |editor, cx| {
15163 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
15164 });
15165 assert_eq!(
15166 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15167 "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n",
15168 "After unfolding the first buffer, its text should be displayed"
15169 );
15170
15171 multi_buffer_editor.update(cx, |editor, cx| {
15172 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
15173 });
15174 assert_eq!(
15175 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15176 full_text,
15177 "After unfolding all buffers, all original text should be displayed"
15178 );
15179}
15180
15181#[gpui::test]
15182async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppContext) {
15183 init_test(cx, |_| {});
15184
15185 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
15186
15187 let fs = FakeFs::new(cx.executor());
15188 fs.insert_tree(
15189 "/a",
15190 json!({
15191 "main.rs": sample_text,
15192 }),
15193 )
15194 .await;
15195 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15196 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15197 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15198 let worktree = project.update(cx, |project, cx| {
15199 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15200 assert_eq!(worktrees.len(), 1);
15201 worktrees.pop().unwrap()
15202 });
15203 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15204
15205 let buffer_1 = project
15206 .update(cx, |project, cx| {
15207 project.open_buffer((worktree_id, "main.rs"), cx)
15208 })
15209 .await
15210 .unwrap();
15211
15212 let multi_buffer = cx.new(|cx| {
15213 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15214 multi_buffer.push_excerpts(
15215 buffer_1.clone(),
15216 [ExcerptRange {
15217 context: Point::new(0, 0)
15218 ..Point::new(
15219 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
15220 0,
15221 ),
15222 primary: None,
15223 }],
15224 cx,
15225 );
15226 multi_buffer
15227 });
15228 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15229 Editor::new(
15230 EditorMode::Full,
15231 multi_buffer,
15232 Some(project.clone()),
15233 true,
15234 window,
15235 cx,
15236 )
15237 });
15238
15239 let selection_range = Point::new(1, 0)..Point::new(2, 0);
15240 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15241 enum TestHighlight {}
15242 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
15243 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
15244 editor.highlight_text::<TestHighlight>(
15245 vec![highlight_range.clone()],
15246 HighlightStyle::color(Hsla::green()),
15247 cx,
15248 );
15249 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
15250 });
15251
15252 let full_text = format!("\n\n\n{sample_text}\n");
15253 assert_eq!(
15254 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15255 full_text,
15256 );
15257}
15258
15259#[gpui::test]
15260async fn test_inline_completion_text(cx: &mut TestAppContext) {
15261 init_test(cx, |_| {});
15262
15263 // Simple insertion
15264 assert_highlighted_edits(
15265 "Hello, world!",
15266 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
15267 true,
15268 cx,
15269 |highlighted_edits, cx| {
15270 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
15271 assert_eq!(highlighted_edits.highlights.len(), 1);
15272 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
15273 assert_eq!(
15274 highlighted_edits.highlights[0].1.background_color,
15275 Some(cx.theme().status().created_background)
15276 );
15277 },
15278 )
15279 .await;
15280
15281 // Replacement
15282 assert_highlighted_edits(
15283 "This is a test.",
15284 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
15285 false,
15286 cx,
15287 |highlighted_edits, cx| {
15288 assert_eq!(highlighted_edits.text, "That is a test.");
15289 assert_eq!(highlighted_edits.highlights.len(), 1);
15290 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
15291 assert_eq!(
15292 highlighted_edits.highlights[0].1.background_color,
15293 Some(cx.theme().status().created_background)
15294 );
15295 },
15296 )
15297 .await;
15298
15299 // Multiple edits
15300 assert_highlighted_edits(
15301 "Hello, world!",
15302 vec![
15303 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
15304 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
15305 ],
15306 false,
15307 cx,
15308 |highlighted_edits, cx| {
15309 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
15310 assert_eq!(highlighted_edits.highlights.len(), 2);
15311 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
15312 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
15313 assert_eq!(
15314 highlighted_edits.highlights[0].1.background_color,
15315 Some(cx.theme().status().created_background)
15316 );
15317 assert_eq!(
15318 highlighted_edits.highlights[1].1.background_color,
15319 Some(cx.theme().status().created_background)
15320 );
15321 },
15322 )
15323 .await;
15324
15325 // Multiple lines with edits
15326 assert_highlighted_edits(
15327 "First line\nSecond line\nThird line\nFourth line",
15328 vec![
15329 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
15330 (
15331 Point::new(2, 0)..Point::new(2, 10),
15332 "New third line".to_string(),
15333 ),
15334 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
15335 ],
15336 false,
15337 cx,
15338 |highlighted_edits, cx| {
15339 assert_eq!(
15340 highlighted_edits.text,
15341 "Second modified\nNew third line\nFourth updated line"
15342 );
15343 assert_eq!(highlighted_edits.highlights.len(), 3);
15344 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
15345 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
15346 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
15347 for highlight in &highlighted_edits.highlights {
15348 assert_eq!(
15349 highlight.1.background_color,
15350 Some(cx.theme().status().created_background)
15351 );
15352 }
15353 },
15354 )
15355 .await;
15356}
15357
15358#[gpui::test]
15359async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
15360 init_test(cx, |_| {});
15361
15362 // Deletion
15363 assert_highlighted_edits(
15364 "Hello, world!",
15365 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
15366 true,
15367 cx,
15368 |highlighted_edits, cx| {
15369 assert_eq!(highlighted_edits.text, "Hello, world!");
15370 assert_eq!(highlighted_edits.highlights.len(), 1);
15371 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
15372 assert_eq!(
15373 highlighted_edits.highlights[0].1.background_color,
15374 Some(cx.theme().status().deleted_background)
15375 );
15376 },
15377 )
15378 .await;
15379
15380 // Insertion
15381 assert_highlighted_edits(
15382 "Hello, world!",
15383 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
15384 true,
15385 cx,
15386 |highlighted_edits, cx| {
15387 assert_eq!(highlighted_edits.highlights.len(), 1);
15388 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
15389 assert_eq!(
15390 highlighted_edits.highlights[0].1.background_color,
15391 Some(cx.theme().status().created_background)
15392 );
15393 },
15394 )
15395 .await;
15396}
15397
15398async fn assert_highlighted_edits(
15399 text: &str,
15400 edits: Vec<(Range<Point>, String)>,
15401 include_deletions: bool,
15402 cx: &mut TestAppContext,
15403 assertion_fn: impl Fn(HighlightedEdits, &App),
15404) {
15405 let window = cx.add_window(|window, cx| {
15406 let buffer = MultiBuffer::build_simple(text, cx);
15407 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
15408 });
15409 let cx = &mut VisualTestContext::from_window(*window, cx);
15410
15411 let (buffer, snapshot) = window
15412 .update(cx, |editor, _window, cx| {
15413 (
15414 editor.buffer().clone(),
15415 editor.buffer().read(cx).snapshot(cx),
15416 )
15417 })
15418 .unwrap();
15419
15420 let edits = edits
15421 .into_iter()
15422 .map(|(range, edit)| {
15423 (
15424 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
15425 edit,
15426 )
15427 })
15428 .collect::<Vec<_>>();
15429
15430 let text_anchor_edits = edits
15431 .clone()
15432 .into_iter()
15433 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
15434 .collect::<Vec<_>>();
15435
15436 let edit_preview = window
15437 .update(cx, |_, _window, cx| {
15438 buffer
15439 .read(cx)
15440 .as_singleton()
15441 .unwrap()
15442 .read(cx)
15443 .preview_edits(text_anchor_edits.into(), cx)
15444 })
15445 .unwrap()
15446 .await;
15447
15448 cx.update(|_window, cx| {
15449 let highlighted_edits = inline_completion_edit_text(
15450 &snapshot.as_singleton().unwrap().2,
15451 &edits,
15452 &edit_preview,
15453 include_deletions,
15454 cx,
15455 )
15456 .expect("Missing highlighted edits");
15457 assertion_fn(highlighted_edits, cx)
15458 });
15459}
15460
15461#[gpui::test]
15462async fn test_rename_with_duplicate_edits(cx: &mut gpui::TestAppContext) {
15463 init_test(cx, |_| {});
15464 let capabilities = lsp::ServerCapabilities {
15465 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
15466 prepare_provider: Some(true),
15467 work_done_progress_options: Default::default(),
15468 })),
15469 ..Default::default()
15470 };
15471 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
15472
15473 cx.set_state(indoc! {"
15474 struct Fˇoo {}
15475 "});
15476
15477 cx.update_editor(|editor, _, cx| {
15478 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
15479 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
15480 editor.highlight_background::<DocumentHighlightRead>(
15481 &[highlight_range],
15482 |c| c.editor_document_highlight_read_background,
15483 cx,
15484 );
15485 });
15486
15487 let mut prepare_rename_handler =
15488 cx.handle_request::<lsp::request::PrepareRenameRequest, _, _>(move |_, _, _| async move {
15489 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
15490 start: lsp::Position {
15491 line: 0,
15492 character: 7,
15493 },
15494 end: lsp::Position {
15495 line: 0,
15496 character: 10,
15497 },
15498 })))
15499 });
15500 let prepare_rename_task = cx
15501 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
15502 .expect("Prepare rename was not started");
15503 prepare_rename_handler.next().await.unwrap();
15504 prepare_rename_task.await.expect("Prepare rename failed");
15505
15506 let mut rename_handler =
15507 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
15508 let edit = lsp::TextEdit {
15509 range: lsp::Range {
15510 start: lsp::Position {
15511 line: 0,
15512 character: 7,
15513 },
15514 end: lsp::Position {
15515 line: 0,
15516 character: 10,
15517 },
15518 },
15519 new_text: "FooRenamed".to_string(),
15520 };
15521 Ok(Some(lsp::WorkspaceEdit::new(
15522 // Specify the same edit twice
15523 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
15524 )))
15525 });
15526 let rename_task = cx
15527 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
15528 .expect("Confirm rename was not started");
15529 rename_handler.next().await.unwrap();
15530 rename_task.await.expect("Confirm rename failed");
15531 cx.run_until_parked();
15532
15533 // Despite two edits, only one is actually applied as those are identical
15534 cx.assert_editor_state(indoc! {"
15535 struct FooRenamedˇ {}
15536 "});
15537}
15538
15539#[gpui::test]
15540async fn test_rename_without_prepare(cx: &mut gpui::TestAppContext) {
15541 init_test(cx, |_| {});
15542 // These capabilities indicate that the server does not support prepare rename.
15543 let capabilities = lsp::ServerCapabilities {
15544 rename_provider: Some(lsp::OneOf::Left(true)),
15545 ..Default::default()
15546 };
15547 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
15548
15549 cx.set_state(indoc! {"
15550 struct Fˇoo {}
15551 "});
15552
15553 cx.update_editor(|editor, _window, cx| {
15554 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
15555 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
15556 editor.highlight_background::<DocumentHighlightRead>(
15557 &[highlight_range],
15558 |c| c.editor_document_highlight_read_background,
15559 cx,
15560 );
15561 });
15562
15563 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
15564 .expect("Prepare rename was not started")
15565 .await
15566 .expect("Prepare rename failed");
15567
15568 let mut rename_handler =
15569 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
15570 let edit = lsp::TextEdit {
15571 range: lsp::Range {
15572 start: lsp::Position {
15573 line: 0,
15574 character: 7,
15575 },
15576 end: lsp::Position {
15577 line: 0,
15578 character: 10,
15579 },
15580 },
15581 new_text: "FooRenamed".to_string(),
15582 };
15583 Ok(Some(lsp::WorkspaceEdit::new(
15584 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
15585 )))
15586 });
15587 let rename_task = cx
15588 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
15589 .expect("Confirm rename was not started");
15590 rename_handler.next().await.unwrap();
15591 rename_task.await.expect("Confirm rename failed");
15592 cx.run_until_parked();
15593
15594 // Correct range is renamed, as `surrounding_word` is used to find it.
15595 cx.assert_editor_state(indoc! {"
15596 struct FooRenamedˇ {}
15597 "});
15598}
15599
15600fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
15601 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
15602 point..point
15603}
15604
15605fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
15606 let (text, ranges) = marked_text_ranges(marked_text, true);
15607 assert_eq!(editor.text(cx), text);
15608 assert_eq!(
15609 editor.selections.ranges(cx),
15610 ranges,
15611 "Assert selections are {}",
15612 marked_text
15613 );
15614}
15615
15616pub fn handle_signature_help_request(
15617 cx: &mut EditorLspTestContext,
15618 mocked_response: lsp::SignatureHelp,
15619) -> impl Future<Output = ()> {
15620 let mut request =
15621 cx.handle_request::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
15622 let mocked_response = mocked_response.clone();
15623 async move { Ok(Some(mocked_response)) }
15624 });
15625
15626 async move {
15627 request.next().await;
15628 }
15629}
15630
15631/// Handle completion request passing a marked string specifying where the completion
15632/// should be triggered from using '|' character, what range should be replaced, and what completions
15633/// should be returned using '<' and '>' to delimit the range
15634pub fn handle_completion_request(
15635 cx: &mut EditorLspTestContext,
15636 marked_string: &str,
15637 completions: Vec<&'static str>,
15638 counter: Arc<AtomicUsize>,
15639) -> impl Future<Output = ()> {
15640 let complete_from_marker: TextRangeMarker = '|'.into();
15641 let replace_range_marker: TextRangeMarker = ('<', '>').into();
15642 let (_, mut marked_ranges) = marked_text_ranges_by(
15643 marked_string,
15644 vec![complete_from_marker.clone(), replace_range_marker.clone()],
15645 );
15646
15647 let complete_from_position =
15648 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
15649 let replace_range =
15650 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
15651
15652 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
15653 let completions = completions.clone();
15654 counter.fetch_add(1, atomic::Ordering::Release);
15655 async move {
15656 assert_eq!(params.text_document_position.text_document.uri, url.clone());
15657 assert_eq!(
15658 params.text_document_position.position,
15659 complete_from_position
15660 );
15661 Ok(Some(lsp::CompletionResponse::Array(
15662 completions
15663 .iter()
15664 .map(|completion_text| lsp::CompletionItem {
15665 label: completion_text.to_string(),
15666 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15667 range: replace_range,
15668 new_text: completion_text.to_string(),
15669 })),
15670 ..Default::default()
15671 })
15672 .collect(),
15673 )))
15674 }
15675 });
15676
15677 async move {
15678 request.next().await;
15679 }
15680}
15681
15682fn handle_resolve_completion_request(
15683 cx: &mut EditorLspTestContext,
15684 edits: Option<Vec<(&'static str, &'static str)>>,
15685) -> impl Future<Output = ()> {
15686 let edits = edits.map(|edits| {
15687 edits
15688 .iter()
15689 .map(|(marked_string, new_text)| {
15690 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
15691 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
15692 lsp::TextEdit::new(replace_range, new_text.to_string())
15693 })
15694 .collect::<Vec<_>>()
15695 });
15696
15697 let mut request =
15698 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15699 let edits = edits.clone();
15700 async move {
15701 Ok(lsp::CompletionItem {
15702 additional_text_edits: edits,
15703 ..Default::default()
15704 })
15705 }
15706 });
15707
15708 async move {
15709 request.next().await;
15710 }
15711}
15712
15713pub(crate) fn update_test_language_settings(
15714 cx: &mut TestAppContext,
15715 f: impl Fn(&mut AllLanguageSettingsContent),
15716) {
15717 cx.update(|cx| {
15718 SettingsStore::update_global(cx, |store, cx| {
15719 store.update_user_settings::<AllLanguageSettings>(cx, f);
15720 });
15721 });
15722}
15723
15724pub(crate) fn update_test_project_settings(
15725 cx: &mut TestAppContext,
15726 f: impl Fn(&mut ProjectSettings),
15727) {
15728 cx.update(|cx| {
15729 SettingsStore::update_global(cx, |store, cx| {
15730 store.update_user_settings::<ProjectSettings>(cx, f);
15731 });
15732 });
15733}
15734
15735pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
15736 cx.update(|cx| {
15737 assets::Assets.load_test_fonts(cx);
15738 let store = SettingsStore::test(cx);
15739 cx.set_global(store);
15740 theme::init(theme::LoadThemes::JustBase, cx);
15741 release_channel::init(SemanticVersion::default(), cx);
15742 client::init_settings(cx);
15743 language::init(cx);
15744 Project::init_settings(cx);
15745 workspace::init_settings(cx);
15746 crate::init(cx);
15747 });
15748
15749 update_test_language_settings(cx, f);
15750}
15751
15752#[track_caller]
15753fn assert_hunk_revert(
15754 not_reverted_text_with_selections: &str,
15755 expected_hunk_statuses_before: Vec<DiffHunkStatus>,
15756 expected_reverted_text_with_selections: &str,
15757 base_text: &str,
15758 cx: &mut EditorLspTestContext,
15759) {
15760 cx.set_state(not_reverted_text_with_selections);
15761 cx.set_diff_base(base_text);
15762 cx.executor().run_until_parked();
15763
15764 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
15765 let snapshot = editor.snapshot(window, cx);
15766 let reverted_hunk_statuses = snapshot
15767 .buffer_snapshot
15768 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
15769 .map(|hunk| hunk.status())
15770 .collect::<Vec<_>>();
15771
15772 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
15773 reverted_hunk_statuses
15774 });
15775 cx.executor().run_until_parked();
15776 cx.assert_editor_state(expected_reverted_text_with_selections);
15777 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
15778}