1use super::*;
2use crate::{
3 scroll::scroll_amount::ScrollAmount,
4 test::{
5 assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
6 editor_test_context::EditorTestContext, select_ranges,
7 },
8 JoinLines,
9};
10use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
11use futures::StreamExt;
12use gpui::{
13 div, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
14 WindowBounds, WindowOptions,
15};
16use indoc::indoc;
17use language::{
18 language_settings::{
19 AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent, PrettierSettings,
20 },
21 BracketPairConfig,
22 Capability::ReadWrite,
23 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
24 Override, Point,
25};
26use language_settings::{Formatter, FormatterList, IndentGuideSettings};
27use multi_buffer::{IndentGuide, PathKey};
28use parking_lot::Mutex;
29use pretty_assertions::{assert_eq, assert_ne};
30use project::project_settings::{LspSettings, ProjectSettings};
31use project::FakeFs;
32use serde_json::{self, json};
33use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
34use std::{
35 iter,
36 sync::atomic::{self, AtomicUsize},
37};
38use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
39use text::ToPoint as _;
40use unindent::Unindent;
41use util::{
42 assert_set_eq, path,
43 test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
44 uri,
45};
46use workspace::{
47 item::{FollowEvent, FollowableItem, Item, ItemHandle},
48 NavigationEntry, ViewId,
49};
50
51#[gpui::test]
52fn test_edit_events(cx: &mut TestAppContext) {
53 init_test(cx, |_| {});
54
55 let buffer = cx.new(|cx| {
56 let mut buffer = language::Buffer::local("123456", cx);
57 buffer.set_group_interval(Duration::from_secs(1));
58 buffer
59 });
60
61 let events = Rc::new(RefCell::new(Vec::new()));
62 let editor1 = cx.add_window({
63 let events = events.clone();
64 |window, cx| {
65 let entity = cx.entity().clone();
66 cx.subscribe_in(
67 &entity,
68 window,
69 move |_, _, event: &EditorEvent, _, _| match event {
70 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
71 EditorEvent::BufferEdited => {
72 events.borrow_mut().push(("editor1", "buffer edited"))
73 }
74 _ => {}
75 },
76 )
77 .detach();
78 Editor::for_buffer(buffer.clone(), None, window, cx)
79 }
80 });
81
82 let editor2 = cx.add_window({
83 let events = events.clone();
84 |window, cx| {
85 cx.subscribe_in(
86 &cx.entity().clone(),
87 window,
88 move |_, _, event: &EditorEvent, _, _| match event {
89 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
90 EditorEvent::BufferEdited => {
91 events.borrow_mut().push(("editor2", "buffer edited"))
92 }
93 _ => {}
94 },
95 )
96 .detach();
97 Editor::for_buffer(buffer.clone(), None, window, cx)
98 }
99 });
100
101 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
102
103 // Mutating editor 1 will emit an `Edited` event only for that editor.
104 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
105 assert_eq!(
106 mem::take(&mut *events.borrow_mut()),
107 [
108 ("editor1", "edited"),
109 ("editor1", "buffer edited"),
110 ("editor2", "buffer edited"),
111 ]
112 );
113
114 // Mutating editor 2 will emit an `Edited` event only for that editor.
115 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
116 assert_eq!(
117 mem::take(&mut *events.borrow_mut()),
118 [
119 ("editor2", "edited"),
120 ("editor1", "buffer edited"),
121 ("editor2", "buffer edited"),
122 ]
123 );
124
125 // Undoing on editor 1 will emit an `Edited` event only for that editor.
126 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
127 assert_eq!(
128 mem::take(&mut *events.borrow_mut()),
129 [
130 ("editor1", "edited"),
131 ("editor1", "buffer edited"),
132 ("editor2", "buffer edited"),
133 ]
134 );
135
136 // Redoing on editor 1 will emit an `Edited` event only for that editor.
137 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
138 assert_eq!(
139 mem::take(&mut *events.borrow_mut()),
140 [
141 ("editor1", "edited"),
142 ("editor1", "buffer edited"),
143 ("editor2", "buffer edited"),
144 ]
145 );
146
147 // Undoing on editor 2 will emit an `Edited` event only for that editor.
148 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
149 assert_eq!(
150 mem::take(&mut *events.borrow_mut()),
151 [
152 ("editor2", "edited"),
153 ("editor1", "buffer edited"),
154 ("editor2", "buffer edited"),
155 ]
156 );
157
158 // Redoing on editor 2 will emit an `Edited` event only for that editor.
159 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
160 assert_eq!(
161 mem::take(&mut *events.borrow_mut()),
162 [
163 ("editor2", "edited"),
164 ("editor1", "buffer edited"),
165 ("editor2", "buffer edited"),
166 ]
167 );
168
169 // No event is emitted when the mutation is a no-op.
170 _ = editor2.update(cx, |editor, window, cx| {
171 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
172
173 editor.backspace(&Backspace, window, cx);
174 });
175 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
176}
177
178#[gpui::test]
179fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
180 init_test(cx, |_| {});
181
182 let mut now = Instant::now();
183 let group_interval = Duration::from_millis(1);
184 let buffer = cx.new(|cx| {
185 let mut buf = language::Buffer::local("123456", cx);
186 buf.set_group_interval(group_interval);
187 buf
188 });
189 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
190 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
191
192 _ = editor.update(cx, |editor, window, cx| {
193 editor.start_transaction_at(now, window, cx);
194 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
195
196 editor.insert("cd", window, cx);
197 editor.end_transaction_at(now, cx);
198 assert_eq!(editor.text(cx), "12cd56");
199 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
200
201 editor.start_transaction_at(now, window, cx);
202 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
203 editor.insert("e", window, cx);
204 editor.end_transaction_at(now, cx);
205 assert_eq!(editor.text(cx), "12cde6");
206 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
207
208 now += group_interval + Duration::from_millis(1);
209 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
210
211 // Simulate an edit in another editor
212 buffer.update(cx, |buffer, cx| {
213 buffer.start_transaction_at(now, cx);
214 buffer.edit([(0..1, "a")], None, cx);
215 buffer.edit([(1..1, "b")], None, cx);
216 buffer.end_transaction_at(now, cx);
217 });
218
219 assert_eq!(editor.text(cx), "ab2cde6");
220 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
221
222 // Last transaction happened past the group interval in a different editor.
223 // Undo it individually and don't restore selections.
224 editor.undo(&Undo, window, cx);
225 assert_eq!(editor.text(cx), "12cde6");
226 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
227
228 // First two transactions happened within the group interval in this editor.
229 // Undo them together and restore selections.
230 editor.undo(&Undo, window, cx);
231 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
232 assert_eq!(editor.text(cx), "123456");
233 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
234
235 // Redo the first two transactions together.
236 editor.redo(&Redo, window, cx);
237 assert_eq!(editor.text(cx), "12cde6");
238 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
239
240 // Redo the last transaction on its own.
241 editor.redo(&Redo, window, cx);
242 assert_eq!(editor.text(cx), "ab2cde6");
243 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
244
245 // Test empty transactions.
246 editor.start_transaction_at(now, window, cx);
247 editor.end_transaction_at(now, cx);
248 editor.undo(&Undo, window, cx);
249 assert_eq!(editor.text(cx), "12cde6");
250 });
251}
252
253#[gpui::test]
254fn test_ime_composition(cx: &mut TestAppContext) {
255 init_test(cx, |_| {});
256
257 let buffer = cx.new(|cx| {
258 let mut buffer = language::Buffer::local("abcde", cx);
259 // Ensure automatic grouping doesn't occur.
260 buffer.set_group_interval(Duration::ZERO);
261 buffer
262 });
263
264 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
265 cx.add_window(|window, cx| {
266 let mut editor = build_editor(buffer.clone(), window, cx);
267
268 // Start a new IME composition.
269 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
270 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
271 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
272 assert_eq!(editor.text(cx), "äbcde");
273 assert_eq!(
274 editor.marked_text_ranges(cx),
275 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
276 );
277
278 // Finalize IME composition.
279 editor.replace_text_in_range(None, "ā", window, cx);
280 assert_eq!(editor.text(cx), "ābcde");
281 assert_eq!(editor.marked_text_ranges(cx), None);
282
283 // IME composition edits are grouped and are undone/redone at once.
284 editor.undo(&Default::default(), window, cx);
285 assert_eq!(editor.text(cx), "abcde");
286 assert_eq!(editor.marked_text_ranges(cx), None);
287 editor.redo(&Default::default(), window, cx);
288 assert_eq!(editor.text(cx), "ābcde");
289 assert_eq!(editor.marked_text_ranges(cx), None);
290
291 // Start a new IME composition.
292 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
293 assert_eq!(
294 editor.marked_text_ranges(cx),
295 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
296 );
297
298 // Undoing during an IME composition cancels it.
299 editor.undo(&Default::default(), window, cx);
300 assert_eq!(editor.text(cx), "ābcde");
301 assert_eq!(editor.marked_text_ranges(cx), None);
302
303 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
304 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
305 assert_eq!(editor.text(cx), "ābcdè");
306 assert_eq!(
307 editor.marked_text_ranges(cx),
308 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
309 );
310
311 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
312 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
313 assert_eq!(editor.text(cx), "ābcdę");
314 assert_eq!(editor.marked_text_ranges(cx), None);
315
316 // Start a new IME composition with multiple cursors.
317 editor.change_selections(None, window, cx, |s| {
318 s.select_ranges([
319 OffsetUtf16(1)..OffsetUtf16(1),
320 OffsetUtf16(3)..OffsetUtf16(3),
321 OffsetUtf16(5)..OffsetUtf16(5),
322 ])
323 });
324 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
325 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
326 assert_eq!(
327 editor.marked_text_ranges(cx),
328 Some(vec![
329 OffsetUtf16(0)..OffsetUtf16(3),
330 OffsetUtf16(4)..OffsetUtf16(7),
331 OffsetUtf16(8)..OffsetUtf16(11)
332 ])
333 );
334
335 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
336 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
337 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
338 assert_eq!(
339 editor.marked_text_ranges(cx),
340 Some(vec![
341 OffsetUtf16(1)..OffsetUtf16(2),
342 OffsetUtf16(5)..OffsetUtf16(6),
343 OffsetUtf16(9)..OffsetUtf16(10)
344 ])
345 );
346
347 // Finalize IME composition with multiple cursors.
348 editor.replace_text_in_range(Some(9..10), "2", window, cx);
349 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
350 assert_eq!(editor.marked_text_ranges(cx), None);
351
352 editor
353 });
354}
355
356#[gpui::test]
357fn test_selection_with_mouse(cx: &mut TestAppContext) {
358 init_test(cx, |_| {});
359
360 let editor = cx.add_window(|window, cx| {
361 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
362 build_editor(buffer, window, cx)
363 });
364
365 _ = editor.update(cx, |editor, window, cx| {
366 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
367 });
368 assert_eq!(
369 editor
370 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
371 .unwrap(),
372 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
373 );
374
375 _ = editor.update(cx, |editor, window, cx| {
376 editor.update_selection(
377 DisplayPoint::new(DisplayRow(3), 3),
378 0,
379 gpui::Point::<f32>::default(),
380 window,
381 cx,
382 );
383 });
384
385 assert_eq!(
386 editor
387 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
388 .unwrap(),
389 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
390 );
391
392 _ = editor.update(cx, |editor, window, cx| {
393 editor.update_selection(
394 DisplayPoint::new(DisplayRow(1), 1),
395 0,
396 gpui::Point::<f32>::default(),
397 window,
398 cx,
399 );
400 });
401
402 assert_eq!(
403 editor
404 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
405 .unwrap(),
406 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
407 );
408
409 _ = editor.update(cx, |editor, window, cx| {
410 editor.end_selection(window, cx);
411 editor.update_selection(
412 DisplayPoint::new(DisplayRow(3), 3),
413 0,
414 gpui::Point::<f32>::default(),
415 window,
416 cx,
417 );
418 });
419
420 assert_eq!(
421 editor
422 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
423 .unwrap(),
424 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
425 );
426
427 _ = editor.update(cx, |editor, window, cx| {
428 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
429 editor.update_selection(
430 DisplayPoint::new(DisplayRow(0), 0),
431 0,
432 gpui::Point::<f32>::default(),
433 window,
434 cx,
435 );
436 });
437
438 assert_eq!(
439 editor
440 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
441 .unwrap(),
442 [
443 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
444 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
445 ]
446 );
447
448 _ = editor.update(cx, |editor, window, cx| {
449 editor.end_selection(window, cx);
450 });
451
452 assert_eq!(
453 editor
454 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
455 .unwrap(),
456 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
457 );
458}
459
460#[gpui::test]
461fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
462 init_test(cx, |_| {});
463
464 let editor = cx.add_window(|window, cx| {
465 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
466 build_editor(buffer, window, cx)
467 });
468
469 _ = editor.update(cx, |editor, window, cx| {
470 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
471 });
472
473 _ = editor.update(cx, |editor, window, cx| {
474 editor.end_selection(window, cx);
475 });
476
477 _ = editor.update(cx, |editor, window, cx| {
478 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
479 });
480
481 _ = editor.update(cx, |editor, window, cx| {
482 editor.end_selection(window, cx);
483 });
484
485 assert_eq!(
486 editor
487 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
488 .unwrap(),
489 [
490 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
491 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
492 ]
493 );
494
495 _ = editor.update(cx, |editor, window, cx| {
496 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
497 });
498
499 _ = editor.update(cx, |editor, window, cx| {
500 editor.end_selection(window, cx);
501 });
502
503 assert_eq!(
504 editor
505 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
506 .unwrap(),
507 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
508 );
509}
510
511#[gpui::test]
512fn test_canceling_pending_selection(cx: &mut TestAppContext) {
513 init_test(cx, |_| {});
514
515 let editor = cx.add_window(|window, cx| {
516 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
517 build_editor(buffer, window, cx)
518 });
519
520 _ = editor.update(cx, |editor, window, cx| {
521 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
522 assert_eq!(
523 editor.selections.display_ranges(cx),
524 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
525 );
526 });
527
528 _ = editor.update(cx, |editor, window, cx| {
529 editor.update_selection(
530 DisplayPoint::new(DisplayRow(3), 3),
531 0,
532 gpui::Point::<f32>::default(),
533 window,
534 cx,
535 );
536 assert_eq!(
537 editor.selections.display_ranges(cx),
538 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
539 );
540 });
541
542 _ = editor.update(cx, |editor, window, cx| {
543 editor.cancel(&Cancel, window, cx);
544 editor.update_selection(
545 DisplayPoint::new(DisplayRow(1), 1),
546 0,
547 gpui::Point::<f32>::default(),
548 window,
549 cx,
550 );
551 assert_eq!(
552 editor.selections.display_ranges(cx),
553 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
554 );
555 });
556}
557
558#[gpui::test]
559fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
560 init_test(cx, |_| {});
561
562 let editor = cx.add_window(|window, cx| {
563 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
564 build_editor(buffer, window, cx)
565 });
566
567 _ = editor.update(cx, |editor, window, cx| {
568 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
569 assert_eq!(
570 editor.selections.display_ranges(cx),
571 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
572 );
573
574 editor.move_down(&Default::default(), window, cx);
575 assert_eq!(
576 editor.selections.display_ranges(cx),
577 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
578 );
579
580 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
581 assert_eq!(
582 editor.selections.display_ranges(cx),
583 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
584 );
585
586 editor.move_up(&Default::default(), window, cx);
587 assert_eq!(
588 editor.selections.display_ranges(cx),
589 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
590 );
591 });
592}
593
594#[gpui::test]
595fn test_clone(cx: &mut TestAppContext) {
596 init_test(cx, |_| {});
597
598 let (text, selection_ranges) = marked_text_ranges(
599 indoc! {"
600 one
601 two
602 threeˇ
603 four
604 fiveˇ
605 "},
606 true,
607 );
608
609 let editor = cx.add_window(|window, cx| {
610 let buffer = MultiBuffer::build_simple(&text, cx);
611 build_editor(buffer, window, cx)
612 });
613
614 _ = editor.update(cx, |editor, window, cx| {
615 editor.change_selections(None, window, cx, |s| {
616 s.select_ranges(selection_ranges.clone())
617 });
618 editor.fold_creases(
619 vec![
620 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
621 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
622 ],
623 true,
624 window,
625 cx,
626 );
627 });
628
629 let cloned_editor = editor
630 .update(cx, |editor, _, cx| {
631 cx.open_window(Default::default(), |window, cx| {
632 cx.new(|cx| editor.clone(window, cx))
633 })
634 })
635 .unwrap()
636 .unwrap();
637
638 let snapshot = editor
639 .update(cx, |e, window, cx| e.snapshot(window, cx))
640 .unwrap();
641 let cloned_snapshot = cloned_editor
642 .update(cx, |e, window, cx| e.snapshot(window, cx))
643 .unwrap();
644
645 assert_eq!(
646 cloned_editor
647 .update(cx, |e, _, cx| e.display_text(cx))
648 .unwrap(),
649 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
650 );
651 assert_eq!(
652 cloned_snapshot
653 .folds_in_range(0..text.len())
654 .collect::<Vec<_>>(),
655 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
656 );
657 assert_set_eq!(
658 cloned_editor
659 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
660 .unwrap(),
661 editor
662 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
663 .unwrap()
664 );
665 assert_set_eq!(
666 cloned_editor
667 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
668 .unwrap(),
669 editor
670 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
671 .unwrap()
672 );
673}
674
675#[gpui::test]
676async fn test_navigation_history(cx: &mut TestAppContext) {
677 init_test(cx, |_| {});
678
679 use workspace::item::Item;
680
681 let fs = FakeFs::new(cx.executor());
682 let project = Project::test(fs, [], cx).await;
683 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
684 let pane = workspace
685 .update(cx, |workspace, _, _| workspace.active_pane().clone())
686 .unwrap();
687
688 _ = workspace.update(cx, |_v, window, cx| {
689 cx.new(|cx| {
690 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
691 let mut editor = build_editor(buffer.clone(), window, cx);
692 let handle = cx.entity();
693 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
694
695 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
696 editor.nav_history.as_mut().unwrap().pop_backward(cx)
697 }
698
699 // Move the cursor a small distance.
700 // Nothing is added to the navigation history.
701 editor.change_selections(None, window, cx, |s| {
702 s.select_display_ranges([
703 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
704 ])
705 });
706 editor.change_selections(None, window, cx, |s| {
707 s.select_display_ranges([
708 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
709 ])
710 });
711 assert!(pop_history(&mut editor, cx).is_none());
712
713 // Move the cursor a large distance.
714 // The history can jump back to the previous position.
715 editor.change_selections(None, window, cx, |s| {
716 s.select_display_ranges([
717 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
718 ])
719 });
720 let nav_entry = pop_history(&mut editor, cx).unwrap();
721 editor.navigate(nav_entry.data.unwrap(), window, cx);
722 assert_eq!(nav_entry.item.id(), cx.entity_id());
723 assert_eq!(
724 editor.selections.display_ranges(cx),
725 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
726 );
727 assert!(pop_history(&mut editor, cx).is_none());
728
729 // Move the cursor a small distance via the mouse.
730 // Nothing is added to the navigation history.
731 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
732 editor.end_selection(window, cx);
733 assert_eq!(
734 editor.selections.display_ranges(cx),
735 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
736 );
737 assert!(pop_history(&mut editor, cx).is_none());
738
739 // Move the cursor a large distance via the mouse.
740 // The history can jump back to the previous position.
741 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
742 editor.end_selection(window, cx);
743 assert_eq!(
744 editor.selections.display_ranges(cx),
745 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
746 );
747 let nav_entry = pop_history(&mut editor, cx).unwrap();
748 editor.navigate(nav_entry.data.unwrap(), window, cx);
749 assert_eq!(nav_entry.item.id(), cx.entity_id());
750 assert_eq!(
751 editor.selections.display_ranges(cx),
752 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
753 );
754 assert!(pop_history(&mut editor, cx).is_none());
755
756 // Set scroll position to check later
757 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
758 let original_scroll_position = editor.scroll_manager.anchor();
759
760 // Jump to the end of the document and adjust scroll
761 editor.move_to_end(&MoveToEnd, window, cx);
762 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
763 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
764
765 let nav_entry = pop_history(&mut editor, cx).unwrap();
766 editor.navigate(nav_entry.data.unwrap(), window, cx);
767 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
768
769 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
770 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
771 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
772 let invalid_point = Point::new(9999, 0);
773 editor.navigate(
774 Box::new(NavigationData {
775 cursor_anchor: invalid_anchor,
776 cursor_position: invalid_point,
777 scroll_anchor: ScrollAnchor {
778 anchor: invalid_anchor,
779 offset: Default::default(),
780 },
781 scroll_top_row: invalid_point.row,
782 }),
783 window,
784 cx,
785 );
786 assert_eq!(
787 editor.selections.display_ranges(cx),
788 &[editor.max_point(cx)..editor.max_point(cx)]
789 );
790 assert_eq!(
791 editor.scroll_position(cx),
792 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
793 );
794
795 editor
796 })
797 });
798}
799
800#[gpui::test]
801fn test_cancel(cx: &mut TestAppContext) {
802 init_test(cx, |_| {});
803
804 let editor = cx.add_window(|window, cx| {
805 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
806 build_editor(buffer, window, cx)
807 });
808
809 _ = editor.update(cx, |editor, window, cx| {
810 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
811 editor.update_selection(
812 DisplayPoint::new(DisplayRow(1), 1),
813 0,
814 gpui::Point::<f32>::default(),
815 window,
816 cx,
817 );
818 editor.end_selection(window, cx);
819
820 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
821 editor.update_selection(
822 DisplayPoint::new(DisplayRow(0), 3),
823 0,
824 gpui::Point::<f32>::default(),
825 window,
826 cx,
827 );
828 editor.end_selection(window, cx);
829 assert_eq!(
830 editor.selections.display_ranges(cx),
831 [
832 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
833 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
834 ]
835 );
836 });
837
838 _ = editor.update(cx, |editor, window, cx| {
839 editor.cancel(&Cancel, window, cx);
840 assert_eq!(
841 editor.selections.display_ranges(cx),
842 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
843 );
844 });
845
846 _ = editor.update(cx, |editor, window, cx| {
847 editor.cancel(&Cancel, window, cx);
848 assert_eq!(
849 editor.selections.display_ranges(cx),
850 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
851 );
852 });
853}
854
855#[gpui::test]
856fn test_fold_action(cx: &mut TestAppContext) {
857 init_test(cx, |_| {});
858
859 let editor = cx.add_window(|window, cx| {
860 let buffer = MultiBuffer::build_simple(
861 &"
862 impl Foo {
863 // Hello!
864
865 fn a() {
866 1
867 }
868
869 fn b() {
870 2
871 }
872
873 fn c() {
874 3
875 }
876 }
877 "
878 .unindent(),
879 cx,
880 );
881 build_editor(buffer.clone(), window, cx)
882 });
883
884 _ = editor.update(cx, |editor, window, cx| {
885 editor.change_selections(None, window, cx, |s| {
886 s.select_display_ranges([
887 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
888 ]);
889 });
890 editor.fold(&Fold, window, cx);
891 assert_eq!(
892 editor.display_text(cx),
893 "
894 impl Foo {
895 // Hello!
896
897 fn a() {
898 1
899 }
900
901 fn b() {⋯
902 }
903
904 fn c() {⋯
905 }
906 }
907 "
908 .unindent(),
909 );
910
911 editor.fold(&Fold, window, cx);
912 assert_eq!(
913 editor.display_text(cx),
914 "
915 impl Foo {⋯
916 }
917 "
918 .unindent(),
919 );
920
921 editor.unfold_lines(&UnfoldLines, window, cx);
922 assert_eq!(
923 editor.display_text(cx),
924 "
925 impl Foo {
926 // Hello!
927
928 fn a() {
929 1
930 }
931
932 fn b() {⋯
933 }
934
935 fn c() {⋯
936 }
937 }
938 "
939 .unindent(),
940 );
941
942 editor.unfold_lines(&UnfoldLines, window, cx);
943 assert_eq!(
944 editor.display_text(cx),
945 editor.buffer.read(cx).read(cx).text()
946 );
947 });
948}
949
950#[gpui::test]
951fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
952 init_test(cx, |_| {});
953
954 let editor = cx.add_window(|window, cx| {
955 let buffer = MultiBuffer::build_simple(
956 &"
957 class Foo:
958 # Hello!
959
960 def a():
961 print(1)
962
963 def b():
964 print(2)
965
966 def c():
967 print(3)
968 "
969 .unindent(),
970 cx,
971 );
972 build_editor(buffer.clone(), window, cx)
973 });
974
975 _ = editor.update(cx, |editor, window, cx| {
976 editor.change_selections(None, window, cx, |s| {
977 s.select_display_ranges([
978 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
979 ]);
980 });
981 editor.fold(&Fold, window, cx);
982 assert_eq!(
983 editor.display_text(cx),
984 "
985 class Foo:
986 # Hello!
987
988 def a():
989 print(1)
990
991 def b():⋯
992
993 def c():⋯
994 "
995 .unindent(),
996 );
997
998 editor.fold(&Fold, window, cx);
999 assert_eq!(
1000 editor.display_text(cx),
1001 "
1002 class Foo:⋯
1003 "
1004 .unindent(),
1005 );
1006
1007 editor.unfold_lines(&UnfoldLines, window, cx);
1008 assert_eq!(
1009 editor.display_text(cx),
1010 "
1011 class Foo:
1012 # Hello!
1013
1014 def a():
1015 print(1)
1016
1017 def b():⋯
1018
1019 def c():⋯
1020 "
1021 .unindent(),
1022 );
1023
1024 editor.unfold_lines(&UnfoldLines, window, cx);
1025 assert_eq!(
1026 editor.display_text(cx),
1027 editor.buffer.read(cx).read(cx).text()
1028 );
1029 });
1030}
1031
1032#[gpui::test]
1033fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1034 init_test(cx, |_| {});
1035
1036 let editor = cx.add_window(|window, cx| {
1037 let buffer = MultiBuffer::build_simple(
1038 &"
1039 class Foo:
1040 # Hello!
1041
1042 def a():
1043 print(1)
1044
1045 def b():
1046 print(2)
1047
1048
1049 def c():
1050 print(3)
1051
1052
1053 "
1054 .unindent(),
1055 cx,
1056 );
1057 build_editor(buffer.clone(), window, cx)
1058 });
1059
1060 _ = editor.update(cx, |editor, window, cx| {
1061 editor.change_selections(None, window, cx, |s| {
1062 s.select_display_ranges([
1063 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1064 ]);
1065 });
1066 editor.fold(&Fold, window, cx);
1067 assert_eq!(
1068 editor.display_text(cx),
1069 "
1070 class Foo:
1071 # Hello!
1072
1073 def a():
1074 print(1)
1075
1076 def b():⋯
1077
1078
1079 def c():⋯
1080
1081
1082 "
1083 .unindent(),
1084 );
1085
1086 editor.fold(&Fold, window, cx);
1087 assert_eq!(
1088 editor.display_text(cx),
1089 "
1090 class Foo:⋯
1091
1092
1093 "
1094 .unindent(),
1095 );
1096
1097 editor.unfold_lines(&UnfoldLines, window, cx);
1098 assert_eq!(
1099 editor.display_text(cx),
1100 "
1101 class Foo:
1102 # Hello!
1103
1104 def a():
1105 print(1)
1106
1107 def b():⋯
1108
1109
1110 def c():⋯
1111
1112
1113 "
1114 .unindent(),
1115 );
1116
1117 editor.unfold_lines(&UnfoldLines, window, cx);
1118 assert_eq!(
1119 editor.display_text(cx),
1120 editor.buffer.read(cx).read(cx).text()
1121 );
1122 });
1123}
1124
1125#[gpui::test]
1126fn test_fold_at_level(cx: &mut TestAppContext) {
1127 init_test(cx, |_| {});
1128
1129 let editor = cx.add_window(|window, cx| {
1130 let buffer = MultiBuffer::build_simple(
1131 &"
1132 class Foo:
1133 # Hello!
1134
1135 def a():
1136 print(1)
1137
1138 def b():
1139 print(2)
1140
1141
1142 class Bar:
1143 # World!
1144
1145 def a():
1146 print(1)
1147
1148 def b():
1149 print(2)
1150
1151
1152 "
1153 .unindent(),
1154 cx,
1155 );
1156 build_editor(buffer.clone(), window, cx)
1157 });
1158
1159 _ = editor.update(cx, |editor, window, cx| {
1160 editor.fold_at_level(&FoldAtLevel(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(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(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 stop_at_indent: true,
1515 };
1516
1517 let move_to_end = MoveToEndOfLine {
1518 stop_at_soft_wraps: true,
1519 };
1520
1521 let editor = cx.add_window(|window, cx| {
1522 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1523 build_editor(buffer, window, cx)
1524 });
1525 _ = editor.update(cx, |editor, window, cx| {
1526 editor.change_selections(None, window, cx, |s| {
1527 s.select_display_ranges([
1528 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1529 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1530 ]);
1531 });
1532 });
1533
1534 _ = editor.update(cx, |editor, window, cx| {
1535 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1536 assert_eq!(
1537 editor.selections.display_ranges(cx),
1538 &[
1539 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1540 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1541 ]
1542 );
1543 });
1544
1545 _ = editor.update(cx, |editor, window, cx| {
1546 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1547 assert_eq!(
1548 editor.selections.display_ranges(cx),
1549 &[
1550 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1551 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1552 ]
1553 );
1554 });
1555
1556 _ = editor.update(cx, |editor, window, cx| {
1557 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1558 assert_eq!(
1559 editor.selections.display_ranges(cx),
1560 &[
1561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1562 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1563 ]
1564 );
1565 });
1566
1567 _ = editor.update(cx, |editor, window, cx| {
1568 editor.move_to_end_of_line(&move_to_end, window, cx);
1569 assert_eq!(
1570 editor.selections.display_ranges(cx),
1571 &[
1572 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1573 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1574 ]
1575 );
1576 });
1577
1578 // Moving to the end of line again is a no-op.
1579 _ = editor.update(cx, |editor, window, cx| {
1580 editor.move_to_end_of_line(&move_to_end, window, cx);
1581 assert_eq!(
1582 editor.selections.display_ranges(cx),
1583 &[
1584 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1585 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1586 ]
1587 );
1588 });
1589
1590 _ = editor.update(cx, |editor, window, cx| {
1591 editor.move_left(&MoveLeft, window, cx);
1592 editor.select_to_beginning_of_line(
1593 &SelectToBeginningOfLine {
1594 stop_at_soft_wraps: true,
1595 stop_at_indent: true,
1596 },
1597 window,
1598 cx,
1599 );
1600 assert_eq!(
1601 editor.selections.display_ranges(cx),
1602 &[
1603 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1604 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1605 ]
1606 );
1607 });
1608
1609 _ = editor.update(cx, |editor, window, cx| {
1610 editor.select_to_beginning_of_line(
1611 &SelectToBeginningOfLine {
1612 stop_at_soft_wraps: true,
1613 stop_at_indent: true,
1614 },
1615 window,
1616 cx,
1617 );
1618 assert_eq!(
1619 editor.selections.display_ranges(cx),
1620 &[
1621 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1622 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1623 ]
1624 );
1625 });
1626
1627 _ = editor.update(cx, |editor, window, cx| {
1628 editor.select_to_beginning_of_line(
1629 &SelectToBeginningOfLine {
1630 stop_at_soft_wraps: true,
1631 stop_at_indent: true,
1632 },
1633 window,
1634 cx,
1635 );
1636 assert_eq!(
1637 editor.selections.display_ranges(cx),
1638 &[
1639 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1640 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1641 ]
1642 );
1643 });
1644
1645 _ = editor.update(cx, |editor, window, cx| {
1646 editor.select_to_end_of_line(
1647 &SelectToEndOfLine {
1648 stop_at_soft_wraps: true,
1649 },
1650 window,
1651 cx,
1652 );
1653 assert_eq!(
1654 editor.selections.display_ranges(cx),
1655 &[
1656 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1657 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1658 ]
1659 );
1660 });
1661
1662 _ = editor.update(cx, |editor, window, cx| {
1663 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1664 assert_eq!(editor.display_text(cx), "ab\n de");
1665 assert_eq!(
1666 editor.selections.display_ranges(cx),
1667 &[
1668 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1669 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1670 ]
1671 );
1672 });
1673
1674 _ = editor.update(cx, |editor, window, cx| {
1675 editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx);
1676 assert_eq!(editor.display_text(cx), "\n");
1677 assert_eq!(
1678 editor.selections.display_ranges(cx),
1679 &[
1680 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1681 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1682 ]
1683 );
1684 });
1685}
1686
1687#[gpui::test]
1688fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1689 init_test(cx, |_| {});
1690 let move_to_beg = MoveToBeginningOfLine {
1691 stop_at_soft_wraps: false,
1692 stop_at_indent: false,
1693 };
1694
1695 let move_to_end = MoveToEndOfLine {
1696 stop_at_soft_wraps: false,
1697 };
1698
1699 let editor = cx.add_window(|window, cx| {
1700 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1701 build_editor(buffer, window, cx)
1702 });
1703
1704 _ = editor.update(cx, |editor, window, cx| {
1705 editor.set_wrap_width(Some(140.0.into()), cx);
1706
1707 // We expect the following lines after wrapping
1708 // ```
1709 // thequickbrownfox
1710 // jumpedoverthelazydo
1711 // gs
1712 // ```
1713 // The final `gs` was soft-wrapped onto a new line.
1714 assert_eq!(
1715 "thequickbrownfox\njumpedoverthelaz\nydogs",
1716 editor.display_text(cx),
1717 );
1718
1719 // First, let's assert behavior on the first line, that was not soft-wrapped.
1720 // Start the cursor at the `k` on the first line
1721 editor.change_selections(None, window, cx, |s| {
1722 s.select_display_ranges([
1723 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1724 ]);
1725 });
1726
1727 // Moving to the beginning of the line should put us at the beginning of the line.
1728 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1729 assert_eq!(
1730 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1731 editor.selections.display_ranges(cx)
1732 );
1733
1734 // Moving to the end of the line should put us at the end of the line.
1735 editor.move_to_end_of_line(&move_to_end, window, cx);
1736 assert_eq!(
1737 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1738 editor.selections.display_ranges(cx)
1739 );
1740
1741 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1742 // Start the cursor at the last line (`y` that was wrapped to a new line)
1743 editor.change_selections(None, window, cx, |s| {
1744 s.select_display_ranges([
1745 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1746 ]);
1747 });
1748
1749 // Moving to the beginning of the line should put us at the start of the second line of
1750 // display text, i.e., the `j`.
1751 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1752 assert_eq!(
1753 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1754 editor.selections.display_ranges(cx)
1755 );
1756
1757 // Moving to the beginning of the line again should be a no-op.
1758 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1759 assert_eq!(
1760 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1761 editor.selections.display_ranges(cx)
1762 );
1763
1764 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1765 // next display line.
1766 editor.move_to_end_of_line(&move_to_end, window, cx);
1767 assert_eq!(
1768 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1769 editor.selections.display_ranges(cx)
1770 );
1771
1772 // Moving to the end of the line again should be a no-op.
1773 editor.move_to_end_of_line(&move_to_end, window, cx);
1774 assert_eq!(
1775 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1776 editor.selections.display_ranges(cx)
1777 );
1778 });
1779}
1780
1781#[gpui::test]
1782fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1783 init_test(cx, |_| {});
1784
1785 let editor = cx.add_window(|window, cx| {
1786 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1787 build_editor(buffer, window, cx)
1788 });
1789 _ = editor.update(cx, |editor, window, cx| {
1790 editor.change_selections(None, window, cx, |s| {
1791 s.select_display_ranges([
1792 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1793 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1794 ])
1795 });
1796
1797 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1798 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1799
1800 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1801 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1802
1803 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1804 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1805
1806 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1807 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1808
1809 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1810 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1811
1812 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1813 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1814
1815 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1816 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1817
1818 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1819 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1820
1821 editor.move_right(&MoveRight, window, cx);
1822 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1823 assert_selection_ranges(
1824 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1825 editor,
1826 cx,
1827 );
1828
1829 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1830 assert_selection_ranges(
1831 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1832 editor,
1833 cx,
1834 );
1835
1836 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1837 assert_selection_ranges(
1838 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1839 editor,
1840 cx,
1841 );
1842 });
1843}
1844
1845#[gpui::test]
1846fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1847 init_test(cx, |_| {});
1848
1849 let editor = cx.add_window(|window, cx| {
1850 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1851 build_editor(buffer, window, cx)
1852 });
1853
1854 _ = editor.update(cx, |editor, window, cx| {
1855 editor.set_wrap_width(Some(140.0.into()), cx);
1856 assert_eq!(
1857 editor.display_text(cx),
1858 "use one::{\n two::three::\n four::five\n};"
1859 );
1860
1861 editor.change_selections(None, window, cx, |s| {
1862 s.select_display_ranges([
1863 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1864 ]);
1865 });
1866
1867 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1868 assert_eq!(
1869 editor.selections.display_ranges(cx),
1870 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1871 );
1872
1873 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1874 assert_eq!(
1875 editor.selections.display_ranges(cx),
1876 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1877 );
1878
1879 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1880 assert_eq!(
1881 editor.selections.display_ranges(cx),
1882 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1883 );
1884
1885 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1886 assert_eq!(
1887 editor.selections.display_ranges(cx),
1888 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
1889 );
1890
1891 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1892 assert_eq!(
1893 editor.selections.display_ranges(cx),
1894 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1895 );
1896
1897 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1898 assert_eq!(
1899 editor.selections.display_ranges(cx),
1900 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1901 );
1902 });
1903}
1904
1905#[gpui::test]
1906async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
1907 init_test(cx, |_| {});
1908 let mut cx = EditorTestContext::new(cx).await;
1909
1910 let line_height = cx.editor(|editor, window, _| {
1911 editor
1912 .style()
1913 .unwrap()
1914 .text
1915 .line_height_in_pixels(window.rem_size())
1916 });
1917 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
1918
1919 cx.set_state(
1920 &r#"ˇone
1921 two
1922
1923 three
1924 fourˇ
1925 five
1926
1927 six"#
1928 .unindent(),
1929 );
1930
1931 cx.update_editor(|editor, window, cx| {
1932 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1933 });
1934 cx.assert_editor_state(
1935 &r#"one
1936 two
1937 ˇ
1938 three
1939 four
1940 five
1941 ˇ
1942 six"#
1943 .unindent(),
1944 );
1945
1946 cx.update_editor(|editor, window, cx| {
1947 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1948 });
1949 cx.assert_editor_state(
1950 &r#"one
1951 two
1952
1953 three
1954 four
1955 five
1956 ˇ
1957 sixˇ"#
1958 .unindent(),
1959 );
1960
1961 cx.update_editor(|editor, window, cx| {
1962 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1963 });
1964 cx.assert_editor_state(
1965 &r#"one
1966 two
1967
1968 three
1969 four
1970 five
1971
1972 sixˇ"#
1973 .unindent(),
1974 );
1975
1976 cx.update_editor(|editor, window, cx| {
1977 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
1978 });
1979 cx.assert_editor_state(
1980 &r#"one
1981 two
1982
1983 three
1984 four
1985 five
1986 ˇ
1987 six"#
1988 .unindent(),
1989 );
1990
1991 cx.update_editor(|editor, window, cx| {
1992 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
1993 });
1994 cx.assert_editor_state(
1995 &r#"one
1996 two
1997 ˇ
1998 three
1999 four
2000 five
2001
2002 six"#
2003 .unindent(),
2004 );
2005
2006 cx.update_editor(|editor, window, cx| {
2007 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2008 });
2009 cx.assert_editor_state(
2010 &r#"ˇone
2011 two
2012
2013 three
2014 four
2015 five
2016
2017 six"#
2018 .unindent(),
2019 );
2020}
2021
2022#[gpui::test]
2023async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2024 init_test(cx, |_| {});
2025 let mut cx = EditorTestContext::new(cx).await;
2026 let line_height = cx.editor(|editor, window, _| {
2027 editor
2028 .style()
2029 .unwrap()
2030 .text
2031 .line_height_in_pixels(window.rem_size())
2032 });
2033 let window = cx.window;
2034 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2035
2036 cx.set_state(
2037 r#"ˇone
2038 two
2039 three
2040 four
2041 five
2042 six
2043 seven
2044 eight
2045 nine
2046 ten
2047 "#,
2048 );
2049
2050 cx.update_editor(|editor, window, cx| {
2051 assert_eq!(
2052 editor.snapshot(window, cx).scroll_position(),
2053 gpui::Point::new(0., 0.)
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., 3.)
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., 6.)
2064 );
2065 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2066 assert_eq!(
2067 editor.snapshot(window, cx).scroll_position(),
2068 gpui::Point::new(0., 3.)
2069 );
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., 1.)
2075 );
2076 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2077 assert_eq!(
2078 editor.snapshot(window, cx).scroll_position(),
2079 gpui::Point::new(0., 3.)
2080 );
2081 });
2082}
2083
2084#[gpui::test]
2085async fn test_autoscroll(cx: &mut TestAppContext) {
2086 init_test(cx, |_| {});
2087 let mut cx = EditorTestContext::new(cx).await;
2088
2089 let line_height = cx.update_editor(|editor, window, cx| {
2090 editor.set_vertical_scroll_margin(2, cx);
2091 editor
2092 .style()
2093 .unwrap()
2094 .text
2095 .line_height_in_pixels(window.rem_size())
2096 });
2097 let window = cx.window;
2098 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2099
2100 cx.set_state(
2101 r#"ˇone
2102 two
2103 three
2104 four
2105 five
2106 six
2107 seven
2108 eight
2109 nine
2110 ten
2111 "#,
2112 );
2113 cx.update_editor(|editor, window, cx| {
2114 assert_eq!(
2115 editor.snapshot(window, cx).scroll_position(),
2116 gpui::Point::new(0., 0.0)
2117 );
2118 });
2119
2120 // Add a cursor below the visible area. Since both cursors cannot fit
2121 // on screen, the editor autoscrolls to reveal the newest cursor, and
2122 // allows the vertical scroll margin below that cursor.
2123 cx.update_editor(|editor, window, cx| {
2124 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2125 selections.select_ranges([
2126 Point::new(0, 0)..Point::new(0, 0),
2127 Point::new(6, 0)..Point::new(6, 0),
2128 ]);
2129 })
2130 });
2131 cx.update_editor(|editor, window, cx| {
2132 assert_eq!(
2133 editor.snapshot(window, cx).scroll_position(),
2134 gpui::Point::new(0., 3.0)
2135 );
2136 });
2137
2138 // Move down. The editor cursor scrolls down to track the newest cursor.
2139 cx.update_editor(|editor, window, cx| {
2140 editor.move_down(&Default::default(), window, cx);
2141 });
2142 cx.update_editor(|editor, window, cx| {
2143 assert_eq!(
2144 editor.snapshot(window, cx).scroll_position(),
2145 gpui::Point::new(0., 4.0)
2146 );
2147 });
2148
2149 // Add a cursor above the visible area. Since both cursors fit on screen,
2150 // the editor scrolls to show both.
2151 cx.update_editor(|editor, window, cx| {
2152 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2153 selections.select_ranges([
2154 Point::new(1, 0)..Point::new(1, 0),
2155 Point::new(6, 0)..Point::new(6, 0),
2156 ]);
2157 })
2158 });
2159 cx.update_editor(|editor, window, cx| {
2160 assert_eq!(
2161 editor.snapshot(window, cx).scroll_position(),
2162 gpui::Point::new(0., 1.0)
2163 );
2164 });
2165}
2166
2167#[gpui::test]
2168async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2169 init_test(cx, |_| {});
2170 let mut cx = EditorTestContext::new(cx).await;
2171
2172 let line_height = cx.editor(|editor, window, _cx| {
2173 editor
2174 .style()
2175 .unwrap()
2176 .text
2177 .line_height_in_pixels(window.rem_size())
2178 });
2179 let window = cx.window;
2180 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2181 cx.set_state(
2182 &r#"
2183 ˇone
2184 two
2185 threeˇ
2186 four
2187 five
2188 six
2189 seven
2190 eight
2191 nine
2192 ten
2193 "#
2194 .unindent(),
2195 );
2196
2197 cx.update_editor(|editor, window, cx| {
2198 editor.move_page_down(&MovePageDown::default(), window, cx)
2199 });
2200 cx.assert_editor_state(
2201 &r#"
2202 one
2203 two
2204 three
2205 ˇfour
2206 five
2207 sixˇ
2208 seven
2209 eight
2210 nine
2211 ten
2212 "#
2213 .unindent(),
2214 );
2215
2216 cx.update_editor(|editor, window, cx| {
2217 editor.move_page_down(&MovePageDown::default(), window, cx)
2218 });
2219 cx.assert_editor_state(
2220 &r#"
2221 one
2222 two
2223 three
2224 four
2225 five
2226 six
2227 ˇseven
2228 eight
2229 nineˇ
2230 ten
2231 "#
2232 .unindent(),
2233 );
2234
2235 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2236 cx.assert_editor_state(
2237 &r#"
2238 one
2239 two
2240 three
2241 ˇfour
2242 five
2243 sixˇ
2244 seven
2245 eight
2246 nine
2247 ten
2248 "#
2249 .unindent(),
2250 );
2251
2252 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2253 cx.assert_editor_state(
2254 &r#"
2255 ˇone
2256 two
2257 threeˇ
2258 four
2259 five
2260 six
2261 seven
2262 eight
2263 nine
2264 ten
2265 "#
2266 .unindent(),
2267 );
2268
2269 // Test select collapsing
2270 cx.update_editor(|editor, window, cx| {
2271 editor.move_page_down(&MovePageDown::default(), window, cx);
2272 editor.move_page_down(&MovePageDown::default(), window, cx);
2273 editor.move_page_down(&MovePageDown::default(), window, cx);
2274 });
2275 cx.assert_editor_state(
2276 &r#"
2277 one
2278 two
2279 three
2280 four
2281 five
2282 six
2283 seven
2284 eight
2285 nine
2286 ˇten
2287 ˇ"#
2288 .unindent(),
2289 );
2290}
2291
2292#[gpui::test]
2293async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2294 init_test(cx, |_| {});
2295 let mut cx = EditorTestContext::new(cx).await;
2296 cx.set_state("one «two threeˇ» four");
2297 cx.update_editor(|editor, window, cx| {
2298 editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx);
2299 assert_eq!(editor.text(cx), " four");
2300 });
2301}
2302
2303#[gpui::test]
2304fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2305 init_test(cx, |_| {});
2306
2307 let editor = cx.add_window(|window, cx| {
2308 let buffer = MultiBuffer::build_simple("one two three four", cx);
2309 build_editor(buffer.clone(), window, cx)
2310 });
2311
2312 _ = editor.update(cx, |editor, window, cx| {
2313 editor.change_selections(None, window, cx, |s| {
2314 s.select_display_ranges([
2315 // an empty selection - the preceding word fragment is deleted
2316 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2317 // characters selected - they are deleted
2318 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2319 ])
2320 });
2321 editor.delete_to_previous_word_start(
2322 &DeleteToPreviousWordStart {
2323 ignore_newlines: false,
2324 },
2325 window,
2326 cx,
2327 );
2328 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2329 });
2330
2331 _ = editor.update(cx, |editor, window, cx| {
2332 editor.change_selections(None, window, cx, |s| {
2333 s.select_display_ranges([
2334 // an empty selection - the following word fragment is deleted
2335 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2336 // characters selected - they are deleted
2337 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2338 ])
2339 });
2340 editor.delete_to_next_word_end(
2341 &DeleteToNextWordEnd {
2342 ignore_newlines: false,
2343 },
2344 window,
2345 cx,
2346 );
2347 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2348 });
2349}
2350
2351#[gpui::test]
2352fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2353 init_test(cx, |_| {});
2354
2355 let editor = cx.add_window(|window, cx| {
2356 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2357 build_editor(buffer.clone(), window, cx)
2358 });
2359 let del_to_prev_word_start = DeleteToPreviousWordStart {
2360 ignore_newlines: false,
2361 };
2362 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2363 ignore_newlines: true,
2364 };
2365
2366 _ = editor.update(cx, |editor, window, cx| {
2367 editor.change_selections(None, window, cx, |s| {
2368 s.select_display_ranges([
2369 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2370 ])
2371 });
2372 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2373 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2374 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2375 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2376 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2377 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2378 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2379 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2380 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2381 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2382 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2383 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2384 });
2385}
2386
2387#[gpui::test]
2388fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2389 init_test(cx, |_| {});
2390
2391 let editor = cx.add_window(|window, cx| {
2392 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2393 build_editor(buffer.clone(), window, cx)
2394 });
2395 let del_to_next_word_end = DeleteToNextWordEnd {
2396 ignore_newlines: false,
2397 };
2398 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2399 ignore_newlines: true,
2400 };
2401
2402 _ = editor.update(cx, |editor, window, cx| {
2403 editor.change_selections(None, window, cx, |s| {
2404 s.select_display_ranges([
2405 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2406 ])
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 "one\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 "\n two\nthree\n four"
2417 );
2418 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2419 assert_eq!(
2420 editor.buffer.read(cx).read(cx).text(),
2421 "two\nthree\n four"
2422 );
2423 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2424 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2425 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2426 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2427 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2428 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2429 });
2430}
2431
2432#[gpui::test]
2433fn test_newline(cx: &mut TestAppContext) {
2434 init_test(cx, |_| {});
2435
2436 let editor = cx.add_window(|window, cx| {
2437 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2438 build_editor(buffer.clone(), window, cx)
2439 });
2440
2441 _ = editor.update(cx, |editor, window, cx| {
2442 editor.change_selections(None, window, cx, |s| {
2443 s.select_display_ranges([
2444 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2445 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2446 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2447 ])
2448 });
2449
2450 editor.newline(&Newline, window, cx);
2451 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2452 });
2453}
2454
2455#[gpui::test]
2456fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2457 init_test(cx, |_| {});
2458
2459 let editor = cx.add_window(|window, cx| {
2460 let buffer = MultiBuffer::build_simple(
2461 "
2462 a
2463 b(
2464 X
2465 )
2466 c(
2467 X
2468 )
2469 "
2470 .unindent()
2471 .as_str(),
2472 cx,
2473 );
2474 let mut editor = build_editor(buffer.clone(), window, cx);
2475 editor.change_selections(None, window, cx, |s| {
2476 s.select_ranges([
2477 Point::new(2, 4)..Point::new(2, 5),
2478 Point::new(5, 4)..Point::new(5, 5),
2479 ])
2480 });
2481 editor
2482 });
2483
2484 _ = editor.update(cx, |editor, window, cx| {
2485 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2486 editor.buffer.update(cx, |buffer, cx| {
2487 buffer.edit(
2488 [
2489 (Point::new(1, 2)..Point::new(3, 0), ""),
2490 (Point::new(4, 2)..Point::new(6, 0), ""),
2491 ],
2492 None,
2493 cx,
2494 );
2495 assert_eq!(
2496 buffer.read(cx).text(),
2497 "
2498 a
2499 b()
2500 c()
2501 "
2502 .unindent()
2503 );
2504 });
2505 assert_eq!(
2506 editor.selections.ranges(cx),
2507 &[
2508 Point::new(1, 2)..Point::new(1, 2),
2509 Point::new(2, 2)..Point::new(2, 2),
2510 ],
2511 );
2512
2513 editor.newline(&Newline, window, cx);
2514 assert_eq!(
2515 editor.text(cx),
2516 "
2517 a
2518 b(
2519 )
2520 c(
2521 )
2522 "
2523 .unindent()
2524 );
2525
2526 // The selections are moved after the inserted newlines
2527 assert_eq!(
2528 editor.selections.ranges(cx),
2529 &[
2530 Point::new(2, 0)..Point::new(2, 0),
2531 Point::new(4, 0)..Point::new(4, 0),
2532 ],
2533 );
2534 });
2535}
2536
2537#[gpui::test]
2538async fn test_newline_above(cx: &mut TestAppContext) {
2539 init_test(cx, |settings| {
2540 settings.defaults.tab_size = NonZeroU32::new(4)
2541 });
2542
2543 let language = Arc::new(
2544 Language::new(
2545 LanguageConfig::default(),
2546 Some(tree_sitter_rust::LANGUAGE.into()),
2547 )
2548 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2549 .unwrap(),
2550 );
2551
2552 let mut cx = EditorTestContext::new(cx).await;
2553 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2554 cx.set_state(indoc! {"
2555 const a: ˇA = (
2556 (ˇ
2557 «const_functionˇ»(ˇ),
2558 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2559 )ˇ
2560 ˇ);ˇ
2561 "});
2562
2563 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2564 cx.assert_editor_state(indoc! {"
2565 ˇ
2566 const a: A = (
2567 ˇ
2568 (
2569 ˇ
2570 ˇ
2571 const_function(),
2572 ˇ
2573 ˇ
2574 ˇ
2575 ˇ
2576 something_else,
2577 ˇ
2578 )
2579 ˇ
2580 ˇ
2581 );
2582 "});
2583}
2584
2585#[gpui::test]
2586async fn test_newline_below(cx: &mut TestAppContext) {
2587 init_test(cx, |settings| {
2588 settings.defaults.tab_size = NonZeroU32::new(4)
2589 });
2590
2591 let language = Arc::new(
2592 Language::new(
2593 LanguageConfig::default(),
2594 Some(tree_sitter_rust::LANGUAGE.into()),
2595 )
2596 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2597 .unwrap(),
2598 );
2599
2600 let mut cx = EditorTestContext::new(cx).await;
2601 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2602 cx.set_state(indoc! {"
2603 const a: ˇA = (
2604 (ˇ
2605 «const_functionˇ»(ˇ),
2606 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2607 )ˇ
2608 ˇ);ˇ
2609 "});
2610
2611 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2612 cx.assert_editor_state(indoc! {"
2613 const a: A = (
2614 ˇ
2615 (
2616 ˇ
2617 const_function(),
2618 ˇ
2619 ˇ
2620 something_else,
2621 ˇ
2622 ˇ
2623 ˇ
2624 ˇ
2625 )
2626 ˇ
2627 );
2628 ˇ
2629 ˇ
2630 "});
2631}
2632
2633#[gpui::test]
2634async fn test_newline_comments(cx: &mut TestAppContext) {
2635 init_test(cx, |settings| {
2636 settings.defaults.tab_size = NonZeroU32::new(4)
2637 });
2638
2639 let language = Arc::new(Language::new(
2640 LanguageConfig {
2641 line_comments: vec!["//".into()],
2642 ..LanguageConfig::default()
2643 },
2644 None,
2645 ));
2646 {
2647 let mut cx = EditorTestContext::new(cx).await;
2648 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2649 cx.set_state(indoc! {"
2650 // Fooˇ
2651 "});
2652
2653 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2654 cx.assert_editor_state(indoc! {"
2655 // Foo
2656 //ˇ
2657 "});
2658 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2659 cx.set_state(indoc! {"
2660 ˇ// Foo
2661 "});
2662 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2663 cx.assert_editor_state(indoc! {"
2664
2665 ˇ// Foo
2666 "});
2667 }
2668 // Ensure that comment continuations can be disabled.
2669 update_test_language_settings(cx, |settings| {
2670 settings.defaults.extend_comment_on_newline = Some(false);
2671 });
2672 let mut cx = EditorTestContext::new(cx).await;
2673 cx.set_state(indoc! {"
2674 // Fooˇ
2675 "});
2676 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2677 cx.assert_editor_state(indoc! {"
2678 // Foo
2679 ˇ
2680 "});
2681}
2682
2683#[gpui::test]
2684fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2685 init_test(cx, |_| {});
2686
2687 let editor = cx.add_window(|window, cx| {
2688 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2689 let mut editor = build_editor(buffer.clone(), window, cx);
2690 editor.change_selections(None, window, cx, |s| {
2691 s.select_ranges([3..4, 11..12, 19..20])
2692 });
2693 editor
2694 });
2695
2696 _ = editor.update(cx, |editor, window, cx| {
2697 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2698 editor.buffer.update(cx, |buffer, cx| {
2699 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2700 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2701 });
2702 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2703
2704 editor.insert("Z", window, cx);
2705 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2706
2707 // The selections are moved after the inserted characters
2708 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2709 });
2710}
2711
2712#[gpui::test]
2713async fn test_tab(cx: &mut TestAppContext) {
2714 init_test(cx, |settings| {
2715 settings.defaults.tab_size = NonZeroU32::new(3)
2716 });
2717
2718 let mut cx = EditorTestContext::new(cx).await;
2719 cx.set_state(indoc! {"
2720 ˇabˇc
2721 ˇ🏀ˇ🏀ˇefg
2722 dˇ
2723 "});
2724 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2725 cx.assert_editor_state(indoc! {"
2726 ˇab ˇc
2727 ˇ🏀 ˇ🏀 ˇefg
2728 d ˇ
2729 "});
2730
2731 cx.set_state(indoc! {"
2732 a
2733 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2734 "});
2735 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2736 cx.assert_editor_state(indoc! {"
2737 a
2738 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2739 "});
2740}
2741
2742#[gpui::test]
2743async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
2744 init_test(cx, |_| {});
2745
2746 let mut cx = EditorTestContext::new(cx).await;
2747 let language = Arc::new(
2748 Language::new(
2749 LanguageConfig::default(),
2750 Some(tree_sitter_rust::LANGUAGE.into()),
2751 )
2752 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2753 .unwrap(),
2754 );
2755 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2756
2757 // cursors that are already at the suggested indent level insert
2758 // a soft tab. cursors that are to the left of the suggested indent
2759 // auto-indent their line.
2760 cx.set_state(indoc! {"
2761 ˇ
2762 const a: B = (
2763 c(
2764 d(
2765 ˇ
2766 )
2767 ˇ
2768 ˇ )
2769 );
2770 "});
2771 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2772 cx.assert_editor_state(indoc! {"
2773 ˇ
2774 const a: B = (
2775 c(
2776 d(
2777 ˇ
2778 )
2779 ˇ
2780 ˇ)
2781 );
2782 "});
2783
2784 // handle auto-indent when there are multiple cursors on the same line
2785 cx.set_state(indoc! {"
2786 const a: B = (
2787 c(
2788 ˇ ˇ
2789 ˇ )
2790 );
2791 "});
2792 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2793 cx.assert_editor_state(indoc! {"
2794 const a: B = (
2795 c(
2796 ˇ
2797 ˇ)
2798 );
2799 "});
2800}
2801
2802#[gpui::test]
2803async fn test_tab_with_mixed_whitespace(cx: &mut TestAppContext) {
2804 init_test(cx, |settings| {
2805 settings.defaults.tab_size = NonZeroU32::new(4)
2806 });
2807
2808 let language = Arc::new(
2809 Language::new(
2810 LanguageConfig::default(),
2811 Some(tree_sitter_rust::LANGUAGE.into()),
2812 )
2813 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2814 .unwrap(),
2815 );
2816
2817 let mut cx = EditorTestContext::new(cx).await;
2818 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2819 cx.set_state(indoc! {"
2820 fn a() {
2821 if b {
2822 \t ˇc
2823 }
2824 }
2825 "});
2826
2827 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2828 cx.assert_editor_state(indoc! {"
2829 fn a() {
2830 if b {
2831 ˇc
2832 }
2833 }
2834 "});
2835}
2836
2837#[gpui::test]
2838async fn test_indent_outdent(cx: &mut TestAppContext) {
2839 init_test(cx, |settings| {
2840 settings.defaults.tab_size = NonZeroU32::new(4);
2841 });
2842
2843 let mut cx = EditorTestContext::new(cx).await;
2844
2845 cx.set_state(indoc! {"
2846 «oneˇ» «twoˇ»
2847 three
2848 four
2849 "});
2850 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2851 cx.assert_editor_state(indoc! {"
2852 «oneˇ» «twoˇ»
2853 three
2854 four
2855 "});
2856
2857 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2858 cx.assert_editor_state(indoc! {"
2859 «oneˇ» «twoˇ»
2860 three
2861 four
2862 "});
2863
2864 // select across line ending
2865 cx.set_state(indoc! {"
2866 one two
2867 t«hree
2868 ˇ» four
2869 "});
2870 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2871 cx.assert_editor_state(indoc! {"
2872 one two
2873 t«hree
2874 ˇ» four
2875 "});
2876
2877 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2878 cx.assert_editor_state(indoc! {"
2879 one two
2880 t«hree
2881 ˇ» four
2882 "});
2883
2884 // Ensure that indenting/outdenting works when the cursor is at column 0.
2885 cx.set_state(indoc! {"
2886 one two
2887 ˇthree
2888 four
2889 "});
2890 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2891 cx.assert_editor_state(indoc! {"
2892 one two
2893 ˇthree
2894 four
2895 "});
2896
2897 cx.set_state(indoc! {"
2898 one two
2899 ˇ three
2900 four
2901 "});
2902 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2903 cx.assert_editor_state(indoc! {"
2904 one two
2905 ˇthree
2906 four
2907 "});
2908}
2909
2910#[gpui::test]
2911async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
2912 init_test(cx, |settings| {
2913 settings.defaults.hard_tabs = Some(true);
2914 });
2915
2916 let mut cx = EditorTestContext::new(cx).await;
2917
2918 // select two ranges on one line
2919 cx.set_state(indoc! {"
2920 «oneˇ» «twoˇ»
2921 three
2922 four
2923 "});
2924 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2925 cx.assert_editor_state(indoc! {"
2926 \t«oneˇ» «twoˇ»
2927 three
2928 four
2929 "});
2930 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2931 cx.assert_editor_state(indoc! {"
2932 \t\t«oneˇ» «twoˇ»
2933 three
2934 four
2935 "});
2936 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2937 cx.assert_editor_state(indoc! {"
2938 \t«oneˇ» «twoˇ»
2939 three
2940 four
2941 "});
2942 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2943 cx.assert_editor_state(indoc! {"
2944 «oneˇ» «twoˇ»
2945 three
2946 four
2947 "});
2948
2949 // select across a line ending
2950 cx.set_state(indoc! {"
2951 one two
2952 t«hree
2953 ˇ»four
2954 "});
2955 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2956 cx.assert_editor_state(indoc! {"
2957 one two
2958 \tt«hree
2959 ˇ»four
2960 "});
2961 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2962 cx.assert_editor_state(indoc! {"
2963 one two
2964 \t\tt«hree
2965 ˇ»four
2966 "});
2967 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2968 cx.assert_editor_state(indoc! {"
2969 one two
2970 \tt«hree
2971 ˇ»four
2972 "});
2973 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2974 cx.assert_editor_state(indoc! {"
2975 one two
2976 t«hree
2977 ˇ»four
2978 "});
2979
2980 // Ensure that indenting/outdenting works when the cursor is at column 0.
2981 cx.set_state(indoc! {"
2982 one two
2983 ˇthree
2984 four
2985 "});
2986 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2987 cx.assert_editor_state(indoc! {"
2988 one two
2989 ˇthree
2990 four
2991 "});
2992 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2993 cx.assert_editor_state(indoc! {"
2994 one two
2995 \tˇthree
2996 four
2997 "});
2998 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2999 cx.assert_editor_state(indoc! {"
3000 one two
3001 ˇthree
3002 four
3003 "});
3004}
3005
3006#[gpui::test]
3007fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3008 init_test(cx, |settings| {
3009 settings.languages.extend([
3010 (
3011 "TOML".into(),
3012 LanguageSettingsContent {
3013 tab_size: NonZeroU32::new(2),
3014 ..Default::default()
3015 },
3016 ),
3017 (
3018 "Rust".into(),
3019 LanguageSettingsContent {
3020 tab_size: NonZeroU32::new(4),
3021 ..Default::default()
3022 },
3023 ),
3024 ]);
3025 });
3026
3027 let toml_language = Arc::new(Language::new(
3028 LanguageConfig {
3029 name: "TOML".into(),
3030 ..Default::default()
3031 },
3032 None,
3033 ));
3034 let rust_language = Arc::new(Language::new(
3035 LanguageConfig {
3036 name: "Rust".into(),
3037 ..Default::default()
3038 },
3039 None,
3040 ));
3041
3042 let toml_buffer =
3043 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3044 let rust_buffer =
3045 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3046 let multibuffer = cx.new(|cx| {
3047 let mut multibuffer = MultiBuffer::new(ReadWrite);
3048 multibuffer.push_excerpts(
3049 toml_buffer.clone(),
3050 [ExcerptRange {
3051 context: Point::new(0, 0)..Point::new(2, 0),
3052 primary: None,
3053 }],
3054 cx,
3055 );
3056 multibuffer.push_excerpts(
3057 rust_buffer.clone(),
3058 [ExcerptRange {
3059 context: Point::new(0, 0)..Point::new(1, 0),
3060 primary: None,
3061 }],
3062 cx,
3063 );
3064 multibuffer
3065 });
3066
3067 cx.add_window(|window, cx| {
3068 let mut editor = build_editor(multibuffer, window, cx);
3069
3070 assert_eq!(
3071 editor.text(cx),
3072 indoc! {"
3073 a = 1
3074 b = 2
3075
3076 const c: usize = 3;
3077 "}
3078 );
3079
3080 select_ranges(
3081 &mut editor,
3082 indoc! {"
3083 «aˇ» = 1
3084 b = 2
3085
3086 «const c:ˇ» usize = 3;
3087 "},
3088 window,
3089 cx,
3090 );
3091
3092 editor.tab(&Tab, window, cx);
3093 assert_text_with_selections(
3094 &mut editor,
3095 indoc! {"
3096 «aˇ» = 1
3097 b = 2
3098
3099 «const c:ˇ» usize = 3;
3100 "},
3101 cx,
3102 );
3103 editor.tab_prev(&TabPrev, window, cx);
3104 assert_text_with_selections(
3105 &mut editor,
3106 indoc! {"
3107 «aˇ» = 1
3108 b = 2
3109
3110 «const c:ˇ» usize = 3;
3111 "},
3112 cx,
3113 );
3114
3115 editor
3116 });
3117}
3118
3119#[gpui::test]
3120async fn test_backspace(cx: &mut TestAppContext) {
3121 init_test(cx, |_| {});
3122
3123 let mut cx = EditorTestContext::new(cx).await;
3124
3125 // Basic backspace
3126 cx.set_state(indoc! {"
3127 onˇe two three
3128 fou«rˇ» five six
3129 seven «ˇeight nine
3130 »ten
3131 "});
3132 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3133 cx.assert_editor_state(indoc! {"
3134 oˇe two three
3135 fouˇ five six
3136 seven ˇten
3137 "});
3138
3139 // Test backspace inside and around indents
3140 cx.set_state(indoc! {"
3141 zero
3142 ˇone
3143 ˇtwo
3144 ˇ ˇ ˇ three
3145 ˇ ˇ four
3146 "});
3147 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3148 cx.assert_editor_state(indoc! {"
3149 zero
3150 ˇone
3151 ˇtwo
3152 ˇ threeˇ four
3153 "});
3154
3155 // Test backspace with line_mode set to true
3156 cx.update_editor(|e, _, _| e.selections.line_mode = true);
3157 cx.set_state(indoc! {"
3158 The ˇquick ˇbrown
3159 fox jumps over
3160 the lazy dog
3161 ˇThe qu«ick bˇ»rown"});
3162 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3163 cx.assert_editor_state(indoc! {"
3164 ˇfox jumps over
3165 the lazy dogˇ"});
3166}
3167
3168#[gpui::test]
3169async fn test_delete(cx: &mut TestAppContext) {
3170 init_test(cx, |_| {});
3171
3172 let mut cx = EditorTestContext::new(cx).await;
3173 cx.set_state(indoc! {"
3174 onˇe two three
3175 fou«rˇ» five six
3176 seven «ˇeight nine
3177 »ten
3178 "});
3179 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3180 cx.assert_editor_state(indoc! {"
3181 onˇ two three
3182 fouˇ five six
3183 seven ˇten
3184 "});
3185
3186 // Test backspace with line_mode set to true
3187 cx.update_editor(|e, _, _| e.selections.line_mode = true);
3188 cx.set_state(indoc! {"
3189 The ˇquick ˇbrown
3190 fox «ˇjum»ps over
3191 the lazy dog
3192 ˇThe qu«ick bˇ»rown"});
3193 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3194 cx.assert_editor_state("ˇthe lazy dogˇ");
3195}
3196
3197#[gpui::test]
3198fn test_delete_line(cx: &mut TestAppContext) {
3199 init_test(cx, |_| {});
3200
3201 let editor = cx.add_window(|window, cx| {
3202 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3203 build_editor(buffer, window, cx)
3204 });
3205 _ = editor.update(cx, |editor, window, cx| {
3206 editor.change_selections(None, window, cx, |s| {
3207 s.select_display_ranges([
3208 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3209 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3210 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3211 ])
3212 });
3213 editor.delete_line(&DeleteLine, window, cx);
3214 assert_eq!(editor.display_text(cx), "ghi");
3215 assert_eq!(
3216 editor.selections.display_ranges(cx),
3217 vec![
3218 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3219 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3220 ]
3221 );
3222 });
3223
3224 let editor = cx.add_window(|window, cx| {
3225 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3226 build_editor(buffer, window, cx)
3227 });
3228 _ = editor.update(cx, |editor, window, cx| {
3229 editor.change_selections(None, window, cx, |s| {
3230 s.select_display_ranges([
3231 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3232 ])
3233 });
3234 editor.delete_line(&DeleteLine, window, cx);
3235 assert_eq!(editor.display_text(cx), "ghi\n");
3236 assert_eq!(
3237 editor.selections.display_ranges(cx),
3238 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3239 );
3240 });
3241}
3242
3243#[gpui::test]
3244fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3245 init_test(cx, |_| {});
3246
3247 cx.add_window(|window, cx| {
3248 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3249 let mut editor = build_editor(buffer.clone(), window, cx);
3250 let buffer = buffer.read(cx).as_singleton().unwrap();
3251
3252 assert_eq!(
3253 editor.selections.ranges::<Point>(cx),
3254 &[Point::new(0, 0)..Point::new(0, 0)]
3255 );
3256
3257 // When on single line, replace newline at end by space
3258 editor.join_lines(&JoinLines, window, cx);
3259 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3260 assert_eq!(
3261 editor.selections.ranges::<Point>(cx),
3262 &[Point::new(0, 3)..Point::new(0, 3)]
3263 );
3264
3265 // When multiple lines are selected, remove newlines that are spanned by the selection
3266 editor.change_selections(None, window, cx, |s| {
3267 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3268 });
3269 editor.join_lines(&JoinLines, window, cx);
3270 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3271 assert_eq!(
3272 editor.selections.ranges::<Point>(cx),
3273 &[Point::new(0, 11)..Point::new(0, 11)]
3274 );
3275
3276 // Undo should be transactional
3277 editor.undo(&Undo, window, cx);
3278 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3279 assert_eq!(
3280 editor.selections.ranges::<Point>(cx),
3281 &[Point::new(0, 5)..Point::new(2, 2)]
3282 );
3283
3284 // When joining an empty line don't insert a space
3285 editor.change_selections(None, window, cx, |s| {
3286 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3287 });
3288 editor.join_lines(&JoinLines, window, cx);
3289 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3290 assert_eq!(
3291 editor.selections.ranges::<Point>(cx),
3292 [Point::new(2, 3)..Point::new(2, 3)]
3293 );
3294
3295 // We can remove trailing newlines
3296 editor.join_lines(&JoinLines, window, cx);
3297 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3298 assert_eq!(
3299 editor.selections.ranges::<Point>(cx),
3300 [Point::new(2, 3)..Point::new(2, 3)]
3301 );
3302
3303 // We don't blow up on the last line
3304 editor.join_lines(&JoinLines, window, cx);
3305 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3306 assert_eq!(
3307 editor.selections.ranges::<Point>(cx),
3308 [Point::new(2, 3)..Point::new(2, 3)]
3309 );
3310
3311 // reset to test indentation
3312 editor.buffer.update(cx, |buffer, cx| {
3313 buffer.edit(
3314 [
3315 (Point::new(1, 0)..Point::new(1, 2), " "),
3316 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3317 ],
3318 None,
3319 cx,
3320 )
3321 });
3322
3323 // We remove any leading spaces
3324 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3325 editor.change_selections(None, window, cx, |s| {
3326 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3327 });
3328 editor.join_lines(&JoinLines, window, cx);
3329 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3330
3331 // We don't insert a space for a line containing only spaces
3332 editor.join_lines(&JoinLines, window, cx);
3333 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3334
3335 // We ignore any leading tabs
3336 editor.join_lines(&JoinLines, window, cx);
3337 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3338
3339 editor
3340 });
3341}
3342
3343#[gpui::test]
3344fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3345 init_test(cx, |_| {});
3346
3347 cx.add_window(|window, cx| {
3348 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3349 let mut editor = build_editor(buffer.clone(), window, cx);
3350 let buffer = buffer.read(cx).as_singleton().unwrap();
3351
3352 editor.change_selections(None, window, cx, |s| {
3353 s.select_ranges([
3354 Point::new(0, 2)..Point::new(1, 1),
3355 Point::new(1, 2)..Point::new(1, 2),
3356 Point::new(3, 1)..Point::new(3, 2),
3357 ])
3358 });
3359
3360 editor.join_lines(&JoinLines, window, cx);
3361 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3362
3363 assert_eq!(
3364 editor.selections.ranges::<Point>(cx),
3365 [
3366 Point::new(0, 7)..Point::new(0, 7),
3367 Point::new(1, 3)..Point::new(1, 3)
3368 ]
3369 );
3370 editor
3371 });
3372}
3373
3374#[gpui::test]
3375async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3376 init_test(cx, |_| {});
3377
3378 let mut cx = EditorTestContext::new(cx).await;
3379
3380 let diff_base = r#"
3381 Line 0
3382 Line 1
3383 Line 2
3384 Line 3
3385 "#
3386 .unindent();
3387
3388 cx.set_state(
3389 &r#"
3390 ˇLine 0
3391 Line 1
3392 Line 2
3393 Line 3
3394 "#
3395 .unindent(),
3396 );
3397
3398 cx.set_head_text(&diff_base);
3399 executor.run_until_parked();
3400
3401 // Join lines
3402 cx.update_editor(|editor, window, cx| {
3403 editor.join_lines(&JoinLines, window, cx);
3404 });
3405 executor.run_until_parked();
3406
3407 cx.assert_editor_state(
3408 &r#"
3409 Line 0ˇ Line 1
3410 Line 2
3411 Line 3
3412 "#
3413 .unindent(),
3414 );
3415 // Join again
3416 cx.update_editor(|editor, window, cx| {
3417 editor.join_lines(&JoinLines, window, cx);
3418 });
3419 executor.run_until_parked();
3420
3421 cx.assert_editor_state(
3422 &r#"
3423 Line 0 Line 1ˇ Line 2
3424 Line 3
3425 "#
3426 .unindent(),
3427 );
3428}
3429
3430#[gpui::test]
3431async fn test_custom_newlines_cause_no_false_positive_diffs(
3432 executor: BackgroundExecutor,
3433 cx: &mut TestAppContext,
3434) {
3435 init_test(cx, |_| {});
3436 let mut cx = EditorTestContext::new(cx).await;
3437 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3438 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3439 executor.run_until_parked();
3440
3441 cx.update_editor(|editor, window, cx| {
3442 let snapshot = editor.snapshot(window, cx);
3443 assert_eq!(
3444 snapshot
3445 .buffer_snapshot
3446 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3447 .collect::<Vec<_>>(),
3448 Vec::new(),
3449 "Should not have any diffs for files with custom newlines"
3450 );
3451 });
3452}
3453
3454#[gpui::test]
3455async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3456 init_test(cx, |_| {});
3457
3458 let mut cx = EditorTestContext::new(cx).await;
3459
3460 // Test sort_lines_case_insensitive()
3461 cx.set_state(indoc! {"
3462 «z
3463 y
3464 x
3465 Z
3466 Y
3467 Xˇ»
3468 "});
3469 cx.update_editor(|e, window, cx| {
3470 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3471 });
3472 cx.assert_editor_state(indoc! {"
3473 «x
3474 X
3475 y
3476 Y
3477 z
3478 Zˇ»
3479 "});
3480
3481 // Test reverse_lines()
3482 cx.set_state(indoc! {"
3483 «5
3484 4
3485 3
3486 2
3487 1ˇ»
3488 "});
3489 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3490 cx.assert_editor_state(indoc! {"
3491 «1
3492 2
3493 3
3494 4
3495 5ˇ»
3496 "});
3497
3498 // Skip testing shuffle_line()
3499
3500 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3501 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3502
3503 // Don't manipulate when cursor is on single line, but expand the selection
3504 cx.set_state(indoc! {"
3505 ddˇdd
3506 ccc
3507 bb
3508 a
3509 "});
3510 cx.update_editor(|e, window, cx| {
3511 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3512 });
3513 cx.assert_editor_state(indoc! {"
3514 «ddddˇ»
3515 ccc
3516 bb
3517 a
3518 "});
3519
3520 // Basic manipulate case
3521 // Start selection moves to column 0
3522 // End of selection shrinks to fit shorter line
3523 cx.set_state(indoc! {"
3524 dd«d
3525 ccc
3526 bb
3527 aaaaaˇ»
3528 "});
3529 cx.update_editor(|e, window, cx| {
3530 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3531 });
3532 cx.assert_editor_state(indoc! {"
3533 «aaaaa
3534 bb
3535 ccc
3536 dddˇ»
3537 "});
3538
3539 // Manipulate case with newlines
3540 cx.set_state(indoc! {"
3541 dd«d
3542 ccc
3543
3544 bb
3545 aaaaa
3546
3547 ˇ»
3548 "});
3549 cx.update_editor(|e, window, cx| {
3550 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3551 });
3552 cx.assert_editor_state(indoc! {"
3553 «
3554
3555 aaaaa
3556 bb
3557 ccc
3558 dddˇ»
3559
3560 "});
3561
3562 // Adding new line
3563 cx.set_state(indoc! {"
3564 aa«a
3565 bbˇ»b
3566 "});
3567 cx.update_editor(|e, window, cx| {
3568 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3569 });
3570 cx.assert_editor_state(indoc! {"
3571 «aaa
3572 bbb
3573 added_lineˇ»
3574 "});
3575
3576 // Removing line
3577 cx.set_state(indoc! {"
3578 aa«a
3579 bbbˇ»
3580 "});
3581 cx.update_editor(|e, window, cx| {
3582 e.manipulate_lines(window, cx, |lines| {
3583 lines.pop();
3584 })
3585 });
3586 cx.assert_editor_state(indoc! {"
3587 «aaaˇ»
3588 "});
3589
3590 // Removing all lines
3591 cx.set_state(indoc! {"
3592 aa«a
3593 bbbˇ»
3594 "});
3595 cx.update_editor(|e, window, cx| {
3596 e.manipulate_lines(window, cx, |lines| {
3597 lines.drain(..);
3598 })
3599 });
3600 cx.assert_editor_state(indoc! {"
3601 ˇ
3602 "});
3603}
3604
3605#[gpui::test]
3606async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3607 init_test(cx, |_| {});
3608
3609 let mut cx = EditorTestContext::new(cx).await;
3610
3611 // Consider continuous selection as single selection
3612 cx.set_state(indoc! {"
3613 Aaa«aa
3614 cˇ»c«c
3615 bb
3616 aaaˇ»aa
3617 "});
3618 cx.update_editor(|e, window, cx| {
3619 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3620 });
3621 cx.assert_editor_state(indoc! {"
3622 «Aaaaa
3623 ccc
3624 bb
3625 aaaaaˇ»
3626 "});
3627
3628 cx.set_state(indoc! {"
3629 Aaa«aa
3630 cˇ»c«c
3631 bb
3632 aaaˇ»aa
3633 "});
3634 cx.update_editor(|e, window, cx| {
3635 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3636 });
3637 cx.assert_editor_state(indoc! {"
3638 «Aaaaa
3639 ccc
3640 bbˇ»
3641 "});
3642
3643 // Consider non continuous selection as distinct dedup operations
3644 cx.set_state(indoc! {"
3645 «aaaaa
3646 bb
3647 aaaaa
3648 aaaaaˇ»
3649
3650 aaa«aaˇ»
3651 "});
3652 cx.update_editor(|e, window, cx| {
3653 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3654 });
3655 cx.assert_editor_state(indoc! {"
3656 «aaaaa
3657 bbˇ»
3658
3659 «aaaaaˇ»
3660 "});
3661}
3662
3663#[gpui::test]
3664async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3665 init_test(cx, |_| {});
3666
3667 let mut cx = EditorTestContext::new(cx).await;
3668
3669 cx.set_state(indoc! {"
3670 «Aaa
3671 aAa
3672 Aaaˇ»
3673 "});
3674 cx.update_editor(|e, window, cx| {
3675 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3676 });
3677 cx.assert_editor_state(indoc! {"
3678 «Aaa
3679 aAaˇ»
3680 "});
3681
3682 cx.set_state(indoc! {"
3683 «Aaa
3684 aAa
3685 aaAˇ»
3686 "});
3687 cx.update_editor(|e, window, cx| {
3688 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3689 });
3690 cx.assert_editor_state(indoc! {"
3691 «Aaaˇ»
3692 "});
3693}
3694
3695#[gpui::test]
3696async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3697 init_test(cx, |_| {});
3698
3699 let mut cx = EditorTestContext::new(cx).await;
3700
3701 // Manipulate with multiple selections on a single line
3702 cx.set_state(indoc! {"
3703 dd«dd
3704 cˇ»c«c
3705 bb
3706 aaaˇ»aa
3707 "});
3708 cx.update_editor(|e, window, cx| {
3709 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3710 });
3711 cx.assert_editor_state(indoc! {"
3712 «aaaaa
3713 bb
3714 ccc
3715 ddddˇ»
3716 "});
3717
3718 // Manipulate with multiple disjoin selections
3719 cx.set_state(indoc! {"
3720 5«
3721 4
3722 3
3723 2
3724 1ˇ»
3725
3726 dd«dd
3727 ccc
3728 bb
3729 aaaˇ»aa
3730 "});
3731 cx.update_editor(|e, window, cx| {
3732 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3733 });
3734 cx.assert_editor_state(indoc! {"
3735 «1
3736 2
3737 3
3738 4
3739 5ˇ»
3740
3741 «aaaaa
3742 bb
3743 ccc
3744 ddddˇ»
3745 "});
3746
3747 // Adding lines on each selection
3748 cx.set_state(indoc! {"
3749 2«
3750 1ˇ»
3751
3752 bb«bb
3753 aaaˇ»aa
3754 "});
3755 cx.update_editor(|e, window, cx| {
3756 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3757 });
3758 cx.assert_editor_state(indoc! {"
3759 «2
3760 1
3761 added lineˇ»
3762
3763 «bbbb
3764 aaaaa
3765 added lineˇ»
3766 "});
3767
3768 // Removing lines on each selection
3769 cx.set_state(indoc! {"
3770 2«
3771 1ˇ»
3772
3773 bb«bb
3774 aaaˇ»aa
3775 "});
3776 cx.update_editor(|e, window, cx| {
3777 e.manipulate_lines(window, cx, |lines| {
3778 lines.pop();
3779 })
3780 });
3781 cx.assert_editor_state(indoc! {"
3782 «2ˇ»
3783
3784 «bbbbˇ»
3785 "});
3786}
3787
3788#[gpui::test]
3789async fn test_manipulate_text(cx: &mut TestAppContext) {
3790 init_test(cx, |_| {});
3791
3792 let mut cx = EditorTestContext::new(cx).await;
3793
3794 // Test convert_to_upper_case()
3795 cx.set_state(indoc! {"
3796 «hello worldˇ»
3797 "});
3798 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3799 cx.assert_editor_state(indoc! {"
3800 «HELLO WORLDˇ»
3801 "});
3802
3803 // Test convert_to_lower_case()
3804 cx.set_state(indoc! {"
3805 «HELLO WORLDˇ»
3806 "});
3807 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3808 cx.assert_editor_state(indoc! {"
3809 «hello worldˇ»
3810 "});
3811
3812 // Test multiple line, single selection case
3813 cx.set_state(indoc! {"
3814 «The quick brown
3815 fox jumps over
3816 the lazy dogˇ»
3817 "});
3818 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3819 cx.assert_editor_state(indoc! {"
3820 «The Quick Brown
3821 Fox Jumps Over
3822 The Lazy Dogˇ»
3823 "});
3824
3825 // Test multiple line, single selection case
3826 cx.set_state(indoc! {"
3827 «The quick brown
3828 fox jumps over
3829 the lazy dogˇ»
3830 "});
3831 cx.update_editor(|e, window, cx| {
3832 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
3833 });
3834 cx.assert_editor_state(indoc! {"
3835 «TheQuickBrown
3836 FoxJumpsOver
3837 TheLazyDogˇ»
3838 "});
3839
3840 // From here on out, test more complex cases of manipulate_text()
3841
3842 // Test no selection case - should affect words cursors are in
3843 // Cursor at beginning, middle, and end of word
3844 cx.set_state(indoc! {"
3845 ˇhello big beauˇtiful worldˇ
3846 "});
3847 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3848 cx.assert_editor_state(indoc! {"
3849 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
3850 "});
3851
3852 // Test multiple selections on a single line and across multiple lines
3853 cx.set_state(indoc! {"
3854 «Theˇ» quick «brown
3855 foxˇ» jumps «overˇ»
3856 the «lazyˇ» dog
3857 "});
3858 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3859 cx.assert_editor_state(indoc! {"
3860 «THEˇ» quick «BROWN
3861 FOXˇ» jumps «OVERˇ»
3862 the «LAZYˇ» dog
3863 "});
3864
3865 // Test case where text length grows
3866 cx.set_state(indoc! {"
3867 «tschüߡ»
3868 "});
3869 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3870 cx.assert_editor_state(indoc! {"
3871 «TSCHÜSSˇ»
3872 "});
3873
3874 // Test to make sure we don't crash when text shrinks
3875 cx.set_state(indoc! {"
3876 aaa_bbbˇ
3877 "});
3878 cx.update_editor(|e, window, cx| {
3879 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3880 });
3881 cx.assert_editor_state(indoc! {"
3882 «aaaBbbˇ»
3883 "});
3884
3885 // Test to make sure we all aware of the fact that each word can grow and shrink
3886 // Final selections should be aware of this fact
3887 cx.set_state(indoc! {"
3888 aaa_bˇbb bbˇb_ccc ˇccc_ddd
3889 "});
3890 cx.update_editor(|e, window, cx| {
3891 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3892 });
3893 cx.assert_editor_state(indoc! {"
3894 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
3895 "});
3896
3897 cx.set_state(indoc! {"
3898 «hElLo, WoRld!ˇ»
3899 "});
3900 cx.update_editor(|e, window, cx| {
3901 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
3902 });
3903 cx.assert_editor_state(indoc! {"
3904 «HeLlO, wOrLD!ˇ»
3905 "});
3906}
3907
3908#[gpui::test]
3909fn test_duplicate_line(cx: &mut TestAppContext) {
3910 init_test(cx, |_| {});
3911
3912 let editor = cx.add_window(|window, cx| {
3913 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3914 build_editor(buffer, window, cx)
3915 });
3916 _ = editor.update(cx, |editor, window, cx| {
3917 editor.change_selections(None, window, cx, |s| {
3918 s.select_display_ranges([
3919 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3920 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3921 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
3922 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3923 ])
3924 });
3925 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
3926 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
3927 assert_eq!(
3928 editor.selections.display_ranges(cx),
3929 vec![
3930 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3931 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3932 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3933 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
3934 ]
3935 );
3936 });
3937
3938 let editor = cx.add_window(|window, cx| {
3939 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3940 build_editor(buffer, window, cx)
3941 });
3942 _ = editor.update(cx, |editor, window, cx| {
3943 editor.change_selections(None, window, cx, |s| {
3944 s.select_display_ranges([
3945 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
3946 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
3947 ])
3948 });
3949 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
3950 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
3951 assert_eq!(
3952 editor.selections.display_ranges(cx),
3953 vec![
3954 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
3955 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
3956 ]
3957 );
3958 });
3959
3960 // With `move_upwards` the selections stay in place, except for
3961 // the lines inserted above them
3962 let editor = cx.add_window(|window, cx| {
3963 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3964 build_editor(buffer, window, cx)
3965 });
3966 _ = editor.update(cx, |editor, window, cx| {
3967 editor.change_selections(None, window, cx, |s| {
3968 s.select_display_ranges([
3969 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3970 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3971 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
3972 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3973 ])
3974 });
3975 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
3976 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
3977 assert_eq!(
3978 editor.selections.display_ranges(cx),
3979 vec![
3980 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3981 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3982 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
3983 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
3984 ]
3985 );
3986 });
3987
3988 let editor = cx.add_window(|window, cx| {
3989 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3990 build_editor(buffer, window, cx)
3991 });
3992 _ = editor.update(cx, |editor, window, cx| {
3993 editor.change_selections(None, window, cx, |s| {
3994 s.select_display_ranges([
3995 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
3996 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
3997 ])
3998 });
3999 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4000 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4001 assert_eq!(
4002 editor.selections.display_ranges(cx),
4003 vec![
4004 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4005 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4006 ]
4007 );
4008 });
4009
4010 let editor = cx.add_window(|window, cx| {
4011 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4012 build_editor(buffer, window, cx)
4013 });
4014 _ = editor.update(cx, |editor, window, cx| {
4015 editor.change_selections(None, window, cx, |s| {
4016 s.select_display_ranges([
4017 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4018 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4019 ])
4020 });
4021 editor.duplicate_selection(&DuplicateSelection, window, cx);
4022 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4023 assert_eq!(
4024 editor.selections.display_ranges(cx),
4025 vec![
4026 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4027 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4028 ]
4029 );
4030 });
4031}
4032
4033#[gpui::test]
4034fn test_move_line_up_down(cx: &mut TestAppContext) {
4035 init_test(cx, |_| {});
4036
4037 let editor = cx.add_window(|window, cx| {
4038 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4039 build_editor(buffer, window, cx)
4040 });
4041 _ = editor.update(cx, |editor, window, cx| {
4042 editor.fold_creases(
4043 vec![
4044 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4045 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4046 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4047 ],
4048 true,
4049 window,
4050 cx,
4051 );
4052 editor.change_selections(None, window, cx, |s| {
4053 s.select_display_ranges([
4054 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4055 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4056 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4057 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4058 ])
4059 });
4060 assert_eq!(
4061 editor.display_text(cx),
4062 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4063 );
4064
4065 editor.move_line_up(&MoveLineUp, window, cx);
4066 assert_eq!(
4067 editor.display_text(cx),
4068 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4069 );
4070 assert_eq!(
4071 editor.selections.display_ranges(cx),
4072 vec![
4073 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4074 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4075 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4076 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4077 ]
4078 );
4079 });
4080
4081 _ = editor.update(cx, |editor, window, cx| {
4082 editor.move_line_down(&MoveLineDown, window, cx);
4083 assert_eq!(
4084 editor.display_text(cx),
4085 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4086 );
4087 assert_eq!(
4088 editor.selections.display_ranges(cx),
4089 vec![
4090 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4091 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4092 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4093 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4094 ]
4095 );
4096 });
4097
4098 _ = editor.update(cx, |editor, window, cx| {
4099 editor.move_line_down(&MoveLineDown, window, cx);
4100 assert_eq!(
4101 editor.display_text(cx),
4102 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4103 );
4104 assert_eq!(
4105 editor.selections.display_ranges(cx),
4106 vec![
4107 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4108 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4109 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4110 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4111 ]
4112 );
4113 });
4114
4115 _ = editor.update(cx, |editor, window, cx| {
4116 editor.move_line_up(&MoveLineUp, window, cx);
4117 assert_eq!(
4118 editor.display_text(cx),
4119 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4120 );
4121 assert_eq!(
4122 editor.selections.display_ranges(cx),
4123 vec![
4124 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4125 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4126 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4127 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4128 ]
4129 );
4130 });
4131}
4132
4133#[gpui::test]
4134fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4135 init_test(cx, |_| {});
4136
4137 let editor = cx.add_window(|window, cx| {
4138 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4139 build_editor(buffer, window, cx)
4140 });
4141 _ = editor.update(cx, |editor, window, cx| {
4142 let snapshot = editor.buffer.read(cx).snapshot(cx);
4143 editor.insert_blocks(
4144 [BlockProperties {
4145 style: BlockStyle::Fixed,
4146 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4147 height: 1,
4148 render: Arc::new(|_| div().into_any()),
4149 priority: 0,
4150 }],
4151 Some(Autoscroll::fit()),
4152 cx,
4153 );
4154 editor.change_selections(None, window, cx, |s| {
4155 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4156 });
4157 editor.move_line_down(&MoveLineDown, window, cx);
4158 });
4159}
4160
4161#[gpui::test]
4162async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4163 init_test(cx, |_| {});
4164
4165 let mut cx = EditorTestContext::new(cx).await;
4166 cx.set_state(
4167 &"
4168 ˇzero
4169 one
4170 two
4171 three
4172 four
4173 five
4174 "
4175 .unindent(),
4176 );
4177
4178 // Create a four-line block that replaces three lines of text.
4179 cx.update_editor(|editor, window, cx| {
4180 let snapshot = editor.snapshot(window, cx);
4181 let snapshot = &snapshot.buffer_snapshot;
4182 let placement = BlockPlacement::Replace(
4183 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4184 );
4185 editor.insert_blocks(
4186 [BlockProperties {
4187 placement,
4188 height: 4,
4189 style: BlockStyle::Sticky,
4190 render: Arc::new(|_| gpui::div().into_any_element()),
4191 priority: 0,
4192 }],
4193 None,
4194 cx,
4195 );
4196 });
4197
4198 // Move down so that the cursor touches the block.
4199 cx.update_editor(|editor, window, cx| {
4200 editor.move_down(&Default::default(), window, cx);
4201 });
4202 cx.assert_editor_state(
4203 &"
4204 zero
4205 «one
4206 two
4207 threeˇ»
4208 four
4209 five
4210 "
4211 .unindent(),
4212 );
4213
4214 // Move down past the block.
4215 cx.update_editor(|editor, window, cx| {
4216 editor.move_down(&Default::default(), window, cx);
4217 });
4218 cx.assert_editor_state(
4219 &"
4220 zero
4221 one
4222 two
4223 three
4224 ˇfour
4225 five
4226 "
4227 .unindent(),
4228 );
4229}
4230
4231#[gpui::test]
4232fn test_transpose(cx: &mut TestAppContext) {
4233 init_test(cx, |_| {});
4234
4235 _ = cx.add_window(|window, cx| {
4236 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4237 editor.set_style(EditorStyle::default(), window, cx);
4238 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4239 editor.transpose(&Default::default(), window, cx);
4240 assert_eq!(editor.text(cx), "bac");
4241 assert_eq!(editor.selections.ranges(cx), [2..2]);
4242
4243 editor.transpose(&Default::default(), window, cx);
4244 assert_eq!(editor.text(cx), "bca");
4245 assert_eq!(editor.selections.ranges(cx), [3..3]);
4246
4247 editor.transpose(&Default::default(), window, cx);
4248 assert_eq!(editor.text(cx), "bac");
4249 assert_eq!(editor.selections.ranges(cx), [3..3]);
4250
4251 editor
4252 });
4253
4254 _ = cx.add_window(|window, cx| {
4255 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4256 editor.set_style(EditorStyle::default(), window, cx);
4257 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4258 editor.transpose(&Default::default(), window, cx);
4259 assert_eq!(editor.text(cx), "acb\nde");
4260 assert_eq!(editor.selections.ranges(cx), [3..3]);
4261
4262 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4263 editor.transpose(&Default::default(), window, cx);
4264 assert_eq!(editor.text(cx), "acbd\ne");
4265 assert_eq!(editor.selections.ranges(cx), [5..5]);
4266
4267 editor.transpose(&Default::default(), window, cx);
4268 assert_eq!(editor.text(cx), "acbde\n");
4269 assert_eq!(editor.selections.ranges(cx), [6..6]);
4270
4271 editor.transpose(&Default::default(), window, cx);
4272 assert_eq!(editor.text(cx), "acbd\ne");
4273 assert_eq!(editor.selections.ranges(cx), [6..6]);
4274
4275 editor
4276 });
4277
4278 _ = cx.add_window(|window, cx| {
4279 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4280 editor.set_style(EditorStyle::default(), window, cx);
4281 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4282 editor.transpose(&Default::default(), window, cx);
4283 assert_eq!(editor.text(cx), "bacd\ne");
4284 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4285
4286 editor.transpose(&Default::default(), window, cx);
4287 assert_eq!(editor.text(cx), "bcade\n");
4288 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4289
4290 editor.transpose(&Default::default(), window, cx);
4291 assert_eq!(editor.text(cx), "bcda\ne");
4292 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4293
4294 editor.transpose(&Default::default(), window, cx);
4295 assert_eq!(editor.text(cx), "bcade\n");
4296 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4297
4298 editor.transpose(&Default::default(), window, cx);
4299 assert_eq!(editor.text(cx), "bcaed\n");
4300 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4301
4302 editor
4303 });
4304
4305 _ = cx.add_window(|window, cx| {
4306 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4307 editor.set_style(EditorStyle::default(), window, cx);
4308 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4309 editor.transpose(&Default::default(), window, cx);
4310 assert_eq!(editor.text(cx), "🏀🍐✋");
4311 assert_eq!(editor.selections.ranges(cx), [8..8]);
4312
4313 editor.transpose(&Default::default(), window, cx);
4314 assert_eq!(editor.text(cx), "🏀✋🍐");
4315 assert_eq!(editor.selections.ranges(cx), [11..11]);
4316
4317 editor.transpose(&Default::default(), window, cx);
4318 assert_eq!(editor.text(cx), "🏀🍐✋");
4319 assert_eq!(editor.selections.ranges(cx), [11..11]);
4320
4321 editor
4322 });
4323}
4324
4325#[gpui::test]
4326async fn test_rewrap(cx: &mut TestAppContext) {
4327 init_test(cx, |settings| {
4328 settings.languages.extend([
4329 (
4330 "Markdown".into(),
4331 LanguageSettingsContent {
4332 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4333 ..Default::default()
4334 },
4335 ),
4336 (
4337 "Plain Text".into(),
4338 LanguageSettingsContent {
4339 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4340 ..Default::default()
4341 },
4342 ),
4343 ])
4344 });
4345
4346 let mut cx = EditorTestContext::new(cx).await;
4347
4348 let language_with_c_comments = Arc::new(Language::new(
4349 LanguageConfig {
4350 line_comments: vec!["// ".into()],
4351 ..LanguageConfig::default()
4352 },
4353 None,
4354 ));
4355 let language_with_pound_comments = Arc::new(Language::new(
4356 LanguageConfig {
4357 line_comments: vec!["# ".into()],
4358 ..LanguageConfig::default()
4359 },
4360 None,
4361 ));
4362 let markdown_language = Arc::new(Language::new(
4363 LanguageConfig {
4364 name: "Markdown".into(),
4365 ..LanguageConfig::default()
4366 },
4367 None,
4368 ));
4369 let language_with_doc_comments = Arc::new(Language::new(
4370 LanguageConfig {
4371 line_comments: vec!["// ".into(), "/// ".into()],
4372 ..LanguageConfig::default()
4373 },
4374 Some(tree_sitter_rust::LANGUAGE.into()),
4375 ));
4376
4377 let plaintext_language = Arc::new(Language::new(
4378 LanguageConfig {
4379 name: "Plain Text".into(),
4380 ..LanguageConfig::default()
4381 },
4382 None,
4383 ));
4384
4385 assert_rewrap(
4386 indoc! {"
4387 // ˇ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.
4388 "},
4389 indoc! {"
4390 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4391 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4392 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4393 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4394 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4395 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4396 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4397 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4398 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4399 // porttitor id. Aliquam id accumsan eros.
4400 "},
4401 language_with_c_comments.clone(),
4402 &mut cx,
4403 );
4404
4405 // Test that rewrapping works inside of a selection
4406 assert_rewrap(
4407 indoc! {"
4408 «// 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.ˇ»
4409 "},
4410 indoc! {"
4411 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4412 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4413 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4414 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4415 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4416 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4417 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4418 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4419 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4420 // porttitor id. Aliquam id accumsan eros.ˇ»
4421 "},
4422 language_with_c_comments.clone(),
4423 &mut cx,
4424 );
4425
4426 // Test that cursors that expand to the same region are collapsed.
4427 assert_rewrap(
4428 indoc! {"
4429 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4430 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4431 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4432 // ˇ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.
4433 "},
4434 indoc! {"
4435 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4436 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4437 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4438 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4439 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4440 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4441 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4442 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4443 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4444 // porttitor id. Aliquam id accumsan eros.
4445 "},
4446 language_with_c_comments.clone(),
4447 &mut cx,
4448 );
4449
4450 // Test that non-contiguous selections are treated separately.
4451 assert_rewrap(
4452 indoc! {"
4453 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4454 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4455 //
4456 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4457 // ˇ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.
4458 "},
4459 indoc! {"
4460 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4461 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4462 // auctor, eu lacinia sapien scelerisque.
4463 //
4464 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4465 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4466 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4467 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4468 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4469 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4470 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4471 "},
4472 language_with_c_comments.clone(),
4473 &mut cx,
4474 );
4475
4476 // Test that different comment prefixes are supported.
4477 assert_rewrap(
4478 indoc! {"
4479 # ˇ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.
4480 "},
4481 indoc! {"
4482 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4483 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4484 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4485 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4486 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4487 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4488 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4489 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4490 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4491 # accumsan eros.
4492 "},
4493 language_with_pound_comments.clone(),
4494 &mut cx,
4495 );
4496
4497 // Test that rewrapping is ignored outside of comments in most languages.
4498 assert_rewrap(
4499 indoc! {"
4500 /// Adds two numbers.
4501 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4502 fn add(a: u32, b: u32) -> u32 {
4503 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ˇ
4504 }
4505 "},
4506 indoc! {"
4507 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4508 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4509 fn add(a: u32, b: u32) -> u32 {
4510 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ˇ
4511 }
4512 "},
4513 language_with_doc_comments.clone(),
4514 &mut cx,
4515 );
4516
4517 // Test that rewrapping works in Markdown and Plain Text languages.
4518 assert_rewrap(
4519 indoc! {"
4520 # Hello
4521
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 # Hello
4526
4527 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4528 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4529 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4530 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4531 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4532 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4533 Integer sit amet scelerisque nisi.
4534 "},
4535 markdown_language,
4536 &mut cx,
4537 );
4538
4539 assert_rewrap(
4540 indoc! {"
4541 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.
4542 "},
4543 indoc! {"
4544 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4545 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4546 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4547 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4548 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4549 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4550 Integer sit amet scelerisque nisi.
4551 "},
4552 plaintext_language,
4553 &mut cx,
4554 );
4555
4556 // Test rewrapping unaligned comments in a selection.
4557 assert_rewrap(
4558 indoc! {"
4559 fn foo() {
4560 if true {
4561 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4562 // Praesent semper egestas tellus id dignissim.ˇ»
4563 do_something();
4564 } else {
4565 //
4566 }
4567 }
4568 "},
4569 indoc! {"
4570 fn foo() {
4571 if true {
4572 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4573 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4574 // egestas tellus id dignissim.ˇ»
4575 do_something();
4576 } else {
4577 //
4578 }
4579 }
4580 "},
4581 language_with_doc_comments.clone(),
4582 &mut cx,
4583 );
4584
4585 assert_rewrap(
4586 indoc! {"
4587 fn foo() {
4588 if true {
4589 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4590 // Praesent semper egestas tellus id dignissim.»
4591 do_something();
4592 } else {
4593 //
4594 }
4595
4596 }
4597 "},
4598 indoc! {"
4599 fn foo() {
4600 if true {
4601 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4602 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4603 // egestas tellus id dignissim.»
4604 do_something();
4605 } else {
4606 //
4607 }
4608
4609 }
4610 "},
4611 language_with_doc_comments.clone(),
4612 &mut cx,
4613 );
4614
4615 #[track_caller]
4616 fn assert_rewrap(
4617 unwrapped_text: &str,
4618 wrapped_text: &str,
4619 language: Arc<Language>,
4620 cx: &mut EditorTestContext,
4621 ) {
4622 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4623 cx.set_state(unwrapped_text);
4624 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4625 cx.assert_editor_state(wrapped_text);
4626 }
4627}
4628
4629#[gpui::test]
4630async fn test_clipboard(cx: &mut TestAppContext) {
4631 init_test(cx, |_| {});
4632
4633 let mut cx = EditorTestContext::new(cx).await;
4634
4635 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4636 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4637 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4638
4639 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4640 cx.set_state("two ˇfour ˇsix ˇ");
4641 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4642 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4643
4644 // Paste again but with only two cursors. Since the number of cursors doesn't
4645 // match the number of slices in the clipboard, the entire clipboard text
4646 // is pasted at each cursor.
4647 cx.set_state("ˇtwo one✅ four three six five ˇ");
4648 cx.update_editor(|e, window, cx| {
4649 e.handle_input("( ", window, cx);
4650 e.paste(&Paste, window, cx);
4651 e.handle_input(") ", window, cx);
4652 });
4653 cx.assert_editor_state(
4654 &([
4655 "( one✅ ",
4656 "three ",
4657 "five ) ˇtwo one✅ four three six five ( one✅ ",
4658 "three ",
4659 "five ) ˇ",
4660 ]
4661 .join("\n")),
4662 );
4663
4664 // Cut with three selections, one of which is full-line.
4665 cx.set_state(indoc! {"
4666 1«2ˇ»3
4667 4ˇ567
4668 «8ˇ»9"});
4669 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4670 cx.assert_editor_state(indoc! {"
4671 1ˇ3
4672 ˇ9"});
4673
4674 // Paste with three selections, noticing how the copied selection that was full-line
4675 // gets inserted before the second cursor.
4676 cx.set_state(indoc! {"
4677 1ˇ3
4678 9ˇ
4679 «oˇ»ne"});
4680 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4681 cx.assert_editor_state(indoc! {"
4682 12ˇ3
4683 4567
4684 9ˇ
4685 8ˇne"});
4686
4687 // Copy with a single cursor only, which writes the whole line into the clipboard.
4688 cx.set_state(indoc! {"
4689 The quick brown
4690 fox juˇmps over
4691 the lazy dog"});
4692 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4693 assert_eq!(
4694 cx.read_from_clipboard()
4695 .and_then(|item| item.text().as_deref().map(str::to_string)),
4696 Some("fox jumps over\n".to_string())
4697 );
4698
4699 // Paste with three selections, noticing how the copied full-line selection is inserted
4700 // before the empty selections but replaces the selection that is non-empty.
4701 cx.set_state(indoc! {"
4702 Tˇhe quick brown
4703 «foˇ»x jumps over
4704 tˇhe lazy dog"});
4705 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4706 cx.assert_editor_state(indoc! {"
4707 fox jumps over
4708 Tˇhe quick brown
4709 fox jumps over
4710 ˇx jumps over
4711 fox jumps over
4712 tˇhe lazy dog"});
4713}
4714
4715#[gpui::test]
4716async fn test_paste_multiline(cx: &mut TestAppContext) {
4717 init_test(cx, |_| {});
4718
4719 let mut cx = EditorTestContext::new(cx).await;
4720 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
4721
4722 // Cut an indented block, without the leading whitespace.
4723 cx.set_state(indoc! {"
4724 const a: B = (
4725 c(),
4726 «d(
4727 e,
4728 f
4729 )ˇ»
4730 );
4731 "});
4732 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4733 cx.assert_editor_state(indoc! {"
4734 const a: B = (
4735 c(),
4736 ˇ
4737 );
4738 "});
4739
4740 // Paste it at the same position.
4741 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4742 cx.assert_editor_state(indoc! {"
4743 const a: B = (
4744 c(),
4745 d(
4746 e,
4747 f
4748 )ˇ
4749 );
4750 "});
4751
4752 // Paste it at a line with a lower indent level.
4753 cx.set_state(indoc! {"
4754 ˇ
4755 const a: B = (
4756 c(),
4757 );
4758 "});
4759 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4760 cx.assert_editor_state(indoc! {"
4761 d(
4762 e,
4763 f
4764 )ˇ
4765 const a: B = (
4766 c(),
4767 );
4768 "});
4769
4770 // Cut an indented block, with the leading whitespace.
4771 cx.set_state(indoc! {"
4772 const a: B = (
4773 c(),
4774 « d(
4775 e,
4776 f
4777 )
4778 ˇ»);
4779 "});
4780 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4781 cx.assert_editor_state(indoc! {"
4782 const a: B = (
4783 c(),
4784 ˇ);
4785 "});
4786
4787 // Paste it at the same position.
4788 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4789 cx.assert_editor_state(indoc! {"
4790 const a: B = (
4791 c(),
4792 d(
4793 e,
4794 f
4795 )
4796 ˇ);
4797 "});
4798
4799 // Paste it at a line with a higher indent level.
4800 cx.set_state(indoc! {"
4801 const a: B = (
4802 c(),
4803 d(
4804 e,
4805 fˇ
4806 )
4807 );
4808 "});
4809 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4810 cx.assert_editor_state(indoc! {"
4811 const a: B = (
4812 c(),
4813 d(
4814 e,
4815 f d(
4816 e,
4817 f
4818 )
4819 ˇ
4820 )
4821 );
4822 "});
4823}
4824
4825#[gpui::test]
4826async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
4827 init_test(cx, |_| {});
4828
4829 cx.write_to_clipboard(ClipboardItem::new_string(
4830 " d(\n e\n );\n".into(),
4831 ));
4832
4833 let mut cx = EditorTestContext::new(cx).await;
4834 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
4835
4836 cx.set_state(indoc! {"
4837 fn a() {
4838 b();
4839 if c() {
4840 ˇ
4841 }
4842 }
4843 "});
4844
4845 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4846 cx.assert_editor_state(indoc! {"
4847 fn a() {
4848 b();
4849 if c() {
4850 d(
4851 e
4852 );
4853 ˇ
4854 }
4855 }
4856 "});
4857
4858 cx.set_state(indoc! {"
4859 fn a() {
4860 b();
4861 ˇ
4862 }
4863 "});
4864
4865 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4866 cx.assert_editor_state(indoc! {"
4867 fn a() {
4868 b();
4869 d(
4870 e
4871 );
4872 ˇ
4873 }
4874 "});
4875}
4876
4877#[gpui::test]
4878fn test_select_all(cx: &mut TestAppContext) {
4879 init_test(cx, |_| {});
4880
4881 let editor = cx.add_window(|window, cx| {
4882 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
4883 build_editor(buffer, window, cx)
4884 });
4885 _ = editor.update(cx, |editor, window, cx| {
4886 editor.select_all(&SelectAll, window, cx);
4887 assert_eq!(
4888 editor.selections.display_ranges(cx),
4889 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
4890 );
4891 });
4892}
4893
4894#[gpui::test]
4895fn test_select_line(cx: &mut TestAppContext) {
4896 init_test(cx, |_| {});
4897
4898 let editor = cx.add_window(|window, cx| {
4899 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
4900 build_editor(buffer, window, cx)
4901 });
4902 _ = editor.update(cx, |editor, window, cx| {
4903 editor.change_selections(None, window, cx, |s| {
4904 s.select_display_ranges([
4905 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4906 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4907 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4908 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
4909 ])
4910 });
4911 editor.select_line(&SelectLine, window, cx);
4912 assert_eq!(
4913 editor.selections.display_ranges(cx),
4914 vec![
4915 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
4916 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
4917 ]
4918 );
4919 });
4920
4921 _ = editor.update(cx, |editor, window, cx| {
4922 editor.select_line(&SelectLine, window, cx);
4923 assert_eq!(
4924 editor.selections.display_ranges(cx),
4925 vec![
4926 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
4927 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
4928 ]
4929 );
4930 });
4931
4932 _ = editor.update(cx, |editor, window, cx| {
4933 editor.select_line(&SelectLine, window, cx);
4934 assert_eq!(
4935 editor.selections.display_ranges(cx),
4936 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
4937 );
4938 });
4939}
4940
4941#[gpui::test]
4942async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
4943 init_test(cx, |_| {});
4944 let mut cx = EditorTestContext::new(cx).await;
4945
4946 #[track_caller]
4947 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
4948 cx.set_state(initial_state);
4949 cx.update_editor(|e, window, cx| {
4950 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
4951 });
4952 cx.assert_editor_state(expected_state);
4953 }
4954
4955 // Selection starts and ends at the middle of lines, left-to-right
4956 test(
4957 &mut cx,
4958 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
4959 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
4960 );
4961 // Same thing, right-to-left
4962 test(
4963 &mut cx,
4964 "aa\nb«b\ncc\ndd\neˇ»e\nff",
4965 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
4966 );
4967
4968 // Whole buffer, left-to-right, last line *doesn't* end with newline
4969 test(
4970 &mut cx,
4971 "«ˇaa\nbb\ncc\ndd\nee\nff»",
4972 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
4973 );
4974 // Same thing, right-to-left
4975 test(
4976 &mut cx,
4977 "«aa\nbb\ncc\ndd\nee\nffˇ»",
4978 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
4979 );
4980
4981 // Whole buffer, left-to-right, last line ends with newline
4982 test(
4983 &mut cx,
4984 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
4985 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
4986 );
4987 // Same thing, right-to-left
4988 test(
4989 &mut cx,
4990 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
4991 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
4992 );
4993
4994 // Starts at the end of a line, ends at the start of another
4995 test(
4996 &mut cx,
4997 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
4998 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
4999 );
5000}
5001
5002#[gpui::test]
5003async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5004 init_test(cx, |_| {});
5005
5006 let editor = cx.add_window(|window, cx| {
5007 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5008 build_editor(buffer, window, cx)
5009 });
5010
5011 // setup
5012 _ = editor.update(cx, |editor, window, cx| {
5013 editor.fold_creases(
5014 vec![
5015 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5016 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5017 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5018 ],
5019 true,
5020 window,
5021 cx,
5022 );
5023 assert_eq!(
5024 editor.display_text(cx),
5025 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5026 );
5027 });
5028
5029 _ = editor.update(cx, |editor, window, cx| {
5030 editor.change_selections(None, window, cx, |s| {
5031 s.select_display_ranges([
5032 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5033 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5034 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5035 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5036 ])
5037 });
5038 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5039 assert_eq!(
5040 editor.display_text(cx),
5041 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5042 );
5043 });
5044 EditorTestContext::for_editor(editor, cx)
5045 .await
5046 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5047
5048 _ = editor.update(cx, |editor, window, cx| {
5049 editor.change_selections(None, window, cx, |s| {
5050 s.select_display_ranges([
5051 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5052 ])
5053 });
5054 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5055 assert_eq!(
5056 editor.display_text(cx),
5057 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5058 );
5059 assert_eq!(
5060 editor.selections.display_ranges(cx),
5061 [
5062 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5063 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5064 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5065 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5066 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5067 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5068 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5069 ]
5070 );
5071 });
5072 EditorTestContext::for_editor(editor, cx)
5073 .await
5074 .assert_editor_state(
5075 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5076 );
5077}
5078
5079#[gpui::test]
5080async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5081 init_test(cx, |_| {});
5082
5083 let mut cx = EditorTestContext::new(cx).await;
5084
5085 cx.set_state(indoc!(
5086 r#"abc
5087 defˇghi
5088
5089 jk
5090 nlmo
5091 "#
5092 ));
5093
5094 cx.update_editor(|editor, window, cx| {
5095 editor.add_selection_above(&Default::default(), window, cx);
5096 });
5097
5098 cx.assert_editor_state(indoc!(
5099 r#"abcˇ
5100 defˇghi
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ˇghi
5114
5115 jk
5116 nlmo
5117 "#
5118 ));
5119
5120 cx.update_editor(|editor, window, cx| {
5121 editor.add_selection_below(&Default::default(), window, cx);
5122 });
5123
5124 cx.assert_editor_state(indoc!(
5125 r#"abc
5126 defˇghi
5127
5128 jk
5129 nlmo
5130 "#
5131 ));
5132
5133 cx.update_editor(|editor, window, cx| {
5134 editor.undo_selection(&Default::default(), window, cx);
5135 });
5136
5137 cx.assert_editor_state(indoc!(
5138 r#"abcˇ
5139 defˇghi
5140
5141 jk
5142 nlmo
5143 "#
5144 ));
5145
5146 cx.update_editor(|editor, window, cx| {
5147 editor.redo_selection(&Default::default(), window, cx);
5148 });
5149
5150 cx.assert_editor_state(indoc!(
5151 r#"abc
5152 defˇghi
5153
5154 jk
5155 nlmo
5156 "#
5157 ));
5158
5159 cx.update_editor(|editor, window, cx| {
5160 editor.add_selection_below(&Default::default(), window, cx);
5161 });
5162
5163 cx.assert_editor_state(indoc!(
5164 r#"abc
5165 defˇghi
5166
5167 jk
5168 nlmˇo
5169 "#
5170 ));
5171
5172 cx.update_editor(|editor, window, cx| {
5173 editor.add_selection_below(&Default::default(), window, cx);
5174 });
5175
5176 cx.assert_editor_state(indoc!(
5177 r#"abc
5178 defˇghi
5179
5180 jk
5181 nlmˇo
5182 "#
5183 ));
5184
5185 // change selections
5186 cx.set_state(indoc!(
5187 r#"abc
5188 def«ˇg»hi
5189
5190 jk
5191 nlmo
5192 "#
5193 ));
5194
5195 cx.update_editor(|editor, window, cx| {
5196 editor.add_selection_below(&Default::default(), window, cx);
5197 });
5198
5199 cx.assert_editor_state(indoc!(
5200 r#"abc
5201 def«ˇg»hi
5202
5203 jk
5204 nlm«ˇo»
5205 "#
5206 ));
5207
5208 cx.update_editor(|editor, window, cx| {
5209 editor.add_selection_below(&Default::default(), window, cx);
5210 });
5211
5212 cx.assert_editor_state(indoc!(
5213 r#"abc
5214 def«ˇg»hi
5215
5216 jk
5217 nlm«ˇo»
5218 "#
5219 ));
5220
5221 cx.update_editor(|editor, window, cx| {
5222 editor.add_selection_above(&Default::default(), window, cx);
5223 });
5224
5225 cx.assert_editor_state(indoc!(
5226 r#"abc
5227 def«ˇg»hi
5228
5229 jk
5230 nlmo
5231 "#
5232 ));
5233
5234 cx.update_editor(|editor, window, cx| {
5235 editor.add_selection_above(&Default::default(), window, cx);
5236 });
5237
5238 cx.assert_editor_state(indoc!(
5239 r#"abc
5240 def«ˇg»hi
5241
5242 jk
5243 nlmo
5244 "#
5245 ));
5246
5247 // Change selections again
5248 cx.set_state(indoc!(
5249 r#"a«bc
5250 defgˇ»hi
5251
5252 jk
5253 nlmo
5254 "#
5255 ));
5256
5257 cx.update_editor(|editor, window, cx| {
5258 editor.add_selection_below(&Default::default(), window, cx);
5259 });
5260
5261 cx.assert_editor_state(indoc!(
5262 r#"a«bcˇ»
5263 d«efgˇ»hi
5264
5265 j«kˇ»
5266 nlmo
5267 "#
5268 ));
5269
5270 cx.update_editor(|editor, window, cx| {
5271 editor.add_selection_below(&Default::default(), window, cx);
5272 });
5273 cx.assert_editor_state(indoc!(
5274 r#"a«bcˇ»
5275 d«efgˇ»hi
5276
5277 j«kˇ»
5278 n«lmoˇ»
5279 "#
5280 ));
5281 cx.update_editor(|editor, window, cx| {
5282 editor.add_selection_above(&Default::default(), window, cx);
5283 });
5284
5285 cx.assert_editor_state(indoc!(
5286 r#"a«bcˇ»
5287 d«efgˇ»hi
5288
5289 j«kˇ»
5290 nlmo
5291 "#
5292 ));
5293
5294 // Change selections again
5295 cx.set_state(indoc!(
5296 r#"abc
5297 d«ˇefghi
5298
5299 jk
5300 nlm»o
5301 "#
5302 ));
5303
5304 cx.update_editor(|editor, window, cx| {
5305 editor.add_selection_above(&Default::default(), window, cx);
5306 });
5307
5308 cx.assert_editor_state(indoc!(
5309 r#"a«ˇbc»
5310 d«ˇef»ghi
5311
5312 j«ˇk»
5313 n«ˇlm»o
5314 "#
5315 ));
5316
5317 cx.update_editor(|editor, window, cx| {
5318 editor.add_selection_below(&Default::default(), window, cx);
5319 });
5320
5321 cx.assert_editor_state(indoc!(
5322 r#"abc
5323 d«ˇef»ghi
5324
5325 j«ˇk»
5326 n«ˇlm»o
5327 "#
5328 ));
5329}
5330
5331#[gpui::test]
5332async fn test_select_next(cx: &mut TestAppContext) {
5333 init_test(cx, |_| {});
5334
5335 let mut cx = EditorTestContext::new(cx).await;
5336 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5337
5338 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5339 .unwrap();
5340 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5341
5342 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5343 .unwrap();
5344 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5345
5346 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5347 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5348
5349 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5350 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5351
5352 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5353 .unwrap();
5354 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5355
5356 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5357 .unwrap();
5358 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5359}
5360
5361#[gpui::test]
5362async fn test_select_all_matches(cx: &mut TestAppContext) {
5363 init_test(cx, |_| {});
5364
5365 let mut cx = EditorTestContext::new(cx).await;
5366
5367 // Test caret-only selections
5368 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5369 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5370 .unwrap();
5371 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5372
5373 // Test left-to-right selections
5374 cx.set_state("abc\n«abcˇ»\nabc");
5375 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5376 .unwrap();
5377 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5378
5379 // Test right-to-left selections
5380 cx.set_state("abc\n«ˇabc»\nabc");
5381 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5382 .unwrap();
5383 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5384
5385 // Test selecting whitespace with caret selection
5386 cx.set_state("abc\nˇ abc\nabc");
5387 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5388 .unwrap();
5389 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5390
5391 // Test selecting whitespace with left-to-right selection
5392 cx.set_state("abc\n«ˇ »abc\nabc");
5393 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5394 .unwrap();
5395 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
5396
5397 // Test no matches with right-to-left selection
5398 cx.set_state("abc\n« ˇ»abc\nabc");
5399 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5400 .unwrap();
5401 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5402}
5403
5404#[gpui::test]
5405async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
5406 init_test(cx, |_| {});
5407
5408 let mut cx = EditorTestContext::new(cx).await;
5409 cx.set_state(
5410 r#"let foo = 2;
5411lˇet foo = 2;
5412let fooˇ = 2;
5413let foo = 2;
5414let foo = ˇ2;"#,
5415 );
5416
5417 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5418 .unwrap();
5419 cx.assert_editor_state(
5420 r#"let foo = 2;
5421«letˇ» foo = 2;
5422let «fooˇ» = 2;
5423let foo = 2;
5424let foo = «2ˇ»;"#,
5425 );
5426
5427 // noop for multiple selections with different contents
5428 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5429 .unwrap();
5430 cx.assert_editor_state(
5431 r#"let foo = 2;
5432«letˇ» foo = 2;
5433let «fooˇ» = 2;
5434let foo = 2;
5435let foo = «2ˇ»;"#,
5436 );
5437}
5438
5439#[gpui::test]
5440async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
5441 init_test(cx, |_| {});
5442
5443 let mut cx =
5444 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
5445
5446 cx.assert_editor_state(indoc! {"
5447 ˇbbb
5448 ccc
5449
5450 bbb
5451 ccc
5452 "});
5453 cx.dispatch_action(SelectPrevious::default());
5454 cx.assert_editor_state(indoc! {"
5455 «bbbˇ»
5456 ccc
5457
5458 bbb
5459 ccc
5460 "});
5461 cx.dispatch_action(SelectPrevious::default());
5462 cx.assert_editor_state(indoc! {"
5463 «bbbˇ»
5464 ccc
5465
5466 «bbbˇ»
5467 ccc
5468 "});
5469}
5470
5471#[gpui::test]
5472async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
5473 init_test(cx, |_| {});
5474
5475 let mut cx = EditorTestContext::new(cx).await;
5476 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5477
5478 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5479 .unwrap();
5480 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5481
5482 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5483 .unwrap();
5484 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5485
5486 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5487 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5488
5489 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5490 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5491
5492 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5493 .unwrap();
5494 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
5495
5496 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5497 .unwrap();
5498 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
5499
5500 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5501 .unwrap();
5502 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5503}
5504
5505#[gpui::test]
5506async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
5507 init_test(cx, |_| {});
5508
5509 let mut cx = EditorTestContext::new(cx).await;
5510 cx.set_state("aˇ");
5511
5512 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5513 .unwrap();
5514 cx.assert_editor_state("«aˇ»");
5515 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5516 .unwrap();
5517 cx.assert_editor_state("«aˇ»");
5518}
5519
5520#[gpui::test]
5521async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
5522 init_test(cx, |_| {});
5523
5524 let mut cx = EditorTestContext::new(cx).await;
5525 cx.set_state(
5526 r#"let foo = 2;
5527lˇet foo = 2;
5528let fooˇ = 2;
5529let foo = 2;
5530let foo = ˇ2;"#,
5531 );
5532
5533 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5534 .unwrap();
5535 cx.assert_editor_state(
5536 r#"let foo = 2;
5537«letˇ» foo = 2;
5538let «fooˇ» = 2;
5539let foo = 2;
5540let foo = «2ˇ»;"#,
5541 );
5542
5543 // noop for multiple selections with different contents
5544 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5545 .unwrap();
5546 cx.assert_editor_state(
5547 r#"let foo = 2;
5548«letˇ» foo = 2;
5549let «fooˇ» = 2;
5550let foo = 2;
5551let foo = «2ˇ»;"#,
5552 );
5553}
5554
5555#[gpui::test]
5556async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
5557 init_test(cx, |_| {});
5558
5559 let mut cx = EditorTestContext::new(cx).await;
5560 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5561
5562 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5563 .unwrap();
5564 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5565
5566 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5567 .unwrap();
5568 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5569
5570 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5571 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5572
5573 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5574 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5575
5576 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5577 .unwrap();
5578 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
5579
5580 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5581 .unwrap();
5582 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5583}
5584
5585#[gpui::test]
5586async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
5587 init_test(cx, |_| {});
5588
5589 let language = Arc::new(Language::new(
5590 LanguageConfig::default(),
5591 Some(tree_sitter_rust::LANGUAGE.into()),
5592 ));
5593
5594 let text = r#"
5595 use mod1::mod2::{mod3, mod4};
5596
5597 fn fn_1(param1: bool, param2: &str) {
5598 let var1 = "text";
5599 }
5600 "#
5601 .unindent();
5602
5603 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5604 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5605 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5606
5607 editor
5608 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5609 .await;
5610
5611 editor.update_in(cx, |editor, window, cx| {
5612 editor.change_selections(None, window, cx, |s| {
5613 s.select_display_ranges([
5614 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
5615 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
5616 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
5617 ]);
5618 });
5619 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5620 });
5621 editor.update(cx, |editor, cx| {
5622 assert_text_with_selections(
5623 editor,
5624 indoc! {r#"
5625 use mod1::mod2::{mod3, «mod4ˇ»};
5626
5627 fn fn_1«ˇ(param1: bool, param2: &str)» {
5628 let var1 = "«textˇ»";
5629 }
5630 "#},
5631 cx,
5632 );
5633 });
5634
5635 editor.update_in(cx, |editor, window, cx| {
5636 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5637 });
5638 editor.update(cx, |editor, cx| {
5639 assert_text_with_selections(
5640 editor,
5641 indoc! {r#"
5642 use mod1::mod2::«{mod3, mod4}ˇ»;
5643
5644 «ˇfn fn_1(param1: bool, param2: &str) {
5645 let var1 = "text";
5646 }»
5647 "#},
5648 cx,
5649 );
5650 });
5651
5652 editor.update_in(cx, |editor, window, cx| {
5653 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5654 });
5655 assert_eq!(
5656 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
5657 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
5658 );
5659
5660 // Trying to expand the selected syntax node one more time has no effect.
5661 editor.update_in(cx, |editor, window, cx| {
5662 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5663 });
5664 assert_eq!(
5665 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
5666 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
5667 );
5668
5669 editor.update_in(cx, |editor, window, cx| {
5670 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5671 });
5672 editor.update(cx, |editor, cx| {
5673 assert_text_with_selections(
5674 editor,
5675 indoc! {r#"
5676 use mod1::mod2::«{mod3, mod4}ˇ»;
5677
5678 «ˇfn fn_1(param1: bool, param2: &str) {
5679 let var1 = "text";
5680 }»
5681 "#},
5682 cx,
5683 );
5684 });
5685
5686 editor.update_in(cx, |editor, window, cx| {
5687 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5688 });
5689 editor.update(cx, |editor, cx| {
5690 assert_text_with_selections(
5691 editor,
5692 indoc! {r#"
5693 use mod1::mod2::{mod3, «mod4ˇ»};
5694
5695 fn fn_1«ˇ(param1: bool, param2: &str)» {
5696 let var1 = "«textˇ»";
5697 }
5698 "#},
5699 cx,
5700 );
5701 });
5702
5703 editor.update_in(cx, |editor, window, cx| {
5704 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5705 });
5706 editor.update(cx, |editor, cx| {
5707 assert_text_with_selections(
5708 editor,
5709 indoc! {r#"
5710 use mod1::mod2::{mod3, mo«ˇ»d4};
5711
5712 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
5713 let var1 = "te«ˇ»xt";
5714 }
5715 "#},
5716 cx,
5717 );
5718 });
5719
5720 // Trying to shrink the selected syntax node one more time has no effect.
5721 editor.update_in(cx, |editor, window, cx| {
5722 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5723 });
5724 editor.update_in(cx, |editor, _, cx| {
5725 assert_text_with_selections(
5726 editor,
5727 indoc! {r#"
5728 use mod1::mod2::{mod3, mo«ˇ»d4};
5729
5730 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
5731 let var1 = "te«ˇ»xt";
5732 }
5733 "#},
5734 cx,
5735 );
5736 });
5737
5738 // Ensure that we keep expanding the selection if the larger selection starts or ends within
5739 // a fold.
5740 editor.update_in(cx, |editor, window, cx| {
5741 editor.fold_creases(
5742 vec![
5743 Crease::simple(
5744 Point::new(0, 21)..Point::new(0, 24),
5745 FoldPlaceholder::test(),
5746 ),
5747 Crease::simple(
5748 Point::new(3, 20)..Point::new(3, 22),
5749 FoldPlaceholder::test(),
5750 ),
5751 ],
5752 true,
5753 window,
5754 cx,
5755 );
5756 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5757 });
5758 editor.update(cx, |editor, cx| {
5759 assert_text_with_selections(
5760 editor,
5761 indoc! {r#"
5762 use mod1::mod2::«{mod3, mod4}ˇ»;
5763
5764 fn fn_1«ˇ(param1: bool, param2: &str)» {
5765 «let var1 = "text";ˇ»
5766 }
5767 "#},
5768 cx,
5769 );
5770 });
5771}
5772
5773#[gpui::test]
5774async fn test_fold_function_bodies(cx: &mut TestAppContext) {
5775 init_test(cx, |_| {});
5776
5777 let base_text = r#"
5778 impl A {
5779 // this is an uncommitted comment
5780
5781 fn b() {
5782 c();
5783 }
5784
5785 // this is another uncommitted comment
5786
5787 fn d() {
5788 // e
5789 // f
5790 }
5791 }
5792
5793 fn g() {
5794 // h
5795 }
5796 "#
5797 .unindent();
5798
5799 let text = r#"
5800 ˇimpl A {
5801
5802 fn b() {
5803 c();
5804 }
5805
5806 fn d() {
5807 // e
5808 // f
5809 }
5810 }
5811
5812 fn g() {
5813 // h
5814 }
5815 "#
5816 .unindent();
5817
5818 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
5819 cx.set_state(&text);
5820 cx.set_head_text(&base_text);
5821 cx.update_editor(|editor, window, cx| {
5822 editor.expand_all_diff_hunks(&Default::default(), window, cx);
5823 });
5824
5825 cx.assert_state_with_diff(
5826 "
5827 ˇimpl A {
5828 - // this is an uncommitted comment
5829
5830 fn b() {
5831 c();
5832 }
5833
5834 - // this is another uncommitted comment
5835 -
5836 fn d() {
5837 // e
5838 // f
5839 }
5840 }
5841
5842 fn g() {
5843 // h
5844 }
5845 "
5846 .unindent(),
5847 );
5848
5849 let expected_display_text = "
5850 impl A {
5851 // this is an uncommitted comment
5852
5853 fn b() {
5854 ⋯
5855 }
5856
5857 // this is another uncommitted comment
5858
5859 fn d() {
5860 ⋯
5861 }
5862 }
5863
5864 fn g() {
5865 ⋯
5866 }
5867 "
5868 .unindent();
5869
5870 cx.update_editor(|editor, window, cx| {
5871 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
5872 assert_eq!(editor.display_text(cx), expected_display_text);
5873 });
5874}
5875
5876#[gpui::test]
5877async fn test_autoindent(cx: &mut TestAppContext) {
5878 init_test(cx, |_| {});
5879
5880 let language = Arc::new(
5881 Language::new(
5882 LanguageConfig {
5883 brackets: BracketPairConfig {
5884 pairs: vec![
5885 BracketPair {
5886 start: "{".to_string(),
5887 end: "}".to_string(),
5888 close: false,
5889 surround: false,
5890 newline: true,
5891 },
5892 BracketPair {
5893 start: "(".to_string(),
5894 end: ")".to_string(),
5895 close: false,
5896 surround: false,
5897 newline: true,
5898 },
5899 ],
5900 ..Default::default()
5901 },
5902 ..Default::default()
5903 },
5904 Some(tree_sitter_rust::LANGUAGE.into()),
5905 )
5906 .with_indents_query(
5907 r#"
5908 (_ "(" ")" @end) @indent
5909 (_ "{" "}" @end) @indent
5910 "#,
5911 )
5912 .unwrap(),
5913 );
5914
5915 let text = "fn a() {}";
5916
5917 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5918 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5919 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5920 editor
5921 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5922 .await;
5923
5924 editor.update_in(cx, |editor, window, cx| {
5925 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
5926 editor.newline(&Newline, window, cx);
5927 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
5928 assert_eq!(
5929 editor.selections.ranges(cx),
5930 &[
5931 Point::new(1, 4)..Point::new(1, 4),
5932 Point::new(3, 4)..Point::new(3, 4),
5933 Point::new(5, 0)..Point::new(5, 0)
5934 ]
5935 );
5936 });
5937}
5938
5939#[gpui::test]
5940async fn test_autoindent_selections(cx: &mut TestAppContext) {
5941 init_test(cx, |_| {});
5942
5943 {
5944 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
5945 cx.set_state(indoc! {"
5946 impl A {
5947
5948 fn b() {}
5949
5950 «fn c() {
5951
5952 }ˇ»
5953 }
5954 "});
5955
5956 cx.update_editor(|editor, window, cx| {
5957 editor.autoindent(&Default::default(), window, cx);
5958 });
5959
5960 cx.assert_editor_state(indoc! {"
5961 impl A {
5962
5963 fn b() {}
5964
5965 «fn c() {
5966
5967 }ˇ»
5968 }
5969 "});
5970 }
5971
5972 {
5973 let mut cx = EditorTestContext::new_multibuffer(
5974 cx,
5975 [indoc! { "
5976 impl A {
5977 «
5978 // a
5979 fn b(){}
5980 »
5981 «
5982 }
5983 fn c(){}
5984 »
5985 "}],
5986 );
5987
5988 let buffer = cx.update_editor(|editor, _, cx| {
5989 let buffer = editor.buffer().update(cx, |buffer, _| {
5990 buffer.all_buffers().iter().next().unwrap().clone()
5991 });
5992 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5993 buffer
5994 });
5995
5996 cx.run_until_parked();
5997 cx.update_editor(|editor, window, cx| {
5998 editor.select_all(&Default::default(), window, cx);
5999 editor.autoindent(&Default::default(), window, cx)
6000 });
6001 cx.run_until_parked();
6002
6003 cx.update(|_, cx| {
6004 pretty_assertions::assert_eq!(
6005 buffer.read(cx).text(),
6006 indoc! { "
6007 impl A {
6008
6009 // a
6010 fn b(){}
6011
6012
6013 }
6014 fn c(){}
6015
6016 " }
6017 )
6018 });
6019 }
6020}
6021
6022#[gpui::test]
6023async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
6024 init_test(cx, |_| {});
6025
6026 let mut cx = EditorTestContext::new(cx).await;
6027
6028 let language = Arc::new(Language::new(
6029 LanguageConfig {
6030 brackets: BracketPairConfig {
6031 pairs: vec![
6032 BracketPair {
6033 start: "{".to_string(),
6034 end: "}".to_string(),
6035 close: true,
6036 surround: true,
6037 newline: true,
6038 },
6039 BracketPair {
6040 start: "(".to_string(),
6041 end: ")".to_string(),
6042 close: true,
6043 surround: true,
6044 newline: true,
6045 },
6046 BracketPair {
6047 start: "/*".to_string(),
6048 end: " */".to_string(),
6049 close: true,
6050 surround: true,
6051 newline: true,
6052 },
6053 BracketPair {
6054 start: "[".to_string(),
6055 end: "]".to_string(),
6056 close: false,
6057 surround: false,
6058 newline: true,
6059 },
6060 BracketPair {
6061 start: "\"".to_string(),
6062 end: "\"".to_string(),
6063 close: true,
6064 surround: true,
6065 newline: false,
6066 },
6067 BracketPair {
6068 start: "<".to_string(),
6069 end: ">".to_string(),
6070 close: false,
6071 surround: true,
6072 newline: true,
6073 },
6074 ],
6075 ..Default::default()
6076 },
6077 autoclose_before: "})]".to_string(),
6078 ..Default::default()
6079 },
6080 Some(tree_sitter_rust::LANGUAGE.into()),
6081 ));
6082
6083 cx.language_registry().add(language.clone());
6084 cx.update_buffer(|buffer, cx| {
6085 buffer.set_language(Some(language), cx);
6086 });
6087
6088 cx.set_state(
6089 &r#"
6090 🏀ˇ
6091 εˇ
6092 ❤️ˇ
6093 "#
6094 .unindent(),
6095 );
6096
6097 // autoclose multiple nested brackets at multiple cursors
6098 cx.update_editor(|editor, window, cx| {
6099 editor.handle_input("{", window, cx);
6100 editor.handle_input("{", window, cx);
6101 editor.handle_input("{", window, cx);
6102 });
6103 cx.assert_editor_state(
6104 &"
6105 🏀{{{ˇ}}}
6106 ε{{{ˇ}}}
6107 ❤️{{{ˇ}}}
6108 "
6109 .unindent(),
6110 );
6111
6112 // insert a different closing bracket
6113 cx.update_editor(|editor, window, cx| {
6114 editor.handle_input(")", window, cx);
6115 });
6116 cx.assert_editor_state(
6117 &"
6118 🏀{{{)ˇ}}}
6119 ε{{{)ˇ}}}
6120 ❤️{{{)ˇ}}}
6121 "
6122 .unindent(),
6123 );
6124
6125 // skip over the auto-closed brackets when typing a closing bracket
6126 cx.update_editor(|editor, window, cx| {
6127 editor.move_right(&MoveRight, window, cx);
6128 editor.handle_input("}", window, cx);
6129 editor.handle_input("}", window, cx);
6130 editor.handle_input("}", window, cx);
6131 });
6132 cx.assert_editor_state(
6133 &"
6134 🏀{{{)}}}}ˇ
6135 ε{{{)}}}}ˇ
6136 ❤️{{{)}}}}ˇ
6137 "
6138 .unindent(),
6139 );
6140
6141 // autoclose multi-character pairs
6142 cx.set_state(
6143 &"
6144 ˇ
6145 ˇ
6146 "
6147 .unindent(),
6148 );
6149 cx.update_editor(|editor, window, cx| {
6150 editor.handle_input("/", window, cx);
6151 editor.handle_input("*", window, cx);
6152 });
6153 cx.assert_editor_state(
6154 &"
6155 /*ˇ */
6156 /*ˇ */
6157 "
6158 .unindent(),
6159 );
6160
6161 // one cursor autocloses a multi-character pair, one cursor
6162 // does not autoclose.
6163 cx.set_state(
6164 &"
6165 /ˇ
6166 ˇ
6167 "
6168 .unindent(),
6169 );
6170 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6171 cx.assert_editor_state(
6172 &"
6173 /*ˇ */
6174 *ˇ
6175 "
6176 .unindent(),
6177 );
6178
6179 // Don't autoclose if the next character isn't whitespace and isn't
6180 // listed in the language's "autoclose_before" section.
6181 cx.set_state("ˇa b");
6182 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6183 cx.assert_editor_state("{ˇa b");
6184
6185 // Don't autoclose if `close` is false for the bracket pair
6186 cx.set_state("ˇ");
6187 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6188 cx.assert_editor_state("[ˇ");
6189
6190 // Surround with brackets if text is selected
6191 cx.set_state("«aˇ» b");
6192 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6193 cx.assert_editor_state("{«aˇ»} b");
6194
6195 // Autclose pair where the start and end characters are the same
6196 cx.set_state("aˇ");
6197 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6198 cx.assert_editor_state("a\"ˇ\"");
6199 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6200 cx.assert_editor_state("a\"\"ˇ");
6201
6202 // Don't autoclose pair if autoclose is disabled
6203 cx.set_state("ˇ");
6204 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6205 cx.assert_editor_state("<ˇ");
6206
6207 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6208 cx.set_state("«aˇ» b");
6209 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6210 cx.assert_editor_state("<«aˇ»> b");
6211}
6212
6213#[gpui::test]
6214async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
6215 init_test(cx, |settings| {
6216 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6217 });
6218
6219 let mut cx = EditorTestContext::new(cx).await;
6220
6221 let language = Arc::new(Language::new(
6222 LanguageConfig {
6223 brackets: BracketPairConfig {
6224 pairs: vec![
6225 BracketPair {
6226 start: "{".to_string(),
6227 end: "}".to_string(),
6228 close: true,
6229 surround: true,
6230 newline: true,
6231 },
6232 BracketPair {
6233 start: "(".to_string(),
6234 end: ")".to_string(),
6235 close: true,
6236 surround: true,
6237 newline: true,
6238 },
6239 BracketPair {
6240 start: "[".to_string(),
6241 end: "]".to_string(),
6242 close: false,
6243 surround: false,
6244 newline: true,
6245 },
6246 ],
6247 ..Default::default()
6248 },
6249 autoclose_before: "})]".to_string(),
6250 ..Default::default()
6251 },
6252 Some(tree_sitter_rust::LANGUAGE.into()),
6253 ));
6254
6255 cx.language_registry().add(language.clone());
6256 cx.update_buffer(|buffer, cx| {
6257 buffer.set_language(Some(language), cx);
6258 });
6259
6260 cx.set_state(
6261 &"
6262 ˇ
6263 ˇ
6264 ˇ
6265 "
6266 .unindent(),
6267 );
6268
6269 // ensure only matching closing brackets are skipped over
6270 cx.update_editor(|editor, window, cx| {
6271 editor.handle_input("}", window, cx);
6272 editor.move_left(&MoveLeft, window, cx);
6273 editor.handle_input(")", window, cx);
6274 editor.move_left(&MoveLeft, window, cx);
6275 });
6276 cx.assert_editor_state(
6277 &"
6278 ˇ)}
6279 ˇ)}
6280 ˇ)}
6281 "
6282 .unindent(),
6283 );
6284
6285 // skip-over closing brackets at multiple cursors
6286 cx.update_editor(|editor, window, cx| {
6287 editor.handle_input(")", window, cx);
6288 editor.handle_input("}", window, cx);
6289 });
6290 cx.assert_editor_state(
6291 &"
6292 )}ˇ
6293 )}ˇ
6294 )}ˇ
6295 "
6296 .unindent(),
6297 );
6298
6299 // ignore non-close brackets
6300 cx.update_editor(|editor, window, cx| {
6301 editor.handle_input("]", window, cx);
6302 editor.move_left(&MoveLeft, window, cx);
6303 editor.handle_input("]", window, cx);
6304 });
6305 cx.assert_editor_state(
6306 &"
6307 )}]ˇ]
6308 )}]ˇ]
6309 )}]ˇ]
6310 "
6311 .unindent(),
6312 );
6313}
6314
6315#[gpui::test]
6316async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
6317 init_test(cx, |_| {});
6318
6319 let mut cx = EditorTestContext::new(cx).await;
6320
6321 let html_language = Arc::new(
6322 Language::new(
6323 LanguageConfig {
6324 name: "HTML".into(),
6325 brackets: BracketPairConfig {
6326 pairs: vec![
6327 BracketPair {
6328 start: "<".into(),
6329 end: ">".into(),
6330 close: true,
6331 ..Default::default()
6332 },
6333 BracketPair {
6334 start: "{".into(),
6335 end: "}".into(),
6336 close: true,
6337 ..Default::default()
6338 },
6339 BracketPair {
6340 start: "(".into(),
6341 end: ")".into(),
6342 close: true,
6343 ..Default::default()
6344 },
6345 ],
6346 ..Default::default()
6347 },
6348 autoclose_before: "})]>".into(),
6349 ..Default::default()
6350 },
6351 Some(tree_sitter_html::LANGUAGE.into()),
6352 )
6353 .with_injection_query(
6354 r#"
6355 (script_element
6356 (raw_text) @injection.content
6357 (#set! injection.language "javascript"))
6358 "#,
6359 )
6360 .unwrap(),
6361 );
6362
6363 let javascript_language = Arc::new(Language::new(
6364 LanguageConfig {
6365 name: "JavaScript".into(),
6366 brackets: BracketPairConfig {
6367 pairs: vec![
6368 BracketPair {
6369 start: "/*".into(),
6370 end: " */".into(),
6371 close: true,
6372 ..Default::default()
6373 },
6374 BracketPair {
6375 start: "{".into(),
6376 end: "}".into(),
6377 close: true,
6378 ..Default::default()
6379 },
6380 BracketPair {
6381 start: "(".into(),
6382 end: ")".into(),
6383 close: true,
6384 ..Default::default()
6385 },
6386 ],
6387 ..Default::default()
6388 },
6389 autoclose_before: "})]>".into(),
6390 ..Default::default()
6391 },
6392 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6393 ));
6394
6395 cx.language_registry().add(html_language.clone());
6396 cx.language_registry().add(javascript_language.clone());
6397
6398 cx.update_buffer(|buffer, cx| {
6399 buffer.set_language(Some(html_language), cx);
6400 });
6401
6402 cx.set_state(
6403 &r#"
6404 <body>ˇ
6405 <script>
6406 var x = 1;ˇ
6407 </script>
6408 </body>ˇ
6409 "#
6410 .unindent(),
6411 );
6412
6413 // Precondition: different languages are active at different locations.
6414 cx.update_editor(|editor, window, cx| {
6415 let snapshot = editor.snapshot(window, cx);
6416 let cursors = editor.selections.ranges::<usize>(cx);
6417 let languages = cursors
6418 .iter()
6419 .map(|c| snapshot.language_at(c.start).unwrap().name())
6420 .collect::<Vec<_>>();
6421 assert_eq!(
6422 languages,
6423 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6424 );
6425 });
6426
6427 // Angle brackets autoclose in HTML, but not JavaScript.
6428 cx.update_editor(|editor, window, cx| {
6429 editor.handle_input("<", window, cx);
6430 editor.handle_input("a", window, cx);
6431 });
6432 cx.assert_editor_state(
6433 &r#"
6434 <body><aˇ>
6435 <script>
6436 var x = 1;<aˇ
6437 </script>
6438 </body><aˇ>
6439 "#
6440 .unindent(),
6441 );
6442
6443 // Curly braces and parens autoclose in both HTML and JavaScript.
6444 cx.update_editor(|editor, window, cx| {
6445 editor.handle_input(" b=", window, cx);
6446 editor.handle_input("{", window, cx);
6447 editor.handle_input("c", window, cx);
6448 editor.handle_input("(", window, cx);
6449 });
6450 cx.assert_editor_state(
6451 &r#"
6452 <body><a b={c(ˇ)}>
6453 <script>
6454 var x = 1;<a b={c(ˇ)}
6455 </script>
6456 </body><a b={c(ˇ)}>
6457 "#
6458 .unindent(),
6459 );
6460
6461 // Brackets that were already autoclosed are skipped.
6462 cx.update_editor(|editor, window, cx| {
6463 editor.handle_input(")", window, cx);
6464 editor.handle_input("d", window, cx);
6465 editor.handle_input("}", window, cx);
6466 });
6467 cx.assert_editor_state(
6468 &r#"
6469 <body><a b={c()d}ˇ>
6470 <script>
6471 var x = 1;<a b={c()d}ˇ
6472 </script>
6473 </body><a b={c()d}ˇ>
6474 "#
6475 .unindent(),
6476 );
6477 cx.update_editor(|editor, window, cx| {
6478 editor.handle_input(">", window, cx);
6479 });
6480 cx.assert_editor_state(
6481 &r#"
6482 <body><a b={c()d}>ˇ
6483 <script>
6484 var x = 1;<a b={c()d}>ˇ
6485 </script>
6486 </body><a b={c()d}>ˇ
6487 "#
6488 .unindent(),
6489 );
6490
6491 // Reset
6492 cx.set_state(
6493 &r#"
6494 <body>ˇ
6495 <script>
6496 var x = 1;ˇ
6497 </script>
6498 </body>ˇ
6499 "#
6500 .unindent(),
6501 );
6502
6503 cx.update_editor(|editor, window, cx| {
6504 editor.handle_input("<", window, cx);
6505 });
6506 cx.assert_editor_state(
6507 &r#"
6508 <body><ˇ>
6509 <script>
6510 var x = 1;<ˇ
6511 </script>
6512 </body><ˇ>
6513 "#
6514 .unindent(),
6515 );
6516
6517 // When backspacing, the closing angle brackets are removed.
6518 cx.update_editor(|editor, window, cx| {
6519 editor.backspace(&Backspace, window, cx);
6520 });
6521 cx.assert_editor_state(
6522 &r#"
6523 <body>ˇ
6524 <script>
6525 var x = 1;ˇ
6526 </script>
6527 </body>ˇ
6528 "#
6529 .unindent(),
6530 );
6531
6532 // Block comments autoclose in JavaScript, but not HTML.
6533 cx.update_editor(|editor, window, cx| {
6534 editor.handle_input("/", window, cx);
6535 editor.handle_input("*", window, cx);
6536 });
6537 cx.assert_editor_state(
6538 &r#"
6539 <body>/*ˇ
6540 <script>
6541 var x = 1;/*ˇ */
6542 </script>
6543 </body>/*ˇ
6544 "#
6545 .unindent(),
6546 );
6547}
6548
6549#[gpui::test]
6550async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
6551 init_test(cx, |_| {});
6552
6553 let mut cx = EditorTestContext::new(cx).await;
6554
6555 let rust_language = Arc::new(
6556 Language::new(
6557 LanguageConfig {
6558 name: "Rust".into(),
6559 brackets: serde_json::from_value(json!([
6560 { "start": "{", "end": "}", "close": true, "newline": true },
6561 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
6562 ]))
6563 .unwrap(),
6564 autoclose_before: "})]>".into(),
6565 ..Default::default()
6566 },
6567 Some(tree_sitter_rust::LANGUAGE.into()),
6568 )
6569 .with_override_query("(string_literal) @string")
6570 .unwrap(),
6571 );
6572
6573 cx.language_registry().add(rust_language.clone());
6574 cx.update_buffer(|buffer, cx| {
6575 buffer.set_language(Some(rust_language), cx);
6576 });
6577
6578 cx.set_state(
6579 &r#"
6580 let x = ˇ
6581 "#
6582 .unindent(),
6583 );
6584
6585 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
6586 cx.update_editor(|editor, window, cx| {
6587 editor.handle_input("\"", window, cx);
6588 });
6589 cx.assert_editor_state(
6590 &r#"
6591 let x = "ˇ"
6592 "#
6593 .unindent(),
6594 );
6595
6596 // Inserting another quotation mark. The cursor moves across the existing
6597 // automatically-inserted quotation mark.
6598 cx.update_editor(|editor, window, cx| {
6599 editor.handle_input("\"", window, cx);
6600 });
6601 cx.assert_editor_state(
6602 &r#"
6603 let x = ""ˇ
6604 "#
6605 .unindent(),
6606 );
6607
6608 // Reset
6609 cx.set_state(
6610 &r#"
6611 let x = ˇ
6612 "#
6613 .unindent(),
6614 );
6615
6616 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
6617 cx.update_editor(|editor, window, cx| {
6618 editor.handle_input("\"", window, cx);
6619 editor.handle_input(" ", window, cx);
6620 editor.move_left(&Default::default(), window, cx);
6621 editor.handle_input("\\", window, cx);
6622 editor.handle_input("\"", window, cx);
6623 });
6624 cx.assert_editor_state(
6625 &r#"
6626 let x = "\"ˇ "
6627 "#
6628 .unindent(),
6629 );
6630
6631 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
6632 // mark. Nothing is inserted.
6633 cx.update_editor(|editor, window, cx| {
6634 editor.move_right(&Default::default(), window, cx);
6635 editor.handle_input("\"", window, cx);
6636 });
6637 cx.assert_editor_state(
6638 &r#"
6639 let x = "\" "ˇ
6640 "#
6641 .unindent(),
6642 );
6643}
6644
6645#[gpui::test]
6646async fn test_surround_with_pair(cx: &mut TestAppContext) {
6647 init_test(cx, |_| {});
6648
6649 let language = Arc::new(Language::new(
6650 LanguageConfig {
6651 brackets: BracketPairConfig {
6652 pairs: vec![
6653 BracketPair {
6654 start: "{".to_string(),
6655 end: "}".to_string(),
6656 close: true,
6657 surround: true,
6658 newline: true,
6659 },
6660 BracketPair {
6661 start: "/* ".to_string(),
6662 end: "*/".to_string(),
6663 close: true,
6664 surround: true,
6665 ..Default::default()
6666 },
6667 ],
6668 ..Default::default()
6669 },
6670 ..Default::default()
6671 },
6672 Some(tree_sitter_rust::LANGUAGE.into()),
6673 ));
6674
6675 let text = r#"
6676 a
6677 b
6678 c
6679 "#
6680 .unindent();
6681
6682 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6683 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6684 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6685 editor
6686 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6687 .await;
6688
6689 editor.update_in(cx, |editor, window, cx| {
6690 editor.change_selections(None, window, cx, |s| {
6691 s.select_display_ranges([
6692 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6693 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6694 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
6695 ])
6696 });
6697
6698 editor.handle_input("{", window, cx);
6699 editor.handle_input("{", window, cx);
6700 editor.handle_input("{", window, cx);
6701 assert_eq!(
6702 editor.text(cx),
6703 "
6704 {{{a}}}
6705 {{{b}}}
6706 {{{c}}}
6707 "
6708 .unindent()
6709 );
6710 assert_eq!(
6711 editor.selections.display_ranges(cx),
6712 [
6713 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
6714 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
6715 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
6716 ]
6717 );
6718
6719 editor.undo(&Undo, window, cx);
6720 editor.undo(&Undo, window, cx);
6721 editor.undo(&Undo, window, cx);
6722 assert_eq!(
6723 editor.text(cx),
6724 "
6725 a
6726 b
6727 c
6728 "
6729 .unindent()
6730 );
6731 assert_eq!(
6732 editor.selections.display_ranges(cx),
6733 [
6734 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6735 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6736 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
6737 ]
6738 );
6739
6740 // Ensure inserting the first character of a multi-byte bracket pair
6741 // doesn't surround the selections with the bracket.
6742 editor.handle_input("/", window, cx);
6743 assert_eq!(
6744 editor.text(cx),
6745 "
6746 /
6747 /
6748 /
6749 "
6750 .unindent()
6751 );
6752 assert_eq!(
6753 editor.selections.display_ranges(cx),
6754 [
6755 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6756 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6757 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
6758 ]
6759 );
6760
6761 editor.undo(&Undo, window, cx);
6762 assert_eq!(
6763 editor.text(cx),
6764 "
6765 a
6766 b
6767 c
6768 "
6769 .unindent()
6770 );
6771 assert_eq!(
6772 editor.selections.display_ranges(cx),
6773 [
6774 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6775 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6776 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
6777 ]
6778 );
6779
6780 // Ensure inserting the last character of a multi-byte bracket pair
6781 // doesn't surround the selections with the bracket.
6782 editor.handle_input("*", window, cx);
6783 assert_eq!(
6784 editor.text(cx),
6785 "
6786 *
6787 *
6788 *
6789 "
6790 .unindent()
6791 );
6792 assert_eq!(
6793 editor.selections.display_ranges(cx),
6794 [
6795 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6796 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6797 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
6798 ]
6799 );
6800 });
6801}
6802
6803#[gpui::test]
6804async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
6805 init_test(cx, |_| {});
6806
6807 let language = Arc::new(Language::new(
6808 LanguageConfig {
6809 brackets: BracketPairConfig {
6810 pairs: vec![BracketPair {
6811 start: "{".to_string(),
6812 end: "}".to_string(),
6813 close: true,
6814 surround: true,
6815 newline: true,
6816 }],
6817 ..Default::default()
6818 },
6819 autoclose_before: "}".to_string(),
6820 ..Default::default()
6821 },
6822 Some(tree_sitter_rust::LANGUAGE.into()),
6823 ));
6824
6825 let text = r#"
6826 a
6827 b
6828 c
6829 "#
6830 .unindent();
6831
6832 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6833 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6834 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6835 editor
6836 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6837 .await;
6838
6839 editor.update_in(cx, |editor, window, cx| {
6840 editor.change_selections(None, window, cx, |s| {
6841 s.select_ranges([
6842 Point::new(0, 1)..Point::new(0, 1),
6843 Point::new(1, 1)..Point::new(1, 1),
6844 Point::new(2, 1)..Point::new(2, 1),
6845 ])
6846 });
6847
6848 editor.handle_input("{", window, cx);
6849 editor.handle_input("{", window, cx);
6850 editor.handle_input("_", window, cx);
6851 assert_eq!(
6852 editor.text(cx),
6853 "
6854 a{{_}}
6855 b{{_}}
6856 c{{_}}
6857 "
6858 .unindent()
6859 );
6860 assert_eq!(
6861 editor.selections.ranges::<Point>(cx),
6862 [
6863 Point::new(0, 4)..Point::new(0, 4),
6864 Point::new(1, 4)..Point::new(1, 4),
6865 Point::new(2, 4)..Point::new(2, 4)
6866 ]
6867 );
6868
6869 editor.backspace(&Default::default(), window, cx);
6870 editor.backspace(&Default::default(), window, cx);
6871 assert_eq!(
6872 editor.text(cx),
6873 "
6874 a{}
6875 b{}
6876 c{}
6877 "
6878 .unindent()
6879 );
6880 assert_eq!(
6881 editor.selections.ranges::<Point>(cx),
6882 [
6883 Point::new(0, 2)..Point::new(0, 2),
6884 Point::new(1, 2)..Point::new(1, 2),
6885 Point::new(2, 2)..Point::new(2, 2)
6886 ]
6887 );
6888
6889 editor.delete_to_previous_word_start(&Default::default(), window, cx);
6890 assert_eq!(
6891 editor.text(cx),
6892 "
6893 a
6894 b
6895 c
6896 "
6897 .unindent()
6898 );
6899 assert_eq!(
6900 editor.selections.ranges::<Point>(cx),
6901 [
6902 Point::new(0, 1)..Point::new(0, 1),
6903 Point::new(1, 1)..Point::new(1, 1),
6904 Point::new(2, 1)..Point::new(2, 1)
6905 ]
6906 );
6907 });
6908}
6909
6910#[gpui::test]
6911async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
6912 init_test(cx, |settings| {
6913 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6914 });
6915
6916 let mut cx = EditorTestContext::new(cx).await;
6917
6918 let language = Arc::new(Language::new(
6919 LanguageConfig {
6920 brackets: BracketPairConfig {
6921 pairs: vec![
6922 BracketPair {
6923 start: "{".to_string(),
6924 end: "}".to_string(),
6925 close: true,
6926 surround: true,
6927 newline: true,
6928 },
6929 BracketPair {
6930 start: "(".to_string(),
6931 end: ")".to_string(),
6932 close: true,
6933 surround: true,
6934 newline: true,
6935 },
6936 BracketPair {
6937 start: "[".to_string(),
6938 end: "]".to_string(),
6939 close: false,
6940 surround: true,
6941 newline: true,
6942 },
6943 ],
6944 ..Default::default()
6945 },
6946 autoclose_before: "})]".to_string(),
6947 ..Default::default()
6948 },
6949 Some(tree_sitter_rust::LANGUAGE.into()),
6950 ));
6951
6952 cx.language_registry().add(language.clone());
6953 cx.update_buffer(|buffer, cx| {
6954 buffer.set_language(Some(language), cx);
6955 });
6956
6957 cx.set_state(
6958 &"
6959 {(ˇ)}
6960 [[ˇ]]
6961 {(ˇ)}
6962 "
6963 .unindent(),
6964 );
6965
6966 cx.update_editor(|editor, window, cx| {
6967 editor.backspace(&Default::default(), window, cx);
6968 editor.backspace(&Default::default(), window, cx);
6969 });
6970
6971 cx.assert_editor_state(
6972 &"
6973 ˇ
6974 ˇ]]
6975 ˇ
6976 "
6977 .unindent(),
6978 );
6979
6980 cx.update_editor(|editor, window, cx| {
6981 editor.handle_input("{", window, cx);
6982 editor.handle_input("{", window, cx);
6983 editor.move_right(&MoveRight, window, cx);
6984 editor.move_right(&MoveRight, window, cx);
6985 editor.move_left(&MoveLeft, window, cx);
6986 editor.move_left(&MoveLeft, window, cx);
6987 editor.backspace(&Default::default(), window, cx);
6988 });
6989
6990 cx.assert_editor_state(
6991 &"
6992 {ˇ}
6993 {ˇ}]]
6994 {ˇ}
6995 "
6996 .unindent(),
6997 );
6998
6999 cx.update_editor(|editor, window, cx| {
7000 editor.backspace(&Default::default(), window, cx);
7001 });
7002
7003 cx.assert_editor_state(
7004 &"
7005 ˇ
7006 ˇ]]
7007 ˇ
7008 "
7009 .unindent(),
7010 );
7011}
7012
7013#[gpui::test]
7014async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
7015 init_test(cx, |_| {});
7016
7017 let language = Arc::new(Language::new(
7018 LanguageConfig::default(),
7019 Some(tree_sitter_rust::LANGUAGE.into()),
7020 ));
7021
7022 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
7023 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7024 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7025 editor
7026 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7027 .await;
7028
7029 editor.update_in(cx, |editor, window, cx| {
7030 editor.set_auto_replace_emoji_shortcode(true);
7031
7032 editor.handle_input("Hello ", window, cx);
7033 editor.handle_input(":wave", window, cx);
7034 assert_eq!(editor.text(cx), "Hello :wave".unindent());
7035
7036 editor.handle_input(":", window, cx);
7037 assert_eq!(editor.text(cx), "Hello 👋".unindent());
7038
7039 editor.handle_input(" :smile", window, cx);
7040 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
7041
7042 editor.handle_input(":", window, cx);
7043 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
7044
7045 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
7046 editor.handle_input(":wave", window, cx);
7047 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
7048
7049 editor.handle_input(":", window, cx);
7050 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
7051
7052 editor.handle_input(":1", window, cx);
7053 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
7054
7055 editor.handle_input(":", window, cx);
7056 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
7057
7058 // Ensure shortcode does not get replaced when it is part of a word
7059 editor.handle_input(" Test:wave", window, cx);
7060 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
7061
7062 editor.handle_input(":", window, cx);
7063 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
7064
7065 editor.set_auto_replace_emoji_shortcode(false);
7066
7067 // Ensure shortcode does not get replaced when auto replace is off
7068 editor.handle_input(" :wave", window, cx);
7069 assert_eq!(
7070 editor.text(cx),
7071 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
7072 );
7073
7074 editor.handle_input(":", window, cx);
7075 assert_eq!(
7076 editor.text(cx),
7077 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
7078 );
7079 });
7080}
7081
7082#[gpui::test]
7083async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
7084 init_test(cx, |_| {});
7085
7086 let (text, insertion_ranges) = marked_text_ranges(
7087 indoc! {"
7088 ˇ
7089 "},
7090 false,
7091 );
7092
7093 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7094 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7095
7096 _ = editor.update_in(cx, |editor, window, cx| {
7097 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7098
7099 editor
7100 .insert_snippet(&insertion_ranges, snippet, window, cx)
7101 .unwrap();
7102
7103 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7104 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7105 assert_eq!(editor.text(cx), expected_text);
7106 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7107 }
7108
7109 assert(
7110 editor,
7111 cx,
7112 indoc! {"
7113 type «» =•
7114 "},
7115 );
7116
7117 assert!(editor.context_menu_visible(), "There should be a matches");
7118 });
7119}
7120
7121#[gpui::test]
7122async fn test_snippets(cx: &mut TestAppContext) {
7123 init_test(cx, |_| {});
7124
7125 let (text, insertion_ranges) = marked_text_ranges(
7126 indoc! {"
7127 a.ˇ b
7128 a.ˇ b
7129 a.ˇ b
7130 "},
7131 false,
7132 );
7133
7134 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7135 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7136
7137 editor.update_in(cx, |editor, window, cx| {
7138 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
7139
7140 editor
7141 .insert_snippet(&insertion_ranges, snippet, window, cx)
7142 .unwrap();
7143
7144 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7145 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7146 assert_eq!(editor.text(cx), expected_text);
7147 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7148 }
7149
7150 assert(
7151 editor,
7152 cx,
7153 indoc! {"
7154 a.f(«one», two, «three») b
7155 a.f(«one», two, «three») b
7156 a.f(«one», two, «three») b
7157 "},
7158 );
7159
7160 // Can't move earlier than the first tab stop
7161 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7162 assert(
7163 editor,
7164 cx,
7165 indoc! {"
7166 a.f(«one», two, «three») b
7167 a.f(«one», two, «three») b
7168 a.f(«one», two, «three») b
7169 "},
7170 );
7171
7172 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7173 assert(
7174 editor,
7175 cx,
7176 indoc! {"
7177 a.f(one, «two», three) b
7178 a.f(one, «two», three) b
7179 a.f(one, «two», three) b
7180 "},
7181 );
7182
7183 editor.move_to_prev_snippet_tabstop(window, cx);
7184 assert(
7185 editor,
7186 cx,
7187 indoc! {"
7188 a.f(«one», two, «three») b
7189 a.f(«one», two, «three») b
7190 a.f(«one», two, «three») b
7191 "},
7192 );
7193
7194 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7195 assert(
7196 editor,
7197 cx,
7198 indoc! {"
7199 a.f(one, «two», three) b
7200 a.f(one, «two», three) b
7201 a.f(one, «two», three) b
7202 "},
7203 );
7204 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7205 assert(
7206 editor,
7207 cx,
7208 indoc! {"
7209 a.f(one, two, three)ˇ b
7210 a.f(one, two, three)ˇ b
7211 a.f(one, two, three)ˇ b
7212 "},
7213 );
7214
7215 // As soon as the last tab stop is reached, snippet state is gone
7216 editor.move_to_prev_snippet_tabstop(window, cx);
7217 assert(
7218 editor,
7219 cx,
7220 indoc! {"
7221 a.f(one, two, three)ˇ b
7222 a.f(one, two, three)ˇ b
7223 a.f(one, two, three)ˇ b
7224 "},
7225 );
7226 });
7227}
7228
7229#[gpui::test]
7230async fn test_document_format_during_save(cx: &mut TestAppContext) {
7231 init_test(cx, |_| {});
7232
7233 let fs = FakeFs::new(cx.executor());
7234 fs.insert_file(path!("/file.rs"), Default::default()).await;
7235
7236 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
7237
7238 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7239 language_registry.add(rust_lang());
7240 let mut fake_servers = language_registry.register_fake_lsp(
7241 "Rust",
7242 FakeLspAdapter {
7243 capabilities: lsp::ServerCapabilities {
7244 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7245 ..Default::default()
7246 },
7247 ..Default::default()
7248 },
7249 );
7250
7251 let buffer = project
7252 .update(cx, |project, cx| {
7253 project.open_local_buffer(path!("/file.rs"), cx)
7254 })
7255 .await
7256 .unwrap();
7257
7258 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7259 let (editor, cx) = cx.add_window_view(|window, cx| {
7260 build_editor_with_project(project.clone(), buffer, window, cx)
7261 });
7262 editor.update_in(cx, |editor, window, cx| {
7263 editor.set_text("one\ntwo\nthree\n", window, cx)
7264 });
7265 assert!(cx.read(|cx| editor.is_dirty(cx)));
7266
7267 cx.executor().start_waiting();
7268 let fake_server = fake_servers.next().await.unwrap();
7269
7270 let save = editor
7271 .update_in(cx, |editor, window, cx| {
7272 editor.save(true, project.clone(), window, cx)
7273 })
7274 .unwrap();
7275 fake_server
7276 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7277 assert_eq!(
7278 params.text_document.uri,
7279 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7280 );
7281 assert_eq!(params.options.tab_size, 4);
7282 Ok(Some(vec![lsp::TextEdit::new(
7283 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7284 ", ".to_string(),
7285 )]))
7286 })
7287 .next()
7288 .await;
7289 cx.executor().start_waiting();
7290 save.await;
7291
7292 assert_eq!(
7293 editor.update(cx, |editor, cx| editor.text(cx)),
7294 "one, two\nthree\n"
7295 );
7296 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7297
7298 editor.update_in(cx, |editor, window, cx| {
7299 editor.set_text("one\ntwo\nthree\n", window, cx)
7300 });
7301 assert!(cx.read(|cx| editor.is_dirty(cx)));
7302
7303 // Ensure we can still save even if formatting hangs.
7304 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7305 assert_eq!(
7306 params.text_document.uri,
7307 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7308 );
7309 futures::future::pending::<()>().await;
7310 unreachable!()
7311 });
7312 let save = editor
7313 .update_in(cx, |editor, window, cx| {
7314 editor.save(true, project.clone(), window, cx)
7315 })
7316 .unwrap();
7317 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7318 cx.executor().start_waiting();
7319 save.await;
7320 assert_eq!(
7321 editor.update(cx, |editor, cx| editor.text(cx)),
7322 "one\ntwo\nthree\n"
7323 );
7324 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7325
7326 // For non-dirty buffer, no formatting request should be sent
7327 let save = editor
7328 .update_in(cx, |editor, window, cx| {
7329 editor.save(true, project.clone(), window, cx)
7330 })
7331 .unwrap();
7332 let _pending_format_request = fake_server
7333 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7334 panic!("Should not be invoked on non-dirty buffer");
7335 })
7336 .next();
7337 cx.executor().start_waiting();
7338 save.await;
7339
7340 // Set rust language override and assert overridden tabsize is sent to language server
7341 update_test_language_settings(cx, |settings| {
7342 settings.languages.insert(
7343 "Rust".into(),
7344 LanguageSettingsContent {
7345 tab_size: NonZeroU32::new(8),
7346 ..Default::default()
7347 },
7348 );
7349 });
7350
7351 editor.update_in(cx, |editor, window, cx| {
7352 editor.set_text("somehting_new\n", window, cx)
7353 });
7354 assert!(cx.read(|cx| editor.is_dirty(cx)));
7355 let save = editor
7356 .update_in(cx, |editor, window, cx| {
7357 editor.save(true, project.clone(), window, cx)
7358 })
7359 .unwrap();
7360 fake_server
7361 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7362 assert_eq!(
7363 params.text_document.uri,
7364 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7365 );
7366 assert_eq!(params.options.tab_size, 8);
7367 Ok(Some(vec![]))
7368 })
7369 .next()
7370 .await;
7371 cx.executor().start_waiting();
7372 save.await;
7373}
7374
7375#[gpui::test]
7376async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
7377 init_test(cx, |_| {});
7378
7379 let cols = 4;
7380 let rows = 10;
7381 let sample_text_1 = sample_text(rows, cols, 'a');
7382 assert_eq!(
7383 sample_text_1,
7384 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7385 );
7386 let sample_text_2 = sample_text(rows, cols, 'l');
7387 assert_eq!(
7388 sample_text_2,
7389 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7390 );
7391 let sample_text_3 = sample_text(rows, cols, 'v');
7392 assert_eq!(
7393 sample_text_3,
7394 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7395 );
7396
7397 let fs = FakeFs::new(cx.executor());
7398 fs.insert_tree(
7399 path!("/a"),
7400 json!({
7401 "main.rs": sample_text_1,
7402 "other.rs": sample_text_2,
7403 "lib.rs": sample_text_3,
7404 }),
7405 )
7406 .await;
7407
7408 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
7409 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7410 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7411
7412 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7413 language_registry.add(rust_lang());
7414 let mut fake_servers = language_registry.register_fake_lsp(
7415 "Rust",
7416 FakeLspAdapter {
7417 capabilities: lsp::ServerCapabilities {
7418 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7419 ..Default::default()
7420 },
7421 ..Default::default()
7422 },
7423 );
7424
7425 let worktree = project.update(cx, |project, cx| {
7426 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7427 assert_eq!(worktrees.len(), 1);
7428 worktrees.pop().unwrap()
7429 });
7430 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
7431
7432 let buffer_1 = project
7433 .update(cx, |project, cx| {
7434 project.open_buffer((worktree_id, "main.rs"), cx)
7435 })
7436 .await
7437 .unwrap();
7438 let buffer_2 = project
7439 .update(cx, |project, cx| {
7440 project.open_buffer((worktree_id, "other.rs"), cx)
7441 })
7442 .await
7443 .unwrap();
7444 let buffer_3 = project
7445 .update(cx, |project, cx| {
7446 project.open_buffer((worktree_id, "lib.rs"), cx)
7447 })
7448 .await
7449 .unwrap();
7450
7451 let multi_buffer = cx.new(|cx| {
7452 let mut multi_buffer = MultiBuffer::new(ReadWrite);
7453 multi_buffer.push_excerpts(
7454 buffer_1.clone(),
7455 [
7456 ExcerptRange {
7457 context: Point::new(0, 0)..Point::new(3, 0),
7458 primary: None,
7459 },
7460 ExcerptRange {
7461 context: Point::new(5, 0)..Point::new(7, 0),
7462 primary: None,
7463 },
7464 ExcerptRange {
7465 context: Point::new(9, 0)..Point::new(10, 4),
7466 primary: None,
7467 },
7468 ],
7469 cx,
7470 );
7471 multi_buffer.push_excerpts(
7472 buffer_2.clone(),
7473 [
7474 ExcerptRange {
7475 context: Point::new(0, 0)..Point::new(3, 0),
7476 primary: None,
7477 },
7478 ExcerptRange {
7479 context: Point::new(5, 0)..Point::new(7, 0),
7480 primary: None,
7481 },
7482 ExcerptRange {
7483 context: Point::new(9, 0)..Point::new(10, 4),
7484 primary: None,
7485 },
7486 ],
7487 cx,
7488 );
7489 multi_buffer.push_excerpts(
7490 buffer_3.clone(),
7491 [
7492 ExcerptRange {
7493 context: Point::new(0, 0)..Point::new(3, 0),
7494 primary: None,
7495 },
7496 ExcerptRange {
7497 context: Point::new(5, 0)..Point::new(7, 0),
7498 primary: None,
7499 },
7500 ExcerptRange {
7501 context: Point::new(9, 0)..Point::new(10, 4),
7502 primary: None,
7503 },
7504 ],
7505 cx,
7506 );
7507 multi_buffer
7508 });
7509 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
7510 Editor::new(
7511 EditorMode::Full,
7512 multi_buffer,
7513 Some(project.clone()),
7514 true,
7515 window,
7516 cx,
7517 )
7518 });
7519
7520 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7521 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7522 s.select_ranges(Some(1..2))
7523 });
7524 editor.insert("|one|two|three|", window, cx);
7525 });
7526 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7527 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7528 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7529 s.select_ranges(Some(60..70))
7530 });
7531 editor.insert("|four|five|six|", window, cx);
7532 });
7533 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7534
7535 // First two buffers should be edited, but not the third one.
7536 assert_eq!(
7537 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7538 "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}",
7539 );
7540 buffer_1.update(cx, |buffer, _| {
7541 assert!(buffer.is_dirty());
7542 assert_eq!(
7543 buffer.text(),
7544 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
7545 )
7546 });
7547 buffer_2.update(cx, |buffer, _| {
7548 assert!(buffer.is_dirty());
7549 assert_eq!(
7550 buffer.text(),
7551 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
7552 )
7553 });
7554 buffer_3.update(cx, |buffer, _| {
7555 assert!(!buffer.is_dirty());
7556 assert_eq!(buffer.text(), sample_text_3,)
7557 });
7558 cx.executor().run_until_parked();
7559
7560 cx.executor().start_waiting();
7561 let save = multi_buffer_editor
7562 .update_in(cx, |editor, window, cx| {
7563 editor.save(true, project.clone(), window, cx)
7564 })
7565 .unwrap();
7566
7567 let fake_server = fake_servers.next().await.unwrap();
7568 fake_server
7569 .server
7570 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7571 Ok(Some(vec![lsp::TextEdit::new(
7572 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7573 format!("[{} formatted]", params.text_document.uri),
7574 )]))
7575 })
7576 .detach();
7577 save.await;
7578
7579 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
7580 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
7581 assert_eq!(
7582 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7583 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\n\nffff\ngggg\n\njjjj\n\nlll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|\nr\n\nuuuu\n\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}"),
7584 );
7585 buffer_1.update(cx, |buffer, _| {
7586 assert!(!buffer.is_dirty());
7587 assert_eq!(
7588 buffer.text(),
7589 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
7590 )
7591 });
7592 buffer_2.update(cx, |buffer, _| {
7593 assert!(!buffer.is_dirty());
7594 assert_eq!(
7595 buffer.text(),
7596 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
7597 )
7598 });
7599 buffer_3.update(cx, |buffer, _| {
7600 assert!(!buffer.is_dirty());
7601 assert_eq!(buffer.text(), sample_text_3,)
7602 });
7603}
7604
7605#[gpui::test]
7606async fn test_range_format_during_save(cx: &mut TestAppContext) {
7607 init_test(cx, |_| {});
7608
7609 let fs = FakeFs::new(cx.executor());
7610 fs.insert_file(path!("/file.rs"), Default::default()).await;
7611
7612 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
7613
7614 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7615 language_registry.add(rust_lang());
7616 let mut fake_servers = language_registry.register_fake_lsp(
7617 "Rust",
7618 FakeLspAdapter {
7619 capabilities: lsp::ServerCapabilities {
7620 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
7621 ..Default::default()
7622 },
7623 ..Default::default()
7624 },
7625 );
7626
7627 let buffer = project
7628 .update(cx, |project, cx| {
7629 project.open_local_buffer(path!("/file.rs"), cx)
7630 })
7631 .await
7632 .unwrap();
7633
7634 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7635 let (editor, cx) = cx.add_window_view(|window, cx| {
7636 build_editor_with_project(project.clone(), buffer, window, cx)
7637 });
7638 editor.update_in(cx, |editor, window, cx| {
7639 editor.set_text("one\ntwo\nthree\n", window, cx)
7640 });
7641 assert!(cx.read(|cx| editor.is_dirty(cx)));
7642
7643 cx.executor().start_waiting();
7644 let fake_server = fake_servers.next().await.unwrap();
7645
7646 let save = editor
7647 .update_in(cx, |editor, window, cx| {
7648 editor.save(true, project.clone(), window, cx)
7649 })
7650 .unwrap();
7651 fake_server
7652 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
7653 assert_eq!(
7654 params.text_document.uri,
7655 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7656 );
7657 assert_eq!(params.options.tab_size, 4);
7658 Ok(Some(vec![lsp::TextEdit::new(
7659 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7660 ", ".to_string(),
7661 )]))
7662 })
7663 .next()
7664 .await;
7665 cx.executor().start_waiting();
7666 save.await;
7667 assert_eq!(
7668 editor.update(cx, |editor, cx| editor.text(cx)),
7669 "one, two\nthree\n"
7670 );
7671 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7672
7673 editor.update_in(cx, |editor, window, cx| {
7674 editor.set_text("one\ntwo\nthree\n", window, cx)
7675 });
7676 assert!(cx.read(|cx| editor.is_dirty(cx)));
7677
7678 // Ensure we can still save even if formatting hangs.
7679 fake_server.handle_request::<lsp::request::RangeFormatting, _, _>(
7680 move |params, _| async move {
7681 assert_eq!(
7682 params.text_document.uri,
7683 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7684 );
7685 futures::future::pending::<()>().await;
7686 unreachable!()
7687 },
7688 );
7689 let save = editor
7690 .update_in(cx, |editor, window, cx| {
7691 editor.save(true, project.clone(), window, cx)
7692 })
7693 .unwrap();
7694 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7695 cx.executor().start_waiting();
7696 save.await;
7697 assert_eq!(
7698 editor.update(cx, |editor, cx| editor.text(cx)),
7699 "one\ntwo\nthree\n"
7700 );
7701 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7702
7703 // For non-dirty buffer, no formatting request should be sent
7704 let save = editor
7705 .update_in(cx, |editor, window, cx| {
7706 editor.save(true, project.clone(), window, cx)
7707 })
7708 .unwrap();
7709 let _pending_format_request = fake_server
7710 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7711 panic!("Should not be invoked on non-dirty buffer");
7712 })
7713 .next();
7714 cx.executor().start_waiting();
7715 save.await;
7716
7717 // Set Rust language override and assert overridden tabsize is sent to language server
7718 update_test_language_settings(cx, |settings| {
7719 settings.languages.insert(
7720 "Rust".into(),
7721 LanguageSettingsContent {
7722 tab_size: NonZeroU32::new(8),
7723 ..Default::default()
7724 },
7725 );
7726 });
7727
7728 editor.update_in(cx, |editor, window, cx| {
7729 editor.set_text("somehting_new\n", window, cx)
7730 });
7731 assert!(cx.read(|cx| editor.is_dirty(cx)));
7732 let save = editor
7733 .update_in(cx, |editor, window, cx| {
7734 editor.save(true, project.clone(), window, cx)
7735 })
7736 .unwrap();
7737 fake_server
7738 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
7739 assert_eq!(
7740 params.text_document.uri,
7741 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7742 );
7743 assert_eq!(params.options.tab_size, 8);
7744 Ok(Some(vec![]))
7745 })
7746 .next()
7747 .await;
7748 cx.executor().start_waiting();
7749 save.await;
7750}
7751
7752#[gpui::test]
7753async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
7754 init_test(cx, |settings| {
7755 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
7756 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
7757 ))
7758 });
7759
7760 let fs = FakeFs::new(cx.executor());
7761 fs.insert_file(path!("/file.rs"), Default::default()).await;
7762
7763 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
7764
7765 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7766 language_registry.add(Arc::new(Language::new(
7767 LanguageConfig {
7768 name: "Rust".into(),
7769 matcher: LanguageMatcher {
7770 path_suffixes: vec!["rs".to_string()],
7771 ..Default::default()
7772 },
7773 ..LanguageConfig::default()
7774 },
7775 Some(tree_sitter_rust::LANGUAGE.into()),
7776 )));
7777 update_test_language_settings(cx, |settings| {
7778 // Enable Prettier formatting for the same buffer, and ensure
7779 // LSP is called instead of Prettier.
7780 settings.defaults.prettier = Some(PrettierSettings {
7781 allowed: true,
7782 ..PrettierSettings::default()
7783 });
7784 });
7785 let mut fake_servers = language_registry.register_fake_lsp(
7786 "Rust",
7787 FakeLspAdapter {
7788 capabilities: lsp::ServerCapabilities {
7789 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7790 ..Default::default()
7791 },
7792 ..Default::default()
7793 },
7794 );
7795
7796 let buffer = project
7797 .update(cx, |project, cx| {
7798 project.open_local_buffer(path!("/file.rs"), cx)
7799 })
7800 .await
7801 .unwrap();
7802
7803 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7804 let (editor, cx) = cx.add_window_view(|window, cx| {
7805 build_editor_with_project(project.clone(), buffer, window, cx)
7806 });
7807 editor.update_in(cx, |editor, window, cx| {
7808 editor.set_text("one\ntwo\nthree\n", window, cx)
7809 });
7810
7811 cx.executor().start_waiting();
7812 let fake_server = fake_servers.next().await.unwrap();
7813
7814 let format = editor
7815 .update_in(cx, |editor, window, cx| {
7816 editor.perform_format(
7817 project.clone(),
7818 FormatTrigger::Manual,
7819 FormatTarget::Buffers,
7820 window,
7821 cx,
7822 )
7823 })
7824 .unwrap();
7825 fake_server
7826 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7827 assert_eq!(
7828 params.text_document.uri,
7829 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7830 );
7831 assert_eq!(params.options.tab_size, 4);
7832 Ok(Some(vec![lsp::TextEdit::new(
7833 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7834 ", ".to_string(),
7835 )]))
7836 })
7837 .next()
7838 .await;
7839 cx.executor().start_waiting();
7840 format.await;
7841 assert_eq!(
7842 editor.update(cx, |editor, cx| editor.text(cx)),
7843 "one, two\nthree\n"
7844 );
7845
7846 editor.update_in(cx, |editor, window, cx| {
7847 editor.set_text("one\ntwo\nthree\n", window, cx)
7848 });
7849 // Ensure we don't lock if formatting hangs.
7850 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7851 assert_eq!(
7852 params.text_document.uri,
7853 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7854 );
7855 futures::future::pending::<()>().await;
7856 unreachable!()
7857 });
7858 let format = editor
7859 .update_in(cx, |editor, window, cx| {
7860 editor.perform_format(
7861 project,
7862 FormatTrigger::Manual,
7863 FormatTarget::Buffers,
7864 window,
7865 cx,
7866 )
7867 })
7868 .unwrap();
7869 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7870 cx.executor().start_waiting();
7871 format.await;
7872 assert_eq!(
7873 editor.update(cx, |editor, cx| editor.text(cx)),
7874 "one\ntwo\nthree\n"
7875 );
7876}
7877
7878#[gpui::test]
7879async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
7880 init_test(cx, |settings| {
7881 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
7882 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
7883 ))
7884 });
7885
7886 let fs = FakeFs::new(cx.executor());
7887 fs.insert_file(path!("/file.ts"), Default::default()).await;
7888
7889 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
7890
7891 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7892 language_registry.add(Arc::new(Language::new(
7893 LanguageConfig {
7894 name: "TypeScript".into(),
7895 matcher: LanguageMatcher {
7896 path_suffixes: vec!["ts".to_string()],
7897 ..Default::default()
7898 },
7899 ..LanguageConfig::default()
7900 },
7901 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
7902 )));
7903 update_test_language_settings(cx, |settings| {
7904 settings.defaults.prettier = Some(PrettierSettings {
7905 allowed: true,
7906 ..PrettierSettings::default()
7907 });
7908 });
7909 let mut fake_servers = language_registry.register_fake_lsp(
7910 "TypeScript",
7911 FakeLspAdapter {
7912 capabilities: lsp::ServerCapabilities {
7913 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
7914 ..Default::default()
7915 },
7916 ..Default::default()
7917 },
7918 );
7919
7920 let buffer = project
7921 .update(cx, |project, cx| {
7922 project.open_local_buffer(path!("/file.ts"), cx)
7923 })
7924 .await
7925 .unwrap();
7926
7927 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7928 let (editor, cx) = cx.add_window_view(|window, cx| {
7929 build_editor_with_project(project.clone(), buffer, window, cx)
7930 });
7931 editor.update_in(cx, |editor, window, cx| {
7932 editor.set_text(
7933 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
7934 window,
7935 cx,
7936 )
7937 });
7938
7939 cx.executor().start_waiting();
7940 let fake_server = fake_servers.next().await.unwrap();
7941
7942 let format = editor
7943 .update_in(cx, |editor, window, cx| {
7944 editor.perform_code_action_kind(
7945 project.clone(),
7946 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
7947 window,
7948 cx,
7949 )
7950 })
7951 .unwrap();
7952 fake_server
7953 .handle_request::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
7954 assert_eq!(
7955 params.text_document.uri,
7956 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
7957 );
7958 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
7959 lsp::CodeAction {
7960 title: "Organize Imports".to_string(),
7961 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
7962 edit: Some(lsp::WorkspaceEdit {
7963 changes: Some(
7964 [(
7965 params.text_document.uri.clone(),
7966 vec![lsp::TextEdit::new(
7967 lsp::Range::new(
7968 lsp::Position::new(1, 0),
7969 lsp::Position::new(2, 0),
7970 ),
7971 "".to_string(),
7972 )],
7973 )]
7974 .into_iter()
7975 .collect(),
7976 ),
7977 ..Default::default()
7978 }),
7979 ..Default::default()
7980 },
7981 )]))
7982 })
7983 .next()
7984 .await;
7985 cx.executor().start_waiting();
7986 format.await;
7987 assert_eq!(
7988 editor.update(cx, |editor, cx| editor.text(cx)),
7989 "import { a } from 'module';\n\nconst x = a;\n"
7990 );
7991
7992 editor.update_in(cx, |editor, window, cx| {
7993 editor.set_text(
7994 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
7995 window,
7996 cx,
7997 )
7998 });
7999 // Ensure we don't lock if code action hangs.
8000 fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
8001 move |params, _| async move {
8002 assert_eq!(
8003 params.text_document.uri,
8004 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8005 );
8006 futures::future::pending::<()>().await;
8007 unreachable!()
8008 },
8009 );
8010 let format = editor
8011 .update_in(cx, |editor, window, cx| {
8012 editor.perform_code_action_kind(
8013 project,
8014 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8015 window,
8016 cx,
8017 )
8018 })
8019 .unwrap();
8020 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
8021 cx.executor().start_waiting();
8022 format.await;
8023 assert_eq!(
8024 editor.update(cx, |editor, cx| editor.text(cx)),
8025 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
8026 );
8027}
8028
8029#[gpui::test]
8030async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
8031 init_test(cx, |_| {});
8032
8033 let mut cx = EditorLspTestContext::new_rust(
8034 lsp::ServerCapabilities {
8035 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8036 ..Default::default()
8037 },
8038 cx,
8039 )
8040 .await;
8041
8042 cx.set_state(indoc! {"
8043 one.twoˇ
8044 "});
8045
8046 // The format request takes a long time. When it completes, it inserts
8047 // a newline and an indent before the `.`
8048 cx.lsp
8049 .handle_request::<lsp::request::Formatting, _, _>(move |_, cx| {
8050 let executor = cx.background_executor().clone();
8051 async move {
8052 executor.timer(Duration::from_millis(100)).await;
8053 Ok(Some(vec![lsp::TextEdit {
8054 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
8055 new_text: "\n ".into(),
8056 }]))
8057 }
8058 });
8059
8060 // Submit a format request.
8061 let format_1 = cx
8062 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8063 .unwrap();
8064 cx.executor().run_until_parked();
8065
8066 // Submit a second format request.
8067 let format_2 = cx
8068 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8069 .unwrap();
8070 cx.executor().run_until_parked();
8071
8072 // Wait for both format requests to complete
8073 cx.executor().advance_clock(Duration::from_millis(200));
8074 cx.executor().start_waiting();
8075 format_1.await.unwrap();
8076 cx.executor().start_waiting();
8077 format_2.await.unwrap();
8078
8079 // The formatting edits only happens once.
8080 cx.assert_editor_state(indoc! {"
8081 one
8082 .twoˇ
8083 "});
8084}
8085
8086#[gpui::test]
8087async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
8088 init_test(cx, |settings| {
8089 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
8090 });
8091
8092 let mut cx = EditorLspTestContext::new_rust(
8093 lsp::ServerCapabilities {
8094 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8095 ..Default::default()
8096 },
8097 cx,
8098 )
8099 .await;
8100
8101 // Set up a buffer white some trailing whitespace and no trailing newline.
8102 cx.set_state(
8103 &[
8104 "one ", //
8105 "twoˇ", //
8106 "three ", //
8107 "four", //
8108 ]
8109 .join("\n"),
8110 );
8111
8112 // Submit a format request.
8113 let format = cx
8114 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8115 .unwrap();
8116
8117 // Record which buffer changes have been sent to the language server
8118 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
8119 cx.lsp
8120 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
8121 let buffer_changes = buffer_changes.clone();
8122 move |params, _| {
8123 buffer_changes.lock().extend(
8124 params
8125 .content_changes
8126 .into_iter()
8127 .map(|e| (e.range.unwrap(), e.text)),
8128 );
8129 }
8130 });
8131
8132 // Handle formatting requests to the language server.
8133 cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
8134 let buffer_changes = buffer_changes.clone();
8135 move |_, _| {
8136 // When formatting is requested, trailing whitespace has already been stripped,
8137 // and the trailing newline has already been added.
8138 assert_eq!(
8139 &buffer_changes.lock()[1..],
8140 &[
8141 (
8142 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
8143 "".into()
8144 ),
8145 (
8146 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
8147 "".into()
8148 ),
8149 (
8150 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
8151 "\n".into()
8152 ),
8153 ]
8154 );
8155
8156 // Insert blank lines between each line of the buffer.
8157 async move {
8158 Ok(Some(vec![
8159 lsp::TextEdit {
8160 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
8161 new_text: "\n".into(),
8162 },
8163 lsp::TextEdit {
8164 range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
8165 new_text: "\n".into(),
8166 },
8167 ]))
8168 }
8169 }
8170 });
8171
8172 // After formatting the buffer, the trailing whitespace is stripped,
8173 // a newline is appended, and the edits provided by the language server
8174 // have been applied.
8175 format.await.unwrap();
8176 cx.assert_editor_state(
8177 &[
8178 "one", //
8179 "", //
8180 "twoˇ", //
8181 "", //
8182 "three", //
8183 "four", //
8184 "", //
8185 ]
8186 .join("\n"),
8187 );
8188
8189 // Undoing the formatting undoes the trailing whitespace removal, the
8190 // trailing newline, and the LSP edits.
8191 cx.update_buffer(|buffer, cx| buffer.undo(cx));
8192 cx.assert_editor_state(
8193 &[
8194 "one ", //
8195 "twoˇ", //
8196 "three ", //
8197 "four", //
8198 ]
8199 .join("\n"),
8200 );
8201}
8202
8203#[gpui::test]
8204async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
8205 cx: &mut TestAppContext,
8206) {
8207 init_test(cx, |_| {});
8208
8209 cx.update(|cx| {
8210 cx.update_global::<SettingsStore, _>(|settings, cx| {
8211 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8212 settings.auto_signature_help = Some(true);
8213 });
8214 });
8215 });
8216
8217 let mut cx = EditorLspTestContext::new_rust(
8218 lsp::ServerCapabilities {
8219 signature_help_provider: Some(lsp::SignatureHelpOptions {
8220 ..Default::default()
8221 }),
8222 ..Default::default()
8223 },
8224 cx,
8225 )
8226 .await;
8227
8228 let language = Language::new(
8229 LanguageConfig {
8230 name: "Rust".into(),
8231 brackets: BracketPairConfig {
8232 pairs: vec![
8233 BracketPair {
8234 start: "{".to_string(),
8235 end: "}".to_string(),
8236 close: true,
8237 surround: true,
8238 newline: true,
8239 },
8240 BracketPair {
8241 start: "(".to_string(),
8242 end: ")".to_string(),
8243 close: true,
8244 surround: true,
8245 newline: true,
8246 },
8247 BracketPair {
8248 start: "/*".to_string(),
8249 end: " */".to_string(),
8250 close: true,
8251 surround: true,
8252 newline: true,
8253 },
8254 BracketPair {
8255 start: "[".to_string(),
8256 end: "]".to_string(),
8257 close: false,
8258 surround: false,
8259 newline: true,
8260 },
8261 BracketPair {
8262 start: "\"".to_string(),
8263 end: "\"".to_string(),
8264 close: true,
8265 surround: true,
8266 newline: false,
8267 },
8268 BracketPair {
8269 start: "<".to_string(),
8270 end: ">".to_string(),
8271 close: false,
8272 surround: true,
8273 newline: true,
8274 },
8275 ],
8276 ..Default::default()
8277 },
8278 autoclose_before: "})]".to_string(),
8279 ..Default::default()
8280 },
8281 Some(tree_sitter_rust::LANGUAGE.into()),
8282 );
8283 let language = Arc::new(language);
8284
8285 cx.language_registry().add(language.clone());
8286 cx.update_buffer(|buffer, cx| {
8287 buffer.set_language(Some(language), cx);
8288 });
8289
8290 cx.set_state(
8291 &r#"
8292 fn main() {
8293 sampleˇ
8294 }
8295 "#
8296 .unindent(),
8297 );
8298
8299 cx.update_editor(|editor, window, cx| {
8300 editor.handle_input("(", window, cx);
8301 });
8302 cx.assert_editor_state(
8303 &"
8304 fn main() {
8305 sample(ˇ)
8306 }
8307 "
8308 .unindent(),
8309 );
8310
8311 let mocked_response = lsp::SignatureHelp {
8312 signatures: vec![lsp::SignatureInformation {
8313 label: "fn sample(param1: u8, param2: u8)".to_string(),
8314 documentation: None,
8315 parameters: Some(vec![
8316 lsp::ParameterInformation {
8317 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8318 documentation: None,
8319 },
8320 lsp::ParameterInformation {
8321 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8322 documentation: None,
8323 },
8324 ]),
8325 active_parameter: None,
8326 }],
8327 active_signature: Some(0),
8328 active_parameter: Some(0),
8329 };
8330 handle_signature_help_request(&mut cx, mocked_response).await;
8331
8332 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8333 .await;
8334
8335 cx.editor(|editor, _, _| {
8336 let signature_help_state = editor.signature_help_state.popover().cloned();
8337 assert_eq!(
8338 signature_help_state.unwrap().label,
8339 "param1: u8, param2: u8"
8340 );
8341 });
8342}
8343
8344#[gpui::test]
8345async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
8346 init_test(cx, |_| {});
8347
8348 cx.update(|cx| {
8349 cx.update_global::<SettingsStore, _>(|settings, cx| {
8350 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8351 settings.auto_signature_help = Some(false);
8352 settings.show_signature_help_after_edits = Some(false);
8353 });
8354 });
8355 });
8356
8357 let mut cx = EditorLspTestContext::new_rust(
8358 lsp::ServerCapabilities {
8359 signature_help_provider: Some(lsp::SignatureHelpOptions {
8360 ..Default::default()
8361 }),
8362 ..Default::default()
8363 },
8364 cx,
8365 )
8366 .await;
8367
8368 let language = Language::new(
8369 LanguageConfig {
8370 name: "Rust".into(),
8371 brackets: BracketPairConfig {
8372 pairs: vec![
8373 BracketPair {
8374 start: "{".to_string(),
8375 end: "}".to_string(),
8376 close: true,
8377 surround: true,
8378 newline: true,
8379 },
8380 BracketPair {
8381 start: "(".to_string(),
8382 end: ")".to_string(),
8383 close: true,
8384 surround: true,
8385 newline: true,
8386 },
8387 BracketPair {
8388 start: "/*".to_string(),
8389 end: " */".to_string(),
8390 close: true,
8391 surround: true,
8392 newline: true,
8393 },
8394 BracketPair {
8395 start: "[".to_string(),
8396 end: "]".to_string(),
8397 close: false,
8398 surround: false,
8399 newline: true,
8400 },
8401 BracketPair {
8402 start: "\"".to_string(),
8403 end: "\"".to_string(),
8404 close: true,
8405 surround: true,
8406 newline: false,
8407 },
8408 BracketPair {
8409 start: "<".to_string(),
8410 end: ">".to_string(),
8411 close: false,
8412 surround: true,
8413 newline: true,
8414 },
8415 ],
8416 ..Default::default()
8417 },
8418 autoclose_before: "})]".to_string(),
8419 ..Default::default()
8420 },
8421 Some(tree_sitter_rust::LANGUAGE.into()),
8422 );
8423 let language = Arc::new(language);
8424
8425 cx.language_registry().add(language.clone());
8426 cx.update_buffer(|buffer, cx| {
8427 buffer.set_language(Some(language), cx);
8428 });
8429
8430 // Ensure that signature_help is not called when no signature help is enabled.
8431 cx.set_state(
8432 &r#"
8433 fn main() {
8434 sampleˇ
8435 }
8436 "#
8437 .unindent(),
8438 );
8439 cx.update_editor(|editor, window, cx| {
8440 editor.handle_input("(", window, cx);
8441 });
8442 cx.assert_editor_state(
8443 &"
8444 fn main() {
8445 sample(ˇ)
8446 }
8447 "
8448 .unindent(),
8449 );
8450 cx.editor(|editor, _, _| {
8451 assert!(editor.signature_help_state.task().is_none());
8452 });
8453
8454 let mocked_response = lsp::SignatureHelp {
8455 signatures: vec![lsp::SignatureInformation {
8456 label: "fn sample(param1: u8, param2: u8)".to_string(),
8457 documentation: None,
8458 parameters: Some(vec![
8459 lsp::ParameterInformation {
8460 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8461 documentation: None,
8462 },
8463 lsp::ParameterInformation {
8464 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8465 documentation: None,
8466 },
8467 ]),
8468 active_parameter: None,
8469 }],
8470 active_signature: Some(0),
8471 active_parameter: Some(0),
8472 };
8473
8474 // Ensure that signature_help is called when enabled afte edits
8475 cx.update(|_, cx| {
8476 cx.update_global::<SettingsStore, _>(|settings, cx| {
8477 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8478 settings.auto_signature_help = Some(false);
8479 settings.show_signature_help_after_edits = Some(true);
8480 });
8481 });
8482 });
8483 cx.set_state(
8484 &r#"
8485 fn main() {
8486 sampleˇ
8487 }
8488 "#
8489 .unindent(),
8490 );
8491 cx.update_editor(|editor, window, cx| {
8492 editor.handle_input("(", window, cx);
8493 });
8494 cx.assert_editor_state(
8495 &"
8496 fn main() {
8497 sample(ˇ)
8498 }
8499 "
8500 .unindent(),
8501 );
8502 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8503 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8504 .await;
8505 cx.update_editor(|editor, _, _| {
8506 let signature_help_state = editor.signature_help_state.popover().cloned();
8507 assert!(signature_help_state.is_some());
8508 assert_eq!(
8509 signature_help_state.unwrap().label,
8510 "param1: u8, param2: u8"
8511 );
8512 editor.signature_help_state = SignatureHelpState::default();
8513 });
8514
8515 // Ensure that signature_help is called when auto signature help override is enabled
8516 cx.update(|_, cx| {
8517 cx.update_global::<SettingsStore, _>(|settings, cx| {
8518 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8519 settings.auto_signature_help = Some(true);
8520 settings.show_signature_help_after_edits = Some(false);
8521 });
8522 });
8523 });
8524 cx.set_state(
8525 &r#"
8526 fn main() {
8527 sampleˇ
8528 }
8529 "#
8530 .unindent(),
8531 );
8532 cx.update_editor(|editor, window, cx| {
8533 editor.handle_input("(", window, cx);
8534 });
8535 cx.assert_editor_state(
8536 &"
8537 fn main() {
8538 sample(ˇ)
8539 }
8540 "
8541 .unindent(),
8542 );
8543 handle_signature_help_request(&mut cx, mocked_response).await;
8544 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8545 .await;
8546 cx.editor(|editor, _, _| {
8547 let signature_help_state = editor.signature_help_state.popover().cloned();
8548 assert!(signature_help_state.is_some());
8549 assert_eq!(
8550 signature_help_state.unwrap().label,
8551 "param1: u8, param2: u8"
8552 );
8553 });
8554}
8555
8556#[gpui::test]
8557async fn test_signature_help(cx: &mut TestAppContext) {
8558 init_test(cx, |_| {});
8559 cx.update(|cx| {
8560 cx.update_global::<SettingsStore, _>(|settings, cx| {
8561 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8562 settings.auto_signature_help = Some(true);
8563 });
8564 });
8565 });
8566
8567 let mut cx = EditorLspTestContext::new_rust(
8568 lsp::ServerCapabilities {
8569 signature_help_provider: Some(lsp::SignatureHelpOptions {
8570 ..Default::default()
8571 }),
8572 ..Default::default()
8573 },
8574 cx,
8575 )
8576 .await;
8577
8578 // A test that directly calls `show_signature_help`
8579 cx.update_editor(|editor, window, cx| {
8580 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8581 });
8582
8583 let mocked_response = lsp::SignatureHelp {
8584 signatures: vec![lsp::SignatureInformation {
8585 label: "fn sample(param1: u8, param2: u8)".to_string(),
8586 documentation: None,
8587 parameters: Some(vec![
8588 lsp::ParameterInformation {
8589 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8590 documentation: None,
8591 },
8592 lsp::ParameterInformation {
8593 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8594 documentation: None,
8595 },
8596 ]),
8597 active_parameter: None,
8598 }],
8599 active_signature: Some(0),
8600 active_parameter: Some(0),
8601 };
8602 handle_signature_help_request(&mut cx, mocked_response).await;
8603
8604 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8605 .await;
8606
8607 cx.editor(|editor, _, _| {
8608 let signature_help_state = editor.signature_help_state.popover().cloned();
8609 assert!(signature_help_state.is_some());
8610 assert_eq!(
8611 signature_help_state.unwrap().label,
8612 "param1: u8, param2: u8"
8613 );
8614 });
8615
8616 // When exiting outside from inside the brackets, `signature_help` is closed.
8617 cx.set_state(indoc! {"
8618 fn main() {
8619 sample(ˇ);
8620 }
8621
8622 fn sample(param1: u8, param2: u8) {}
8623 "});
8624
8625 cx.update_editor(|editor, window, cx| {
8626 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
8627 });
8628
8629 let mocked_response = lsp::SignatureHelp {
8630 signatures: Vec::new(),
8631 active_signature: None,
8632 active_parameter: None,
8633 };
8634 handle_signature_help_request(&mut cx, mocked_response).await;
8635
8636 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
8637 .await;
8638
8639 cx.editor(|editor, _, _| {
8640 assert!(!editor.signature_help_state.is_shown());
8641 });
8642
8643 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
8644 cx.set_state(indoc! {"
8645 fn main() {
8646 sample(ˇ);
8647 }
8648
8649 fn sample(param1: u8, param2: u8) {}
8650 "});
8651
8652 let mocked_response = lsp::SignatureHelp {
8653 signatures: vec![lsp::SignatureInformation {
8654 label: "fn sample(param1: u8, param2: u8)".to_string(),
8655 documentation: None,
8656 parameters: Some(vec![
8657 lsp::ParameterInformation {
8658 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8659 documentation: None,
8660 },
8661 lsp::ParameterInformation {
8662 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8663 documentation: None,
8664 },
8665 ]),
8666 active_parameter: None,
8667 }],
8668 active_signature: Some(0),
8669 active_parameter: Some(0),
8670 };
8671 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8672 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8673 .await;
8674 cx.editor(|editor, _, _| {
8675 assert!(editor.signature_help_state.is_shown());
8676 });
8677
8678 // Restore the popover with more parameter input
8679 cx.set_state(indoc! {"
8680 fn main() {
8681 sample(param1, param2ˇ);
8682 }
8683
8684 fn sample(param1: u8, param2: u8) {}
8685 "});
8686
8687 let mocked_response = lsp::SignatureHelp {
8688 signatures: vec![lsp::SignatureInformation {
8689 label: "fn sample(param1: u8, param2: u8)".to_string(),
8690 documentation: None,
8691 parameters: Some(vec![
8692 lsp::ParameterInformation {
8693 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8694 documentation: None,
8695 },
8696 lsp::ParameterInformation {
8697 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8698 documentation: None,
8699 },
8700 ]),
8701 active_parameter: None,
8702 }],
8703 active_signature: Some(0),
8704 active_parameter: Some(1),
8705 };
8706 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8707 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8708 .await;
8709
8710 // When selecting a range, the popover is gone.
8711 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
8712 cx.update_editor(|editor, window, cx| {
8713 editor.change_selections(None, window, cx, |s| {
8714 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
8715 })
8716 });
8717 cx.assert_editor_state(indoc! {"
8718 fn main() {
8719 sample(param1, «ˇparam2»);
8720 }
8721
8722 fn sample(param1: u8, param2: u8) {}
8723 "});
8724 cx.editor(|editor, _, _| {
8725 assert!(!editor.signature_help_state.is_shown());
8726 });
8727
8728 // When unselecting again, the popover is back if within the brackets.
8729 cx.update_editor(|editor, window, cx| {
8730 editor.change_selections(None, window, cx, |s| {
8731 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8732 })
8733 });
8734 cx.assert_editor_state(indoc! {"
8735 fn main() {
8736 sample(param1, ˇparam2);
8737 }
8738
8739 fn sample(param1: u8, param2: u8) {}
8740 "});
8741 handle_signature_help_request(&mut cx, mocked_response).await;
8742 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8743 .await;
8744 cx.editor(|editor, _, _| {
8745 assert!(editor.signature_help_state.is_shown());
8746 });
8747
8748 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
8749 cx.update_editor(|editor, window, cx| {
8750 editor.change_selections(None, window, cx, |s| {
8751 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
8752 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8753 })
8754 });
8755 cx.assert_editor_state(indoc! {"
8756 fn main() {
8757 sample(param1, ˇparam2);
8758 }
8759
8760 fn sample(param1: u8, param2: u8) {}
8761 "});
8762
8763 let mocked_response = lsp::SignatureHelp {
8764 signatures: vec![lsp::SignatureInformation {
8765 label: "fn sample(param1: u8, param2: u8)".to_string(),
8766 documentation: None,
8767 parameters: Some(vec![
8768 lsp::ParameterInformation {
8769 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8770 documentation: None,
8771 },
8772 lsp::ParameterInformation {
8773 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8774 documentation: None,
8775 },
8776 ]),
8777 active_parameter: None,
8778 }],
8779 active_signature: Some(0),
8780 active_parameter: Some(1),
8781 };
8782 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8783 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8784 .await;
8785 cx.update_editor(|editor, _, cx| {
8786 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
8787 });
8788 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
8789 .await;
8790 cx.update_editor(|editor, window, cx| {
8791 editor.change_selections(None, window, cx, |s| {
8792 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
8793 })
8794 });
8795 cx.assert_editor_state(indoc! {"
8796 fn main() {
8797 sample(param1, «ˇparam2»);
8798 }
8799
8800 fn sample(param1: u8, param2: u8) {}
8801 "});
8802 cx.update_editor(|editor, window, cx| {
8803 editor.change_selections(None, window, cx, |s| {
8804 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8805 })
8806 });
8807 cx.assert_editor_state(indoc! {"
8808 fn main() {
8809 sample(param1, ˇparam2);
8810 }
8811
8812 fn sample(param1: u8, param2: u8) {}
8813 "});
8814 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
8815 .await;
8816}
8817
8818#[gpui::test]
8819async fn test_completion(cx: &mut TestAppContext) {
8820 init_test(cx, |_| {});
8821
8822 let mut cx = EditorLspTestContext::new_rust(
8823 lsp::ServerCapabilities {
8824 completion_provider: Some(lsp::CompletionOptions {
8825 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
8826 resolve_provider: Some(true),
8827 ..Default::default()
8828 }),
8829 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
8830 ..Default::default()
8831 },
8832 cx,
8833 )
8834 .await;
8835 let counter = Arc::new(AtomicUsize::new(0));
8836
8837 cx.set_state(indoc! {"
8838 oneˇ
8839 two
8840 three
8841 "});
8842 cx.simulate_keystroke(".");
8843 handle_completion_request(
8844 &mut cx,
8845 indoc! {"
8846 one.|<>
8847 two
8848 three
8849 "},
8850 vec!["first_completion", "second_completion"],
8851 counter.clone(),
8852 )
8853 .await;
8854 cx.condition(|editor, _| editor.context_menu_visible())
8855 .await;
8856 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
8857
8858 let _handler = handle_signature_help_request(
8859 &mut cx,
8860 lsp::SignatureHelp {
8861 signatures: vec![lsp::SignatureInformation {
8862 label: "test signature".to_string(),
8863 documentation: None,
8864 parameters: Some(vec![lsp::ParameterInformation {
8865 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
8866 documentation: None,
8867 }]),
8868 active_parameter: None,
8869 }],
8870 active_signature: None,
8871 active_parameter: None,
8872 },
8873 );
8874 cx.update_editor(|editor, window, cx| {
8875 assert!(
8876 !editor.signature_help_state.is_shown(),
8877 "No signature help was called for"
8878 );
8879 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8880 });
8881 cx.run_until_parked();
8882 cx.update_editor(|editor, _, _| {
8883 assert!(
8884 !editor.signature_help_state.is_shown(),
8885 "No signature help should be shown when completions menu is open"
8886 );
8887 });
8888
8889 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8890 editor.context_menu_next(&Default::default(), window, cx);
8891 editor
8892 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8893 .unwrap()
8894 });
8895 cx.assert_editor_state(indoc! {"
8896 one.second_completionˇ
8897 two
8898 three
8899 "});
8900
8901 handle_resolve_completion_request(
8902 &mut cx,
8903 Some(vec![
8904 (
8905 //This overlaps with the primary completion edit which is
8906 //misbehavior from the LSP spec, test that we filter it out
8907 indoc! {"
8908 one.second_ˇcompletion
8909 two
8910 threeˇ
8911 "},
8912 "overlapping additional edit",
8913 ),
8914 (
8915 indoc! {"
8916 one.second_completion
8917 two
8918 threeˇ
8919 "},
8920 "\nadditional edit",
8921 ),
8922 ]),
8923 )
8924 .await;
8925 apply_additional_edits.await.unwrap();
8926 cx.assert_editor_state(indoc! {"
8927 one.second_completionˇ
8928 two
8929 three
8930 additional edit
8931 "});
8932
8933 cx.set_state(indoc! {"
8934 one.second_completion
8935 twoˇ
8936 threeˇ
8937 additional edit
8938 "});
8939 cx.simulate_keystroke(" ");
8940 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8941 cx.simulate_keystroke("s");
8942 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8943
8944 cx.assert_editor_state(indoc! {"
8945 one.second_completion
8946 two sˇ
8947 three sˇ
8948 additional edit
8949 "});
8950 handle_completion_request(
8951 &mut cx,
8952 indoc! {"
8953 one.second_completion
8954 two s
8955 three <s|>
8956 additional edit
8957 "},
8958 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
8959 counter.clone(),
8960 )
8961 .await;
8962 cx.condition(|editor, _| editor.context_menu_visible())
8963 .await;
8964 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
8965
8966 cx.simulate_keystroke("i");
8967
8968 handle_completion_request(
8969 &mut cx,
8970 indoc! {"
8971 one.second_completion
8972 two si
8973 three <si|>
8974 additional edit
8975 "},
8976 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
8977 counter.clone(),
8978 )
8979 .await;
8980 cx.condition(|editor, _| editor.context_menu_visible())
8981 .await;
8982 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
8983
8984 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8985 editor
8986 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8987 .unwrap()
8988 });
8989 cx.assert_editor_state(indoc! {"
8990 one.second_completion
8991 two sixth_completionˇ
8992 three sixth_completionˇ
8993 additional edit
8994 "});
8995
8996 apply_additional_edits.await.unwrap();
8997
8998 update_test_language_settings(&mut cx, |settings| {
8999 settings.defaults.show_completions_on_input = Some(false);
9000 });
9001 cx.set_state("editorˇ");
9002 cx.simulate_keystroke(".");
9003 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9004 cx.simulate_keystroke("c");
9005 cx.simulate_keystroke("l");
9006 cx.simulate_keystroke("o");
9007 cx.assert_editor_state("editor.cloˇ");
9008 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9009 cx.update_editor(|editor, window, cx| {
9010 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9011 });
9012 handle_completion_request(
9013 &mut cx,
9014 "editor.<clo|>",
9015 vec!["close", "clobber"],
9016 counter.clone(),
9017 )
9018 .await;
9019 cx.condition(|editor, _| editor.context_menu_visible())
9020 .await;
9021 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
9022
9023 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9024 editor
9025 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9026 .unwrap()
9027 });
9028 cx.assert_editor_state("editor.closeˇ");
9029 handle_resolve_completion_request(&mut cx, None).await;
9030 apply_additional_edits.await.unwrap();
9031}
9032
9033#[gpui::test]
9034async fn test_multiline_completion(cx: &mut TestAppContext) {
9035 init_test(cx, |_| {});
9036
9037 let fs = FakeFs::new(cx.executor());
9038 fs.insert_tree(
9039 path!("/a"),
9040 json!({
9041 "main.ts": "a",
9042 }),
9043 )
9044 .await;
9045
9046 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9047 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9048 let typescript_language = Arc::new(Language::new(
9049 LanguageConfig {
9050 name: "TypeScript".into(),
9051 matcher: LanguageMatcher {
9052 path_suffixes: vec!["ts".to_string()],
9053 ..LanguageMatcher::default()
9054 },
9055 line_comments: vec!["// ".into()],
9056 ..LanguageConfig::default()
9057 },
9058 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9059 ));
9060 language_registry.add(typescript_language.clone());
9061 let mut fake_servers = language_registry.register_fake_lsp(
9062 "TypeScript",
9063 FakeLspAdapter {
9064 capabilities: lsp::ServerCapabilities {
9065 completion_provider: Some(lsp::CompletionOptions {
9066 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9067 ..lsp::CompletionOptions::default()
9068 }),
9069 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9070 ..lsp::ServerCapabilities::default()
9071 },
9072 // Emulate vtsls label generation
9073 label_for_completion: Some(Box::new(|item, _| {
9074 let text = if let Some(description) = item
9075 .label_details
9076 .as_ref()
9077 .and_then(|label_details| label_details.description.as_ref())
9078 {
9079 format!("{} {}", item.label, description)
9080 } else if let Some(detail) = &item.detail {
9081 format!("{} {}", item.label, detail)
9082 } else {
9083 item.label.clone()
9084 };
9085 let len = text.len();
9086 Some(language::CodeLabel {
9087 text,
9088 runs: Vec::new(),
9089 filter_range: 0..len,
9090 })
9091 })),
9092 ..FakeLspAdapter::default()
9093 },
9094 );
9095 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9096 let cx = &mut VisualTestContext::from_window(*workspace, cx);
9097 let worktree_id = workspace
9098 .update(cx, |workspace, _window, cx| {
9099 workspace.project().update(cx, |project, cx| {
9100 project.worktrees(cx).next().unwrap().read(cx).id()
9101 })
9102 })
9103 .unwrap();
9104 let _buffer = project
9105 .update(cx, |project, cx| {
9106 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
9107 })
9108 .await
9109 .unwrap();
9110 let editor = workspace
9111 .update(cx, |workspace, window, cx| {
9112 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
9113 })
9114 .unwrap()
9115 .await
9116 .unwrap()
9117 .downcast::<Editor>()
9118 .unwrap();
9119 let fake_server = fake_servers.next().await.unwrap();
9120
9121 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
9122 let multiline_label_2 = "a\nb\nc\n";
9123 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
9124 let multiline_description = "d\ne\nf\n";
9125 let multiline_detail_2 = "g\nh\ni\n";
9126
9127 let mut completion_handle =
9128 fake_server.handle_request::<lsp::request::Completion, _, _>(move |params, _| async move {
9129 Ok(Some(lsp::CompletionResponse::Array(vec![
9130 lsp::CompletionItem {
9131 label: multiline_label.to_string(),
9132 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9133 range: lsp::Range {
9134 start: lsp::Position {
9135 line: params.text_document_position.position.line,
9136 character: params.text_document_position.position.character,
9137 },
9138 end: lsp::Position {
9139 line: params.text_document_position.position.line,
9140 character: params.text_document_position.position.character,
9141 },
9142 },
9143 new_text: "new_text_1".to_string(),
9144 })),
9145 ..lsp::CompletionItem::default()
9146 },
9147 lsp::CompletionItem {
9148 label: "single line label 1".to_string(),
9149 detail: Some(multiline_detail.to_string()),
9150 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9151 range: lsp::Range {
9152 start: lsp::Position {
9153 line: params.text_document_position.position.line,
9154 character: params.text_document_position.position.character,
9155 },
9156 end: lsp::Position {
9157 line: params.text_document_position.position.line,
9158 character: params.text_document_position.position.character,
9159 },
9160 },
9161 new_text: "new_text_2".to_string(),
9162 })),
9163 ..lsp::CompletionItem::default()
9164 },
9165 lsp::CompletionItem {
9166 label: "single line label 2".to_string(),
9167 label_details: Some(lsp::CompletionItemLabelDetails {
9168 description: Some(multiline_description.to_string()),
9169 detail: None,
9170 }),
9171 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9172 range: lsp::Range {
9173 start: lsp::Position {
9174 line: params.text_document_position.position.line,
9175 character: params.text_document_position.position.character,
9176 },
9177 end: lsp::Position {
9178 line: params.text_document_position.position.line,
9179 character: params.text_document_position.position.character,
9180 },
9181 },
9182 new_text: "new_text_2".to_string(),
9183 })),
9184 ..lsp::CompletionItem::default()
9185 },
9186 lsp::CompletionItem {
9187 label: multiline_label_2.to_string(),
9188 detail: Some(multiline_detail_2.to_string()),
9189 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9190 range: lsp::Range {
9191 start: lsp::Position {
9192 line: params.text_document_position.position.line,
9193 character: params.text_document_position.position.character,
9194 },
9195 end: lsp::Position {
9196 line: params.text_document_position.position.line,
9197 character: params.text_document_position.position.character,
9198 },
9199 },
9200 new_text: "new_text_3".to_string(),
9201 })),
9202 ..lsp::CompletionItem::default()
9203 },
9204 lsp::CompletionItem {
9205 label: "Label with many spaces and \t but without newlines".to_string(),
9206 detail: Some(
9207 "Details with many spaces and \t but without newlines".to_string(),
9208 ),
9209 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9210 range: lsp::Range {
9211 start: lsp::Position {
9212 line: params.text_document_position.position.line,
9213 character: params.text_document_position.position.character,
9214 },
9215 end: lsp::Position {
9216 line: params.text_document_position.position.line,
9217 character: params.text_document_position.position.character,
9218 },
9219 },
9220 new_text: "new_text_4".to_string(),
9221 })),
9222 ..lsp::CompletionItem::default()
9223 },
9224 ])))
9225 });
9226
9227 editor.update_in(cx, |editor, window, cx| {
9228 cx.focus_self(window);
9229 editor.move_to_end(&MoveToEnd, window, cx);
9230 editor.handle_input(".", window, cx);
9231 });
9232 cx.run_until_parked();
9233 completion_handle.next().await.unwrap();
9234
9235 editor.update(cx, |editor, _| {
9236 assert!(editor.context_menu_visible());
9237 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9238 {
9239 let completion_labels = menu
9240 .completions
9241 .borrow()
9242 .iter()
9243 .map(|c| c.label.text.clone())
9244 .collect::<Vec<_>>();
9245 assert_eq!(
9246 completion_labels,
9247 &[
9248 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
9249 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
9250 "single line label 2 d e f ",
9251 "a b c g h i ",
9252 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
9253 ],
9254 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
9255 );
9256
9257 for completion in menu
9258 .completions
9259 .borrow()
9260 .iter() {
9261 assert_eq!(
9262 completion.label.filter_range,
9263 0..completion.label.text.len(),
9264 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
9265 );
9266 }
9267
9268 } else {
9269 panic!("expected completion menu to be open");
9270 }
9271 });
9272}
9273
9274#[gpui::test]
9275async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
9276 init_test(cx, |_| {});
9277 let mut cx = EditorLspTestContext::new_rust(
9278 lsp::ServerCapabilities {
9279 completion_provider: Some(lsp::CompletionOptions {
9280 trigger_characters: Some(vec![".".to_string()]),
9281 ..Default::default()
9282 }),
9283 ..Default::default()
9284 },
9285 cx,
9286 )
9287 .await;
9288 cx.lsp
9289 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9290 Ok(Some(lsp::CompletionResponse::Array(vec![
9291 lsp::CompletionItem {
9292 label: "first".into(),
9293 ..Default::default()
9294 },
9295 lsp::CompletionItem {
9296 label: "last".into(),
9297 ..Default::default()
9298 },
9299 ])))
9300 });
9301 cx.set_state("variableˇ");
9302 cx.simulate_keystroke(".");
9303 cx.executor().run_until_parked();
9304
9305 cx.update_editor(|editor, _, _| {
9306 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9307 {
9308 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
9309 } else {
9310 panic!("expected completion menu to be open");
9311 }
9312 });
9313
9314 cx.update_editor(|editor, window, cx| {
9315 editor.move_page_down(&MovePageDown::default(), window, cx);
9316 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9317 {
9318 assert!(
9319 menu.selected_item == 1,
9320 "expected PageDown to select the last item from the context menu"
9321 );
9322 } else {
9323 panic!("expected completion menu to stay open after PageDown");
9324 }
9325 });
9326
9327 cx.update_editor(|editor, window, cx| {
9328 editor.move_page_up(&MovePageUp::default(), window, cx);
9329 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9330 {
9331 assert!(
9332 menu.selected_item == 0,
9333 "expected PageUp to select the first item from the context menu"
9334 );
9335 } else {
9336 panic!("expected completion menu to stay open after PageUp");
9337 }
9338 });
9339}
9340
9341#[gpui::test]
9342async fn test_completion_sort(cx: &mut TestAppContext) {
9343 init_test(cx, |_| {});
9344 let mut cx = EditorLspTestContext::new_rust(
9345 lsp::ServerCapabilities {
9346 completion_provider: Some(lsp::CompletionOptions {
9347 trigger_characters: Some(vec![".".to_string()]),
9348 ..Default::default()
9349 }),
9350 ..Default::default()
9351 },
9352 cx,
9353 )
9354 .await;
9355 cx.lsp
9356 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9357 Ok(Some(lsp::CompletionResponse::Array(vec![
9358 lsp::CompletionItem {
9359 label: "Range".into(),
9360 sort_text: Some("a".into()),
9361 ..Default::default()
9362 },
9363 lsp::CompletionItem {
9364 label: "r".into(),
9365 sort_text: Some("b".into()),
9366 ..Default::default()
9367 },
9368 lsp::CompletionItem {
9369 label: "ret".into(),
9370 sort_text: Some("c".into()),
9371 ..Default::default()
9372 },
9373 lsp::CompletionItem {
9374 label: "return".into(),
9375 sort_text: Some("d".into()),
9376 ..Default::default()
9377 },
9378 lsp::CompletionItem {
9379 label: "slice".into(),
9380 sort_text: Some("d".into()),
9381 ..Default::default()
9382 },
9383 ])))
9384 });
9385 cx.set_state("rˇ");
9386 cx.executor().run_until_parked();
9387 cx.update_editor(|editor, window, cx| {
9388 editor.show_completions(
9389 &ShowCompletions {
9390 trigger: Some("r".into()),
9391 },
9392 window,
9393 cx,
9394 );
9395 });
9396 cx.executor().run_until_parked();
9397
9398 cx.update_editor(|editor, _, _| {
9399 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9400 {
9401 assert_eq!(
9402 completion_menu_entries(&menu),
9403 &["r", "ret", "Range", "return"]
9404 );
9405 } else {
9406 panic!("expected completion menu to be open");
9407 }
9408 });
9409}
9410
9411#[gpui::test]
9412async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
9413 init_test(cx, |_| {});
9414
9415 let mut cx = EditorLspTestContext::new_rust(
9416 lsp::ServerCapabilities {
9417 completion_provider: Some(lsp::CompletionOptions {
9418 trigger_characters: Some(vec![".".to_string()]),
9419 resolve_provider: Some(true),
9420 ..Default::default()
9421 }),
9422 ..Default::default()
9423 },
9424 cx,
9425 )
9426 .await;
9427
9428 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
9429 cx.simulate_keystroke(".");
9430 let completion_item = lsp::CompletionItem {
9431 label: "Some".into(),
9432 kind: Some(lsp::CompletionItemKind::SNIPPET),
9433 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
9434 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
9435 kind: lsp::MarkupKind::Markdown,
9436 value: "```rust\nSome(2)\n```".to_string(),
9437 })),
9438 deprecated: Some(false),
9439 sort_text: Some("Some".to_string()),
9440 filter_text: Some("Some".to_string()),
9441 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
9442 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9443 range: lsp::Range {
9444 start: lsp::Position {
9445 line: 0,
9446 character: 22,
9447 },
9448 end: lsp::Position {
9449 line: 0,
9450 character: 22,
9451 },
9452 },
9453 new_text: "Some(2)".to_string(),
9454 })),
9455 additional_text_edits: Some(vec![lsp::TextEdit {
9456 range: lsp::Range {
9457 start: lsp::Position {
9458 line: 0,
9459 character: 20,
9460 },
9461 end: lsp::Position {
9462 line: 0,
9463 character: 22,
9464 },
9465 },
9466 new_text: "".to_string(),
9467 }]),
9468 ..Default::default()
9469 };
9470
9471 let closure_completion_item = completion_item.clone();
9472 let counter = Arc::new(AtomicUsize::new(0));
9473 let counter_clone = counter.clone();
9474 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
9475 let task_completion_item = closure_completion_item.clone();
9476 counter_clone.fetch_add(1, atomic::Ordering::Release);
9477 async move {
9478 Ok(Some(lsp::CompletionResponse::Array(vec![
9479 task_completion_item,
9480 ])))
9481 }
9482 });
9483
9484 cx.condition(|editor, _| editor.context_menu_visible())
9485 .await;
9486 cx.assert_editor_state(indoc! {"fn main() { let a = 2.ˇ; }"});
9487 assert!(request.next().await.is_some());
9488 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9489
9490 cx.simulate_keystroke("S");
9491 cx.simulate_keystroke("o");
9492 cx.simulate_keystroke("m");
9493 cx.condition(|editor, _| editor.context_menu_visible())
9494 .await;
9495 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Somˇ; }"});
9496 assert!(request.next().await.is_some());
9497 assert!(request.next().await.is_some());
9498 assert!(request.next().await.is_some());
9499 request.close();
9500 assert!(request.next().await.is_none());
9501 assert_eq!(
9502 counter.load(atomic::Ordering::Acquire),
9503 4,
9504 "With the completions menu open, only one LSP request should happen per input"
9505 );
9506}
9507
9508#[gpui::test]
9509async fn test_toggle_comment(cx: &mut TestAppContext) {
9510 init_test(cx, |_| {});
9511 let mut cx = EditorTestContext::new(cx).await;
9512 let language = Arc::new(Language::new(
9513 LanguageConfig {
9514 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
9515 ..Default::default()
9516 },
9517 Some(tree_sitter_rust::LANGUAGE.into()),
9518 ));
9519 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
9520
9521 // If multiple selections intersect a line, the line is only toggled once.
9522 cx.set_state(indoc! {"
9523 fn a() {
9524 «//b();
9525 ˇ»// «c();
9526 //ˇ» d();
9527 }
9528 "});
9529
9530 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9531
9532 cx.assert_editor_state(indoc! {"
9533 fn a() {
9534 «b();
9535 c();
9536 ˇ» d();
9537 }
9538 "});
9539
9540 // The comment prefix is inserted at the same column for every line in a
9541 // selection.
9542 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9543
9544 cx.assert_editor_state(indoc! {"
9545 fn a() {
9546 // «b();
9547 // c();
9548 ˇ»// d();
9549 }
9550 "});
9551
9552 // If a selection ends at the beginning of a line, that line is not toggled.
9553 cx.set_selections_state(indoc! {"
9554 fn a() {
9555 // b();
9556 «// c();
9557 ˇ» // d();
9558 }
9559 "});
9560
9561 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9562
9563 cx.assert_editor_state(indoc! {"
9564 fn a() {
9565 // b();
9566 «c();
9567 ˇ» // d();
9568 }
9569 "});
9570
9571 // If a selection span a single line and is empty, the line is toggled.
9572 cx.set_state(indoc! {"
9573 fn a() {
9574 a();
9575 b();
9576 ˇ
9577 }
9578 "});
9579
9580 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9581
9582 cx.assert_editor_state(indoc! {"
9583 fn a() {
9584 a();
9585 b();
9586 //•ˇ
9587 }
9588 "});
9589
9590 // If a selection span multiple lines, empty lines are not toggled.
9591 cx.set_state(indoc! {"
9592 fn a() {
9593 «a();
9594
9595 c();ˇ»
9596 }
9597 "});
9598
9599 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9600
9601 cx.assert_editor_state(indoc! {"
9602 fn a() {
9603 // «a();
9604
9605 // c();ˇ»
9606 }
9607 "});
9608
9609 // If a selection includes multiple comment prefixes, all lines are uncommented.
9610 cx.set_state(indoc! {"
9611 fn a() {
9612 «// a();
9613 /// b();
9614 //! c();ˇ»
9615 }
9616 "});
9617
9618 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9619
9620 cx.assert_editor_state(indoc! {"
9621 fn a() {
9622 «a();
9623 b();
9624 c();ˇ»
9625 }
9626 "});
9627}
9628
9629#[gpui::test]
9630async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
9631 init_test(cx, |_| {});
9632 let mut cx = EditorTestContext::new(cx).await;
9633 let language = Arc::new(Language::new(
9634 LanguageConfig {
9635 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
9636 ..Default::default()
9637 },
9638 Some(tree_sitter_rust::LANGUAGE.into()),
9639 ));
9640 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
9641
9642 let toggle_comments = &ToggleComments {
9643 advance_downwards: false,
9644 ignore_indent: true,
9645 };
9646
9647 // If multiple selections intersect a line, the line is only toggled once.
9648 cx.set_state(indoc! {"
9649 fn a() {
9650 // «b();
9651 // c();
9652 // ˇ» d();
9653 }
9654 "});
9655
9656 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9657
9658 cx.assert_editor_state(indoc! {"
9659 fn a() {
9660 «b();
9661 c();
9662 ˇ» d();
9663 }
9664 "});
9665
9666 // The comment prefix is inserted at the beginning of each line
9667 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9668
9669 cx.assert_editor_state(indoc! {"
9670 fn a() {
9671 // «b();
9672 // c();
9673 // ˇ» d();
9674 }
9675 "});
9676
9677 // If a selection ends at the beginning of a line, that line is not toggled.
9678 cx.set_selections_state(indoc! {"
9679 fn a() {
9680 // b();
9681 // «c();
9682 ˇ»// d();
9683 }
9684 "});
9685
9686 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9687
9688 cx.assert_editor_state(indoc! {"
9689 fn a() {
9690 // b();
9691 «c();
9692 ˇ»// d();
9693 }
9694 "});
9695
9696 // If a selection span a single line and is empty, the line is toggled.
9697 cx.set_state(indoc! {"
9698 fn a() {
9699 a();
9700 b();
9701 ˇ
9702 }
9703 "});
9704
9705 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9706
9707 cx.assert_editor_state(indoc! {"
9708 fn a() {
9709 a();
9710 b();
9711 //ˇ
9712 }
9713 "});
9714
9715 // If a selection span multiple lines, empty lines are not toggled.
9716 cx.set_state(indoc! {"
9717 fn a() {
9718 «a();
9719
9720 c();ˇ»
9721 }
9722 "});
9723
9724 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9725
9726 cx.assert_editor_state(indoc! {"
9727 fn a() {
9728 // «a();
9729
9730 // c();ˇ»
9731 }
9732 "});
9733
9734 // If a selection includes multiple comment prefixes, all lines are uncommented.
9735 cx.set_state(indoc! {"
9736 fn a() {
9737 // «a();
9738 /// b();
9739 //! c();ˇ»
9740 }
9741 "});
9742
9743 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9744
9745 cx.assert_editor_state(indoc! {"
9746 fn a() {
9747 «a();
9748 b();
9749 c();ˇ»
9750 }
9751 "});
9752}
9753
9754#[gpui::test]
9755async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
9756 init_test(cx, |_| {});
9757
9758 let language = Arc::new(Language::new(
9759 LanguageConfig {
9760 line_comments: vec!["// ".into()],
9761 ..Default::default()
9762 },
9763 Some(tree_sitter_rust::LANGUAGE.into()),
9764 ));
9765
9766 let mut cx = EditorTestContext::new(cx).await;
9767
9768 cx.language_registry().add(language.clone());
9769 cx.update_buffer(|buffer, cx| {
9770 buffer.set_language(Some(language), cx);
9771 });
9772
9773 let toggle_comments = &ToggleComments {
9774 advance_downwards: true,
9775 ignore_indent: false,
9776 };
9777
9778 // Single cursor on one line -> advance
9779 // Cursor moves horizontally 3 characters as well on non-blank line
9780 cx.set_state(indoc!(
9781 "fn a() {
9782 ˇdog();
9783 cat();
9784 }"
9785 ));
9786 cx.update_editor(|editor, window, cx| {
9787 editor.toggle_comments(toggle_comments, window, cx);
9788 });
9789 cx.assert_editor_state(indoc!(
9790 "fn a() {
9791 // dog();
9792 catˇ();
9793 }"
9794 ));
9795
9796 // Single selection on one line -> don't advance
9797 cx.set_state(indoc!(
9798 "fn a() {
9799 «dog()ˇ»;
9800 cat();
9801 }"
9802 ));
9803 cx.update_editor(|editor, window, cx| {
9804 editor.toggle_comments(toggle_comments, window, cx);
9805 });
9806 cx.assert_editor_state(indoc!(
9807 "fn a() {
9808 // «dog()ˇ»;
9809 cat();
9810 }"
9811 ));
9812
9813 // Multiple cursors on one line -> advance
9814 cx.set_state(indoc!(
9815 "fn a() {
9816 ˇdˇog();
9817 cat();
9818 }"
9819 ));
9820 cx.update_editor(|editor, window, cx| {
9821 editor.toggle_comments(toggle_comments, window, cx);
9822 });
9823 cx.assert_editor_state(indoc!(
9824 "fn a() {
9825 // dog();
9826 catˇ(ˇ);
9827 }"
9828 ));
9829
9830 // Multiple cursors on one line, with selection -> don't advance
9831 cx.set_state(indoc!(
9832 "fn a() {
9833 ˇdˇog«()ˇ»;
9834 cat();
9835 }"
9836 ));
9837 cx.update_editor(|editor, window, cx| {
9838 editor.toggle_comments(toggle_comments, window, cx);
9839 });
9840 cx.assert_editor_state(indoc!(
9841 "fn a() {
9842 // ˇdˇog«()ˇ»;
9843 cat();
9844 }"
9845 ));
9846
9847 // Single cursor on one line -> advance
9848 // Cursor moves to column 0 on blank line
9849 cx.set_state(indoc!(
9850 "fn a() {
9851 ˇdog();
9852
9853 cat();
9854 }"
9855 ));
9856 cx.update_editor(|editor, window, cx| {
9857 editor.toggle_comments(toggle_comments, window, cx);
9858 });
9859 cx.assert_editor_state(indoc!(
9860 "fn a() {
9861 // dog();
9862 ˇ
9863 cat();
9864 }"
9865 ));
9866
9867 // Single cursor on one line -> advance
9868 // Cursor starts and ends at column 0
9869 cx.set_state(indoc!(
9870 "fn a() {
9871 ˇ dog();
9872 cat();
9873 }"
9874 ));
9875 cx.update_editor(|editor, window, cx| {
9876 editor.toggle_comments(toggle_comments, window, cx);
9877 });
9878 cx.assert_editor_state(indoc!(
9879 "fn a() {
9880 // dog();
9881 ˇ cat();
9882 }"
9883 ));
9884}
9885
9886#[gpui::test]
9887async fn test_toggle_block_comment(cx: &mut TestAppContext) {
9888 init_test(cx, |_| {});
9889
9890 let mut cx = EditorTestContext::new(cx).await;
9891
9892 let html_language = Arc::new(
9893 Language::new(
9894 LanguageConfig {
9895 name: "HTML".into(),
9896 block_comment: Some(("<!-- ".into(), " -->".into())),
9897 ..Default::default()
9898 },
9899 Some(tree_sitter_html::LANGUAGE.into()),
9900 )
9901 .with_injection_query(
9902 r#"
9903 (script_element
9904 (raw_text) @injection.content
9905 (#set! injection.language "javascript"))
9906 "#,
9907 )
9908 .unwrap(),
9909 );
9910
9911 let javascript_language = Arc::new(Language::new(
9912 LanguageConfig {
9913 name: "JavaScript".into(),
9914 line_comments: vec!["// ".into()],
9915 ..Default::default()
9916 },
9917 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9918 ));
9919
9920 cx.language_registry().add(html_language.clone());
9921 cx.language_registry().add(javascript_language.clone());
9922 cx.update_buffer(|buffer, cx| {
9923 buffer.set_language(Some(html_language), cx);
9924 });
9925
9926 // Toggle comments for empty selections
9927 cx.set_state(
9928 &r#"
9929 <p>A</p>ˇ
9930 <p>B</p>ˇ
9931 <p>C</p>ˇ
9932 "#
9933 .unindent(),
9934 );
9935 cx.update_editor(|editor, window, cx| {
9936 editor.toggle_comments(&ToggleComments::default(), window, cx)
9937 });
9938 cx.assert_editor_state(
9939 &r#"
9940 <!-- <p>A</p>ˇ -->
9941 <!-- <p>B</p>ˇ -->
9942 <!-- <p>C</p>ˇ -->
9943 "#
9944 .unindent(),
9945 );
9946 cx.update_editor(|editor, window, cx| {
9947 editor.toggle_comments(&ToggleComments::default(), window, cx)
9948 });
9949 cx.assert_editor_state(
9950 &r#"
9951 <p>A</p>ˇ
9952 <p>B</p>ˇ
9953 <p>C</p>ˇ
9954 "#
9955 .unindent(),
9956 );
9957
9958 // Toggle comments for mixture of empty and non-empty selections, where
9959 // multiple selections occupy a given line.
9960 cx.set_state(
9961 &r#"
9962 <p>A«</p>
9963 <p>ˇ»B</p>ˇ
9964 <p>C«</p>
9965 <p>ˇ»D</p>ˇ
9966 "#
9967 .unindent(),
9968 );
9969
9970 cx.update_editor(|editor, window, cx| {
9971 editor.toggle_comments(&ToggleComments::default(), window, cx)
9972 });
9973 cx.assert_editor_state(
9974 &r#"
9975 <!-- <p>A«</p>
9976 <p>ˇ»B</p>ˇ -->
9977 <!-- <p>C«</p>
9978 <p>ˇ»D</p>ˇ -->
9979 "#
9980 .unindent(),
9981 );
9982 cx.update_editor(|editor, window, cx| {
9983 editor.toggle_comments(&ToggleComments::default(), window, cx)
9984 });
9985 cx.assert_editor_state(
9986 &r#"
9987 <p>A«</p>
9988 <p>ˇ»B</p>ˇ
9989 <p>C«</p>
9990 <p>ˇ»D</p>ˇ
9991 "#
9992 .unindent(),
9993 );
9994
9995 // Toggle comments when different languages are active for different
9996 // selections.
9997 cx.set_state(
9998 &r#"
9999 ˇ<script>
10000 ˇvar x = new Y();
10001 ˇ</script>
10002 "#
10003 .unindent(),
10004 );
10005 cx.executor().run_until_parked();
10006 cx.update_editor(|editor, window, cx| {
10007 editor.toggle_comments(&ToggleComments::default(), window, cx)
10008 });
10009 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
10010 // Uncommenting and commenting from this position brings in even more wrong artifacts.
10011 cx.assert_editor_state(
10012 &r#"
10013 <!-- ˇ<script> -->
10014 // ˇvar x = new Y();
10015 <!-- ˇ</script> -->
10016 "#
10017 .unindent(),
10018 );
10019}
10020
10021#[gpui::test]
10022fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
10023 init_test(cx, |_| {});
10024
10025 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10026 let multibuffer = cx.new(|cx| {
10027 let mut multibuffer = MultiBuffer::new(ReadWrite);
10028 multibuffer.push_excerpts(
10029 buffer.clone(),
10030 [
10031 ExcerptRange {
10032 context: Point::new(0, 0)..Point::new(0, 4),
10033 primary: None,
10034 },
10035 ExcerptRange {
10036 context: Point::new(1, 0)..Point::new(1, 4),
10037 primary: None,
10038 },
10039 ],
10040 cx,
10041 );
10042 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
10043 multibuffer
10044 });
10045
10046 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
10047 editor.update_in(cx, |editor, window, cx| {
10048 assert_eq!(editor.text(cx), "aaaa\nbbbb");
10049 editor.change_selections(None, window, cx, |s| {
10050 s.select_ranges([
10051 Point::new(0, 0)..Point::new(0, 0),
10052 Point::new(1, 0)..Point::new(1, 0),
10053 ])
10054 });
10055
10056 editor.handle_input("X", window, cx);
10057 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
10058 assert_eq!(
10059 editor.selections.ranges(cx),
10060 [
10061 Point::new(0, 1)..Point::new(0, 1),
10062 Point::new(1, 1)..Point::new(1, 1),
10063 ]
10064 );
10065
10066 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
10067 editor.change_selections(None, window, cx, |s| {
10068 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
10069 });
10070 editor.backspace(&Default::default(), window, cx);
10071 assert_eq!(editor.text(cx), "Xa\nbbb");
10072 assert_eq!(
10073 editor.selections.ranges(cx),
10074 [Point::new(1, 0)..Point::new(1, 0)]
10075 );
10076
10077 editor.change_selections(None, window, cx, |s| {
10078 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
10079 });
10080 editor.backspace(&Default::default(), window, cx);
10081 assert_eq!(editor.text(cx), "X\nbb");
10082 assert_eq!(
10083 editor.selections.ranges(cx),
10084 [Point::new(0, 1)..Point::new(0, 1)]
10085 );
10086 });
10087}
10088
10089#[gpui::test]
10090fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
10091 init_test(cx, |_| {});
10092
10093 let markers = vec![('[', ']').into(), ('(', ')').into()];
10094 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
10095 indoc! {"
10096 [aaaa
10097 (bbbb]
10098 cccc)",
10099 },
10100 markers.clone(),
10101 );
10102 let excerpt_ranges = markers.into_iter().map(|marker| {
10103 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
10104 ExcerptRange {
10105 context,
10106 primary: None,
10107 }
10108 });
10109 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
10110 let multibuffer = cx.new(|cx| {
10111 let mut multibuffer = MultiBuffer::new(ReadWrite);
10112 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
10113 multibuffer
10114 });
10115
10116 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
10117 editor.update_in(cx, |editor, window, cx| {
10118 let (expected_text, selection_ranges) = marked_text_ranges(
10119 indoc! {"
10120 aaaa
10121 bˇbbb
10122 bˇbbˇb
10123 cccc"
10124 },
10125 true,
10126 );
10127 assert_eq!(editor.text(cx), expected_text);
10128 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
10129
10130 editor.handle_input("X", window, cx);
10131
10132 let (expected_text, expected_selections) = marked_text_ranges(
10133 indoc! {"
10134 aaaa
10135 bXˇbbXb
10136 bXˇbbXˇb
10137 cccc"
10138 },
10139 false,
10140 );
10141 assert_eq!(editor.text(cx), expected_text);
10142 assert_eq!(editor.selections.ranges(cx), expected_selections);
10143
10144 editor.newline(&Newline, window, cx);
10145 let (expected_text, expected_selections) = marked_text_ranges(
10146 indoc! {"
10147 aaaa
10148 bX
10149 ˇbbX
10150 b
10151 bX
10152 ˇbbX
10153 ˇb
10154 cccc"
10155 },
10156 false,
10157 );
10158 assert_eq!(editor.text(cx), expected_text);
10159 assert_eq!(editor.selections.ranges(cx), expected_selections);
10160 });
10161}
10162
10163#[gpui::test]
10164fn test_refresh_selections(cx: &mut TestAppContext) {
10165 init_test(cx, |_| {});
10166
10167 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10168 let mut excerpt1_id = None;
10169 let multibuffer = cx.new(|cx| {
10170 let mut multibuffer = MultiBuffer::new(ReadWrite);
10171 excerpt1_id = multibuffer
10172 .push_excerpts(
10173 buffer.clone(),
10174 [
10175 ExcerptRange {
10176 context: Point::new(0, 0)..Point::new(1, 4),
10177 primary: None,
10178 },
10179 ExcerptRange {
10180 context: Point::new(1, 0)..Point::new(2, 4),
10181 primary: None,
10182 },
10183 ],
10184 cx,
10185 )
10186 .into_iter()
10187 .next();
10188 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
10189 multibuffer
10190 });
10191
10192 let editor = cx.add_window(|window, cx| {
10193 let mut editor = build_editor(multibuffer.clone(), window, cx);
10194 let snapshot = editor.snapshot(window, cx);
10195 editor.change_selections(None, window, cx, |s| {
10196 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
10197 });
10198 editor.begin_selection(
10199 Point::new(2, 1).to_display_point(&snapshot),
10200 true,
10201 1,
10202 window,
10203 cx,
10204 );
10205 assert_eq!(
10206 editor.selections.ranges(cx),
10207 [
10208 Point::new(1, 3)..Point::new(1, 3),
10209 Point::new(2, 1)..Point::new(2, 1),
10210 ]
10211 );
10212 editor
10213 });
10214
10215 // Refreshing selections is a no-op when excerpts haven't changed.
10216 _ = editor.update(cx, |editor, window, cx| {
10217 editor.change_selections(None, window, cx, |s| s.refresh());
10218 assert_eq!(
10219 editor.selections.ranges(cx),
10220 [
10221 Point::new(1, 3)..Point::new(1, 3),
10222 Point::new(2, 1)..Point::new(2, 1),
10223 ]
10224 );
10225 });
10226
10227 multibuffer.update(cx, |multibuffer, cx| {
10228 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
10229 });
10230 _ = editor.update(cx, |editor, window, cx| {
10231 // Removing an excerpt causes the first selection to become degenerate.
10232 assert_eq!(
10233 editor.selections.ranges(cx),
10234 [
10235 Point::new(0, 0)..Point::new(0, 0),
10236 Point::new(0, 1)..Point::new(0, 1)
10237 ]
10238 );
10239
10240 // Refreshing selections will relocate the first selection to the original buffer
10241 // location.
10242 editor.change_selections(None, window, cx, |s| s.refresh());
10243 assert_eq!(
10244 editor.selections.ranges(cx),
10245 [
10246 Point::new(0, 1)..Point::new(0, 1),
10247 Point::new(0, 3)..Point::new(0, 3)
10248 ]
10249 );
10250 assert!(editor.selections.pending_anchor().is_some());
10251 });
10252}
10253
10254#[gpui::test]
10255fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
10256 init_test(cx, |_| {});
10257
10258 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10259 let mut excerpt1_id = None;
10260 let multibuffer = cx.new(|cx| {
10261 let mut multibuffer = MultiBuffer::new(ReadWrite);
10262 excerpt1_id = multibuffer
10263 .push_excerpts(
10264 buffer.clone(),
10265 [
10266 ExcerptRange {
10267 context: Point::new(0, 0)..Point::new(1, 4),
10268 primary: None,
10269 },
10270 ExcerptRange {
10271 context: Point::new(1, 0)..Point::new(2, 4),
10272 primary: None,
10273 },
10274 ],
10275 cx,
10276 )
10277 .into_iter()
10278 .next();
10279 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
10280 multibuffer
10281 });
10282
10283 let editor = cx.add_window(|window, cx| {
10284 let mut editor = build_editor(multibuffer.clone(), window, cx);
10285 let snapshot = editor.snapshot(window, cx);
10286 editor.begin_selection(
10287 Point::new(1, 3).to_display_point(&snapshot),
10288 false,
10289 1,
10290 window,
10291 cx,
10292 );
10293 assert_eq!(
10294 editor.selections.ranges(cx),
10295 [Point::new(1, 3)..Point::new(1, 3)]
10296 );
10297 editor
10298 });
10299
10300 multibuffer.update(cx, |multibuffer, cx| {
10301 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
10302 });
10303 _ = editor.update(cx, |editor, window, cx| {
10304 assert_eq!(
10305 editor.selections.ranges(cx),
10306 [Point::new(0, 0)..Point::new(0, 0)]
10307 );
10308
10309 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
10310 editor.change_selections(None, window, cx, |s| s.refresh());
10311 assert_eq!(
10312 editor.selections.ranges(cx),
10313 [Point::new(0, 3)..Point::new(0, 3)]
10314 );
10315 assert!(editor.selections.pending_anchor().is_some());
10316 });
10317}
10318
10319#[gpui::test]
10320async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
10321 init_test(cx, |_| {});
10322
10323 let language = Arc::new(
10324 Language::new(
10325 LanguageConfig {
10326 brackets: BracketPairConfig {
10327 pairs: vec![
10328 BracketPair {
10329 start: "{".to_string(),
10330 end: "}".to_string(),
10331 close: true,
10332 surround: true,
10333 newline: true,
10334 },
10335 BracketPair {
10336 start: "/* ".to_string(),
10337 end: " */".to_string(),
10338 close: true,
10339 surround: true,
10340 newline: true,
10341 },
10342 ],
10343 ..Default::default()
10344 },
10345 ..Default::default()
10346 },
10347 Some(tree_sitter_rust::LANGUAGE.into()),
10348 )
10349 .with_indents_query("")
10350 .unwrap(),
10351 );
10352
10353 let text = concat!(
10354 "{ }\n", //
10355 " x\n", //
10356 " /* */\n", //
10357 "x\n", //
10358 "{{} }\n", //
10359 );
10360
10361 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10362 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10363 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10364 editor
10365 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10366 .await;
10367
10368 editor.update_in(cx, |editor, window, cx| {
10369 editor.change_selections(None, window, cx, |s| {
10370 s.select_display_ranges([
10371 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
10372 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
10373 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
10374 ])
10375 });
10376 editor.newline(&Newline, window, cx);
10377
10378 assert_eq!(
10379 editor.buffer().read(cx).read(cx).text(),
10380 concat!(
10381 "{ \n", // Suppress rustfmt
10382 "\n", //
10383 "}\n", //
10384 " x\n", //
10385 " /* \n", //
10386 " \n", //
10387 " */\n", //
10388 "x\n", //
10389 "{{} \n", //
10390 "}\n", //
10391 )
10392 );
10393 });
10394}
10395
10396#[gpui::test]
10397fn test_highlighted_ranges(cx: &mut TestAppContext) {
10398 init_test(cx, |_| {});
10399
10400 let editor = cx.add_window(|window, cx| {
10401 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
10402 build_editor(buffer.clone(), window, cx)
10403 });
10404
10405 _ = editor.update(cx, |editor, window, cx| {
10406 struct Type1;
10407 struct Type2;
10408
10409 let buffer = editor.buffer.read(cx).snapshot(cx);
10410
10411 let anchor_range =
10412 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
10413
10414 editor.highlight_background::<Type1>(
10415 &[
10416 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
10417 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
10418 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
10419 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
10420 ],
10421 |_| Hsla::red(),
10422 cx,
10423 );
10424 editor.highlight_background::<Type2>(
10425 &[
10426 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
10427 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
10428 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
10429 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
10430 ],
10431 |_| Hsla::green(),
10432 cx,
10433 );
10434
10435 let snapshot = editor.snapshot(window, cx);
10436 let mut highlighted_ranges = editor.background_highlights_in_range(
10437 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
10438 &snapshot,
10439 cx.theme().colors(),
10440 );
10441 // Enforce a consistent ordering based on color without relying on the ordering of the
10442 // highlight's `TypeId` which is non-executor.
10443 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
10444 assert_eq!(
10445 highlighted_ranges,
10446 &[
10447 (
10448 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
10449 Hsla::red(),
10450 ),
10451 (
10452 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
10453 Hsla::red(),
10454 ),
10455 (
10456 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
10457 Hsla::green(),
10458 ),
10459 (
10460 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
10461 Hsla::green(),
10462 ),
10463 ]
10464 );
10465 assert_eq!(
10466 editor.background_highlights_in_range(
10467 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
10468 &snapshot,
10469 cx.theme().colors(),
10470 ),
10471 &[(
10472 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
10473 Hsla::red(),
10474 )]
10475 );
10476 });
10477}
10478
10479#[gpui::test]
10480async fn test_following(cx: &mut TestAppContext) {
10481 init_test(cx, |_| {});
10482
10483 let fs = FakeFs::new(cx.executor());
10484 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
10485
10486 let buffer = project.update(cx, |project, cx| {
10487 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
10488 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
10489 });
10490 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
10491 let follower = cx.update(|cx| {
10492 cx.open_window(
10493 WindowOptions {
10494 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
10495 gpui::Point::new(px(0.), px(0.)),
10496 gpui::Point::new(px(10.), px(80.)),
10497 ))),
10498 ..Default::default()
10499 },
10500 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
10501 )
10502 .unwrap()
10503 });
10504
10505 let is_still_following = Rc::new(RefCell::new(true));
10506 let follower_edit_event_count = Rc::new(RefCell::new(0));
10507 let pending_update = Rc::new(RefCell::new(None));
10508 let leader_entity = leader.root(cx).unwrap();
10509 let follower_entity = follower.root(cx).unwrap();
10510 _ = follower.update(cx, {
10511 let update = pending_update.clone();
10512 let is_still_following = is_still_following.clone();
10513 let follower_edit_event_count = follower_edit_event_count.clone();
10514 |_, window, cx| {
10515 cx.subscribe_in(
10516 &leader_entity,
10517 window,
10518 move |_, leader, event, window, cx| {
10519 leader.read(cx).add_event_to_update_proto(
10520 event,
10521 &mut update.borrow_mut(),
10522 window,
10523 cx,
10524 );
10525 },
10526 )
10527 .detach();
10528
10529 cx.subscribe_in(
10530 &follower_entity,
10531 window,
10532 move |_, _, event: &EditorEvent, _window, _cx| {
10533 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
10534 *is_still_following.borrow_mut() = false;
10535 }
10536
10537 if let EditorEvent::BufferEdited = event {
10538 *follower_edit_event_count.borrow_mut() += 1;
10539 }
10540 },
10541 )
10542 .detach();
10543 }
10544 });
10545
10546 // Update the selections only
10547 _ = leader.update(cx, |leader, window, cx| {
10548 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
10549 });
10550 follower
10551 .update(cx, |follower, window, cx| {
10552 follower.apply_update_proto(
10553 &project,
10554 pending_update.borrow_mut().take().unwrap(),
10555 window,
10556 cx,
10557 )
10558 })
10559 .unwrap()
10560 .await
10561 .unwrap();
10562 _ = follower.update(cx, |follower, _, cx| {
10563 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
10564 });
10565 assert!(*is_still_following.borrow());
10566 assert_eq!(*follower_edit_event_count.borrow(), 0);
10567
10568 // Update the scroll position only
10569 _ = leader.update(cx, |leader, window, cx| {
10570 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
10571 });
10572 follower
10573 .update(cx, |follower, window, cx| {
10574 follower.apply_update_proto(
10575 &project,
10576 pending_update.borrow_mut().take().unwrap(),
10577 window,
10578 cx,
10579 )
10580 })
10581 .unwrap()
10582 .await
10583 .unwrap();
10584 assert_eq!(
10585 follower
10586 .update(cx, |follower, _, cx| follower.scroll_position(cx))
10587 .unwrap(),
10588 gpui::Point::new(1.5, 3.5)
10589 );
10590 assert!(*is_still_following.borrow());
10591 assert_eq!(*follower_edit_event_count.borrow(), 0);
10592
10593 // Update the selections and scroll position. The follower's scroll position is updated
10594 // via autoscroll, not via the leader's exact scroll position.
10595 _ = leader.update(cx, |leader, window, cx| {
10596 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10597 leader.request_autoscroll(Autoscroll::newest(), cx);
10598 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
10599 });
10600 follower
10601 .update(cx, |follower, window, cx| {
10602 follower.apply_update_proto(
10603 &project,
10604 pending_update.borrow_mut().take().unwrap(),
10605 window,
10606 cx,
10607 )
10608 })
10609 .unwrap()
10610 .await
10611 .unwrap();
10612 _ = follower.update(cx, |follower, _, cx| {
10613 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
10614 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
10615 });
10616 assert!(*is_still_following.borrow());
10617
10618 // Creating a pending selection that precedes another selection
10619 _ = leader.update(cx, |leader, window, cx| {
10620 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
10621 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
10622 });
10623 follower
10624 .update(cx, |follower, window, cx| {
10625 follower.apply_update_proto(
10626 &project,
10627 pending_update.borrow_mut().take().unwrap(),
10628 window,
10629 cx,
10630 )
10631 })
10632 .unwrap()
10633 .await
10634 .unwrap();
10635 _ = follower.update(cx, |follower, _, cx| {
10636 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
10637 });
10638 assert!(*is_still_following.borrow());
10639
10640 // Extend the pending selection so that it surrounds another selection
10641 _ = leader.update(cx, |leader, window, cx| {
10642 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
10643 });
10644 follower
10645 .update(cx, |follower, window, cx| {
10646 follower.apply_update_proto(
10647 &project,
10648 pending_update.borrow_mut().take().unwrap(),
10649 window,
10650 cx,
10651 )
10652 })
10653 .unwrap()
10654 .await
10655 .unwrap();
10656 _ = follower.update(cx, |follower, _, cx| {
10657 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
10658 });
10659
10660 // Scrolling locally breaks the follow
10661 _ = follower.update(cx, |follower, window, cx| {
10662 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
10663 follower.set_scroll_anchor(
10664 ScrollAnchor {
10665 anchor: top_anchor,
10666 offset: gpui::Point::new(0.0, 0.5),
10667 },
10668 window,
10669 cx,
10670 );
10671 });
10672 assert!(!(*is_still_following.borrow()));
10673}
10674
10675#[gpui::test]
10676async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
10677 init_test(cx, |_| {});
10678
10679 let fs = FakeFs::new(cx.executor());
10680 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
10681 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10682 let pane = workspace
10683 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10684 .unwrap();
10685
10686 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10687
10688 let leader = pane.update_in(cx, |_, window, cx| {
10689 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
10690 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
10691 });
10692
10693 // Start following the editor when it has no excerpts.
10694 let mut state_message =
10695 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
10696 let workspace_entity = workspace.root(cx).unwrap();
10697 let follower_1 = cx
10698 .update_window(*workspace.deref(), |_, window, cx| {
10699 Editor::from_state_proto(
10700 workspace_entity,
10701 ViewId {
10702 creator: Default::default(),
10703 id: 0,
10704 },
10705 &mut state_message,
10706 window,
10707 cx,
10708 )
10709 })
10710 .unwrap()
10711 .unwrap()
10712 .await
10713 .unwrap();
10714
10715 let update_message = Rc::new(RefCell::new(None));
10716 follower_1.update_in(cx, {
10717 let update = update_message.clone();
10718 |_, window, cx| {
10719 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
10720 leader.read(cx).add_event_to_update_proto(
10721 event,
10722 &mut update.borrow_mut(),
10723 window,
10724 cx,
10725 );
10726 })
10727 .detach();
10728 }
10729 });
10730
10731 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
10732 (
10733 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
10734 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
10735 )
10736 });
10737
10738 // Insert some excerpts.
10739 leader.update(cx, |leader, cx| {
10740 leader.buffer.update(cx, |multibuffer, cx| {
10741 let excerpt_ids = multibuffer.push_excerpts(
10742 buffer_1.clone(),
10743 [
10744 ExcerptRange {
10745 context: 1..6,
10746 primary: None,
10747 },
10748 ExcerptRange {
10749 context: 12..15,
10750 primary: None,
10751 },
10752 ExcerptRange {
10753 context: 0..3,
10754 primary: None,
10755 },
10756 ],
10757 cx,
10758 );
10759 multibuffer.insert_excerpts_after(
10760 excerpt_ids[0],
10761 buffer_2.clone(),
10762 [
10763 ExcerptRange {
10764 context: 8..12,
10765 primary: None,
10766 },
10767 ExcerptRange {
10768 context: 0..6,
10769 primary: None,
10770 },
10771 ],
10772 cx,
10773 );
10774 });
10775 });
10776
10777 // Apply the update of adding the excerpts.
10778 follower_1
10779 .update_in(cx, |follower, window, cx| {
10780 follower.apply_update_proto(
10781 &project,
10782 update_message.borrow().clone().unwrap(),
10783 window,
10784 cx,
10785 )
10786 })
10787 .await
10788 .unwrap();
10789 assert_eq!(
10790 follower_1.update(cx, |editor, cx| editor.text(cx)),
10791 leader.update(cx, |editor, cx| editor.text(cx))
10792 );
10793 update_message.borrow_mut().take();
10794
10795 // Start following separately after it already has excerpts.
10796 let mut state_message =
10797 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
10798 let workspace_entity = workspace.root(cx).unwrap();
10799 let follower_2 = cx
10800 .update_window(*workspace.deref(), |_, window, cx| {
10801 Editor::from_state_proto(
10802 workspace_entity,
10803 ViewId {
10804 creator: Default::default(),
10805 id: 0,
10806 },
10807 &mut state_message,
10808 window,
10809 cx,
10810 )
10811 })
10812 .unwrap()
10813 .unwrap()
10814 .await
10815 .unwrap();
10816 assert_eq!(
10817 follower_2.update(cx, |editor, cx| editor.text(cx)),
10818 leader.update(cx, |editor, cx| editor.text(cx))
10819 );
10820
10821 // Remove some excerpts.
10822 leader.update(cx, |leader, cx| {
10823 leader.buffer.update(cx, |multibuffer, cx| {
10824 let excerpt_ids = multibuffer.excerpt_ids();
10825 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
10826 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
10827 });
10828 });
10829
10830 // Apply the update of removing the excerpts.
10831 follower_1
10832 .update_in(cx, |follower, window, cx| {
10833 follower.apply_update_proto(
10834 &project,
10835 update_message.borrow().clone().unwrap(),
10836 window,
10837 cx,
10838 )
10839 })
10840 .await
10841 .unwrap();
10842 follower_2
10843 .update_in(cx, |follower, window, cx| {
10844 follower.apply_update_proto(
10845 &project,
10846 update_message.borrow().clone().unwrap(),
10847 window,
10848 cx,
10849 )
10850 })
10851 .await
10852 .unwrap();
10853 update_message.borrow_mut().take();
10854 assert_eq!(
10855 follower_1.update(cx, |editor, cx| editor.text(cx)),
10856 leader.update(cx, |editor, cx| editor.text(cx))
10857 );
10858}
10859
10860#[gpui::test]
10861async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
10862 init_test(cx, |_| {});
10863
10864 let mut cx = EditorTestContext::new(cx).await;
10865 let lsp_store =
10866 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10867
10868 cx.set_state(indoc! {"
10869 ˇfn func(abc def: i32) -> u32 {
10870 }
10871 "});
10872
10873 cx.update(|_, cx| {
10874 lsp_store.update(cx, |lsp_store, cx| {
10875 lsp_store
10876 .update_diagnostics(
10877 LanguageServerId(0),
10878 lsp::PublishDiagnosticsParams {
10879 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
10880 version: None,
10881 diagnostics: vec![
10882 lsp::Diagnostic {
10883 range: lsp::Range::new(
10884 lsp::Position::new(0, 11),
10885 lsp::Position::new(0, 12),
10886 ),
10887 severity: Some(lsp::DiagnosticSeverity::ERROR),
10888 ..Default::default()
10889 },
10890 lsp::Diagnostic {
10891 range: lsp::Range::new(
10892 lsp::Position::new(0, 12),
10893 lsp::Position::new(0, 15),
10894 ),
10895 severity: Some(lsp::DiagnosticSeverity::ERROR),
10896 ..Default::default()
10897 },
10898 lsp::Diagnostic {
10899 range: lsp::Range::new(
10900 lsp::Position::new(0, 25),
10901 lsp::Position::new(0, 28),
10902 ),
10903 severity: Some(lsp::DiagnosticSeverity::ERROR),
10904 ..Default::default()
10905 },
10906 ],
10907 },
10908 &[],
10909 cx,
10910 )
10911 .unwrap()
10912 });
10913 });
10914
10915 executor.run_until_parked();
10916
10917 cx.update_editor(|editor, window, cx| {
10918 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10919 });
10920
10921 cx.assert_editor_state(indoc! {"
10922 fn func(abc def: i32) -> ˇu32 {
10923 }
10924 "});
10925
10926 cx.update_editor(|editor, window, cx| {
10927 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10928 });
10929
10930 cx.assert_editor_state(indoc! {"
10931 fn func(abc ˇdef: i32) -> u32 {
10932 }
10933 "});
10934
10935 cx.update_editor(|editor, window, cx| {
10936 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10937 });
10938
10939 cx.assert_editor_state(indoc! {"
10940 fn func(abcˇ def: i32) -> u32 {
10941 }
10942 "});
10943
10944 cx.update_editor(|editor, window, cx| {
10945 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10946 });
10947
10948 cx.assert_editor_state(indoc! {"
10949 fn func(abc def: i32) -> ˇu32 {
10950 }
10951 "});
10952}
10953
10954#[gpui::test]
10955async fn cycle_through_same_place_diagnostics(
10956 executor: BackgroundExecutor,
10957 cx: &mut TestAppContext,
10958) {
10959 init_test(cx, |_| {});
10960
10961 let mut cx = EditorTestContext::new(cx).await;
10962 let lsp_store =
10963 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10964
10965 cx.set_state(indoc! {"
10966 ˇfn func(abc def: i32) -> u32 {
10967 }
10968 "});
10969
10970 cx.update(|_, cx| {
10971 lsp_store.update(cx, |lsp_store, cx| {
10972 lsp_store
10973 .update_diagnostics(
10974 LanguageServerId(0),
10975 lsp::PublishDiagnosticsParams {
10976 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
10977 version: None,
10978 diagnostics: vec![
10979 lsp::Diagnostic {
10980 range: lsp::Range::new(
10981 lsp::Position::new(0, 11),
10982 lsp::Position::new(0, 12),
10983 ),
10984 severity: Some(lsp::DiagnosticSeverity::ERROR),
10985 ..Default::default()
10986 },
10987 lsp::Diagnostic {
10988 range: lsp::Range::new(
10989 lsp::Position::new(0, 12),
10990 lsp::Position::new(0, 15),
10991 ),
10992 severity: Some(lsp::DiagnosticSeverity::ERROR),
10993 ..Default::default()
10994 },
10995 lsp::Diagnostic {
10996 range: lsp::Range::new(
10997 lsp::Position::new(0, 12),
10998 lsp::Position::new(0, 15),
10999 ),
11000 severity: Some(lsp::DiagnosticSeverity::ERROR),
11001 ..Default::default()
11002 },
11003 lsp::Diagnostic {
11004 range: lsp::Range::new(
11005 lsp::Position::new(0, 25),
11006 lsp::Position::new(0, 28),
11007 ),
11008 severity: Some(lsp::DiagnosticSeverity::ERROR),
11009 ..Default::default()
11010 },
11011 ],
11012 },
11013 &[],
11014 cx,
11015 )
11016 .unwrap()
11017 });
11018 });
11019 executor.run_until_parked();
11020
11021 //// Backward
11022
11023 // Fourth diagnostic
11024 cx.update_editor(|editor, window, cx| {
11025 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
11026 });
11027 cx.assert_editor_state(indoc! {"
11028 fn func(abc def: i32) -> ˇu32 {
11029 }
11030 "});
11031
11032 // Third diagnostic
11033 cx.update_editor(|editor, window, cx| {
11034 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
11035 });
11036 cx.assert_editor_state(indoc! {"
11037 fn func(abc ˇdef: i32) -> u32 {
11038 }
11039 "});
11040
11041 // Second diagnostic, same place
11042 cx.update_editor(|editor, window, cx| {
11043 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
11044 });
11045 cx.assert_editor_state(indoc! {"
11046 fn func(abc ˇdef: i32) -> u32 {
11047 }
11048 "});
11049
11050 // First diagnostic
11051 cx.update_editor(|editor, window, cx| {
11052 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
11053 });
11054 cx.assert_editor_state(indoc! {"
11055 fn func(abcˇ def: i32) -> u32 {
11056 }
11057 "});
11058
11059 // Wrapped over, fourth diagnostic
11060 cx.update_editor(|editor, window, cx| {
11061 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
11062 });
11063 cx.assert_editor_state(indoc! {"
11064 fn func(abc def: i32) -> ˇu32 {
11065 }
11066 "});
11067
11068 cx.update_editor(|editor, window, cx| {
11069 editor.move_to_beginning(&MoveToBeginning, window, cx);
11070 });
11071 cx.assert_editor_state(indoc! {"
11072 ˇfn func(abc def: i32) -> u32 {
11073 }
11074 "});
11075
11076 //// Forward
11077
11078 // First diagnostic
11079 cx.update_editor(|editor, window, cx| {
11080 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11081 });
11082 cx.assert_editor_state(indoc! {"
11083 fn func(abcˇ def: i32) -> u32 {
11084 }
11085 "});
11086
11087 // Second diagnostic
11088 cx.update_editor(|editor, window, cx| {
11089 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11090 });
11091 cx.assert_editor_state(indoc! {"
11092 fn func(abc ˇdef: i32) -> u32 {
11093 }
11094 "});
11095
11096 // Third diagnostic, same place
11097 cx.update_editor(|editor, window, cx| {
11098 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11099 });
11100 cx.assert_editor_state(indoc! {"
11101 fn func(abc ˇdef: i32) -> u32 {
11102 }
11103 "});
11104
11105 // Fourth diagnostic
11106 cx.update_editor(|editor, window, cx| {
11107 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11108 });
11109 cx.assert_editor_state(indoc! {"
11110 fn func(abc def: i32) -> ˇu32 {
11111 }
11112 "});
11113
11114 // Wrapped around, first diagnostic
11115 cx.update_editor(|editor, window, cx| {
11116 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11117 });
11118 cx.assert_editor_state(indoc! {"
11119 fn func(abcˇ def: i32) -> u32 {
11120 }
11121 "});
11122}
11123
11124#[gpui::test]
11125async fn active_diagnostics_dismiss_after_invalidation(
11126 executor: BackgroundExecutor,
11127 cx: &mut TestAppContext,
11128) {
11129 init_test(cx, |_| {});
11130
11131 let mut cx = EditorTestContext::new(cx).await;
11132 let lsp_store =
11133 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11134
11135 cx.set_state(indoc! {"
11136 ˇfn func(abc def: i32) -> u32 {
11137 }
11138 "});
11139
11140 let message = "Something's wrong!";
11141 cx.update(|_, cx| {
11142 lsp_store.update(cx, |lsp_store, cx| {
11143 lsp_store
11144 .update_diagnostics(
11145 LanguageServerId(0),
11146 lsp::PublishDiagnosticsParams {
11147 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11148 version: None,
11149 diagnostics: vec![lsp::Diagnostic {
11150 range: lsp::Range::new(
11151 lsp::Position::new(0, 11),
11152 lsp::Position::new(0, 12),
11153 ),
11154 severity: Some(lsp::DiagnosticSeverity::ERROR),
11155 message: message.to_string(),
11156 ..Default::default()
11157 }],
11158 },
11159 &[],
11160 cx,
11161 )
11162 .unwrap()
11163 });
11164 });
11165 executor.run_until_parked();
11166
11167 cx.update_editor(|editor, window, cx| {
11168 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11169 assert_eq!(
11170 editor
11171 .active_diagnostics
11172 .as_ref()
11173 .map(|diagnostics_group| diagnostics_group.primary_message.as_str()),
11174 Some(message),
11175 "Should have a diagnostics group activated"
11176 );
11177 });
11178 cx.assert_editor_state(indoc! {"
11179 fn func(abcˇ def: i32) -> u32 {
11180 }
11181 "});
11182
11183 cx.update(|_, cx| {
11184 lsp_store.update(cx, |lsp_store, cx| {
11185 lsp_store
11186 .update_diagnostics(
11187 LanguageServerId(0),
11188 lsp::PublishDiagnosticsParams {
11189 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11190 version: None,
11191 diagnostics: Vec::new(),
11192 },
11193 &[],
11194 cx,
11195 )
11196 .unwrap()
11197 });
11198 });
11199 executor.run_until_parked();
11200 cx.update_editor(|editor, _, _| {
11201 assert_eq!(
11202 editor.active_diagnostics, None,
11203 "After no diagnostics set to the editor, no diagnostics should be active"
11204 );
11205 });
11206 cx.assert_editor_state(indoc! {"
11207 fn func(abcˇ def: i32) -> u32 {
11208 }
11209 "});
11210
11211 cx.update_editor(|editor, window, cx| {
11212 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11213 assert_eq!(
11214 editor.active_diagnostics, None,
11215 "Should be no diagnostics to go to and activate"
11216 );
11217 });
11218 cx.assert_editor_state(indoc! {"
11219 fn func(abcˇ def: i32) -> u32 {
11220 }
11221 "});
11222}
11223
11224#[gpui::test]
11225async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
11226 init_test(cx, |_| {});
11227
11228 let mut cx = EditorTestContext::new(cx).await;
11229
11230 cx.set_state(indoc! {"
11231 fn func(abˇc def: i32) -> u32 {
11232 }
11233 "});
11234 let lsp_store =
11235 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11236
11237 cx.update(|_, cx| {
11238 lsp_store.update(cx, |lsp_store, cx| {
11239 lsp_store.update_diagnostics(
11240 LanguageServerId(0),
11241 lsp::PublishDiagnosticsParams {
11242 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11243 version: None,
11244 diagnostics: vec![lsp::Diagnostic {
11245 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
11246 severity: Some(lsp::DiagnosticSeverity::ERROR),
11247 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
11248 ..Default::default()
11249 }],
11250 },
11251 &[],
11252 cx,
11253 )
11254 })
11255 }).unwrap();
11256 cx.run_until_parked();
11257 cx.update_editor(|editor, window, cx| {
11258 hover_popover::hover(editor, &Default::default(), window, cx)
11259 });
11260 cx.run_until_parked();
11261 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
11262}
11263
11264#[gpui::test]
11265async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
11266 init_test(cx, |_| {});
11267
11268 let mut cx = EditorTestContext::new(cx).await;
11269
11270 let diff_base = r#"
11271 use some::mod;
11272
11273 const A: u32 = 42;
11274
11275 fn main() {
11276 println!("hello");
11277
11278 println!("world");
11279 }
11280 "#
11281 .unindent();
11282
11283 // Edits are modified, removed, modified, added
11284 cx.set_state(
11285 &r#"
11286 use some::modified;
11287
11288 ˇ
11289 fn main() {
11290 println!("hello there");
11291
11292 println!("around the");
11293 println!("world");
11294 }
11295 "#
11296 .unindent(),
11297 );
11298
11299 cx.set_head_text(&diff_base);
11300 executor.run_until_parked();
11301
11302 cx.update_editor(|editor, window, cx| {
11303 //Wrap around the bottom of the buffer
11304 for _ in 0..3 {
11305 editor.go_to_next_hunk(&GoToHunk, window, cx);
11306 }
11307 });
11308
11309 cx.assert_editor_state(
11310 &r#"
11311 ˇuse some::modified;
11312
11313
11314 fn main() {
11315 println!("hello there");
11316
11317 println!("around the");
11318 println!("world");
11319 }
11320 "#
11321 .unindent(),
11322 );
11323
11324 cx.update_editor(|editor, window, cx| {
11325 //Wrap around the top of the buffer
11326 for _ in 0..2 {
11327 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
11328 }
11329 });
11330
11331 cx.assert_editor_state(
11332 &r#"
11333 use some::modified;
11334
11335
11336 fn main() {
11337 ˇ println!("hello there");
11338
11339 println!("around the");
11340 println!("world");
11341 }
11342 "#
11343 .unindent(),
11344 );
11345
11346 cx.update_editor(|editor, window, cx| {
11347 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
11348 });
11349
11350 cx.assert_editor_state(
11351 &r#"
11352 use some::modified;
11353
11354 ˇ
11355 fn main() {
11356 println!("hello there");
11357
11358 println!("around the");
11359 println!("world");
11360 }
11361 "#
11362 .unindent(),
11363 );
11364
11365 cx.update_editor(|editor, window, cx| {
11366 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
11367 });
11368
11369 cx.assert_editor_state(
11370 &r#"
11371 ˇuse some::modified;
11372
11373
11374 fn main() {
11375 println!("hello there");
11376
11377 println!("around the");
11378 println!("world");
11379 }
11380 "#
11381 .unindent(),
11382 );
11383
11384 cx.update_editor(|editor, window, cx| {
11385 for _ in 0..2 {
11386 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
11387 }
11388 });
11389
11390 cx.assert_editor_state(
11391 &r#"
11392 use some::modified;
11393
11394
11395 fn main() {
11396 ˇ println!("hello there");
11397
11398 println!("around the");
11399 println!("world");
11400 }
11401 "#
11402 .unindent(),
11403 );
11404
11405 cx.update_editor(|editor, window, cx| {
11406 editor.fold(&Fold, window, cx);
11407 });
11408
11409 cx.update_editor(|editor, window, cx| {
11410 editor.go_to_next_hunk(&GoToHunk, window, cx);
11411 });
11412
11413 cx.assert_editor_state(
11414 &r#"
11415 ˇuse some::modified;
11416
11417
11418 fn main() {
11419 println!("hello there");
11420
11421 println!("around the");
11422 println!("world");
11423 }
11424 "#
11425 .unindent(),
11426 );
11427}
11428
11429#[test]
11430fn test_split_words() {
11431 fn split(text: &str) -> Vec<&str> {
11432 split_words(text).collect()
11433 }
11434
11435 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
11436 assert_eq!(split("hello_world"), &["hello_", "world"]);
11437 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
11438 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
11439 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
11440 assert_eq!(split("helloworld"), &["helloworld"]);
11441
11442 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
11443}
11444
11445#[gpui::test]
11446async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
11447 init_test(cx, |_| {});
11448
11449 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
11450 let mut assert = |before, after| {
11451 let _state_context = cx.set_state(before);
11452 cx.run_until_parked();
11453 cx.update_editor(|editor, window, cx| {
11454 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
11455 });
11456 cx.assert_editor_state(after);
11457 };
11458
11459 // Outside bracket jumps to outside of matching bracket
11460 assert("console.logˇ(var);", "console.log(var)ˇ;");
11461 assert("console.log(var)ˇ;", "console.logˇ(var);");
11462
11463 // Inside bracket jumps to inside of matching bracket
11464 assert("console.log(ˇvar);", "console.log(varˇ);");
11465 assert("console.log(varˇ);", "console.log(ˇvar);");
11466
11467 // When outside a bracket and inside, favor jumping to the inside bracket
11468 assert(
11469 "console.log('foo', [1, 2, 3]ˇ);",
11470 "console.log(ˇ'foo', [1, 2, 3]);",
11471 );
11472 assert(
11473 "console.log(ˇ'foo', [1, 2, 3]);",
11474 "console.log('foo', [1, 2, 3]ˇ);",
11475 );
11476
11477 // Bias forward if two options are equally likely
11478 assert(
11479 "let result = curried_fun()ˇ();",
11480 "let result = curried_fun()()ˇ;",
11481 );
11482
11483 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
11484 assert(
11485 indoc! {"
11486 function test() {
11487 console.log('test')ˇ
11488 }"},
11489 indoc! {"
11490 function test() {
11491 console.logˇ('test')
11492 }"},
11493 );
11494}
11495
11496#[gpui::test]
11497async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
11498 init_test(cx, |_| {});
11499
11500 let fs = FakeFs::new(cx.executor());
11501 fs.insert_tree(
11502 path!("/a"),
11503 json!({
11504 "main.rs": "fn main() { let a = 5; }",
11505 "other.rs": "// Test file",
11506 }),
11507 )
11508 .await;
11509 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11510
11511 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11512 language_registry.add(Arc::new(Language::new(
11513 LanguageConfig {
11514 name: "Rust".into(),
11515 matcher: LanguageMatcher {
11516 path_suffixes: vec!["rs".to_string()],
11517 ..Default::default()
11518 },
11519 brackets: BracketPairConfig {
11520 pairs: vec![BracketPair {
11521 start: "{".to_string(),
11522 end: "}".to_string(),
11523 close: true,
11524 surround: true,
11525 newline: true,
11526 }],
11527 disabled_scopes_by_bracket_ix: Vec::new(),
11528 },
11529 ..Default::default()
11530 },
11531 Some(tree_sitter_rust::LANGUAGE.into()),
11532 )));
11533 let mut fake_servers = language_registry.register_fake_lsp(
11534 "Rust",
11535 FakeLspAdapter {
11536 capabilities: lsp::ServerCapabilities {
11537 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
11538 first_trigger_character: "{".to_string(),
11539 more_trigger_character: None,
11540 }),
11541 ..Default::default()
11542 },
11543 ..Default::default()
11544 },
11545 );
11546
11547 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11548
11549 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11550
11551 let worktree_id = workspace
11552 .update(cx, |workspace, _, cx| {
11553 workspace.project().update(cx, |project, cx| {
11554 project.worktrees(cx).next().unwrap().read(cx).id()
11555 })
11556 })
11557 .unwrap();
11558
11559 let buffer = project
11560 .update(cx, |project, cx| {
11561 project.open_local_buffer(path!("/a/main.rs"), cx)
11562 })
11563 .await
11564 .unwrap();
11565 let editor_handle = workspace
11566 .update(cx, |workspace, window, cx| {
11567 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
11568 })
11569 .unwrap()
11570 .await
11571 .unwrap()
11572 .downcast::<Editor>()
11573 .unwrap();
11574
11575 cx.executor().start_waiting();
11576 let fake_server = fake_servers.next().await.unwrap();
11577
11578 fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
11579 assert_eq!(
11580 params.text_document_position.text_document.uri,
11581 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
11582 );
11583 assert_eq!(
11584 params.text_document_position.position,
11585 lsp::Position::new(0, 21),
11586 );
11587
11588 Ok(Some(vec![lsp::TextEdit {
11589 new_text: "]".to_string(),
11590 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11591 }]))
11592 });
11593
11594 editor_handle.update_in(cx, |editor, window, cx| {
11595 window.focus(&editor.focus_handle(cx));
11596 editor.change_selections(None, window, cx, |s| {
11597 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
11598 });
11599 editor.handle_input("{", window, cx);
11600 });
11601
11602 cx.executor().run_until_parked();
11603
11604 buffer.update(cx, |buffer, _| {
11605 assert_eq!(
11606 buffer.text(),
11607 "fn main() { let a = {5}; }",
11608 "No extra braces from on type formatting should appear in the buffer"
11609 )
11610 });
11611}
11612
11613#[gpui::test]
11614async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
11615 init_test(cx, |_| {});
11616
11617 let fs = FakeFs::new(cx.executor());
11618 fs.insert_tree(
11619 path!("/a"),
11620 json!({
11621 "main.rs": "fn main() { let a = 5; }",
11622 "other.rs": "// Test file",
11623 }),
11624 )
11625 .await;
11626
11627 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11628
11629 let server_restarts = Arc::new(AtomicUsize::new(0));
11630 let closure_restarts = Arc::clone(&server_restarts);
11631 let language_server_name = "test language server";
11632 let language_name: LanguageName = "Rust".into();
11633
11634 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11635 language_registry.add(Arc::new(Language::new(
11636 LanguageConfig {
11637 name: language_name.clone(),
11638 matcher: LanguageMatcher {
11639 path_suffixes: vec!["rs".to_string()],
11640 ..Default::default()
11641 },
11642 ..Default::default()
11643 },
11644 Some(tree_sitter_rust::LANGUAGE.into()),
11645 )));
11646 let mut fake_servers = language_registry.register_fake_lsp(
11647 "Rust",
11648 FakeLspAdapter {
11649 name: language_server_name,
11650 initialization_options: Some(json!({
11651 "testOptionValue": true
11652 })),
11653 initializer: Some(Box::new(move |fake_server| {
11654 let task_restarts = Arc::clone(&closure_restarts);
11655 fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
11656 task_restarts.fetch_add(1, atomic::Ordering::Release);
11657 futures::future::ready(Ok(()))
11658 });
11659 })),
11660 ..Default::default()
11661 },
11662 );
11663
11664 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11665 let _buffer = project
11666 .update(cx, |project, cx| {
11667 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
11668 })
11669 .await
11670 .unwrap();
11671 let _fake_server = fake_servers.next().await.unwrap();
11672 update_test_language_settings(cx, |language_settings| {
11673 language_settings.languages.insert(
11674 language_name.clone(),
11675 LanguageSettingsContent {
11676 tab_size: NonZeroU32::new(8),
11677 ..Default::default()
11678 },
11679 );
11680 });
11681 cx.executor().run_until_parked();
11682 assert_eq!(
11683 server_restarts.load(atomic::Ordering::Acquire),
11684 0,
11685 "Should not restart LSP server on an unrelated change"
11686 );
11687
11688 update_test_project_settings(cx, |project_settings| {
11689 project_settings.lsp.insert(
11690 "Some other server name".into(),
11691 LspSettings {
11692 binary: None,
11693 settings: None,
11694 initialization_options: Some(json!({
11695 "some other init value": false
11696 })),
11697 },
11698 );
11699 });
11700 cx.executor().run_until_parked();
11701 assert_eq!(
11702 server_restarts.load(atomic::Ordering::Acquire),
11703 0,
11704 "Should not restart LSP server on an unrelated LSP settings change"
11705 );
11706
11707 update_test_project_settings(cx, |project_settings| {
11708 project_settings.lsp.insert(
11709 language_server_name.into(),
11710 LspSettings {
11711 binary: None,
11712 settings: None,
11713 initialization_options: Some(json!({
11714 "anotherInitValue": false
11715 })),
11716 },
11717 );
11718 });
11719 cx.executor().run_until_parked();
11720 assert_eq!(
11721 server_restarts.load(atomic::Ordering::Acquire),
11722 1,
11723 "Should restart LSP server on a related LSP settings change"
11724 );
11725
11726 update_test_project_settings(cx, |project_settings| {
11727 project_settings.lsp.insert(
11728 language_server_name.into(),
11729 LspSettings {
11730 binary: None,
11731 settings: None,
11732 initialization_options: Some(json!({
11733 "anotherInitValue": false
11734 })),
11735 },
11736 );
11737 });
11738 cx.executor().run_until_parked();
11739 assert_eq!(
11740 server_restarts.load(atomic::Ordering::Acquire),
11741 1,
11742 "Should not restart LSP server on a related LSP settings change that is the same"
11743 );
11744
11745 update_test_project_settings(cx, |project_settings| {
11746 project_settings.lsp.insert(
11747 language_server_name.into(),
11748 LspSettings {
11749 binary: None,
11750 settings: None,
11751 initialization_options: None,
11752 },
11753 );
11754 });
11755 cx.executor().run_until_parked();
11756 assert_eq!(
11757 server_restarts.load(atomic::Ordering::Acquire),
11758 2,
11759 "Should restart LSP server on another related LSP settings change"
11760 );
11761}
11762
11763#[gpui::test]
11764async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
11765 init_test(cx, |_| {});
11766
11767 let mut cx = EditorLspTestContext::new_rust(
11768 lsp::ServerCapabilities {
11769 completion_provider: Some(lsp::CompletionOptions {
11770 trigger_characters: Some(vec![".".to_string()]),
11771 resolve_provider: Some(true),
11772 ..Default::default()
11773 }),
11774 ..Default::default()
11775 },
11776 cx,
11777 )
11778 .await;
11779
11780 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11781 cx.simulate_keystroke(".");
11782 let completion_item = lsp::CompletionItem {
11783 label: "some".into(),
11784 kind: Some(lsp::CompletionItemKind::SNIPPET),
11785 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11786 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11787 kind: lsp::MarkupKind::Markdown,
11788 value: "```rust\nSome(2)\n```".to_string(),
11789 })),
11790 deprecated: Some(false),
11791 sort_text: Some("fffffff2".to_string()),
11792 filter_text: Some("some".to_string()),
11793 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11794 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11795 range: lsp::Range {
11796 start: lsp::Position {
11797 line: 0,
11798 character: 22,
11799 },
11800 end: lsp::Position {
11801 line: 0,
11802 character: 22,
11803 },
11804 },
11805 new_text: "Some(2)".to_string(),
11806 })),
11807 additional_text_edits: Some(vec![lsp::TextEdit {
11808 range: lsp::Range {
11809 start: lsp::Position {
11810 line: 0,
11811 character: 20,
11812 },
11813 end: lsp::Position {
11814 line: 0,
11815 character: 22,
11816 },
11817 },
11818 new_text: "".to_string(),
11819 }]),
11820 ..Default::default()
11821 };
11822
11823 let closure_completion_item = completion_item.clone();
11824 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11825 let task_completion_item = closure_completion_item.clone();
11826 async move {
11827 Ok(Some(lsp::CompletionResponse::Array(vec![
11828 task_completion_item,
11829 ])))
11830 }
11831 });
11832
11833 request.next().await;
11834
11835 cx.condition(|editor, _| editor.context_menu_visible())
11836 .await;
11837 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11838 editor
11839 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11840 .unwrap()
11841 });
11842 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
11843
11844 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
11845 let task_completion_item = completion_item.clone();
11846 async move { Ok(task_completion_item) }
11847 })
11848 .next()
11849 .await
11850 .unwrap();
11851 apply_additional_edits.await.unwrap();
11852 cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
11853}
11854
11855#[gpui::test]
11856async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
11857 init_test(cx, |_| {});
11858
11859 let mut cx = EditorLspTestContext::new_rust(
11860 lsp::ServerCapabilities {
11861 completion_provider: Some(lsp::CompletionOptions {
11862 trigger_characters: Some(vec![".".to_string()]),
11863 resolve_provider: Some(true),
11864 ..Default::default()
11865 }),
11866 ..Default::default()
11867 },
11868 cx,
11869 )
11870 .await;
11871
11872 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11873 cx.simulate_keystroke(".");
11874
11875 let item1 = lsp::CompletionItem {
11876 label: "method id()".to_string(),
11877 filter_text: Some("id".to_string()),
11878 detail: None,
11879 documentation: None,
11880 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11881 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11882 new_text: ".id".to_string(),
11883 })),
11884 ..lsp::CompletionItem::default()
11885 };
11886
11887 let item2 = lsp::CompletionItem {
11888 label: "other".to_string(),
11889 filter_text: Some("other".to_string()),
11890 detail: None,
11891 documentation: None,
11892 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11893 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11894 new_text: ".other".to_string(),
11895 })),
11896 ..lsp::CompletionItem::default()
11897 };
11898
11899 let item1 = item1.clone();
11900 cx.handle_request::<lsp::request::Completion, _, _>({
11901 let item1 = item1.clone();
11902 move |_, _, _| {
11903 let item1 = item1.clone();
11904 let item2 = item2.clone();
11905 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
11906 }
11907 })
11908 .next()
11909 .await;
11910
11911 cx.condition(|editor, _| editor.context_menu_visible())
11912 .await;
11913 cx.update_editor(|editor, _, _| {
11914 let context_menu = editor.context_menu.borrow_mut();
11915 let context_menu = context_menu
11916 .as_ref()
11917 .expect("Should have the context menu deployed");
11918 match context_menu {
11919 CodeContextMenu::Completions(completions_menu) => {
11920 let completions = completions_menu.completions.borrow_mut();
11921 assert_eq!(
11922 completions
11923 .iter()
11924 .map(|completion| &completion.label.text)
11925 .collect::<Vec<_>>(),
11926 vec!["method id()", "other"]
11927 )
11928 }
11929 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11930 }
11931 });
11932
11933 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>({
11934 let item1 = item1.clone();
11935 move |_, item_to_resolve, _| {
11936 let item1 = item1.clone();
11937 async move {
11938 if item1 == item_to_resolve {
11939 Ok(lsp::CompletionItem {
11940 label: "method id()".to_string(),
11941 filter_text: Some("id".to_string()),
11942 detail: Some("Now resolved!".to_string()),
11943 documentation: Some(lsp::Documentation::String("Docs".to_string())),
11944 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11945 range: lsp::Range::new(
11946 lsp::Position::new(0, 22),
11947 lsp::Position::new(0, 22),
11948 ),
11949 new_text: ".id".to_string(),
11950 })),
11951 ..lsp::CompletionItem::default()
11952 })
11953 } else {
11954 Ok(item_to_resolve)
11955 }
11956 }
11957 }
11958 })
11959 .next()
11960 .await
11961 .unwrap();
11962 cx.run_until_parked();
11963
11964 cx.update_editor(|editor, window, cx| {
11965 editor.context_menu_next(&Default::default(), window, cx);
11966 });
11967
11968 cx.update_editor(|editor, _, _| {
11969 let context_menu = editor.context_menu.borrow_mut();
11970 let context_menu = context_menu
11971 .as_ref()
11972 .expect("Should have the context menu deployed");
11973 match context_menu {
11974 CodeContextMenu::Completions(completions_menu) => {
11975 let completions = completions_menu.completions.borrow_mut();
11976 assert_eq!(
11977 completions
11978 .iter()
11979 .map(|completion| &completion.label.text)
11980 .collect::<Vec<_>>(),
11981 vec!["method id() Now resolved!", "other"],
11982 "Should update first completion label, but not second as the filter text did not match."
11983 );
11984 }
11985 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11986 }
11987 });
11988}
11989
11990#[gpui::test]
11991async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
11992 init_test(cx, |_| {});
11993
11994 let mut cx = EditorLspTestContext::new_rust(
11995 lsp::ServerCapabilities {
11996 completion_provider: Some(lsp::CompletionOptions {
11997 trigger_characters: Some(vec![".".to_string()]),
11998 resolve_provider: Some(true),
11999 ..Default::default()
12000 }),
12001 ..Default::default()
12002 },
12003 cx,
12004 )
12005 .await;
12006
12007 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
12008 cx.simulate_keystroke(".");
12009
12010 let unresolved_item_1 = lsp::CompletionItem {
12011 label: "id".to_string(),
12012 filter_text: Some("id".to_string()),
12013 detail: None,
12014 documentation: None,
12015 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12016 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12017 new_text: ".id".to_string(),
12018 })),
12019 ..lsp::CompletionItem::default()
12020 };
12021 let resolved_item_1 = lsp::CompletionItem {
12022 additional_text_edits: Some(vec![lsp::TextEdit {
12023 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
12024 new_text: "!!".to_string(),
12025 }]),
12026 ..unresolved_item_1.clone()
12027 };
12028 let unresolved_item_2 = lsp::CompletionItem {
12029 label: "other".to_string(),
12030 filter_text: Some("other".to_string()),
12031 detail: None,
12032 documentation: None,
12033 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12034 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12035 new_text: ".other".to_string(),
12036 })),
12037 ..lsp::CompletionItem::default()
12038 };
12039 let resolved_item_2 = lsp::CompletionItem {
12040 additional_text_edits: Some(vec![lsp::TextEdit {
12041 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
12042 new_text: "??".to_string(),
12043 }]),
12044 ..unresolved_item_2.clone()
12045 };
12046
12047 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
12048 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
12049 cx.lsp
12050 .server
12051 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
12052 let unresolved_item_1 = unresolved_item_1.clone();
12053 let resolved_item_1 = resolved_item_1.clone();
12054 let unresolved_item_2 = unresolved_item_2.clone();
12055 let resolved_item_2 = resolved_item_2.clone();
12056 let resolve_requests_1 = resolve_requests_1.clone();
12057 let resolve_requests_2 = resolve_requests_2.clone();
12058 move |unresolved_request, _| {
12059 let unresolved_item_1 = unresolved_item_1.clone();
12060 let resolved_item_1 = resolved_item_1.clone();
12061 let unresolved_item_2 = unresolved_item_2.clone();
12062 let resolved_item_2 = resolved_item_2.clone();
12063 let resolve_requests_1 = resolve_requests_1.clone();
12064 let resolve_requests_2 = resolve_requests_2.clone();
12065 async move {
12066 if unresolved_request == unresolved_item_1 {
12067 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
12068 Ok(resolved_item_1.clone())
12069 } else if unresolved_request == unresolved_item_2 {
12070 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
12071 Ok(resolved_item_2.clone())
12072 } else {
12073 panic!("Unexpected completion item {unresolved_request:?}")
12074 }
12075 }
12076 }
12077 })
12078 .detach();
12079
12080 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
12081 let unresolved_item_1 = unresolved_item_1.clone();
12082 let unresolved_item_2 = unresolved_item_2.clone();
12083 async move {
12084 Ok(Some(lsp::CompletionResponse::Array(vec![
12085 unresolved_item_1,
12086 unresolved_item_2,
12087 ])))
12088 }
12089 })
12090 .next()
12091 .await;
12092
12093 cx.condition(|editor, _| editor.context_menu_visible())
12094 .await;
12095 cx.update_editor(|editor, _, _| {
12096 let context_menu = editor.context_menu.borrow_mut();
12097 let context_menu = context_menu
12098 .as_ref()
12099 .expect("Should have the context menu deployed");
12100 match context_menu {
12101 CodeContextMenu::Completions(completions_menu) => {
12102 let completions = completions_menu.completions.borrow_mut();
12103 assert_eq!(
12104 completions
12105 .iter()
12106 .map(|completion| &completion.label.text)
12107 .collect::<Vec<_>>(),
12108 vec!["id", "other"]
12109 )
12110 }
12111 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12112 }
12113 });
12114 cx.run_until_parked();
12115
12116 cx.update_editor(|editor, window, cx| {
12117 editor.context_menu_next(&ContextMenuNext, window, cx);
12118 });
12119 cx.run_until_parked();
12120 cx.update_editor(|editor, window, cx| {
12121 editor.context_menu_prev(&ContextMenuPrev, window, cx);
12122 });
12123 cx.run_until_parked();
12124 cx.update_editor(|editor, window, cx| {
12125 editor.context_menu_next(&ContextMenuNext, window, cx);
12126 });
12127 cx.run_until_parked();
12128 cx.update_editor(|editor, window, cx| {
12129 editor
12130 .compose_completion(&ComposeCompletion::default(), window, cx)
12131 .expect("No task returned")
12132 })
12133 .await
12134 .expect("Completion failed");
12135 cx.run_until_parked();
12136
12137 cx.update_editor(|editor, _, cx| {
12138 assert_eq!(
12139 resolve_requests_1.load(atomic::Ordering::Acquire),
12140 1,
12141 "Should always resolve once despite multiple selections"
12142 );
12143 assert_eq!(
12144 resolve_requests_2.load(atomic::Ordering::Acquire),
12145 1,
12146 "Should always resolve once after multiple selections and applying the completion"
12147 );
12148 assert_eq!(
12149 editor.text(cx),
12150 "fn main() { let a = ??.other; }",
12151 "Should use resolved data when applying the completion"
12152 );
12153 });
12154}
12155
12156#[gpui::test]
12157async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
12158 init_test(cx, |_| {});
12159
12160 let item_0 = lsp::CompletionItem {
12161 label: "abs".into(),
12162 insert_text: Some("abs".into()),
12163 data: Some(json!({ "very": "special"})),
12164 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
12165 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12166 lsp::InsertReplaceEdit {
12167 new_text: "abs".to_string(),
12168 insert: lsp::Range::default(),
12169 replace: lsp::Range::default(),
12170 },
12171 )),
12172 ..lsp::CompletionItem::default()
12173 };
12174 let items = iter::once(item_0.clone())
12175 .chain((11..51).map(|i| lsp::CompletionItem {
12176 label: format!("item_{}", i),
12177 insert_text: Some(format!("item_{}", i)),
12178 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
12179 ..lsp::CompletionItem::default()
12180 }))
12181 .collect::<Vec<_>>();
12182
12183 let default_commit_characters = vec!["?".to_string()];
12184 let default_data = json!({ "default": "data"});
12185 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
12186 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
12187 let default_edit_range = lsp::Range {
12188 start: lsp::Position {
12189 line: 0,
12190 character: 5,
12191 },
12192 end: lsp::Position {
12193 line: 0,
12194 character: 5,
12195 },
12196 };
12197
12198 let item_0_out = lsp::CompletionItem {
12199 commit_characters: Some(default_commit_characters.clone()),
12200 insert_text_format: Some(default_insert_text_format),
12201 ..item_0
12202 };
12203 let items_out = iter::once(item_0_out)
12204 .chain(items[1..].iter().map(|item| lsp::CompletionItem {
12205 commit_characters: Some(default_commit_characters.clone()),
12206 data: Some(default_data.clone()),
12207 insert_text_mode: Some(default_insert_text_mode),
12208 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12209 range: default_edit_range,
12210 new_text: item.label.clone(),
12211 })),
12212 ..item.clone()
12213 }))
12214 .collect::<Vec<lsp::CompletionItem>>();
12215
12216 let mut cx = EditorLspTestContext::new_rust(
12217 lsp::ServerCapabilities {
12218 completion_provider: Some(lsp::CompletionOptions {
12219 trigger_characters: Some(vec![".".to_string()]),
12220 resolve_provider: Some(true),
12221 ..Default::default()
12222 }),
12223 ..Default::default()
12224 },
12225 cx,
12226 )
12227 .await;
12228
12229 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
12230 cx.simulate_keystroke(".");
12231
12232 let completion_data = default_data.clone();
12233 let completion_characters = default_commit_characters.clone();
12234 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
12235 let default_data = completion_data.clone();
12236 let default_commit_characters = completion_characters.clone();
12237 let items = items.clone();
12238 async move {
12239 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
12240 items,
12241 item_defaults: Some(lsp::CompletionListItemDefaults {
12242 data: Some(default_data.clone()),
12243 commit_characters: Some(default_commit_characters.clone()),
12244 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
12245 default_edit_range,
12246 )),
12247 insert_text_format: Some(default_insert_text_format),
12248 insert_text_mode: Some(default_insert_text_mode),
12249 }),
12250 ..lsp::CompletionList::default()
12251 })))
12252 }
12253 })
12254 .next()
12255 .await;
12256
12257 let resolved_items = Arc::new(Mutex::new(Vec::new()));
12258 cx.lsp
12259 .server
12260 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
12261 let closure_resolved_items = resolved_items.clone();
12262 move |item_to_resolve, _| {
12263 let closure_resolved_items = closure_resolved_items.clone();
12264 async move {
12265 closure_resolved_items.lock().push(item_to_resolve.clone());
12266 Ok(item_to_resolve)
12267 }
12268 }
12269 })
12270 .detach();
12271
12272 cx.condition(|editor, _| editor.context_menu_visible())
12273 .await;
12274 cx.run_until_parked();
12275 cx.update_editor(|editor, _, _| {
12276 let menu = editor.context_menu.borrow_mut();
12277 match menu.as_ref().expect("should have the completions menu") {
12278 CodeContextMenu::Completions(completions_menu) => {
12279 assert_eq!(
12280 completions_menu
12281 .entries
12282 .borrow()
12283 .iter()
12284 .map(|mat| mat.string.clone())
12285 .collect::<Vec<String>>(),
12286 items_out
12287 .iter()
12288 .map(|completion| completion.label.clone())
12289 .collect::<Vec<String>>()
12290 );
12291 }
12292 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
12293 }
12294 });
12295 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
12296 // with 4 from the end.
12297 assert_eq!(
12298 *resolved_items.lock(),
12299 [
12300 &items_out[0..16],
12301 &items_out[items_out.len() - 4..items_out.len()]
12302 ]
12303 .concat()
12304 .iter()
12305 .cloned()
12306 .collect::<Vec<lsp::CompletionItem>>()
12307 );
12308 resolved_items.lock().clear();
12309
12310 cx.update_editor(|editor, window, cx| {
12311 editor.context_menu_prev(&ContextMenuPrev, window, cx);
12312 });
12313 cx.run_until_parked();
12314 // Completions that have already been resolved are skipped.
12315 assert_eq!(
12316 *resolved_items.lock(),
12317 items_out[items_out.len() - 16..items_out.len() - 4]
12318 .iter()
12319 .cloned()
12320 .collect::<Vec<lsp::CompletionItem>>()
12321 );
12322 resolved_items.lock().clear();
12323}
12324
12325#[gpui::test]
12326async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
12327 init_test(cx, |_| {});
12328
12329 let mut cx = EditorLspTestContext::new(
12330 Language::new(
12331 LanguageConfig {
12332 matcher: LanguageMatcher {
12333 path_suffixes: vec!["jsx".into()],
12334 ..Default::default()
12335 },
12336 overrides: [(
12337 "element".into(),
12338 LanguageConfigOverride {
12339 word_characters: Override::Set(['-'].into_iter().collect()),
12340 ..Default::default()
12341 },
12342 )]
12343 .into_iter()
12344 .collect(),
12345 ..Default::default()
12346 },
12347 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12348 )
12349 .with_override_query("(jsx_self_closing_element) @element")
12350 .unwrap(),
12351 lsp::ServerCapabilities {
12352 completion_provider: Some(lsp::CompletionOptions {
12353 trigger_characters: Some(vec![":".to_string()]),
12354 ..Default::default()
12355 }),
12356 ..Default::default()
12357 },
12358 cx,
12359 )
12360 .await;
12361
12362 cx.lsp
12363 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12364 Ok(Some(lsp::CompletionResponse::Array(vec![
12365 lsp::CompletionItem {
12366 label: "bg-blue".into(),
12367 ..Default::default()
12368 },
12369 lsp::CompletionItem {
12370 label: "bg-red".into(),
12371 ..Default::default()
12372 },
12373 lsp::CompletionItem {
12374 label: "bg-yellow".into(),
12375 ..Default::default()
12376 },
12377 ])))
12378 });
12379
12380 cx.set_state(r#"<p class="bgˇ" />"#);
12381
12382 // Trigger completion when typing a dash, because the dash is an extra
12383 // word character in the 'element' scope, which contains the cursor.
12384 cx.simulate_keystroke("-");
12385 cx.executor().run_until_parked();
12386 cx.update_editor(|editor, _, _| {
12387 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12388 {
12389 assert_eq!(
12390 completion_menu_entries(&menu),
12391 &["bg-red", "bg-blue", "bg-yellow"]
12392 );
12393 } else {
12394 panic!("expected completion menu to be open");
12395 }
12396 });
12397
12398 cx.simulate_keystroke("l");
12399 cx.executor().run_until_parked();
12400 cx.update_editor(|editor, _, _| {
12401 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12402 {
12403 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
12404 } else {
12405 panic!("expected completion menu to be open");
12406 }
12407 });
12408
12409 // When filtering completions, consider the character after the '-' to
12410 // be the start of a subword.
12411 cx.set_state(r#"<p class="yelˇ" />"#);
12412 cx.simulate_keystroke("l");
12413 cx.executor().run_until_parked();
12414 cx.update_editor(|editor, _, _| {
12415 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12416 {
12417 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
12418 } else {
12419 panic!("expected completion menu to be open");
12420 }
12421 });
12422}
12423
12424fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
12425 let entries = menu.entries.borrow();
12426 entries.iter().map(|mat| mat.string.clone()).collect()
12427}
12428
12429#[gpui::test]
12430async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
12431 init_test(cx, |settings| {
12432 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
12433 FormatterList(vec![Formatter::Prettier].into()),
12434 ))
12435 });
12436
12437 let fs = FakeFs::new(cx.executor());
12438 fs.insert_file(path!("/file.ts"), Default::default()).await;
12439
12440 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
12441 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12442
12443 language_registry.add(Arc::new(Language::new(
12444 LanguageConfig {
12445 name: "TypeScript".into(),
12446 matcher: LanguageMatcher {
12447 path_suffixes: vec!["ts".to_string()],
12448 ..Default::default()
12449 },
12450 ..Default::default()
12451 },
12452 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12453 )));
12454 update_test_language_settings(cx, |settings| {
12455 settings.defaults.prettier = Some(PrettierSettings {
12456 allowed: true,
12457 ..PrettierSettings::default()
12458 });
12459 });
12460
12461 let test_plugin = "test_plugin";
12462 let _ = language_registry.register_fake_lsp(
12463 "TypeScript",
12464 FakeLspAdapter {
12465 prettier_plugins: vec![test_plugin],
12466 ..Default::default()
12467 },
12468 );
12469
12470 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
12471 let buffer = project
12472 .update(cx, |project, cx| {
12473 project.open_local_buffer(path!("/file.ts"), cx)
12474 })
12475 .await
12476 .unwrap();
12477
12478 let buffer_text = "one\ntwo\nthree\n";
12479 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12480 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12481 editor.update_in(cx, |editor, window, cx| {
12482 editor.set_text(buffer_text, window, cx)
12483 });
12484
12485 editor
12486 .update_in(cx, |editor, window, cx| {
12487 editor.perform_format(
12488 project.clone(),
12489 FormatTrigger::Manual,
12490 FormatTarget::Buffers,
12491 window,
12492 cx,
12493 )
12494 })
12495 .unwrap()
12496 .await;
12497 assert_eq!(
12498 editor.update(cx, |editor, cx| editor.text(cx)),
12499 buffer_text.to_string() + prettier_format_suffix,
12500 "Test prettier formatting was not applied to the original buffer text",
12501 );
12502
12503 update_test_language_settings(cx, |settings| {
12504 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
12505 });
12506 let format = editor.update_in(cx, |editor, window, cx| {
12507 editor.perform_format(
12508 project.clone(),
12509 FormatTrigger::Manual,
12510 FormatTarget::Buffers,
12511 window,
12512 cx,
12513 )
12514 });
12515 format.await.unwrap();
12516 assert_eq!(
12517 editor.update(cx, |editor, cx| editor.text(cx)),
12518 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
12519 "Autoformatting (via test prettier) was not applied to the original buffer text",
12520 );
12521}
12522
12523#[gpui::test]
12524async fn test_addition_reverts(cx: &mut TestAppContext) {
12525 init_test(cx, |_| {});
12526 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12527 let base_text = indoc! {r#"
12528 struct Row;
12529 struct Row1;
12530 struct Row2;
12531
12532 struct Row4;
12533 struct Row5;
12534 struct Row6;
12535
12536 struct Row8;
12537 struct Row9;
12538 struct Row10;"#};
12539
12540 // When addition hunks are not adjacent to carets, no hunk revert is performed
12541 assert_hunk_revert(
12542 indoc! {r#"struct Row;
12543 struct Row1;
12544 struct Row1.1;
12545 struct Row1.2;
12546 struct Row2;ˇ
12547
12548 struct Row4;
12549 struct Row5;
12550 struct Row6;
12551
12552 struct Row8;
12553 ˇstruct Row9;
12554 struct Row9.1;
12555 struct Row9.2;
12556 struct Row9.3;
12557 struct Row10;"#},
12558 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
12559 indoc! {r#"struct Row;
12560 struct Row1;
12561 struct Row1.1;
12562 struct Row1.2;
12563 struct Row2;ˇ
12564
12565 struct Row4;
12566 struct Row5;
12567 struct Row6;
12568
12569 struct Row8;
12570 ˇstruct Row9;
12571 struct Row9.1;
12572 struct Row9.2;
12573 struct Row9.3;
12574 struct Row10;"#},
12575 base_text,
12576 &mut cx,
12577 );
12578 // Same for selections
12579 assert_hunk_revert(
12580 indoc! {r#"struct Row;
12581 struct Row1;
12582 struct Row2;
12583 struct Row2.1;
12584 struct Row2.2;
12585 «ˇ
12586 struct Row4;
12587 struct» Row5;
12588 «struct Row6;
12589 ˇ»
12590 struct Row9.1;
12591 struct Row9.2;
12592 struct Row9.3;
12593 struct Row8;
12594 struct Row9;
12595 struct Row10;"#},
12596 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
12597 indoc! {r#"struct Row;
12598 struct Row1;
12599 struct Row2;
12600 struct Row2.1;
12601 struct Row2.2;
12602 «ˇ
12603 struct Row4;
12604 struct» Row5;
12605 «struct Row6;
12606 ˇ»
12607 struct Row9.1;
12608 struct Row9.2;
12609 struct Row9.3;
12610 struct Row8;
12611 struct Row9;
12612 struct Row10;"#},
12613 base_text,
12614 &mut cx,
12615 );
12616
12617 // When carets and selections intersect the addition hunks, those are reverted.
12618 // Adjacent carets got merged.
12619 assert_hunk_revert(
12620 indoc! {r#"struct Row;
12621 ˇ// something on the top
12622 struct Row1;
12623 struct Row2;
12624 struct Roˇw3.1;
12625 struct Row2.2;
12626 struct Row2.3;ˇ
12627
12628 struct Row4;
12629 struct ˇRow5.1;
12630 struct Row5.2;
12631 struct «Rowˇ»5.3;
12632 struct Row5;
12633 struct Row6;
12634 ˇ
12635 struct Row9.1;
12636 struct «Rowˇ»9.2;
12637 struct «ˇRow»9.3;
12638 struct Row8;
12639 struct Row9;
12640 «ˇ// something on bottom»
12641 struct Row10;"#},
12642 vec![
12643 DiffHunkStatusKind::Added,
12644 DiffHunkStatusKind::Added,
12645 DiffHunkStatusKind::Added,
12646 DiffHunkStatusKind::Added,
12647 DiffHunkStatusKind::Added,
12648 ],
12649 indoc! {r#"struct Row;
12650 ˇstruct Row1;
12651 struct Row2;
12652 ˇ
12653 struct Row4;
12654 ˇstruct Row5;
12655 struct Row6;
12656 ˇ
12657 ˇstruct Row8;
12658 struct Row9;
12659 ˇstruct Row10;"#},
12660 base_text,
12661 &mut cx,
12662 );
12663}
12664
12665#[gpui::test]
12666async fn test_modification_reverts(cx: &mut TestAppContext) {
12667 init_test(cx, |_| {});
12668 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12669 let base_text = indoc! {r#"
12670 struct Row;
12671 struct Row1;
12672 struct Row2;
12673
12674 struct Row4;
12675 struct Row5;
12676 struct Row6;
12677
12678 struct Row8;
12679 struct Row9;
12680 struct Row10;"#};
12681
12682 // Modification hunks behave the same as the addition ones.
12683 assert_hunk_revert(
12684 indoc! {r#"struct Row;
12685 struct Row1;
12686 struct Row33;
12687 ˇ
12688 struct Row4;
12689 struct Row5;
12690 struct Row6;
12691 ˇ
12692 struct Row99;
12693 struct Row9;
12694 struct Row10;"#},
12695 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
12696 indoc! {r#"struct Row;
12697 struct Row1;
12698 struct Row33;
12699 ˇ
12700 struct Row4;
12701 struct Row5;
12702 struct Row6;
12703 ˇ
12704 struct Row99;
12705 struct Row9;
12706 struct Row10;"#},
12707 base_text,
12708 &mut cx,
12709 );
12710 assert_hunk_revert(
12711 indoc! {r#"struct Row;
12712 struct Row1;
12713 struct Row33;
12714 «ˇ
12715 struct Row4;
12716 struct» Row5;
12717 «struct Row6;
12718 ˇ»
12719 struct Row99;
12720 struct Row9;
12721 struct Row10;"#},
12722 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
12723 indoc! {r#"struct Row;
12724 struct Row1;
12725 struct Row33;
12726 «ˇ
12727 struct Row4;
12728 struct» Row5;
12729 «struct Row6;
12730 ˇ»
12731 struct Row99;
12732 struct Row9;
12733 struct Row10;"#},
12734 base_text,
12735 &mut cx,
12736 );
12737
12738 assert_hunk_revert(
12739 indoc! {r#"ˇstruct Row1.1;
12740 struct Row1;
12741 «ˇstr»uct Row22;
12742
12743 struct ˇRow44;
12744 struct Row5;
12745 struct «Rˇ»ow66;ˇ
12746
12747 «struˇ»ct Row88;
12748 struct Row9;
12749 struct Row1011;ˇ"#},
12750 vec![
12751 DiffHunkStatusKind::Modified,
12752 DiffHunkStatusKind::Modified,
12753 DiffHunkStatusKind::Modified,
12754 DiffHunkStatusKind::Modified,
12755 DiffHunkStatusKind::Modified,
12756 DiffHunkStatusKind::Modified,
12757 ],
12758 indoc! {r#"struct Row;
12759 ˇstruct Row1;
12760 struct Row2;
12761 ˇ
12762 struct Row4;
12763 ˇstruct Row5;
12764 struct Row6;
12765 ˇ
12766 struct Row8;
12767 ˇstruct Row9;
12768 struct Row10;ˇ"#},
12769 base_text,
12770 &mut cx,
12771 );
12772}
12773
12774#[gpui::test]
12775async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
12776 init_test(cx, |_| {});
12777 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12778 let base_text = indoc! {r#"
12779 one
12780
12781 two
12782 three
12783 "#};
12784
12785 cx.set_head_text(base_text);
12786 cx.set_state("\nˇ\n");
12787 cx.executor().run_until_parked();
12788 cx.update_editor(|editor, _window, cx| {
12789 editor.expand_selected_diff_hunks(cx);
12790 });
12791 cx.executor().run_until_parked();
12792 cx.update_editor(|editor, window, cx| {
12793 editor.backspace(&Default::default(), window, cx);
12794 });
12795 cx.run_until_parked();
12796 cx.assert_state_with_diff(
12797 indoc! {r#"
12798
12799 - two
12800 - threeˇ
12801 +
12802 "#}
12803 .to_string(),
12804 );
12805}
12806
12807#[gpui::test]
12808async fn test_deletion_reverts(cx: &mut TestAppContext) {
12809 init_test(cx, |_| {});
12810 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12811 let base_text = indoc! {r#"struct Row;
12812struct Row1;
12813struct Row2;
12814
12815struct Row4;
12816struct Row5;
12817struct Row6;
12818
12819struct Row8;
12820struct Row9;
12821struct Row10;"#};
12822
12823 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
12824 assert_hunk_revert(
12825 indoc! {r#"struct Row;
12826 struct Row2;
12827
12828 ˇstruct Row4;
12829 struct Row5;
12830 struct Row6;
12831 ˇ
12832 struct Row8;
12833 struct Row10;"#},
12834 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
12835 indoc! {r#"struct Row;
12836 struct Row2;
12837
12838 ˇstruct Row4;
12839 struct Row5;
12840 struct Row6;
12841 ˇ
12842 struct Row8;
12843 struct Row10;"#},
12844 base_text,
12845 &mut cx,
12846 );
12847 assert_hunk_revert(
12848 indoc! {r#"struct Row;
12849 struct Row2;
12850
12851 «ˇstruct Row4;
12852 struct» Row5;
12853 «struct Row6;
12854 ˇ»
12855 struct Row8;
12856 struct Row10;"#},
12857 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
12858 indoc! {r#"struct Row;
12859 struct Row2;
12860
12861 «ˇstruct Row4;
12862 struct» Row5;
12863 «struct Row6;
12864 ˇ»
12865 struct Row8;
12866 struct Row10;"#},
12867 base_text,
12868 &mut cx,
12869 );
12870
12871 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
12872 assert_hunk_revert(
12873 indoc! {r#"struct Row;
12874 ˇstruct Row2;
12875
12876 struct Row4;
12877 struct Row5;
12878 struct Row6;
12879
12880 struct Row8;ˇ
12881 struct Row10;"#},
12882 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
12883 indoc! {r#"struct Row;
12884 struct Row1;
12885 ˇstruct Row2;
12886
12887 struct Row4;
12888 struct Row5;
12889 struct Row6;
12890
12891 struct Row8;ˇ
12892 struct Row9;
12893 struct Row10;"#},
12894 base_text,
12895 &mut cx,
12896 );
12897 assert_hunk_revert(
12898 indoc! {r#"struct Row;
12899 struct Row2«ˇ;
12900 struct Row4;
12901 struct» Row5;
12902 «struct Row6;
12903
12904 struct Row8;ˇ»
12905 struct Row10;"#},
12906 vec![
12907 DiffHunkStatusKind::Deleted,
12908 DiffHunkStatusKind::Deleted,
12909 DiffHunkStatusKind::Deleted,
12910 ],
12911 indoc! {r#"struct Row;
12912 struct Row1;
12913 struct Row2«ˇ;
12914
12915 struct Row4;
12916 struct» Row5;
12917 «struct Row6;
12918
12919 struct Row8;ˇ»
12920 struct Row9;
12921 struct Row10;"#},
12922 base_text,
12923 &mut cx,
12924 );
12925}
12926
12927#[gpui::test]
12928async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
12929 init_test(cx, |_| {});
12930
12931 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
12932 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
12933 let base_text_3 =
12934 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
12935
12936 let text_1 = edit_first_char_of_every_line(base_text_1);
12937 let text_2 = edit_first_char_of_every_line(base_text_2);
12938 let text_3 = edit_first_char_of_every_line(base_text_3);
12939
12940 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
12941 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
12942 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
12943
12944 let multibuffer = cx.new(|cx| {
12945 let mut multibuffer = MultiBuffer::new(ReadWrite);
12946 multibuffer.push_excerpts(
12947 buffer_1.clone(),
12948 [
12949 ExcerptRange {
12950 context: Point::new(0, 0)..Point::new(3, 0),
12951 primary: None,
12952 },
12953 ExcerptRange {
12954 context: Point::new(5, 0)..Point::new(7, 0),
12955 primary: None,
12956 },
12957 ExcerptRange {
12958 context: Point::new(9, 0)..Point::new(10, 4),
12959 primary: None,
12960 },
12961 ],
12962 cx,
12963 );
12964 multibuffer.push_excerpts(
12965 buffer_2.clone(),
12966 [
12967 ExcerptRange {
12968 context: Point::new(0, 0)..Point::new(3, 0),
12969 primary: None,
12970 },
12971 ExcerptRange {
12972 context: Point::new(5, 0)..Point::new(7, 0),
12973 primary: None,
12974 },
12975 ExcerptRange {
12976 context: Point::new(9, 0)..Point::new(10, 4),
12977 primary: None,
12978 },
12979 ],
12980 cx,
12981 );
12982 multibuffer.push_excerpts(
12983 buffer_3.clone(),
12984 [
12985 ExcerptRange {
12986 context: Point::new(0, 0)..Point::new(3, 0),
12987 primary: None,
12988 },
12989 ExcerptRange {
12990 context: Point::new(5, 0)..Point::new(7, 0),
12991 primary: None,
12992 },
12993 ExcerptRange {
12994 context: Point::new(9, 0)..Point::new(10, 4),
12995 primary: None,
12996 },
12997 ],
12998 cx,
12999 );
13000 multibuffer
13001 });
13002
13003 let fs = FakeFs::new(cx.executor());
13004 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13005 let (editor, cx) = cx
13006 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
13007 editor.update_in(cx, |editor, _window, cx| {
13008 for (buffer, diff_base) in [
13009 (buffer_1.clone(), base_text_1),
13010 (buffer_2.clone(), base_text_2),
13011 (buffer_3.clone(), base_text_3),
13012 ] {
13013 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
13014 editor
13015 .buffer
13016 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
13017 }
13018 });
13019 cx.executor().run_until_parked();
13020
13021 editor.update_in(cx, |editor, window, cx| {
13022 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}");
13023 editor.select_all(&SelectAll, window, cx);
13024 editor.git_restore(&Default::default(), window, cx);
13025 });
13026 cx.executor().run_until_parked();
13027
13028 // When all ranges are selected, all buffer hunks are reverted.
13029 editor.update(cx, |editor, cx| {
13030 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");
13031 });
13032 buffer_1.update(cx, |buffer, _| {
13033 assert_eq!(buffer.text(), base_text_1);
13034 });
13035 buffer_2.update(cx, |buffer, _| {
13036 assert_eq!(buffer.text(), base_text_2);
13037 });
13038 buffer_3.update(cx, |buffer, _| {
13039 assert_eq!(buffer.text(), base_text_3);
13040 });
13041
13042 editor.update_in(cx, |editor, window, cx| {
13043 editor.undo(&Default::default(), window, cx);
13044 });
13045
13046 editor.update_in(cx, |editor, window, cx| {
13047 editor.change_selections(None, window, cx, |s| {
13048 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
13049 });
13050 editor.git_restore(&Default::default(), window, cx);
13051 });
13052
13053 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
13054 // but not affect buffer_2 and its related excerpts.
13055 editor.update(cx, |editor, cx| {
13056 assert_eq!(
13057 editor.text(cx),
13058 "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}"
13059 );
13060 });
13061 buffer_1.update(cx, |buffer, _| {
13062 assert_eq!(buffer.text(), base_text_1);
13063 });
13064 buffer_2.update(cx, |buffer, _| {
13065 assert_eq!(
13066 buffer.text(),
13067 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
13068 );
13069 });
13070 buffer_3.update(cx, |buffer, _| {
13071 assert_eq!(
13072 buffer.text(),
13073 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
13074 );
13075 });
13076
13077 fn edit_first_char_of_every_line(text: &str) -> String {
13078 text.split('\n')
13079 .map(|line| format!("X{}", &line[1..]))
13080 .collect::<Vec<_>>()
13081 .join("\n")
13082 }
13083}
13084
13085#[gpui::test]
13086async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
13087 init_test(cx, |_| {});
13088
13089 let cols = 4;
13090 let rows = 10;
13091 let sample_text_1 = sample_text(rows, cols, 'a');
13092 assert_eq!(
13093 sample_text_1,
13094 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
13095 );
13096 let sample_text_2 = sample_text(rows, cols, 'l');
13097 assert_eq!(
13098 sample_text_2,
13099 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
13100 );
13101 let sample_text_3 = sample_text(rows, cols, 'v');
13102 assert_eq!(
13103 sample_text_3,
13104 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
13105 );
13106
13107 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
13108 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
13109 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
13110
13111 let multi_buffer = cx.new(|cx| {
13112 let mut multibuffer = MultiBuffer::new(ReadWrite);
13113 multibuffer.push_excerpts(
13114 buffer_1.clone(),
13115 [
13116 ExcerptRange {
13117 context: Point::new(0, 0)..Point::new(3, 0),
13118 primary: None,
13119 },
13120 ExcerptRange {
13121 context: Point::new(5, 0)..Point::new(7, 0),
13122 primary: None,
13123 },
13124 ExcerptRange {
13125 context: Point::new(9, 0)..Point::new(10, 4),
13126 primary: None,
13127 },
13128 ],
13129 cx,
13130 );
13131 multibuffer.push_excerpts(
13132 buffer_2.clone(),
13133 [
13134 ExcerptRange {
13135 context: Point::new(0, 0)..Point::new(3, 0),
13136 primary: None,
13137 },
13138 ExcerptRange {
13139 context: Point::new(5, 0)..Point::new(7, 0),
13140 primary: None,
13141 },
13142 ExcerptRange {
13143 context: Point::new(9, 0)..Point::new(10, 4),
13144 primary: None,
13145 },
13146 ],
13147 cx,
13148 );
13149 multibuffer.push_excerpts(
13150 buffer_3.clone(),
13151 [
13152 ExcerptRange {
13153 context: Point::new(0, 0)..Point::new(3, 0),
13154 primary: None,
13155 },
13156 ExcerptRange {
13157 context: Point::new(5, 0)..Point::new(7, 0),
13158 primary: None,
13159 },
13160 ExcerptRange {
13161 context: Point::new(9, 0)..Point::new(10, 4),
13162 primary: None,
13163 },
13164 ],
13165 cx,
13166 );
13167 multibuffer
13168 });
13169
13170 let fs = FakeFs::new(cx.executor());
13171 fs.insert_tree(
13172 "/a",
13173 json!({
13174 "main.rs": sample_text_1,
13175 "other.rs": sample_text_2,
13176 "lib.rs": sample_text_3,
13177 }),
13178 )
13179 .await;
13180 let project = Project::test(fs, ["/a".as_ref()], cx).await;
13181 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13182 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13183 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
13184 Editor::new(
13185 EditorMode::Full,
13186 multi_buffer,
13187 Some(project.clone()),
13188 true,
13189 window,
13190 cx,
13191 )
13192 });
13193 let multibuffer_item_id = workspace
13194 .update(cx, |workspace, window, cx| {
13195 assert!(
13196 workspace.active_item(cx).is_none(),
13197 "active item should be None before the first item is added"
13198 );
13199 workspace.add_item_to_active_pane(
13200 Box::new(multi_buffer_editor.clone()),
13201 None,
13202 true,
13203 window,
13204 cx,
13205 );
13206 let active_item = workspace
13207 .active_item(cx)
13208 .expect("should have an active item after adding the multi buffer");
13209 assert!(
13210 !active_item.is_singleton(cx),
13211 "A multi buffer was expected to active after adding"
13212 );
13213 active_item.item_id()
13214 })
13215 .unwrap();
13216 cx.executor().run_until_parked();
13217
13218 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13219 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
13220 s.select_ranges(Some(1..2))
13221 });
13222 editor.open_excerpts(&OpenExcerpts, window, cx);
13223 });
13224 cx.executor().run_until_parked();
13225 let first_item_id = workspace
13226 .update(cx, |workspace, window, cx| {
13227 let active_item = workspace
13228 .active_item(cx)
13229 .expect("should have an active item after navigating into the 1st buffer");
13230 let first_item_id = active_item.item_id();
13231 assert_ne!(
13232 first_item_id, multibuffer_item_id,
13233 "Should navigate into the 1st buffer and activate it"
13234 );
13235 assert!(
13236 active_item.is_singleton(cx),
13237 "New active item should be a singleton buffer"
13238 );
13239 assert_eq!(
13240 active_item
13241 .act_as::<Editor>(cx)
13242 .expect("should have navigated into an editor for the 1st buffer")
13243 .read(cx)
13244 .text(cx),
13245 sample_text_1
13246 );
13247
13248 workspace
13249 .go_back(workspace.active_pane().downgrade(), window, cx)
13250 .detach_and_log_err(cx);
13251
13252 first_item_id
13253 })
13254 .unwrap();
13255 cx.executor().run_until_parked();
13256 workspace
13257 .update(cx, |workspace, _, cx| {
13258 let active_item = workspace
13259 .active_item(cx)
13260 .expect("should have an active item after navigating back");
13261 assert_eq!(
13262 active_item.item_id(),
13263 multibuffer_item_id,
13264 "Should navigate back to the multi buffer"
13265 );
13266 assert!(!active_item.is_singleton(cx));
13267 })
13268 .unwrap();
13269
13270 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13271 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
13272 s.select_ranges(Some(39..40))
13273 });
13274 editor.open_excerpts(&OpenExcerpts, window, cx);
13275 });
13276 cx.executor().run_until_parked();
13277 let second_item_id = workspace
13278 .update(cx, |workspace, window, cx| {
13279 let active_item = workspace
13280 .active_item(cx)
13281 .expect("should have an active item after navigating into the 2nd buffer");
13282 let second_item_id = active_item.item_id();
13283 assert_ne!(
13284 second_item_id, multibuffer_item_id,
13285 "Should navigate away from the multibuffer"
13286 );
13287 assert_ne!(
13288 second_item_id, first_item_id,
13289 "Should navigate into the 2nd buffer and activate it"
13290 );
13291 assert!(
13292 active_item.is_singleton(cx),
13293 "New active item should be a singleton buffer"
13294 );
13295 assert_eq!(
13296 active_item
13297 .act_as::<Editor>(cx)
13298 .expect("should have navigated into an editor")
13299 .read(cx)
13300 .text(cx),
13301 sample_text_2
13302 );
13303
13304 workspace
13305 .go_back(workspace.active_pane().downgrade(), window, cx)
13306 .detach_and_log_err(cx);
13307
13308 second_item_id
13309 })
13310 .unwrap();
13311 cx.executor().run_until_parked();
13312 workspace
13313 .update(cx, |workspace, _, cx| {
13314 let active_item = workspace
13315 .active_item(cx)
13316 .expect("should have an active item after navigating back from the 2nd buffer");
13317 assert_eq!(
13318 active_item.item_id(),
13319 multibuffer_item_id,
13320 "Should navigate back from the 2nd buffer to the multi buffer"
13321 );
13322 assert!(!active_item.is_singleton(cx));
13323 })
13324 .unwrap();
13325
13326 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13327 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
13328 s.select_ranges(Some(70..70))
13329 });
13330 editor.open_excerpts(&OpenExcerpts, window, cx);
13331 });
13332 cx.executor().run_until_parked();
13333 workspace
13334 .update(cx, |workspace, window, cx| {
13335 let active_item = workspace
13336 .active_item(cx)
13337 .expect("should have an active item after navigating into the 3rd buffer");
13338 let third_item_id = active_item.item_id();
13339 assert_ne!(
13340 third_item_id, multibuffer_item_id,
13341 "Should navigate into the 3rd buffer and activate it"
13342 );
13343 assert_ne!(third_item_id, first_item_id);
13344 assert_ne!(third_item_id, second_item_id);
13345 assert!(
13346 active_item.is_singleton(cx),
13347 "New active item should be a singleton buffer"
13348 );
13349 assert_eq!(
13350 active_item
13351 .act_as::<Editor>(cx)
13352 .expect("should have navigated into an editor")
13353 .read(cx)
13354 .text(cx),
13355 sample_text_3
13356 );
13357
13358 workspace
13359 .go_back(workspace.active_pane().downgrade(), window, cx)
13360 .detach_and_log_err(cx);
13361 })
13362 .unwrap();
13363 cx.executor().run_until_parked();
13364 workspace
13365 .update(cx, |workspace, _, cx| {
13366 let active_item = workspace
13367 .active_item(cx)
13368 .expect("should have an active item after navigating back from the 3rd buffer");
13369 assert_eq!(
13370 active_item.item_id(),
13371 multibuffer_item_id,
13372 "Should navigate back from the 3rd buffer to the multi buffer"
13373 );
13374 assert!(!active_item.is_singleton(cx));
13375 })
13376 .unwrap();
13377}
13378
13379#[gpui::test]
13380async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13381 init_test(cx, |_| {});
13382
13383 let mut cx = EditorTestContext::new(cx).await;
13384
13385 let diff_base = r#"
13386 use some::mod;
13387
13388 const A: u32 = 42;
13389
13390 fn main() {
13391 println!("hello");
13392
13393 println!("world");
13394 }
13395 "#
13396 .unindent();
13397
13398 cx.set_state(
13399 &r#"
13400 use some::modified;
13401
13402 ˇ
13403 fn main() {
13404 println!("hello there");
13405
13406 println!("around the");
13407 println!("world");
13408 }
13409 "#
13410 .unindent(),
13411 );
13412
13413 cx.set_head_text(&diff_base);
13414 executor.run_until_parked();
13415
13416 cx.update_editor(|editor, window, cx| {
13417 editor.go_to_next_hunk(&GoToHunk, window, cx);
13418 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13419 });
13420 executor.run_until_parked();
13421 cx.assert_state_with_diff(
13422 r#"
13423 use some::modified;
13424
13425
13426 fn main() {
13427 - println!("hello");
13428 + ˇ println!("hello there");
13429
13430 println!("around the");
13431 println!("world");
13432 }
13433 "#
13434 .unindent(),
13435 );
13436
13437 cx.update_editor(|editor, window, cx| {
13438 for _ in 0..2 {
13439 editor.go_to_next_hunk(&GoToHunk, window, cx);
13440 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13441 }
13442 });
13443 executor.run_until_parked();
13444 cx.assert_state_with_diff(
13445 r#"
13446 - use some::mod;
13447 + ˇuse some::modified;
13448
13449
13450 fn main() {
13451 - println!("hello");
13452 + println!("hello there");
13453
13454 + println!("around the");
13455 println!("world");
13456 }
13457 "#
13458 .unindent(),
13459 );
13460
13461 cx.update_editor(|editor, window, cx| {
13462 editor.go_to_next_hunk(&GoToHunk, window, cx);
13463 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13464 });
13465 executor.run_until_parked();
13466 cx.assert_state_with_diff(
13467 r#"
13468 - use some::mod;
13469 + use some::modified;
13470
13471 - const A: u32 = 42;
13472 ˇ
13473 fn main() {
13474 - println!("hello");
13475 + println!("hello there");
13476
13477 + println!("around the");
13478 println!("world");
13479 }
13480 "#
13481 .unindent(),
13482 );
13483
13484 cx.update_editor(|editor, window, cx| {
13485 editor.cancel(&Cancel, window, cx);
13486 });
13487
13488 cx.assert_state_with_diff(
13489 r#"
13490 use some::modified;
13491
13492 ˇ
13493 fn main() {
13494 println!("hello there");
13495
13496 println!("around the");
13497 println!("world");
13498 }
13499 "#
13500 .unindent(),
13501 );
13502}
13503
13504#[gpui::test]
13505async fn test_diff_base_change_with_expanded_diff_hunks(
13506 executor: BackgroundExecutor,
13507 cx: &mut TestAppContext,
13508) {
13509 init_test(cx, |_| {});
13510
13511 let mut cx = EditorTestContext::new(cx).await;
13512
13513 let diff_base = r#"
13514 use some::mod1;
13515 use some::mod2;
13516
13517 const A: u32 = 42;
13518 const B: u32 = 42;
13519 const C: u32 = 42;
13520
13521 fn main() {
13522 println!("hello");
13523
13524 println!("world");
13525 }
13526 "#
13527 .unindent();
13528
13529 cx.set_state(
13530 &r#"
13531 use some::mod2;
13532
13533 const A: u32 = 42;
13534 const C: u32 = 42;
13535
13536 fn main(ˇ) {
13537 //println!("hello");
13538
13539 println!("world");
13540 //
13541 //
13542 }
13543 "#
13544 .unindent(),
13545 );
13546
13547 cx.set_head_text(&diff_base);
13548 executor.run_until_parked();
13549
13550 cx.update_editor(|editor, window, cx| {
13551 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
13552 });
13553 executor.run_until_parked();
13554 cx.assert_state_with_diff(
13555 r#"
13556 - use some::mod1;
13557 use some::mod2;
13558
13559 const A: u32 = 42;
13560 - const B: u32 = 42;
13561 const C: u32 = 42;
13562
13563 fn main(ˇ) {
13564 - println!("hello");
13565 + //println!("hello");
13566
13567 println!("world");
13568 + //
13569 + //
13570 }
13571 "#
13572 .unindent(),
13573 );
13574
13575 cx.set_head_text("new diff base!");
13576 executor.run_until_parked();
13577 cx.assert_state_with_diff(
13578 r#"
13579 - new diff base!
13580 + use some::mod2;
13581 +
13582 + const A: u32 = 42;
13583 + const C: u32 = 42;
13584 +
13585 + fn main(ˇ) {
13586 + //println!("hello");
13587 +
13588 + println!("world");
13589 + //
13590 + //
13591 + }
13592 "#
13593 .unindent(),
13594 );
13595}
13596
13597#[gpui::test]
13598async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
13599 init_test(cx, |_| {});
13600
13601 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
13602 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
13603 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
13604 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
13605 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
13606 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
13607
13608 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
13609 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
13610 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
13611
13612 let multi_buffer = cx.new(|cx| {
13613 let mut multibuffer = MultiBuffer::new(ReadWrite);
13614 multibuffer.push_excerpts(
13615 buffer_1.clone(),
13616 [
13617 ExcerptRange {
13618 context: Point::new(0, 0)..Point::new(3, 0),
13619 primary: None,
13620 },
13621 ExcerptRange {
13622 context: Point::new(5, 0)..Point::new(7, 0),
13623 primary: None,
13624 },
13625 ExcerptRange {
13626 context: Point::new(9, 0)..Point::new(10, 3),
13627 primary: None,
13628 },
13629 ],
13630 cx,
13631 );
13632 multibuffer.push_excerpts(
13633 buffer_2.clone(),
13634 [
13635 ExcerptRange {
13636 context: Point::new(0, 0)..Point::new(3, 0),
13637 primary: None,
13638 },
13639 ExcerptRange {
13640 context: Point::new(5, 0)..Point::new(7, 0),
13641 primary: None,
13642 },
13643 ExcerptRange {
13644 context: Point::new(9, 0)..Point::new(10, 3),
13645 primary: None,
13646 },
13647 ],
13648 cx,
13649 );
13650 multibuffer.push_excerpts(
13651 buffer_3.clone(),
13652 [
13653 ExcerptRange {
13654 context: Point::new(0, 0)..Point::new(3, 0),
13655 primary: None,
13656 },
13657 ExcerptRange {
13658 context: Point::new(5, 0)..Point::new(7, 0),
13659 primary: None,
13660 },
13661 ExcerptRange {
13662 context: Point::new(9, 0)..Point::new(10, 3),
13663 primary: None,
13664 },
13665 ],
13666 cx,
13667 );
13668 multibuffer
13669 });
13670
13671 let editor = cx.add_window(|window, cx| {
13672 Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
13673 });
13674 editor
13675 .update(cx, |editor, _window, cx| {
13676 for (buffer, diff_base) in [
13677 (buffer_1.clone(), file_1_old),
13678 (buffer_2.clone(), file_2_old),
13679 (buffer_3.clone(), file_3_old),
13680 ] {
13681 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
13682 editor
13683 .buffer
13684 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
13685 }
13686 })
13687 .unwrap();
13688
13689 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13690 cx.run_until_parked();
13691
13692 cx.assert_editor_state(
13693 &"
13694 ˇaaa
13695 ccc
13696 ddd
13697
13698 ggg
13699 hhh
13700
13701
13702 lll
13703 mmm
13704 NNN
13705
13706 qqq
13707 rrr
13708
13709 uuu
13710 111
13711 222
13712 333
13713
13714 666
13715 777
13716
13717 000
13718 !!!"
13719 .unindent(),
13720 );
13721
13722 cx.update_editor(|editor, window, cx| {
13723 editor.select_all(&SelectAll, window, cx);
13724 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13725 });
13726 cx.executor().run_until_parked();
13727
13728 cx.assert_state_with_diff(
13729 "
13730 «aaa
13731 - bbb
13732 ccc
13733 ddd
13734
13735 ggg
13736 hhh
13737
13738
13739 lll
13740 mmm
13741 - nnn
13742 + NNN
13743
13744 qqq
13745 rrr
13746
13747 uuu
13748 111
13749 222
13750 333
13751
13752 + 666
13753 777
13754
13755 000
13756 !!!ˇ»"
13757 .unindent(),
13758 );
13759}
13760
13761#[gpui::test]
13762async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
13763 init_test(cx, |_| {});
13764
13765 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
13766 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
13767
13768 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
13769 let multi_buffer = cx.new(|cx| {
13770 let mut multibuffer = MultiBuffer::new(ReadWrite);
13771 multibuffer.push_excerpts(
13772 buffer.clone(),
13773 [
13774 ExcerptRange {
13775 context: Point::new(0, 0)..Point::new(2, 0),
13776 primary: None,
13777 },
13778 ExcerptRange {
13779 context: Point::new(4, 0)..Point::new(7, 0),
13780 primary: None,
13781 },
13782 ExcerptRange {
13783 context: Point::new(9, 0)..Point::new(10, 0),
13784 primary: None,
13785 },
13786 ],
13787 cx,
13788 );
13789 multibuffer
13790 });
13791
13792 let editor = cx.add_window(|window, cx| {
13793 Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
13794 });
13795 editor
13796 .update(cx, |editor, _window, cx| {
13797 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
13798 editor
13799 .buffer
13800 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
13801 })
13802 .unwrap();
13803
13804 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13805 cx.run_until_parked();
13806
13807 cx.update_editor(|editor, window, cx| {
13808 editor.expand_all_diff_hunks(&Default::default(), window, cx)
13809 });
13810 cx.executor().run_until_parked();
13811
13812 // When the start of a hunk coincides with the start of its excerpt,
13813 // the hunk is expanded. When the start of a a hunk is earlier than
13814 // the start of its excerpt, the hunk is not expanded.
13815 cx.assert_state_with_diff(
13816 "
13817 ˇaaa
13818 - bbb
13819 + BBB
13820
13821 - ddd
13822 - eee
13823 + DDD
13824 + EEE
13825 fff
13826
13827 iii
13828 "
13829 .unindent(),
13830 );
13831}
13832
13833#[gpui::test]
13834async fn test_edits_around_expanded_insertion_hunks(
13835 executor: BackgroundExecutor,
13836 cx: &mut TestAppContext,
13837) {
13838 init_test(cx, |_| {});
13839
13840 let mut cx = EditorTestContext::new(cx).await;
13841
13842 let diff_base = r#"
13843 use some::mod1;
13844 use some::mod2;
13845
13846 const A: u32 = 42;
13847
13848 fn main() {
13849 println!("hello");
13850
13851 println!("world");
13852 }
13853 "#
13854 .unindent();
13855 executor.run_until_parked();
13856 cx.set_state(
13857 &r#"
13858 use some::mod1;
13859 use some::mod2;
13860
13861 const A: u32 = 42;
13862 const B: u32 = 42;
13863 const C: u32 = 42;
13864 ˇ
13865
13866 fn main() {
13867 println!("hello");
13868
13869 println!("world");
13870 }
13871 "#
13872 .unindent(),
13873 );
13874
13875 cx.set_head_text(&diff_base);
13876 executor.run_until_parked();
13877
13878 cx.update_editor(|editor, window, cx| {
13879 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
13880 });
13881 executor.run_until_parked();
13882
13883 cx.assert_state_with_diff(
13884 r#"
13885 use some::mod1;
13886 use some::mod2;
13887
13888 const A: u32 = 42;
13889 + const B: u32 = 42;
13890 + const C: u32 = 42;
13891 + ˇ
13892
13893 fn main() {
13894 println!("hello");
13895
13896 println!("world");
13897 }
13898 "#
13899 .unindent(),
13900 );
13901
13902 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
13903 executor.run_until_parked();
13904
13905 cx.assert_state_with_diff(
13906 r#"
13907 use some::mod1;
13908 use some::mod2;
13909
13910 const A: u32 = 42;
13911 + const B: u32 = 42;
13912 + const C: u32 = 42;
13913 + const D: u32 = 42;
13914 + ˇ
13915
13916 fn main() {
13917 println!("hello");
13918
13919 println!("world");
13920 }
13921 "#
13922 .unindent(),
13923 );
13924
13925 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
13926 executor.run_until_parked();
13927
13928 cx.assert_state_with_diff(
13929 r#"
13930 use some::mod1;
13931 use some::mod2;
13932
13933 const A: u32 = 42;
13934 + const B: u32 = 42;
13935 + const C: u32 = 42;
13936 + const D: u32 = 42;
13937 + const E: u32 = 42;
13938 + ˇ
13939
13940 fn main() {
13941 println!("hello");
13942
13943 println!("world");
13944 }
13945 "#
13946 .unindent(),
13947 );
13948
13949 cx.update_editor(|editor, window, cx| {
13950 editor.delete_line(&DeleteLine, window, cx);
13951 });
13952 executor.run_until_parked();
13953
13954 cx.assert_state_with_diff(
13955 r#"
13956 use some::mod1;
13957 use some::mod2;
13958
13959 const A: u32 = 42;
13960 + const B: u32 = 42;
13961 + const C: u32 = 42;
13962 + const D: u32 = 42;
13963 + const E: u32 = 42;
13964 ˇ
13965 fn main() {
13966 println!("hello");
13967
13968 println!("world");
13969 }
13970 "#
13971 .unindent(),
13972 );
13973
13974 cx.update_editor(|editor, window, cx| {
13975 editor.move_up(&MoveUp, window, cx);
13976 editor.delete_line(&DeleteLine, window, cx);
13977 editor.move_up(&MoveUp, window, cx);
13978 editor.delete_line(&DeleteLine, window, cx);
13979 editor.move_up(&MoveUp, window, cx);
13980 editor.delete_line(&DeleteLine, window, cx);
13981 });
13982 executor.run_until_parked();
13983 cx.assert_state_with_diff(
13984 r#"
13985 use some::mod1;
13986 use some::mod2;
13987
13988 const A: u32 = 42;
13989 + const B: u32 = 42;
13990 ˇ
13991 fn main() {
13992 println!("hello");
13993
13994 println!("world");
13995 }
13996 "#
13997 .unindent(),
13998 );
13999
14000 cx.update_editor(|editor, window, cx| {
14001 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
14002 editor.delete_line(&DeleteLine, window, cx);
14003 });
14004 executor.run_until_parked();
14005 cx.assert_state_with_diff(
14006 r#"
14007 ˇ
14008 fn main() {
14009 println!("hello");
14010
14011 println!("world");
14012 }
14013 "#
14014 .unindent(),
14015 );
14016}
14017
14018#[gpui::test]
14019async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
14020 init_test(cx, |_| {});
14021
14022 let mut cx = EditorTestContext::new(cx).await;
14023 cx.set_head_text(indoc! { "
14024 one
14025 two
14026 three
14027 four
14028 five
14029 "
14030 });
14031 cx.set_state(indoc! { "
14032 one
14033 ˇthree
14034 five
14035 "});
14036 cx.run_until_parked();
14037 cx.update_editor(|editor, window, cx| {
14038 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14039 });
14040 cx.assert_state_with_diff(
14041 indoc! { "
14042 one
14043 - two
14044 ˇthree
14045 - four
14046 five
14047 "}
14048 .to_string(),
14049 );
14050 cx.update_editor(|editor, window, cx| {
14051 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14052 });
14053
14054 cx.assert_state_with_diff(
14055 indoc! { "
14056 one
14057 ˇthree
14058 five
14059 "}
14060 .to_string(),
14061 );
14062
14063 cx.set_state(indoc! { "
14064 one
14065 ˇTWO
14066 three
14067 four
14068 five
14069 "});
14070 cx.run_until_parked();
14071 cx.update_editor(|editor, window, cx| {
14072 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14073 });
14074
14075 cx.assert_state_with_diff(
14076 indoc! { "
14077 one
14078 - two
14079 + ˇTWO
14080 three
14081 four
14082 five
14083 "}
14084 .to_string(),
14085 );
14086 cx.update_editor(|editor, window, cx| {
14087 editor.move_up(&Default::default(), window, cx);
14088 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14089 });
14090 cx.assert_state_with_diff(
14091 indoc! { "
14092 one
14093 ˇTWO
14094 three
14095 four
14096 five
14097 "}
14098 .to_string(),
14099 );
14100}
14101
14102#[gpui::test]
14103async fn test_edits_around_expanded_deletion_hunks(
14104 executor: BackgroundExecutor,
14105 cx: &mut TestAppContext,
14106) {
14107 init_test(cx, |_| {});
14108
14109 let mut cx = EditorTestContext::new(cx).await;
14110
14111 let diff_base = r#"
14112 use some::mod1;
14113 use some::mod2;
14114
14115 const A: u32 = 42;
14116 const B: u32 = 42;
14117 const C: u32 = 42;
14118
14119
14120 fn main() {
14121 println!("hello");
14122
14123 println!("world");
14124 }
14125 "#
14126 .unindent();
14127 executor.run_until_parked();
14128 cx.set_state(
14129 &r#"
14130 use some::mod1;
14131 use some::mod2;
14132
14133 ˇconst B: u32 = 42;
14134 const C: u32 = 42;
14135
14136
14137 fn main() {
14138 println!("hello");
14139
14140 println!("world");
14141 }
14142 "#
14143 .unindent(),
14144 );
14145
14146 cx.set_head_text(&diff_base);
14147 executor.run_until_parked();
14148
14149 cx.update_editor(|editor, window, cx| {
14150 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14151 });
14152 executor.run_until_parked();
14153
14154 cx.assert_state_with_diff(
14155 r#"
14156 use some::mod1;
14157 use some::mod2;
14158
14159 - const A: u32 = 42;
14160 ˇconst B: u32 = 42;
14161 const C: u32 = 42;
14162
14163
14164 fn main() {
14165 println!("hello");
14166
14167 println!("world");
14168 }
14169 "#
14170 .unindent(),
14171 );
14172
14173 cx.update_editor(|editor, window, cx| {
14174 editor.delete_line(&DeleteLine, window, cx);
14175 });
14176 executor.run_until_parked();
14177 cx.assert_state_with_diff(
14178 r#"
14179 use some::mod1;
14180 use some::mod2;
14181
14182 - const A: u32 = 42;
14183 - const B: u32 = 42;
14184 ˇconst C: u32 = 42;
14185
14186
14187 fn main() {
14188 println!("hello");
14189
14190 println!("world");
14191 }
14192 "#
14193 .unindent(),
14194 );
14195
14196 cx.update_editor(|editor, window, cx| {
14197 editor.delete_line(&DeleteLine, window, cx);
14198 });
14199 executor.run_until_parked();
14200 cx.assert_state_with_diff(
14201 r#"
14202 use some::mod1;
14203 use some::mod2;
14204
14205 - const A: u32 = 42;
14206 - const B: u32 = 42;
14207 - const C: u32 = 42;
14208 ˇ
14209
14210 fn main() {
14211 println!("hello");
14212
14213 println!("world");
14214 }
14215 "#
14216 .unindent(),
14217 );
14218
14219 cx.update_editor(|editor, window, cx| {
14220 editor.handle_input("replacement", window, cx);
14221 });
14222 executor.run_until_parked();
14223 cx.assert_state_with_diff(
14224 r#"
14225 use some::mod1;
14226 use some::mod2;
14227
14228 - const A: u32 = 42;
14229 - const B: u32 = 42;
14230 - const C: u32 = 42;
14231 -
14232 + replacementˇ
14233
14234 fn main() {
14235 println!("hello");
14236
14237 println!("world");
14238 }
14239 "#
14240 .unindent(),
14241 );
14242}
14243
14244#[gpui::test]
14245async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14246 init_test(cx, |_| {});
14247
14248 let mut cx = EditorTestContext::new(cx).await;
14249
14250 let base_text = r#"
14251 one
14252 two
14253 three
14254 four
14255 five
14256 "#
14257 .unindent();
14258 executor.run_until_parked();
14259 cx.set_state(
14260 &r#"
14261 one
14262 two
14263 fˇour
14264 five
14265 "#
14266 .unindent(),
14267 );
14268
14269 cx.set_head_text(&base_text);
14270 executor.run_until_parked();
14271
14272 cx.update_editor(|editor, window, cx| {
14273 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14274 });
14275 executor.run_until_parked();
14276
14277 cx.assert_state_with_diff(
14278 r#"
14279 one
14280 two
14281 - three
14282 fˇour
14283 five
14284 "#
14285 .unindent(),
14286 );
14287
14288 cx.update_editor(|editor, window, cx| {
14289 editor.backspace(&Backspace, window, cx);
14290 editor.backspace(&Backspace, window, cx);
14291 });
14292 executor.run_until_parked();
14293 cx.assert_state_with_diff(
14294 r#"
14295 one
14296 two
14297 - threeˇ
14298 - four
14299 + our
14300 five
14301 "#
14302 .unindent(),
14303 );
14304}
14305
14306#[gpui::test]
14307async fn test_edit_after_expanded_modification_hunk(
14308 executor: BackgroundExecutor,
14309 cx: &mut TestAppContext,
14310) {
14311 init_test(cx, |_| {});
14312
14313 let mut cx = EditorTestContext::new(cx).await;
14314
14315 let diff_base = r#"
14316 use some::mod1;
14317 use some::mod2;
14318
14319 const A: u32 = 42;
14320 const B: u32 = 42;
14321 const C: u32 = 42;
14322 const D: u32 = 42;
14323
14324
14325 fn main() {
14326 println!("hello");
14327
14328 println!("world");
14329 }"#
14330 .unindent();
14331
14332 cx.set_state(
14333 &r#"
14334 use some::mod1;
14335 use some::mod2;
14336
14337 const A: u32 = 42;
14338 const B: u32 = 42;
14339 const C: u32 = 43ˇ
14340 const D: u32 = 42;
14341
14342
14343 fn main() {
14344 println!("hello");
14345
14346 println!("world");
14347 }"#
14348 .unindent(),
14349 );
14350
14351 cx.set_head_text(&diff_base);
14352 executor.run_until_parked();
14353 cx.update_editor(|editor, window, cx| {
14354 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14355 });
14356 executor.run_until_parked();
14357
14358 cx.assert_state_with_diff(
14359 r#"
14360 use some::mod1;
14361 use some::mod2;
14362
14363 const A: u32 = 42;
14364 const B: u32 = 42;
14365 - const C: u32 = 42;
14366 + const C: u32 = 43ˇ
14367 const D: u32 = 42;
14368
14369
14370 fn main() {
14371 println!("hello");
14372
14373 println!("world");
14374 }"#
14375 .unindent(),
14376 );
14377
14378 cx.update_editor(|editor, window, cx| {
14379 editor.handle_input("\nnew_line\n", window, cx);
14380 });
14381 executor.run_until_parked();
14382
14383 cx.assert_state_with_diff(
14384 r#"
14385 use some::mod1;
14386 use some::mod2;
14387
14388 const A: u32 = 42;
14389 const B: u32 = 42;
14390 - const C: u32 = 42;
14391 + const C: u32 = 43
14392 + new_line
14393 + ˇ
14394 const D: u32 = 42;
14395
14396
14397 fn main() {
14398 println!("hello");
14399
14400 println!("world");
14401 }"#
14402 .unindent(),
14403 );
14404}
14405
14406#[gpui::test]
14407async fn test_stage_and_unstage_added_file_hunk(
14408 executor: BackgroundExecutor,
14409 cx: &mut TestAppContext,
14410) {
14411 init_test(cx, |_| {});
14412
14413 let mut cx = EditorTestContext::new(cx).await;
14414 cx.update_editor(|editor, _, cx| {
14415 editor.set_expand_all_diff_hunks(cx);
14416 });
14417
14418 let working_copy = r#"
14419 ˇfn main() {
14420 println!("hello, world!");
14421 }
14422 "#
14423 .unindent();
14424
14425 cx.set_state(&working_copy);
14426 executor.run_until_parked();
14427
14428 cx.assert_state_with_diff(
14429 r#"
14430 + ˇfn main() {
14431 + println!("hello, world!");
14432 + }
14433 "#
14434 .unindent(),
14435 );
14436 cx.assert_index_text(None);
14437
14438 cx.update_editor(|editor, window, cx| {
14439 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
14440 });
14441 executor.run_until_parked();
14442 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
14443 cx.assert_state_with_diff(
14444 r#"
14445 + ˇfn main() {
14446 + println!("hello, world!");
14447 + }
14448 "#
14449 .unindent(),
14450 );
14451
14452 cx.update_editor(|editor, window, cx| {
14453 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
14454 });
14455 executor.run_until_parked();
14456 cx.assert_index_text(None);
14457}
14458
14459async fn setup_indent_guides_editor(
14460 text: &str,
14461 cx: &mut TestAppContext,
14462) -> (BufferId, EditorTestContext) {
14463 init_test(cx, |_| {});
14464
14465 let mut cx = EditorTestContext::new(cx).await;
14466
14467 let buffer_id = cx.update_editor(|editor, window, cx| {
14468 editor.set_text(text, window, cx);
14469 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
14470
14471 buffer_ids[0]
14472 });
14473
14474 (buffer_id, cx)
14475}
14476
14477fn assert_indent_guides(
14478 range: Range<u32>,
14479 expected: Vec<IndentGuide>,
14480 active_indices: Option<Vec<usize>>,
14481 cx: &mut EditorTestContext,
14482) {
14483 let indent_guides = cx.update_editor(|editor, window, cx| {
14484 let snapshot = editor.snapshot(window, cx).display_snapshot;
14485 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
14486 editor,
14487 MultiBufferRow(range.start)..MultiBufferRow(range.end),
14488 true,
14489 &snapshot,
14490 cx,
14491 );
14492
14493 indent_guides.sort_by(|a, b| {
14494 a.depth.cmp(&b.depth).then(
14495 a.start_row
14496 .cmp(&b.start_row)
14497 .then(a.end_row.cmp(&b.end_row)),
14498 )
14499 });
14500 indent_guides
14501 });
14502
14503 if let Some(expected) = active_indices {
14504 let active_indices = cx.update_editor(|editor, window, cx| {
14505 let snapshot = editor.snapshot(window, cx).display_snapshot;
14506 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
14507 });
14508
14509 assert_eq!(
14510 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
14511 expected,
14512 "Active indent guide indices do not match"
14513 );
14514 }
14515
14516 assert_eq!(indent_guides, expected, "Indent guides do not match");
14517}
14518
14519fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
14520 IndentGuide {
14521 buffer_id,
14522 start_row: MultiBufferRow(start_row),
14523 end_row: MultiBufferRow(end_row),
14524 depth,
14525 tab_size: 4,
14526 settings: IndentGuideSettings {
14527 enabled: true,
14528 line_width: 1,
14529 active_line_width: 1,
14530 ..Default::default()
14531 },
14532 }
14533}
14534
14535#[gpui::test]
14536async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
14537 let (buffer_id, mut cx) = setup_indent_guides_editor(
14538 &"
14539 fn main() {
14540 let a = 1;
14541 }"
14542 .unindent(),
14543 cx,
14544 )
14545 .await;
14546
14547 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
14548}
14549
14550#[gpui::test]
14551async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
14552 let (buffer_id, mut cx) = setup_indent_guides_editor(
14553 &"
14554 fn main() {
14555 let a = 1;
14556 let b = 2;
14557 }"
14558 .unindent(),
14559 cx,
14560 )
14561 .await;
14562
14563 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
14564}
14565
14566#[gpui::test]
14567async fn test_indent_guide_nested(cx: &mut TestAppContext) {
14568 let (buffer_id, mut cx) = setup_indent_guides_editor(
14569 &"
14570 fn main() {
14571 let a = 1;
14572 if a == 3 {
14573 let b = 2;
14574 } else {
14575 let c = 3;
14576 }
14577 }"
14578 .unindent(),
14579 cx,
14580 )
14581 .await;
14582
14583 assert_indent_guides(
14584 0..8,
14585 vec![
14586 indent_guide(buffer_id, 1, 6, 0),
14587 indent_guide(buffer_id, 3, 3, 1),
14588 indent_guide(buffer_id, 5, 5, 1),
14589 ],
14590 None,
14591 &mut cx,
14592 );
14593}
14594
14595#[gpui::test]
14596async fn test_indent_guide_tab(cx: &mut TestAppContext) {
14597 let (buffer_id, mut cx) = setup_indent_guides_editor(
14598 &"
14599 fn main() {
14600 let a = 1;
14601 let b = 2;
14602 let c = 3;
14603 }"
14604 .unindent(),
14605 cx,
14606 )
14607 .await;
14608
14609 assert_indent_guides(
14610 0..5,
14611 vec![
14612 indent_guide(buffer_id, 1, 3, 0),
14613 indent_guide(buffer_id, 2, 2, 1),
14614 ],
14615 None,
14616 &mut cx,
14617 );
14618}
14619
14620#[gpui::test]
14621async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
14622 let (buffer_id, mut cx) = setup_indent_guides_editor(
14623 &"
14624 fn main() {
14625 let a = 1;
14626
14627 let c = 3;
14628 }"
14629 .unindent(),
14630 cx,
14631 )
14632 .await;
14633
14634 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
14635}
14636
14637#[gpui::test]
14638async fn test_indent_guide_complex(cx: &mut TestAppContext) {
14639 let (buffer_id, mut cx) = setup_indent_guides_editor(
14640 &"
14641 fn main() {
14642 let a = 1;
14643
14644 let c = 3;
14645
14646 if a == 3 {
14647 let b = 2;
14648 } else {
14649 let c = 3;
14650 }
14651 }"
14652 .unindent(),
14653 cx,
14654 )
14655 .await;
14656
14657 assert_indent_guides(
14658 0..11,
14659 vec![
14660 indent_guide(buffer_id, 1, 9, 0),
14661 indent_guide(buffer_id, 6, 6, 1),
14662 indent_guide(buffer_id, 8, 8, 1),
14663 ],
14664 None,
14665 &mut cx,
14666 );
14667}
14668
14669#[gpui::test]
14670async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
14671 let (buffer_id, mut cx) = setup_indent_guides_editor(
14672 &"
14673 fn main() {
14674 let a = 1;
14675
14676 let c = 3;
14677
14678 if a == 3 {
14679 let b = 2;
14680 } else {
14681 let c = 3;
14682 }
14683 }"
14684 .unindent(),
14685 cx,
14686 )
14687 .await;
14688
14689 assert_indent_guides(
14690 1..11,
14691 vec![
14692 indent_guide(buffer_id, 1, 9, 0),
14693 indent_guide(buffer_id, 6, 6, 1),
14694 indent_guide(buffer_id, 8, 8, 1),
14695 ],
14696 None,
14697 &mut cx,
14698 );
14699}
14700
14701#[gpui::test]
14702async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
14703 let (buffer_id, mut cx) = setup_indent_guides_editor(
14704 &"
14705 fn main() {
14706 let a = 1;
14707
14708 let c = 3;
14709
14710 if a == 3 {
14711 let b = 2;
14712 } else {
14713 let c = 3;
14714 }
14715 }"
14716 .unindent(),
14717 cx,
14718 )
14719 .await;
14720
14721 assert_indent_guides(
14722 1..10,
14723 vec![
14724 indent_guide(buffer_id, 1, 9, 0),
14725 indent_guide(buffer_id, 6, 6, 1),
14726 indent_guide(buffer_id, 8, 8, 1),
14727 ],
14728 None,
14729 &mut cx,
14730 );
14731}
14732
14733#[gpui::test]
14734async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
14735 let (buffer_id, mut cx) = setup_indent_guides_editor(
14736 &"
14737 block1
14738 block2
14739 block3
14740 block4
14741 block2
14742 block1
14743 block1"
14744 .unindent(),
14745 cx,
14746 )
14747 .await;
14748
14749 assert_indent_guides(
14750 1..10,
14751 vec![
14752 indent_guide(buffer_id, 1, 4, 0),
14753 indent_guide(buffer_id, 2, 3, 1),
14754 indent_guide(buffer_id, 3, 3, 2),
14755 ],
14756 None,
14757 &mut cx,
14758 );
14759}
14760
14761#[gpui::test]
14762async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
14763 let (buffer_id, mut cx) = setup_indent_guides_editor(
14764 &"
14765 block1
14766 block2
14767 block3
14768
14769 block1
14770 block1"
14771 .unindent(),
14772 cx,
14773 )
14774 .await;
14775
14776 assert_indent_guides(
14777 0..6,
14778 vec![
14779 indent_guide(buffer_id, 1, 2, 0),
14780 indent_guide(buffer_id, 2, 2, 1),
14781 ],
14782 None,
14783 &mut cx,
14784 );
14785}
14786
14787#[gpui::test]
14788async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
14789 let (buffer_id, mut cx) = setup_indent_guides_editor(
14790 &"
14791 block1
14792
14793
14794
14795 block2
14796 "
14797 .unindent(),
14798 cx,
14799 )
14800 .await;
14801
14802 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
14803}
14804
14805#[gpui::test]
14806async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
14807 let (buffer_id, mut cx) = setup_indent_guides_editor(
14808 &"
14809 def a:
14810 \tb = 3
14811 \tif True:
14812 \t\tc = 4
14813 \t\td = 5
14814 \tprint(b)
14815 "
14816 .unindent(),
14817 cx,
14818 )
14819 .await;
14820
14821 assert_indent_guides(
14822 0..6,
14823 vec![
14824 indent_guide(buffer_id, 1, 6, 0),
14825 indent_guide(buffer_id, 3, 4, 1),
14826 ],
14827 None,
14828 &mut cx,
14829 );
14830}
14831
14832#[gpui::test]
14833async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
14834 let (buffer_id, mut cx) = setup_indent_guides_editor(
14835 &"
14836 fn main() {
14837 let a = 1;
14838 }"
14839 .unindent(),
14840 cx,
14841 )
14842 .await;
14843
14844 cx.update_editor(|editor, window, cx| {
14845 editor.change_selections(None, window, cx, |s| {
14846 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14847 });
14848 });
14849
14850 assert_indent_guides(
14851 0..3,
14852 vec![indent_guide(buffer_id, 1, 1, 0)],
14853 Some(vec![0]),
14854 &mut cx,
14855 );
14856}
14857
14858#[gpui::test]
14859async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
14860 let (buffer_id, mut cx) = setup_indent_guides_editor(
14861 &"
14862 fn main() {
14863 if 1 == 2 {
14864 let a = 1;
14865 }
14866 }"
14867 .unindent(),
14868 cx,
14869 )
14870 .await;
14871
14872 cx.update_editor(|editor, window, cx| {
14873 editor.change_selections(None, window, cx, |s| {
14874 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14875 });
14876 });
14877
14878 assert_indent_guides(
14879 0..4,
14880 vec![
14881 indent_guide(buffer_id, 1, 3, 0),
14882 indent_guide(buffer_id, 2, 2, 1),
14883 ],
14884 Some(vec![1]),
14885 &mut cx,
14886 );
14887
14888 cx.update_editor(|editor, window, cx| {
14889 editor.change_selections(None, window, cx, |s| {
14890 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
14891 });
14892 });
14893
14894 assert_indent_guides(
14895 0..4,
14896 vec![
14897 indent_guide(buffer_id, 1, 3, 0),
14898 indent_guide(buffer_id, 2, 2, 1),
14899 ],
14900 Some(vec![1]),
14901 &mut cx,
14902 );
14903
14904 cx.update_editor(|editor, window, cx| {
14905 editor.change_selections(None, window, cx, |s| {
14906 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
14907 });
14908 });
14909
14910 assert_indent_guides(
14911 0..4,
14912 vec![
14913 indent_guide(buffer_id, 1, 3, 0),
14914 indent_guide(buffer_id, 2, 2, 1),
14915 ],
14916 Some(vec![0]),
14917 &mut cx,
14918 );
14919}
14920
14921#[gpui::test]
14922async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
14923 let (buffer_id, mut cx) = setup_indent_guides_editor(
14924 &"
14925 fn main() {
14926 let a = 1;
14927
14928 let b = 2;
14929 }"
14930 .unindent(),
14931 cx,
14932 )
14933 .await;
14934
14935 cx.update_editor(|editor, window, cx| {
14936 editor.change_selections(None, window, cx, |s| {
14937 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
14938 });
14939 });
14940
14941 assert_indent_guides(
14942 0..5,
14943 vec![indent_guide(buffer_id, 1, 3, 0)],
14944 Some(vec![0]),
14945 &mut cx,
14946 );
14947}
14948
14949#[gpui::test]
14950async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
14951 let (buffer_id, mut cx) = setup_indent_guides_editor(
14952 &"
14953 def m:
14954 a = 1
14955 pass"
14956 .unindent(),
14957 cx,
14958 )
14959 .await;
14960
14961 cx.update_editor(|editor, window, cx| {
14962 editor.change_selections(None, window, cx, |s| {
14963 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14964 });
14965 });
14966
14967 assert_indent_guides(
14968 0..3,
14969 vec![indent_guide(buffer_id, 1, 2, 0)],
14970 Some(vec![0]),
14971 &mut cx,
14972 );
14973}
14974
14975#[gpui::test]
14976async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
14977 init_test(cx, |_| {});
14978 let mut cx = EditorTestContext::new(cx).await;
14979 let text = indoc! {
14980 "
14981 impl A {
14982 fn b() {
14983 0;
14984 3;
14985 5;
14986 6;
14987 7;
14988 }
14989 }
14990 "
14991 };
14992 let base_text = indoc! {
14993 "
14994 impl A {
14995 fn b() {
14996 0;
14997 1;
14998 2;
14999 3;
15000 4;
15001 }
15002 fn c() {
15003 5;
15004 6;
15005 7;
15006 }
15007 }
15008 "
15009 };
15010
15011 cx.update_editor(|editor, window, cx| {
15012 editor.set_text(text, window, cx);
15013
15014 editor.buffer().update(cx, |multibuffer, cx| {
15015 let buffer = multibuffer.as_singleton().unwrap();
15016 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
15017
15018 multibuffer.set_all_diff_hunks_expanded(cx);
15019 multibuffer.add_diff(diff, cx);
15020
15021 buffer.read(cx).remote_id()
15022 })
15023 });
15024 cx.run_until_parked();
15025
15026 cx.assert_state_with_diff(
15027 indoc! { "
15028 impl A {
15029 fn b() {
15030 0;
15031 - 1;
15032 - 2;
15033 3;
15034 - 4;
15035 - }
15036 - fn c() {
15037 5;
15038 6;
15039 7;
15040 }
15041 }
15042 ˇ"
15043 }
15044 .to_string(),
15045 );
15046
15047 let mut actual_guides = cx.update_editor(|editor, window, cx| {
15048 editor
15049 .snapshot(window, cx)
15050 .buffer_snapshot
15051 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
15052 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
15053 .collect::<Vec<_>>()
15054 });
15055 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
15056 assert_eq!(
15057 actual_guides,
15058 vec![
15059 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
15060 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
15061 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
15062 ]
15063 );
15064}
15065
15066#[gpui::test]
15067async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15068 init_test(cx, |_| {});
15069 let mut cx = EditorTestContext::new(cx).await;
15070
15071 let diff_base = r#"
15072 a
15073 b
15074 c
15075 "#
15076 .unindent();
15077
15078 cx.set_state(
15079 &r#"
15080 ˇA
15081 b
15082 C
15083 "#
15084 .unindent(),
15085 );
15086 cx.set_head_text(&diff_base);
15087 cx.update_editor(|editor, window, cx| {
15088 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15089 });
15090 executor.run_until_parked();
15091
15092 let both_hunks_expanded = r#"
15093 - a
15094 + ˇA
15095 b
15096 - c
15097 + C
15098 "#
15099 .unindent();
15100
15101 cx.assert_state_with_diff(both_hunks_expanded.clone());
15102
15103 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15104 let snapshot = editor.snapshot(window, cx);
15105 let hunks = editor
15106 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15107 .collect::<Vec<_>>();
15108 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15109 let buffer_id = hunks[0].buffer_id;
15110 hunks
15111 .into_iter()
15112 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15113 .collect::<Vec<_>>()
15114 });
15115 assert_eq!(hunk_ranges.len(), 2);
15116
15117 cx.update_editor(|editor, _, cx| {
15118 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15119 });
15120 executor.run_until_parked();
15121
15122 let second_hunk_expanded = r#"
15123 ˇA
15124 b
15125 - c
15126 + C
15127 "#
15128 .unindent();
15129
15130 cx.assert_state_with_diff(second_hunk_expanded);
15131
15132 cx.update_editor(|editor, _, cx| {
15133 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15134 });
15135 executor.run_until_parked();
15136
15137 cx.assert_state_with_diff(both_hunks_expanded.clone());
15138
15139 cx.update_editor(|editor, _, cx| {
15140 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15141 });
15142 executor.run_until_parked();
15143
15144 let first_hunk_expanded = r#"
15145 - a
15146 + ˇA
15147 b
15148 C
15149 "#
15150 .unindent();
15151
15152 cx.assert_state_with_diff(first_hunk_expanded);
15153
15154 cx.update_editor(|editor, _, cx| {
15155 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15156 });
15157 executor.run_until_parked();
15158
15159 cx.assert_state_with_diff(both_hunks_expanded);
15160
15161 cx.set_state(
15162 &r#"
15163 ˇA
15164 b
15165 "#
15166 .unindent(),
15167 );
15168 cx.run_until_parked();
15169
15170 // TODO this cursor position seems bad
15171 cx.assert_state_with_diff(
15172 r#"
15173 - ˇa
15174 + A
15175 b
15176 "#
15177 .unindent(),
15178 );
15179
15180 cx.update_editor(|editor, window, cx| {
15181 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15182 });
15183
15184 cx.assert_state_with_diff(
15185 r#"
15186 - ˇa
15187 + A
15188 b
15189 - c
15190 "#
15191 .unindent(),
15192 );
15193
15194 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15195 let snapshot = editor.snapshot(window, cx);
15196 let hunks = editor
15197 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15198 .collect::<Vec<_>>();
15199 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15200 let buffer_id = hunks[0].buffer_id;
15201 hunks
15202 .into_iter()
15203 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15204 .collect::<Vec<_>>()
15205 });
15206 assert_eq!(hunk_ranges.len(), 2);
15207
15208 cx.update_editor(|editor, _, cx| {
15209 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15210 });
15211 executor.run_until_parked();
15212
15213 cx.assert_state_with_diff(
15214 r#"
15215 - ˇa
15216 + A
15217 b
15218 "#
15219 .unindent(),
15220 );
15221}
15222
15223#[gpui::test]
15224async fn test_toggle_deletion_hunk_at_start_of_file(
15225 executor: BackgroundExecutor,
15226 cx: &mut TestAppContext,
15227) {
15228 init_test(cx, |_| {});
15229 let mut cx = EditorTestContext::new(cx).await;
15230
15231 let diff_base = r#"
15232 a
15233 b
15234 c
15235 "#
15236 .unindent();
15237
15238 cx.set_state(
15239 &r#"
15240 ˇb
15241 c
15242 "#
15243 .unindent(),
15244 );
15245 cx.set_head_text(&diff_base);
15246 cx.update_editor(|editor, window, cx| {
15247 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15248 });
15249 executor.run_until_parked();
15250
15251 let hunk_expanded = r#"
15252 - a
15253 ˇb
15254 c
15255 "#
15256 .unindent();
15257
15258 cx.assert_state_with_diff(hunk_expanded.clone());
15259
15260 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15261 let snapshot = editor.snapshot(window, cx);
15262 let hunks = editor
15263 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15264 .collect::<Vec<_>>();
15265 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15266 let buffer_id = hunks[0].buffer_id;
15267 hunks
15268 .into_iter()
15269 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15270 .collect::<Vec<_>>()
15271 });
15272 assert_eq!(hunk_ranges.len(), 1);
15273
15274 cx.update_editor(|editor, _, cx| {
15275 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15276 });
15277 executor.run_until_parked();
15278
15279 let hunk_collapsed = r#"
15280 ˇb
15281 c
15282 "#
15283 .unindent();
15284
15285 cx.assert_state_with_diff(hunk_collapsed);
15286
15287 cx.update_editor(|editor, _, cx| {
15288 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15289 });
15290 executor.run_until_parked();
15291
15292 cx.assert_state_with_diff(hunk_expanded.clone());
15293}
15294
15295#[gpui::test]
15296async fn test_display_diff_hunks(cx: &mut TestAppContext) {
15297 init_test(cx, |_| {});
15298
15299 let fs = FakeFs::new(cx.executor());
15300 fs.insert_tree(
15301 path!("/test"),
15302 json!({
15303 ".git": {},
15304 "file-1": "ONE\n",
15305 "file-2": "TWO\n",
15306 "file-3": "THREE\n",
15307 }),
15308 )
15309 .await;
15310
15311 fs.set_head_for_repo(
15312 path!("/test/.git").as_ref(),
15313 &[
15314 ("file-1".into(), "one\n".into()),
15315 ("file-2".into(), "two\n".into()),
15316 ("file-3".into(), "three\n".into()),
15317 ],
15318 );
15319
15320 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
15321 let mut buffers = vec![];
15322 for i in 1..=3 {
15323 let buffer = project
15324 .update(cx, |project, cx| {
15325 let path = format!(path!("/test/file-{}"), i);
15326 project.open_local_buffer(path, cx)
15327 })
15328 .await
15329 .unwrap();
15330 buffers.push(buffer);
15331 }
15332
15333 let multibuffer = cx.new(|cx| {
15334 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
15335 multibuffer.set_all_diff_hunks_expanded(cx);
15336 for buffer in &buffers {
15337 let snapshot = buffer.read(cx).snapshot();
15338 multibuffer.set_excerpts_for_path(
15339 PathKey::namespaced("", buffer.read(cx).file().unwrap().path().clone()),
15340 buffer.clone(),
15341 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
15342 DEFAULT_MULTIBUFFER_CONTEXT,
15343 cx,
15344 );
15345 }
15346 multibuffer
15347 });
15348
15349 let editor = cx.add_window(|window, cx| {
15350 Editor::new(
15351 EditorMode::Full,
15352 multibuffer,
15353 Some(project),
15354 true,
15355 window,
15356 cx,
15357 )
15358 });
15359 cx.run_until_parked();
15360
15361 let snapshot = editor
15362 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
15363 .unwrap();
15364 let hunks = snapshot
15365 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
15366 .map(|hunk| match hunk {
15367 DisplayDiffHunk::Unfolded {
15368 display_row_range, ..
15369 } => display_row_range,
15370 DisplayDiffHunk::Folded { .. } => unreachable!(),
15371 })
15372 .collect::<Vec<_>>();
15373 assert_eq!(
15374 hunks,
15375 [
15376 DisplayRow(3)..DisplayRow(5),
15377 DisplayRow(10)..DisplayRow(12),
15378 DisplayRow(17)..DisplayRow(19),
15379 ]
15380 );
15381}
15382
15383#[gpui::test]
15384async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
15385 init_test(cx, |_| {});
15386
15387 let mut cx = EditorTestContext::new(cx).await;
15388 cx.set_head_text(indoc! { "
15389 one
15390 two
15391 three
15392 four
15393 five
15394 "
15395 });
15396 cx.set_index_text(indoc! { "
15397 one
15398 two
15399 three
15400 four
15401 five
15402 "
15403 });
15404 cx.set_state(indoc! {"
15405 one
15406 TWO
15407 ˇTHREE
15408 FOUR
15409 five
15410 "});
15411 cx.run_until_parked();
15412 cx.update_editor(|editor, window, cx| {
15413 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15414 });
15415 cx.run_until_parked();
15416 cx.assert_index_text(Some(indoc! {"
15417 one
15418 TWO
15419 THREE
15420 FOUR
15421 five
15422 "}));
15423 cx.set_state(indoc! { "
15424 one
15425 TWO
15426 ˇTHREE-HUNDRED
15427 FOUR
15428 five
15429 "});
15430 cx.run_until_parked();
15431 cx.update_editor(|editor, window, cx| {
15432 let snapshot = editor.snapshot(window, cx);
15433 let hunks = editor
15434 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15435 .collect::<Vec<_>>();
15436 assert_eq!(hunks.len(), 1);
15437 assert_eq!(
15438 hunks[0].status(),
15439 DiffHunkStatus {
15440 kind: DiffHunkStatusKind::Modified,
15441 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
15442 }
15443 );
15444
15445 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15446 });
15447 cx.run_until_parked();
15448 cx.assert_index_text(Some(indoc! {"
15449 one
15450 TWO
15451 THREE-HUNDRED
15452 FOUR
15453 five
15454 "}));
15455}
15456
15457#[gpui::test]
15458fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
15459 init_test(cx, |_| {});
15460
15461 let editor = cx.add_window(|window, cx| {
15462 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
15463 build_editor(buffer, window, cx)
15464 });
15465
15466 let render_args = Arc::new(Mutex::new(None));
15467 let snapshot = editor
15468 .update(cx, |editor, window, cx| {
15469 let snapshot = editor.buffer().read(cx).snapshot(cx);
15470 let range =
15471 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
15472
15473 struct RenderArgs {
15474 row: MultiBufferRow,
15475 folded: bool,
15476 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
15477 }
15478
15479 let crease = Crease::inline(
15480 range,
15481 FoldPlaceholder::test(),
15482 {
15483 let toggle_callback = render_args.clone();
15484 move |row, folded, callback, _window, _cx| {
15485 *toggle_callback.lock() = Some(RenderArgs {
15486 row,
15487 folded,
15488 callback,
15489 });
15490 div()
15491 }
15492 },
15493 |_row, _folded, _window, _cx| div(),
15494 );
15495
15496 editor.insert_creases(Some(crease), cx);
15497 let snapshot = editor.snapshot(window, cx);
15498 let _div = snapshot.render_crease_toggle(
15499 MultiBufferRow(1),
15500 false,
15501 cx.entity().clone(),
15502 window,
15503 cx,
15504 );
15505 snapshot
15506 })
15507 .unwrap();
15508
15509 let render_args = render_args.lock().take().unwrap();
15510 assert_eq!(render_args.row, MultiBufferRow(1));
15511 assert!(!render_args.folded);
15512 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
15513
15514 cx.update_window(*editor, |_, window, cx| {
15515 (render_args.callback)(true, window, cx)
15516 })
15517 .unwrap();
15518 let snapshot = editor
15519 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
15520 .unwrap();
15521 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
15522
15523 cx.update_window(*editor, |_, window, cx| {
15524 (render_args.callback)(false, window, cx)
15525 })
15526 .unwrap();
15527 let snapshot = editor
15528 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
15529 .unwrap();
15530 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
15531}
15532
15533#[gpui::test]
15534async fn test_input_text(cx: &mut TestAppContext) {
15535 init_test(cx, |_| {});
15536 let mut cx = EditorTestContext::new(cx).await;
15537
15538 cx.set_state(
15539 &r#"ˇone
15540 two
15541
15542 three
15543 fourˇ
15544 five
15545
15546 siˇx"#
15547 .unindent(),
15548 );
15549
15550 cx.dispatch_action(HandleInput(String::new()));
15551 cx.assert_editor_state(
15552 &r#"ˇone
15553 two
15554
15555 three
15556 fourˇ
15557 five
15558
15559 siˇx"#
15560 .unindent(),
15561 );
15562
15563 cx.dispatch_action(HandleInput("AAAA".to_string()));
15564 cx.assert_editor_state(
15565 &r#"AAAAˇone
15566 two
15567
15568 three
15569 fourAAAAˇ
15570 five
15571
15572 siAAAAˇx"#
15573 .unindent(),
15574 );
15575}
15576
15577#[gpui::test]
15578async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
15579 init_test(cx, |_| {});
15580
15581 let mut cx = EditorTestContext::new(cx).await;
15582 cx.set_state(
15583 r#"let foo = 1;
15584let foo = 2;
15585let foo = 3;
15586let fooˇ = 4;
15587let foo = 5;
15588let foo = 6;
15589let foo = 7;
15590let foo = 8;
15591let foo = 9;
15592let foo = 10;
15593let foo = 11;
15594let foo = 12;
15595let foo = 13;
15596let foo = 14;
15597let foo = 15;"#,
15598 );
15599
15600 cx.update_editor(|e, window, cx| {
15601 assert_eq!(
15602 e.next_scroll_position,
15603 NextScrollCursorCenterTopBottom::Center,
15604 "Default next scroll direction is center",
15605 );
15606
15607 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
15608 assert_eq!(
15609 e.next_scroll_position,
15610 NextScrollCursorCenterTopBottom::Top,
15611 "After center, next scroll direction should be top",
15612 );
15613
15614 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
15615 assert_eq!(
15616 e.next_scroll_position,
15617 NextScrollCursorCenterTopBottom::Bottom,
15618 "After top, next scroll direction should be bottom",
15619 );
15620
15621 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
15622 assert_eq!(
15623 e.next_scroll_position,
15624 NextScrollCursorCenterTopBottom::Center,
15625 "After bottom, scrolling should start over",
15626 );
15627
15628 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
15629 assert_eq!(
15630 e.next_scroll_position,
15631 NextScrollCursorCenterTopBottom::Top,
15632 "Scrolling continues if retriggered fast enough"
15633 );
15634 });
15635
15636 cx.executor()
15637 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
15638 cx.executor().run_until_parked();
15639 cx.update_editor(|e, _, _| {
15640 assert_eq!(
15641 e.next_scroll_position,
15642 NextScrollCursorCenterTopBottom::Center,
15643 "If scrolling is not triggered fast enough, it should reset"
15644 );
15645 });
15646}
15647
15648#[gpui::test]
15649async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
15650 init_test(cx, |_| {});
15651 let mut cx = EditorLspTestContext::new_rust(
15652 lsp::ServerCapabilities {
15653 definition_provider: Some(lsp::OneOf::Left(true)),
15654 references_provider: Some(lsp::OneOf::Left(true)),
15655 ..lsp::ServerCapabilities::default()
15656 },
15657 cx,
15658 )
15659 .await;
15660
15661 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
15662 let go_to_definition = cx.lsp.handle_request::<lsp::request::GotoDefinition, _, _>(
15663 move |params, _| async move {
15664 if empty_go_to_definition {
15665 Ok(None)
15666 } else {
15667 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
15668 uri: params.text_document_position_params.text_document.uri,
15669 range: lsp::Range::new(lsp::Position::new(4, 3), lsp::Position::new(4, 6)),
15670 })))
15671 }
15672 },
15673 );
15674 let references =
15675 cx.lsp
15676 .handle_request::<lsp::request::References, _, _>(move |params, _| async move {
15677 Ok(Some(vec![lsp::Location {
15678 uri: params.text_document_position.text_document.uri,
15679 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
15680 }]))
15681 });
15682 (go_to_definition, references)
15683 };
15684
15685 cx.set_state(
15686 &r#"fn one() {
15687 let mut a = ˇtwo();
15688 }
15689
15690 fn two() {}"#
15691 .unindent(),
15692 );
15693 set_up_lsp_handlers(false, &mut cx);
15694 let navigated = cx
15695 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
15696 .await
15697 .expect("Failed to navigate to definition");
15698 assert_eq!(
15699 navigated,
15700 Navigated::Yes,
15701 "Should have navigated to definition from the GetDefinition response"
15702 );
15703 cx.assert_editor_state(
15704 &r#"fn one() {
15705 let mut a = two();
15706 }
15707
15708 fn «twoˇ»() {}"#
15709 .unindent(),
15710 );
15711
15712 let editors = cx.update_workspace(|workspace, _, cx| {
15713 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
15714 });
15715 cx.update_editor(|_, _, test_editor_cx| {
15716 assert_eq!(
15717 editors.len(),
15718 1,
15719 "Initially, only one, test, editor should be open in the workspace"
15720 );
15721 assert_eq!(
15722 test_editor_cx.entity(),
15723 editors.last().expect("Asserted len is 1").clone()
15724 );
15725 });
15726
15727 set_up_lsp_handlers(true, &mut cx);
15728 let navigated = cx
15729 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
15730 .await
15731 .expect("Failed to navigate to lookup references");
15732 assert_eq!(
15733 navigated,
15734 Navigated::Yes,
15735 "Should have navigated to references as a fallback after empty GoToDefinition response"
15736 );
15737 // We should not change the selections in the existing file,
15738 // if opening another milti buffer with the references
15739 cx.assert_editor_state(
15740 &r#"fn one() {
15741 let mut a = two();
15742 }
15743
15744 fn «twoˇ»() {}"#
15745 .unindent(),
15746 );
15747 let editors = cx.update_workspace(|workspace, _, cx| {
15748 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
15749 });
15750 cx.update_editor(|_, _, test_editor_cx| {
15751 assert_eq!(
15752 editors.len(),
15753 2,
15754 "After falling back to references search, we open a new editor with the results"
15755 );
15756 let references_fallback_text = editors
15757 .into_iter()
15758 .find(|new_editor| *new_editor != test_editor_cx.entity())
15759 .expect("Should have one non-test editor now")
15760 .read(test_editor_cx)
15761 .text(test_editor_cx);
15762 assert_eq!(
15763 references_fallback_text, "fn one() {\n let mut a = two();\n}",
15764 "Should use the range from the references response and not the GoToDefinition one"
15765 );
15766 });
15767}
15768
15769#[gpui::test]
15770async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
15771 init_test(cx, |_| {});
15772
15773 let language = Arc::new(Language::new(
15774 LanguageConfig::default(),
15775 Some(tree_sitter_rust::LANGUAGE.into()),
15776 ));
15777
15778 let text = r#"
15779 #[cfg(test)]
15780 mod tests() {
15781 #[test]
15782 fn runnable_1() {
15783 let a = 1;
15784 }
15785
15786 #[test]
15787 fn runnable_2() {
15788 let a = 1;
15789 let b = 2;
15790 }
15791 }
15792 "#
15793 .unindent();
15794
15795 let fs = FakeFs::new(cx.executor());
15796 fs.insert_file("/file.rs", Default::default()).await;
15797
15798 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15799 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15800 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15801 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
15802 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
15803
15804 let editor = cx.new_window_entity(|window, cx| {
15805 Editor::new(
15806 EditorMode::Full,
15807 multi_buffer,
15808 Some(project.clone()),
15809 true,
15810 window,
15811 cx,
15812 )
15813 });
15814
15815 editor.update_in(cx, |editor, window, cx| {
15816 let snapshot = editor.buffer().read(cx).snapshot(cx);
15817 editor.tasks.insert(
15818 (buffer.read(cx).remote_id(), 3),
15819 RunnableTasks {
15820 templates: vec![],
15821 offset: snapshot.anchor_before(43),
15822 column: 0,
15823 extra_variables: HashMap::default(),
15824 context_range: BufferOffset(43)..BufferOffset(85),
15825 },
15826 );
15827 editor.tasks.insert(
15828 (buffer.read(cx).remote_id(), 8),
15829 RunnableTasks {
15830 templates: vec![],
15831 offset: snapshot.anchor_before(86),
15832 column: 0,
15833 extra_variables: HashMap::default(),
15834 context_range: BufferOffset(86)..BufferOffset(191),
15835 },
15836 );
15837
15838 // Test finding task when cursor is inside function body
15839 editor.change_selections(None, window, cx, |s| {
15840 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
15841 });
15842 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
15843 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
15844
15845 // Test finding task when cursor is on function name
15846 editor.change_selections(None, window, cx, |s| {
15847 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
15848 });
15849 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
15850 assert_eq!(row, 8, "Should find task when cursor is on function name");
15851 });
15852}
15853
15854#[gpui::test]
15855async fn test_folding_buffers(cx: &mut TestAppContext) {
15856 init_test(cx, |_| {});
15857
15858 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
15859 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
15860 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
15861
15862 let fs = FakeFs::new(cx.executor());
15863 fs.insert_tree(
15864 path!("/a"),
15865 json!({
15866 "first.rs": sample_text_1,
15867 "second.rs": sample_text_2,
15868 "third.rs": sample_text_3,
15869 }),
15870 )
15871 .await;
15872 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15873 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15874 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15875 let worktree = project.update(cx, |project, cx| {
15876 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15877 assert_eq!(worktrees.len(), 1);
15878 worktrees.pop().unwrap()
15879 });
15880 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15881
15882 let buffer_1 = project
15883 .update(cx, |project, cx| {
15884 project.open_buffer((worktree_id, "first.rs"), cx)
15885 })
15886 .await
15887 .unwrap();
15888 let buffer_2 = project
15889 .update(cx, |project, cx| {
15890 project.open_buffer((worktree_id, "second.rs"), cx)
15891 })
15892 .await
15893 .unwrap();
15894 let buffer_3 = project
15895 .update(cx, |project, cx| {
15896 project.open_buffer((worktree_id, "third.rs"), cx)
15897 })
15898 .await
15899 .unwrap();
15900
15901 let multi_buffer = cx.new(|cx| {
15902 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15903 multi_buffer.push_excerpts(
15904 buffer_1.clone(),
15905 [
15906 ExcerptRange {
15907 context: Point::new(0, 0)..Point::new(3, 0),
15908 primary: None,
15909 },
15910 ExcerptRange {
15911 context: Point::new(5, 0)..Point::new(7, 0),
15912 primary: None,
15913 },
15914 ExcerptRange {
15915 context: Point::new(9, 0)..Point::new(10, 4),
15916 primary: None,
15917 },
15918 ],
15919 cx,
15920 );
15921 multi_buffer.push_excerpts(
15922 buffer_2.clone(),
15923 [
15924 ExcerptRange {
15925 context: Point::new(0, 0)..Point::new(3, 0),
15926 primary: None,
15927 },
15928 ExcerptRange {
15929 context: Point::new(5, 0)..Point::new(7, 0),
15930 primary: None,
15931 },
15932 ExcerptRange {
15933 context: Point::new(9, 0)..Point::new(10, 4),
15934 primary: None,
15935 },
15936 ],
15937 cx,
15938 );
15939 multi_buffer.push_excerpts(
15940 buffer_3.clone(),
15941 [
15942 ExcerptRange {
15943 context: Point::new(0, 0)..Point::new(3, 0),
15944 primary: None,
15945 },
15946 ExcerptRange {
15947 context: Point::new(5, 0)..Point::new(7, 0),
15948 primary: None,
15949 },
15950 ExcerptRange {
15951 context: Point::new(9, 0)..Point::new(10, 4),
15952 primary: None,
15953 },
15954 ],
15955 cx,
15956 );
15957 multi_buffer
15958 });
15959 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15960 Editor::new(
15961 EditorMode::Full,
15962 multi_buffer.clone(),
15963 Some(project.clone()),
15964 true,
15965 window,
15966 cx,
15967 )
15968 });
15969
15970 assert_eq!(
15971 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15972 "\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",
15973 );
15974
15975 multi_buffer_editor.update(cx, |editor, cx| {
15976 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
15977 });
15978 assert_eq!(
15979 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15980 "\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",
15981 "After folding the first buffer, its text should not be displayed"
15982 );
15983
15984 multi_buffer_editor.update(cx, |editor, cx| {
15985 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
15986 });
15987 assert_eq!(
15988 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15989 "\n\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
15990 "After folding the second buffer, its text should not be displayed"
15991 );
15992
15993 multi_buffer_editor.update(cx, |editor, cx| {
15994 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
15995 });
15996 assert_eq!(
15997 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15998 "\n\n\n\n\n",
15999 "After folding the third buffer, its text should not be displayed"
16000 );
16001
16002 // Emulate selection inside the fold logic, that should work
16003 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16004 editor
16005 .snapshot(window, cx)
16006 .next_line_boundary(Point::new(0, 4));
16007 });
16008
16009 multi_buffer_editor.update(cx, |editor, cx| {
16010 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
16011 });
16012 assert_eq!(
16013 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16014 "\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
16015 "After unfolding the second buffer, its text should be displayed"
16016 );
16017
16018 // Typing inside of buffer 1 causes that buffer to be unfolded.
16019 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16020 assert_eq!(
16021 multi_buffer
16022 .read(cx)
16023 .snapshot(cx)
16024 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
16025 .collect::<String>(),
16026 "bbbb"
16027 );
16028 editor.change_selections(None, window, cx, |selections| {
16029 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
16030 });
16031 editor.handle_input("B", window, cx);
16032 });
16033
16034 assert_eq!(
16035 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16036 "\n\n\nB\n\n\n\n\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
16037 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
16038 );
16039
16040 multi_buffer_editor.update(cx, |editor, cx| {
16041 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
16042 });
16043 assert_eq!(
16044 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16045 "\n\n\nB\n\n\n\n\n\n\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",
16046 "After unfolding the all buffers, all original text should be displayed"
16047 );
16048}
16049
16050#[gpui::test]
16051async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
16052 init_test(cx, |_| {});
16053
16054 let sample_text_1 = "1111\n2222\n3333".to_string();
16055 let sample_text_2 = "4444\n5555\n6666".to_string();
16056 let sample_text_3 = "7777\n8888\n9999".to_string();
16057
16058 let fs = FakeFs::new(cx.executor());
16059 fs.insert_tree(
16060 path!("/a"),
16061 json!({
16062 "first.rs": sample_text_1,
16063 "second.rs": sample_text_2,
16064 "third.rs": sample_text_3,
16065 }),
16066 )
16067 .await;
16068 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16069 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16070 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16071 let worktree = project.update(cx, |project, cx| {
16072 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16073 assert_eq!(worktrees.len(), 1);
16074 worktrees.pop().unwrap()
16075 });
16076 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16077
16078 let buffer_1 = project
16079 .update(cx, |project, cx| {
16080 project.open_buffer((worktree_id, "first.rs"), cx)
16081 })
16082 .await
16083 .unwrap();
16084 let buffer_2 = project
16085 .update(cx, |project, cx| {
16086 project.open_buffer((worktree_id, "second.rs"), cx)
16087 })
16088 .await
16089 .unwrap();
16090 let buffer_3 = project
16091 .update(cx, |project, cx| {
16092 project.open_buffer((worktree_id, "third.rs"), cx)
16093 })
16094 .await
16095 .unwrap();
16096
16097 let multi_buffer = cx.new(|cx| {
16098 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16099 multi_buffer.push_excerpts(
16100 buffer_1.clone(),
16101 [ExcerptRange {
16102 context: Point::new(0, 0)..Point::new(3, 0),
16103 primary: None,
16104 }],
16105 cx,
16106 );
16107 multi_buffer.push_excerpts(
16108 buffer_2.clone(),
16109 [ExcerptRange {
16110 context: Point::new(0, 0)..Point::new(3, 0),
16111 primary: None,
16112 }],
16113 cx,
16114 );
16115 multi_buffer.push_excerpts(
16116 buffer_3.clone(),
16117 [ExcerptRange {
16118 context: Point::new(0, 0)..Point::new(3, 0),
16119 primary: None,
16120 }],
16121 cx,
16122 );
16123 multi_buffer
16124 });
16125
16126 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16127 Editor::new(
16128 EditorMode::Full,
16129 multi_buffer,
16130 Some(project.clone()),
16131 true,
16132 window,
16133 cx,
16134 )
16135 });
16136
16137 let full_text = "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n";
16138 assert_eq!(
16139 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16140 full_text,
16141 );
16142
16143 multi_buffer_editor.update(cx, |editor, cx| {
16144 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
16145 });
16146 assert_eq!(
16147 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16148 "\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n",
16149 "After folding the first buffer, its text should not be displayed"
16150 );
16151
16152 multi_buffer_editor.update(cx, |editor, cx| {
16153 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
16154 });
16155
16156 assert_eq!(
16157 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16158 "\n\n\n\n\n\n\n7777\n8888\n9999\n",
16159 "After folding the second buffer, its text should not be displayed"
16160 );
16161
16162 multi_buffer_editor.update(cx, |editor, cx| {
16163 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
16164 });
16165 assert_eq!(
16166 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16167 "\n\n\n\n\n",
16168 "After folding the third buffer, its text should not be displayed"
16169 );
16170
16171 multi_buffer_editor.update(cx, |editor, cx| {
16172 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
16173 });
16174 assert_eq!(
16175 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16176 "\n\n\n\n\n4444\n5555\n6666\n\n\n",
16177 "After unfolding the second buffer, its text should be displayed"
16178 );
16179
16180 multi_buffer_editor.update(cx, |editor, cx| {
16181 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
16182 });
16183 assert_eq!(
16184 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16185 "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n",
16186 "After unfolding the first buffer, its text should be displayed"
16187 );
16188
16189 multi_buffer_editor.update(cx, |editor, cx| {
16190 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
16191 });
16192 assert_eq!(
16193 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16194 full_text,
16195 "After unfolding all buffers, all original text should be displayed"
16196 );
16197}
16198
16199#[gpui::test]
16200async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
16201 init_test(cx, |_| {});
16202
16203 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
16204
16205 let fs = FakeFs::new(cx.executor());
16206 fs.insert_tree(
16207 path!("/a"),
16208 json!({
16209 "main.rs": sample_text,
16210 }),
16211 )
16212 .await;
16213 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16214 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16215 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16216 let worktree = project.update(cx, |project, cx| {
16217 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16218 assert_eq!(worktrees.len(), 1);
16219 worktrees.pop().unwrap()
16220 });
16221 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16222
16223 let buffer_1 = project
16224 .update(cx, |project, cx| {
16225 project.open_buffer((worktree_id, "main.rs"), cx)
16226 })
16227 .await
16228 .unwrap();
16229
16230 let multi_buffer = cx.new(|cx| {
16231 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16232 multi_buffer.push_excerpts(
16233 buffer_1.clone(),
16234 [ExcerptRange {
16235 context: Point::new(0, 0)
16236 ..Point::new(
16237 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
16238 0,
16239 ),
16240 primary: None,
16241 }],
16242 cx,
16243 );
16244 multi_buffer
16245 });
16246 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16247 Editor::new(
16248 EditorMode::Full,
16249 multi_buffer,
16250 Some(project.clone()),
16251 true,
16252 window,
16253 cx,
16254 )
16255 });
16256
16257 let selection_range = Point::new(1, 0)..Point::new(2, 0);
16258 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16259 enum TestHighlight {}
16260 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
16261 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
16262 editor.highlight_text::<TestHighlight>(
16263 vec![highlight_range.clone()],
16264 HighlightStyle::color(Hsla::green()),
16265 cx,
16266 );
16267 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
16268 });
16269
16270 let full_text = format!("\n\n\n{sample_text}\n");
16271 assert_eq!(
16272 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16273 full_text,
16274 );
16275}
16276
16277#[gpui::test]
16278async fn test_inline_completion_text(cx: &mut TestAppContext) {
16279 init_test(cx, |_| {});
16280
16281 // Simple insertion
16282 assert_highlighted_edits(
16283 "Hello, world!",
16284 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
16285 true,
16286 cx,
16287 |highlighted_edits, cx| {
16288 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
16289 assert_eq!(highlighted_edits.highlights.len(), 1);
16290 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
16291 assert_eq!(
16292 highlighted_edits.highlights[0].1.background_color,
16293 Some(cx.theme().status().created_background)
16294 );
16295 },
16296 )
16297 .await;
16298
16299 // Replacement
16300 assert_highlighted_edits(
16301 "This is a test.",
16302 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
16303 false,
16304 cx,
16305 |highlighted_edits, cx| {
16306 assert_eq!(highlighted_edits.text, "That is a test.");
16307 assert_eq!(highlighted_edits.highlights.len(), 1);
16308 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
16309 assert_eq!(
16310 highlighted_edits.highlights[0].1.background_color,
16311 Some(cx.theme().status().created_background)
16312 );
16313 },
16314 )
16315 .await;
16316
16317 // Multiple edits
16318 assert_highlighted_edits(
16319 "Hello, world!",
16320 vec![
16321 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
16322 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
16323 ],
16324 false,
16325 cx,
16326 |highlighted_edits, cx| {
16327 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
16328 assert_eq!(highlighted_edits.highlights.len(), 2);
16329 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
16330 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
16331 assert_eq!(
16332 highlighted_edits.highlights[0].1.background_color,
16333 Some(cx.theme().status().created_background)
16334 );
16335 assert_eq!(
16336 highlighted_edits.highlights[1].1.background_color,
16337 Some(cx.theme().status().created_background)
16338 );
16339 },
16340 )
16341 .await;
16342
16343 // Multiple lines with edits
16344 assert_highlighted_edits(
16345 "First line\nSecond line\nThird line\nFourth line",
16346 vec![
16347 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
16348 (
16349 Point::new(2, 0)..Point::new(2, 10),
16350 "New third line".to_string(),
16351 ),
16352 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
16353 ],
16354 false,
16355 cx,
16356 |highlighted_edits, cx| {
16357 assert_eq!(
16358 highlighted_edits.text,
16359 "Second modified\nNew third line\nFourth updated line"
16360 );
16361 assert_eq!(highlighted_edits.highlights.len(), 3);
16362 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
16363 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
16364 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
16365 for highlight in &highlighted_edits.highlights {
16366 assert_eq!(
16367 highlight.1.background_color,
16368 Some(cx.theme().status().created_background)
16369 );
16370 }
16371 },
16372 )
16373 .await;
16374}
16375
16376#[gpui::test]
16377async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
16378 init_test(cx, |_| {});
16379
16380 // Deletion
16381 assert_highlighted_edits(
16382 "Hello, world!",
16383 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
16384 true,
16385 cx,
16386 |highlighted_edits, cx| {
16387 assert_eq!(highlighted_edits.text, "Hello, world!");
16388 assert_eq!(highlighted_edits.highlights.len(), 1);
16389 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
16390 assert_eq!(
16391 highlighted_edits.highlights[0].1.background_color,
16392 Some(cx.theme().status().deleted_background)
16393 );
16394 },
16395 )
16396 .await;
16397
16398 // Insertion
16399 assert_highlighted_edits(
16400 "Hello, world!",
16401 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
16402 true,
16403 cx,
16404 |highlighted_edits, cx| {
16405 assert_eq!(highlighted_edits.highlights.len(), 1);
16406 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
16407 assert_eq!(
16408 highlighted_edits.highlights[0].1.background_color,
16409 Some(cx.theme().status().created_background)
16410 );
16411 },
16412 )
16413 .await;
16414}
16415
16416async fn assert_highlighted_edits(
16417 text: &str,
16418 edits: Vec<(Range<Point>, String)>,
16419 include_deletions: bool,
16420 cx: &mut TestAppContext,
16421 assertion_fn: impl Fn(HighlightedText, &App),
16422) {
16423 let window = cx.add_window(|window, cx| {
16424 let buffer = MultiBuffer::build_simple(text, cx);
16425 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
16426 });
16427 let cx = &mut VisualTestContext::from_window(*window, cx);
16428
16429 let (buffer, snapshot) = window
16430 .update(cx, |editor, _window, cx| {
16431 (
16432 editor.buffer().clone(),
16433 editor.buffer().read(cx).snapshot(cx),
16434 )
16435 })
16436 .unwrap();
16437
16438 let edits = edits
16439 .into_iter()
16440 .map(|(range, edit)| {
16441 (
16442 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
16443 edit,
16444 )
16445 })
16446 .collect::<Vec<_>>();
16447
16448 let text_anchor_edits = edits
16449 .clone()
16450 .into_iter()
16451 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
16452 .collect::<Vec<_>>();
16453
16454 let edit_preview = window
16455 .update(cx, |_, _window, cx| {
16456 buffer
16457 .read(cx)
16458 .as_singleton()
16459 .unwrap()
16460 .read(cx)
16461 .preview_edits(text_anchor_edits.into(), cx)
16462 })
16463 .unwrap()
16464 .await;
16465
16466 cx.update(|_window, cx| {
16467 let highlighted_edits = inline_completion_edit_text(
16468 &snapshot.as_singleton().unwrap().2,
16469 &edits,
16470 &edit_preview,
16471 include_deletions,
16472 cx,
16473 );
16474 assertion_fn(highlighted_edits, cx)
16475 });
16476}
16477
16478#[gpui::test]
16479async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
16480 init_test(cx, |_| {});
16481 let capabilities = lsp::ServerCapabilities {
16482 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
16483 prepare_provider: Some(true),
16484 work_done_progress_options: Default::default(),
16485 })),
16486 ..Default::default()
16487 };
16488 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
16489
16490 cx.set_state(indoc! {"
16491 struct Fˇoo {}
16492 "});
16493
16494 cx.update_editor(|editor, _, cx| {
16495 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
16496 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
16497 editor.highlight_background::<DocumentHighlightRead>(
16498 &[highlight_range],
16499 |c| c.editor_document_highlight_read_background,
16500 cx,
16501 );
16502 });
16503
16504 let mut prepare_rename_handler =
16505 cx.handle_request::<lsp::request::PrepareRenameRequest, _, _>(move |_, _, _| async move {
16506 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
16507 start: lsp::Position {
16508 line: 0,
16509 character: 7,
16510 },
16511 end: lsp::Position {
16512 line: 0,
16513 character: 10,
16514 },
16515 })))
16516 });
16517 let prepare_rename_task = cx
16518 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
16519 .expect("Prepare rename was not started");
16520 prepare_rename_handler.next().await.unwrap();
16521 prepare_rename_task.await.expect("Prepare rename failed");
16522
16523 let mut rename_handler =
16524 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
16525 let edit = lsp::TextEdit {
16526 range: lsp::Range {
16527 start: lsp::Position {
16528 line: 0,
16529 character: 7,
16530 },
16531 end: lsp::Position {
16532 line: 0,
16533 character: 10,
16534 },
16535 },
16536 new_text: "FooRenamed".to_string(),
16537 };
16538 Ok(Some(lsp::WorkspaceEdit::new(
16539 // Specify the same edit twice
16540 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
16541 )))
16542 });
16543 let rename_task = cx
16544 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
16545 .expect("Confirm rename was not started");
16546 rename_handler.next().await.unwrap();
16547 rename_task.await.expect("Confirm rename failed");
16548 cx.run_until_parked();
16549
16550 // Despite two edits, only one is actually applied as those are identical
16551 cx.assert_editor_state(indoc! {"
16552 struct FooRenamedˇ {}
16553 "});
16554}
16555
16556#[gpui::test]
16557async fn test_rename_without_prepare(cx: &mut TestAppContext) {
16558 init_test(cx, |_| {});
16559 // These capabilities indicate that the server does not support prepare rename.
16560 let capabilities = lsp::ServerCapabilities {
16561 rename_provider: Some(lsp::OneOf::Left(true)),
16562 ..Default::default()
16563 };
16564 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
16565
16566 cx.set_state(indoc! {"
16567 struct Fˇoo {}
16568 "});
16569
16570 cx.update_editor(|editor, _window, cx| {
16571 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
16572 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
16573 editor.highlight_background::<DocumentHighlightRead>(
16574 &[highlight_range],
16575 |c| c.editor_document_highlight_read_background,
16576 cx,
16577 );
16578 });
16579
16580 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
16581 .expect("Prepare rename was not started")
16582 .await
16583 .expect("Prepare rename failed");
16584
16585 let mut rename_handler =
16586 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
16587 let edit = lsp::TextEdit {
16588 range: lsp::Range {
16589 start: lsp::Position {
16590 line: 0,
16591 character: 7,
16592 },
16593 end: lsp::Position {
16594 line: 0,
16595 character: 10,
16596 },
16597 },
16598 new_text: "FooRenamed".to_string(),
16599 };
16600 Ok(Some(lsp::WorkspaceEdit::new(
16601 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
16602 )))
16603 });
16604 let rename_task = cx
16605 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
16606 .expect("Confirm rename was not started");
16607 rename_handler.next().await.unwrap();
16608 rename_task.await.expect("Confirm rename failed");
16609 cx.run_until_parked();
16610
16611 // Correct range is renamed, as `surrounding_word` is used to find it.
16612 cx.assert_editor_state(indoc! {"
16613 struct FooRenamedˇ {}
16614 "});
16615}
16616
16617#[gpui::test]
16618async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
16619 init_test(cx, |_| {});
16620 let mut cx = EditorTestContext::new(cx).await;
16621
16622 let language = Arc::new(
16623 Language::new(
16624 LanguageConfig::default(),
16625 Some(tree_sitter_html::LANGUAGE.into()),
16626 )
16627 .with_brackets_query(
16628 r#"
16629 ("<" @open "/>" @close)
16630 ("</" @open ">" @close)
16631 ("<" @open ">" @close)
16632 ("\"" @open "\"" @close)
16633 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
16634 "#,
16635 )
16636 .unwrap(),
16637 );
16638 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16639
16640 cx.set_state(indoc! {"
16641 <span>ˇ</span>
16642 "});
16643 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
16644 cx.assert_editor_state(indoc! {"
16645 <span>
16646 ˇ
16647 </span>
16648 "});
16649
16650 cx.set_state(indoc! {"
16651 <span><span></span>ˇ</span>
16652 "});
16653 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
16654 cx.assert_editor_state(indoc! {"
16655 <span><span></span>
16656 ˇ</span>
16657 "});
16658
16659 cx.set_state(indoc! {"
16660 <span>ˇ
16661 </span>
16662 "});
16663 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
16664 cx.assert_editor_state(indoc! {"
16665 <span>
16666 ˇ
16667 </span>
16668 "});
16669}
16670
16671fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
16672 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
16673 point..point
16674}
16675
16676fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
16677 let (text, ranges) = marked_text_ranges(marked_text, true);
16678 assert_eq!(editor.text(cx), text);
16679 assert_eq!(
16680 editor.selections.ranges(cx),
16681 ranges,
16682 "Assert selections are {}",
16683 marked_text
16684 );
16685}
16686
16687pub fn handle_signature_help_request(
16688 cx: &mut EditorLspTestContext,
16689 mocked_response: lsp::SignatureHelp,
16690) -> impl Future<Output = ()> {
16691 let mut request =
16692 cx.handle_request::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
16693 let mocked_response = mocked_response.clone();
16694 async move { Ok(Some(mocked_response)) }
16695 });
16696
16697 async move {
16698 request.next().await;
16699 }
16700}
16701
16702/// Handle completion request passing a marked string specifying where the completion
16703/// should be triggered from using '|' character, what range should be replaced, and what completions
16704/// should be returned using '<' and '>' to delimit the range
16705pub fn handle_completion_request(
16706 cx: &mut EditorLspTestContext,
16707 marked_string: &str,
16708 completions: Vec<&'static str>,
16709 counter: Arc<AtomicUsize>,
16710) -> impl Future<Output = ()> {
16711 let complete_from_marker: TextRangeMarker = '|'.into();
16712 let replace_range_marker: TextRangeMarker = ('<', '>').into();
16713 let (_, mut marked_ranges) = marked_text_ranges_by(
16714 marked_string,
16715 vec![complete_from_marker.clone(), replace_range_marker.clone()],
16716 );
16717
16718 let complete_from_position =
16719 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
16720 let replace_range =
16721 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
16722
16723 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
16724 let completions = completions.clone();
16725 counter.fetch_add(1, atomic::Ordering::Release);
16726 async move {
16727 assert_eq!(params.text_document_position.text_document.uri, url.clone());
16728 assert_eq!(
16729 params.text_document_position.position,
16730 complete_from_position
16731 );
16732 Ok(Some(lsp::CompletionResponse::Array(
16733 completions
16734 .iter()
16735 .map(|completion_text| lsp::CompletionItem {
16736 label: completion_text.to_string(),
16737 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16738 range: replace_range,
16739 new_text: completion_text.to_string(),
16740 })),
16741 ..Default::default()
16742 })
16743 .collect(),
16744 )))
16745 }
16746 });
16747
16748 async move {
16749 request.next().await;
16750 }
16751}
16752
16753fn handle_resolve_completion_request(
16754 cx: &mut EditorLspTestContext,
16755 edits: Option<Vec<(&'static str, &'static str)>>,
16756) -> impl Future<Output = ()> {
16757 let edits = edits.map(|edits| {
16758 edits
16759 .iter()
16760 .map(|(marked_string, new_text)| {
16761 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
16762 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
16763 lsp::TextEdit::new(replace_range, new_text.to_string())
16764 })
16765 .collect::<Vec<_>>()
16766 });
16767
16768 let mut request =
16769 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
16770 let edits = edits.clone();
16771 async move {
16772 Ok(lsp::CompletionItem {
16773 additional_text_edits: edits,
16774 ..Default::default()
16775 })
16776 }
16777 });
16778
16779 async move {
16780 request.next().await;
16781 }
16782}
16783
16784pub(crate) fn update_test_language_settings(
16785 cx: &mut TestAppContext,
16786 f: impl Fn(&mut AllLanguageSettingsContent),
16787) {
16788 cx.update(|cx| {
16789 SettingsStore::update_global(cx, |store, cx| {
16790 store.update_user_settings::<AllLanguageSettings>(cx, f);
16791 });
16792 });
16793}
16794
16795pub(crate) fn update_test_project_settings(
16796 cx: &mut TestAppContext,
16797 f: impl Fn(&mut ProjectSettings),
16798) {
16799 cx.update(|cx| {
16800 SettingsStore::update_global(cx, |store, cx| {
16801 store.update_user_settings::<ProjectSettings>(cx, f);
16802 });
16803 });
16804}
16805
16806pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
16807 cx.update(|cx| {
16808 assets::Assets.load_test_fonts(cx);
16809 let store = SettingsStore::test(cx);
16810 cx.set_global(store);
16811 theme::init(theme::LoadThemes::JustBase, cx);
16812 release_channel::init(SemanticVersion::default(), cx);
16813 client::init_settings(cx);
16814 language::init(cx);
16815 Project::init_settings(cx);
16816 workspace::init_settings(cx);
16817 crate::init(cx);
16818 });
16819
16820 update_test_language_settings(cx, f);
16821}
16822
16823#[track_caller]
16824fn assert_hunk_revert(
16825 not_reverted_text_with_selections: &str,
16826 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
16827 expected_reverted_text_with_selections: &str,
16828 base_text: &str,
16829 cx: &mut EditorLspTestContext,
16830) {
16831 cx.set_state(not_reverted_text_with_selections);
16832 cx.set_head_text(base_text);
16833 cx.executor().run_until_parked();
16834
16835 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
16836 let snapshot = editor.snapshot(window, cx);
16837 let reverted_hunk_statuses = snapshot
16838 .buffer_snapshot
16839 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
16840 .map(|hunk| hunk.status().kind)
16841 .collect::<Vec<_>>();
16842
16843 editor.git_restore(&Default::default(), window, cx);
16844 reverted_hunk_statuses
16845 });
16846 cx.executor().run_until_parked();
16847 cx.assert_editor_state(expected_reverted_text_with_selections);
16848 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
16849}