1use super::*;
2use crate::{
3 scroll::scroll_amount::ScrollAmount,
4 test::{
5 assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
6 editor_test_context::EditorTestContext, select_ranges,
7 },
8 JoinLines,
9};
10use buffer_diff::{BufferDiff, DiffHunkStatus};
11use futures::StreamExt;
12use gpui::{
13 div, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
14 WindowBounds, WindowOptions,
15};
16use indoc::indoc;
17use language::{
18 language_settings::{
19 AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent, PrettierSettings,
20 },
21 BracketPairConfig,
22 Capability::ReadWrite,
23 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
24 Override, ParsedMarkdown, Point,
25};
26use language_settings::{Formatter, FormatterList, IndentGuideSettings};
27use multi_buffer::IndentGuide;
28use parking_lot::Mutex;
29use pretty_assertions::{assert_eq, assert_ne};
30use project::FakeFs;
31use project::{
32 lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT,
33 project_settings::{LspSettings, ProjectSettings},
34};
35use serde_json::{self, json};
36use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
37use std::{
38 iter,
39 sync::atomic::{self, AtomicUsize},
40};
41use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
42use unindent::Unindent;
43use util::{
44 assert_set_eq, path,
45 test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
46 uri,
47};
48use workspace::{
49 item::{FollowEvent, FollowableItem, Item, ItemHandle},
50 NavigationEntry, ViewId,
51};
52
53#[gpui::test]
54fn test_edit_events(cx: &mut TestAppContext) {
55 init_test(cx, |_| {});
56
57 let buffer = cx.new(|cx| {
58 let mut buffer = language::Buffer::local("123456", cx);
59 buffer.set_group_interval(Duration::from_secs(1));
60 buffer
61 });
62
63 let events = Rc::new(RefCell::new(Vec::new()));
64 let editor1 = cx.add_window({
65 let events = events.clone();
66 |window, cx| {
67 let entity = cx.entity().clone();
68 cx.subscribe_in(
69 &entity,
70 window,
71 move |_, _, event: &EditorEvent, _, _| match event {
72 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
73 EditorEvent::BufferEdited => {
74 events.borrow_mut().push(("editor1", "buffer edited"))
75 }
76 _ => {}
77 },
78 )
79 .detach();
80 Editor::for_buffer(buffer.clone(), None, window, cx)
81 }
82 });
83
84 let editor2 = cx.add_window({
85 let events = events.clone();
86 |window, cx| {
87 cx.subscribe_in(
88 &cx.entity().clone(),
89 window,
90 move |_, _, event: &EditorEvent, _, _| match event {
91 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
92 EditorEvent::BufferEdited => {
93 events.borrow_mut().push(("editor2", "buffer edited"))
94 }
95 _ => {}
96 },
97 )
98 .detach();
99 Editor::for_buffer(buffer.clone(), None, window, cx)
100 }
101 });
102
103 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
104
105 // Mutating editor 1 will emit an `Edited` event only for that editor.
106 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
107 assert_eq!(
108 mem::take(&mut *events.borrow_mut()),
109 [
110 ("editor1", "edited"),
111 ("editor1", "buffer edited"),
112 ("editor2", "buffer edited"),
113 ]
114 );
115
116 // Mutating editor 2 will emit an `Edited` event only for that editor.
117 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
118 assert_eq!(
119 mem::take(&mut *events.borrow_mut()),
120 [
121 ("editor2", "edited"),
122 ("editor1", "buffer edited"),
123 ("editor2", "buffer edited"),
124 ]
125 );
126
127 // Undoing on editor 1 will emit an `Edited` event only for that editor.
128 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
129 assert_eq!(
130 mem::take(&mut *events.borrow_mut()),
131 [
132 ("editor1", "edited"),
133 ("editor1", "buffer edited"),
134 ("editor2", "buffer edited"),
135 ]
136 );
137
138 // Redoing on editor 1 will emit an `Edited` event only for that editor.
139 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
140 assert_eq!(
141 mem::take(&mut *events.borrow_mut()),
142 [
143 ("editor1", "edited"),
144 ("editor1", "buffer edited"),
145 ("editor2", "buffer edited"),
146 ]
147 );
148
149 // Undoing on editor 2 will emit an `Edited` event only for that editor.
150 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
151 assert_eq!(
152 mem::take(&mut *events.borrow_mut()),
153 [
154 ("editor2", "edited"),
155 ("editor1", "buffer edited"),
156 ("editor2", "buffer edited"),
157 ]
158 );
159
160 // Redoing on editor 2 will emit an `Edited` event only for that editor.
161 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
162 assert_eq!(
163 mem::take(&mut *events.borrow_mut()),
164 [
165 ("editor2", "edited"),
166 ("editor1", "buffer edited"),
167 ("editor2", "buffer edited"),
168 ]
169 );
170
171 // No event is emitted when the mutation is a no-op.
172 _ = editor2.update(cx, |editor, window, cx| {
173 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
174
175 editor.backspace(&Backspace, window, cx);
176 });
177 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
178}
179
180#[gpui::test]
181fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
182 init_test(cx, |_| {});
183
184 let mut now = Instant::now();
185 let group_interval = Duration::from_millis(1);
186 let buffer = cx.new(|cx| {
187 let mut buf = language::Buffer::local("123456", cx);
188 buf.set_group_interval(group_interval);
189 buf
190 });
191 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
192 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
193
194 _ = editor.update(cx, |editor, window, cx| {
195 editor.start_transaction_at(now, window, cx);
196 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
197
198 editor.insert("cd", window, cx);
199 editor.end_transaction_at(now, cx);
200 assert_eq!(editor.text(cx), "12cd56");
201 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
202
203 editor.start_transaction_at(now, window, cx);
204 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
205 editor.insert("e", window, cx);
206 editor.end_transaction_at(now, cx);
207 assert_eq!(editor.text(cx), "12cde6");
208 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
209
210 now += group_interval + Duration::from_millis(1);
211 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
212
213 // Simulate an edit in another editor
214 buffer.update(cx, |buffer, cx| {
215 buffer.start_transaction_at(now, cx);
216 buffer.edit([(0..1, "a")], None, cx);
217 buffer.edit([(1..1, "b")], None, cx);
218 buffer.end_transaction_at(now, cx);
219 });
220
221 assert_eq!(editor.text(cx), "ab2cde6");
222 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
223
224 // Last transaction happened past the group interval in a different editor.
225 // Undo it individually and don't restore selections.
226 editor.undo(&Undo, window, cx);
227 assert_eq!(editor.text(cx), "12cde6");
228 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
229
230 // First two transactions happened within the group interval in this editor.
231 // Undo them together and restore selections.
232 editor.undo(&Undo, window, cx);
233 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
234 assert_eq!(editor.text(cx), "123456");
235 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
236
237 // Redo the first two transactions together.
238 editor.redo(&Redo, window, cx);
239 assert_eq!(editor.text(cx), "12cde6");
240 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
241
242 // Redo the last transaction on its own.
243 editor.redo(&Redo, window, cx);
244 assert_eq!(editor.text(cx), "ab2cde6");
245 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
246
247 // Test empty transactions.
248 editor.start_transaction_at(now, window, cx);
249 editor.end_transaction_at(now, cx);
250 editor.undo(&Undo, window, cx);
251 assert_eq!(editor.text(cx), "12cde6");
252 });
253}
254
255#[gpui::test]
256fn test_ime_composition(cx: &mut TestAppContext) {
257 init_test(cx, |_| {});
258
259 let buffer = cx.new(|cx| {
260 let mut buffer = language::Buffer::local("abcde", cx);
261 // Ensure automatic grouping doesn't occur.
262 buffer.set_group_interval(Duration::ZERO);
263 buffer
264 });
265
266 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
267 cx.add_window(|window, cx| {
268 let mut editor = build_editor(buffer.clone(), window, cx);
269
270 // Start a new IME composition.
271 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
272 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
273 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
274 assert_eq!(editor.text(cx), "äbcde");
275 assert_eq!(
276 editor.marked_text_ranges(cx),
277 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
278 );
279
280 // Finalize IME composition.
281 editor.replace_text_in_range(None, "ā", window, cx);
282 assert_eq!(editor.text(cx), "ābcde");
283 assert_eq!(editor.marked_text_ranges(cx), None);
284
285 // IME composition edits are grouped and are undone/redone at once.
286 editor.undo(&Default::default(), window, cx);
287 assert_eq!(editor.text(cx), "abcde");
288 assert_eq!(editor.marked_text_ranges(cx), None);
289 editor.redo(&Default::default(), window, cx);
290 assert_eq!(editor.text(cx), "ābcde");
291 assert_eq!(editor.marked_text_ranges(cx), None);
292
293 // Start a new IME composition.
294 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
295 assert_eq!(
296 editor.marked_text_ranges(cx),
297 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
298 );
299
300 // Undoing during an IME composition cancels it.
301 editor.undo(&Default::default(), window, cx);
302 assert_eq!(editor.text(cx), "ābcde");
303 assert_eq!(editor.marked_text_ranges(cx), None);
304
305 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
306 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
307 assert_eq!(editor.text(cx), "ābcdè");
308 assert_eq!(
309 editor.marked_text_ranges(cx),
310 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
311 );
312
313 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
314 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
315 assert_eq!(editor.text(cx), "ābcdę");
316 assert_eq!(editor.marked_text_ranges(cx), None);
317
318 // Start a new IME composition with multiple cursors.
319 editor.change_selections(None, window, cx, |s| {
320 s.select_ranges([
321 OffsetUtf16(1)..OffsetUtf16(1),
322 OffsetUtf16(3)..OffsetUtf16(3),
323 OffsetUtf16(5)..OffsetUtf16(5),
324 ])
325 });
326 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
327 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
328 assert_eq!(
329 editor.marked_text_ranges(cx),
330 Some(vec![
331 OffsetUtf16(0)..OffsetUtf16(3),
332 OffsetUtf16(4)..OffsetUtf16(7),
333 OffsetUtf16(8)..OffsetUtf16(11)
334 ])
335 );
336
337 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
338 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
339 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
340 assert_eq!(
341 editor.marked_text_ranges(cx),
342 Some(vec![
343 OffsetUtf16(1)..OffsetUtf16(2),
344 OffsetUtf16(5)..OffsetUtf16(6),
345 OffsetUtf16(9)..OffsetUtf16(10)
346 ])
347 );
348
349 // Finalize IME composition with multiple cursors.
350 editor.replace_text_in_range(Some(9..10), "2", window, cx);
351 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
352 assert_eq!(editor.marked_text_ranges(cx), None);
353
354 editor
355 });
356}
357
358#[gpui::test]
359fn test_selection_with_mouse(cx: &mut TestAppContext) {
360 init_test(cx, |_| {});
361
362 let editor = cx.add_window(|window, cx| {
363 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
364 build_editor(buffer, window, cx)
365 });
366
367 _ = editor.update(cx, |editor, window, cx| {
368 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
369 });
370 assert_eq!(
371 editor
372 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
373 .unwrap(),
374 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
375 );
376
377 _ = editor.update(cx, |editor, window, cx| {
378 editor.update_selection(
379 DisplayPoint::new(DisplayRow(3), 3),
380 0,
381 gpui::Point::<f32>::default(),
382 window,
383 cx,
384 );
385 });
386
387 assert_eq!(
388 editor
389 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
390 .unwrap(),
391 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
392 );
393
394 _ = editor.update(cx, |editor, window, cx| {
395 editor.update_selection(
396 DisplayPoint::new(DisplayRow(1), 1),
397 0,
398 gpui::Point::<f32>::default(),
399 window,
400 cx,
401 );
402 });
403
404 assert_eq!(
405 editor
406 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
407 .unwrap(),
408 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
409 );
410
411 _ = editor.update(cx, |editor, window, cx| {
412 editor.end_selection(window, cx);
413 editor.update_selection(
414 DisplayPoint::new(DisplayRow(3), 3),
415 0,
416 gpui::Point::<f32>::default(),
417 window,
418 cx,
419 );
420 });
421
422 assert_eq!(
423 editor
424 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
425 .unwrap(),
426 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
427 );
428
429 _ = editor.update(cx, |editor, window, cx| {
430 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
431 editor.update_selection(
432 DisplayPoint::new(DisplayRow(0), 0),
433 0,
434 gpui::Point::<f32>::default(),
435 window,
436 cx,
437 );
438 });
439
440 assert_eq!(
441 editor
442 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
443 .unwrap(),
444 [
445 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
446 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
447 ]
448 );
449
450 _ = editor.update(cx, |editor, window, cx| {
451 editor.end_selection(window, cx);
452 });
453
454 assert_eq!(
455 editor
456 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
457 .unwrap(),
458 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
459 );
460}
461
462#[gpui::test]
463fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
464 init_test(cx, |_| {});
465
466 let editor = cx.add_window(|window, cx| {
467 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
468 build_editor(buffer, window, cx)
469 });
470
471 _ = editor.update(cx, |editor, window, cx| {
472 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
473 });
474
475 _ = editor.update(cx, |editor, window, cx| {
476 editor.end_selection(window, cx);
477 });
478
479 _ = editor.update(cx, |editor, window, cx| {
480 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
481 });
482
483 _ = editor.update(cx, |editor, window, cx| {
484 editor.end_selection(window, cx);
485 });
486
487 assert_eq!(
488 editor
489 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
490 .unwrap(),
491 [
492 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
493 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
494 ]
495 );
496
497 _ = editor.update(cx, |editor, window, cx| {
498 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
499 });
500
501 _ = editor.update(cx, |editor, window, cx| {
502 editor.end_selection(window, cx);
503 });
504
505 assert_eq!(
506 editor
507 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
508 .unwrap(),
509 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
510 );
511}
512
513#[gpui::test]
514fn test_canceling_pending_selection(cx: &mut TestAppContext) {
515 init_test(cx, |_| {});
516
517 let editor = cx.add_window(|window, cx| {
518 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
519 build_editor(buffer, window, cx)
520 });
521
522 _ = editor.update(cx, |editor, window, cx| {
523 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
524 assert_eq!(
525 editor.selections.display_ranges(cx),
526 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
527 );
528 });
529
530 _ = editor.update(cx, |editor, window, cx| {
531 editor.update_selection(
532 DisplayPoint::new(DisplayRow(3), 3),
533 0,
534 gpui::Point::<f32>::default(),
535 window,
536 cx,
537 );
538 assert_eq!(
539 editor.selections.display_ranges(cx),
540 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
541 );
542 });
543
544 _ = editor.update(cx, |editor, window, cx| {
545 editor.cancel(&Cancel, window, cx);
546 editor.update_selection(
547 DisplayPoint::new(DisplayRow(1), 1),
548 0,
549 gpui::Point::<f32>::default(),
550 window,
551 cx,
552 );
553 assert_eq!(
554 editor.selections.display_ranges(cx),
555 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
556 );
557 });
558}
559
560#[gpui::test]
561fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
562 init_test(cx, |_| {});
563
564 let editor = cx.add_window(|window, cx| {
565 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
566 build_editor(buffer, window, cx)
567 });
568
569 _ = editor.update(cx, |editor, window, cx| {
570 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
571 assert_eq!(
572 editor.selections.display_ranges(cx),
573 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
574 );
575
576 editor.move_down(&Default::default(), window, cx);
577 assert_eq!(
578 editor.selections.display_ranges(cx),
579 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
580 );
581
582 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
583 assert_eq!(
584 editor.selections.display_ranges(cx),
585 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
586 );
587
588 editor.move_up(&Default::default(), window, cx);
589 assert_eq!(
590 editor.selections.display_ranges(cx),
591 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
592 );
593 });
594}
595
596#[gpui::test]
597fn test_clone(cx: &mut TestAppContext) {
598 init_test(cx, |_| {});
599
600 let (text, selection_ranges) = marked_text_ranges(
601 indoc! {"
602 one
603 two
604 threeˇ
605 four
606 fiveˇ
607 "},
608 true,
609 );
610
611 let editor = cx.add_window(|window, cx| {
612 let buffer = MultiBuffer::build_simple(&text, cx);
613 build_editor(buffer, window, cx)
614 });
615
616 _ = editor.update(cx, |editor, window, cx| {
617 editor.change_selections(None, window, cx, |s| {
618 s.select_ranges(selection_ranges.clone())
619 });
620 editor.fold_creases(
621 vec![
622 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
623 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
624 ],
625 true,
626 window,
627 cx,
628 );
629 });
630
631 let cloned_editor = editor
632 .update(cx, |editor, _, cx| {
633 cx.open_window(Default::default(), |window, cx| {
634 cx.new(|cx| editor.clone(window, cx))
635 })
636 })
637 .unwrap()
638 .unwrap();
639
640 let snapshot = editor
641 .update(cx, |e, window, cx| e.snapshot(window, cx))
642 .unwrap();
643 let cloned_snapshot = cloned_editor
644 .update(cx, |e, window, cx| e.snapshot(window, cx))
645 .unwrap();
646
647 assert_eq!(
648 cloned_editor
649 .update(cx, |e, _, cx| e.display_text(cx))
650 .unwrap(),
651 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
652 );
653 assert_eq!(
654 cloned_snapshot
655 .folds_in_range(0..text.len())
656 .collect::<Vec<_>>(),
657 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
658 );
659 assert_set_eq!(
660 cloned_editor
661 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
662 .unwrap(),
663 editor
664 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
665 .unwrap()
666 );
667 assert_set_eq!(
668 cloned_editor
669 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
670 .unwrap(),
671 editor
672 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
673 .unwrap()
674 );
675}
676
677#[gpui::test]
678async fn test_navigation_history(cx: &mut TestAppContext) {
679 init_test(cx, |_| {});
680
681 use workspace::item::Item;
682
683 let fs = FakeFs::new(cx.executor());
684 let project = Project::test(fs, [], cx).await;
685 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
686 let pane = workspace
687 .update(cx, |workspace, _, _| workspace.active_pane().clone())
688 .unwrap();
689
690 _ = workspace.update(cx, |_v, window, cx| {
691 cx.new(|cx| {
692 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
693 let mut editor = build_editor(buffer.clone(), window, cx);
694 let handle = cx.entity();
695 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
696
697 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
698 editor.nav_history.as_mut().unwrap().pop_backward(cx)
699 }
700
701 // Move the cursor a small distance.
702 // Nothing is added to the navigation history.
703 editor.change_selections(None, window, cx, |s| {
704 s.select_display_ranges([
705 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
706 ])
707 });
708 editor.change_selections(None, window, cx, |s| {
709 s.select_display_ranges([
710 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
711 ])
712 });
713 assert!(pop_history(&mut editor, cx).is_none());
714
715 // Move the cursor a large distance.
716 // The history can jump back to the previous position.
717 editor.change_selections(None, window, cx, |s| {
718 s.select_display_ranges([
719 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
720 ])
721 });
722 let nav_entry = pop_history(&mut editor, cx).unwrap();
723 editor.navigate(nav_entry.data.unwrap(), window, cx);
724 assert_eq!(nav_entry.item.id(), cx.entity_id());
725 assert_eq!(
726 editor.selections.display_ranges(cx),
727 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
728 );
729 assert!(pop_history(&mut editor, cx).is_none());
730
731 // Move the cursor a small distance via the mouse.
732 // Nothing is added to the navigation history.
733 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
734 editor.end_selection(window, cx);
735 assert_eq!(
736 editor.selections.display_ranges(cx),
737 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
738 );
739 assert!(pop_history(&mut editor, cx).is_none());
740
741 // Move the cursor a large distance via the mouse.
742 // The history can jump back to the previous position.
743 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
744 editor.end_selection(window, cx);
745 assert_eq!(
746 editor.selections.display_ranges(cx),
747 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
748 );
749 let nav_entry = pop_history(&mut editor, cx).unwrap();
750 editor.navigate(nav_entry.data.unwrap(), window, cx);
751 assert_eq!(nav_entry.item.id(), cx.entity_id());
752 assert_eq!(
753 editor.selections.display_ranges(cx),
754 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
755 );
756 assert!(pop_history(&mut editor, cx).is_none());
757
758 // Set scroll position to check later
759 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
760 let original_scroll_position = editor.scroll_manager.anchor();
761
762 // Jump to the end of the document and adjust scroll
763 editor.move_to_end(&MoveToEnd, window, cx);
764 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
765 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
766
767 let nav_entry = pop_history(&mut editor, cx).unwrap();
768 editor.navigate(nav_entry.data.unwrap(), window, cx);
769 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
770
771 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
772 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
773 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
774 let invalid_point = Point::new(9999, 0);
775 editor.navigate(
776 Box::new(NavigationData {
777 cursor_anchor: invalid_anchor,
778 cursor_position: invalid_point,
779 scroll_anchor: ScrollAnchor {
780 anchor: invalid_anchor,
781 offset: Default::default(),
782 },
783 scroll_top_row: invalid_point.row,
784 }),
785 window,
786 cx,
787 );
788 assert_eq!(
789 editor.selections.display_ranges(cx),
790 &[editor.max_point(cx)..editor.max_point(cx)]
791 );
792 assert_eq!(
793 editor.scroll_position(cx),
794 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
795 );
796
797 editor
798 })
799 });
800}
801
802#[gpui::test]
803fn test_cancel(cx: &mut TestAppContext) {
804 init_test(cx, |_| {});
805
806 let editor = cx.add_window(|window, cx| {
807 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
808 build_editor(buffer, window, cx)
809 });
810
811 _ = editor.update(cx, |editor, window, cx| {
812 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
813 editor.update_selection(
814 DisplayPoint::new(DisplayRow(1), 1),
815 0,
816 gpui::Point::<f32>::default(),
817 window,
818 cx,
819 );
820 editor.end_selection(window, cx);
821
822 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
823 editor.update_selection(
824 DisplayPoint::new(DisplayRow(0), 3),
825 0,
826 gpui::Point::<f32>::default(),
827 window,
828 cx,
829 );
830 editor.end_selection(window, cx);
831 assert_eq!(
832 editor.selections.display_ranges(cx),
833 [
834 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
835 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
836 ]
837 );
838 });
839
840 _ = editor.update(cx, |editor, window, cx| {
841 editor.cancel(&Cancel, window, cx);
842 assert_eq!(
843 editor.selections.display_ranges(cx),
844 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
845 );
846 });
847
848 _ = editor.update(cx, |editor, window, cx| {
849 editor.cancel(&Cancel, window, cx);
850 assert_eq!(
851 editor.selections.display_ranges(cx),
852 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
853 );
854 });
855}
856
857#[gpui::test]
858fn test_fold_action(cx: &mut TestAppContext) {
859 init_test(cx, |_| {});
860
861 let editor = cx.add_window(|window, cx| {
862 let buffer = MultiBuffer::build_simple(
863 &"
864 impl Foo {
865 // Hello!
866
867 fn a() {
868 1
869 }
870
871 fn b() {
872 2
873 }
874
875 fn c() {
876 3
877 }
878 }
879 "
880 .unindent(),
881 cx,
882 );
883 build_editor(buffer.clone(), window, cx)
884 });
885
886 _ = editor.update(cx, |editor, window, cx| {
887 editor.change_selections(None, window, cx, |s| {
888 s.select_display_ranges([
889 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
890 ]);
891 });
892 editor.fold(&Fold, window, cx);
893 assert_eq!(
894 editor.display_text(cx),
895 "
896 impl Foo {
897 // Hello!
898
899 fn a() {
900 1
901 }
902
903 fn b() {⋯
904 }
905
906 fn c() {⋯
907 }
908 }
909 "
910 .unindent(),
911 );
912
913 editor.fold(&Fold, window, cx);
914 assert_eq!(
915 editor.display_text(cx),
916 "
917 impl Foo {⋯
918 }
919 "
920 .unindent(),
921 );
922
923 editor.unfold_lines(&UnfoldLines, window, cx);
924 assert_eq!(
925 editor.display_text(cx),
926 "
927 impl Foo {
928 // Hello!
929
930 fn a() {
931 1
932 }
933
934 fn b() {⋯
935 }
936
937 fn c() {⋯
938 }
939 }
940 "
941 .unindent(),
942 );
943
944 editor.unfold_lines(&UnfoldLines, window, cx);
945 assert_eq!(
946 editor.display_text(cx),
947 editor.buffer.read(cx).read(cx).text()
948 );
949 });
950}
951
952#[gpui::test]
953fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
954 init_test(cx, |_| {});
955
956 let editor = cx.add_window(|window, cx| {
957 let buffer = MultiBuffer::build_simple(
958 &"
959 class Foo:
960 # Hello!
961
962 def a():
963 print(1)
964
965 def b():
966 print(2)
967
968 def c():
969 print(3)
970 "
971 .unindent(),
972 cx,
973 );
974 build_editor(buffer.clone(), window, cx)
975 });
976
977 _ = editor.update(cx, |editor, window, cx| {
978 editor.change_selections(None, window, cx, |s| {
979 s.select_display_ranges([
980 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
981 ]);
982 });
983 editor.fold(&Fold, window, cx);
984 assert_eq!(
985 editor.display_text(cx),
986 "
987 class Foo:
988 # Hello!
989
990 def a():
991 print(1)
992
993 def b():⋯
994
995 def c():⋯
996 "
997 .unindent(),
998 );
999
1000 editor.fold(&Fold, window, cx);
1001 assert_eq!(
1002 editor.display_text(cx),
1003 "
1004 class Foo:⋯
1005 "
1006 .unindent(),
1007 );
1008
1009 editor.unfold_lines(&UnfoldLines, window, cx);
1010 assert_eq!(
1011 editor.display_text(cx),
1012 "
1013 class Foo:
1014 # Hello!
1015
1016 def a():
1017 print(1)
1018
1019 def b():⋯
1020
1021 def c():⋯
1022 "
1023 .unindent(),
1024 );
1025
1026 editor.unfold_lines(&UnfoldLines, window, cx);
1027 assert_eq!(
1028 editor.display_text(cx),
1029 editor.buffer.read(cx).read(cx).text()
1030 );
1031 });
1032}
1033
1034#[gpui::test]
1035fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1036 init_test(cx, |_| {});
1037
1038 let editor = cx.add_window(|window, cx| {
1039 let buffer = MultiBuffer::build_simple(
1040 &"
1041 class Foo:
1042 # Hello!
1043
1044 def a():
1045 print(1)
1046
1047 def b():
1048 print(2)
1049
1050
1051 def c():
1052 print(3)
1053
1054
1055 "
1056 .unindent(),
1057 cx,
1058 );
1059 build_editor(buffer.clone(), window, cx)
1060 });
1061
1062 _ = editor.update(cx, |editor, window, cx| {
1063 editor.change_selections(None, window, cx, |s| {
1064 s.select_display_ranges([
1065 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1066 ]);
1067 });
1068 editor.fold(&Fold, window, cx);
1069 assert_eq!(
1070 editor.display_text(cx),
1071 "
1072 class Foo:
1073 # Hello!
1074
1075 def a():
1076 print(1)
1077
1078 def b():⋯
1079
1080
1081 def c():⋯
1082
1083
1084 "
1085 .unindent(),
1086 );
1087
1088 editor.fold(&Fold, window, cx);
1089 assert_eq!(
1090 editor.display_text(cx),
1091 "
1092 class Foo:⋯
1093
1094
1095 "
1096 .unindent(),
1097 );
1098
1099 editor.unfold_lines(&UnfoldLines, window, cx);
1100 assert_eq!(
1101 editor.display_text(cx),
1102 "
1103 class Foo:
1104 # Hello!
1105
1106 def a():
1107 print(1)
1108
1109 def b():⋯
1110
1111
1112 def c():⋯
1113
1114
1115 "
1116 .unindent(),
1117 );
1118
1119 editor.unfold_lines(&UnfoldLines, window, cx);
1120 assert_eq!(
1121 editor.display_text(cx),
1122 editor.buffer.read(cx).read(cx).text()
1123 );
1124 });
1125}
1126
1127#[gpui::test]
1128fn test_fold_at_level(cx: &mut TestAppContext) {
1129 init_test(cx, |_| {});
1130
1131 let editor = cx.add_window(|window, cx| {
1132 let buffer = MultiBuffer::build_simple(
1133 &"
1134 class Foo:
1135 # Hello!
1136
1137 def a():
1138 print(1)
1139
1140 def b():
1141 print(2)
1142
1143
1144 class Bar:
1145 # World!
1146
1147 def a():
1148 print(1)
1149
1150 def b():
1151 print(2)
1152
1153
1154 "
1155 .unindent(),
1156 cx,
1157 );
1158 build_editor(buffer.clone(), window, cx)
1159 });
1160
1161 _ = editor.update(cx, |editor, window, cx| {
1162 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1163 assert_eq!(
1164 editor.display_text(cx),
1165 "
1166 class Foo:
1167 # Hello!
1168
1169 def a():⋯
1170
1171 def b():⋯
1172
1173
1174 class Bar:
1175 # World!
1176
1177 def a():⋯
1178
1179 def b():⋯
1180
1181
1182 "
1183 .unindent(),
1184 );
1185
1186 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1187 assert_eq!(
1188 editor.display_text(cx),
1189 "
1190 class Foo:⋯
1191
1192
1193 class Bar:⋯
1194
1195
1196 "
1197 .unindent(),
1198 );
1199
1200 editor.unfold_all(&UnfoldAll, window, cx);
1201 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1202 assert_eq!(
1203 editor.display_text(cx),
1204 "
1205 class Foo:
1206 # Hello!
1207
1208 def a():
1209 print(1)
1210
1211 def b():
1212 print(2)
1213
1214
1215 class Bar:
1216 # World!
1217
1218 def a():
1219 print(1)
1220
1221 def b():
1222 print(2)
1223
1224
1225 "
1226 .unindent(),
1227 );
1228
1229 assert_eq!(
1230 editor.display_text(cx),
1231 editor.buffer.read(cx).read(cx).text()
1232 );
1233 });
1234}
1235
1236#[gpui::test]
1237fn test_move_cursor(cx: &mut TestAppContext) {
1238 init_test(cx, |_| {});
1239
1240 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1241 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1242
1243 buffer.update(cx, |buffer, cx| {
1244 buffer.edit(
1245 vec![
1246 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1247 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1248 ],
1249 None,
1250 cx,
1251 );
1252 });
1253 _ = editor.update(cx, |editor, window, cx| {
1254 assert_eq!(
1255 editor.selections.display_ranges(cx),
1256 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1257 );
1258
1259 editor.move_down(&MoveDown, window, cx);
1260 assert_eq!(
1261 editor.selections.display_ranges(cx),
1262 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1263 );
1264
1265 editor.move_right(&MoveRight, window, cx);
1266 assert_eq!(
1267 editor.selections.display_ranges(cx),
1268 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1269 );
1270
1271 editor.move_left(&MoveLeft, window, cx);
1272 assert_eq!(
1273 editor.selections.display_ranges(cx),
1274 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1275 );
1276
1277 editor.move_up(&MoveUp, window, cx);
1278 assert_eq!(
1279 editor.selections.display_ranges(cx),
1280 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1281 );
1282
1283 editor.move_to_end(&MoveToEnd, window, cx);
1284 assert_eq!(
1285 editor.selections.display_ranges(cx),
1286 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1287 );
1288
1289 editor.move_to_beginning(&MoveToBeginning, window, cx);
1290 assert_eq!(
1291 editor.selections.display_ranges(cx),
1292 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1293 );
1294
1295 editor.change_selections(None, window, cx, |s| {
1296 s.select_display_ranges([
1297 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1298 ]);
1299 });
1300 editor.select_to_beginning(&SelectToBeginning, window, cx);
1301 assert_eq!(
1302 editor.selections.display_ranges(cx),
1303 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1304 );
1305
1306 editor.select_to_end(&SelectToEnd, window, cx);
1307 assert_eq!(
1308 editor.selections.display_ranges(cx),
1309 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1310 );
1311 });
1312}
1313
1314// TODO: Re-enable this test
1315#[cfg(target_os = "macos")]
1316#[gpui::test]
1317fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1318 init_test(cx, |_| {});
1319
1320 let editor = cx.add_window(|window, cx| {
1321 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1322 build_editor(buffer.clone(), window, cx)
1323 });
1324
1325 assert_eq!('🟥'.len_utf8(), 4);
1326 assert_eq!('α'.len_utf8(), 2);
1327
1328 _ = editor.update(cx, |editor, window, cx| {
1329 editor.fold_creases(
1330 vec![
1331 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1332 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1333 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1334 ],
1335 true,
1336 window,
1337 cx,
1338 );
1339 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1340
1341 editor.move_right(&MoveRight, window, cx);
1342 assert_eq!(
1343 editor.selections.display_ranges(cx),
1344 &[empty_range(0, "🟥".len())]
1345 );
1346 editor.move_right(&MoveRight, window, cx);
1347 assert_eq!(
1348 editor.selections.display_ranges(cx),
1349 &[empty_range(0, "🟥🟧".len())]
1350 );
1351 editor.move_right(&MoveRight, window, cx);
1352 assert_eq!(
1353 editor.selections.display_ranges(cx),
1354 &[empty_range(0, "🟥🟧⋯".len())]
1355 );
1356
1357 editor.move_down(&MoveDown, window, cx);
1358 assert_eq!(
1359 editor.selections.display_ranges(cx),
1360 &[empty_range(1, "ab⋯e".len())]
1361 );
1362 editor.move_left(&MoveLeft, window, cx);
1363 assert_eq!(
1364 editor.selections.display_ranges(cx),
1365 &[empty_range(1, "ab⋯".len())]
1366 );
1367 editor.move_left(&MoveLeft, window, cx);
1368 assert_eq!(
1369 editor.selections.display_ranges(cx),
1370 &[empty_range(1, "ab".len())]
1371 );
1372 editor.move_left(&MoveLeft, window, cx);
1373 assert_eq!(
1374 editor.selections.display_ranges(cx),
1375 &[empty_range(1, "a".len())]
1376 );
1377
1378 editor.move_down(&MoveDown, window, cx);
1379 assert_eq!(
1380 editor.selections.display_ranges(cx),
1381 &[empty_range(2, "α".len())]
1382 );
1383 editor.move_right(&MoveRight, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[empty_range(2, "αβ".len())]
1387 );
1388 editor.move_right(&MoveRight, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[empty_range(2, "αβ⋯".len())]
1392 );
1393 editor.move_right(&MoveRight, window, cx);
1394 assert_eq!(
1395 editor.selections.display_ranges(cx),
1396 &[empty_range(2, "αβ⋯ε".len())]
1397 );
1398
1399 editor.move_up(&MoveUp, window, cx);
1400 assert_eq!(
1401 editor.selections.display_ranges(cx),
1402 &[empty_range(1, "ab⋯e".len())]
1403 );
1404 editor.move_down(&MoveDown, window, cx);
1405 assert_eq!(
1406 editor.selections.display_ranges(cx),
1407 &[empty_range(2, "αβ⋯ε".len())]
1408 );
1409 editor.move_up(&MoveUp, window, cx);
1410 assert_eq!(
1411 editor.selections.display_ranges(cx),
1412 &[empty_range(1, "ab⋯e".len())]
1413 );
1414
1415 editor.move_up(&MoveUp, window, cx);
1416 assert_eq!(
1417 editor.selections.display_ranges(cx),
1418 &[empty_range(0, "🟥🟧".len())]
1419 );
1420 editor.move_left(&MoveLeft, window, cx);
1421 assert_eq!(
1422 editor.selections.display_ranges(cx),
1423 &[empty_range(0, "🟥".len())]
1424 );
1425 editor.move_left(&MoveLeft, window, cx);
1426 assert_eq!(
1427 editor.selections.display_ranges(cx),
1428 &[empty_range(0, "".len())]
1429 );
1430 });
1431}
1432
1433#[gpui::test]
1434fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1435 init_test(cx, |_| {});
1436
1437 let editor = cx.add_window(|window, cx| {
1438 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1439 build_editor(buffer.clone(), window, cx)
1440 });
1441 _ = editor.update(cx, |editor, window, cx| {
1442 editor.change_selections(None, window, cx, |s| {
1443 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1444 });
1445
1446 // moving above start of document should move selection to start of document,
1447 // but the next move down should still be at the original goal_x
1448 editor.move_up(&MoveUp, window, cx);
1449 assert_eq!(
1450 editor.selections.display_ranges(cx),
1451 &[empty_range(0, "".len())]
1452 );
1453
1454 editor.move_down(&MoveDown, window, cx);
1455 assert_eq!(
1456 editor.selections.display_ranges(cx),
1457 &[empty_range(1, "abcd".len())]
1458 );
1459
1460 editor.move_down(&MoveDown, window, cx);
1461 assert_eq!(
1462 editor.selections.display_ranges(cx),
1463 &[empty_range(2, "αβγ".len())]
1464 );
1465
1466 editor.move_down(&MoveDown, window, cx);
1467 assert_eq!(
1468 editor.selections.display_ranges(cx),
1469 &[empty_range(3, "abcd".len())]
1470 );
1471
1472 editor.move_down(&MoveDown, window, cx);
1473 assert_eq!(
1474 editor.selections.display_ranges(cx),
1475 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1476 );
1477
1478 // moving past end of document should not change goal_x
1479 editor.move_down(&MoveDown, window, cx);
1480 assert_eq!(
1481 editor.selections.display_ranges(cx),
1482 &[empty_range(5, "".len())]
1483 );
1484
1485 editor.move_down(&MoveDown, window, cx);
1486 assert_eq!(
1487 editor.selections.display_ranges(cx),
1488 &[empty_range(5, "".len())]
1489 );
1490
1491 editor.move_up(&MoveUp, window, cx);
1492 assert_eq!(
1493 editor.selections.display_ranges(cx),
1494 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1495 );
1496
1497 editor.move_up(&MoveUp, window, cx);
1498 assert_eq!(
1499 editor.selections.display_ranges(cx),
1500 &[empty_range(3, "abcd".len())]
1501 );
1502
1503 editor.move_up(&MoveUp, window, cx);
1504 assert_eq!(
1505 editor.selections.display_ranges(cx),
1506 &[empty_range(2, "αβγ".len())]
1507 );
1508 });
1509}
1510
1511#[gpui::test]
1512fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1513 init_test(cx, |_| {});
1514 let move_to_beg = MoveToBeginningOfLine {
1515 stop_at_soft_wraps: true,
1516 };
1517
1518 let move_to_end = MoveToEndOfLine {
1519 stop_at_soft_wraps: true,
1520 };
1521
1522 let editor = cx.add_window(|window, cx| {
1523 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1524 build_editor(buffer, window, cx)
1525 });
1526 _ = editor.update(cx, |editor, window, cx| {
1527 editor.change_selections(None, window, cx, |s| {
1528 s.select_display_ranges([
1529 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1530 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1531 ]);
1532 });
1533 });
1534
1535 _ = editor.update(cx, |editor, window, cx| {
1536 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1537 assert_eq!(
1538 editor.selections.display_ranges(cx),
1539 &[
1540 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1541 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1542 ]
1543 );
1544 });
1545
1546 _ = editor.update(cx, |editor, window, cx| {
1547 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1548 assert_eq!(
1549 editor.selections.display_ranges(cx),
1550 &[
1551 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1552 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1553 ]
1554 );
1555 });
1556
1557 _ = editor.update(cx, |editor, window, cx| {
1558 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1559 assert_eq!(
1560 editor.selections.display_ranges(cx),
1561 &[
1562 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1563 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1564 ]
1565 );
1566 });
1567
1568 _ = editor.update(cx, |editor, window, cx| {
1569 editor.move_to_end_of_line(&move_to_end, window, cx);
1570 assert_eq!(
1571 editor.selections.display_ranges(cx),
1572 &[
1573 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1574 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1575 ]
1576 );
1577 });
1578
1579 // Moving to the end of line again is a no-op.
1580 _ = editor.update(cx, |editor, window, cx| {
1581 editor.move_to_end_of_line(&move_to_end, window, cx);
1582 assert_eq!(
1583 editor.selections.display_ranges(cx),
1584 &[
1585 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1586 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1587 ]
1588 );
1589 });
1590
1591 _ = editor.update(cx, |editor, window, cx| {
1592 editor.move_left(&MoveLeft, window, cx);
1593 editor.select_to_beginning_of_line(
1594 &SelectToBeginningOfLine {
1595 stop_at_soft_wraps: true,
1596 },
1597 window,
1598 cx,
1599 );
1600 assert_eq!(
1601 editor.selections.display_ranges(cx),
1602 &[
1603 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1604 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1605 ]
1606 );
1607 });
1608
1609 _ = editor.update(cx, |editor, window, cx| {
1610 editor.select_to_beginning_of_line(
1611 &SelectToBeginningOfLine {
1612 stop_at_soft_wraps: true,
1613 },
1614 window,
1615 cx,
1616 );
1617 assert_eq!(
1618 editor.selections.display_ranges(cx),
1619 &[
1620 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1621 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1622 ]
1623 );
1624 });
1625
1626 _ = editor.update(cx, |editor, window, cx| {
1627 editor.select_to_beginning_of_line(
1628 &SelectToBeginningOfLine {
1629 stop_at_soft_wraps: true,
1630 },
1631 window,
1632 cx,
1633 );
1634 assert_eq!(
1635 editor.selections.display_ranges(cx),
1636 &[
1637 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1638 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1639 ]
1640 );
1641 });
1642
1643 _ = editor.update(cx, |editor, window, cx| {
1644 editor.select_to_end_of_line(
1645 &SelectToEndOfLine {
1646 stop_at_soft_wraps: true,
1647 },
1648 window,
1649 cx,
1650 );
1651 assert_eq!(
1652 editor.selections.display_ranges(cx),
1653 &[
1654 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1655 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1656 ]
1657 );
1658 });
1659
1660 _ = editor.update(cx, |editor, window, cx| {
1661 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1662 assert_eq!(editor.display_text(cx), "ab\n de");
1663 assert_eq!(
1664 editor.selections.display_ranges(cx),
1665 &[
1666 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1667 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1668 ]
1669 );
1670 });
1671
1672 _ = editor.update(cx, |editor, window, cx| {
1673 editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx);
1674 assert_eq!(editor.display_text(cx), "\n");
1675 assert_eq!(
1676 editor.selections.display_ranges(cx),
1677 &[
1678 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1679 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1680 ]
1681 );
1682 });
1683}
1684
1685#[gpui::test]
1686fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1687 init_test(cx, |_| {});
1688 let move_to_beg = MoveToBeginningOfLine {
1689 stop_at_soft_wraps: false,
1690 };
1691
1692 let move_to_end = MoveToEndOfLine {
1693 stop_at_soft_wraps: false,
1694 };
1695
1696 let editor = cx.add_window(|window, cx| {
1697 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1698 build_editor(buffer, window, cx)
1699 });
1700
1701 _ = editor.update(cx, |editor, window, cx| {
1702 editor.set_wrap_width(Some(140.0.into()), cx);
1703
1704 // We expect the following lines after wrapping
1705 // ```
1706 // thequickbrownfox
1707 // jumpedoverthelazydo
1708 // gs
1709 // ```
1710 // The final `gs` was soft-wrapped onto a new line.
1711 assert_eq!(
1712 "thequickbrownfox\njumpedoverthelaz\nydogs",
1713 editor.display_text(cx),
1714 );
1715
1716 // First, let's assert behavior on the first line, that was not soft-wrapped.
1717 // Start the cursor at the `k` on the first line
1718 editor.change_selections(None, window, cx, |s| {
1719 s.select_display_ranges([
1720 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1721 ]);
1722 });
1723
1724 // Moving to the beginning of the line should put us at the beginning of the line.
1725 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1726 assert_eq!(
1727 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1728 editor.selections.display_ranges(cx)
1729 );
1730
1731 // Moving to the end of the line should put us at the end of the line.
1732 editor.move_to_end_of_line(&move_to_end, window, cx);
1733 assert_eq!(
1734 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1735 editor.selections.display_ranges(cx)
1736 );
1737
1738 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1739 // Start the cursor at the last line (`y` that was wrapped to a new line)
1740 editor.change_selections(None, window, cx, |s| {
1741 s.select_display_ranges([
1742 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1743 ]);
1744 });
1745
1746 // Moving to the beginning of the line should put us at the start of the second line of
1747 // display text, i.e., the `j`.
1748 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1749 assert_eq!(
1750 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1751 editor.selections.display_ranges(cx)
1752 );
1753
1754 // Moving to the beginning of the line again should be a no-op.
1755 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1756 assert_eq!(
1757 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1758 editor.selections.display_ranges(cx)
1759 );
1760
1761 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1762 // next display line.
1763 editor.move_to_end_of_line(&move_to_end, window, cx);
1764 assert_eq!(
1765 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1766 editor.selections.display_ranges(cx)
1767 );
1768
1769 // Moving to the end of the line again should be a no-op.
1770 editor.move_to_end_of_line(&move_to_end, window, cx);
1771 assert_eq!(
1772 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1773 editor.selections.display_ranges(cx)
1774 );
1775 });
1776}
1777
1778#[gpui::test]
1779fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1780 init_test(cx, |_| {});
1781
1782 let editor = cx.add_window(|window, cx| {
1783 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1784 build_editor(buffer, window, cx)
1785 });
1786 _ = editor.update(cx, |editor, window, cx| {
1787 editor.change_selections(None, window, cx, |s| {
1788 s.select_display_ranges([
1789 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1790 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1791 ])
1792 });
1793
1794 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1795 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1796
1797 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1798 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1799
1800 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1801 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1802
1803 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1804 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1805
1806 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1807 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1808
1809 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1810 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1811
1812 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1813 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1814
1815 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1816 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1817
1818 editor.move_right(&MoveRight, window, cx);
1819 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1820 assert_selection_ranges(
1821 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1822 editor,
1823 cx,
1824 );
1825
1826 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1827 assert_selection_ranges(
1828 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1829 editor,
1830 cx,
1831 );
1832
1833 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1834 assert_selection_ranges(
1835 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1836 editor,
1837 cx,
1838 );
1839 });
1840}
1841
1842#[gpui::test]
1843fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1844 init_test(cx, |_| {});
1845
1846 let editor = cx.add_window(|window, cx| {
1847 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1848 build_editor(buffer, window, cx)
1849 });
1850
1851 _ = editor.update(cx, |editor, window, cx| {
1852 editor.set_wrap_width(Some(140.0.into()), cx);
1853 assert_eq!(
1854 editor.display_text(cx),
1855 "use one::{\n two::three::\n four::five\n};"
1856 );
1857
1858 editor.change_selections(None, window, cx, |s| {
1859 s.select_display_ranges([
1860 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1861 ]);
1862 });
1863
1864 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1865 assert_eq!(
1866 editor.selections.display_ranges(cx),
1867 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1868 );
1869
1870 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1871 assert_eq!(
1872 editor.selections.display_ranges(cx),
1873 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1874 );
1875
1876 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1877 assert_eq!(
1878 editor.selections.display_ranges(cx),
1879 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1880 );
1881
1882 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1883 assert_eq!(
1884 editor.selections.display_ranges(cx),
1885 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
1886 );
1887
1888 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1889 assert_eq!(
1890 editor.selections.display_ranges(cx),
1891 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1892 );
1893
1894 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1895 assert_eq!(
1896 editor.selections.display_ranges(cx),
1897 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1898 );
1899 });
1900}
1901
1902#[gpui::test]
1903async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1904 init_test(cx, |_| {});
1905 let mut cx = EditorTestContext::new(cx).await;
1906
1907 let line_height = cx.editor(|editor, window, _| {
1908 editor
1909 .style()
1910 .unwrap()
1911 .text
1912 .line_height_in_pixels(window.rem_size())
1913 });
1914 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
1915
1916 cx.set_state(
1917 &r#"ˇone
1918 two
1919
1920 three
1921 fourˇ
1922 five
1923
1924 six"#
1925 .unindent(),
1926 );
1927
1928 cx.update_editor(|editor, window, cx| {
1929 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1930 });
1931 cx.assert_editor_state(
1932 &r#"one
1933 two
1934 ˇ
1935 three
1936 four
1937 five
1938 ˇ
1939 six"#
1940 .unindent(),
1941 );
1942
1943 cx.update_editor(|editor, window, cx| {
1944 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1945 });
1946 cx.assert_editor_state(
1947 &r#"one
1948 two
1949
1950 three
1951 four
1952 five
1953 ˇ
1954 sixˇ"#
1955 .unindent(),
1956 );
1957
1958 cx.update_editor(|editor, window, cx| {
1959 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1960 });
1961 cx.assert_editor_state(
1962 &r#"one
1963 two
1964
1965 three
1966 four
1967 five
1968
1969 sixˇ"#
1970 .unindent(),
1971 );
1972
1973 cx.update_editor(|editor, window, cx| {
1974 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
1975 });
1976 cx.assert_editor_state(
1977 &r#"one
1978 two
1979
1980 three
1981 four
1982 five
1983 ˇ
1984 six"#
1985 .unindent(),
1986 );
1987
1988 cx.update_editor(|editor, window, cx| {
1989 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
1990 });
1991 cx.assert_editor_state(
1992 &r#"one
1993 two
1994 ˇ
1995 three
1996 four
1997 five
1998
1999 six"#
2000 .unindent(),
2001 );
2002
2003 cx.update_editor(|editor, window, cx| {
2004 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2005 });
2006 cx.assert_editor_state(
2007 &r#"ˇone
2008 two
2009
2010 three
2011 four
2012 five
2013
2014 six"#
2015 .unindent(),
2016 );
2017}
2018
2019#[gpui::test]
2020async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
2021 init_test(cx, |_| {});
2022 let mut cx = EditorTestContext::new(cx).await;
2023 let line_height = cx.editor(|editor, window, _| {
2024 editor
2025 .style()
2026 .unwrap()
2027 .text
2028 .line_height_in_pixels(window.rem_size())
2029 });
2030 let window = cx.window;
2031 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2032
2033 cx.set_state(
2034 r#"ˇone
2035 two
2036 three
2037 four
2038 five
2039 six
2040 seven
2041 eight
2042 nine
2043 ten
2044 "#,
2045 );
2046
2047 cx.update_editor(|editor, window, cx| {
2048 assert_eq!(
2049 editor.snapshot(window, cx).scroll_position(),
2050 gpui::Point::new(0., 0.)
2051 );
2052 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2053 assert_eq!(
2054 editor.snapshot(window, cx).scroll_position(),
2055 gpui::Point::new(0., 3.)
2056 );
2057 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2058 assert_eq!(
2059 editor.snapshot(window, cx).scroll_position(),
2060 gpui::Point::new(0., 6.)
2061 );
2062 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2063 assert_eq!(
2064 editor.snapshot(window, cx).scroll_position(),
2065 gpui::Point::new(0., 3.)
2066 );
2067
2068 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2069 assert_eq!(
2070 editor.snapshot(window, cx).scroll_position(),
2071 gpui::Point::new(0., 1.)
2072 );
2073 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2074 assert_eq!(
2075 editor.snapshot(window, cx).scroll_position(),
2076 gpui::Point::new(0., 3.)
2077 );
2078 });
2079}
2080
2081#[gpui::test]
2082async fn test_autoscroll(cx: &mut gpui::TestAppContext) {
2083 init_test(cx, |_| {});
2084 let mut cx = EditorTestContext::new(cx).await;
2085
2086 let line_height = cx.update_editor(|editor, window, cx| {
2087 editor.set_vertical_scroll_margin(2, cx);
2088 editor
2089 .style()
2090 .unwrap()
2091 .text
2092 .line_height_in_pixels(window.rem_size())
2093 });
2094 let window = cx.window;
2095 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2096
2097 cx.set_state(
2098 r#"ˇone
2099 two
2100 three
2101 four
2102 five
2103 six
2104 seven
2105 eight
2106 nine
2107 ten
2108 "#,
2109 );
2110 cx.update_editor(|editor, window, cx| {
2111 assert_eq!(
2112 editor.snapshot(window, cx).scroll_position(),
2113 gpui::Point::new(0., 0.0)
2114 );
2115 });
2116
2117 // Add a cursor below the visible area. Since both cursors cannot fit
2118 // on screen, the editor autoscrolls to reveal the newest cursor, and
2119 // allows the vertical scroll margin below that cursor.
2120 cx.update_editor(|editor, window, cx| {
2121 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2122 selections.select_ranges([
2123 Point::new(0, 0)..Point::new(0, 0),
2124 Point::new(6, 0)..Point::new(6, 0),
2125 ]);
2126 })
2127 });
2128 cx.update_editor(|editor, window, cx| {
2129 assert_eq!(
2130 editor.snapshot(window, cx).scroll_position(),
2131 gpui::Point::new(0., 3.0)
2132 );
2133 });
2134
2135 // Move down. The editor cursor scrolls down to track the newest cursor.
2136 cx.update_editor(|editor, window, cx| {
2137 editor.move_down(&Default::default(), window, cx);
2138 });
2139 cx.update_editor(|editor, window, cx| {
2140 assert_eq!(
2141 editor.snapshot(window, cx).scroll_position(),
2142 gpui::Point::new(0., 4.0)
2143 );
2144 });
2145
2146 // Add a cursor above the visible area. Since both cursors fit on screen,
2147 // the editor scrolls to show both.
2148 cx.update_editor(|editor, window, cx| {
2149 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2150 selections.select_ranges([
2151 Point::new(1, 0)..Point::new(1, 0),
2152 Point::new(6, 0)..Point::new(6, 0),
2153 ]);
2154 })
2155 });
2156 cx.update_editor(|editor, window, cx| {
2157 assert_eq!(
2158 editor.snapshot(window, cx).scroll_position(),
2159 gpui::Point::new(0., 1.0)
2160 );
2161 });
2162}
2163
2164#[gpui::test]
2165async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
2166 init_test(cx, |_| {});
2167 let mut cx = EditorTestContext::new(cx).await;
2168
2169 let line_height = cx.editor(|editor, window, _cx| {
2170 editor
2171 .style()
2172 .unwrap()
2173 .text
2174 .line_height_in_pixels(window.rem_size())
2175 });
2176 let window = cx.window;
2177 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2178 cx.set_state(
2179 &r#"
2180 ˇone
2181 two
2182 threeˇ
2183 four
2184 five
2185 six
2186 seven
2187 eight
2188 nine
2189 ten
2190 "#
2191 .unindent(),
2192 );
2193
2194 cx.update_editor(|editor, window, cx| {
2195 editor.move_page_down(&MovePageDown::default(), window, cx)
2196 });
2197 cx.assert_editor_state(
2198 &r#"
2199 one
2200 two
2201 three
2202 ˇfour
2203 five
2204 sixˇ
2205 seven
2206 eight
2207 nine
2208 ten
2209 "#
2210 .unindent(),
2211 );
2212
2213 cx.update_editor(|editor, window, cx| {
2214 editor.move_page_down(&MovePageDown::default(), window, cx)
2215 });
2216 cx.assert_editor_state(
2217 &r#"
2218 one
2219 two
2220 three
2221 four
2222 five
2223 six
2224 ˇseven
2225 eight
2226 nineˇ
2227 ten
2228 "#
2229 .unindent(),
2230 );
2231
2232 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2233 cx.assert_editor_state(
2234 &r#"
2235 one
2236 two
2237 three
2238 ˇfour
2239 five
2240 sixˇ
2241 seven
2242 eight
2243 nine
2244 ten
2245 "#
2246 .unindent(),
2247 );
2248
2249 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2250 cx.assert_editor_state(
2251 &r#"
2252 ˇone
2253 two
2254 threeˇ
2255 four
2256 five
2257 six
2258 seven
2259 eight
2260 nine
2261 ten
2262 "#
2263 .unindent(),
2264 );
2265
2266 // Test select collapsing
2267 cx.update_editor(|editor, window, cx| {
2268 editor.move_page_down(&MovePageDown::default(), window, cx);
2269 editor.move_page_down(&MovePageDown::default(), window, cx);
2270 editor.move_page_down(&MovePageDown::default(), window, cx);
2271 });
2272 cx.assert_editor_state(
2273 &r#"
2274 one
2275 two
2276 three
2277 four
2278 five
2279 six
2280 seven
2281 eight
2282 nine
2283 ˇten
2284 ˇ"#
2285 .unindent(),
2286 );
2287}
2288
2289#[gpui::test]
2290async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
2291 init_test(cx, |_| {});
2292 let mut cx = EditorTestContext::new(cx).await;
2293 cx.set_state("one «two threeˇ» four");
2294 cx.update_editor(|editor, window, cx| {
2295 editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx);
2296 assert_eq!(editor.text(cx), " four");
2297 });
2298}
2299
2300#[gpui::test]
2301fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2302 init_test(cx, |_| {});
2303
2304 let editor = cx.add_window(|window, cx| {
2305 let buffer = MultiBuffer::build_simple("one two three four", cx);
2306 build_editor(buffer.clone(), window, cx)
2307 });
2308
2309 _ = editor.update(cx, |editor, window, cx| {
2310 editor.change_selections(None, window, cx, |s| {
2311 s.select_display_ranges([
2312 // an empty selection - the preceding word fragment is deleted
2313 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2314 // characters selected - they are deleted
2315 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2316 ])
2317 });
2318 editor.delete_to_previous_word_start(
2319 &DeleteToPreviousWordStart {
2320 ignore_newlines: false,
2321 },
2322 window,
2323 cx,
2324 );
2325 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2326 });
2327
2328 _ = editor.update(cx, |editor, window, cx| {
2329 editor.change_selections(None, window, cx, |s| {
2330 s.select_display_ranges([
2331 // an empty selection - the following word fragment is deleted
2332 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2333 // characters selected - they are deleted
2334 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2335 ])
2336 });
2337 editor.delete_to_next_word_end(
2338 &DeleteToNextWordEnd {
2339 ignore_newlines: false,
2340 },
2341 window,
2342 cx,
2343 );
2344 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2345 });
2346}
2347
2348#[gpui::test]
2349fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2350 init_test(cx, |_| {});
2351
2352 let editor = cx.add_window(|window, cx| {
2353 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2354 build_editor(buffer.clone(), window, cx)
2355 });
2356 let del_to_prev_word_start = DeleteToPreviousWordStart {
2357 ignore_newlines: false,
2358 };
2359 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2360 ignore_newlines: true,
2361 };
2362
2363 _ = editor.update(cx, |editor, window, cx| {
2364 editor.change_selections(None, window, cx, |s| {
2365 s.select_display_ranges([
2366 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2367 ])
2368 });
2369 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2370 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2371 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2372 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2373 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2374 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2375 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2376 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2377 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2378 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2379 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2380 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2381 });
2382}
2383
2384#[gpui::test]
2385fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2386 init_test(cx, |_| {});
2387
2388 let editor = cx.add_window(|window, cx| {
2389 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2390 build_editor(buffer.clone(), window, cx)
2391 });
2392 let del_to_next_word_end = DeleteToNextWordEnd {
2393 ignore_newlines: false,
2394 };
2395 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2396 ignore_newlines: true,
2397 };
2398
2399 _ = editor.update(cx, |editor, window, cx| {
2400 editor.change_selections(None, window, cx, |s| {
2401 s.select_display_ranges([
2402 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2403 ])
2404 });
2405 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2406 assert_eq!(
2407 editor.buffer.read(cx).read(cx).text(),
2408 "one\n two\nthree\n four"
2409 );
2410 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2411 assert_eq!(
2412 editor.buffer.read(cx).read(cx).text(),
2413 "\n two\nthree\n four"
2414 );
2415 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2416 assert_eq!(
2417 editor.buffer.read(cx).read(cx).text(),
2418 "two\nthree\n four"
2419 );
2420 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2421 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2422 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2423 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2424 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2425 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2426 });
2427}
2428
2429#[gpui::test]
2430fn test_newline(cx: &mut TestAppContext) {
2431 init_test(cx, |_| {});
2432
2433 let editor = cx.add_window(|window, cx| {
2434 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2435 build_editor(buffer.clone(), window, cx)
2436 });
2437
2438 _ = editor.update(cx, |editor, window, cx| {
2439 editor.change_selections(None, window, cx, |s| {
2440 s.select_display_ranges([
2441 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2442 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2443 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2444 ])
2445 });
2446
2447 editor.newline(&Newline, window, cx);
2448 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2449 });
2450}
2451
2452#[gpui::test]
2453fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2454 init_test(cx, |_| {});
2455
2456 let editor = cx.add_window(|window, cx| {
2457 let buffer = MultiBuffer::build_simple(
2458 "
2459 a
2460 b(
2461 X
2462 )
2463 c(
2464 X
2465 )
2466 "
2467 .unindent()
2468 .as_str(),
2469 cx,
2470 );
2471 let mut editor = build_editor(buffer.clone(), window, cx);
2472 editor.change_selections(None, window, cx, |s| {
2473 s.select_ranges([
2474 Point::new(2, 4)..Point::new(2, 5),
2475 Point::new(5, 4)..Point::new(5, 5),
2476 ])
2477 });
2478 editor
2479 });
2480
2481 _ = editor.update(cx, |editor, window, cx| {
2482 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2483 editor.buffer.update(cx, |buffer, cx| {
2484 buffer.edit(
2485 [
2486 (Point::new(1, 2)..Point::new(3, 0), ""),
2487 (Point::new(4, 2)..Point::new(6, 0), ""),
2488 ],
2489 None,
2490 cx,
2491 );
2492 assert_eq!(
2493 buffer.read(cx).text(),
2494 "
2495 a
2496 b()
2497 c()
2498 "
2499 .unindent()
2500 );
2501 });
2502 assert_eq!(
2503 editor.selections.ranges(cx),
2504 &[
2505 Point::new(1, 2)..Point::new(1, 2),
2506 Point::new(2, 2)..Point::new(2, 2),
2507 ],
2508 );
2509
2510 editor.newline(&Newline, window, cx);
2511 assert_eq!(
2512 editor.text(cx),
2513 "
2514 a
2515 b(
2516 )
2517 c(
2518 )
2519 "
2520 .unindent()
2521 );
2522
2523 // The selections are moved after the inserted newlines
2524 assert_eq!(
2525 editor.selections.ranges(cx),
2526 &[
2527 Point::new(2, 0)..Point::new(2, 0),
2528 Point::new(4, 0)..Point::new(4, 0),
2529 ],
2530 );
2531 });
2532}
2533
2534#[gpui::test]
2535async fn test_newline_above(cx: &mut gpui::TestAppContext) {
2536 init_test(cx, |settings| {
2537 settings.defaults.tab_size = NonZeroU32::new(4)
2538 });
2539
2540 let language = Arc::new(
2541 Language::new(
2542 LanguageConfig::default(),
2543 Some(tree_sitter_rust::LANGUAGE.into()),
2544 )
2545 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2546 .unwrap(),
2547 );
2548
2549 let mut cx = EditorTestContext::new(cx).await;
2550 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2551 cx.set_state(indoc! {"
2552 const a: ˇA = (
2553 (ˇ
2554 «const_functionˇ»(ˇ),
2555 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2556 )ˇ
2557 ˇ);ˇ
2558 "});
2559
2560 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2561 cx.assert_editor_state(indoc! {"
2562 ˇ
2563 const a: A = (
2564 ˇ
2565 (
2566 ˇ
2567 ˇ
2568 const_function(),
2569 ˇ
2570 ˇ
2571 ˇ
2572 ˇ
2573 something_else,
2574 ˇ
2575 )
2576 ˇ
2577 ˇ
2578 );
2579 "});
2580}
2581
2582#[gpui::test]
2583async fn test_newline_below(cx: &mut gpui::TestAppContext) {
2584 init_test(cx, |settings| {
2585 settings.defaults.tab_size = NonZeroU32::new(4)
2586 });
2587
2588 let language = Arc::new(
2589 Language::new(
2590 LanguageConfig::default(),
2591 Some(tree_sitter_rust::LANGUAGE.into()),
2592 )
2593 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2594 .unwrap(),
2595 );
2596
2597 let mut cx = EditorTestContext::new(cx).await;
2598 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2599 cx.set_state(indoc! {"
2600 const a: ˇA = (
2601 (ˇ
2602 «const_functionˇ»(ˇ),
2603 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2604 )ˇ
2605 ˇ);ˇ
2606 "});
2607
2608 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2609 cx.assert_editor_state(indoc! {"
2610 const a: A = (
2611 ˇ
2612 (
2613 ˇ
2614 const_function(),
2615 ˇ
2616 ˇ
2617 something_else,
2618 ˇ
2619 ˇ
2620 ˇ
2621 ˇ
2622 )
2623 ˇ
2624 );
2625 ˇ
2626 ˇ
2627 "});
2628}
2629
2630#[gpui::test]
2631async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
2632 init_test(cx, |settings| {
2633 settings.defaults.tab_size = NonZeroU32::new(4)
2634 });
2635
2636 let language = Arc::new(Language::new(
2637 LanguageConfig {
2638 line_comments: vec!["//".into()],
2639 ..LanguageConfig::default()
2640 },
2641 None,
2642 ));
2643 {
2644 let mut cx = EditorTestContext::new(cx).await;
2645 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2646 cx.set_state(indoc! {"
2647 // Fooˇ
2648 "});
2649
2650 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2651 cx.assert_editor_state(indoc! {"
2652 // Foo
2653 //ˇ
2654 "});
2655 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2656 cx.set_state(indoc! {"
2657 ˇ// Foo
2658 "});
2659 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2660 cx.assert_editor_state(indoc! {"
2661
2662 ˇ// Foo
2663 "});
2664 }
2665 // Ensure that comment continuations can be disabled.
2666 update_test_language_settings(cx, |settings| {
2667 settings.defaults.extend_comment_on_newline = Some(false);
2668 });
2669 let mut cx = EditorTestContext::new(cx).await;
2670 cx.set_state(indoc! {"
2671 // Fooˇ
2672 "});
2673 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2674 cx.assert_editor_state(indoc! {"
2675 // Foo
2676 ˇ
2677 "});
2678}
2679
2680#[gpui::test]
2681fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2682 init_test(cx, |_| {});
2683
2684 let editor = cx.add_window(|window, cx| {
2685 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2686 let mut editor = build_editor(buffer.clone(), window, cx);
2687 editor.change_selections(None, window, cx, |s| {
2688 s.select_ranges([3..4, 11..12, 19..20])
2689 });
2690 editor
2691 });
2692
2693 _ = editor.update(cx, |editor, window, cx| {
2694 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2695 editor.buffer.update(cx, |buffer, cx| {
2696 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2697 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2698 });
2699 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2700
2701 editor.insert("Z", window, cx);
2702 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2703
2704 // The selections are moved after the inserted characters
2705 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2706 });
2707}
2708
2709#[gpui::test]
2710async fn test_tab(cx: &mut gpui::TestAppContext) {
2711 init_test(cx, |settings| {
2712 settings.defaults.tab_size = NonZeroU32::new(3)
2713 });
2714
2715 let mut cx = EditorTestContext::new(cx).await;
2716 cx.set_state(indoc! {"
2717 ˇabˇc
2718 ˇ🏀ˇ🏀ˇefg
2719 dˇ
2720 "});
2721 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2722 cx.assert_editor_state(indoc! {"
2723 ˇab ˇc
2724 ˇ🏀 ˇ🏀 ˇefg
2725 d ˇ
2726 "});
2727
2728 cx.set_state(indoc! {"
2729 a
2730 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2731 "});
2732 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2733 cx.assert_editor_state(indoc! {"
2734 a
2735 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2736 "});
2737}
2738
2739#[gpui::test]
2740async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) {
2741 init_test(cx, |_| {});
2742
2743 let mut cx = EditorTestContext::new(cx).await;
2744 let language = Arc::new(
2745 Language::new(
2746 LanguageConfig::default(),
2747 Some(tree_sitter_rust::LANGUAGE.into()),
2748 )
2749 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2750 .unwrap(),
2751 );
2752 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2753
2754 // cursors that are already at the suggested indent level insert
2755 // a soft tab. cursors that are to the left of the suggested indent
2756 // auto-indent their line.
2757 cx.set_state(indoc! {"
2758 ˇ
2759 const a: B = (
2760 c(
2761 d(
2762 ˇ
2763 )
2764 ˇ
2765 ˇ )
2766 );
2767 "});
2768 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2769 cx.assert_editor_state(indoc! {"
2770 ˇ
2771 const a: B = (
2772 c(
2773 d(
2774 ˇ
2775 )
2776 ˇ
2777 ˇ)
2778 );
2779 "});
2780
2781 // handle auto-indent when there are multiple cursors on the same line
2782 cx.set_state(indoc! {"
2783 const a: B = (
2784 c(
2785 ˇ ˇ
2786 ˇ )
2787 );
2788 "});
2789 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2790 cx.assert_editor_state(indoc! {"
2791 const a: B = (
2792 c(
2793 ˇ
2794 ˇ)
2795 );
2796 "});
2797}
2798
2799#[gpui::test]
2800async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
2801 init_test(cx, |settings| {
2802 settings.defaults.tab_size = NonZeroU32::new(4)
2803 });
2804
2805 let language = Arc::new(
2806 Language::new(
2807 LanguageConfig::default(),
2808 Some(tree_sitter_rust::LANGUAGE.into()),
2809 )
2810 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2811 .unwrap(),
2812 );
2813
2814 let mut cx = EditorTestContext::new(cx).await;
2815 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2816 cx.set_state(indoc! {"
2817 fn a() {
2818 if b {
2819 \t ˇc
2820 }
2821 }
2822 "});
2823
2824 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2825 cx.assert_editor_state(indoc! {"
2826 fn a() {
2827 if b {
2828 ˇc
2829 }
2830 }
2831 "});
2832}
2833
2834#[gpui::test]
2835async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
2836 init_test(cx, |settings| {
2837 settings.defaults.tab_size = NonZeroU32::new(4);
2838 });
2839
2840 let mut cx = EditorTestContext::new(cx).await;
2841
2842 cx.set_state(indoc! {"
2843 «oneˇ» «twoˇ»
2844 three
2845 four
2846 "});
2847 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2848 cx.assert_editor_state(indoc! {"
2849 «oneˇ» «twoˇ»
2850 three
2851 four
2852 "});
2853
2854 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2855 cx.assert_editor_state(indoc! {"
2856 «oneˇ» «twoˇ»
2857 three
2858 four
2859 "});
2860
2861 // select across line ending
2862 cx.set_state(indoc! {"
2863 one two
2864 t«hree
2865 ˇ» four
2866 "});
2867 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2868 cx.assert_editor_state(indoc! {"
2869 one two
2870 t«hree
2871 ˇ» four
2872 "});
2873
2874 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2875 cx.assert_editor_state(indoc! {"
2876 one two
2877 t«hree
2878 ˇ» four
2879 "});
2880
2881 // Ensure that indenting/outdenting works when the cursor is at column 0.
2882 cx.set_state(indoc! {"
2883 one two
2884 ˇthree
2885 four
2886 "});
2887 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2888 cx.assert_editor_state(indoc! {"
2889 one two
2890 ˇthree
2891 four
2892 "});
2893
2894 cx.set_state(indoc! {"
2895 one two
2896 ˇ three
2897 four
2898 "});
2899 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2900 cx.assert_editor_state(indoc! {"
2901 one two
2902 ˇthree
2903 four
2904 "});
2905}
2906
2907#[gpui::test]
2908async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
2909 init_test(cx, |settings| {
2910 settings.defaults.hard_tabs = Some(true);
2911 });
2912
2913 let mut cx = EditorTestContext::new(cx).await;
2914
2915 // select two ranges on one line
2916 cx.set_state(indoc! {"
2917 «oneˇ» «twoˇ»
2918 three
2919 four
2920 "});
2921 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2922 cx.assert_editor_state(indoc! {"
2923 \t«oneˇ» «twoˇ»
2924 three
2925 four
2926 "});
2927 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2928 cx.assert_editor_state(indoc! {"
2929 \t\t«oneˇ» «twoˇ»
2930 three
2931 four
2932 "});
2933 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2934 cx.assert_editor_state(indoc! {"
2935 \t«oneˇ» «twoˇ»
2936 three
2937 four
2938 "});
2939 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2940 cx.assert_editor_state(indoc! {"
2941 «oneˇ» «twoˇ»
2942 three
2943 four
2944 "});
2945
2946 // select across a line ending
2947 cx.set_state(indoc! {"
2948 one two
2949 t«hree
2950 ˇ»four
2951 "});
2952 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2953 cx.assert_editor_state(indoc! {"
2954 one two
2955 \tt«hree
2956 ˇ»four
2957 "});
2958 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2959 cx.assert_editor_state(indoc! {"
2960 one two
2961 \t\tt«hree
2962 ˇ»four
2963 "});
2964 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2965 cx.assert_editor_state(indoc! {"
2966 one two
2967 \tt«hree
2968 ˇ»four
2969 "});
2970 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2971 cx.assert_editor_state(indoc! {"
2972 one two
2973 t«hree
2974 ˇ»four
2975 "});
2976
2977 // Ensure that indenting/outdenting works when the cursor is at column 0.
2978 cx.set_state(indoc! {"
2979 one two
2980 ˇthree
2981 four
2982 "});
2983 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2984 cx.assert_editor_state(indoc! {"
2985 one two
2986 ˇthree
2987 four
2988 "});
2989 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2990 cx.assert_editor_state(indoc! {"
2991 one two
2992 \tˇthree
2993 four
2994 "});
2995 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2996 cx.assert_editor_state(indoc! {"
2997 one two
2998 ˇthree
2999 four
3000 "});
3001}
3002
3003#[gpui::test]
3004fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3005 init_test(cx, |settings| {
3006 settings.languages.extend([
3007 (
3008 "TOML".into(),
3009 LanguageSettingsContent {
3010 tab_size: NonZeroU32::new(2),
3011 ..Default::default()
3012 },
3013 ),
3014 (
3015 "Rust".into(),
3016 LanguageSettingsContent {
3017 tab_size: NonZeroU32::new(4),
3018 ..Default::default()
3019 },
3020 ),
3021 ]);
3022 });
3023
3024 let toml_language = Arc::new(Language::new(
3025 LanguageConfig {
3026 name: "TOML".into(),
3027 ..Default::default()
3028 },
3029 None,
3030 ));
3031 let rust_language = Arc::new(Language::new(
3032 LanguageConfig {
3033 name: "Rust".into(),
3034 ..Default::default()
3035 },
3036 None,
3037 ));
3038
3039 let toml_buffer =
3040 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3041 let rust_buffer =
3042 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3043 let multibuffer = cx.new(|cx| {
3044 let mut multibuffer = MultiBuffer::new(ReadWrite);
3045 multibuffer.push_excerpts(
3046 toml_buffer.clone(),
3047 [ExcerptRange {
3048 context: Point::new(0, 0)..Point::new(2, 0),
3049 primary: None,
3050 }],
3051 cx,
3052 );
3053 multibuffer.push_excerpts(
3054 rust_buffer.clone(),
3055 [ExcerptRange {
3056 context: Point::new(0, 0)..Point::new(1, 0),
3057 primary: None,
3058 }],
3059 cx,
3060 );
3061 multibuffer
3062 });
3063
3064 cx.add_window(|window, cx| {
3065 let mut editor = build_editor(multibuffer, window, cx);
3066
3067 assert_eq!(
3068 editor.text(cx),
3069 indoc! {"
3070 a = 1
3071 b = 2
3072
3073 const c: usize = 3;
3074 "}
3075 );
3076
3077 select_ranges(
3078 &mut editor,
3079 indoc! {"
3080 «aˇ» = 1
3081 b = 2
3082
3083 «const c:ˇ» usize = 3;
3084 "},
3085 window,
3086 cx,
3087 );
3088
3089 editor.tab(&Tab, window, cx);
3090 assert_text_with_selections(
3091 &mut editor,
3092 indoc! {"
3093 «aˇ» = 1
3094 b = 2
3095
3096 «const c:ˇ» usize = 3;
3097 "},
3098 cx,
3099 );
3100 editor.tab_prev(&TabPrev, window, cx);
3101 assert_text_with_selections(
3102 &mut editor,
3103 indoc! {"
3104 «aˇ» = 1
3105 b = 2
3106
3107 «const c:ˇ» usize = 3;
3108 "},
3109 cx,
3110 );
3111
3112 editor
3113 });
3114}
3115
3116#[gpui::test]
3117async fn test_backspace(cx: &mut gpui::TestAppContext) {
3118 init_test(cx, |_| {});
3119
3120 let mut cx = EditorTestContext::new(cx).await;
3121
3122 // Basic backspace
3123 cx.set_state(indoc! {"
3124 onˇe two three
3125 fou«rˇ» five six
3126 seven «ˇeight nine
3127 »ten
3128 "});
3129 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3130 cx.assert_editor_state(indoc! {"
3131 oˇe two three
3132 fouˇ five six
3133 seven ˇten
3134 "});
3135
3136 // Test backspace inside and around indents
3137 cx.set_state(indoc! {"
3138 zero
3139 ˇone
3140 ˇtwo
3141 ˇ ˇ ˇ three
3142 ˇ ˇ four
3143 "});
3144 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3145 cx.assert_editor_state(indoc! {"
3146 zero
3147 ˇone
3148 ˇtwo
3149 ˇ threeˇ four
3150 "});
3151
3152 // Test backspace with line_mode set to true
3153 cx.update_editor(|e, _, _| e.selections.line_mode = true);
3154 cx.set_state(indoc! {"
3155 The ˇquick ˇbrown
3156 fox jumps over
3157 the lazy dog
3158 ˇThe qu«ick bˇ»rown"});
3159 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3160 cx.assert_editor_state(indoc! {"
3161 ˇfox jumps over
3162 the lazy dogˇ"});
3163}
3164
3165#[gpui::test]
3166async fn test_delete(cx: &mut gpui::TestAppContext) {
3167 init_test(cx, |_| {});
3168
3169 let mut cx = EditorTestContext::new(cx).await;
3170 cx.set_state(indoc! {"
3171 onˇe two three
3172 fou«rˇ» five six
3173 seven «ˇeight nine
3174 »ten
3175 "});
3176 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3177 cx.assert_editor_state(indoc! {"
3178 onˇ two three
3179 fouˇ five six
3180 seven ˇten
3181 "});
3182
3183 // Test backspace with line_mode set to true
3184 cx.update_editor(|e, _, _| e.selections.line_mode = true);
3185 cx.set_state(indoc! {"
3186 The ˇquick ˇbrown
3187 fox «ˇjum»ps over
3188 the lazy dog
3189 ˇThe qu«ick bˇ»rown"});
3190 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3191 cx.assert_editor_state("ˇthe lazy dogˇ");
3192}
3193
3194#[gpui::test]
3195fn test_delete_line(cx: &mut TestAppContext) {
3196 init_test(cx, |_| {});
3197
3198 let editor = cx.add_window(|window, cx| {
3199 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3200 build_editor(buffer, window, cx)
3201 });
3202 _ = editor.update(cx, |editor, window, cx| {
3203 editor.change_selections(None, window, cx, |s| {
3204 s.select_display_ranges([
3205 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3206 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3207 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3208 ])
3209 });
3210 editor.delete_line(&DeleteLine, window, cx);
3211 assert_eq!(editor.display_text(cx), "ghi");
3212 assert_eq!(
3213 editor.selections.display_ranges(cx),
3214 vec![
3215 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3216 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3217 ]
3218 );
3219 });
3220
3221 let editor = cx.add_window(|window, cx| {
3222 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3223 build_editor(buffer, window, cx)
3224 });
3225 _ = editor.update(cx, |editor, window, cx| {
3226 editor.change_selections(None, window, cx, |s| {
3227 s.select_display_ranges([
3228 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3229 ])
3230 });
3231 editor.delete_line(&DeleteLine, window, cx);
3232 assert_eq!(editor.display_text(cx), "ghi\n");
3233 assert_eq!(
3234 editor.selections.display_ranges(cx),
3235 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3236 );
3237 });
3238}
3239
3240#[gpui::test]
3241fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3242 init_test(cx, |_| {});
3243
3244 cx.add_window(|window, cx| {
3245 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3246 let mut editor = build_editor(buffer.clone(), window, cx);
3247 let buffer = buffer.read(cx).as_singleton().unwrap();
3248
3249 assert_eq!(
3250 editor.selections.ranges::<Point>(cx),
3251 &[Point::new(0, 0)..Point::new(0, 0)]
3252 );
3253
3254 // When on single line, replace newline at end by space
3255 editor.join_lines(&JoinLines, window, cx);
3256 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3257 assert_eq!(
3258 editor.selections.ranges::<Point>(cx),
3259 &[Point::new(0, 3)..Point::new(0, 3)]
3260 );
3261
3262 // When multiple lines are selected, remove newlines that are spanned by the selection
3263 editor.change_selections(None, window, cx, |s| {
3264 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3265 });
3266 editor.join_lines(&JoinLines, window, cx);
3267 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3268 assert_eq!(
3269 editor.selections.ranges::<Point>(cx),
3270 &[Point::new(0, 11)..Point::new(0, 11)]
3271 );
3272
3273 // Undo should be transactional
3274 editor.undo(&Undo, window, cx);
3275 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3276 assert_eq!(
3277 editor.selections.ranges::<Point>(cx),
3278 &[Point::new(0, 5)..Point::new(2, 2)]
3279 );
3280
3281 // When joining an empty line don't insert a space
3282 editor.change_selections(None, window, cx, |s| {
3283 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3284 });
3285 editor.join_lines(&JoinLines, window, cx);
3286 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3287 assert_eq!(
3288 editor.selections.ranges::<Point>(cx),
3289 [Point::new(2, 3)..Point::new(2, 3)]
3290 );
3291
3292 // We can remove trailing newlines
3293 editor.join_lines(&JoinLines, window, cx);
3294 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3295 assert_eq!(
3296 editor.selections.ranges::<Point>(cx),
3297 [Point::new(2, 3)..Point::new(2, 3)]
3298 );
3299
3300 // We don't blow up on the last line
3301 editor.join_lines(&JoinLines, window, cx);
3302 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3303 assert_eq!(
3304 editor.selections.ranges::<Point>(cx),
3305 [Point::new(2, 3)..Point::new(2, 3)]
3306 );
3307
3308 // reset to test indentation
3309 editor.buffer.update(cx, |buffer, cx| {
3310 buffer.edit(
3311 [
3312 (Point::new(1, 0)..Point::new(1, 2), " "),
3313 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3314 ],
3315 None,
3316 cx,
3317 )
3318 });
3319
3320 // We remove any leading spaces
3321 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3322 editor.change_selections(None, window, cx, |s| {
3323 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3324 });
3325 editor.join_lines(&JoinLines, window, cx);
3326 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3327
3328 // We don't insert a space for a line containing only spaces
3329 editor.join_lines(&JoinLines, window, cx);
3330 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3331
3332 // We ignore any leading tabs
3333 editor.join_lines(&JoinLines, window, cx);
3334 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3335
3336 editor
3337 });
3338}
3339
3340#[gpui::test]
3341fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3342 init_test(cx, |_| {});
3343
3344 cx.add_window(|window, cx| {
3345 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3346 let mut editor = build_editor(buffer.clone(), window, cx);
3347 let buffer = buffer.read(cx).as_singleton().unwrap();
3348
3349 editor.change_selections(None, window, cx, |s| {
3350 s.select_ranges([
3351 Point::new(0, 2)..Point::new(1, 1),
3352 Point::new(1, 2)..Point::new(1, 2),
3353 Point::new(3, 1)..Point::new(3, 2),
3354 ])
3355 });
3356
3357 editor.join_lines(&JoinLines, window, cx);
3358 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3359
3360 assert_eq!(
3361 editor.selections.ranges::<Point>(cx),
3362 [
3363 Point::new(0, 7)..Point::new(0, 7),
3364 Point::new(1, 3)..Point::new(1, 3)
3365 ]
3366 );
3367 editor
3368 });
3369}
3370
3371#[gpui::test]
3372async fn test_join_lines_with_git_diff_base(
3373 executor: BackgroundExecutor,
3374 cx: &mut gpui::TestAppContext,
3375) {
3376 init_test(cx, |_| {});
3377
3378 let mut cx = EditorTestContext::new(cx).await;
3379
3380 let diff_base = r#"
3381 Line 0
3382 Line 1
3383 Line 2
3384 Line 3
3385 "#
3386 .unindent();
3387
3388 cx.set_state(
3389 &r#"
3390 ˇLine 0
3391 Line 1
3392 Line 2
3393 Line 3
3394 "#
3395 .unindent(),
3396 );
3397
3398 cx.set_diff_base(&diff_base);
3399 executor.run_until_parked();
3400
3401 // Join lines
3402 cx.update_editor(|editor, window, cx| {
3403 editor.join_lines(&JoinLines, window, cx);
3404 });
3405 executor.run_until_parked();
3406
3407 cx.assert_editor_state(
3408 &r#"
3409 Line 0ˇ Line 1
3410 Line 2
3411 Line 3
3412 "#
3413 .unindent(),
3414 );
3415 // Join again
3416 cx.update_editor(|editor, window, cx| {
3417 editor.join_lines(&JoinLines, window, cx);
3418 });
3419 executor.run_until_parked();
3420
3421 cx.assert_editor_state(
3422 &r#"
3423 Line 0 Line 1ˇ Line 2
3424 Line 3
3425 "#
3426 .unindent(),
3427 );
3428}
3429
3430#[gpui::test]
3431async fn test_custom_newlines_cause_no_false_positive_diffs(
3432 executor: BackgroundExecutor,
3433 cx: &mut gpui::TestAppContext,
3434) {
3435 init_test(cx, |_| {});
3436 let mut cx = EditorTestContext::new(cx).await;
3437 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3438 cx.set_diff_base("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3439 executor.run_until_parked();
3440
3441 cx.update_editor(|editor, window, cx| {
3442 let snapshot = editor.snapshot(window, cx);
3443 assert_eq!(
3444 snapshot
3445 .buffer_snapshot
3446 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3447 .collect::<Vec<_>>(),
3448 Vec::new(),
3449 "Should not have any diffs for files with custom newlines"
3450 );
3451 });
3452}
3453
3454#[gpui::test]
3455async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3456 init_test(cx, |_| {});
3457
3458 let mut cx = EditorTestContext::new(cx).await;
3459
3460 // Test sort_lines_case_insensitive()
3461 cx.set_state(indoc! {"
3462 «z
3463 y
3464 x
3465 Z
3466 Y
3467 Xˇ»
3468 "});
3469 cx.update_editor(|e, window, cx| {
3470 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3471 });
3472 cx.assert_editor_state(indoc! {"
3473 «x
3474 X
3475 y
3476 Y
3477 z
3478 Zˇ»
3479 "});
3480
3481 // Test reverse_lines()
3482 cx.set_state(indoc! {"
3483 «5
3484 4
3485 3
3486 2
3487 1ˇ»
3488 "});
3489 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3490 cx.assert_editor_state(indoc! {"
3491 «1
3492 2
3493 3
3494 4
3495 5ˇ»
3496 "});
3497
3498 // Skip testing shuffle_line()
3499
3500 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3501 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3502
3503 // Don't manipulate when cursor is on single line, but expand the selection
3504 cx.set_state(indoc! {"
3505 ddˇdd
3506 ccc
3507 bb
3508 a
3509 "});
3510 cx.update_editor(|e, window, cx| {
3511 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3512 });
3513 cx.assert_editor_state(indoc! {"
3514 «ddddˇ»
3515 ccc
3516 bb
3517 a
3518 "});
3519
3520 // Basic manipulate case
3521 // Start selection moves to column 0
3522 // End of selection shrinks to fit shorter line
3523 cx.set_state(indoc! {"
3524 dd«d
3525 ccc
3526 bb
3527 aaaaaˇ»
3528 "});
3529 cx.update_editor(|e, window, cx| {
3530 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3531 });
3532 cx.assert_editor_state(indoc! {"
3533 «aaaaa
3534 bb
3535 ccc
3536 dddˇ»
3537 "});
3538
3539 // Manipulate case with newlines
3540 cx.set_state(indoc! {"
3541 dd«d
3542 ccc
3543
3544 bb
3545 aaaaa
3546
3547 ˇ»
3548 "});
3549 cx.update_editor(|e, window, cx| {
3550 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3551 });
3552 cx.assert_editor_state(indoc! {"
3553 «
3554
3555 aaaaa
3556 bb
3557 ccc
3558 dddˇ»
3559
3560 "});
3561
3562 // Adding new line
3563 cx.set_state(indoc! {"
3564 aa«a
3565 bbˇ»b
3566 "});
3567 cx.update_editor(|e, window, cx| {
3568 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3569 });
3570 cx.assert_editor_state(indoc! {"
3571 «aaa
3572 bbb
3573 added_lineˇ»
3574 "});
3575
3576 // Removing line
3577 cx.set_state(indoc! {"
3578 aa«a
3579 bbbˇ»
3580 "});
3581 cx.update_editor(|e, window, cx| {
3582 e.manipulate_lines(window, cx, |lines| {
3583 lines.pop();
3584 })
3585 });
3586 cx.assert_editor_state(indoc! {"
3587 «aaaˇ»
3588 "});
3589
3590 // Removing all lines
3591 cx.set_state(indoc! {"
3592 aa«a
3593 bbbˇ»
3594 "});
3595 cx.update_editor(|e, window, cx| {
3596 e.manipulate_lines(window, cx, |lines| {
3597 lines.drain(..);
3598 })
3599 });
3600 cx.assert_editor_state(indoc! {"
3601 ˇ
3602 "});
3603}
3604
3605#[gpui::test]
3606async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3607 init_test(cx, |_| {});
3608
3609 let mut cx = EditorTestContext::new(cx).await;
3610
3611 // Consider continuous selection as single selection
3612 cx.set_state(indoc! {"
3613 Aaa«aa
3614 cˇ»c«c
3615 bb
3616 aaaˇ»aa
3617 "});
3618 cx.update_editor(|e, window, cx| {
3619 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3620 });
3621 cx.assert_editor_state(indoc! {"
3622 «Aaaaa
3623 ccc
3624 bb
3625 aaaaaˇ»
3626 "});
3627
3628 cx.set_state(indoc! {"
3629 Aaa«aa
3630 cˇ»c«c
3631 bb
3632 aaaˇ»aa
3633 "});
3634 cx.update_editor(|e, window, cx| {
3635 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3636 });
3637 cx.assert_editor_state(indoc! {"
3638 «Aaaaa
3639 ccc
3640 bbˇ»
3641 "});
3642
3643 // Consider non continuous selection as distinct dedup operations
3644 cx.set_state(indoc! {"
3645 «aaaaa
3646 bb
3647 aaaaa
3648 aaaaaˇ»
3649
3650 aaa«aaˇ»
3651 "});
3652 cx.update_editor(|e, window, cx| {
3653 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3654 });
3655 cx.assert_editor_state(indoc! {"
3656 «aaaaa
3657 bbˇ»
3658
3659 «aaaaaˇ»
3660 "});
3661}
3662
3663#[gpui::test]
3664async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3665 init_test(cx, |_| {});
3666
3667 let mut cx = EditorTestContext::new(cx).await;
3668
3669 cx.set_state(indoc! {"
3670 «Aaa
3671 aAa
3672 Aaaˇ»
3673 "});
3674 cx.update_editor(|e, window, cx| {
3675 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3676 });
3677 cx.assert_editor_state(indoc! {"
3678 «Aaa
3679 aAaˇ»
3680 "});
3681
3682 cx.set_state(indoc! {"
3683 «Aaa
3684 aAa
3685 aaAˇ»
3686 "});
3687 cx.update_editor(|e, window, cx| {
3688 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3689 });
3690 cx.assert_editor_state(indoc! {"
3691 «Aaaˇ»
3692 "});
3693}
3694
3695#[gpui::test]
3696async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3697 init_test(cx, |_| {});
3698
3699 let mut cx = EditorTestContext::new(cx).await;
3700
3701 // Manipulate with multiple selections on a single line
3702 cx.set_state(indoc! {"
3703 dd«dd
3704 cˇ»c«c
3705 bb
3706 aaaˇ»aa
3707 "});
3708 cx.update_editor(|e, window, cx| {
3709 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3710 });
3711 cx.assert_editor_state(indoc! {"
3712 «aaaaa
3713 bb
3714 ccc
3715 ddddˇ»
3716 "});
3717
3718 // Manipulate with multiple disjoin selections
3719 cx.set_state(indoc! {"
3720 5«
3721 4
3722 3
3723 2
3724 1ˇ»
3725
3726 dd«dd
3727 ccc
3728 bb
3729 aaaˇ»aa
3730 "});
3731 cx.update_editor(|e, window, cx| {
3732 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3733 });
3734 cx.assert_editor_state(indoc! {"
3735 «1
3736 2
3737 3
3738 4
3739 5ˇ»
3740
3741 «aaaaa
3742 bb
3743 ccc
3744 ddddˇ»
3745 "});
3746
3747 // Adding lines on each selection
3748 cx.set_state(indoc! {"
3749 2«
3750 1ˇ»
3751
3752 bb«bb
3753 aaaˇ»aa
3754 "});
3755 cx.update_editor(|e, window, cx| {
3756 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3757 });
3758 cx.assert_editor_state(indoc! {"
3759 «2
3760 1
3761 added lineˇ»
3762
3763 «bbbb
3764 aaaaa
3765 added lineˇ»
3766 "});
3767
3768 // Removing lines on each selection
3769 cx.set_state(indoc! {"
3770 2«
3771 1ˇ»
3772
3773 bb«bb
3774 aaaˇ»aa
3775 "});
3776 cx.update_editor(|e, window, cx| {
3777 e.manipulate_lines(window, cx, |lines| {
3778 lines.pop();
3779 })
3780 });
3781 cx.assert_editor_state(indoc! {"
3782 «2ˇ»
3783
3784 «bbbbˇ»
3785 "});
3786}
3787
3788#[gpui::test]
3789async fn test_manipulate_text(cx: &mut TestAppContext) {
3790 init_test(cx, |_| {});
3791
3792 let mut cx = EditorTestContext::new(cx).await;
3793
3794 // Test convert_to_upper_case()
3795 cx.set_state(indoc! {"
3796 «hello worldˇ»
3797 "});
3798 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3799 cx.assert_editor_state(indoc! {"
3800 «HELLO WORLDˇ»
3801 "});
3802
3803 // Test convert_to_lower_case()
3804 cx.set_state(indoc! {"
3805 «HELLO WORLDˇ»
3806 "});
3807 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3808 cx.assert_editor_state(indoc! {"
3809 «hello worldˇ»
3810 "});
3811
3812 // Test multiple line, single selection case
3813 cx.set_state(indoc! {"
3814 «The quick brown
3815 fox jumps over
3816 the lazy dogˇ»
3817 "});
3818 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3819 cx.assert_editor_state(indoc! {"
3820 «The Quick Brown
3821 Fox Jumps Over
3822 The Lazy Dogˇ»
3823 "});
3824
3825 // Test multiple line, single selection case
3826 cx.set_state(indoc! {"
3827 «The quick brown
3828 fox jumps over
3829 the lazy dogˇ»
3830 "});
3831 cx.update_editor(|e, window, cx| {
3832 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
3833 });
3834 cx.assert_editor_state(indoc! {"
3835 «TheQuickBrown
3836 FoxJumpsOver
3837 TheLazyDogˇ»
3838 "});
3839
3840 // From here on out, test more complex cases of manipulate_text()
3841
3842 // Test no selection case - should affect words cursors are in
3843 // Cursor at beginning, middle, and end of word
3844 cx.set_state(indoc! {"
3845 ˇhello big beauˇtiful worldˇ
3846 "});
3847 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3848 cx.assert_editor_state(indoc! {"
3849 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
3850 "});
3851
3852 // Test multiple selections on a single line and across multiple lines
3853 cx.set_state(indoc! {"
3854 «Theˇ» quick «brown
3855 foxˇ» jumps «overˇ»
3856 the «lazyˇ» dog
3857 "});
3858 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3859 cx.assert_editor_state(indoc! {"
3860 «THEˇ» quick «BROWN
3861 FOXˇ» jumps «OVERˇ»
3862 the «LAZYˇ» dog
3863 "});
3864
3865 // Test case where text length grows
3866 cx.set_state(indoc! {"
3867 «tschüߡ»
3868 "});
3869 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3870 cx.assert_editor_state(indoc! {"
3871 «TSCHÜSSˇ»
3872 "});
3873
3874 // Test to make sure we don't crash when text shrinks
3875 cx.set_state(indoc! {"
3876 aaa_bbbˇ
3877 "});
3878 cx.update_editor(|e, window, cx| {
3879 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3880 });
3881 cx.assert_editor_state(indoc! {"
3882 «aaaBbbˇ»
3883 "});
3884
3885 // Test to make sure we all aware of the fact that each word can grow and shrink
3886 // Final selections should be aware of this fact
3887 cx.set_state(indoc! {"
3888 aaa_bˇbb bbˇb_ccc ˇccc_ddd
3889 "});
3890 cx.update_editor(|e, window, cx| {
3891 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3892 });
3893 cx.assert_editor_state(indoc! {"
3894 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
3895 "});
3896
3897 cx.set_state(indoc! {"
3898 «hElLo, WoRld!ˇ»
3899 "});
3900 cx.update_editor(|e, window, cx| {
3901 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
3902 });
3903 cx.assert_editor_state(indoc! {"
3904 «HeLlO, wOrLD!ˇ»
3905 "});
3906}
3907
3908#[gpui::test]
3909fn test_duplicate_line(cx: &mut TestAppContext) {
3910 init_test(cx, |_| {});
3911
3912 let editor = cx.add_window(|window, cx| {
3913 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3914 build_editor(buffer, window, cx)
3915 });
3916 _ = editor.update(cx, |editor, window, cx| {
3917 editor.change_selections(None, window, cx, |s| {
3918 s.select_display_ranges([
3919 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3920 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3921 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
3922 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3923 ])
3924 });
3925 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
3926 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
3927 assert_eq!(
3928 editor.selections.display_ranges(cx),
3929 vec![
3930 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3931 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3932 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3933 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
3934 ]
3935 );
3936 });
3937
3938 let editor = cx.add_window(|window, cx| {
3939 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3940 build_editor(buffer, window, cx)
3941 });
3942 _ = editor.update(cx, |editor, window, cx| {
3943 editor.change_selections(None, window, cx, |s| {
3944 s.select_display_ranges([
3945 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
3946 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
3947 ])
3948 });
3949 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
3950 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
3951 assert_eq!(
3952 editor.selections.display_ranges(cx),
3953 vec![
3954 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
3955 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
3956 ]
3957 );
3958 });
3959
3960 // With `move_upwards` the selections stay in place, except for
3961 // the lines inserted above them
3962 let editor = cx.add_window(|window, cx| {
3963 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3964 build_editor(buffer, window, cx)
3965 });
3966 _ = editor.update(cx, |editor, window, cx| {
3967 editor.change_selections(None, window, cx, |s| {
3968 s.select_display_ranges([
3969 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3970 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3971 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
3972 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3973 ])
3974 });
3975 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
3976 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
3977 assert_eq!(
3978 editor.selections.display_ranges(cx),
3979 vec![
3980 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3981 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3982 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
3983 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
3984 ]
3985 );
3986 });
3987
3988 let editor = cx.add_window(|window, cx| {
3989 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3990 build_editor(buffer, window, cx)
3991 });
3992 _ = editor.update(cx, |editor, window, cx| {
3993 editor.change_selections(None, window, cx, |s| {
3994 s.select_display_ranges([
3995 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
3996 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
3997 ])
3998 });
3999 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4000 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4001 assert_eq!(
4002 editor.selections.display_ranges(cx),
4003 vec![
4004 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4005 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4006 ]
4007 );
4008 });
4009
4010 let editor = cx.add_window(|window, cx| {
4011 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4012 build_editor(buffer, window, cx)
4013 });
4014 _ = editor.update(cx, |editor, window, cx| {
4015 editor.change_selections(None, window, cx, |s| {
4016 s.select_display_ranges([
4017 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4018 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4019 ])
4020 });
4021 editor.duplicate_selection(&DuplicateSelection, window, cx);
4022 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4023 assert_eq!(
4024 editor.selections.display_ranges(cx),
4025 vec![
4026 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4027 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4028 ]
4029 );
4030 });
4031}
4032
4033#[gpui::test]
4034fn test_move_line_up_down(cx: &mut TestAppContext) {
4035 init_test(cx, |_| {});
4036
4037 let editor = cx.add_window(|window, cx| {
4038 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4039 build_editor(buffer, window, cx)
4040 });
4041 _ = editor.update(cx, |editor, window, cx| {
4042 editor.fold_creases(
4043 vec![
4044 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4045 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4046 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4047 ],
4048 true,
4049 window,
4050 cx,
4051 );
4052 editor.change_selections(None, window, cx, |s| {
4053 s.select_display_ranges([
4054 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4055 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4056 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4057 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4058 ])
4059 });
4060 assert_eq!(
4061 editor.display_text(cx),
4062 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4063 );
4064
4065 editor.move_line_up(&MoveLineUp, window, cx);
4066 assert_eq!(
4067 editor.display_text(cx),
4068 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4069 );
4070 assert_eq!(
4071 editor.selections.display_ranges(cx),
4072 vec![
4073 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4074 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4075 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4076 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4077 ]
4078 );
4079 });
4080
4081 _ = editor.update(cx, |editor, window, cx| {
4082 editor.move_line_down(&MoveLineDown, window, cx);
4083 assert_eq!(
4084 editor.display_text(cx),
4085 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4086 );
4087 assert_eq!(
4088 editor.selections.display_ranges(cx),
4089 vec![
4090 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4091 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4092 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4093 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4094 ]
4095 );
4096 });
4097
4098 _ = editor.update(cx, |editor, window, cx| {
4099 editor.move_line_down(&MoveLineDown, window, cx);
4100 assert_eq!(
4101 editor.display_text(cx),
4102 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4103 );
4104 assert_eq!(
4105 editor.selections.display_ranges(cx),
4106 vec![
4107 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4108 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4109 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4110 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4111 ]
4112 );
4113 });
4114
4115 _ = editor.update(cx, |editor, window, cx| {
4116 editor.move_line_up(&MoveLineUp, window, cx);
4117 assert_eq!(
4118 editor.display_text(cx),
4119 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4120 );
4121 assert_eq!(
4122 editor.selections.display_ranges(cx),
4123 vec![
4124 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4125 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4126 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4127 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4128 ]
4129 );
4130 });
4131}
4132
4133#[gpui::test]
4134fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4135 init_test(cx, |_| {});
4136
4137 let editor = cx.add_window(|window, cx| {
4138 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4139 build_editor(buffer, window, cx)
4140 });
4141 _ = editor.update(cx, |editor, window, cx| {
4142 let snapshot = editor.buffer.read(cx).snapshot(cx);
4143 editor.insert_blocks(
4144 [BlockProperties {
4145 style: BlockStyle::Fixed,
4146 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4147 height: 1,
4148 render: Arc::new(|_| div().into_any()),
4149 priority: 0,
4150 }],
4151 Some(Autoscroll::fit()),
4152 cx,
4153 );
4154 editor.change_selections(None, window, cx, |s| {
4155 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4156 });
4157 editor.move_line_down(&MoveLineDown, window, cx);
4158 });
4159}
4160
4161#[gpui::test]
4162async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4163 init_test(cx, |_| {});
4164
4165 let mut cx = EditorTestContext::new(cx).await;
4166 cx.set_state(
4167 &"
4168 ˇzero
4169 one
4170 two
4171 three
4172 four
4173 five
4174 "
4175 .unindent(),
4176 );
4177
4178 // Create a four-line block that replaces three lines of text.
4179 cx.update_editor(|editor, window, cx| {
4180 let snapshot = editor.snapshot(window, cx);
4181 let snapshot = &snapshot.buffer_snapshot;
4182 let placement = BlockPlacement::Replace(
4183 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4184 );
4185 editor.insert_blocks(
4186 [BlockProperties {
4187 placement,
4188 height: 4,
4189 style: BlockStyle::Sticky,
4190 render: Arc::new(|_| gpui::div().into_any_element()),
4191 priority: 0,
4192 }],
4193 None,
4194 cx,
4195 );
4196 });
4197
4198 // Move down so that the cursor touches the block.
4199 cx.update_editor(|editor, window, cx| {
4200 editor.move_down(&Default::default(), window, cx);
4201 });
4202 cx.assert_editor_state(
4203 &"
4204 zero
4205 «one
4206 two
4207 threeˇ»
4208 four
4209 five
4210 "
4211 .unindent(),
4212 );
4213
4214 // Move down past the block.
4215 cx.update_editor(|editor, window, cx| {
4216 editor.move_down(&Default::default(), window, cx);
4217 });
4218 cx.assert_editor_state(
4219 &"
4220 zero
4221 one
4222 two
4223 three
4224 ˇfour
4225 five
4226 "
4227 .unindent(),
4228 );
4229}
4230
4231#[gpui::test]
4232fn test_transpose(cx: &mut TestAppContext) {
4233 init_test(cx, |_| {});
4234
4235 _ = cx.add_window(|window, cx| {
4236 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4237 editor.set_style(EditorStyle::default(), window, cx);
4238 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4239 editor.transpose(&Default::default(), window, cx);
4240 assert_eq!(editor.text(cx), "bac");
4241 assert_eq!(editor.selections.ranges(cx), [2..2]);
4242
4243 editor.transpose(&Default::default(), window, cx);
4244 assert_eq!(editor.text(cx), "bca");
4245 assert_eq!(editor.selections.ranges(cx), [3..3]);
4246
4247 editor.transpose(&Default::default(), window, cx);
4248 assert_eq!(editor.text(cx), "bac");
4249 assert_eq!(editor.selections.ranges(cx), [3..3]);
4250
4251 editor
4252 });
4253
4254 _ = cx.add_window(|window, cx| {
4255 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4256 editor.set_style(EditorStyle::default(), window, cx);
4257 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4258 editor.transpose(&Default::default(), window, cx);
4259 assert_eq!(editor.text(cx), "acb\nde");
4260 assert_eq!(editor.selections.ranges(cx), [3..3]);
4261
4262 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4263 editor.transpose(&Default::default(), window, cx);
4264 assert_eq!(editor.text(cx), "acbd\ne");
4265 assert_eq!(editor.selections.ranges(cx), [5..5]);
4266
4267 editor.transpose(&Default::default(), window, cx);
4268 assert_eq!(editor.text(cx), "acbde\n");
4269 assert_eq!(editor.selections.ranges(cx), [6..6]);
4270
4271 editor.transpose(&Default::default(), window, cx);
4272 assert_eq!(editor.text(cx), "acbd\ne");
4273 assert_eq!(editor.selections.ranges(cx), [6..6]);
4274
4275 editor
4276 });
4277
4278 _ = cx.add_window(|window, cx| {
4279 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4280 editor.set_style(EditorStyle::default(), window, cx);
4281 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4282 editor.transpose(&Default::default(), window, cx);
4283 assert_eq!(editor.text(cx), "bacd\ne");
4284 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4285
4286 editor.transpose(&Default::default(), window, cx);
4287 assert_eq!(editor.text(cx), "bcade\n");
4288 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4289
4290 editor.transpose(&Default::default(), window, cx);
4291 assert_eq!(editor.text(cx), "bcda\ne");
4292 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4293
4294 editor.transpose(&Default::default(), window, cx);
4295 assert_eq!(editor.text(cx), "bcade\n");
4296 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4297
4298 editor.transpose(&Default::default(), window, cx);
4299 assert_eq!(editor.text(cx), "bcaed\n");
4300 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4301
4302 editor
4303 });
4304
4305 _ = cx.add_window(|window, cx| {
4306 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4307 editor.set_style(EditorStyle::default(), window, cx);
4308 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4309 editor.transpose(&Default::default(), window, cx);
4310 assert_eq!(editor.text(cx), "🏀🍐✋");
4311 assert_eq!(editor.selections.ranges(cx), [8..8]);
4312
4313 editor.transpose(&Default::default(), window, cx);
4314 assert_eq!(editor.text(cx), "🏀✋🍐");
4315 assert_eq!(editor.selections.ranges(cx), [11..11]);
4316
4317 editor.transpose(&Default::default(), window, cx);
4318 assert_eq!(editor.text(cx), "🏀🍐✋");
4319 assert_eq!(editor.selections.ranges(cx), [11..11]);
4320
4321 editor
4322 });
4323}
4324
4325#[gpui::test]
4326async fn test_rewrap(cx: &mut TestAppContext) {
4327 init_test(cx, |_| {});
4328
4329 let mut cx = EditorTestContext::new(cx).await;
4330
4331 let language_with_c_comments = Arc::new(Language::new(
4332 LanguageConfig {
4333 line_comments: vec!["// ".into()],
4334 ..LanguageConfig::default()
4335 },
4336 None,
4337 ));
4338 let language_with_pound_comments = Arc::new(Language::new(
4339 LanguageConfig {
4340 line_comments: vec!["# ".into()],
4341 ..LanguageConfig::default()
4342 },
4343 None,
4344 ));
4345 let markdown_language = Arc::new(Language::new(
4346 LanguageConfig {
4347 name: "Markdown".into(),
4348 ..LanguageConfig::default()
4349 },
4350 None,
4351 ));
4352 let language_with_doc_comments = Arc::new(Language::new(
4353 LanguageConfig {
4354 line_comments: vec!["// ".into(), "/// ".into()],
4355 ..LanguageConfig::default()
4356 },
4357 Some(tree_sitter_rust::LANGUAGE.into()),
4358 ));
4359
4360 let plaintext_language = Arc::new(Language::new(
4361 LanguageConfig {
4362 name: "Plain Text".into(),
4363 ..LanguageConfig::default()
4364 },
4365 None,
4366 ));
4367
4368 assert_rewrap(
4369 indoc! {"
4370 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4371 "},
4372 indoc! {"
4373 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4374 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4375 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4376 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4377 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4378 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4379 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4380 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4381 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4382 // porttitor id. Aliquam id accumsan eros.
4383 "},
4384 language_with_c_comments.clone(),
4385 &mut cx,
4386 );
4387
4388 // Test that rewrapping works inside of a selection
4389 assert_rewrap(
4390 indoc! {"
4391 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.ˇ»
4392 "},
4393 indoc! {"
4394 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4395 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4396 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4397 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4398 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4399 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4400 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4401 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4402 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4403 // porttitor id. Aliquam id accumsan eros.ˇ»
4404 "},
4405 language_with_c_comments.clone(),
4406 &mut cx,
4407 );
4408
4409 // Test that cursors that expand to the same region are collapsed.
4410 assert_rewrap(
4411 indoc! {"
4412 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4413 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4414 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4415 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4416 "},
4417 indoc! {"
4418 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4419 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4420 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4421 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4422 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4423 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4424 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4425 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4426 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4427 // porttitor id. Aliquam id accumsan eros.
4428 "},
4429 language_with_c_comments.clone(),
4430 &mut cx,
4431 );
4432
4433 // Test that non-contiguous selections are treated separately.
4434 assert_rewrap(
4435 indoc! {"
4436 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4437 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4438 //
4439 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4440 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4441 "},
4442 indoc! {"
4443 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4444 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4445 // auctor, eu lacinia sapien scelerisque.
4446 //
4447 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4448 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4449 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4450 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4451 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4452 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4453 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4454 "},
4455 language_with_c_comments.clone(),
4456 &mut cx,
4457 );
4458
4459 // Test that different comment prefixes are supported.
4460 assert_rewrap(
4461 indoc! {"
4462 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4463 "},
4464 indoc! {"
4465 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4466 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4467 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4468 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4469 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4470 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4471 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4472 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4473 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4474 # accumsan eros.
4475 "},
4476 language_with_pound_comments.clone(),
4477 &mut cx,
4478 );
4479
4480 // Test that rewrapping is ignored outside of comments in most languages.
4481 assert_rewrap(
4482 indoc! {"
4483 /// Adds two numbers.
4484 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4485 fn add(a: u32, b: u32) -> u32 {
4486 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
4487 }
4488 "},
4489 indoc! {"
4490 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4491 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4492 fn add(a: u32, b: u32) -> u32 {
4493 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
4494 }
4495 "},
4496 language_with_doc_comments.clone(),
4497 &mut cx,
4498 );
4499
4500 // Test that rewrapping works in Markdown and Plain Text languages.
4501 assert_rewrap(
4502 indoc! {"
4503 # Hello
4504
4505 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
4506 "},
4507 indoc! {"
4508 # Hello
4509
4510 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4511 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4512 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4513 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4514 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4515 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4516 Integer sit amet scelerisque nisi.
4517 "},
4518 markdown_language,
4519 &mut cx,
4520 );
4521
4522 assert_rewrap(
4523 indoc! {"
4524 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
4525 "},
4526 indoc! {"
4527 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4528 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4529 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4530 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4531 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4532 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4533 Integer sit amet scelerisque nisi.
4534 "},
4535 plaintext_language,
4536 &mut cx,
4537 );
4538
4539 // Test rewrapping unaligned comments in a selection.
4540 assert_rewrap(
4541 indoc! {"
4542 fn foo() {
4543 if true {
4544 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4545 // Praesent semper egestas tellus id dignissim.ˇ»
4546 do_something();
4547 } else {
4548 //
4549 }
4550 }
4551 "},
4552 indoc! {"
4553 fn foo() {
4554 if true {
4555 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4556 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4557 // egestas tellus id dignissim.ˇ»
4558 do_something();
4559 } else {
4560 //
4561 }
4562 }
4563 "},
4564 language_with_doc_comments.clone(),
4565 &mut cx,
4566 );
4567
4568 assert_rewrap(
4569 indoc! {"
4570 fn foo() {
4571 if true {
4572 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4573 // Praesent semper egestas tellus id dignissim.»
4574 do_something();
4575 } else {
4576 //
4577 }
4578
4579 }
4580 "},
4581 indoc! {"
4582 fn foo() {
4583 if true {
4584 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4585 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4586 // egestas tellus id dignissim.»
4587 do_something();
4588 } else {
4589 //
4590 }
4591
4592 }
4593 "},
4594 language_with_doc_comments.clone(),
4595 &mut cx,
4596 );
4597
4598 #[track_caller]
4599 fn assert_rewrap(
4600 unwrapped_text: &str,
4601 wrapped_text: &str,
4602 language: Arc<Language>,
4603 cx: &mut EditorTestContext,
4604 ) {
4605 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4606 cx.set_state(unwrapped_text);
4607 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4608 cx.assert_editor_state(wrapped_text);
4609 }
4610}
4611
4612#[gpui::test]
4613async fn test_clipboard(cx: &mut gpui::TestAppContext) {
4614 init_test(cx, |_| {});
4615
4616 let mut cx = EditorTestContext::new(cx).await;
4617
4618 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4619 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4620 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4621
4622 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4623 cx.set_state("two ˇfour ˇsix ˇ");
4624 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4625 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4626
4627 // Paste again but with only two cursors. Since the number of cursors doesn't
4628 // match the number of slices in the clipboard, the entire clipboard text
4629 // is pasted at each cursor.
4630 cx.set_state("ˇtwo one✅ four three six five ˇ");
4631 cx.update_editor(|e, window, cx| {
4632 e.handle_input("( ", window, cx);
4633 e.paste(&Paste, window, cx);
4634 e.handle_input(") ", window, cx);
4635 });
4636 cx.assert_editor_state(
4637 &([
4638 "( one✅ ",
4639 "three ",
4640 "five ) ˇtwo one✅ four three six five ( one✅ ",
4641 "three ",
4642 "five ) ˇ",
4643 ]
4644 .join("\n")),
4645 );
4646
4647 // Cut with three selections, one of which is full-line.
4648 cx.set_state(indoc! {"
4649 1«2ˇ»3
4650 4ˇ567
4651 «8ˇ»9"});
4652 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4653 cx.assert_editor_state(indoc! {"
4654 1ˇ3
4655 ˇ9"});
4656
4657 // Paste with three selections, noticing how the copied selection that was full-line
4658 // gets inserted before the second cursor.
4659 cx.set_state(indoc! {"
4660 1ˇ3
4661 9ˇ
4662 «oˇ»ne"});
4663 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4664 cx.assert_editor_state(indoc! {"
4665 12ˇ3
4666 4567
4667 9ˇ
4668 8ˇne"});
4669
4670 // Copy with a single cursor only, which writes the whole line into the clipboard.
4671 cx.set_state(indoc! {"
4672 The quick brown
4673 fox juˇmps over
4674 the lazy dog"});
4675 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4676 assert_eq!(
4677 cx.read_from_clipboard()
4678 .and_then(|item| item.text().as_deref().map(str::to_string)),
4679 Some("fox jumps over\n".to_string())
4680 );
4681
4682 // Paste with three selections, noticing how the copied full-line selection is inserted
4683 // before the empty selections but replaces the selection that is non-empty.
4684 cx.set_state(indoc! {"
4685 Tˇhe quick brown
4686 «foˇ»x jumps over
4687 tˇhe lazy dog"});
4688 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4689 cx.assert_editor_state(indoc! {"
4690 fox jumps over
4691 Tˇhe quick brown
4692 fox jumps over
4693 ˇx jumps over
4694 fox jumps over
4695 tˇhe lazy dog"});
4696}
4697
4698#[gpui::test]
4699async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
4700 init_test(cx, |_| {});
4701
4702 let mut cx = EditorTestContext::new(cx).await;
4703 let language = Arc::new(Language::new(
4704 LanguageConfig::default(),
4705 Some(tree_sitter_rust::LANGUAGE.into()),
4706 ));
4707 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4708
4709 // Cut an indented block, without the leading whitespace.
4710 cx.set_state(indoc! {"
4711 const a: B = (
4712 c(),
4713 «d(
4714 e,
4715 f
4716 )ˇ»
4717 );
4718 "});
4719 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4720 cx.assert_editor_state(indoc! {"
4721 const a: B = (
4722 c(),
4723 ˇ
4724 );
4725 "});
4726
4727 // Paste it at the same position.
4728 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4729 cx.assert_editor_state(indoc! {"
4730 const a: B = (
4731 c(),
4732 d(
4733 e,
4734 f
4735 )ˇ
4736 );
4737 "});
4738
4739 // Paste it at a line with a lower indent level.
4740 cx.set_state(indoc! {"
4741 ˇ
4742 const a: B = (
4743 c(),
4744 );
4745 "});
4746 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4747 cx.assert_editor_state(indoc! {"
4748 d(
4749 e,
4750 f
4751 )ˇ
4752 const a: B = (
4753 c(),
4754 );
4755 "});
4756
4757 // Cut an indented block, with the leading whitespace.
4758 cx.set_state(indoc! {"
4759 const a: B = (
4760 c(),
4761 « d(
4762 e,
4763 f
4764 )
4765 ˇ»);
4766 "});
4767 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4768 cx.assert_editor_state(indoc! {"
4769 const a: B = (
4770 c(),
4771 ˇ);
4772 "});
4773
4774 // Paste it at the same position.
4775 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4776 cx.assert_editor_state(indoc! {"
4777 const a: B = (
4778 c(),
4779 d(
4780 e,
4781 f
4782 )
4783 ˇ);
4784 "});
4785
4786 // Paste it at a line with a higher indent level.
4787 cx.set_state(indoc! {"
4788 const a: B = (
4789 c(),
4790 d(
4791 e,
4792 fˇ
4793 )
4794 );
4795 "});
4796 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4797 cx.assert_editor_state(indoc! {"
4798 const a: B = (
4799 c(),
4800 d(
4801 e,
4802 f d(
4803 e,
4804 f
4805 )
4806 ˇ
4807 )
4808 );
4809 "});
4810}
4811
4812#[gpui::test]
4813fn test_select_all(cx: &mut TestAppContext) {
4814 init_test(cx, |_| {});
4815
4816 let editor = cx.add_window(|window, cx| {
4817 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
4818 build_editor(buffer, window, cx)
4819 });
4820 _ = editor.update(cx, |editor, window, cx| {
4821 editor.select_all(&SelectAll, window, cx);
4822 assert_eq!(
4823 editor.selections.display_ranges(cx),
4824 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
4825 );
4826 });
4827}
4828
4829#[gpui::test]
4830fn test_select_line(cx: &mut TestAppContext) {
4831 init_test(cx, |_| {});
4832
4833 let editor = cx.add_window(|window, cx| {
4834 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
4835 build_editor(buffer, window, cx)
4836 });
4837 _ = editor.update(cx, |editor, window, cx| {
4838 editor.change_selections(None, window, cx, |s| {
4839 s.select_display_ranges([
4840 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4841 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4842 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4843 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
4844 ])
4845 });
4846 editor.select_line(&SelectLine, window, cx);
4847 assert_eq!(
4848 editor.selections.display_ranges(cx),
4849 vec![
4850 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
4851 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
4852 ]
4853 );
4854 });
4855
4856 _ = editor.update(cx, |editor, window, cx| {
4857 editor.select_line(&SelectLine, window, cx);
4858 assert_eq!(
4859 editor.selections.display_ranges(cx),
4860 vec![
4861 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
4862 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
4863 ]
4864 );
4865 });
4866
4867 _ = editor.update(cx, |editor, window, cx| {
4868 editor.select_line(&SelectLine, window, cx);
4869 assert_eq!(
4870 editor.selections.display_ranges(cx),
4871 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
4872 );
4873 });
4874}
4875
4876#[gpui::test]
4877async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
4878 init_test(cx, |_| {});
4879 let mut cx = EditorTestContext::new(cx).await;
4880
4881 #[track_caller]
4882 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
4883 cx.set_state(initial_state);
4884 cx.update_editor(|e, window, cx| {
4885 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
4886 });
4887 cx.assert_editor_state(expected_state);
4888 }
4889
4890 // Selection starts and ends at the middle of lines, left-to-right
4891 test(
4892 &mut cx,
4893 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
4894 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
4895 );
4896 // Same thing, right-to-left
4897 test(
4898 &mut cx,
4899 "aa\nb«b\ncc\ndd\neˇ»e\nff",
4900 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
4901 );
4902
4903 // Whole buffer, left-to-right, last line *doesn't* end with newline
4904 test(
4905 &mut cx,
4906 "«ˇaa\nbb\ncc\ndd\nee\nff»",
4907 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
4908 );
4909 // Same thing, right-to-left
4910 test(
4911 &mut cx,
4912 "«aa\nbb\ncc\ndd\nee\nffˇ»",
4913 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
4914 );
4915
4916 // Whole buffer, left-to-right, last line ends with newline
4917 test(
4918 &mut cx,
4919 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
4920 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
4921 );
4922 // Same thing, right-to-left
4923 test(
4924 &mut cx,
4925 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
4926 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
4927 );
4928
4929 // Starts at the end of a line, ends at the start of another
4930 test(
4931 &mut cx,
4932 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
4933 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
4934 );
4935}
4936
4937#[gpui::test]
4938async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
4939 init_test(cx, |_| {});
4940
4941 let editor = cx.add_window(|window, cx| {
4942 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
4943 build_editor(buffer, window, cx)
4944 });
4945
4946 // setup
4947 _ = editor.update(cx, |editor, window, cx| {
4948 editor.fold_creases(
4949 vec![
4950 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4951 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4952 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4953 ],
4954 true,
4955 window,
4956 cx,
4957 );
4958 assert_eq!(
4959 editor.display_text(cx),
4960 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
4961 );
4962 });
4963
4964 _ = editor.update(cx, |editor, window, cx| {
4965 editor.change_selections(None, window, cx, |s| {
4966 s.select_display_ranges([
4967 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4968 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4969 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4970 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
4971 ])
4972 });
4973 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
4974 assert_eq!(
4975 editor.display_text(cx),
4976 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
4977 );
4978 });
4979 EditorTestContext::for_editor(editor, cx)
4980 .await
4981 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
4982
4983 _ = editor.update(cx, |editor, window, cx| {
4984 editor.change_selections(None, window, cx, |s| {
4985 s.select_display_ranges([
4986 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
4987 ])
4988 });
4989 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
4990 assert_eq!(
4991 editor.display_text(cx),
4992 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
4993 );
4994 assert_eq!(
4995 editor.selections.display_ranges(cx),
4996 [
4997 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
4998 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
4999 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5000 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5001 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5002 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5003 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5004 ]
5005 );
5006 });
5007 EditorTestContext::for_editor(editor, cx)
5008 .await
5009 .assert_editor_state(
5010 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5011 );
5012}
5013
5014#[gpui::test]
5015async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5016 init_test(cx, |_| {});
5017
5018 let mut cx = EditorTestContext::new(cx).await;
5019
5020 cx.set_state(indoc!(
5021 r#"abc
5022 defˇghi
5023
5024 jk
5025 nlmo
5026 "#
5027 ));
5028
5029 cx.update_editor(|editor, window, cx| {
5030 editor.add_selection_above(&Default::default(), window, cx);
5031 });
5032
5033 cx.assert_editor_state(indoc!(
5034 r#"abcˇ
5035 defˇghi
5036
5037 jk
5038 nlmo
5039 "#
5040 ));
5041
5042 cx.update_editor(|editor, window, cx| {
5043 editor.add_selection_above(&Default::default(), window, cx);
5044 });
5045
5046 cx.assert_editor_state(indoc!(
5047 r#"abcˇ
5048 defˇghi
5049
5050 jk
5051 nlmo
5052 "#
5053 ));
5054
5055 cx.update_editor(|editor, window, cx| {
5056 editor.add_selection_below(&Default::default(), window, cx);
5057 });
5058
5059 cx.assert_editor_state(indoc!(
5060 r#"abc
5061 defˇghi
5062
5063 jk
5064 nlmo
5065 "#
5066 ));
5067
5068 cx.update_editor(|editor, window, cx| {
5069 editor.undo_selection(&Default::default(), window, cx);
5070 });
5071
5072 cx.assert_editor_state(indoc!(
5073 r#"abcˇ
5074 defˇghi
5075
5076 jk
5077 nlmo
5078 "#
5079 ));
5080
5081 cx.update_editor(|editor, window, cx| {
5082 editor.redo_selection(&Default::default(), window, cx);
5083 });
5084
5085 cx.assert_editor_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_below(&Default::default(), window, cx);
5096 });
5097
5098 cx.assert_editor_state(indoc!(
5099 r#"abc
5100 defˇghi
5101
5102 jk
5103 nlmˇo
5104 "#
5105 ));
5106
5107 cx.update_editor(|editor, window, cx| {
5108 editor.add_selection_below(&Default::default(), window, cx);
5109 });
5110
5111 cx.assert_editor_state(indoc!(
5112 r#"abc
5113 defˇghi
5114
5115 jk
5116 nlmˇo
5117 "#
5118 ));
5119
5120 // change selections
5121 cx.set_state(indoc!(
5122 r#"abc
5123 def«ˇg»hi
5124
5125 jk
5126 nlmo
5127 "#
5128 ));
5129
5130 cx.update_editor(|editor, window, cx| {
5131 editor.add_selection_below(&Default::default(), window, cx);
5132 });
5133
5134 cx.assert_editor_state(indoc!(
5135 r#"abc
5136 def«ˇg»hi
5137
5138 jk
5139 nlm«ˇo»
5140 "#
5141 ));
5142
5143 cx.update_editor(|editor, window, cx| {
5144 editor.add_selection_below(&Default::default(), window, cx);
5145 });
5146
5147 cx.assert_editor_state(indoc!(
5148 r#"abc
5149 def«ˇg»hi
5150
5151 jk
5152 nlm«ˇo»
5153 "#
5154 ));
5155
5156 cx.update_editor(|editor, window, cx| {
5157 editor.add_selection_above(&Default::default(), window, cx);
5158 });
5159
5160 cx.assert_editor_state(indoc!(
5161 r#"abc
5162 def«ˇg»hi
5163
5164 jk
5165 nlmo
5166 "#
5167 ));
5168
5169 cx.update_editor(|editor, window, cx| {
5170 editor.add_selection_above(&Default::default(), window, cx);
5171 });
5172
5173 cx.assert_editor_state(indoc!(
5174 r#"abc
5175 def«ˇg»hi
5176
5177 jk
5178 nlmo
5179 "#
5180 ));
5181
5182 // Change selections again
5183 cx.set_state(indoc!(
5184 r#"a«bc
5185 defgˇ»hi
5186
5187 jk
5188 nlmo
5189 "#
5190 ));
5191
5192 cx.update_editor(|editor, window, cx| {
5193 editor.add_selection_below(&Default::default(), window, cx);
5194 });
5195
5196 cx.assert_editor_state(indoc!(
5197 r#"a«bcˇ»
5198 d«efgˇ»hi
5199
5200 j«kˇ»
5201 nlmo
5202 "#
5203 ));
5204
5205 cx.update_editor(|editor, window, cx| {
5206 editor.add_selection_below(&Default::default(), window, cx);
5207 });
5208 cx.assert_editor_state(indoc!(
5209 r#"a«bcˇ»
5210 d«efgˇ»hi
5211
5212 j«kˇ»
5213 n«lmoˇ»
5214 "#
5215 ));
5216 cx.update_editor(|editor, window, cx| {
5217 editor.add_selection_above(&Default::default(), window, cx);
5218 });
5219
5220 cx.assert_editor_state(indoc!(
5221 r#"a«bcˇ»
5222 d«efgˇ»hi
5223
5224 j«kˇ»
5225 nlmo
5226 "#
5227 ));
5228
5229 // Change selections again
5230 cx.set_state(indoc!(
5231 r#"abc
5232 d«ˇefghi
5233
5234 jk
5235 nlm»o
5236 "#
5237 ));
5238
5239 cx.update_editor(|editor, window, cx| {
5240 editor.add_selection_above(&Default::default(), window, cx);
5241 });
5242
5243 cx.assert_editor_state(indoc!(
5244 r#"a«ˇbc»
5245 d«ˇef»ghi
5246
5247 j«ˇk»
5248 n«ˇlm»o
5249 "#
5250 ));
5251
5252 cx.update_editor(|editor, window, cx| {
5253 editor.add_selection_below(&Default::default(), window, cx);
5254 });
5255
5256 cx.assert_editor_state(indoc!(
5257 r#"abc
5258 d«ˇef»ghi
5259
5260 j«ˇk»
5261 n«ˇlm»o
5262 "#
5263 ));
5264}
5265
5266#[gpui::test]
5267async fn test_select_next(cx: &mut gpui::TestAppContext) {
5268 init_test(cx, |_| {});
5269
5270 let mut cx = EditorTestContext::new(cx).await;
5271 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5272
5273 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5274 .unwrap();
5275 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5276
5277 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5278 .unwrap();
5279 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5280
5281 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5282 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5283
5284 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5285 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5286
5287 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5288 .unwrap();
5289 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5290
5291 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5292 .unwrap();
5293 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5294}
5295
5296#[gpui::test]
5297async fn test_select_all_matches(cx: &mut gpui::TestAppContext) {
5298 init_test(cx, |_| {});
5299
5300 let mut cx = EditorTestContext::new(cx).await;
5301
5302 // Test caret-only selections
5303 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5304 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5305 .unwrap();
5306 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5307
5308 // Test left-to-right selections
5309 cx.set_state("abc\n«abcˇ»\nabc");
5310 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5311 .unwrap();
5312 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5313
5314 // Test right-to-left selections
5315 cx.set_state("abc\n«ˇabc»\nabc");
5316 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5317 .unwrap();
5318 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5319
5320 // Test selecting whitespace with caret selection
5321 cx.set_state("abc\nˇ abc\nabc");
5322 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5323 .unwrap();
5324 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5325
5326 // Test selecting whitespace with left-to-right selection
5327 cx.set_state("abc\n«ˇ »abc\nabc");
5328 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5329 .unwrap();
5330 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
5331
5332 // Test no matches with right-to-left selection
5333 cx.set_state("abc\n« ˇ»abc\nabc");
5334 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5335 .unwrap();
5336 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5337}
5338
5339#[gpui::test]
5340async fn test_select_next_with_multiple_carets(cx: &mut gpui::TestAppContext) {
5341 init_test(cx, |_| {});
5342
5343 let mut cx = EditorTestContext::new(cx).await;
5344 cx.set_state(
5345 r#"let foo = 2;
5346lˇet foo = 2;
5347let fooˇ = 2;
5348let foo = 2;
5349let foo = ˇ2;"#,
5350 );
5351
5352 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5353 .unwrap();
5354 cx.assert_editor_state(
5355 r#"let foo = 2;
5356«letˇ» foo = 2;
5357let «fooˇ» = 2;
5358let foo = 2;
5359let foo = «2ˇ»;"#,
5360 );
5361
5362 // noop for multiple selections with different contents
5363 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5364 .unwrap();
5365 cx.assert_editor_state(
5366 r#"let foo = 2;
5367«letˇ» foo = 2;
5368let «fooˇ» = 2;
5369let foo = 2;
5370let foo = «2ˇ»;"#,
5371 );
5372}
5373
5374#[gpui::test]
5375async fn test_select_previous_multibuffer(cx: &mut gpui::TestAppContext) {
5376 init_test(cx, |_| {});
5377
5378 let mut cx =
5379 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
5380
5381 cx.assert_editor_state(indoc! {"
5382 ˇbbb
5383 ccc
5384
5385 bbb
5386 ccc
5387 "});
5388 cx.dispatch_action(SelectPrevious::default());
5389 cx.assert_editor_state(indoc! {"
5390 «bbbˇ»
5391 ccc
5392
5393 bbb
5394 ccc
5395 "});
5396 cx.dispatch_action(SelectPrevious::default());
5397 cx.assert_editor_state(indoc! {"
5398 «bbbˇ»
5399 ccc
5400
5401 «bbbˇ»
5402 ccc
5403 "});
5404}
5405
5406#[gpui::test]
5407async fn test_select_previous_with_single_caret(cx: &mut gpui::TestAppContext) {
5408 init_test(cx, |_| {});
5409
5410 let mut cx = EditorTestContext::new(cx).await;
5411 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5412
5413 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5414 .unwrap();
5415 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5416
5417 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5418 .unwrap();
5419 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5420
5421 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5422 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5423
5424 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5425 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5426
5427 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5428 .unwrap();
5429 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
5430
5431 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5432 .unwrap();
5433 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
5434
5435 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5436 .unwrap();
5437 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5438}
5439
5440#[gpui::test]
5441async fn test_select_previous_empty_buffer(cx: &mut gpui::TestAppContext) {
5442 init_test(cx, |_| {});
5443
5444 let mut cx = EditorTestContext::new(cx).await;
5445 cx.set_state("aˇ");
5446
5447 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5448 .unwrap();
5449 cx.assert_editor_state("«aˇ»");
5450 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5451 .unwrap();
5452 cx.assert_editor_state("«aˇ»");
5453}
5454
5455#[gpui::test]
5456async fn test_select_previous_with_multiple_carets(cx: &mut gpui::TestAppContext) {
5457 init_test(cx, |_| {});
5458
5459 let mut cx = EditorTestContext::new(cx).await;
5460 cx.set_state(
5461 r#"let foo = 2;
5462lˇet foo = 2;
5463let fooˇ = 2;
5464let foo = 2;
5465let foo = ˇ2;"#,
5466 );
5467
5468 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5469 .unwrap();
5470 cx.assert_editor_state(
5471 r#"let foo = 2;
5472«letˇ» foo = 2;
5473let «fooˇ» = 2;
5474let foo = 2;
5475let foo = «2ˇ»;"#,
5476 );
5477
5478 // noop for multiple selections with different contents
5479 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5480 .unwrap();
5481 cx.assert_editor_state(
5482 r#"let foo = 2;
5483«letˇ» foo = 2;
5484let «fooˇ» = 2;
5485let foo = 2;
5486let foo = «2ˇ»;"#,
5487 );
5488}
5489
5490#[gpui::test]
5491async fn test_select_previous_with_single_selection(cx: &mut gpui::TestAppContext) {
5492 init_test(cx, |_| {});
5493
5494 let mut cx = EditorTestContext::new(cx).await;
5495 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5496
5497 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5498 .unwrap();
5499 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5500
5501 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5502 .unwrap();
5503 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5504
5505 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5506 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5507
5508 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5509 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5510
5511 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5512 .unwrap();
5513 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
5514
5515 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5516 .unwrap();
5517 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5518}
5519
5520#[gpui::test]
5521async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
5522 init_test(cx, |_| {});
5523
5524 let language = Arc::new(Language::new(
5525 LanguageConfig::default(),
5526 Some(tree_sitter_rust::LANGUAGE.into()),
5527 ));
5528
5529 let text = r#"
5530 use mod1::mod2::{mod3, mod4};
5531
5532 fn fn_1(param1: bool, param2: &str) {
5533 let var1 = "text";
5534 }
5535 "#
5536 .unindent();
5537
5538 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5539 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5540 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5541
5542 editor
5543 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5544 .await;
5545
5546 editor.update_in(cx, |editor, window, cx| {
5547 editor.change_selections(None, window, cx, |s| {
5548 s.select_display_ranges([
5549 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
5550 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
5551 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
5552 ]);
5553 });
5554 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5555 });
5556 editor.update(cx, |editor, cx| {
5557 assert_text_with_selections(
5558 editor,
5559 indoc! {r#"
5560 use mod1::mod2::{mod3, «mod4ˇ»};
5561
5562 fn fn_1«ˇ(param1: bool, param2: &str)» {
5563 let var1 = "«textˇ»";
5564 }
5565 "#},
5566 cx,
5567 );
5568 });
5569
5570 editor.update_in(cx, |editor, window, cx| {
5571 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5572 });
5573 editor.update(cx, |editor, cx| {
5574 assert_text_with_selections(
5575 editor,
5576 indoc! {r#"
5577 use mod1::mod2::«{mod3, mod4}ˇ»;
5578
5579 «ˇfn fn_1(param1: bool, param2: &str) {
5580 let var1 = "text";
5581 }»
5582 "#},
5583 cx,
5584 );
5585 });
5586
5587 editor.update_in(cx, |editor, window, cx| {
5588 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5589 });
5590 assert_eq!(
5591 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
5592 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
5593 );
5594
5595 // Trying to expand the selected syntax node one more time has no effect.
5596 editor.update_in(cx, |editor, window, cx| {
5597 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5598 });
5599 assert_eq!(
5600 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
5601 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
5602 );
5603
5604 editor.update_in(cx, |editor, window, cx| {
5605 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5606 });
5607 editor.update(cx, |editor, cx| {
5608 assert_text_with_selections(
5609 editor,
5610 indoc! {r#"
5611 use mod1::mod2::«{mod3, mod4}ˇ»;
5612
5613 «ˇfn fn_1(param1: bool, param2: &str) {
5614 let var1 = "text";
5615 }»
5616 "#},
5617 cx,
5618 );
5619 });
5620
5621 editor.update_in(cx, |editor, window, cx| {
5622 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5623 });
5624 editor.update(cx, |editor, cx| {
5625 assert_text_with_selections(
5626 editor,
5627 indoc! {r#"
5628 use mod1::mod2::{mod3, «mod4ˇ»};
5629
5630 fn fn_1«ˇ(param1: bool, param2: &str)» {
5631 let var1 = "«textˇ»";
5632 }
5633 "#},
5634 cx,
5635 );
5636 });
5637
5638 editor.update_in(cx, |editor, window, cx| {
5639 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5640 });
5641 editor.update(cx, |editor, cx| {
5642 assert_text_with_selections(
5643 editor,
5644 indoc! {r#"
5645 use mod1::mod2::{mod3, mo«ˇ»d4};
5646
5647 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
5648 let var1 = "te«ˇ»xt";
5649 }
5650 "#},
5651 cx,
5652 );
5653 });
5654
5655 // Trying to shrink the selected syntax node one more time has no effect.
5656 editor.update_in(cx, |editor, window, cx| {
5657 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5658 });
5659 editor.update_in(cx, |editor, _, cx| {
5660 assert_text_with_selections(
5661 editor,
5662 indoc! {r#"
5663 use mod1::mod2::{mod3, mo«ˇ»d4};
5664
5665 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
5666 let var1 = "te«ˇ»xt";
5667 }
5668 "#},
5669 cx,
5670 );
5671 });
5672
5673 // Ensure that we keep expanding the selection if the larger selection starts or ends within
5674 // a fold.
5675 editor.update_in(cx, |editor, window, cx| {
5676 editor.fold_creases(
5677 vec![
5678 Crease::simple(
5679 Point::new(0, 21)..Point::new(0, 24),
5680 FoldPlaceholder::test(),
5681 ),
5682 Crease::simple(
5683 Point::new(3, 20)..Point::new(3, 22),
5684 FoldPlaceholder::test(),
5685 ),
5686 ],
5687 true,
5688 window,
5689 cx,
5690 );
5691 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5692 });
5693 editor.update(cx, |editor, cx| {
5694 assert_text_with_selections(
5695 editor,
5696 indoc! {r#"
5697 use mod1::mod2::«{mod3, mod4}ˇ»;
5698
5699 fn fn_1«ˇ(param1: bool, param2: &str)» {
5700 «let var1 = "text";ˇ»
5701 }
5702 "#},
5703 cx,
5704 );
5705 });
5706}
5707
5708#[gpui::test]
5709async fn test_fold_function_bodies(cx: &mut gpui::TestAppContext) {
5710 init_test(cx, |_| {});
5711
5712 let base_text = r#"
5713 impl A {
5714 // this is an uncommitted comment
5715
5716 fn b() {
5717 c();
5718 }
5719
5720 // this is another uncommitted comment
5721
5722 fn d() {
5723 // e
5724 // f
5725 }
5726 }
5727
5728 fn g() {
5729 // h
5730 }
5731 "#
5732 .unindent();
5733
5734 let text = r#"
5735 ˇimpl A {
5736
5737 fn b() {
5738 c();
5739 }
5740
5741 fn d() {
5742 // e
5743 // f
5744 }
5745 }
5746
5747 fn g() {
5748 // h
5749 }
5750 "#
5751 .unindent();
5752
5753 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
5754 cx.set_state(&text);
5755 cx.set_diff_base(&base_text);
5756 cx.update_editor(|editor, window, cx| {
5757 editor.expand_all_diff_hunks(&Default::default(), window, cx);
5758 });
5759
5760 cx.assert_state_with_diff(
5761 "
5762 ˇimpl A {
5763 - // this is an uncommitted comment
5764
5765 fn b() {
5766 c();
5767 }
5768
5769 - // this is another uncommitted comment
5770 -
5771 fn d() {
5772 // e
5773 // f
5774 }
5775 }
5776
5777 fn g() {
5778 // h
5779 }
5780 "
5781 .unindent(),
5782 );
5783
5784 let expected_display_text = "
5785 impl A {
5786 // this is an uncommitted comment
5787
5788 fn b() {
5789 ⋯
5790 }
5791
5792 // this is another uncommitted comment
5793
5794 fn d() {
5795 ⋯
5796 }
5797 }
5798
5799 fn g() {
5800 ⋯
5801 }
5802 "
5803 .unindent();
5804
5805 cx.update_editor(|editor, window, cx| {
5806 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
5807 assert_eq!(editor.display_text(cx), expected_display_text);
5808 });
5809}
5810
5811#[gpui::test]
5812async fn test_autoindent(cx: &mut gpui::TestAppContext) {
5813 init_test(cx, |_| {});
5814
5815 let language = Arc::new(
5816 Language::new(
5817 LanguageConfig {
5818 brackets: BracketPairConfig {
5819 pairs: vec![
5820 BracketPair {
5821 start: "{".to_string(),
5822 end: "}".to_string(),
5823 close: false,
5824 surround: false,
5825 newline: true,
5826 },
5827 BracketPair {
5828 start: "(".to_string(),
5829 end: ")".to_string(),
5830 close: false,
5831 surround: false,
5832 newline: true,
5833 },
5834 ],
5835 ..Default::default()
5836 },
5837 ..Default::default()
5838 },
5839 Some(tree_sitter_rust::LANGUAGE.into()),
5840 )
5841 .with_indents_query(
5842 r#"
5843 (_ "(" ")" @end) @indent
5844 (_ "{" "}" @end) @indent
5845 "#,
5846 )
5847 .unwrap(),
5848 );
5849
5850 let text = "fn a() {}";
5851
5852 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5853 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5854 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5855 editor
5856 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5857 .await;
5858
5859 editor.update_in(cx, |editor, window, cx| {
5860 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
5861 editor.newline(&Newline, window, cx);
5862 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
5863 assert_eq!(
5864 editor.selections.ranges(cx),
5865 &[
5866 Point::new(1, 4)..Point::new(1, 4),
5867 Point::new(3, 4)..Point::new(3, 4),
5868 Point::new(5, 0)..Point::new(5, 0)
5869 ]
5870 );
5871 });
5872}
5873
5874#[gpui::test]
5875async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
5876 init_test(cx, |_| {});
5877
5878 {
5879 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
5880 cx.set_state(indoc! {"
5881 impl A {
5882
5883 fn b() {}
5884
5885 «fn c() {
5886
5887 }ˇ»
5888 }
5889 "});
5890
5891 cx.update_editor(|editor, window, cx| {
5892 editor.autoindent(&Default::default(), window, cx);
5893 });
5894
5895 cx.assert_editor_state(indoc! {"
5896 impl A {
5897
5898 fn b() {}
5899
5900 «fn c() {
5901
5902 }ˇ»
5903 }
5904 "});
5905 }
5906
5907 {
5908 let mut cx = EditorTestContext::new_multibuffer(
5909 cx,
5910 [indoc! { "
5911 impl A {
5912 «
5913 // a
5914 fn b(){}
5915 »
5916 «
5917 }
5918 fn c(){}
5919 »
5920 "}],
5921 );
5922
5923 let buffer = cx.update_editor(|editor, _, cx| {
5924 let buffer = editor.buffer().update(cx, |buffer, _| {
5925 buffer.all_buffers().iter().next().unwrap().clone()
5926 });
5927 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5928 buffer
5929 });
5930
5931 cx.run_until_parked();
5932 cx.update_editor(|editor, window, cx| {
5933 editor.select_all(&Default::default(), window, cx);
5934 editor.autoindent(&Default::default(), window, cx)
5935 });
5936 cx.run_until_parked();
5937
5938 cx.update(|_, cx| {
5939 pretty_assertions::assert_eq!(
5940 buffer.read(cx).text(),
5941 indoc! { "
5942 impl A {
5943
5944 // a
5945 fn b(){}
5946
5947
5948 }
5949 fn c(){}
5950
5951 " }
5952 )
5953 });
5954 }
5955}
5956
5957#[gpui::test]
5958async fn test_autoclose_and_auto_surround_pairs(cx: &mut gpui::TestAppContext) {
5959 init_test(cx, |_| {});
5960
5961 let mut cx = EditorTestContext::new(cx).await;
5962
5963 let language = Arc::new(Language::new(
5964 LanguageConfig {
5965 brackets: BracketPairConfig {
5966 pairs: vec![
5967 BracketPair {
5968 start: "{".to_string(),
5969 end: "}".to_string(),
5970 close: true,
5971 surround: true,
5972 newline: true,
5973 },
5974 BracketPair {
5975 start: "(".to_string(),
5976 end: ")".to_string(),
5977 close: true,
5978 surround: true,
5979 newline: true,
5980 },
5981 BracketPair {
5982 start: "/*".to_string(),
5983 end: " */".to_string(),
5984 close: true,
5985 surround: true,
5986 newline: true,
5987 },
5988 BracketPair {
5989 start: "[".to_string(),
5990 end: "]".to_string(),
5991 close: false,
5992 surround: false,
5993 newline: true,
5994 },
5995 BracketPair {
5996 start: "\"".to_string(),
5997 end: "\"".to_string(),
5998 close: true,
5999 surround: true,
6000 newline: false,
6001 },
6002 BracketPair {
6003 start: "<".to_string(),
6004 end: ">".to_string(),
6005 close: false,
6006 surround: true,
6007 newline: true,
6008 },
6009 ],
6010 ..Default::default()
6011 },
6012 autoclose_before: "})]".to_string(),
6013 ..Default::default()
6014 },
6015 Some(tree_sitter_rust::LANGUAGE.into()),
6016 ));
6017
6018 cx.language_registry().add(language.clone());
6019 cx.update_buffer(|buffer, cx| {
6020 buffer.set_language(Some(language), cx);
6021 });
6022
6023 cx.set_state(
6024 &r#"
6025 🏀ˇ
6026 εˇ
6027 ❤️ˇ
6028 "#
6029 .unindent(),
6030 );
6031
6032 // autoclose multiple nested brackets at multiple cursors
6033 cx.update_editor(|editor, window, cx| {
6034 editor.handle_input("{", window, cx);
6035 editor.handle_input("{", window, cx);
6036 editor.handle_input("{", window, cx);
6037 });
6038 cx.assert_editor_state(
6039 &"
6040 🏀{{{ˇ}}}
6041 ε{{{ˇ}}}
6042 ❤️{{{ˇ}}}
6043 "
6044 .unindent(),
6045 );
6046
6047 // insert a different closing bracket
6048 cx.update_editor(|editor, window, cx| {
6049 editor.handle_input(")", window, cx);
6050 });
6051 cx.assert_editor_state(
6052 &"
6053 🏀{{{)ˇ}}}
6054 ε{{{)ˇ}}}
6055 ❤️{{{)ˇ}}}
6056 "
6057 .unindent(),
6058 );
6059
6060 // skip over the auto-closed brackets when typing a closing bracket
6061 cx.update_editor(|editor, window, cx| {
6062 editor.move_right(&MoveRight, window, cx);
6063 editor.handle_input("}", window, cx);
6064 editor.handle_input("}", window, cx);
6065 editor.handle_input("}", window, cx);
6066 });
6067 cx.assert_editor_state(
6068 &"
6069 🏀{{{)}}}}ˇ
6070 ε{{{)}}}}ˇ
6071 ❤️{{{)}}}}ˇ
6072 "
6073 .unindent(),
6074 );
6075
6076 // autoclose multi-character pairs
6077 cx.set_state(
6078 &"
6079 ˇ
6080 ˇ
6081 "
6082 .unindent(),
6083 );
6084 cx.update_editor(|editor, window, cx| {
6085 editor.handle_input("/", window, cx);
6086 editor.handle_input("*", window, cx);
6087 });
6088 cx.assert_editor_state(
6089 &"
6090 /*ˇ */
6091 /*ˇ */
6092 "
6093 .unindent(),
6094 );
6095
6096 // one cursor autocloses a multi-character pair, one cursor
6097 // does not autoclose.
6098 cx.set_state(
6099 &"
6100 /ˇ
6101 ˇ
6102 "
6103 .unindent(),
6104 );
6105 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6106 cx.assert_editor_state(
6107 &"
6108 /*ˇ */
6109 *ˇ
6110 "
6111 .unindent(),
6112 );
6113
6114 // Don't autoclose if the next character isn't whitespace and isn't
6115 // listed in the language's "autoclose_before" section.
6116 cx.set_state("ˇa b");
6117 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6118 cx.assert_editor_state("{ˇa b");
6119
6120 // Don't autoclose if `close` is false for the bracket pair
6121 cx.set_state("ˇ");
6122 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6123 cx.assert_editor_state("[ˇ");
6124
6125 // Surround with brackets if text is selected
6126 cx.set_state("«aˇ» b");
6127 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6128 cx.assert_editor_state("{«aˇ»} b");
6129
6130 // Autclose pair where the start and end characters are the same
6131 cx.set_state("aˇ");
6132 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6133 cx.assert_editor_state("a\"ˇ\"");
6134 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6135 cx.assert_editor_state("a\"\"ˇ");
6136
6137 // Don't autoclose pair if autoclose is disabled
6138 cx.set_state("ˇ");
6139 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6140 cx.assert_editor_state("<ˇ");
6141
6142 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6143 cx.set_state("«aˇ» b");
6144 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6145 cx.assert_editor_state("<«aˇ»> b");
6146}
6147
6148#[gpui::test]
6149async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut gpui::TestAppContext) {
6150 init_test(cx, |settings| {
6151 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6152 });
6153
6154 let mut cx = EditorTestContext::new(cx).await;
6155
6156 let language = Arc::new(Language::new(
6157 LanguageConfig {
6158 brackets: BracketPairConfig {
6159 pairs: vec![
6160 BracketPair {
6161 start: "{".to_string(),
6162 end: "}".to_string(),
6163 close: true,
6164 surround: true,
6165 newline: true,
6166 },
6167 BracketPair {
6168 start: "(".to_string(),
6169 end: ")".to_string(),
6170 close: true,
6171 surround: true,
6172 newline: true,
6173 },
6174 BracketPair {
6175 start: "[".to_string(),
6176 end: "]".to_string(),
6177 close: false,
6178 surround: false,
6179 newline: true,
6180 },
6181 ],
6182 ..Default::default()
6183 },
6184 autoclose_before: "})]".to_string(),
6185 ..Default::default()
6186 },
6187 Some(tree_sitter_rust::LANGUAGE.into()),
6188 ));
6189
6190 cx.language_registry().add(language.clone());
6191 cx.update_buffer(|buffer, cx| {
6192 buffer.set_language(Some(language), cx);
6193 });
6194
6195 cx.set_state(
6196 &"
6197 ˇ
6198 ˇ
6199 ˇ
6200 "
6201 .unindent(),
6202 );
6203
6204 // ensure only matching closing brackets are skipped over
6205 cx.update_editor(|editor, window, cx| {
6206 editor.handle_input("}", window, cx);
6207 editor.move_left(&MoveLeft, window, cx);
6208 editor.handle_input(")", window, cx);
6209 editor.move_left(&MoveLeft, window, cx);
6210 });
6211 cx.assert_editor_state(
6212 &"
6213 ˇ)}
6214 ˇ)}
6215 ˇ)}
6216 "
6217 .unindent(),
6218 );
6219
6220 // skip-over closing brackets at multiple cursors
6221 cx.update_editor(|editor, window, cx| {
6222 editor.handle_input(")", window, cx);
6223 editor.handle_input("}", window, cx);
6224 });
6225 cx.assert_editor_state(
6226 &"
6227 )}ˇ
6228 )}ˇ
6229 )}ˇ
6230 "
6231 .unindent(),
6232 );
6233
6234 // ignore non-close brackets
6235 cx.update_editor(|editor, window, cx| {
6236 editor.handle_input("]", window, cx);
6237 editor.move_left(&MoveLeft, window, cx);
6238 editor.handle_input("]", window, cx);
6239 });
6240 cx.assert_editor_state(
6241 &"
6242 )}]ˇ]
6243 )}]ˇ]
6244 )}]ˇ]
6245 "
6246 .unindent(),
6247 );
6248}
6249
6250#[gpui::test]
6251async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
6252 init_test(cx, |_| {});
6253
6254 let mut cx = EditorTestContext::new(cx).await;
6255
6256 let html_language = Arc::new(
6257 Language::new(
6258 LanguageConfig {
6259 name: "HTML".into(),
6260 brackets: BracketPairConfig {
6261 pairs: vec![
6262 BracketPair {
6263 start: "<".into(),
6264 end: ">".into(),
6265 close: true,
6266 ..Default::default()
6267 },
6268 BracketPair {
6269 start: "{".into(),
6270 end: "}".into(),
6271 close: true,
6272 ..Default::default()
6273 },
6274 BracketPair {
6275 start: "(".into(),
6276 end: ")".into(),
6277 close: true,
6278 ..Default::default()
6279 },
6280 ],
6281 ..Default::default()
6282 },
6283 autoclose_before: "})]>".into(),
6284 ..Default::default()
6285 },
6286 Some(tree_sitter_html::LANGUAGE.into()),
6287 )
6288 .with_injection_query(
6289 r#"
6290 (script_element
6291 (raw_text) @injection.content
6292 (#set! injection.language "javascript"))
6293 "#,
6294 )
6295 .unwrap(),
6296 );
6297
6298 let javascript_language = Arc::new(Language::new(
6299 LanguageConfig {
6300 name: "JavaScript".into(),
6301 brackets: BracketPairConfig {
6302 pairs: vec![
6303 BracketPair {
6304 start: "/*".into(),
6305 end: " */".into(),
6306 close: true,
6307 ..Default::default()
6308 },
6309 BracketPair {
6310 start: "{".into(),
6311 end: "}".into(),
6312 close: true,
6313 ..Default::default()
6314 },
6315 BracketPair {
6316 start: "(".into(),
6317 end: ")".into(),
6318 close: true,
6319 ..Default::default()
6320 },
6321 ],
6322 ..Default::default()
6323 },
6324 autoclose_before: "})]>".into(),
6325 ..Default::default()
6326 },
6327 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6328 ));
6329
6330 cx.language_registry().add(html_language.clone());
6331 cx.language_registry().add(javascript_language.clone());
6332
6333 cx.update_buffer(|buffer, cx| {
6334 buffer.set_language(Some(html_language), cx);
6335 });
6336
6337 cx.set_state(
6338 &r#"
6339 <body>ˇ
6340 <script>
6341 var x = 1;ˇ
6342 </script>
6343 </body>ˇ
6344 "#
6345 .unindent(),
6346 );
6347
6348 // Precondition: different languages are active at different locations.
6349 cx.update_editor(|editor, window, cx| {
6350 let snapshot = editor.snapshot(window, cx);
6351 let cursors = editor.selections.ranges::<usize>(cx);
6352 let languages = cursors
6353 .iter()
6354 .map(|c| snapshot.language_at(c.start).unwrap().name())
6355 .collect::<Vec<_>>();
6356 assert_eq!(
6357 languages,
6358 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6359 );
6360 });
6361
6362 // Angle brackets autoclose in HTML, but not JavaScript.
6363 cx.update_editor(|editor, window, cx| {
6364 editor.handle_input("<", window, cx);
6365 editor.handle_input("a", window, cx);
6366 });
6367 cx.assert_editor_state(
6368 &r#"
6369 <body><aˇ>
6370 <script>
6371 var x = 1;<aˇ
6372 </script>
6373 </body><aˇ>
6374 "#
6375 .unindent(),
6376 );
6377
6378 // Curly braces and parens autoclose in both HTML and JavaScript.
6379 cx.update_editor(|editor, window, cx| {
6380 editor.handle_input(" b=", window, cx);
6381 editor.handle_input("{", window, cx);
6382 editor.handle_input("c", window, cx);
6383 editor.handle_input("(", window, cx);
6384 });
6385 cx.assert_editor_state(
6386 &r#"
6387 <body><a b={c(ˇ)}>
6388 <script>
6389 var x = 1;<a b={c(ˇ)}
6390 </script>
6391 </body><a b={c(ˇ)}>
6392 "#
6393 .unindent(),
6394 );
6395
6396 // Brackets that were already autoclosed are skipped.
6397 cx.update_editor(|editor, window, cx| {
6398 editor.handle_input(")", window, cx);
6399 editor.handle_input("d", window, cx);
6400 editor.handle_input("}", window, cx);
6401 });
6402 cx.assert_editor_state(
6403 &r#"
6404 <body><a b={c()d}ˇ>
6405 <script>
6406 var x = 1;<a b={c()d}ˇ
6407 </script>
6408 </body><a b={c()d}ˇ>
6409 "#
6410 .unindent(),
6411 );
6412 cx.update_editor(|editor, window, cx| {
6413 editor.handle_input(">", window, cx);
6414 });
6415 cx.assert_editor_state(
6416 &r#"
6417 <body><a b={c()d}>ˇ
6418 <script>
6419 var x = 1;<a b={c()d}>ˇ
6420 </script>
6421 </body><a b={c()d}>ˇ
6422 "#
6423 .unindent(),
6424 );
6425
6426 // Reset
6427 cx.set_state(
6428 &r#"
6429 <body>ˇ
6430 <script>
6431 var x = 1;ˇ
6432 </script>
6433 </body>ˇ
6434 "#
6435 .unindent(),
6436 );
6437
6438 cx.update_editor(|editor, window, cx| {
6439 editor.handle_input("<", window, cx);
6440 });
6441 cx.assert_editor_state(
6442 &r#"
6443 <body><ˇ>
6444 <script>
6445 var x = 1;<ˇ
6446 </script>
6447 </body><ˇ>
6448 "#
6449 .unindent(),
6450 );
6451
6452 // When backspacing, the closing angle brackets are removed.
6453 cx.update_editor(|editor, window, cx| {
6454 editor.backspace(&Backspace, window, cx);
6455 });
6456 cx.assert_editor_state(
6457 &r#"
6458 <body>ˇ
6459 <script>
6460 var x = 1;ˇ
6461 </script>
6462 </body>ˇ
6463 "#
6464 .unindent(),
6465 );
6466
6467 // Block comments autoclose in JavaScript, but not HTML.
6468 cx.update_editor(|editor, window, cx| {
6469 editor.handle_input("/", window, cx);
6470 editor.handle_input("*", window, cx);
6471 });
6472 cx.assert_editor_state(
6473 &r#"
6474 <body>/*ˇ
6475 <script>
6476 var x = 1;/*ˇ */
6477 </script>
6478 </body>/*ˇ
6479 "#
6480 .unindent(),
6481 );
6482}
6483
6484#[gpui::test]
6485async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
6486 init_test(cx, |_| {});
6487
6488 let mut cx = EditorTestContext::new(cx).await;
6489
6490 let rust_language = Arc::new(
6491 Language::new(
6492 LanguageConfig {
6493 name: "Rust".into(),
6494 brackets: serde_json::from_value(json!([
6495 { "start": "{", "end": "}", "close": true, "newline": true },
6496 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
6497 ]))
6498 .unwrap(),
6499 autoclose_before: "})]>".into(),
6500 ..Default::default()
6501 },
6502 Some(tree_sitter_rust::LANGUAGE.into()),
6503 )
6504 .with_override_query("(string_literal) @string")
6505 .unwrap(),
6506 );
6507
6508 cx.language_registry().add(rust_language.clone());
6509 cx.update_buffer(|buffer, cx| {
6510 buffer.set_language(Some(rust_language), cx);
6511 });
6512
6513 cx.set_state(
6514 &r#"
6515 let x = ˇ
6516 "#
6517 .unindent(),
6518 );
6519
6520 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
6521 cx.update_editor(|editor, window, cx| {
6522 editor.handle_input("\"", window, cx);
6523 });
6524 cx.assert_editor_state(
6525 &r#"
6526 let x = "ˇ"
6527 "#
6528 .unindent(),
6529 );
6530
6531 // Inserting another quotation mark. The cursor moves across the existing
6532 // automatically-inserted quotation mark.
6533 cx.update_editor(|editor, window, cx| {
6534 editor.handle_input("\"", window, cx);
6535 });
6536 cx.assert_editor_state(
6537 &r#"
6538 let x = ""ˇ
6539 "#
6540 .unindent(),
6541 );
6542
6543 // Reset
6544 cx.set_state(
6545 &r#"
6546 let x = ˇ
6547 "#
6548 .unindent(),
6549 );
6550
6551 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
6552 cx.update_editor(|editor, window, cx| {
6553 editor.handle_input("\"", window, cx);
6554 editor.handle_input(" ", window, cx);
6555 editor.move_left(&Default::default(), window, cx);
6556 editor.handle_input("\\", window, cx);
6557 editor.handle_input("\"", window, cx);
6558 });
6559 cx.assert_editor_state(
6560 &r#"
6561 let x = "\"ˇ "
6562 "#
6563 .unindent(),
6564 );
6565
6566 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
6567 // mark. Nothing is inserted.
6568 cx.update_editor(|editor, window, cx| {
6569 editor.move_right(&Default::default(), window, cx);
6570 editor.handle_input("\"", window, cx);
6571 });
6572 cx.assert_editor_state(
6573 &r#"
6574 let x = "\" "ˇ
6575 "#
6576 .unindent(),
6577 );
6578}
6579
6580#[gpui::test]
6581async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
6582 init_test(cx, |_| {});
6583
6584 let language = Arc::new(Language::new(
6585 LanguageConfig {
6586 brackets: BracketPairConfig {
6587 pairs: vec![
6588 BracketPair {
6589 start: "{".to_string(),
6590 end: "}".to_string(),
6591 close: true,
6592 surround: true,
6593 newline: true,
6594 },
6595 BracketPair {
6596 start: "/* ".to_string(),
6597 end: "*/".to_string(),
6598 close: true,
6599 surround: true,
6600 ..Default::default()
6601 },
6602 ],
6603 ..Default::default()
6604 },
6605 ..Default::default()
6606 },
6607 Some(tree_sitter_rust::LANGUAGE.into()),
6608 ));
6609
6610 let text = r#"
6611 a
6612 b
6613 c
6614 "#
6615 .unindent();
6616
6617 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6618 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6619 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6620 editor
6621 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6622 .await;
6623
6624 editor.update_in(cx, |editor, window, cx| {
6625 editor.change_selections(None, window, cx, |s| {
6626 s.select_display_ranges([
6627 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6628 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6629 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
6630 ])
6631 });
6632
6633 editor.handle_input("{", window, cx);
6634 editor.handle_input("{", window, cx);
6635 editor.handle_input("{", window, cx);
6636 assert_eq!(
6637 editor.text(cx),
6638 "
6639 {{{a}}}
6640 {{{b}}}
6641 {{{c}}}
6642 "
6643 .unindent()
6644 );
6645 assert_eq!(
6646 editor.selections.display_ranges(cx),
6647 [
6648 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
6649 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
6650 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
6651 ]
6652 );
6653
6654 editor.undo(&Undo, window, cx);
6655 editor.undo(&Undo, window, cx);
6656 editor.undo(&Undo, window, cx);
6657 assert_eq!(
6658 editor.text(cx),
6659 "
6660 a
6661 b
6662 c
6663 "
6664 .unindent()
6665 );
6666 assert_eq!(
6667 editor.selections.display_ranges(cx),
6668 [
6669 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6670 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6671 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
6672 ]
6673 );
6674
6675 // Ensure inserting the first character of a multi-byte bracket pair
6676 // doesn't surround the selections with the bracket.
6677 editor.handle_input("/", window, cx);
6678 assert_eq!(
6679 editor.text(cx),
6680 "
6681 /
6682 /
6683 /
6684 "
6685 .unindent()
6686 );
6687 assert_eq!(
6688 editor.selections.display_ranges(cx),
6689 [
6690 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6691 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6692 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
6693 ]
6694 );
6695
6696 editor.undo(&Undo, window, cx);
6697 assert_eq!(
6698 editor.text(cx),
6699 "
6700 a
6701 b
6702 c
6703 "
6704 .unindent()
6705 );
6706 assert_eq!(
6707 editor.selections.display_ranges(cx),
6708 [
6709 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6710 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6711 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
6712 ]
6713 );
6714
6715 // Ensure inserting the last character of a multi-byte bracket pair
6716 // doesn't surround the selections with the bracket.
6717 editor.handle_input("*", window, cx);
6718 assert_eq!(
6719 editor.text(cx),
6720 "
6721 *
6722 *
6723 *
6724 "
6725 .unindent()
6726 );
6727 assert_eq!(
6728 editor.selections.display_ranges(cx),
6729 [
6730 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6731 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6732 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
6733 ]
6734 );
6735 });
6736}
6737
6738#[gpui::test]
6739async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
6740 init_test(cx, |_| {});
6741
6742 let language = Arc::new(Language::new(
6743 LanguageConfig {
6744 brackets: BracketPairConfig {
6745 pairs: vec![BracketPair {
6746 start: "{".to_string(),
6747 end: "}".to_string(),
6748 close: true,
6749 surround: true,
6750 newline: true,
6751 }],
6752 ..Default::default()
6753 },
6754 autoclose_before: "}".to_string(),
6755 ..Default::default()
6756 },
6757 Some(tree_sitter_rust::LANGUAGE.into()),
6758 ));
6759
6760 let text = r#"
6761 a
6762 b
6763 c
6764 "#
6765 .unindent();
6766
6767 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6768 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6769 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6770 editor
6771 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6772 .await;
6773
6774 editor.update_in(cx, |editor, window, cx| {
6775 editor.change_selections(None, window, cx, |s| {
6776 s.select_ranges([
6777 Point::new(0, 1)..Point::new(0, 1),
6778 Point::new(1, 1)..Point::new(1, 1),
6779 Point::new(2, 1)..Point::new(2, 1),
6780 ])
6781 });
6782
6783 editor.handle_input("{", window, cx);
6784 editor.handle_input("{", window, cx);
6785 editor.handle_input("_", window, cx);
6786 assert_eq!(
6787 editor.text(cx),
6788 "
6789 a{{_}}
6790 b{{_}}
6791 c{{_}}
6792 "
6793 .unindent()
6794 );
6795 assert_eq!(
6796 editor.selections.ranges::<Point>(cx),
6797 [
6798 Point::new(0, 4)..Point::new(0, 4),
6799 Point::new(1, 4)..Point::new(1, 4),
6800 Point::new(2, 4)..Point::new(2, 4)
6801 ]
6802 );
6803
6804 editor.backspace(&Default::default(), window, cx);
6805 editor.backspace(&Default::default(), window, cx);
6806 assert_eq!(
6807 editor.text(cx),
6808 "
6809 a{}
6810 b{}
6811 c{}
6812 "
6813 .unindent()
6814 );
6815 assert_eq!(
6816 editor.selections.ranges::<Point>(cx),
6817 [
6818 Point::new(0, 2)..Point::new(0, 2),
6819 Point::new(1, 2)..Point::new(1, 2),
6820 Point::new(2, 2)..Point::new(2, 2)
6821 ]
6822 );
6823
6824 editor.delete_to_previous_word_start(&Default::default(), window, cx);
6825 assert_eq!(
6826 editor.text(cx),
6827 "
6828 a
6829 b
6830 c
6831 "
6832 .unindent()
6833 );
6834 assert_eq!(
6835 editor.selections.ranges::<Point>(cx),
6836 [
6837 Point::new(0, 1)..Point::new(0, 1),
6838 Point::new(1, 1)..Point::new(1, 1),
6839 Point::new(2, 1)..Point::new(2, 1)
6840 ]
6841 );
6842 });
6843}
6844
6845#[gpui::test]
6846async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut gpui::TestAppContext) {
6847 init_test(cx, |settings| {
6848 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6849 });
6850
6851 let mut cx = EditorTestContext::new(cx).await;
6852
6853 let language = Arc::new(Language::new(
6854 LanguageConfig {
6855 brackets: BracketPairConfig {
6856 pairs: vec![
6857 BracketPair {
6858 start: "{".to_string(),
6859 end: "}".to_string(),
6860 close: true,
6861 surround: true,
6862 newline: true,
6863 },
6864 BracketPair {
6865 start: "(".to_string(),
6866 end: ")".to_string(),
6867 close: true,
6868 surround: true,
6869 newline: true,
6870 },
6871 BracketPair {
6872 start: "[".to_string(),
6873 end: "]".to_string(),
6874 close: false,
6875 surround: true,
6876 newline: true,
6877 },
6878 ],
6879 ..Default::default()
6880 },
6881 autoclose_before: "})]".to_string(),
6882 ..Default::default()
6883 },
6884 Some(tree_sitter_rust::LANGUAGE.into()),
6885 ));
6886
6887 cx.language_registry().add(language.clone());
6888 cx.update_buffer(|buffer, cx| {
6889 buffer.set_language(Some(language), cx);
6890 });
6891
6892 cx.set_state(
6893 &"
6894 {(ˇ)}
6895 [[ˇ]]
6896 {(ˇ)}
6897 "
6898 .unindent(),
6899 );
6900
6901 cx.update_editor(|editor, window, cx| {
6902 editor.backspace(&Default::default(), window, cx);
6903 editor.backspace(&Default::default(), window, cx);
6904 });
6905
6906 cx.assert_editor_state(
6907 &"
6908 ˇ
6909 ˇ]]
6910 ˇ
6911 "
6912 .unindent(),
6913 );
6914
6915 cx.update_editor(|editor, window, cx| {
6916 editor.handle_input("{", window, cx);
6917 editor.handle_input("{", window, cx);
6918 editor.move_right(&MoveRight, window, cx);
6919 editor.move_right(&MoveRight, window, cx);
6920 editor.move_left(&MoveLeft, window, cx);
6921 editor.move_left(&MoveLeft, window, cx);
6922 editor.backspace(&Default::default(), window, cx);
6923 });
6924
6925 cx.assert_editor_state(
6926 &"
6927 {ˇ}
6928 {ˇ}]]
6929 {ˇ}
6930 "
6931 .unindent(),
6932 );
6933
6934 cx.update_editor(|editor, window, cx| {
6935 editor.backspace(&Default::default(), window, cx);
6936 });
6937
6938 cx.assert_editor_state(
6939 &"
6940 ˇ
6941 ˇ]]
6942 ˇ
6943 "
6944 .unindent(),
6945 );
6946}
6947
6948#[gpui::test]
6949async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) {
6950 init_test(cx, |_| {});
6951
6952 let language = Arc::new(Language::new(
6953 LanguageConfig::default(),
6954 Some(tree_sitter_rust::LANGUAGE.into()),
6955 ));
6956
6957 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
6958 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6959 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6960 editor
6961 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6962 .await;
6963
6964 editor.update_in(cx, |editor, window, cx| {
6965 editor.set_auto_replace_emoji_shortcode(true);
6966
6967 editor.handle_input("Hello ", window, cx);
6968 editor.handle_input(":wave", window, cx);
6969 assert_eq!(editor.text(cx), "Hello :wave".unindent());
6970
6971 editor.handle_input(":", window, cx);
6972 assert_eq!(editor.text(cx), "Hello 👋".unindent());
6973
6974 editor.handle_input(" :smile", window, cx);
6975 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
6976
6977 editor.handle_input(":", window, cx);
6978 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
6979
6980 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
6981 editor.handle_input(":wave", window, cx);
6982 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
6983
6984 editor.handle_input(":", window, cx);
6985 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
6986
6987 editor.handle_input(":1", window, cx);
6988 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
6989
6990 editor.handle_input(":", window, cx);
6991 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
6992
6993 // Ensure shortcode does not get replaced when it is part of a word
6994 editor.handle_input(" Test:wave", window, cx);
6995 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
6996
6997 editor.handle_input(":", window, cx);
6998 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
6999
7000 editor.set_auto_replace_emoji_shortcode(false);
7001
7002 // Ensure shortcode does not get replaced when auto replace is off
7003 editor.handle_input(" :wave", window, cx);
7004 assert_eq!(
7005 editor.text(cx),
7006 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
7007 );
7008
7009 editor.handle_input(":", window, cx);
7010 assert_eq!(
7011 editor.text(cx),
7012 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
7013 );
7014 });
7015}
7016
7017#[gpui::test]
7018async fn test_snippet_placeholder_choices(cx: &mut gpui::TestAppContext) {
7019 init_test(cx, |_| {});
7020
7021 let (text, insertion_ranges) = marked_text_ranges(
7022 indoc! {"
7023 ˇ
7024 "},
7025 false,
7026 );
7027
7028 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7029 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7030
7031 _ = editor.update_in(cx, |editor, window, cx| {
7032 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7033
7034 editor
7035 .insert_snippet(&insertion_ranges, snippet, window, cx)
7036 .unwrap();
7037
7038 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7039 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7040 assert_eq!(editor.text(cx), expected_text);
7041 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7042 }
7043
7044 assert(
7045 editor,
7046 cx,
7047 indoc! {"
7048 type «» =•
7049 "},
7050 );
7051
7052 assert!(editor.context_menu_visible(), "There should be a matches");
7053 });
7054}
7055
7056#[gpui::test]
7057async fn test_snippets(cx: &mut gpui::TestAppContext) {
7058 init_test(cx, |_| {});
7059
7060 let (text, insertion_ranges) = marked_text_ranges(
7061 indoc! {"
7062 a.ˇ b
7063 a.ˇ b
7064 a.ˇ b
7065 "},
7066 false,
7067 );
7068
7069 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7070 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7071
7072 editor.update_in(cx, |editor, window, cx| {
7073 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
7074
7075 editor
7076 .insert_snippet(&insertion_ranges, snippet, window, cx)
7077 .unwrap();
7078
7079 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7080 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7081 assert_eq!(editor.text(cx), expected_text);
7082 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7083 }
7084
7085 assert(
7086 editor,
7087 cx,
7088 indoc! {"
7089 a.f(«one», two, «three») b
7090 a.f(«one», two, «three») b
7091 a.f(«one», two, «three») b
7092 "},
7093 );
7094
7095 // Can't move earlier than the first tab stop
7096 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7097 assert(
7098 editor,
7099 cx,
7100 indoc! {"
7101 a.f(«one», two, «three») b
7102 a.f(«one», two, «three») b
7103 a.f(«one», two, «three») b
7104 "},
7105 );
7106
7107 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7108 assert(
7109 editor,
7110 cx,
7111 indoc! {"
7112 a.f(one, «two», three) b
7113 a.f(one, «two», three) b
7114 a.f(one, «two», three) b
7115 "},
7116 );
7117
7118 editor.move_to_prev_snippet_tabstop(window, cx);
7119 assert(
7120 editor,
7121 cx,
7122 indoc! {"
7123 a.f(«one», two, «three») b
7124 a.f(«one», two, «three») b
7125 a.f(«one», two, «three») b
7126 "},
7127 );
7128
7129 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7130 assert(
7131 editor,
7132 cx,
7133 indoc! {"
7134 a.f(one, «two», three) b
7135 a.f(one, «two», three) b
7136 a.f(one, «two», three) b
7137 "},
7138 );
7139 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7140 assert(
7141 editor,
7142 cx,
7143 indoc! {"
7144 a.f(one, two, three)ˇ b
7145 a.f(one, two, three)ˇ b
7146 a.f(one, two, three)ˇ b
7147 "},
7148 );
7149
7150 // As soon as the last tab stop is reached, snippet state is gone
7151 editor.move_to_prev_snippet_tabstop(window, cx);
7152 assert(
7153 editor,
7154 cx,
7155 indoc! {"
7156 a.f(one, two, three)ˇ b
7157 a.f(one, two, three)ˇ b
7158 a.f(one, two, three)ˇ b
7159 "},
7160 );
7161 });
7162}
7163
7164#[gpui::test]
7165async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
7166 init_test(cx, |_| {});
7167
7168 let fs = FakeFs::new(cx.executor());
7169 fs.insert_file(path!("/file.rs"), Default::default()).await;
7170
7171 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
7172
7173 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7174 language_registry.add(rust_lang());
7175 let mut fake_servers = language_registry.register_fake_lsp(
7176 "Rust",
7177 FakeLspAdapter {
7178 capabilities: lsp::ServerCapabilities {
7179 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7180 ..Default::default()
7181 },
7182 ..Default::default()
7183 },
7184 );
7185
7186 let buffer = project
7187 .update(cx, |project, cx| {
7188 project.open_local_buffer(path!("/file.rs"), cx)
7189 })
7190 .await
7191 .unwrap();
7192
7193 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7194 let (editor, cx) = cx.add_window_view(|window, cx| {
7195 build_editor_with_project(project.clone(), buffer, window, cx)
7196 });
7197 editor.update_in(cx, |editor, window, cx| {
7198 editor.set_text("one\ntwo\nthree\n", window, cx)
7199 });
7200 assert!(cx.read(|cx| editor.is_dirty(cx)));
7201
7202 cx.executor().start_waiting();
7203 let fake_server = fake_servers.next().await.unwrap();
7204
7205 let save = editor
7206 .update_in(cx, |editor, window, cx| {
7207 editor.save(true, project.clone(), window, cx)
7208 })
7209 .unwrap();
7210 fake_server
7211 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7212 assert_eq!(
7213 params.text_document.uri,
7214 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7215 );
7216 assert_eq!(params.options.tab_size, 4);
7217 Ok(Some(vec![lsp::TextEdit::new(
7218 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7219 ", ".to_string(),
7220 )]))
7221 })
7222 .next()
7223 .await;
7224 cx.executor().start_waiting();
7225 save.await;
7226
7227 assert_eq!(
7228 editor.update(cx, |editor, cx| editor.text(cx)),
7229 "one, two\nthree\n"
7230 );
7231 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7232
7233 editor.update_in(cx, |editor, window, cx| {
7234 editor.set_text("one\ntwo\nthree\n", window, cx)
7235 });
7236 assert!(cx.read(|cx| editor.is_dirty(cx)));
7237
7238 // Ensure we can still save even if formatting hangs.
7239 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7240 assert_eq!(
7241 params.text_document.uri,
7242 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7243 );
7244 futures::future::pending::<()>().await;
7245 unreachable!()
7246 });
7247 let save = editor
7248 .update_in(cx, |editor, window, cx| {
7249 editor.save(true, project.clone(), window, cx)
7250 })
7251 .unwrap();
7252 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7253 cx.executor().start_waiting();
7254 save.await;
7255 assert_eq!(
7256 editor.update(cx, |editor, cx| editor.text(cx)),
7257 "one\ntwo\nthree\n"
7258 );
7259 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7260
7261 // For non-dirty buffer, no formatting request should be sent
7262 let save = editor
7263 .update_in(cx, |editor, window, cx| {
7264 editor.save(true, project.clone(), window, cx)
7265 })
7266 .unwrap();
7267 let _pending_format_request = fake_server
7268 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7269 panic!("Should not be invoked on non-dirty buffer");
7270 })
7271 .next();
7272 cx.executor().start_waiting();
7273 save.await;
7274
7275 // Set rust language override and assert overridden tabsize is sent to language server
7276 update_test_language_settings(cx, |settings| {
7277 settings.languages.insert(
7278 "Rust".into(),
7279 LanguageSettingsContent {
7280 tab_size: NonZeroU32::new(8),
7281 ..Default::default()
7282 },
7283 );
7284 });
7285
7286 editor.update_in(cx, |editor, window, cx| {
7287 editor.set_text("somehting_new\n", window, cx)
7288 });
7289 assert!(cx.read(|cx| editor.is_dirty(cx)));
7290 let save = editor
7291 .update_in(cx, |editor, window, cx| {
7292 editor.save(true, project.clone(), window, cx)
7293 })
7294 .unwrap();
7295 fake_server
7296 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7297 assert_eq!(
7298 params.text_document.uri,
7299 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7300 );
7301 assert_eq!(params.options.tab_size, 8);
7302 Ok(Some(vec![]))
7303 })
7304 .next()
7305 .await;
7306 cx.executor().start_waiting();
7307 save.await;
7308}
7309
7310#[gpui::test]
7311async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) {
7312 init_test(cx, |_| {});
7313
7314 let cols = 4;
7315 let rows = 10;
7316 let sample_text_1 = sample_text(rows, cols, 'a');
7317 assert_eq!(
7318 sample_text_1,
7319 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7320 );
7321 let sample_text_2 = sample_text(rows, cols, 'l');
7322 assert_eq!(
7323 sample_text_2,
7324 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7325 );
7326 let sample_text_3 = sample_text(rows, cols, 'v');
7327 assert_eq!(
7328 sample_text_3,
7329 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7330 );
7331
7332 let fs = FakeFs::new(cx.executor());
7333 fs.insert_tree(
7334 path!("/a"),
7335 json!({
7336 "main.rs": sample_text_1,
7337 "other.rs": sample_text_2,
7338 "lib.rs": sample_text_3,
7339 }),
7340 )
7341 .await;
7342
7343 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
7344 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7345 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7346
7347 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7348 language_registry.add(rust_lang());
7349 let mut fake_servers = language_registry.register_fake_lsp(
7350 "Rust",
7351 FakeLspAdapter {
7352 capabilities: lsp::ServerCapabilities {
7353 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7354 ..Default::default()
7355 },
7356 ..Default::default()
7357 },
7358 );
7359
7360 let worktree = project.update(cx, |project, cx| {
7361 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7362 assert_eq!(worktrees.len(), 1);
7363 worktrees.pop().unwrap()
7364 });
7365 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
7366
7367 let buffer_1 = project
7368 .update(cx, |project, cx| {
7369 project.open_buffer((worktree_id, "main.rs"), cx)
7370 })
7371 .await
7372 .unwrap();
7373 let buffer_2 = project
7374 .update(cx, |project, cx| {
7375 project.open_buffer((worktree_id, "other.rs"), cx)
7376 })
7377 .await
7378 .unwrap();
7379 let buffer_3 = project
7380 .update(cx, |project, cx| {
7381 project.open_buffer((worktree_id, "lib.rs"), cx)
7382 })
7383 .await
7384 .unwrap();
7385
7386 let multi_buffer = cx.new(|cx| {
7387 let mut multi_buffer = MultiBuffer::new(ReadWrite);
7388 multi_buffer.push_excerpts(
7389 buffer_1.clone(),
7390 [
7391 ExcerptRange {
7392 context: Point::new(0, 0)..Point::new(3, 0),
7393 primary: None,
7394 },
7395 ExcerptRange {
7396 context: Point::new(5, 0)..Point::new(7, 0),
7397 primary: None,
7398 },
7399 ExcerptRange {
7400 context: Point::new(9, 0)..Point::new(10, 4),
7401 primary: None,
7402 },
7403 ],
7404 cx,
7405 );
7406 multi_buffer.push_excerpts(
7407 buffer_2.clone(),
7408 [
7409 ExcerptRange {
7410 context: Point::new(0, 0)..Point::new(3, 0),
7411 primary: None,
7412 },
7413 ExcerptRange {
7414 context: Point::new(5, 0)..Point::new(7, 0),
7415 primary: None,
7416 },
7417 ExcerptRange {
7418 context: Point::new(9, 0)..Point::new(10, 4),
7419 primary: None,
7420 },
7421 ],
7422 cx,
7423 );
7424 multi_buffer.push_excerpts(
7425 buffer_3.clone(),
7426 [
7427 ExcerptRange {
7428 context: Point::new(0, 0)..Point::new(3, 0),
7429 primary: None,
7430 },
7431 ExcerptRange {
7432 context: Point::new(5, 0)..Point::new(7, 0),
7433 primary: None,
7434 },
7435 ExcerptRange {
7436 context: Point::new(9, 0)..Point::new(10, 4),
7437 primary: None,
7438 },
7439 ],
7440 cx,
7441 );
7442 multi_buffer
7443 });
7444 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
7445 Editor::new(
7446 EditorMode::Full,
7447 multi_buffer,
7448 Some(project.clone()),
7449 true,
7450 window,
7451 cx,
7452 )
7453 });
7454
7455 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7456 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7457 s.select_ranges(Some(1..2))
7458 });
7459 editor.insert("|one|two|three|", window, cx);
7460 });
7461 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7462 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7463 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7464 s.select_ranges(Some(60..70))
7465 });
7466 editor.insert("|four|five|six|", window, cx);
7467 });
7468 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7469
7470 // First two buffers should be edited, but not the third one.
7471 assert_eq!(
7472 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7473 "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}",
7474 );
7475 buffer_1.update(cx, |buffer, _| {
7476 assert!(buffer.is_dirty());
7477 assert_eq!(
7478 buffer.text(),
7479 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
7480 )
7481 });
7482 buffer_2.update(cx, |buffer, _| {
7483 assert!(buffer.is_dirty());
7484 assert_eq!(
7485 buffer.text(),
7486 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
7487 )
7488 });
7489 buffer_3.update(cx, |buffer, _| {
7490 assert!(!buffer.is_dirty());
7491 assert_eq!(buffer.text(), sample_text_3,)
7492 });
7493 cx.executor().run_until_parked();
7494
7495 cx.executor().start_waiting();
7496 let save = multi_buffer_editor
7497 .update_in(cx, |editor, window, cx| {
7498 editor.save(true, project.clone(), window, cx)
7499 })
7500 .unwrap();
7501
7502 let fake_server = fake_servers.next().await.unwrap();
7503 fake_server
7504 .server
7505 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7506 Ok(Some(vec![lsp::TextEdit::new(
7507 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7508 format!("[{} formatted]", params.text_document.uri),
7509 )]))
7510 })
7511 .detach();
7512 save.await;
7513
7514 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
7515 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
7516 assert_eq!(
7517 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7518 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}"),
7519 );
7520 buffer_1.update(cx, |buffer, _| {
7521 assert!(!buffer.is_dirty());
7522 assert_eq!(
7523 buffer.text(),
7524 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
7525 )
7526 });
7527 buffer_2.update(cx, |buffer, _| {
7528 assert!(!buffer.is_dirty());
7529 assert_eq!(
7530 buffer.text(),
7531 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
7532 )
7533 });
7534 buffer_3.update(cx, |buffer, _| {
7535 assert!(!buffer.is_dirty());
7536 assert_eq!(buffer.text(), sample_text_3,)
7537 });
7538}
7539
7540#[gpui::test]
7541async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
7542 init_test(cx, |_| {});
7543
7544 let fs = FakeFs::new(cx.executor());
7545 fs.insert_file(path!("/file.rs"), Default::default()).await;
7546
7547 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
7548
7549 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7550 language_registry.add(rust_lang());
7551 let mut fake_servers = language_registry.register_fake_lsp(
7552 "Rust",
7553 FakeLspAdapter {
7554 capabilities: lsp::ServerCapabilities {
7555 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
7556 ..Default::default()
7557 },
7558 ..Default::default()
7559 },
7560 );
7561
7562 let buffer = project
7563 .update(cx, |project, cx| {
7564 project.open_local_buffer(path!("/file.rs"), cx)
7565 })
7566 .await
7567 .unwrap();
7568
7569 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7570 let (editor, cx) = cx.add_window_view(|window, cx| {
7571 build_editor_with_project(project.clone(), buffer, window, cx)
7572 });
7573 editor.update_in(cx, |editor, window, cx| {
7574 editor.set_text("one\ntwo\nthree\n", window, cx)
7575 });
7576 assert!(cx.read(|cx| editor.is_dirty(cx)));
7577
7578 cx.executor().start_waiting();
7579 let fake_server = fake_servers.next().await.unwrap();
7580
7581 let save = editor
7582 .update_in(cx, |editor, window, cx| {
7583 editor.save(true, project.clone(), window, cx)
7584 })
7585 .unwrap();
7586 fake_server
7587 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
7588 assert_eq!(
7589 params.text_document.uri,
7590 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7591 );
7592 assert_eq!(params.options.tab_size, 4);
7593 Ok(Some(vec![lsp::TextEdit::new(
7594 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7595 ", ".to_string(),
7596 )]))
7597 })
7598 .next()
7599 .await;
7600 cx.executor().start_waiting();
7601 save.await;
7602 assert_eq!(
7603 editor.update(cx, |editor, cx| editor.text(cx)),
7604 "one, two\nthree\n"
7605 );
7606 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7607
7608 editor.update_in(cx, |editor, window, cx| {
7609 editor.set_text("one\ntwo\nthree\n", window, cx)
7610 });
7611 assert!(cx.read(|cx| editor.is_dirty(cx)));
7612
7613 // Ensure we can still save even if formatting hangs.
7614 fake_server.handle_request::<lsp::request::RangeFormatting, _, _>(
7615 move |params, _| async move {
7616 assert_eq!(
7617 params.text_document.uri,
7618 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7619 );
7620 futures::future::pending::<()>().await;
7621 unreachable!()
7622 },
7623 );
7624 let save = editor
7625 .update_in(cx, |editor, window, cx| {
7626 editor.save(true, project.clone(), window, cx)
7627 })
7628 .unwrap();
7629 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7630 cx.executor().start_waiting();
7631 save.await;
7632 assert_eq!(
7633 editor.update(cx, |editor, cx| editor.text(cx)),
7634 "one\ntwo\nthree\n"
7635 );
7636 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7637
7638 // For non-dirty buffer, no formatting request should be sent
7639 let save = editor
7640 .update_in(cx, |editor, window, cx| {
7641 editor.save(true, project.clone(), window, cx)
7642 })
7643 .unwrap();
7644 let _pending_format_request = fake_server
7645 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7646 panic!("Should not be invoked on non-dirty buffer");
7647 })
7648 .next();
7649 cx.executor().start_waiting();
7650 save.await;
7651
7652 // Set Rust language override and assert overridden tabsize is sent to language server
7653 update_test_language_settings(cx, |settings| {
7654 settings.languages.insert(
7655 "Rust".into(),
7656 LanguageSettingsContent {
7657 tab_size: NonZeroU32::new(8),
7658 ..Default::default()
7659 },
7660 );
7661 });
7662
7663 editor.update_in(cx, |editor, window, cx| {
7664 editor.set_text("somehting_new\n", window, cx)
7665 });
7666 assert!(cx.read(|cx| editor.is_dirty(cx)));
7667 let save = editor
7668 .update_in(cx, |editor, window, cx| {
7669 editor.save(true, project.clone(), window, cx)
7670 })
7671 .unwrap();
7672 fake_server
7673 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
7674 assert_eq!(
7675 params.text_document.uri,
7676 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7677 );
7678 assert_eq!(params.options.tab_size, 8);
7679 Ok(Some(vec![]))
7680 })
7681 .next()
7682 .await;
7683 cx.executor().start_waiting();
7684 save.await;
7685}
7686
7687#[gpui::test]
7688async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
7689 init_test(cx, |settings| {
7690 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
7691 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
7692 ))
7693 });
7694
7695 let fs = FakeFs::new(cx.executor());
7696 fs.insert_file(path!("/file.rs"), Default::default()).await;
7697
7698 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
7699
7700 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7701 language_registry.add(Arc::new(Language::new(
7702 LanguageConfig {
7703 name: "Rust".into(),
7704 matcher: LanguageMatcher {
7705 path_suffixes: vec!["rs".to_string()],
7706 ..Default::default()
7707 },
7708 ..LanguageConfig::default()
7709 },
7710 Some(tree_sitter_rust::LANGUAGE.into()),
7711 )));
7712 update_test_language_settings(cx, |settings| {
7713 // Enable Prettier formatting for the same buffer, and ensure
7714 // LSP is called instead of Prettier.
7715 settings.defaults.prettier = Some(PrettierSettings {
7716 allowed: true,
7717 ..PrettierSettings::default()
7718 });
7719 });
7720 let mut fake_servers = language_registry.register_fake_lsp(
7721 "Rust",
7722 FakeLspAdapter {
7723 capabilities: lsp::ServerCapabilities {
7724 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7725 ..Default::default()
7726 },
7727 ..Default::default()
7728 },
7729 );
7730
7731 let buffer = project
7732 .update(cx, |project, cx| {
7733 project.open_local_buffer(path!("/file.rs"), cx)
7734 })
7735 .await
7736 .unwrap();
7737
7738 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7739 let (editor, cx) = cx.add_window_view(|window, cx| {
7740 build_editor_with_project(project.clone(), buffer, window, cx)
7741 });
7742 editor.update_in(cx, |editor, window, cx| {
7743 editor.set_text("one\ntwo\nthree\n", window, cx)
7744 });
7745
7746 cx.executor().start_waiting();
7747 let fake_server = fake_servers.next().await.unwrap();
7748
7749 let format = editor
7750 .update_in(cx, |editor, window, cx| {
7751 editor.perform_format(
7752 project.clone(),
7753 FormatTrigger::Manual,
7754 FormatTarget::Buffers,
7755 window,
7756 cx,
7757 )
7758 })
7759 .unwrap();
7760 fake_server
7761 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7762 assert_eq!(
7763 params.text_document.uri,
7764 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7765 );
7766 assert_eq!(params.options.tab_size, 4);
7767 Ok(Some(vec![lsp::TextEdit::new(
7768 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7769 ", ".to_string(),
7770 )]))
7771 })
7772 .next()
7773 .await;
7774 cx.executor().start_waiting();
7775 format.await;
7776 assert_eq!(
7777 editor.update(cx, |editor, cx| editor.text(cx)),
7778 "one, two\nthree\n"
7779 );
7780
7781 editor.update_in(cx, |editor, window, cx| {
7782 editor.set_text("one\ntwo\nthree\n", window, cx)
7783 });
7784 // Ensure we don't lock if formatting hangs.
7785 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7786 assert_eq!(
7787 params.text_document.uri,
7788 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7789 );
7790 futures::future::pending::<()>().await;
7791 unreachable!()
7792 });
7793 let format = editor
7794 .update_in(cx, |editor, window, cx| {
7795 editor.perform_format(
7796 project,
7797 FormatTrigger::Manual,
7798 FormatTarget::Buffers,
7799 window,
7800 cx,
7801 )
7802 })
7803 .unwrap();
7804 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7805 cx.executor().start_waiting();
7806 format.await;
7807 assert_eq!(
7808 editor.update(cx, |editor, cx| editor.text(cx)),
7809 "one\ntwo\nthree\n"
7810 );
7811}
7812
7813#[gpui::test]
7814async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
7815 init_test(cx, |_| {});
7816
7817 let mut cx = EditorLspTestContext::new_rust(
7818 lsp::ServerCapabilities {
7819 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7820 ..Default::default()
7821 },
7822 cx,
7823 )
7824 .await;
7825
7826 cx.set_state(indoc! {"
7827 one.twoˇ
7828 "});
7829
7830 // The format request takes a long time. When it completes, it inserts
7831 // a newline and an indent before the `.`
7832 cx.lsp
7833 .handle_request::<lsp::request::Formatting, _, _>(move |_, cx| {
7834 let executor = cx.background_executor().clone();
7835 async move {
7836 executor.timer(Duration::from_millis(100)).await;
7837 Ok(Some(vec![lsp::TextEdit {
7838 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
7839 new_text: "\n ".into(),
7840 }]))
7841 }
7842 });
7843
7844 // Submit a format request.
7845 let format_1 = cx
7846 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7847 .unwrap();
7848 cx.executor().run_until_parked();
7849
7850 // Submit a second format request.
7851 let format_2 = cx
7852 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7853 .unwrap();
7854 cx.executor().run_until_parked();
7855
7856 // Wait for both format requests to complete
7857 cx.executor().advance_clock(Duration::from_millis(200));
7858 cx.executor().start_waiting();
7859 format_1.await.unwrap();
7860 cx.executor().start_waiting();
7861 format_2.await.unwrap();
7862
7863 // The formatting edits only happens once.
7864 cx.assert_editor_state(indoc! {"
7865 one
7866 .twoˇ
7867 "});
7868}
7869
7870#[gpui::test]
7871async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
7872 init_test(cx, |settings| {
7873 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
7874 });
7875
7876 let mut cx = EditorLspTestContext::new_rust(
7877 lsp::ServerCapabilities {
7878 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7879 ..Default::default()
7880 },
7881 cx,
7882 )
7883 .await;
7884
7885 // Set up a buffer white some trailing whitespace and no trailing newline.
7886 cx.set_state(
7887 &[
7888 "one ", //
7889 "twoˇ", //
7890 "three ", //
7891 "four", //
7892 ]
7893 .join("\n"),
7894 );
7895
7896 // Submit a format request.
7897 let format = cx
7898 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7899 .unwrap();
7900
7901 // Record which buffer changes have been sent to the language server
7902 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
7903 cx.lsp
7904 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
7905 let buffer_changes = buffer_changes.clone();
7906 move |params, _| {
7907 buffer_changes.lock().extend(
7908 params
7909 .content_changes
7910 .into_iter()
7911 .map(|e| (e.range.unwrap(), e.text)),
7912 );
7913 }
7914 });
7915
7916 // Handle formatting requests to the language server.
7917 cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
7918 let buffer_changes = buffer_changes.clone();
7919 move |_, _| {
7920 // When formatting is requested, trailing whitespace has already been stripped,
7921 // and the trailing newline has already been added.
7922 assert_eq!(
7923 &buffer_changes.lock()[1..],
7924 &[
7925 (
7926 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
7927 "".into()
7928 ),
7929 (
7930 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
7931 "".into()
7932 ),
7933 (
7934 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
7935 "\n".into()
7936 ),
7937 ]
7938 );
7939
7940 // Insert blank lines between each line of the buffer.
7941 async move {
7942 Ok(Some(vec![
7943 lsp::TextEdit {
7944 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
7945 new_text: "\n".into(),
7946 },
7947 lsp::TextEdit {
7948 range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
7949 new_text: "\n".into(),
7950 },
7951 ]))
7952 }
7953 }
7954 });
7955
7956 // After formatting the buffer, the trailing whitespace is stripped,
7957 // a newline is appended, and the edits provided by the language server
7958 // have been applied.
7959 format.await.unwrap();
7960 cx.assert_editor_state(
7961 &[
7962 "one", //
7963 "", //
7964 "twoˇ", //
7965 "", //
7966 "three", //
7967 "four", //
7968 "", //
7969 ]
7970 .join("\n"),
7971 );
7972
7973 // Undoing the formatting undoes the trailing whitespace removal, the
7974 // trailing newline, and the LSP edits.
7975 cx.update_buffer(|buffer, cx| buffer.undo(cx));
7976 cx.assert_editor_state(
7977 &[
7978 "one ", //
7979 "twoˇ", //
7980 "three ", //
7981 "four", //
7982 ]
7983 .join("\n"),
7984 );
7985}
7986
7987#[gpui::test]
7988async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
7989 cx: &mut gpui::TestAppContext,
7990) {
7991 init_test(cx, |_| {});
7992
7993 cx.update(|cx| {
7994 cx.update_global::<SettingsStore, _>(|settings, cx| {
7995 settings.update_user_settings::<EditorSettings>(cx, |settings| {
7996 settings.auto_signature_help = Some(true);
7997 });
7998 });
7999 });
8000
8001 let mut cx = EditorLspTestContext::new_rust(
8002 lsp::ServerCapabilities {
8003 signature_help_provider: Some(lsp::SignatureHelpOptions {
8004 ..Default::default()
8005 }),
8006 ..Default::default()
8007 },
8008 cx,
8009 )
8010 .await;
8011
8012 let language = Language::new(
8013 LanguageConfig {
8014 name: "Rust".into(),
8015 brackets: BracketPairConfig {
8016 pairs: vec![
8017 BracketPair {
8018 start: "{".to_string(),
8019 end: "}".to_string(),
8020 close: true,
8021 surround: true,
8022 newline: true,
8023 },
8024 BracketPair {
8025 start: "(".to_string(),
8026 end: ")".to_string(),
8027 close: true,
8028 surround: true,
8029 newline: true,
8030 },
8031 BracketPair {
8032 start: "/*".to_string(),
8033 end: " */".to_string(),
8034 close: true,
8035 surround: true,
8036 newline: true,
8037 },
8038 BracketPair {
8039 start: "[".to_string(),
8040 end: "]".to_string(),
8041 close: false,
8042 surround: false,
8043 newline: true,
8044 },
8045 BracketPair {
8046 start: "\"".to_string(),
8047 end: "\"".to_string(),
8048 close: true,
8049 surround: true,
8050 newline: false,
8051 },
8052 BracketPair {
8053 start: "<".to_string(),
8054 end: ">".to_string(),
8055 close: false,
8056 surround: true,
8057 newline: true,
8058 },
8059 ],
8060 ..Default::default()
8061 },
8062 autoclose_before: "})]".to_string(),
8063 ..Default::default()
8064 },
8065 Some(tree_sitter_rust::LANGUAGE.into()),
8066 );
8067 let language = Arc::new(language);
8068
8069 cx.language_registry().add(language.clone());
8070 cx.update_buffer(|buffer, cx| {
8071 buffer.set_language(Some(language), cx);
8072 });
8073
8074 cx.set_state(
8075 &r#"
8076 fn main() {
8077 sampleˇ
8078 }
8079 "#
8080 .unindent(),
8081 );
8082
8083 cx.update_editor(|editor, window, cx| {
8084 editor.handle_input("(", window, cx);
8085 });
8086 cx.assert_editor_state(
8087 &"
8088 fn main() {
8089 sample(ˇ)
8090 }
8091 "
8092 .unindent(),
8093 );
8094
8095 let mocked_response = lsp::SignatureHelp {
8096 signatures: vec![lsp::SignatureInformation {
8097 label: "fn sample(param1: u8, param2: u8)".to_string(),
8098 documentation: None,
8099 parameters: Some(vec![
8100 lsp::ParameterInformation {
8101 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8102 documentation: None,
8103 },
8104 lsp::ParameterInformation {
8105 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8106 documentation: None,
8107 },
8108 ]),
8109 active_parameter: None,
8110 }],
8111 active_signature: Some(0),
8112 active_parameter: Some(0),
8113 };
8114 handle_signature_help_request(&mut cx, mocked_response).await;
8115
8116 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8117 .await;
8118
8119 cx.editor(|editor, _, _| {
8120 let signature_help_state = editor.signature_help_state.popover().cloned();
8121 assert!(signature_help_state.is_some());
8122 let ParsedMarkdown {
8123 text, highlights, ..
8124 } = signature_help_state.unwrap().parsed_content;
8125 assert_eq!(text, "param1: u8, param2: u8");
8126 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8127 });
8128}
8129
8130#[gpui::test]
8131async fn test_handle_input_with_different_show_signature_settings(cx: &mut gpui::TestAppContext) {
8132 init_test(cx, |_| {});
8133
8134 cx.update(|cx| {
8135 cx.update_global::<SettingsStore, _>(|settings, cx| {
8136 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8137 settings.auto_signature_help = Some(false);
8138 settings.show_signature_help_after_edits = Some(false);
8139 });
8140 });
8141 });
8142
8143 let mut cx = EditorLspTestContext::new_rust(
8144 lsp::ServerCapabilities {
8145 signature_help_provider: Some(lsp::SignatureHelpOptions {
8146 ..Default::default()
8147 }),
8148 ..Default::default()
8149 },
8150 cx,
8151 )
8152 .await;
8153
8154 let language = Language::new(
8155 LanguageConfig {
8156 name: "Rust".into(),
8157 brackets: BracketPairConfig {
8158 pairs: vec![
8159 BracketPair {
8160 start: "{".to_string(),
8161 end: "}".to_string(),
8162 close: true,
8163 surround: true,
8164 newline: true,
8165 },
8166 BracketPair {
8167 start: "(".to_string(),
8168 end: ")".to_string(),
8169 close: true,
8170 surround: true,
8171 newline: true,
8172 },
8173 BracketPair {
8174 start: "/*".to_string(),
8175 end: " */".to_string(),
8176 close: true,
8177 surround: true,
8178 newline: true,
8179 },
8180 BracketPair {
8181 start: "[".to_string(),
8182 end: "]".to_string(),
8183 close: false,
8184 surround: false,
8185 newline: true,
8186 },
8187 BracketPair {
8188 start: "\"".to_string(),
8189 end: "\"".to_string(),
8190 close: true,
8191 surround: true,
8192 newline: false,
8193 },
8194 BracketPair {
8195 start: "<".to_string(),
8196 end: ">".to_string(),
8197 close: false,
8198 surround: true,
8199 newline: true,
8200 },
8201 ],
8202 ..Default::default()
8203 },
8204 autoclose_before: "})]".to_string(),
8205 ..Default::default()
8206 },
8207 Some(tree_sitter_rust::LANGUAGE.into()),
8208 );
8209 let language = Arc::new(language);
8210
8211 cx.language_registry().add(language.clone());
8212 cx.update_buffer(|buffer, cx| {
8213 buffer.set_language(Some(language), cx);
8214 });
8215
8216 // Ensure that signature_help is not called when no signature help is enabled.
8217 cx.set_state(
8218 &r#"
8219 fn main() {
8220 sampleˇ
8221 }
8222 "#
8223 .unindent(),
8224 );
8225 cx.update_editor(|editor, window, cx| {
8226 editor.handle_input("(", window, cx);
8227 });
8228 cx.assert_editor_state(
8229 &"
8230 fn main() {
8231 sample(ˇ)
8232 }
8233 "
8234 .unindent(),
8235 );
8236 cx.editor(|editor, _, _| {
8237 assert!(editor.signature_help_state.task().is_none());
8238 });
8239
8240 let mocked_response = lsp::SignatureHelp {
8241 signatures: vec![lsp::SignatureInformation {
8242 label: "fn sample(param1: u8, param2: u8)".to_string(),
8243 documentation: None,
8244 parameters: Some(vec![
8245 lsp::ParameterInformation {
8246 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8247 documentation: None,
8248 },
8249 lsp::ParameterInformation {
8250 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8251 documentation: None,
8252 },
8253 ]),
8254 active_parameter: None,
8255 }],
8256 active_signature: Some(0),
8257 active_parameter: Some(0),
8258 };
8259
8260 // Ensure that signature_help is called when enabled afte edits
8261 cx.update(|_, cx| {
8262 cx.update_global::<SettingsStore, _>(|settings, cx| {
8263 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8264 settings.auto_signature_help = Some(false);
8265 settings.show_signature_help_after_edits = Some(true);
8266 });
8267 });
8268 });
8269 cx.set_state(
8270 &r#"
8271 fn main() {
8272 sampleˇ
8273 }
8274 "#
8275 .unindent(),
8276 );
8277 cx.update_editor(|editor, window, cx| {
8278 editor.handle_input("(", window, cx);
8279 });
8280 cx.assert_editor_state(
8281 &"
8282 fn main() {
8283 sample(ˇ)
8284 }
8285 "
8286 .unindent(),
8287 );
8288 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8289 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8290 .await;
8291 cx.update_editor(|editor, _, _| {
8292 let signature_help_state = editor.signature_help_state.popover().cloned();
8293 assert!(signature_help_state.is_some());
8294 let ParsedMarkdown {
8295 text, highlights, ..
8296 } = signature_help_state.unwrap().parsed_content;
8297 assert_eq!(text, "param1: u8, param2: u8");
8298 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8299 editor.signature_help_state = SignatureHelpState::default();
8300 });
8301
8302 // Ensure that signature_help is called when auto signature help override is enabled
8303 cx.update(|_, cx| {
8304 cx.update_global::<SettingsStore, _>(|settings, cx| {
8305 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8306 settings.auto_signature_help = Some(true);
8307 settings.show_signature_help_after_edits = Some(false);
8308 });
8309 });
8310 });
8311 cx.set_state(
8312 &r#"
8313 fn main() {
8314 sampleˇ
8315 }
8316 "#
8317 .unindent(),
8318 );
8319 cx.update_editor(|editor, window, cx| {
8320 editor.handle_input("(", window, cx);
8321 });
8322 cx.assert_editor_state(
8323 &"
8324 fn main() {
8325 sample(ˇ)
8326 }
8327 "
8328 .unindent(),
8329 );
8330 handle_signature_help_request(&mut cx, mocked_response).await;
8331 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8332 .await;
8333 cx.editor(|editor, _, _| {
8334 let signature_help_state = editor.signature_help_state.popover().cloned();
8335 assert!(signature_help_state.is_some());
8336 let ParsedMarkdown {
8337 text, highlights, ..
8338 } = signature_help_state.unwrap().parsed_content;
8339 assert_eq!(text, "param1: u8, param2: u8");
8340 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8341 });
8342}
8343
8344#[gpui::test]
8345async fn test_signature_help(cx: &mut gpui::TestAppContext) {
8346 init_test(cx, |_| {});
8347 cx.update(|cx| {
8348 cx.update_global::<SettingsStore, _>(|settings, cx| {
8349 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8350 settings.auto_signature_help = Some(true);
8351 });
8352 });
8353 });
8354
8355 let mut cx = EditorLspTestContext::new_rust(
8356 lsp::ServerCapabilities {
8357 signature_help_provider: Some(lsp::SignatureHelpOptions {
8358 ..Default::default()
8359 }),
8360 ..Default::default()
8361 },
8362 cx,
8363 )
8364 .await;
8365
8366 // A test that directly calls `show_signature_help`
8367 cx.update_editor(|editor, window, cx| {
8368 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8369 });
8370
8371 let mocked_response = lsp::SignatureHelp {
8372 signatures: vec![lsp::SignatureInformation {
8373 label: "fn sample(param1: u8, param2: u8)".to_string(),
8374 documentation: None,
8375 parameters: Some(vec![
8376 lsp::ParameterInformation {
8377 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8378 documentation: None,
8379 },
8380 lsp::ParameterInformation {
8381 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8382 documentation: None,
8383 },
8384 ]),
8385 active_parameter: None,
8386 }],
8387 active_signature: Some(0),
8388 active_parameter: Some(0),
8389 };
8390 handle_signature_help_request(&mut cx, mocked_response).await;
8391
8392 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8393 .await;
8394
8395 cx.editor(|editor, _, _| {
8396 let signature_help_state = editor.signature_help_state.popover().cloned();
8397 assert!(signature_help_state.is_some());
8398 let ParsedMarkdown {
8399 text, highlights, ..
8400 } = signature_help_state.unwrap().parsed_content;
8401 assert_eq!(text, "param1: u8, param2: u8");
8402 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8403 });
8404
8405 // When exiting outside from inside the brackets, `signature_help` is closed.
8406 cx.set_state(indoc! {"
8407 fn main() {
8408 sample(ˇ);
8409 }
8410
8411 fn sample(param1: u8, param2: u8) {}
8412 "});
8413
8414 cx.update_editor(|editor, window, cx| {
8415 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
8416 });
8417
8418 let mocked_response = lsp::SignatureHelp {
8419 signatures: Vec::new(),
8420 active_signature: None,
8421 active_parameter: None,
8422 };
8423 handle_signature_help_request(&mut cx, mocked_response).await;
8424
8425 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
8426 .await;
8427
8428 cx.editor(|editor, _, _| {
8429 assert!(!editor.signature_help_state.is_shown());
8430 });
8431
8432 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
8433 cx.set_state(indoc! {"
8434 fn main() {
8435 sample(ˇ);
8436 }
8437
8438 fn sample(param1: u8, param2: u8) {}
8439 "});
8440
8441 let mocked_response = lsp::SignatureHelp {
8442 signatures: vec![lsp::SignatureInformation {
8443 label: "fn sample(param1: u8, param2: u8)".to_string(),
8444 documentation: None,
8445 parameters: Some(vec![
8446 lsp::ParameterInformation {
8447 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8448 documentation: None,
8449 },
8450 lsp::ParameterInformation {
8451 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8452 documentation: None,
8453 },
8454 ]),
8455 active_parameter: None,
8456 }],
8457 active_signature: Some(0),
8458 active_parameter: Some(0),
8459 };
8460 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8461 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8462 .await;
8463 cx.editor(|editor, _, _| {
8464 assert!(editor.signature_help_state.is_shown());
8465 });
8466
8467 // Restore the popover with more parameter input
8468 cx.set_state(indoc! {"
8469 fn main() {
8470 sample(param1, param2ˇ);
8471 }
8472
8473 fn sample(param1: u8, param2: u8) {}
8474 "});
8475
8476 let mocked_response = lsp::SignatureHelp {
8477 signatures: vec![lsp::SignatureInformation {
8478 label: "fn sample(param1: u8, param2: u8)".to_string(),
8479 documentation: None,
8480 parameters: Some(vec![
8481 lsp::ParameterInformation {
8482 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8483 documentation: None,
8484 },
8485 lsp::ParameterInformation {
8486 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8487 documentation: None,
8488 },
8489 ]),
8490 active_parameter: None,
8491 }],
8492 active_signature: Some(0),
8493 active_parameter: Some(1),
8494 };
8495 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8496 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8497 .await;
8498
8499 // When selecting a range, the popover is gone.
8500 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
8501 cx.update_editor(|editor, window, cx| {
8502 editor.change_selections(None, window, cx, |s| {
8503 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
8504 })
8505 });
8506 cx.assert_editor_state(indoc! {"
8507 fn main() {
8508 sample(param1, «ˇparam2»);
8509 }
8510
8511 fn sample(param1: u8, param2: u8) {}
8512 "});
8513 cx.editor(|editor, _, _| {
8514 assert!(!editor.signature_help_state.is_shown());
8515 });
8516
8517 // When unselecting again, the popover is back if within the brackets.
8518 cx.update_editor(|editor, window, cx| {
8519 editor.change_selections(None, window, cx, |s| {
8520 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8521 })
8522 });
8523 cx.assert_editor_state(indoc! {"
8524 fn main() {
8525 sample(param1, ˇparam2);
8526 }
8527
8528 fn sample(param1: u8, param2: u8) {}
8529 "});
8530 handle_signature_help_request(&mut cx, mocked_response).await;
8531 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8532 .await;
8533 cx.editor(|editor, _, _| {
8534 assert!(editor.signature_help_state.is_shown());
8535 });
8536
8537 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
8538 cx.update_editor(|editor, window, cx| {
8539 editor.change_selections(None, window, cx, |s| {
8540 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
8541 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8542 })
8543 });
8544 cx.assert_editor_state(indoc! {"
8545 fn main() {
8546 sample(param1, ˇparam2);
8547 }
8548
8549 fn sample(param1: u8, param2: u8) {}
8550 "});
8551
8552 let mocked_response = lsp::SignatureHelp {
8553 signatures: vec![lsp::SignatureInformation {
8554 label: "fn sample(param1: u8, param2: u8)".to_string(),
8555 documentation: None,
8556 parameters: Some(vec![
8557 lsp::ParameterInformation {
8558 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8559 documentation: None,
8560 },
8561 lsp::ParameterInformation {
8562 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8563 documentation: None,
8564 },
8565 ]),
8566 active_parameter: None,
8567 }],
8568 active_signature: Some(0),
8569 active_parameter: Some(1),
8570 };
8571 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8572 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8573 .await;
8574 cx.update_editor(|editor, _, cx| {
8575 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
8576 });
8577 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
8578 .await;
8579 cx.update_editor(|editor, window, cx| {
8580 editor.change_selections(None, window, cx, |s| {
8581 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
8582 })
8583 });
8584 cx.assert_editor_state(indoc! {"
8585 fn main() {
8586 sample(param1, «ˇparam2»);
8587 }
8588
8589 fn sample(param1: u8, param2: u8) {}
8590 "});
8591 cx.update_editor(|editor, window, cx| {
8592 editor.change_selections(None, window, cx, |s| {
8593 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8594 })
8595 });
8596 cx.assert_editor_state(indoc! {"
8597 fn main() {
8598 sample(param1, ˇparam2);
8599 }
8600
8601 fn sample(param1: u8, param2: u8) {}
8602 "});
8603 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
8604 .await;
8605}
8606
8607#[gpui::test]
8608async fn test_completion(cx: &mut gpui::TestAppContext) {
8609 init_test(cx, |_| {});
8610
8611 let mut cx = EditorLspTestContext::new_rust(
8612 lsp::ServerCapabilities {
8613 completion_provider: Some(lsp::CompletionOptions {
8614 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
8615 resolve_provider: Some(true),
8616 ..Default::default()
8617 }),
8618 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
8619 ..Default::default()
8620 },
8621 cx,
8622 )
8623 .await;
8624 let counter = Arc::new(AtomicUsize::new(0));
8625
8626 cx.set_state(indoc! {"
8627 oneˇ
8628 two
8629 three
8630 "});
8631 cx.simulate_keystroke(".");
8632 handle_completion_request(
8633 &mut cx,
8634 indoc! {"
8635 one.|<>
8636 two
8637 three
8638 "},
8639 vec!["first_completion", "second_completion"],
8640 counter.clone(),
8641 )
8642 .await;
8643 cx.condition(|editor, _| editor.context_menu_visible())
8644 .await;
8645 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
8646
8647 let _handler = handle_signature_help_request(
8648 &mut cx,
8649 lsp::SignatureHelp {
8650 signatures: vec![lsp::SignatureInformation {
8651 label: "test signature".to_string(),
8652 documentation: None,
8653 parameters: Some(vec![lsp::ParameterInformation {
8654 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
8655 documentation: None,
8656 }]),
8657 active_parameter: None,
8658 }],
8659 active_signature: None,
8660 active_parameter: None,
8661 },
8662 );
8663 cx.update_editor(|editor, window, cx| {
8664 assert!(
8665 !editor.signature_help_state.is_shown(),
8666 "No signature help was called for"
8667 );
8668 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8669 });
8670 cx.run_until_parked();
8671 cx.update_editor(|editor, _, _| {
8672 assert!(
8673 !editor.signature_help_state.is_shown(),
8674 "No signature help should be shown when completions menu is open"
8675 );
8676 });
8677
8678 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8679 editor.context_menu_next(&Default::default(), window, cx);
8680 editor
8681 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8682 .unwrap()
8683 });
8684 cx.assert_editor_state(indoc! {"
8685 one.second_completionˇ
8686 two
8687 three
8688 "});
8689
8690 handle_resolve_completion_request(
8691 &mut cx,
8692 Some(vec![
8693 (
8694 //This overlaps with the primary completion edit which is
8695 //misbehavior from the LSP spec, test that we filter it out
8696 indoc! {"
8697 one.second_ˇcompletion
8698 two
8699 threeˇ
8700 "},
8701 "overlapping additional edit",
8702 ),
8703 (
8704 indoc! {"
8705 one.second_completion
8706 two
8707 threeˇ
8708 "},
8709 "\nadditional edit",
8710 ),
8711 ]),
8712 )
8713 .await;
8714 apply_additional_edits.await.unwrap();
8715 cx.assert_editor_state(indoc! {"
8716 one.second_completionˇ
8717 two
8718 three
8719 additional edit
8720 "});
8721
8722 cx.set_state(indoc! {"
8723 one.second_completion
8724 twoˇ
8725 threeˇ
8726 additional edit
8727 "});
8728 cx.simulate_keystroke(" ");
8729 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8730 cx.simulate_keystroke("s");
8731 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8732
8733 cx.assert_editor_state(indoc! {"
8734 one.second_completion
8735 two sˇ
8736 three sˇ
8737 additional edit
8738 "});
8739 handle_completion_request(
8740 &mut cx,
8741 indoc! {"
8742 one.second_completion
8743 two s
8744 three <s|>
8745 additional edit
8746 "},
8747 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
8748 counter.clone(),
8749 )
8750 .await;
8751 cx.condition(|editor, _| editor.context_menu_visible())
8752 .await;
8753 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
8754
8755 cx.simulate_keystroke("i");
8756
8757 handle_completion_request(
8758 &mut cx,
8759 indoc! {"
8760 one.second_completion
8761 two si
8762 three <si|>
8763 additional edit
8764 "},
8765 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
8766 counter.clone(),
8767 )
8768 .await;
8769 cx.condition(|editor, _| editor.context_menu_visible())
8770 .await;
8771 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
8772
8773 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8774 editor
8775 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8776 .unwrap()
8777 });
8778 cx.assert_editor_state(indoc! {"
8779 one.second_completion
8780 two sixth_completionˇ
8781 three sixth_completionˇ
8782 additional edit
8783 "});
8784
8785 apply_additional_edits.await.unwrap();
8786
8787 update_test_language_settings(&mut cx, |settings| {
8788 settings.defaults.show_completions_on_input = Some(false);
8789 });
8790 cx.set_state("editorˇ");
8791 cx.simulate_keystroke(".");
8792 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8793 cx.simulate_keystroke("c");
8794 cx.simulate_keystroke("l");
8795 cx.simulate_keystroke("o");
8796 cx.assert_editor_state("editor.cloˇ");
8797 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8798 cx.update_editor(|editor, window, cx| {
8799 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
8800 });
8801 handle_completion_request(
8802 &mut cx,
8803 "editor.<clo|>",
8804 vec!["close", "clobber"],
8805 counter.clone(),
8806 )
8807 .await;
8808 cx.condition(|editor, _| editor.context_menu_visible())
8809 .await;
8810 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
8811
8812 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8813 editor
8814 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8815 .unwrap()
8816 });
8817 cx.assert_editor_state("editor.closeˇ");
8818 handle_resolve_completion_request(&mut cx, None).await;
8819 apply_additional_edits.await.unwrap();
8820}
8821
8822#[gpui::test]
8823async fn test_multiline_completion(cx: &mut gpui::TestAppContext) {
8824 init_test(cx, |_| {});
8825
8826 let fs = FakeFs::new(cx.executor());
8827 fs.insert_tree(
8828 path!("/a"),
8829 json!({
8830 "main.ts": "a",
8831 }),
8832 )
8833 .await;
8834
8835 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8836 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8837 let typescript_language = Arc::new(Language::new(
8838 LanguageConfig {
8839 name: "TypeScript".into(),
8840 matcher: LanguageMatcher {
8841 path_suffixes: vec!["ts".to_string()],
8842 ..LanguageMatcher::default()
8843 },
8844 line_comments: vec!["// ".into()],
8845 ..LanguageConfig::default()
8846 },
8847 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8848 ));
8849 language_registry.add(typescript_language.clone());
8850 let mut fake_servers = language_registry.register_fake_lsp(
8851 "TypeScript",
8852 FakeLspAdapter {
8853 capabilities: lsp::ServerCapabilities {
8854 completion_provider: Some(lsp::CompletionOptions {
8855 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
8856 ..lsp::CompletionOptions::default()
8857 }),
8858 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
8859 ..lsp::ServerCapabilities::default()
8860 },
8861 // Emulate vtsls label generation
8862 label_for_completion: Some(Box::new(|item, _| {
8863 let text = if let Some(description) = item
8864 .label_details
8865 .as_ref()
8866 .and_then(|label_details| label_details.description.as_ref())
8867 {
8868 format!("{} {}", item.label, description)
8869 } else if let Some(detail) = &item.detail {
8870 format!("{} {}", item.label, detail)
8871 } else {
8872 item.label.clone()
8873 };
8874 let len = text.len();
8875 Some(language::CodeLabel {
8876 text,
8877 runs: Vec::new(),
8878 filter_range: 0..len,
8879 })
8880 })),
8881 ..FakeLspAdapter::default()
8882 },
8883 );
8884 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8885 let cx = &mut VisualTestContext::from_window(*workspace, cx);
8886 let worktree_id = workspace
8887 .update(cx, |workspace, _window, cx| {
8888 workspace.project().update(cx, |project, cx| {
8889 project.worktrees(cx).next().unwrap().read(cx).id()
8890 })
8891 })
8892 .unwrap();
8893 let _buffer = project
8894 .update(cx, |project, cx| {
8895 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
8896 })
8897 .await
8898 .unwrap();
8899 let editor = workspace
8900 .update(cx, |workspace, window, cx| {
8901 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
8902 })
8903 .unwrap()
8904 .await
8905 .unwrap()
8906 .downcast::<Editor>()
8907 .unwrap();
8908 let fake_server = fake_servers.next().await.unwrap();
8909
8910 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
8911 let multiline_label_2 = "a\nb\nc\n";
8912 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
8913 let multiline_description = "d\ne\nf\n";
8914 let multiline_detail_2 = "g\nh\ni\n";
8915
8916 let mut completion_handle =
8917 fake_server.handle_request::<lsp::request::Completion, _, _>(move |params, _| async move {
8918 Ok(Some(lsp::CompletionResponse::Array(vec![
8919 lsp::CompletionItem {
8920 label: multiline_label.to_string(),
8921 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8922 range: lsp::Range {
8923 start: lsp::Position {
8924 line: params.text_document_position.position.line,
8925 character: params.text_document_position.position.character,
8926 },
8927 end: lsp::Position {
8928 line: params.text_document_position.position.line,
8929 character: params.text_document_position.position.character,
8930 },
8931 },
8932 new_text: "new_text_1".to_string(),
8933 })),
8934 ..lsp::CompletionItem::default()
8935 },
8936 lsp::CompletionItem {
8937 label: "single line label 1".to_string(),
8938 detail: Some(multiline_detail.to_string()),
8939 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8940 range: lsp::Range {
8941 start: lsp::Position {
8942 line: params.text_document_position.position.line,
8943 character: params.text_document_position.position.character,
8944 },
8945 end: lsp::Position {
8946 line: params.text_document_position.position.line,
8947 character: params.text_document_position.position.character,
8948 },
8949 },
8950 new_text: "new_text_2".to_string(),
8951 })),
8952 ..lsp::CompletionItem::default()
8953 },
8954 lsp::CompletionItem {
8955 label: "single line label 2".to_string(),
8956 label_details: Some(lsp::CompletionItemLabelDetails {
8957 description: Some(multiline_description.to_string()),
8958 detail: None,
8959 }),
8960 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8961 range: lsp::Range {
8962 start: lsp::Position {
8963 line: params.text_document_position.position.line,
8964 character: params.text_document_position.position.character,
8965 },
8966 end: lsp::Position {
8967 line: params.text_document_position.position.line,
8968 character: params.text_document_position.position.character,
8969 },
8970 },
8971 new_text: "new_text_2".to_string(),
8972 })),
8973 ..lsp::CompletionItem::default()
8974 },
8975 lsp::CompletionItem {
8976 label: multiline_label_2.to_string(),
8977 detail: Some(multiline_detail_2.to_string()),
8978 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8979 range: lsp::Range {
8980 start: lsp::Position {
8981 line: params.text_document_position.position.line,
8982 character: params.text_document_position.position.character,
8983 },
8984 end: lsp::Position {
8985 line: params.text_document_position.position.line,
8986 character: params.text_document_position.position.character,
8987 },
8988 },
8989 new_text: "new_text_3".to_string(),
8990 })),
8991 ..lsp::CompletionItem::default()
8992 },
8993 lsp::CompletionItem {
8994 label: "Label with many spaces and \t but without newlines".to_string(),
8995 detail: Some(
8996 "Details with many spaces and \t but without newlines".to_string(),
8997 ),
8998 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8999 range: lsp::Range {
9000 start: lsp::Position {
9001 line: params.text_document_position.position.line,
9002 character: params.text_document_position.position.character,
9003 },
9004 end: lsp::Position {
9005 line: params.text_document_position.position.line,
9006 character: params.text_document_position.position.character,
9007 },
9008 },
9009 new_text: "new_text_4".to_string(),
9010 })),
9011 ..lsp::CompletionItem::default()
9012 },
9013 ])))
9014 });
9015
9016 editor.update_in(cx, |editor, window, cx| {
9017 cx.focus_self(window);
9018 editor.move_to_end(&MoveToEnd, window, cx);
9019 editor.handle_input(".", window, cx);
9020 });
9021 cx.run_until_parked();
9022 completion_handle.next().await.unwrap();
9023
9024 editor.update(cx, |editor, _| {
9025 assert!(editor.context_menu_visible());
9026 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9027 {
9028 let completion_labels = menu
9029 .completions
9030 .borrow()
9031 .iter()
9032 .map(|c| c.label.text.clone())
9033 .collect::<Vec<_>>();
9034 assert_eq!(
9035 completion_labels,
9036 &[
9037 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
9038 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
9039 "single line label 2 d e f ",
9040 "a b c g h i ",
9041 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
9042 ],
9043 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
9044 );
9045
9046 for completion in menu
9047 .completions
9048 .borrow()
9049 .iter() {
9050 assert_eq!(
9051 completion.label.filter_range,
9052 0..completion.label.text.len(),
9053 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
9054 );
9055 }
9056
9057 } else {
9058 panic!("expected completion menu to be open");
9059 }
9060 });
9061}
9062
9063#[gpui::test]
9064async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
9065 init_test(cx, |_| {});
9066 let mut cx = EditorLspTestContext::new_rust(
9067 lsp::ServerCapabilities {
9068 completion_provider: Some(lsp::CompletionOptions {
9069 trigger_characters: Some(vec![".".to_string()]),
9070 ..Default::default()
9071 }),
9072 ..Default::default()
9073 },
9074 cx,
9075 )
9076 .await;
9077 cx.lsp
9078 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9079 Ok(Some(lsp::CompletionResponse::Array(vec![
9080 lsp::CompletionItem {
9081 label: "first".into(),
9082 ..Default::default()
9083 },
9084 lsp::CompletionItem {
9085 label: "last".into(),
9086 ..Default::default()
9087 },
9088 ])))
9089 });
9090 cx.set_state("variableˇ");
9091 cx.simulate_keystroke(".");
9092 cx.executor().run_until_parked();
9093
9094 cx.update_editor(|editor, _, _| {
9095 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9096 {
9097 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
9098 } else {
9099 panic!("expected completion menu to be open");
9100 }
9101 });
9102
9103 cx.update_editor(|editor, window, cx| {
9104 editor.move_page_down(&MovePageDown::default(), window, cx);
9105 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9106 {
9107 assert!(
9108 menu.selected_item == 1,
9109 "expected PageDown to select the last item from the context menu"
9110 );
9111 } else {
9112 panic!("expected completion menu to stay open after PageDown");
9113 }
9114 });
9115
9116 cx.update_editor(|editor, window, cx| {
9117 editor.move_page_up(&MovePageUp::default(), window, cx);
9118 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9119 {
9120 assert!(
9121 menu.selected_item == 0,
9122 "expected PageUp to select the first item from the context menu"
9123 );
9124 } else {
9125 panic!("expected completion menu to stay open after PageUp");
9126 }
9127 });
9128}
9129
9130#[gpui::test]
9131async fn test_completion_sort(cx: &mut gpui::TestAppContext) {
9132 init_test(cx, |_| {});
9133 let mut cx = EditorLspTestContext::new_rust(
9134 lsp::ServerCapabilities {
9135 completion_provider: Some(lsp::CompletionOptions {
9136 trigger_characters: Some(vec![".".to_string()]),
9137 ..Default::default()
9138 }),
9139 ..Default::default()
9140 },
9141 cx,
9142 )
9143 .await;
9144 cx.lsp
9145 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9146 Ok(Some(lsp::CompletionResponse::Array(vec![
9147 lsp::CompletionItem {
9148 label: "Range".into(),
9149 sort_text: Some("a".into()),
9150 ..Default::default()
9151 },
9152 lsp::CompletionItem {
9153 label: "r".into(),
9154 sort_text: Some("b".into()),
9155 ..Default::default()
9156 },
9157 lsp::CompletionItem {
9158 label: "ret".into(),
9159 sort_text: Some("c".into()),
9160 ..Default::default()
9161 },
9162 lsp::CompletionItem {
9163 label: "return".into(),
9164 sort_text: Some("d".into()),
9165 ..Default::default()
9166 },
9167 lsp::CompletionItem {
9168 label: "slice".into(),
9169 sort_text: Some("d".into()),
9170 ..Default::default()
9171 },
9172 ])))
9173 });
9174 cx.set_state("rˇ");
9175 cx.executor().run_until_parked();
9176 cx.update_editor(|editor, window, cx| {
9177 editor.show_completions(
9178 &ShowCompletions {
9179 trigger: Some("r".into()),
9180 },
9181 window,
9182 cx,
9183 );
9184 });
9185 cx.executor().run_until_parked();
9186
9187 cx.update_editor(|editor, _, _| {
9188 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9189 {
9190 assert_eq!(
9191 completion_menu_entries(&menu),
9192 &["r", "ret", "Range", "return"]
9193 );
9194 } else {
9195 panic!("expected completion menu to be open");
9196 }
9197 });
9198}
9199
9200#[gpui::test]
9201async fn test_no_duplicated_completion_requests(cx: &mut gpui::TestAppContext) {
9202 init_test(cx, |_| {});
9203
9204 let mut cx = EditorLspTestContext::new_rust(
9205 lsp::ServerCapabilities {
9206 completion_provider: Some(lsp::CompletionOptions {
9207 trigger_characters: Some(vec![".".to_string()]),
9208 resolve_provider: Some(true),
9209 ..Default::default()
9210 }),
9211 ..Default::default()
9212 },
9213 cx,
9214 )
9215 .await;
9216
9217 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
9218 cx.simulate_keystroke(".");
9219 let completion_item = lsp::CompletionItem {
9220 label: "Some".into(),
9221 kind: Some(lsp::CompletionItemKind::SNIPPET),
9222 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
9223 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
9224 kind: lsp::MarkupKind::Markdown,
9225 value: "```rust\nSome(2)\n```".to_string(),
9226 })),
9227 deprecated: Some(false),
9228 sort_text: Some("Some".to_string()),
9229 filter_text: Some("Some".to_string()),
9230 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
9231 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9232 range: lsp::Range {
9233 start: lsp::Position {
9234 line: 0,
9235 character: 22,
9236 },
9237 end: lsp::Position {
9238 line: 0,
9239 character: 22,
9240 },
9241 },
9242 new_text: "Some(2)".to_string(),
9243 })),
9244 additional_text_edits: Some(vec![lsp::TextEdit {
9245 range: lsp::Range {
9246 start: lsp::Position {
9247 line: 0,
9248 character: 20,
9249 },
9250 end: lsp::Position {
9251 line: 0,
9252 character: 22,
9253 },
9254 },
9255 new_text: "".to_string(),
9256 }]),
9257 ..Default::default()
9258 };
9259
9260 let closure_completion_item = completion_item.clone();
9261 let counter = Arc::new(AtomicUsize::new(0));
9262 let counter_clone = counter.clone();
9263 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
9264 let task_completion_item = closure_completion_item.clone();
9265 counter_clone.fetch_add(1, atomic::Ordering::Release);
9266 async move {
9267 Ok(Some(lsp::CompletionResponse::Array(vec![
9268 task_completion_item,
9269 ])))
9270 }
9271 });
9272
9273 cx.condition(|editor, _| editor.context_menu_visible())
9274 .await;
9275 cx.assert_editor_state(indoc! {"fn main() { let a = 2.ˇ; }"});
9276 assert!(request.next().await.is_some());
9277 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9278
9279 cx.simulate_keystroke("S");
9280 cx.simulate_keystroke("o");
9281 cx.simulate_keystroke("m");
9282 cx.condition(|editor, _| editor.context_menu_visible())
9283 .await;
9284 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Somˇ; }"});
9285 assert!(request.next().await.is_some());
9286 assert!(request.next().await.is_some());
9287 assert!(request.next().await.is_some());
9288 request.close();
9289 assert!(request.next().await.is_none());
9290 assert_eq!(
9291 counter.load(atomic::Ordering::Acquire),
9292 4,
9293 "With the completions menu open, only one LSP request should happen per input"
9294 );
9295}
9296
9297#[gpui::test]
9298async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
9299 init_test(cx, |_| {});
9300 let mut cx = EditorTestContext::new(cx).await;
9301 let language = Arc::new(Language::new(
9302 LanguageConfig {
9303 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
9304 ..Default::default()
9305 },
9306 Some(tree_sitter_rust::LANGUAGE.into()),
9307 ));
9308 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
9309
9310 // If multiple selections intersect a line, the line is only toggled once.
9311 cx.set_state(indoc! {"
9312 fn a() {
9313 «//b();
9314 ˇ»// «c();
9315 //ˇ» d();
9316 }
9317 "});
9318
9319 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9320
9321 cx.assert_editor_state(indoc! {"
9322 fn a() {
9323 «b();
9324 c();
9325 ˇ» d();
9326 }
9327 "});
9328
9329 // The comment prefix is inserted at the same column for every line in a
9330 // selection.
9331 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9332
9333 cx.assert_editor_state(indoc! {"
9334 fn a() {
9335 // «b();
9336 // c();
9337 ˇ»// d();
9338 }
9339 "});
9340
9341 // If a selection ends at the beginning of a line, that line is not toggled.
9342 cx.set_selections_state(indoc! {"
9343 fn a() {
9344 // b();
9345 «// c();
9346 ˇ» // d();
9347 }
9348 "});
9349
9350 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9351
9352 cx.assert_editor_state(indoc! {"
9353 fn a() {
9354 // b();
9355 «c();
9356 ˇ» // d();
9357 }
9358 "});
9359
9360 // If a selection span a single line and is empty, the line is toggled.
9361 cx.set_state(indoc! {"
9362 fn a() {
9363 a();
9364 b();
9365 ˇ
9366 }
9367 "});
9368
9369 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9370
9371 cx.assert_editor_state(indoc! {"
9372 fn a() {
9373 a();
9374 b();
9375 //•ˇ
9376 }
9377 "});
9378
9379 // If a selection span multiple lines, empty lines are not toggled.
9380 cx.set_state(indoc! {"
9381 fn a() {
9382 «a();
9383
9384 c();ˇ»
9385 }
9386 "});
9387
9388 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9389
9390 cx.assert_editor_state(indoc! {"
9391 fn a() {
9392 // «a();
9393
9394 // c();ˇ»
9395 }
9396 "});
9397
9398 // If a selection includes multiple comment prefixes, all lines are uncommented.
9399 cx.set_state(indoc! {"
9400 fn a() {
9401 «// a();
9402 /// b();
9403 //! c();ˇ»
9404 }
9405 "});
9406
9407 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9408
9409 cx.assert_editor_state(indoc! {"
9410 fn a() {
9411 «a();
9412 b();
9413 c();ˇ»
9414 }
9415 "});
9416}
9417
9418#[gpui::test]
9419async fn test_toggle_comment_ignore_indent(cx: &mut gpui::TestAppContext) {
9420 init_test(cx, |_| {});
9421 let mut cx = EditorTestContext::new(cx).await;
9422 let language = Arc::new(Language::new(
9423 LanguageConfig {
9424 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
9425 ..Default::default()
9426 },
9427 Some(tree_sitter_rust::LANGUAGE.into()),
9428 ));
9429 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
9430
9431 let toggle_comments = &ToggleComments {
9432 advance_downwards: false,
9433 ignore_indent: true,
9434 };
9435
9436 // If multiple selections intersect a line, the line is only toggled once.
9437 cx.set_state(indoc! {"
9438 fn a() {
9439 // «b();
9440 // c();
9441 // ˇ» d();
9442 }
9443 "});
9444
9445 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9446
9447 cx.assert_editor_state(indoc! {"
9448 fn a() {
9449 «b();
9450 c();
9451 ˇ» d();
9452 }
9453 "});
9454
9455 // The comment prefix is inserted at the beginning of each line
9456 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9457
9458 cx.assert_editor_state(indoc! {"
9459 fn a() {
9460 // «b();
9461 // c();
9462 // ˇ» d();
9463 }
9464 "});
9465
9466 // If a selection ends at the beginning of a line, that line is not toggled.
9467 cx.set_selections_state(indoc! {"
9468 fn a() {
9469 // b();
9470 // «c();
9471 ˇ»// d();
9472 }
9473 "});
9474
9475 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9476
9477 cx.assert_editor_state(indoc! {"
9478 fn a() {
9479 // b();
9480 «c();
9481 ˇ»// d();
9482 }
9483 "});
9484
9485 // If a selection span a single line and is empty, the line is toggled.
9486 cx.set_state(indoc! {"
9487 fn a() {
9488 a();
9489 b();
9490 ˇ
9491 }
9492 "});
9493
9494 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9495
9496 cx.assert_editor_state(indoc! {"
9497 fn a() {
9498 a();
9499 b();
9500 //ˇ
9501 }
9502 "});
9503
9504 // If a selection span multiple lines, empty lines are not toggled.
9505 cx.set_state(indoc! {"
9506 fn a() {
9507 «a();
9508
9509 c();ˇ»
9510 }
9511 "});
9512
9513 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9514
9515 cx.assert_editor_state(indoc! {"
9516 fn a() {
9517 // «a();
9518
9519 // c();ˇ»
9520 }
9521 "});
9522
9523 // If a selection includes multiple comment prefixes, all lines are uncommented.
9524 cx.set_state(indoc! {"
9525 fn a() {
9526 // «a();
9527 /// b();
9528 //! c();ˇ»
9529 }
9530 "});
9531
9532 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9533
9534 cx.assert_editor_state(indoc! {"
9535 fn a() {
9536 «a();
9537 b();
9538 c();ˇ»
9539 }
9540 "});
9541}
9542
9543#[gpui::test]
9544async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
9545 init_test(cx, |_| {});
9546
9547 let language = Arc::new(Language::new(
9548 LanguageConfig {
9549 line_comments: vec!["// ".into()],
9550 ..Default::default()
9551 },
9552 Some(tree_sitter_rust::LANGUAGE.into()),
9553 ));
9554
9555 let mut cx = EditorTestContext::new(cx).await;
9556
9557 cx.language_registry().add(language.clone());
9558 cx.update_buffer(|buffer, cx| {
9559 buffer.set_language(Some(language), cx);
9560 });
9561
9562 let toggle_comments = &ToggleComments {
9563 advance_downwards: true,
9564 ignore_indent: false,
9565 };
9566
9567 // Single cursor on one line -> advance
9568 // Cursor moves horizontally 3 characters as well on non-blank line
9569 cx.set_state(indoc!(
9570 "fn a() {
9571 ˇdog();
9572 cat();
9573 }"
9574 ));
9575 cx.update_editor(|editor, window, cx| {
9576 editor.toggle_comments(toggle_comments, window, cx);
9577 });
9578 cx.assert_editor_state(indoc!(
9579 "fn a() {
9580 // dog();
9581 catˇ();
9582 }"
9583 ));
9584
9585 // Single selection on one line -> don't advance
9586 cx.set_state(indoc!(
9587 "fn a() {
9588 «dog()ˇ»;
9589 cat();
9590 }"
9591 ));
9592 cx.update_editor(|editor, window, cx| {
9593 editor.toggle_comments(toggle_comments, window, cx);
9594 });
9595 cx.assert_editor_state(indoc!(
9596 "fn a() {
9597 // «dog()ˇ»;
9598 cat();
9599 }"
9600 ));
9601
9602 // Multiple cursors on one line -> advance
9603 cx.set_state(indoc!(
9604 "fn a() {
9605 ˇdˇog();
9606 cat();
9607 }"
9608 ));
9609 cx.update_editor(|editor, window, cx| {
9610 editor.toggle_comments(toggle_comments, window, cx);
9611 });
9612 cx.assert_editor_state(indoc!(
9613 "fn a() {
9614 // dog();
9615 catˇ(ˇ);
9616 }"
9617 ));
9618
9619 // Multiple cursors on one line, with selection -> don't advance
9620 cx.set_state(indoc!(
9621 "fn a() {
9622 ˇdˇog«()ˇ»;
9623 cat();
9624 }"
9625 ));
9626 cx.update_editor(|editor, window, cx| {
9627 editor.toggle_comments(toggle_comments, window, cx);
9628 });
9629 cx.assert_editor_state(indoc!(
9630 "fn a() {
9631 // ˇdˇog«()ˇ»;
9632 cat();
9633 }"
9634 ));
9635
9636 // Single cursor on one line -> advance
9637 // Cursor moves to column 0 on blank line
9638 cx.set_state(indoc!(
9639 "fn a() {
9640 ˇdog();
9641
9642 cat();
9643 }"
9644 ));
9645 cx.update_editor(|editor, window, cx| {
9646 editor.toggle_comments(toggle_comments, window, cx);
9647 });
9648 cx.assert_editor_state(indoc!(
9649 "fn a() {
9650 // dog();
9651 ˇ
9652 cat();
9653 }"
9654 ));
9655
9656 // Single cursor on one line -> advance
9657 // Cursor starts and ends at column 0
9658 cx.set_state(indoc!(
9659 "fn a() {
9660 ˇ dog();
9661 cat();
9662 }"
9663 ));
9664 cx.update_editor(|editor, window, cx| {
9665 editor.toggle_comments(toggle_comments, window, cx);
9666 });
9667 cx.assert_editor_state(indoc!(
9668 "fn a() {
9669 // dog();
9670 ˇ cat();
9671 }"
9672 ));
9673}
9674
9675#[gpui::test]
9676async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
9677 init_test(cx, |_| {});
9678
9679 let mut cx = EditorTestContext::new(cx).await;
9680
9681 let html_language = Arc::new(
9682 Language::new(
9683 LanguageConfig {
9684 name: "HTML".into(),
9685 block_comment: Some(("<!-- ".into(), " -->".into())),
9686 ..Default::default()
9687 },
9688 Some(tree_sitter_html::LANGUAGE.into()),
9689 )
9690 .with_injection_query(
9691 r#"
9692 (script_element
9693 (raw_text) @injection.content
9694 (#set! injection.language "javascript"))
9695 "#,
9696 )
9697 .unwrap(),
9698 );
9699
9700 let javascript_language = Arc::new(Language::new(
9701 LanguageConfig {
9702 name: "JavaScript".into(),
9703 line_comments: vec!["// ".into()],
9704 ..Default::default()
9705 },
9706 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9707 ));
9708
9709 cx.language_registry().add(html_language.clone());
9710 cx.language_registry().add(javascript_language.clone());
9711 cx.update_buffer(|buffer, cx| {
9712 buffer.set_language(Some(html_language), cx);
9713 });
9714
9715 // Toggle comments for empty selections
9716 cx.set_state(
9717 &r#"
9718 <p>A</p>ˇ
9719 <p>B</p>ˇ
9720 <p>C</p>ˇ
9721 "#
9722 .unindent(),
9723 );
9724 cx.update_editor(|editor, window, cx| {
9725 editor.toggle_comments(&ToggleComments::default(), window, cx)
9726 });
9727 cx.assert_editor_state(
9728 &r#"
9729 <!-- <p>A</p>ˇ -->
9730 <!-- <p>B</p>ˇ -->
9731 <!-- <p>C</p>ˇ -->
9732 "#
9733 .unindent(),
9734 );
9735 cx.update_editor(|editor, window, cx| {
9736 editor.toggle_comments(&ToggleComments::default(), window, cx)
9737 });
9738 cx.assert_editor_state(
9739 &r#"
9740 <p>A</p>ˇ
9741 <p>B</p>ˇ
9742 <p>C</p>ˇ
9743 "#
9744 .unindent(),
9745 );
9746
9747 // Toggle comments for mixture of empty and non-empty selections, where
9748 // multiple selections occupy a given line.
9749 cx.set_state(
9750 &r#"
9751 <p>A«</p>
9752 <p>ˇ»B</p>ˇ
9753 <p>C«</p>
9754 <p>ˇ»D</p>ˇ
9755 "#
9756 .unindent(),
9757 );
9758
9759 cx.update_editor(|editor, window, cx| {
9760 editor.toggle_comments(&ToggleComments::default(), window, cx)
9761 });
9762 cx.assert_editor_state(
9763 &r#"
9764 <!-- <p>A«</p>
9765 <p>ˇ»B</p>ˇ -->
9766 <!-- <p>C«</p>
9767 <p>ˇ»D</p>ˇ -->
9768 "#
9769 .unindent(),
9770 );
9771 cx.update_editor(|editor, window, cx| {
9772 editor.toggle_comments(&ToggleComments::default(), window, cx)
9773 });
9774 cx.assert_editor_state(
9775 &r#"
9776 <p>A«</p>
9777 <p>ˇ»B</p>ˇ
9778 <p>C«</p>
9779 <p>ˇ»D</p>ˇ
9780 "#
9781 .unindent(),
9782 );
9783
9784 // Toggle comments when different languages are active for different
9785 // selections.
9786 cx.set_state(
9787 &r#"
9788 ˇ<script>
9789 ˇvar x = new Y();
9790 ˇ</script>
9791 "#
9792 .unindent(),
9793 );
9794 cx.executor().run_until_parked();
9795 cx.update_editor(|editor, window, cx| {
9796 editor.toggle_comments(&ToggleComments::default(), window, cx)
9797 });
9798 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
9799 // Uncommenting and commenting from this position brings in even more wrong artifacts.
9800 cx.assert_editor_state(
9801 &r#"
9802 <!-- ˇ<script> -->
9803 // ˇvar x = new Y();
9804 <!-- ˇ</script> -->
9805 "#
9806 .unindent(),
9807 );
9808}
9809
9810#[gpui::test]
9811fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
9812 init_test(cx, |_| {});
9813
9814 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9815 let multibuffer = cx.new(|cx| {
9816 let mut multibuffer = MultiBuffer::new(ReadWrite);
9817 multibuffer.push_excerpts(
9818 buffer.clone(),
9819 [
9820 ExcerptRange {
9821 context: Point::new(0, 0)..Point::new(0, 4),
9822 primary: None,
9823 },
9824 ExcerptRange {
9825 context: Point::new(1, 0)..Point::new(1, 4),
9826 primary: None,
9827 },
9828 ],
9829 cx,
9830 );
9831 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
9832 multibuffer
9833 });
9834
9835 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
9836 editor.update_in(cx, |editor, window, cx| {
9837 assert_eq!(editor.text(cx), "aaaa\nbbbb");
9838 editor.change_selections(None, window, cx, |s| {
9839 s.select_ranges([
9840 Point::new(0, 0)..Point::new(0, 0),
9841 Point::new(1, 0)..Point::new(1, 0),
9842 ])
9843 });
9844
9845 editor.handle_input("X", window, cx);
9846 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
9847 assert_eq!(
9848 editor.selections.ranges(cx),
9849 [
9850 Point::new(0, 1)..Point::new(0, 1),
9851 Point::new(1, 1)..Point::new(1, 1),
9852 ]
9853 );
9854
9855 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
9856 editor.change_selections(None, window, cx, |s| {
9857 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
9858 });
9859 editor.backspace(&Default::default(), window, cx);
9860 assert_eq!(editor.text(cx), "Xa\nbbb");
9861 assert_eq!(
9862 editor.selections.ranges(cx),
9863 [Point::new(1, 0)..Point::new(1, 0)]
9864 );
9865
9866 editor.change_selections(None, window, cx, |s| {
9867 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
9868 });
9869 editor.backspace(&Default::default(), window, cx);
9870 assert_eq!(editor.text(cx), "X\nbb");
9871 assert_eq!(
9872 editor.selections.ranges(cx),
9873 [Point::new(0, 1)..Point::new(0, 1)]
9874 );
9875 });
9876}
9877
9878#[gpui::test]
9879fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
9880 init_test(cx, |_| {});
9881
9882 let markers = vec![('[', ']').into(), ('(', ')').into()];
9883 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
9884 indoc! {"
9885 [aaaa
9886 (bbbb]
9887 cccc)",
9888 },
9889 markers.clone(),
9890 );
9891 let excerpt_ranges = markers.into_iter().map(|marker| {
9892 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
9893 ExcerptRange {
9894 context,
9895 primary: None,
9896 }
9897 });
9898 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
9899 let multibuffer = cx.new(|cx| {
9900 let mut multibuffer = MultiBuffer::new(ReadWrite);
9901 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
9902 multibuffer
9903 });
9904
9905 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
9906 editor.update_in(cx, |editor, window, cx| {
9907 let (expected_text, selection_ranges) = marked_text_ranges(
9908 indoc! {"
9909 aaaa
9910 bˇbbb
9911 bˇbbˇb
9912 cccc"
9913 },
9914 true,
9915 );
9916 assert_eq!(editor.text(cx), expected_text);
9917 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
9918
9919 editor.handle_input("X", window, cx);
9920
9921 let (expected_text, expected_selections) = marked_text_ranges(
9922 indoc! {"
9923 aaaa
9924 bXˇbbXb
9925 bXˇbbXˇb
9926 cccc"
9927 },
9928 false,
9929 );
9930 assert_eq!(editor.text(cx), expected_text);
9931 assert_eq!(editor.selections.ranges(cx), expected_selections);
9932
9933 editor.newline(&Newline, window, cx);
9934 let (expected_text, expected_selections) = marked_text_ranges(
9935 indoc! {"
9936 aaaa
9937 bX
9938 ˇbbX
9939 b
9940 bX
9941 ˇbbX
9942 ˇb
9943 cccc"
9944 },
9945 false,
9946 );
9947 assert_eq!(editor.text(cx), expected_text);
9948 assert_eq!(editor.selections.ranges(cx), expected_selections);
9949 });
9950}
9951
9952#[gpui::test]
9953fn test_refresh_selections(cx: &mut TestAppContext) {
9954 init_test(cx, |_| {});
9955
9956 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9957 let mut excerpt1_id = None;
9958 let multibuffer = cx.new(|cx| {
9959 let mut multibuffer = MultiBuffer::new(ReadWrite);
9960 excerpt1_id = multibuffer
9961 .push_excerpts(
9962 buffer.clone(),
9963 [
9964 ExcerptRange {
9965 context: Point::new(0, 0)..Point::new(1, 4),
9966 primary: None,
9967 },
9968 ExcerptRange {
9969 context: Point::new(1, 0)..Point::new(2, 4),
9970 primary: None,
9971 },
9972 ],
9973 cx,
9974 )
9975 .into_iter()
9976 .next();
9977 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
9978 multibuffer
9979 });
9980
9981 let editor = cx.add_window(|window, cx| {
9982 let mut editor = build_editor(multibuffer.clone(), window, cx);
9983 let snapshot = editor.snapshot(window, cx);
9984 editor.change_selections(None, window, cx, |s| {
9985 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
9986 });
9987 editor.begin_selection(
9988 Point::new(2, 1).to_display_point(&snapshot),
9989 true,
9990 1,
9991 window,
9992 cx,
9993 );
9994 assert_eq!(
9995 editor.selections.ranges(cx),
9996 [
9997 Point::new(1, 3)..Point::new(1, 3),
9998 Point::new(2, 1)..Point::new(2, 1),
9999 ]
10000 );
10001 editor
10002 });
10003
10004 // Refreshing selections is a no-op when excerpts haven't changed.
10005 _ = editor.update(cx, |editor, window, cx| {
10006 editor.change_selections(None, window, cx, |s| s.refresh());
10007 assert_eq!(
10008 editor.selections.ranges(cx),
10009 [
10010 Point::new(1, 3)..Point::new(1, 3),
10011 Point::new(2, 1)..Point::new(2, 1),
10012 ]
10013 );
10014 });
10015
10016 multibuffer.update(cx, |multibuffer, cx| {
10017 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
10018 });
10019 _ = editor.update(cx, |editor, window, cx| {
10020 // Removing an excerpt causes the first selection to become degenerate.
10021 assert_eq!(
10022 editor.selections.ranges(cx),
10023 [
10024 Point::new(0, 0)..Point::new(0, 0),
10025 Point::new(0, 1)..Point::new(0, 1)
10026 ]
10027 );
10028
10029 // Refreshing selections will relocate the first selection to the original buffer
10030 // location.
10031 editor.change_selections(None, window, cx, |s| s.refresh());
10032 assert_eq!(
10033 editor.selections.ranges(cx),
10034 [
10035 Point::new(0, 1)..Point::new(0, 1),
10036 Point::new(0, 3)..Point::new(0, 3)
10037 ]
10038 );
10039 assert!(editor.selections.pending_anchor().is_some());
10040 });
10041}
10042
10043#[gpui::test]
10044fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
10045 init_test(cx, |_| {});
10046
10047 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10048 let mut excerpt1_id = None;
10049 let multibuffer = cx.new(|cx| {
10050 let mut multibuffer = MultiBuffer::new(ReadWrite);
10051 excerpt1_id = multibuffer
10052 .push_excerpts(
10053 buffer.clone(),
10054 [
10055 ExcerptRange {
10056 context: Point::new(0, 0)..Point::new(1, 4),
10057 primary: None,
10058 },
10059 ExcerptRange {
10060 context: Point::new(1, 0)..Point::new(2, 4),
10061 primary: None,
10062 },
10063 ],
10064 cx,
10065 )
10066 .into_iter()
10067 .next();
10068 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
10069 multibuffer
10070 });
10071
10072 let editor = cx.add_window(|window, cx| {
10073 let mut editor = build_editor(multibuffer.clone(), window, cx);
10074 let snapshot = editor.snapshot(window, cx);
10075 editor.begin_selection(
10076 Point::new(1, 3).to_display_point(&snapshot),
10077 false,
10078 1,
10079 window,
10080 cx,
10081 );
10082 assert_eq!(
10083 editor.selections.ranges(cx),
10084 [Point::new(1, 3)..Point::new(1, 3)]
10085 );
10086 editor
10087 });
10088
10089 multibuffer.update(cx, |multibuffer, cx| {
10090 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
10091 });
10092 _ = editor.update(cx, |editor, window, cx| {
10093 assert_eq!(
10094 editor.selections.ranges(cx),
10095 [Point::new(0, 0)..Point::new(0, 0)]
10096 );
10097
10098 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
10099 editor.change_selections(None, window, cx, |s| s.refresh());
10100 assert_eq!(
10101 editor.selections.ranges(cx),
10102 [Point::new(0, 3)..Point::new(0, 3)]
10103 );
10104 assert!(editor.selections.pending_anchor().is_some());
10105 });
10106}
10107
10108#[gpui::test]
10109async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
10110 init_test(cx, |_| {});
10111
10112 let language = Arc::new(
10113 Language::new(
10114 LanguageConfig {
10115 brackets: BracketPairConfig {
10116 pairs: vec![
10117 BracketPair {
10118 start: "{".to_string(),
10119 end: "}".to_string(),
10120 close: true,
10121 surround: true,
10122 newline: true,
10123 },
10124 BracketPair {
10125 start: "/* ".to_string(),
10126 end: " */".to_string(),
10127 close: true,
10128 surround: true,
10129 newline: true,
10130 },
10131 ],
10132 ..Default::default()
10133 },
10134 ..Default::default()
10135 },
10136 Some(tree_sitter_rust::LANGUAGE.into()),
10137 )
10138 .with_indents_query("")
10139 .unwrap(),
10140 );
10141
10142 let text = concat!(
10143 "{ }\n", //
10144 " x\n", //
10145 " /* */\n", //
10146 "x\n", //
10147 "{{} }\n", //
10148 );
10149
10150 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10151 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10152 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10153 editor
10154 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10155 .await;
10156
10157 editor.update_in(cx, |editor, window, cx| {
10158 editor.change_selections(None, window, cx, |s| {
10159 s.select_display_ranges([
10160 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
10161 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
10162 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
10163 ])
10164 });
10165 editor.newline(&Newline, window, cx);
10166
10167 assert_eq!(
10168 editor.buffer().read(cx).read(cx).text(),
10169 concat!(
10170 "{ \n", // Suppress rustfmt
10171 "\n", //
10172 "}\n", //
10173 " x\n", //
10174 " /* \n", //
10175 " \n", //
10176 " */\n", //
10177 "x\n", //
10178 "{{} \n", //
10179 "}\n", //
10180 )
10181 );
10182 });
10183}
10184
10185#[gpui::test]
10186fn test_highlighted_ranges(cx: &mut TestAppContext) {
10187 init_test(cx, |_| {});
10188
10189 let editor = cx.add_window(|window, cx| {
10190 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
10191 build_editor(buffer.clone(), window, cx)
10192 });
10193
10194 _ = editor.update(cx, |editor, window, cx| {
10195 struct Type1;
10196 struct Type2;
10197
10198 let buffer = editor.buffer.read(cx).snapshot(cx);
10199
10200 let anchor_range =
10201 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
10202
10203 editor.highlight_background::<Type1>(
10204 &[
10205 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
10206 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
10207 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
10208 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
10209 ],
10210 |_| Hsla::red(),
10211 cx,
10212 );
10213 editor.highlight_background::<Type2>(
10214 &[
10215 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
10216 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
10217 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
10218 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
10219 ],
10220 |_| Hsla::green(),
10221 cx,
10222 );
10223
10224 let snapshot = editor.snapshot(window, cx);
10225 let mut highlighted_ranges = editor.background_highlights_in_range(
10226 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
10227 &snapshot,
10228 cx.theme().colors(),
10229 );
10230 // Enforce a consistent ordering based on color without relying on the ordering of the
10231 // highlight's `TypeId` which is non-executor.
10232 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
10233 assert_eq!(
10234 highlighted_ranges,
10235 &[
10236 (
10237 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
10238 Hsla::red(),
10239 ),
10240 (
10241 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
10242 Hsla::red(),
10243 ),
10244 (
10245 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
10246 Hsla::green(),
10247 ),
10248 (
10249 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
10250 Hsla::green(),
10251 ),
10252 ]
10253 );
10254 assert_eq!(
10255 editor.background_highlights_in_range(
10256 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
10257 &snapshot,
10258 cx.theme().colors(),
10259 ),
10260 &[(
10261 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
10262 Hsla::red(),
10263 )]
10264 );
10265 });
10266}
10267
10268#[gpui::test]
10269async fn test_following(cx: &mut gpui::TestAppContext) {
10270 init_test(cx, |_| {});
10271
10272 let fs = FakeFs::new(cx.executor());
10273 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
10274
10275 let buffer = project.update(cx, |project, cx| {
10276 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
10277 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
10278 });
10279 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
10280 let follower = cx.update(|cx| {
10281 cx.open_window(
10282 WindowOptions {
10283 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
10284 gpui::Point::new(px(0.), px(0.)),
10285 gpui::Point::new(px(10.), px(80.)),
10286 ))),
10287 ..Default::default()
10288 },
10289 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
10290 )
10291 .unwrap()
10292 });
10293
10294 let is_still_following = Rc::new(RefCell::new(true));
10295 let follower_edit_event_count = Rc::new(RefCell::new(0));
10296 let pending_update = Rc::new(RefCell::new(None));
10297 let leader_entity = leader.root(cx).unwrap();
10298 let follower_entity = follower.root(cx).unwrap();
10299 _ = follower.update(cx, {
10300 let update = pending_update.clone();
10301 let is_still_following = is_still_following.clone();
10302 let follower_edit_event_count = follower_edit_event_count.clone();
10303 |_, window, cx| {
10304 cx.subscribe_in(
10305 &leader_entity,
10306 window,
10307 move |_, leader, event, window, cx| {
10308 leader.read(cx).add_event_to_update_proto(
10309 event,
10310 &mut update.borrow_mut(),
10311 window,
10312 cx,
10313 );
10314 },
10315 )
10316 .detach();
10317
10318 cx.subscribe_in(
10319 &follower_entity,
10320 window,
10321 move |_, _, event: &EditorEvent, _window, _cx| {
10322 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
10323 *is_still_following.borrow_mut() = false;
10324 }
10325
10326 if let EditorEvent::BufferEdited = event {
10327 *follower_edit_event_count.borrow_mut() += 1;
10328 }
10329 },
10330 )
10331 .detach();
10332 }
10333 });
10334
10335 // Update the selections only
10336 _ = leader.update(cx, |leader, window, cx| {
10337 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
10338 });
10339 follower
10340 .update(cx, |follower, window, cx| {
10341 follower.apply_update_proto(
10342 &project,
10343 pending_update.borrow_mut().take().unwrap(),
10344 window,
10345 cx,
10346 )
10347 })
10348 .unwrap()
10349 .await
10350 .unwrap();
10351 _ = follower.update(cx, |follower, _, cx| {
10352 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
10353 });
10354 assert!(*is_still_following.borrow());
10355 assert_eq!(*follower_edit_event_count.borrow(), 0);
10356
10357 // Update the scroll position only
10358 _ = leader.update(cx, |leader, window, cx| {
10359 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
10360 });
10361 follower
10362 .update(cx, |follower, window, cx| {
10363 follower.apply_update_proto(
10364 &project,
10365 pending_update.borrow_mut().take().unwrap(),
10366 window,
10367 cx,
10368 )
10369 })
10370 .unwrap()
10371 .await
10372 .unwrap();
10373 assert_eq!(
10374 follower
10375 .update(cx, |follower, _, cx| follower.scroll_position(cx))
10376 .unwrap(),
10377 gpui::Point::new(1.5, 3.5)
10378 );
10379 assert!(*is_still_following.borrow());
10380 assert_eq!(*follower_edit_event_count.borrow(), 0);
10381
10382 // Update the selections and scroll position. The follower's scroll position is updated
10383 // via autoscroll, not via the leader's exact scroll position.
10384 _ = leader.update(cx, |leader, window, cx| {
10385 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10386 leader.request_autoscroll(Autoscroll::newest(), cx);
10387 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
10388 });
10389 follower
10390 .update(cx, |follower, window, cx| {
10391 follower.apply_update_proto(
10392 &project,
10393 pending_update.borrow_mut().take().unwrap(),
10394 window,
10395 cx,
10396 )
10397 })
10398 .unwrap()
10399 .await
10400 .unwrap();
10401 _ = follower.update(cx, |follower, _, cx| {
10402 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
10403 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
10404 });
10405 assert!(*is_still_following.borrow());
10406
10407 // Creating a pending selection that precedes another selection
10408 _ = leader.update(cx, |leader, window, cx| {
10409 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
10410 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
10411 });
10412 follower
10413 .update(cx, |follower, window, cx| {
10414 follower.apply_update_proto(
10415 &project,
10416 pending_update.borrow_mut().take().unwrap(),
10417 window,
10418 cx,
10419 )
10420 })
10421 .unwrap()
10422 .await
10423 .unwrap();
10424 _ = follower.update(cx, |follower, _, cx| {
10425 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
10426 });
10427 assert!(*is_still_following.borrow());
10428
10429 // Extend the pending selection so that it surrounds another selection
10430 _ = leader.update(cx, |leader, window, cx| {
10431 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
10432 });
10433 follower
10434 .update(cx, |follower, window, cx| {
10435 follower.apply_update_proto(
10436 &project,
10437 pending_update.borrow_mut().take().unwrap(),
10438 window,
10439 cx,
10440 )
10441 })
10442 .unwrap()
10443 .await
10444 .unwrap();
10445 _ = follower.update(cx, |follower, _, cx| {
10446 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
10447 });
10448
10449 // Scrolling locally breaks the follow
10450 _ = follower.update(cx, |follower, window, cx| {
10451 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
10452 follower.set_scroll_anchor(
10453 ScrollAnchor {
10454 anchor: top_anchor,
10455 offset: gpui::Point::new(0.0, 0.5),
10456 },
10457 window,
10458 cx,
10459 );
10460 });
10461 assert!(!(*is_still_following.borrow()));
10462}
10463
10464#[gpui::test]
10465async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
10466 init_test(cx, |_| {});
10467
10468 let fs = FakeFs::new(cx.executor());
10469 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
10470 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10471 let pane = workspace
10472 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10473 .unwrap();
10474
10475 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10476
10477 let leader = pane.update_in(cx, |_, window, cx| {
10478 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
10479 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
10480 });
10481
10482 // Start following the editor when it has no excerpts.
10483 let mut state_message =
10484 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
10485 let workspace_entity = workspace.root(cx).unwrap();
10486 let follower_1 = cx
10487 .update_window(*workspace.deref(), |_, window, cx| {
10488 Editor::from_state_proto(
10489 workspace_entity,
10490 ViewId {
10491 creator: Default::default(),
10492 id: 0,
10493 },
10494 &mut state_message,
10495 window,
10496 cx,
10497 )
10498 })
10499 .unwrap()
10500 .unwrap()
10501 .await
10502 .unwrap();
10503
10504 let update_message = Rc::new(RefCell::new(None));
10505 follower_1.update_in(cx, {
10506 let update = update_message.clone();
10507 |_, window, cx| {
10508 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
10509 leader.read(cx).add_event_to_update_proto(
10510 event,
10511 &mut update.borrow_mut(),
10512 window,
10513 cx,
10514 );
10515 })
10516 .detach();
10517 }
10518 });
10519
10520 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
10521 (
10522 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
10523 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
10524 )
10525 });
10526
10527 // Insert some excerpts.
10528 leader.update(cx, |leader, cx| {
10529 leader.buffer.update(cx, |multibuffer, cx| {
10530 let excerpt_ids = multibuffer.push_excerpts(
10531 buffer_1.clone(),
10532 [
10533 ExcerptRange {
10534 context: 1..6,
10535 primary: None,
10536 },
10537 ExcerptRange {
10538 context: 12..15,
10539 primary: None,
10540 },
10541 ExcerptRange {
10542 context: 0..3,
10543 primary: None,
10544 },
10545 ],
10546 cx,
10547 );
10548 multibuffer.insert_excerpts_after(
10549 excerpt_ids[0],
10550 buffer_2.clone(),
10551 [
10552 ExcerptRange {
10553 context: 8..12,
10554 primary: None,
10555 },
10556 ExcerptRange {
10557 context: 0..6,
10558 primary: None,
10559 },
10560 ],
10561 cx,
10562 );
10563 });
10564 });
10565
10566 // Apply the update of adding the excerpts.
10567 follower_1
10568 .update_in(cx, |follower, window, cx| {
10569 follower.apply_update_proto(
10570 &project,
10571 update_message.borrow().clone().unwrap(),
10572 window,
10573 cx,
10574 )
10575 })
10576 .await
10577 .unwrap();
10578 assert_eq!(
10579 follower_1.update(cx, |editor, cx| editor.text(cx)),
10580 leader.update(cx, |editor, cx| editor.text(cx))
10581 );
10582 update_message.borrow_mut().take();
10583
10584 // Start following separately after it already has excerpts.
10585 let mut state_message =
10586 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
10587 let workspace_entity = workspace.root(cx).unwrap();
10588 let follower_2 = cx
10589 .update_window(*workspace.deref(), |_, window, cx| {
10590 Editor::from_state_proto(
10591 workspace_entity,
10592 ViewId {
10593 creator: Default::default(),
10594 id: 0,
10595 },
10596 &mut state_message,
10597 window,
10598 cx,
10599 )
10600 })
10601 .unwrap()
10602 .unwrap()
10603 .await
10604 .unwrap();
10605 assert_eq!(
10606 follower_2.update(cx, |editor, cx| editor.text(cx)),
10607 leader.update(cx, |editor, cx| editor.text(cx))
10608 );
10609
10610 // Remove some excerpts.
10611 leader.update(cx, |leader, cx| {
10612 leader.buffer.update(cx, |multibuffer, cx| {
10613 let excerpt_ids = multibuffer.excerpt_ids();
10614 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
10615 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
10616 });
10617 });
10618
10619 // Apply the update of removing the excerpts.
10620 follower_1
10621 .update_in(cx, |follower, window, cx| {
10622 follower.apply_update_proto(
10623 &project,
10624 update_message.borrow().clone().unwrap(),
10625 window,
10626 cx,
10627 )
10628 })
10629 .await
10630 .unwrap();
10631 follower_2
10632 .update_in(cx, |follower, window, cx| {
10633 follower.apply_update_proto(
10634 &project,
10635 update_message.borrow().clone().unwrap(),
10636 window,
10637 cx,
10638 )
10639 })
10640 .await
10641 .unwrap();
10642 update_message.borrow_mut().take();
10643 assert_eq!(
10644 follower_1.update(cx, |editor, cx| editor.text(cx)),
10645 leader.update(cx, |editor, cx| editor.text(cx))
10646 );
10647}
10648
10649#[gpui::test]
10650async fn go_to_prev_overlapping_diagnostic(
10651 executor: BackgroundExecutor,
10652 cx: &mut gpui::TestAppContext,
10653) {
10654 init_test(cx, |_| {});
10655
10656 let mut cx = EditorTestContext::new(cx).await;
10657 let lsp_store =
10658 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10659
10660 cx.set_state(indoc! {"
10661 ˇfn func(abc def: i32) -> u32 {
10662 }
10663 "});
10664
10665 cx.update(|_, cx| {
10666 lsp_store.update(cx, |lsp_store, cx| {
10667 lsp_store
10668 .update_diagnostics(
10669 LanguageServerId(0),
10670 lsp::PublishDiagnosticsParams {
10671 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
10672 version: None,
10673 diagnostics: vec![
10674 lsp::Diagnostic {
10675 range: lsp::Range::new(
10676 lsp::Position::new(0, 11),
10677 lsp::Position::new(0, 12),
10678 ),
10679 severity: Some(lsp::DiagnosticSeverity::ERROR),
10680 ..Default::default()
10681 },
10682 lsp::Diagnostic {
10683 range: lsp::Range::new(
10684 lsp::Position::new(0, 12),
10685 lsp::Position::new(0, 15),
10686 ),
10687 severity: Some(lsp::DiagnosticSeverity::ERROR),
10688 ..Default::default()
10689 },
10690 lsp::Diagnostic {
10691 range: lsp::Range::new(
10692 lsp::Position::new(0, 25),
10693 lsp::Position::new(0, 28),
10694 ),
10695 severity: Some(lsp::DiagnosticSeverity::ERROR),
10696 ..Default::default()
10697 },
10698 ],
10699 },
10700 &[],
10701 cx,
10702 )
10703 .unwrap()
10704 });
10705 });
10706
10707 executor.run_until_parked();
10708
10709 cx.update_editor(|editor, window, cx| {
10710 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10711 });
10712
10713 cx.assert_editor_state(indoc! {"
10714 fn func(abc def: i32) -> ˇu32 {
10715 }
10716 "});
10717
10718 cx.update_editor(|editor, window, cx| {
10719 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10720 });
10721
10722 cx.assert_editor_state(indoc! {"
10723 fn func(abc ˇdef: i32) -> u32 {
10724 }
10725 "});
10726
10727 cx.update_editor(|editor, window, cx| {
10728 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10729 });
10730
10731 cx.assert_editor_state(indoc! {"
10732 fn func(abcˇ def: i32) -> u32 {
10733 }
10734 "});
10735
10736 cx.update_editor(|editor, window, cx| {
10737 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10738 });
10739
10740 cx.assert_editor_state(indoc! {"
10741 fn func(abc def: i32) -> ˇu32 {
10742 }
10743 "});
10744}
10745
10746#[gpui::test]
10747async fn cycle_through_same_place_diagnostics(
10748 executor: BackgroundExecutor,
10749 cx: &mut gpui::TestAppContext,
10750) {
10751 init_test(cx, |_| {});
10752
10753 let mut cx = EditorTestContext::new(cx).await;
10754 let lsp_store =
10755 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10756
10757 cx.set_state(indoc! {"
10758 ˇfn func(abc def: i32) -> u32 {
10759 }
10760 "});
10761
10762 cx.update(|_, cx| {
10763 lsp_store.update(cx, |lsp_store, cx| {
10764 lsp_store
10765 .update_diagnostics(
10766 LanguageServerId(0),
10767 lsp::PublishDiagnosticsParams {
10768 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
10769 version: None,
10770 diagnostics: vec![
10771 lsp::Diagnostic {
10772 range: lsp::Range::new(
10773 lsp::Position::new(0, 11),
10774 lsp::Position::new(0, 12),
10775 ),
10776 severity: Some(lsp::DiagnosticSeverity::ERROR),
10777 ..Default::default()
10778 },
10779 lsp::Diagnostic {
10780 range: lsp::Range::new(
10781 lsp::Position::new(0, 12),
10782 lsp::Position::new(0, 15),
10783 ),
10784 severity: Some(lsp::DiagnosticSeverity::ERROR),
10785 ..Default::default()
10786 },
10787 lsp::Diagnostic {
10788 range: lsp::Range::new(
10789 lsp::Position::new(0, 12),
10790 lsp::Position::new(0, 15),
10791 ),
10792 severity: Some(lsp::DiagnosticSeverity::ERROR),
10793 ..Default::default()
10794 },
10795 lsp::Diagnostic {
10796 range: lsp::Range::new(
10797 lsp::Position::new(0, 25),
10798 lsp::Position::new(0, 28),
10799 ),
10800 severity: Some(lsp::DiagnosticSeverity::ERROR),
10801 ..Default::default()
10802 },
10803 ],
10804 },
10805 &[],
10806 cx,
10807 )
10808 .unwrap()
10809 });
10810 });
10811 executor.run_until_parked();
10812
10813 //// Backward
10814
10815 // Fourth diagnostic
10816 cx.update_editor(|editor, window, cx| {
10817 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10818 });
10819 cx.assert_editor_state(indoc! {"
10820 fn func(abc def: i32) -> ˇu32 {
10821 }
10822 "});
10823
10824 // Third diagnostic
10825 cx.update_editor(|editor, window, cx| {
10826 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10827 });
10828 cx.assert_editor_state(indoc! {"
10829 fn func(abc ˇdef: i32) -> u32 {
10830 }
10831 "});
10832
10833 // Second diagnostic, same place
10834 cx.update_editor(|editor, window, cx| {
10835 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10836 });
10837 cx.assert_editor_state(indoc! {"
10838 fn func(abc ˇdef: i32) -> u32 {
10839 }
10840 "});
10841
10842 // First diagnostic
10843 cx.update_editor(|editor, window, cx| {
10844 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10845 });
10846 cx.assert_editor_state(indoc! {"
10847 fn func(abcˇ def: i32) -> u32 {
10848 }
10849 "});
10850
10851 // Wrapped over, fourth diagnostic
10852 cx.update_editor(|editor, window, cx| {
10853 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10854 });
10855 cx.assert_editor_state(indoc! {"
10856 fn func(abc def: i32) -> ˇu32 {
10857 }
10858 "});
10859
10860 cx.update_editor(|editor, window, cx| {
10861 editor.move_to_beginning(&MoveToBeginning, window, cx);
10862 });
10863 cx.assert_editor_state(indoc! {"
10864 ˇfn func(abc def: i32) -> u32 {
10865 }
10866 "});
10867
10868 //// Forward
10869
10870 // First diagnostic
10871 cx.update_editor(|editor, window, cx| {
10872 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10873 });
10874 cx.assert_editor_state(indoc! {"
10875 fn func(abcˇ def: i32) -> u32 {
10876 }
10877 "});
10878
10879 // Second diagnostic
10880 cx.update_editor(|editor, window, cx| {
10881 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10882 });
10883 cx.assert_editor_state(indoc! {"
10884 fn func(abc ˇdef: i32) -> u32 {
10885 }
10886 "});
10887
10888 // Third diagnostic, same place
10889 cx.update_editor(|editor, window, cx| {
10890 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10891 });
10892 cx.assert_editor_state(indoc! {"
10893 fn func(abc ˇdef: i32) -> u32 {
10894 }
10895 "});
10896
10897 // Fourth diagnostic
10898 cx.update_editor(|editor, window, cx| {
10899 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10900 });
10901 cx.assert_editor_state(indoc! {"
10902 fn func(abc def: i32) -> ˇu32 {
10903 }
10904 "});
10905
10906 // Wrapped around, first diagnostic
10907 cx.update_editor(|editor, window, cx| {
10908 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
10909 });
10910 cx.assert_editor_state(indoc! {"
10911 fn func(abcˇ def: i32) -> u32 {
10912 }
10913 "});
10914}
10915
10916#[gpui::test]
10917async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
10918 init_test(cx, |_| {});
10919
10920 let mut cx = EditorTestContext::new(cx).await;
10921
10922 cx.set_state(indoc! {"
10923 fn func(abˇc def: i32) -> u32 {
10924 }
10925 "});
10926 let lsp_store =
10927 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10928
10929 cx.update(|_, cx| {
10930 lsp_store.update(cx, |lsp_store, cx| {
10931 lsp_store.update_diagnostics(
10932 LanguageServerId(0),
10933 lsp::PublishDiagnosticsParams {
10934 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
10935 version: None,
10936 diagnostics: vec![lsp::Diagnostic {
10937 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
10938 severity: Some(lsp::DiagnosticSeverity::ERROR),
10939 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
10940 ..Default::default()
10941 }],
10942 },
10943 &[],
10944 cx,
10945 )
10946 })
10947 }).unwrap();
10948 cx.run_until_parked();
10949 cx.update_editor(|editor, window, cx| {
10950 hover_popover::hover(editor, &Default::default(), window, cx)
10951 });
10952 cx.run_until_parked();
10953 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
10954}
10955
10956#[gpui::test]
10957async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
10958 init_test(cx, |_| {});
10959
10960 let mut cx = EditorTestContext::new(cx).await;
10961
10962 let diff_base = r#"
10963 use some::mod;
10964
10965 const A: u32 = 42;
10966
10967 fn main() {
10968 println!("hello");
10969
10970 println!("world");
10971 }
10972 "#
10973 .unindent();
10974
10975 // Edits are modified, removed, modified, added
10976 cx.set_state(
10977 &r#"
10978 use some::modified;
10979
10980 ˇ
10981 fn main() {
10982 println!("hello there");
10983
10984 println!("around the");
10985 println!("world");
10986 }
10987 "#
10988 .unindent(),
10989 );
10990
10991 cx.set_diff_base(&diff_base);
10992 executor.run_until_parked();
10993
10994 cx.update_editor(|editor, window, cx| {
10995 //Wrap around the bottom of the buffer
10996 for _ in 0..3 {
10997 editor.go_to_next_hunk(&GoToHunk, window, cx);
10998 }
10999 });
11000
11001 cx.assert_editor_state(
11002 &r#"
11003 ˇuse some::modified;
11004
11005
11006 fn main() {
11007 println!("hello there");
11008
11009 println!("around the");
11010 println!("world");
11011 }
11012 "#
11013 .unindent(),
11014 );
11015
11016 cx.update_editor(|editor, window, cx| {
11017 //Wrap around the top of the buffer
11018 for _ in 0..2 {
11019 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
11020 }
11021 });
11022
11023 cx.assert_editor_state(
11024 &r#"
11025 use some::modified;
11026
11027
11028 fn main() {
11029 ˇ println!("hello there");
11030
11031 println!("around the");
11032 println!("world");
11033 }
11034 "#
11035 .unindent(),
11036 );
11037
11038 cx.update_editor(|editor, window, cx| {
11039 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
11040 });
11041
11042 cx.assert_editor_state(
11043 &r#"
11044 use some::modified;
11045
11046 ˇ
11047 fn main() {
11048 println!("hello there");
11049
11050 println!("around the");
11051 println!("world");
11052 }
11053 "#
11054 .unindent(),
11055 );
11056
11057 cx.update_editor(|editor, window, cx| {
11058 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
11059 });
11060
11061 cx.assert_editor_state(
11062 &r#"
11063 ˇuse some::modified;
11064
11065
11066 fn main() {
11067 println!("hello there");
11068
11069 println!("around the");
11070 println!("world");
11071 }
11072 "#
11073 .unindent(),
11074 );
11075
11076 cx.update_editor(|editor, window, cx| {
11077 for _ in 0..2 {
11078 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
11079 }
11080 });
11081
11082 cx.assert_editor_state(
11083 &r#"
11084 use some::modified;
11085
11086
11087 fn main() {
11088 ˇ println!("hello there");
11089
11090 println!("around the");
11091 println!("world");
11092 }
11093 "#
11094 .unindent(),
11095 );
11096
11097 cx.update_editor(|editor, window, cx| {
11098 editor.fold(&Fold, window, cx);
11099 });
11100
11101 cx.update_editor(|editor, window, cx| {
11102 editor.go_to_next_hunk(&GoToHunk, window, cx);
11103 });
11104
11105 cx.assert_editor_state(
11106 &r#"
11107 ˇuse some::modified;
11108
11109
11110 fn main() {
11111 println!("hello there");
11112
11113 println!("around the");
11114 println!("world");
11115 }
11116 "#
11117 .unindent(),
11118 );
11119}
11120
11121#[test]
11122fn test_split_words() {
11123 fn split(text: &str) -> Vec<&str> {
11124 split_words(text).collect()
11125 }
11126
11127 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
11128 assert_eq!(split("hello_world"), &["hello_", "world"]);
11129 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
11130 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
11131 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
11132 assert_eq!(split("helloworld"), &["helloworld"]);
11133
11134 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
11135}
11136
11137#[gpui::test]
11138async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
11139 init_test(cx, |_| {});
11140
11141 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
11142 let mut assert = |before, after| {
11143 let _state_context = cx.set_state(before);
11144 cx.run_until_parked();
11145 cx.update_editor(|editor, window, cx| {
11146 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
11147 });
11148 cx.assert_editor_state(after);
11149 };
11150
11151 // Outside bracket jumps to outside of matching bracket
11152 assert("console.logˇ(var);", "console.log(var)ˇ;");
11153 assert("console.log(var)ˇ;", "console.logˇ(var);");
11154
11155 // Inside bracket jumps to inside of matching bracket
11156 assert("console.log(ˇvar);", "console.log(varˇ);");
11157 assert("console.log(varˇ);", "console.log(ˇvar);");
11158
11159 // When outside a bracket and inside, favor jumping to the inside bracket
11160 assert(
11161 "console.log('foo', [1, 2, 3]ˇ);",
11162 "console.log(ˇ'foo', [1, 2, 3]);",
11163 );
11164 assert(
11165 "console.log(ˇ'foo', [1, 2, 3]);",
11166 "console.log('foo', [1, 2, 3]ˇ);",
11167 );
11168
11169 // Bias forward if two options are equally likely
11170 assert(
11171 "let result = curried_fun()ˇ();",
11172 "let result = curried_fun()()ˇ;",
11173 );
11174
11175 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
11176 assert(
11177 indoc! {"
11178 function test() {
11179 console.log('test')ˇ
11180 }"},
11181 indoc! {"
11182 function test() {
11183 console.logˇ('test')
11184 }"},
11185 );
11186}
11187
11188#[gpui::test]
11189async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
11190 init_test(cx, |_| {});
11191
11192 let fs = FakeFs::new(cx.executor());
11193 fs.insert_tree(
11194 path!("/a"),
11195 json!({
11196 "main.rs": "fn main() { let a = 5; }",
11197 "other.rs": "// Test file",
11198 }),
11199 )
11200 .await;
11201 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11202
11203 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11204 language_registry.add(Arc::new(Language::new(
11205 LanguageConfig {
11206 name: "Rust".into(),
11207 matcher: LanguageMatcher {
11208 path_suffixes: vec!["rs".to_string()],
11209 ..Default::default()
11210 },
11211 brackets: BracketPairConfig {
11212 pairs: vec![BracketPair {
11213 start: "{".to_string(),
11214 end: "}".to_string(),
11215 close: true,
11216 surround: true,
11217 newline: true,
11218 }],
11219 disabled_scopes_by_bracket_ix: Vec::new(),
11220 },
11221 ..Default::default()
11222 },
11223 Some(tree_sitter_rust::LANGUAGE.into()),
11224 )));
11225 let mut fake_servers = language_registry.register_fake_lsp(
11226 "Rust",
11227 FakeLspAdapter {
11228 capabilities: lsp::ServerCapabilities {
11229 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
11230 first_trigger_character: "{".to_string(),
11231 more_trigger_character: None,
11232 }),
11233 ..Default::default()
11234 },
11235 ..Default::default()
11236 },
11237 );
11238
11239 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11240
11241 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11242
11243 let worktree_id = workspace
11244 .update(cx, |workspace, _, cx| {
11245 workspace.project().update(cx, |project, cx| {
11246 project.worktrees(cx).next().unwrap().read(cx).id()
11247 })
11248 })
11249 .unwrap();
11250
11251 let buffer = project
11252 .update(cx, |project, cx| {
11253 project.open_local_buffer(path!("/a/main.rs"), cx)
11254 })
11255 .await
11256 .unwrap();
11257 let editor_handle = workspace
11258 .update(cx, |workspace, window, cx| {
11259 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
11260 })
11261 .unwrap()
11262 .await
11263 .unwrap()
11264 .downcast::<Editor>()
11265 .unwrap();
11266
11267 cx.executor().start_waiting();
11268 let fake_server = fake_servers.next().await.unwrap();
11269
11270 fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
11271 assert_eq!(
11272 params.text_document_position.text_document.uri,
11273 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
11274 );
11275 assert_eq!(
11276 params.text_document_position.position,
11277 lsp::Position::new(0, 21),
11278 );
11279
11280 Ok(Some(vec![lsp::TextEdit {
11281 new_text: "]".to_string(),
11282 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11283 }]))
11284 });
11285
11286 editor_handle.update_in(cx, |editor, window, cx| {
11287 window.focus(&editor.focus_handle(cx));
11288 editor.change_selections(None, window, cx, |s| {
11289 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
11290 });
11291 editor.handle_input("{", window, cx);
11292 });
11293
11294 cx.executor().run_until_parked();
11295
11296 buffer.update(cx, |buffer, _| {
11297 assert_eq!(
11298 buffer.text(),
11299 "fn main() { let a = {5}; }",
11300 "No extra braces from on type formatting should appear in the buffer"
11301 )
11302 });
11303}
11304
11305#[gpui::test]
11306async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) {
11307 init_test(cx, |_| {});
11308
11309 let fs = FakeFs::new(cx.executor());
11310 fs.insert_tree(
11311 path!("/a"),
11312 json!({
11313 "main.rs": "fn main() { let a = 5; }",
11314 "other.rs": "// Test file",
11315 }),
11316 )
11317 .await;
11318
11319 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11320
11321 let server_restarts = Arc::new(AtomicUsize::new(0));
11322 let closure_restarts = Arc::clone(&server_restarts);
11323 let language_server_name = "test language server";
11324 let language_name: LanguageName = "Rust".into();
11325
11326 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11327 language_registry.add(Arc::new(Language::new(
11328 LanguageConfig {
11329 name: language_name.clone(),
11330 matcher: LanguageMatcher {
11331 path_suffixes: vec!["rs".to_string()],
11332 ..Default::default()
11333 },
11334 ..Default::default()
11335 },
11336 Some(tree_sitter_rust::LANGUAGE.into()),
11337 )));
11338 let mut fake_servers = language_registry.register_fake_lsp(
11339 "Rust",
11340 FakeLspAdapter {
11341 name: language_server_name,
11342 initialization_options: Some(json!({
11343 "testOptionValue": true
11344 })),
11345 initializer: Some(Box::new(move |fake_server| {
11346 let task_restarts = Arc::clone(&closure_restarts);
11347 fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
11348 task_restarts.fetch_add(1, atomic::Ordering::Release);
11349 futures::future::ready(Ok(()))
11350 });
11351 })),
11352 ..Default::default()
11353 },
11354 );
11355
11356 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11357 let _buffer = project
11358 .update(cx, |project, cx| {
11359 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
11360 })
11361 .await
11362 .unwrap();
11363 let _fake_server = fake_servers.next().await.unwrap();
11364 update_test_language_settings(cx, |language_settings| {
11365 language_settings.languages.insert(
11366 language_name.clone(),
11367 LanguageSettingsContent {
11368 tab_size: NonZeroU32::new(8),
11369 ..Default::default()
11370 },
11371 );
11372 });
11373 cx.executor().run_until_parked();
11374 assert_eq!(
11375 server_restarts.load(atomic::Ordering::Acquire),
11376 0,
11377 "Should not restart LSP server on an unrelated change"
11378 );
11379
11380 update_test_project_settings(cx, |project_settings| {
11381 project_settings.lsp.insert(
11382 "Some other server name".into(),
11383 LspSettings {
11384 binary: None,
11385 settings: None,
11386 initialization_options: Some(json!({
11387 "some other init value": false
11388 })),
11389 },
11390 );
11391 });
11392 cx.executor().run_until_parked();
11393 assert_eq!(
11394 server_restarts.load(atomic::Ordering::Acquire),
11395 0,
11396 "Should not restart LSP server on an unrelated LSP settings change"
11397 );
11398
11399 update_test_project_settings(cx, |project_settings| {
11400 project_settings.lsp.insert(
11401 language_server_name.into(),
11402 LspSettings {
11403 binary: None,
11404 settings: None,
11405 initialization_options: Some(json!({
11406 "anotherInitValue": false
11407 })),
11408 },
11409 );
11410 });
11411 cx.executor().run_until_parked();
11412 assert_eq!(
11413 server_restarts.load(atomic::Ordering::Acquire),
11414 1,
11415 "Should restart LSP server on a related LSP settings change"
11416 );
11417
11418 update_test_project_settings(cx, |project_settings| {
11419 project_settings.lsp.insert(
11420 language_server_name.into(),
11421 LspSettings {
11422 binary: None,
11423 settings: None,
11424 initialization_options: Some(json!({
11425 "anotherInitValue": false
11426 })),
11427 },
11428 );
11429 });
11430 cx.executor().run_until_parked();
11431 assert_eq!(
11432 server_restarts.load(atomic::Ordering::Acquire),
11433 1,
11434 "Should not restart LSP server on a related LSP settings change that is the same"
11435 );
11436
11437 update_test_project_settings(cx, |project_settings| {
11438 project_settings.lsp.insert(
11439 language_server_name.into(),
11440 LspSettings {
11441 binary: None,
11442 settings: None,
11443 initialization_options: None,
11444 },
11445 );
11446 });
11447 cx.executor().run_until_parked();
11448 assert_eq!(
11449 server_restarts.load(atomic::Ordering::Acquire),
11450 2,
11451 "Should restart LSP server on another related LSP settings change"
11452 );
11453}
11454
11455#[gpui::test]
11456async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
11457 init_test(cx, |_| {});
11458
11459 let mut cx = EditorLspTestContext::new_rust(
11460 lsp::ServerCapabilities {
11461 completion_provider: Some(lsp::CompletionOptions {
11462 trigger_characters: Some(vec![".".to_string()]),
11463 resolve_provider: Some(true),
11464 ..Default::default()
11465 }),
11466 ..Default::default()
11467 },
11468 cx,
11469 )
11470 .await;
11471
11472 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11473 cx.simulate_keystroke(".");
11474 let completion_item = lsp::CompletionItem {
11475 label: "some".into(),
11476 kind: Some(lsp::CompletionItemKind::SNIPPET),
11477 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11478 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11479 kind: lsp::MarkupKind::Markdown,
11480 value: "```rust\nSome(2)\n```".to_string(),
11481 })),
11482 deprecated: Some(false),
11483 sort_text: Some("fffffff2".to_string()),
11484 filter_text: Some("some".to_string()),
11485 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11486 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11487 range: lsp::Range {
11488 start: lsp::Position {
11489 line: 0,
11490 character: 22,
11491 },
11492 end: lsp::Position {
11493 line: 0,
11494 character: 22,
11495 },
11496 },
11497 new_text: "Some(2)".to_string(),
11498 })),
11499 additional_text_edits: Some(vec![lsp::TextEdit {
11500 range: lsp::Range {
11501 start: lsp::Position {
11502 line: 0,
11503 character: 20,
11504 },
11505 end: lsp::Position {
11506 line: 0,
11507 character: 22,
11508 },
11509 },
11510 new_text: "".to_string(),
11511 }]),
11512 ..Default::default()
11513 };
11514
11515 let closure_completion_item = completion_item.clone();
11516 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11517 let task_completion_item = closure_completion_item.clone();
11518 async move {
11519 Ok(Some(lsp::CompletionResponse::Array(vec![
11520 task_completion_item,
11521 ])))
11522 }
11523 });
11524
11525 request.next().await;
11526
11527 cx.condition(|editor, _| editor.context_menu_visible())
11528 .await;
11529 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11530 editor
11531 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11532 .unwrap()
11533 });
11534 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
11535
11536 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
11537 let task_completion_item = completion_item.clone();
11538 async move { Ok(task_completion_item) }
11539 })
11540 .next()
11541 .await
11542 .unwrap();
11543 apply_additional_edits.await.unwrap();
11544 cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
11545}
11546
11547#[gpui::test]
11548async fn test_completions_resolve_updates_labels_if_filter_text_matches(
11549 cx: &mut gpui::TestAppContext,
11550) {
11551 init_test(cx, |_| {});
11552
11553 let mut cx = EditorLspTestContext::new_rust(
11554 lsp::ServerCapabilities {
11555 completion_provider: Some(lsp::CompletionOptions {
11556 trigger_characters: Some(vec![".".to_string()]),
11557 resolve_provider: Some(true),
11558 ..Default::default()
11559 }),
11560 ..Default::default()
11561 },
11562 cx,
11563 )
11564 .await;
11565
11566 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11567 cx.simulate_keystroke(".");
11568
11569 let item1 = lsp::CompletionItem {
11570 label: "method id()".to_string(),
11571 filter_text: Some("id".to_string()),
11572 detail: None,
11573 documentation: None,
11574 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11575 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11576 new_text: ".id".to_string(),
11577 })),
11578 ..lsp::CompletionItem::default()
11579 };
11580
11581 let item2 = lsp::CompletionItem {
11582 label: "other".to_string(),
11583 filter_text: Some("other".to_string()),
11584 detail: None,
11585 documentation: None,
11586 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11587 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11588 new_text: ".other".to_string(),
11589 })),
11590 ..lsp::CompletionItem::default()
11591 };
11592
11593 let item1 = item1.clone();
11594 cx.handle_request::<lsp::request::Completion, _, _>({
11595 let item1 = item1.clone();
11596 move |_, _, _| {
11597 let item1 = item1.clone();
11598 let item2 = item2.clone();
11599 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
11600 }
11601 })
11602 .next()
11603 .await;
11604
11605 cx.condition(|editor, _| editor.context_menu_visible())
11606 .await;
11607 cx.update_editor(|editor, _, _| {
11608 let context_menu = editor.context_menu.borrow_mut();
11609 let context_menu = context_menu
11610 .as_ref()
11611 .expect("Should have the context menu deployed");
11612 match context_menu {
11613 CodeContextMenu::Completions(completions_menu) => {
11614 let completions = completions_menu.completions.borrow_mut();
11615 assert_eq!(
11616 completions
11617 .iter()
11618 .map(|completion| &completion.label.text)
11619 .collect::<Vec<_>>(),
11620 vec!["method id()", "other"]
11621 )
11622 }
11623 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11624 }
11625 });
11626
11627 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>({
11628 let item1 = item1.clone();
11629 move |_, item_to_resolve, _| {
11630 let item1 = item1.clone();
11631 async move {
11632 if item1 == item_to_resolve {
11633 Ok(lsp::CompletionItem {
11634 label: "method id()".to_string(),
11635 filter_text: Some("id".to_string()),
11636 detail: Some("Now resolved!".to_string()),
11637 documentation: Some(lsp::Documentation::String("Docs".to_string())),
11638 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11639 range: lsp::Range::new(
11640 lsp::Position::new(0, 22),
11641 lsp::Position::new(0, 22),
11642 ),
11643 new_text: ".id".to_string(),
11644 })),
11645 ..lsp::CompletionItem::default()
11646 })
11647 } else {
11648 Ok(item_to_resolve)
11649 }
11650 }
11651 }
11652 })
11653 .next()
11654 .await
11655 .unwrap();
11656 cx.run_until_parked();
11657
11658 cx.update_editor(|editor, window, cx| {
11659 editor.context_menu_next(&Default::default(), window, cx);
11660 });
11661
11662 cx.update_editor(|editor, _, _| {
11663 let context_menu = editor.context_menu.borrow_mut();
11664 let context_menu = context_menu
11665 .as_ref()
11666 .expect("Should have the context menu deployed");
11667 match context_menu {
11668 CodeContextMenu::Completions(completions_menu) => {
11669 let completions = completions_menu.completions.borrow_mut();
11670 assert_eq!(
11671 completions
11672 .iter()
11673 .map(|completion| &completion.label.text)
11674 .collect::<Vec<_>>(),
11675 vec!["method id() Now resolved!", "other"],
11676 "Should update first completion label, but not second as the filter text did not match."
11677 );
11678 }
11679 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11680 }
11681 });
11682}
11683
11684#[gpui::test]
11685async fn test_completions_resolve_happens_once(cx: &mut gpui::TestAppContext) {
11686 init_test(cx, |_| {});
11687
11688 let mut cx = EditorLspTestContext::new_rust(
11689 lsp::ServerCapabilities {
11690 completion_provider: Some(lsp::CompletionOptions {
11691 trigger_characters: Some(vec![".".to_string()]),
11692 resolve_provider: Some(true),
11693 ..Default::default()
11694 }),
11695 ..Default::default()
11696 },
11697 cx,
11698 )
11699 .await;
11700
11701 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11702 cx.simulate_keystroke(".");
11703
11704 let unresolved_item_1 = lsp::CompletionItem {
11705 label: "id".to_string(),
11706 filter_text: Some("id".to_string()),
11707 detail: None,
11708 documentation: None,
11709 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11710 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11711 new_text: ".id".to_string(),
11712 })),
11713 ..lsp::CompletionItem::default()
11714 };
11715 let resolved_item_1 = lsp::CompletionItem {
11716 additional_text_edits: Some(vec![lsp::TextEdit {
11717 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
11718 new_text: "!!".to_string(),
11719 }]),
11720 ..unresolved_item_1.clone()
11721 };
11722 let unresolved_item_2 = lsp::CompletionItem {
11723 label: "other".to_string(),
11724 filter_text: Some("other".to_string()),
11725 detail: None,
11726 documentation: None,
11727 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11728 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11729 new_text: ".other".to_string(),
11730 })),
11731 ..lsp::CompletionItem::default()
11732 };
11733 let resolved_item_2 = lsp::CompletionItem {
11734 additional_text_edits: Some(vec![lsp::TextEdit {
11735 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
11736 new_text: "??".to_string(),
11737 }]),
11738 ..unresolved_item_2.clone()
11739 };
11740
11741 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
11742 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
11743 cx.lsp
11744 .server
11745 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
11746 let unresolved_item_1 = unresolved_item_1.clone();
11747 let resolved_item_1 = resolved_item_1.clone();
11748 let unresolved_item_2 = unresolved_item_2.clone();
11749 let resolved_item_2 = resolved_item_2.clone();
11750 let resolve_requests_1 = resolve_requests_1.clone();
11751 let resolve_requests_2 = resolve_requests_2.clone();
11752 move |unresolved_request, _| {
11753 let unresolved_item_1 = unresolved_item_1.clone();
11754 let resolved_item_1 = resolved_item_1.clone();
11755 let unresolved_item_2 = unresolved_item_2.clone();
11756 let resolved_item_2 = resolved_item_2.clone();
11757 let resolve_requests_1 = resolve_requests_1.clone();
11758 let resolve_requests_2 = resolve_requests_2.clone();
11759 async move {
11760 if unresolved_request == unresolved_item_1 {
11761 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
11762 Ok(resolved_item_1.clone())
11763 } else if unresolved_request == unresolved_item_2 {
11764 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
11765 Ok(resolved_item_2.clone())
11766 } else {
11767 panic!("Unexpected completion item {unresolved_request:?}")
11768 }
11769 }
11770 }
11771 })
11772 .detach();
11773
11774 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11775 let unresolved_item_1 = unresolved_item_1.clone();
11776 let unresolved_item_2 = unresolved_item_2.clone();
11777 async move {
11778 Ok(Some(lsp::CompletionResponse::Array(vec![
11779 unresolved_item_1,
11780 unresolved_item_2,
11781 ])))
11782 }
11783 })
11784 .next()
11785 .await;
11786
11787 cx.condition(|editor, _| editor.context_menu_visible())
11788 .await;
11789 cx.update_editor(|editor, _, _| {
11790 let context_menu = editor.context_menu.borrow_mut();
11791 let context_menu = context_menu
11792 .as_ref()
11793 .expect("Should have the context menu deployed");
11794 match context_menu {
11795 CodeContextMenu::Completions(completions_menu) => {
11796 let completions = completions_menu.completions.borrow_mut();
11797 assert_eq!(
11798 completions
11799 .iter()
11800 .map(|completion| &completion.label.text)
11801 .collect::<Vec<_>>(),
11802 vec!["id", "other"]
11803 )
11804 }
11805 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11806 }
11807 });
11808 cx.run_until_parked();
11809
11810 cx.update_editor(|editor, window, cx| {
11811 editor.context_menu_next(&ContextMenuNext, window, cx);
11812 });
11813 cx.run_until_parked();
11814 cx.update_editor(|editor, window, cx| {
11815 editor.context_menu_prev(&ContextMenuPrev, window, cx);
11816 });
11817 cx.run_until_parked();
11818 cx.update_editor(|editor, window, cx| {
11819 editor.context_menu_next(&ContextMenuNext, window, cx);
11820 });
11821 cx.run_until_parked();
11822 cx.update_editor(|editor, window, cx| {
11823 editor
11824 .compose_completion(&ComposeCompletion::default(), window, cx)
11825 .expect("No task returned")
11826 })
11827 .await
11828 .expect("Completion failed");
11829 cx.run_until_parked();
11830
11831 cx.update_editor(|editor, _, cx| {
11832 assert_eq!(
11833 resolve_requests_1.load(atomic::Ordering::Acquire),
11834 1,
11835 "Should always resolve once despite multiple selections"
11836 );
11837 assert_eq!(
11838 resolve_requests_2.load(atomic::Ordering::Acquire),
11839 1,
11840 "Should always resolve once after multiple selections and applying the completion"
11841 );
11842 assert_eq!(
11843 editor.text(cx),
11844 "fn main() { let a = ??.other; }",
11845 "Should use resolved data when applying the completion"
11846 );
11847 });
11848}
11849
11850#[gpui::test]
11851async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
11852 init_test(cx, |_| {});
11853
11854 let item_0 = lsp::CompletionItem {
11855 label: "abs".into(),
11856 insert_text: Some("abs".into()),
11857 data: Some(json!({ "very": "special"})),
11858 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
11859 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11860 lsp::InsertReplaceEdit {
11861 new_text: "abs".to_string(),
11862 insert: lsp::Range::default(),
11863 replace: lsp::Range::default(),
11864 },
11865 )),
11866 ..lsp::CompletionItem::default()
11867 };
11868 let items = iter::once(item_0.clone())
11869 .chain((11..51).map(|i| lsp::CompletionItem {
11870 label: format!("item_{}", i),
11871 insert_text: Some(format!("item_{}", i)),
11872 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
11873 ..lsp::CompletionItem::default()
11874 }))
11875 .collect::<Vec<_>>();
11876
11877 let default_commit_characters = vec!["?".to_string()];
11878 let default_data = json!({ "default": "data"});
11879 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
11880 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
11881 let default_edit_range = lsp::Range {
11882 start: lsp::Position {
11883 line: 0,
11884 character: 5,
11885 },
11886 end: lsp::Position {
11887 line: 0,
11888 character: 5,
11889 },
11890 };
11891
11892 let item_0_out = lsp::CompletionItem {
11893 commit_characters: Some(default_commit_characters.clone()),
11894 insert_text_format: Some(default_insert_text_format),
11895 ..item_0
11896 };
11897 let items_out = iter::once(item_0_out)
11898 .chain(items[1..].iter().map(|item| lsp::CompletionItem {
11899 commit_characters: Some(default_commit_characters.clone()),
11900 data: Some(default_data.clone()),
11901 insert_text_mode: Some(default_insert_text_mode),
11902 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11903 range: default_edit_range,
11904 new_text: item.label.clone(),
11905 })),
11906 ..item.clone()
11907 }))
11908 .collect::<Vec<lsp::CompletionItem>>();
11909
11910 let mut cx = EditorLspTestContext::new_rust(
11911 lsp::ServerCapabilities {
11912 completion_provider: Some(lsp::CompletionOptions {
11913 trigger_characters: Some(vec![".".to_string()]),
11914 resolve_provider: Some(true),
11915 ..Default::default()
11916 }),
11917 ..Default::default()
11918 },
11919 cx,
11920 )
11921 .await;
11922
11923 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11924 cx.simulate_keystroke(".");
11925
11926 let completion_data = default_data.clone();
11927 let completion_characters = default_commit_characters.clone();
11928 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11929 let default_data = completion_data.clone();
11930 let default_commit_characters = completion_characters.clone();
11931 let items = items.clone();
11932 async move {
11933 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
11934 items,
11935 item_defaults: Some(lsp::CompletionListItemDefaults {
11936 data: Some(default_data.clone()),
11937 commit_characters: Some(default_commit_characters.clone()),
11938 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
11939 default_edit_range,
11940 )),
11941 insert_text_format: Some(default_insert_text_format),
11942 insert_text_mode: Some(default_insert_text_mode),
11943 }),
11944 ..lsp::CompletionList::default()
11945 })))
11946 }
11947 })
11948 .next()
11949 .await;
11950
11951 let resolved_items = Arc::new(Mutex::new(Vec::new()));
11952 cx.lsp
11953 .server
11954 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
11955 let closure_resolved_items = resolved_items.clone();
11956 move |item_to_resolve, _| {
11957 let closure_resolved_items = closure_resolved_items.clone();
11958 async move {
11959 closure_resolved_items.lock().push(item_to_resolve.clone());
11960 Ok(item_to_resolve)
11961 }
11962 }
11963 })
11964 .detach();
11965
11966 cx.condition(|editor, _| editor.context_menu_visible())
11967 .await;
11968 cx.run_until_parked();
11969 cx.update_editor(|editor, _, _| {
11970 let menu = editor.context_menu.borrow_mut();
11971 match menu.as_ref().expect("should have the completions menu") {
11972 CodeContextMenu::Completions(completions_menu) => {
11973 assert_eq!(
11974 completions_menu
11975 .entries
11976 .borrow()
11977 .iter()
11978 .map(|mat| mat.string.clone())
11979 .collect::<Vec<String>>(),
11980 items_out
11981 .iter()
11982 .map(|completion| completion.label.clone())
11983 .collect::<Vec<String>>()
11984 );
11985 }
11986 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
11987 }
11988 });
11989 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
11990 // with 4 from the end.
11991 assert_eq!(
11992 *resolved_items.lock(),
11993 [
11994 &items_out[0..16],
11995 &items_out[items_out.len() - 4..items_out.len()]
11996 ]
11997 .concat()
11998 .iter()
11999 .cloned()
12000 .collect::<Vec<lsp::CompletionItem>>()
12001 );
12002 resolved_items.lock().clear();
12003
12004 cx.update_editor(|editor, window, cx| {
12005 editor.context_menu_prev(&ContextMenuPrev, window, cx);
12006 });
12007 cx.run_until_parked();
12008 // Completions that have already been resolved are skipped.
12009 assert_eq!(
12010 *resolved_items.lock(),
12011 items_out[items_out.len() - 16..items_out.len() - 4]
12012 .iter()
12013 .cloned()
12014 .collect::<Vec<lsp::CompletionItem>>()
12015 );
12016 resolved_items.lock().clear();
12017}
12018
12019#[gpui::test]
12020async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
12021 init_test(cx, |_| {});
12022
12023 let mut cx = EditorLspTestContext::new(
12024 Language::new(
12025 LanguageConfig {
12026 matcher: LanguageMatcher {
12027 path_suffixes: vec!["jsx".into()],
12028 ..Default::default()
12029 },
12030 overrides: [(
12031 "element".into(),
12032 LanguageConfigOverride {
12033 word_characters: Override::Set(['-'].into_iter().collect()),
12034 ..Default::default()
12035 },
12036 )]
12037 .into_iter()
12038 .collect(),
12039 ..Default::default()
12040 },
12041 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12042 )
12043 .with_override_query("(jsx_self_closing_element) @element")
12044 .unwrap(),
12045 lsp::ServerCapabilities {
12046 completion_provider: Some(lsp::CompletionOptions {
12047 trigger_characters: Some(vec![":".to_string()]),
12048 ..Default::default()
12049 }),
12050 ..Default::default()
12051 },
12052 cx,
12053 )
12054 .await;
12055
12056 cx.lsp
12057 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12058 Ok(Some(lsp::CompletionResponse::Array(vec![
12059 lsp::CompletionItem {
12060 label: "bg-blue".into(),
12061 ..Default::default()
12062 },
12063 lsp::CompletionItem {
12064 label: "bg-red".into(),
12065 ..Default::default()
12066 },
12067 lsp::CompletionItem {
12068 label: "bg-yellow".into(),
12069 ..Default::default()
12070 },
12071 ])))
12072 });
12073
12074 cx.set_state(r#"<p class="bgˇ" />"#);
12075
12076 // Trigger completion when typing a dash, because the dash is an extra
12077 // word character in the 'element' scope, which contains the cursor.
12078 cx.simulate_keystroke("-");
12079 cx.executor().run_until_parked();
12080 cx.update_editor(|editor, _, _| {
12081 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12082 {
12083 assert_eq!(
12084 completion_menu_entries(&menu),
12085 &["bg-red", "bg-blue", "bg-yellow"]
12086 );
12087 } else {
12088 panic!("expected completion menu to be open");
12089 }
12090 });
12091
12092 cx.simulate_keystroke("l");
12093 cx.executor().run_until_parked();
12094 cx.update_editor(|editor, _, _| {
12095 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12096 {
12097 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
12098 } else {
12099 panic!("expected completion menu to be open");
12100 }
12101 });
12102
12103 // When filtering completions, consider the character after the '-' to
12104 // be the start of a subword.
12105 cx.set_state(r#"<p class="yelˇ" />"#);
12106 cx.simulate_keystroke("l");
12107 cx.executor().run_until_parked();
12108 cx.update_editor(|editor, _, _| {
12109 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12110 {
12111 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
12112 } else {
12113 panic!("expected completion menu to be open");
12114 }
12115 });
12116}
12117
12118fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
12119 let entries = menu.entries.borrow();
12120 entries.iter().map(|mat| mat.string.clone()).collect()
12121}
12122
12123#[gpui::test]
12124async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
12125 init_test(cx, |settings| {
12126 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
12127 FormatterList(vec![Formatter::Prettier].into()),
12128 ))
12129 });
12130
12131 let fs = FakeFs::new(cx.executor());
12132 fs.insert_file(path!("/file.ts"), Default::default()).await;
12133
12134 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
12135 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12136
12137 language_registry.add(Arc::new(Language::new(
12138 LanguageConfig {
12139 name: "TypeScript".into(),
12140 matcher: LanguageMatcher {
12141 path_suffixes: vec!["ts".to_string()],
12142 ..Default::default()
12143 },
12144 ..Default::default()
12145 },
12146 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12147 )));
12148 update_test_language_settings(cx, |settings| {
12149 settings.defaults.prettier = Some(PrettierSettings {
12150 allowed: true,
12151 ..PrettierSettings::default()
12152 });
12153 });
12154
12155 let test_plugin = "test_plugin";
12156 let _ = language_registry.register_fake_lsp(
12157 "TypeScript",
12158 FakeLspAdapter {
12159 prettier_plugins: vec![test_plugin],
12160 ..Default::default()
12161 },
12162 );
12163
12164 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
12165 let buffer = project
12166 .update(cx, |project, cx| {
12167 project.open_local_buffer(path!("/file.ts"), cx)
12168 })
12169 .await
12170 .unwrap();
12171
12172 let buffer_text = "one\ntwo\nthree\n";
12173 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12174 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12175 editor.update_in(cx, |editor, window, cx| {
12176 editor.set_text(buffer_text, window, cx)
12177 });
12178
12179 editor
12180 .update_in(cx, |editor, window, cx| {
12181 editor.perform_format(
12182 project.clone(),
12183 FormatTrigger::Manual,
12184 FormatTarget::Buffers,
12185 window,
12186 cx,
12187 )
12188 })
12189 .unwrap()
12190 .await;
12191 assert_eq!(
12192 editor.update(cx, |editor, cx| editor.text(cx)),
12193 buffer_text.to_string() + prettier_format_suffix,
12194 "Test prettier formatting was not applied to the original buffer text",
12195 );
12196
12197 update_test_language_settings(cx, |settings| {
12198 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
12199 });
12200 let format = editor.update_in(cx, |editor, window, cx| {
12201 editor.perform_format(
12202 project.clone(),
12203 FormatTrigger::Manual,
12204 FormatTarget::Buffers,
12205 window,
12206 cx,
12207 )
12208 });
12209 format.await.unwrap();
12210 assert_eq!(
12211 editor.update(cx, |editor, cx| editor.text(cx)),
12212 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
12213 "Autoformatting (via test prettier) was not applied to the original buffer text",
12214 );
12215}
12216
12217#[gpui::test]
12218async fn test_addition_reverts(cx: &mut gpui::TestAppContext) {
12219 init_test(cx, |_| {});
12220 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12221 let base_text = indoc! {r#"
12222 struct Row;
12223 struct Row1;
12224 struct Row2;
12225
12226 struct Row4;
12227 struct Row5;
12228 struct Row6;
12229
12230 struct Row8;
12231 struct Row9;
12232 struct Row10;"#};
12233
12234 // When addition hunks are not adjacent to carets, no hunk revert is performed
12235 assert_hunk_revert(
12236 indoc! {r#"struct Row;
12237 struct Row1;
12238 struct Row1.1;
12239 struct Row1.2;
12240 struct Row2;ˇ
12241
12242 struct Row4;
12243 struct Row5;
12244 struct Row6;
12245
12246 struct Row8;
12247 ˇstruct Row9;
12248 struct Row9.1;
12249 struct Row9.2;
12250 struct Row9.3;
12251 struct Row10;"#},
12252 vec![DiffHunkStatus::added(), DiffHunkStatus::added()],
12253 indoc! {r#"struct Row;
12254 struct Row1;
12255 struct Row1.1;
12256 struct Row1.2;
12257 struct Row2;ˇ
12258
12259 struct Row4;
12260 struct Row5;
12261 struct Row6;
12262
12263 struct Row8;
12264 ˇstruct Row9;
12265 struct Row9.1;
12266 struct Row9.2;
12267 struct Row9.3;
12268 struct Row10;"#},
12269 base_text,
12270 &mut cx,
12271 );
12272 // Same for selections
12273 assert_hunk_revert(
12274 indoc! {r#"struct Row;
12275 struct Row1;
12276 struct Row2;
12277 struct Row2.1;
12278 struct Row2.2;
12279 «ˇ
12280 struct Row4;
12281 struct» Row5;
12282 «struct Row6;
12283 ˇ»
12284 struct Row9.1;
12285 struct Row9.2;
12286 struct Row9.3;
12287 struct Row8;
12288 struct Row9;
12289 struct Row10;"#},
12290 vec![DiffHunkStatus::added(), DiffHunkStatus::added()],
12291 indoc! {r#"struct Row;
12292 struct Row1;
12293 struct Row2;
12294 struct Row2.1;
12295 struct Row2.2;
12296 «ˇ
12297 struct Row4;
12298 struct» Row5;
12299 «struct Row6;
12300 ˇ»
12301 struct Row9.1;
12302 struct Row9.2;
12303 struct Row9.3;
12304 struct Row8;
12305 struct Row9;
12306 struct Row10;"#},
12307 base_text,
12308 &mut cx,
12309 );
12310
12311 // When carets and selections intersect the addition hunks, those are reverted.
12312 // Adjacent carets got merged.
12313 assert_hunk_revert(
12314 indoc! {r#"struct Row;
12315 ˇ// something on the top
12316 struct Row1;
12317 struct Row2;
12318 struct Roˇw3.1;
12319 struct Row2.2;
12320 struct Row2.3;ˇ
12321
12322 struct Row4;
12323 struct ˇRow5.1;
12324 struct Row5.2;
12325 struct «Rowˇ»5.3;
12326 struct Row5;
12327 struct Row6;
12328 ˇ
12329 struct Row9.1;
12330 struct «Rowˇ»9.2;
12331 struct «ˇRow»9.3;
12332 struct Row8;
12333 struct Row9;
12334 «ˇ// something on bottom»
12335 struct Row10;"#},
12336 vec![
12337 DiffHunkStatus::added(),
12338 DiffHunkStatus::added(),
12339 DiffHunkStatus::added(),
12340 DiffHunkStatus::added(),
12341 DiffHunkStatus::added(),
12342 ],
12343 indoc! {r#"struct Row;
12344 ˇstruct Row1;
12345 struct Row2;
12346 ˇ
12347 struct Row4;
12348 ˇstruct Row5;
12349 struct Row6;
12350 ˇ
12351 ˇstruct Row8;
12352 struct Row9;
12353 ˇstruct Row10;"#},
12354 base_text,
12355 &mut cx,
12356 );
12357}
12358
12359#[gpui::test]
12360async fn test_modification_reverts(cx: &mut gpui::TestAppContext) {
12361 init_test(cx, |_| {});
12362 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12363 let base_text = indoc! {r#"
12364 struct Row;
12365 struct Row1;
12366 struct Row2;
12367
12368 struct Row4;
12369 struct Row5;
12370 struct Row6;
12371
12372 struct Row8;
12373 struct Row9;
12374 struct Row10;"#};
12375
12376 // Modification hunks behave the same as the addition ones.
12377 assert_hunk_revert(
12378 indoc! {r#"struct Row;
12379 struct Row1;
12380 struct Row33;
12381 ˇ
12382 struct Row4;
12383 struct Row5;
12384 struct Row6;
12385 ˇ
12386 struct Row99;
12387 struct Row9;
12388 struct Row10;"#},
12389 vec![DiffHunkStatus::modified(), DiffHunkStatus::modified()],
12390 indoc! {r#"struct Row;
12391 struct Row1;
12392 struct Row33;
12393 ˇ
12394 struct Row4;
12395 struct Row5;
12396 struct Row6;
12397 ˇ
12398 struct Row99;
12399 struct Row9;
12400 struct Row10;"#},
12401 base_text,
12402 &mut cx,
12403 );
12404 assert_hunk_revert(
12405 indoc! {r#"struct Row;
12406 struct Row1;
12407 struct Row33;
12408 «ˇ
12409 struct Row4;
12410 struct» Row5;
12411 «struct Row6;
12412 ˇ»
12413 struct Row99;
12414 struct Row9;
12415 struct Row10;"#},
12416 vec![DiffHunkStatus::modified(), DiffHunkStatus::modified()],
12417 indoc! {r#"struct Row;
12418 struct Row1;
12419 struct Row33;
12420 «ˇ
12421 struct Row4;
12422 struct» Row5;
12423 «struct Row6;
12424 ˇ»
12425 struct Row99;
12426 struct Row9;
12427 struct Row10;"#},
12428 base_text,
12429 &mut cx,
12430 );
12431
12432 assert_hunk_revert(
12433 indoc! {r#"ˇstruct Row1.1;
12434 struct Row1;
12435 «ˇstr»uct Row22;
12436
12437 struct ˇRow44;
12438 struct Row5;
12439 struct «Rˇ»ow66;ˇ
12440
12441 «struˇ»ct Row88;
12442 struct Row9;
12443 struct Row1011;ˇ"#},
12444 vec![
12445 DiffHunkStatus::modified(),
12446 DiffHunkStatus::modified(),
12447 DiffHunkStatus::modified(),
12448 DiffHunkStatus::modified(),
12449 DiffHunkStatus::modified(),
12450 DiffHunkStatus::modified(),
12451 ],
12452 indoc! {r#"struct Row;
12453 ˇstruct Row1;
12454 struct Row2;
12455 ˇ
12456 struct Row4;
12457 ˇstruct Row5;
12458 struct Row6;
12459 ˇ
12460 struct Row8;
12461 ˇstruct Row9;
12462 struct Row10;ˇ"#},
12463 base_text,
12464 &mut cx,
12465 );
12466}
12467
12468#[gpui::test]
12469async fn test_deleting_over_diff_hunk(cx: &mut gpui::TestAppContext) {
12470 init_test(cx, |_| {});
12471 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12472 let base_text = indoc! {r#"
12473 one
12474
12475 two
12476 three
12477 "#};
12478
12479 cx.set_diff_base(base_text);
12480 cx.set_state("\nˇ\n");
12481 cx.executor().run_until_parked();
12482 cx.update_editor(|editor, _window, cx| {
12483 editor.expand_selected_diff_hunks(cx);
12484 });
12485 cx.executor().run_until_parked();
12486 cx.update_editor(|editor, window, cx| {
12487 editor.backspace(&Default::default(), window, cx);
12488 });
12489 cx.run_until_parked();
12490 cx.assert_state_with_diff(
12491 indoc! {r#"
12492
12493 - two
12494 - threeˇ
12495 +
12496 "#}
12497 .to_string(),
12498 );
12499}
12500
12501#[gpui::test]
12502async fn test_deletion_reverts(cx: &mut gpui::TestAppContext) {
12503 init_test(cx, |_| {});
12504 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12505 let base_text = indoc! {r#"struct Row;
12506struct Row1;
12507struct Row2;
12508
12509struct Row4;
12510struct Row5;
12511struct Row6;
12512
12513struct Row8;
12514struct Row9;
12515struct Row10;"#};
12516
12517 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
12518 assert_hunk_revert(
12519 indoc! {r#"struct Row;
12520 struct Row2;
12521
12522 ˇstruct Row4;
12523 struct Row5;
12524 struct Row6;
12525 ˇ
12526 struct Row8;
12527 struct Row10;"#},
12528 vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()],
12529 indoc! {r#"struct Row;
12530 struct Row2;
12531
12532 ˇstruct Row4;
12533 struct Row5;
12534 struct Row6;
12535 ˇ
12536 struct Row8;
12537 struct Row10;"#},
12538 base_text,
12539 &mut cx,
12540 );
12541 assert_hunk_revert(
12542 indoc! {r#"struct Row;
12543 struct Row2;
12544
12545 «ˇstruct Row4;
12546 struct» Row5;
12547 «struct Row6;
12548 ˇ»
12549 struct Row8;
12550 struct Row10;"#},
12551 vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()],
12552 indoc! {r#"struct Row;
12553 struct Row2;
12554
12555 «ˇstruct Row4;
12556 struct» Row5;
12557 «struct Row6;
12558 ˇ»
12559 struct Row8;
12560 struct Row10;"#},
12561 base_text,
12562 &mut cx,
12563 );
12564
12565 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
12566 assert_hunk_revert(
12567 indoc! {r#"struct Row;
12568 ˇstruct Row2;
12569
12570 struct Row4;
12571 struct Row5;
12572 struct Row6;
12573
12574 struct Row8;ˇ
12575 struct Row10;"#},
12576 vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()],
12577 indoc! {r#"struct Row;
12578 struct Row1;
12579 ˇstruct Row2;
12580
12581 struct Row4;
12582 struct Row5;
12583 struct Row6;
12584
12585 struct Row8;ˇ
12586 struct Row9;
12587 struct Row10;"#},
12588 base_text,
12589 &mut cx,
12590 );
12591 assert_hunk_revert(
12592 indoc! {r#"struct Row;
12593 struct Row2«ˇ;
12594 struct Row4;
12595 struct» Row5;
12596 «struct Row6;
12597
12598 struct Row8;ˇ»
12599 struct Row10;"#},
12600 vec![
12601 DiffHunkStatus::removed(),
12602 DiffHunkStatus::removed(),
12603 DiffHunkStatus::removed(),
12604 ],
12605 indoc! {r#"struct Row;
12606 struct Row1;
12607 struct Row2«ˇ;
12608
12609 struct Row4;
12610 struct» Row5;
12611 «struct Row6;
12612
12613 struct Row8;ˇ»
12614 struct Row9;
12615 struct Row10;"#},
12616 base_text,
12617 &mut cx,
12618 );
12619}
12620
12621#[gpui::test]
12622async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
12623 init_test(cx, |_| {});
12624
12625 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
12626 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
12627 let base_text_3 =
12628 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
12629
12630 let text_1 = edit_first_char_of_every_line(base_text_1);
12631 let text_2 = edit_first_char_of_every_line(base_text_2);
12632 let text_3 = edit_first_char_of_every_line(base_text_3);
12633
12634 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
12635 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
12636 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
12637
12638 let multibuffer = cx.new(|cx| {
12639 let mut multibuffer = MultiBuffer::new(ReadWrite);
12640 multibuffer.push_excerpts(
12641 buffer_1.clone(),
12642 [
12643 ExcerptRange {
12644 context: Point::new(0, 0)..Point::new(3, 0),
12645 primary: None,
12646 },
12647 ExcerptRange {
12648 context: Point::new(5, 0)..Point::new(7, 0),
12649 primary: None,
12650 },
12651 ExcerptRange {
12652 context: Point::new(9, 0)..Point::new(10, 4),
12653 primary: None,
12654 },
12655 ],
12656 cx,
12657 );
12658 multibuffer.push_excerpts(
12659 buffer_2.clone(),
12660 [
12661 ExcerptRange {
12662 context: Point::new(0, 0)..Point::new(3, 0),
12663 primary: None,
12664 },
12665 ExcerptRange {
12666 context: Point::new(5, 0)..Point::new(7, 0),
12667 primary: None,
12668 },
12669 ExcerptRange {
12670 context: Point::new(9, 0)..Point::new(10, 4),
12671 primary: None,
12672 },
12673 ],
12674 cx,
12675 );
12676 multibuffer.push_excerpts(
12677 buffer_3.clone(),
12678 [
12679 ExcerptRange {
12680 context: Point::new(0, 0)..Point::new(3, 0),
12681 primary: None,
12682 },
12683 ExcerptRange {
12684 context: Point::new(5, 0)..Point::new(7, 0),
12685 primary: None,
12686 },
12687 ExcerptRange {
12688 context: Point::new(9, 0)..Point::new(10, 4),
12689 primary: None,
12690 },
12691 ],
12692 cx,
12693 );
12694 multibuffer
12695 });
12696
12697 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12698 editor.update_in(cx, |editor, _window, cx| {
12699 for (buffer, diff_base) in [
12700 (buffer_1.clone(), base_text_1),
12701 (buffer_2.clone(), base_text_2),
12702 (buffer_3.clone(), base_text_3),
12703 ] {
12704 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
12705 editor
12706 .buffer
12707 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
12708 }
12709 });
12710 cx.executor().run_until_parked();
12711
12712 editor.update_in(cx, |editor, window, cx| {
12713 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}");
12714 editor.select_all(&SelectAll, window, cx);
12715 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
12716 });
12717 cx.executor().run_until_parked();
12718
12719 // When all ranges are selected, all buffer hunks are reverted.
12720 editor.update(cx, |editor, cx| {
12721 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");
12722 });
12723 buffer_1.update(cx, |buffer, _| {
12724 assert_eq!(buffer.text(), base_text_1);
12725 });
12726 buffer_2.update(cx, |buffer, _| {
12727 assert_eq!(buffer.text(), base_text_2);
12728 });
12729 buffer_3.update(cx, |buffer, _| {
12730 assert_eq!(buffer.text(), base_text_3);
12731 });
12732
12733 editor.update_in(cx, |editor, window, cx| {
12734 editor.undo(&Default::default(), window, cx);
12735 });
12736
12737 editor.update_in(cx, |editor, window, cx| {
12738 editor.change_selections(None, window, cx, |s| {
12739 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
12740 });
12741 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
12742 });
12743
12744 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
12745 // but not affect buffer_2 and its related excerpts.
12746 editor.update(cx, |editor, cx| {
12747 assert_eq!(
12748 editor.text(cx),
12749 "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}"
12750 );
12751 });
12752 buffer_1.update(cx, |buffer, _| {
12753 assert_eq!(buffer.text(), base_text_1);
12754 });
12755 buffer_2.update(cx, |buffer, _| {
12756 assert_eq!(
12757 buffer.text(),
12758 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
12759 );
12760 });
12761 buffer_3.update(cx, |buffer, _| {
12762 assert_eq!(
12763 buffer.text(),
12764 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
12765 );
12766 });
12767
12768 fn edit_first_char_of_every_line(text: &str) -> String {
12769 text.split('\n')
12770 .map(|line| format!("X{}", &line[1..]))
12771 .collect::<Vec<_>>()
12772 .join("\n")
12773 }
12774}
12775
12776#[gpui::test]
12777async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
12778 init_test(cx, |_| {});
12779
12780 let cols = 4;
12781 let rows = 10;
12782 let sample_text_1 = sample_text(rows, cols, 'a');
12783 assert_eq!(
12784 sample_text_1,
12785 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
12786 );
12787 let sample_text_2 = sample_text(rows, cols, 'l');
12788 assert_eq!(
12789 sample_text_2,
12790 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
12791 );
12792 let sample_text_3 = sample_text(rows, cols, 'v');
12793 assert_eq!(
12794 sample_text_3,
12795 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
12796 );
12797
12798 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
12799 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
12800 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
12801
12802 let multi_buffer = cx.new(|cx| {
12803 let mut multibuffer = MultiBuffer::new(ReadWrite);
12804 multibuffer.push_excerpts(
12805 buffer_1.clone(),
12806 [
12807 ExcerptRange {
12808 context: Point::new(0, 0)..Point::new(3, 0),
12809 primary: None,
12810 },
12811 ExcerptRange {
12812 context: Point::new(5, 0)..Point::new(7, 0),
12813 primary: None,
12814 },
12815 ExcerptRange {
12816 context: Point::new(9, 0)..Point::new(10, 4),
12817 primary: None,
12818 },
12819 ],
12820 cx,
12821 );
12822 multibuffer.push_excerpts(
12823 buffer_2.clone(),
12824 [
12825 ExcerptRange {
12826 context: Point::new(0, 0)..Point::new(3, 0),
12827 primary: None,
12828 },
12829 ExcerptRange {
12830 context: Point::new(5, 0)..Point::new(7, 0),
12831 primary: None,
12832 },
12833 ExcerptRange {
12834 context: Point::new(9, 0)..Point::new(10, 4),
12835 primary: None,
12836 },
12837 ],
12838 cx,
12839 );
12840 multibuffer.push_excerpts(
12841 buffer_3.clone(),
12842 [
12843 ExcerptRange {
12844 context: Point::new(0, 0)..Point::new(3, 0),
12845 primary: None,
12846 },
12847 ExcerptRange {
12848 context: Point::new(5, 0)..Point::new(7, 0),
12849 primary: None,
12850 },
12851 ExcerptRange {
12852 context: Point::new(9, 0)..Point::new(10, 4),
12853 primary: None,
12854 },
12855 ],
12856 cx,
12857 );
12858 multibuffer
12859 });
12860
12861 let fs = FakeFs::new(cx.executor());
12862 fs.insert_tree(
12863 "/a",
12864 json!({
12865 "main.rs": sample_text_1,
12866 "other.rs": sample_text_2,
12867 "lib.rs": sample_text_3,
12868 }),
12869 )
12870 .await;
12871 let project = Project::test(fs, ["/a".as_ref()], cx).await;
12872 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12873 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12874 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
12875 Editor::new(
12876 EditorMode::Full,
12877 multi_buffer,
12878 Some(project.clone()),
12879 true,
12880 window,
12881 cx,
12882 )
12883 });
12884 let multibuffer_item_id = workspace
12885 .update(cx, |workspace, window, cx| {
12886 assert!(
12887 workspace.active_item(cx).is_none(),
12888 "active item should be None before the first item is added"
12889 );
12890 workspace.add_item_to_active_pane(
12891 Box::new(multi_buffer_editor.clone()),
12892 None,
12893 true,
12894 window,
12895 cx,
12896 );
12897 let active_item = workspace
12898 .active_item(cx)
12899 .expect("should have an active item after adding the multi buffer");
12900 assert!(
12901 !active_item.is_singleton(cx),
12902 "A multi buffer was expected to active after adding"
12903 );
12904 active_item.item_id()
12905 })
12906 .unwrap();
12907 cx.executor().run_until_parked();
12908
12909 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12910 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12911 s.select_ranges(Some(1..2))
12912 });
12913 editor.open_excerpts(&OpenExcerpts, window, cx);
12914 });
12915 cx.executor().run_until_parked();
12916 let first_item_id = workspace
12917 .update(cx, |workspace, window, cx| {
12918 let active_item = workspace
12919 .active_item(cx)
12920 .expect("should have an active item after navigating into the 1st buffer");
12921 let first_item_id = active_item.item_id();
12922 assert_ne!(
12923 first_item_id, multibuffer_item_id,
12924 "Should navigate into the 1st buffer and activate it"
12925 );
12926 assert!(
12927 active_item.is_singleton(cx),
12928 "New active item should be a singleton buffer"
12929 );
12930 assert_eq!(
12931 active_item
12932 .act_as::<Editor>(cx)
12933 .expect("should have navigated into an editor for the 1st buffer")
12934 .read(cx)
12935 .text(cx),
12936 sample_text_1
12937 );
12938
12939 workspace
12940 .go_back(workspace.active_pane().downgrade(), window, cx)
12941 .detach_and_log_err(cx);
12942
12943 first_item_id
12944 })
12945 .unwrap();
12946 cx.executor().run_until_parked();
12947 workspace
12948 .update(cx, |workspace, _, cx| {
12949 let active_item = workspace
12950 .active_item(cx)
12951 .expect("should have an active item after navigating back");
12952 assert_eq!(
12953 active_item.item_id(),
12954 multibuffer_item_id,
12955 "Should navigate back to the multi buffer"
12956 );
12957 assert!(!active_item.is_singleton(cx));
12958 })
12959 .unwrap();
12960
12961 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12962 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12963 s.select_ranges(Some(39..40))
12964 });
12965 editor.open_excerpts(&OpenExcerpts, window, cx);
12966 });
12967 cx.executor().run_until_parked();
12968 let second_item_id = workspace
12969 .update(cx, |workspace, window, cx| {
12970 let active_item = workspace
12971 .active_item(cx)
12972 .expect("should have an active item after navigating into the 2nd buffer");
12973 let second_item_id = active_item.item_id();
12974 assert_ne!(
12975 second_item_id, multibuffer_item_id,
12976 "Should navigate away from the multibuffer"
12977 );
12978 assert_ne!(
12979 second_item_id, first_item_id,
12980 "Should navigate into the 2nd buffer and activate it"
12981 );
12982 assert!(
12983 active_item.is_singleton(cx),
12984 "New active item should be a singleton buffer"
12985 );
12986 assert_eq!(
12987 active_item
12988 .act_as::<Editor>(cx)
12989 .expect("should have navigated into an editor")
12990 .read(cx)
12991 .text(cx),
12992 sample_text_2
12993 );
12994
12995 workspace
12996 .go_back(workspace.active_pane().downgrade(), window, cx)
12997 .detach_and_log_err(cx);
12998
12999 second_item_id
13000 })
13001 .unwrap();
13002 cx.executor().run_until_parked();
13003 workspace
13004 .update(cx, |workspace, _, cx| {
13005 let active_item = workspace
13006 .active_item(cx)
13007 .expect("should have an active item after navigating back from the 2nd buffer");
13008 assert_eq!(
13009 active_item.item_id(),
13010 multibuffer_item_id,
13011 "Should navigate back from the 2nd buffer to the multi buffer"
13012 );
13013 assert!(!active_item.is_singleton(cx));
13014 })
13015 .unwrap();
13016
13017 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13018 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
13019 s.select_ranges(Some(70..70))
13020 });
13021 editor.open_excerpts(&OpenExcerpts, window, cx);
13022 });
13023 cx.executor().run_until_parked();
13024 workspace
13025 .update(cx, |workspace, window, cx| {
13026 let active_item = workspace
13027 .active_item(cx)
13028 .expect("should have an active item after navigating into the 3rd buffer");
13029 let third_item_id = active_item.item_id();
13030 assert_ne!(
13031 third_item_id, multibuffer_item_id,
13032 "Should navigate into the 3rd buffer and activate it"
13033 );
13034 assert_ne!(third_item_id, first_item_id);
13035 assert_ne!(third_item_id, second_item_id);
13036 assert!(
13037 active_item.is_singleton(cx),
13038 "New active item should be a singleton buffer"
13039 );
13040 assert_eq!(
13041 active_item
13042 .act_as::<Editor>(cx)
13043 .expect("should have navigated into an editor")
13044 .read(cx)
13045 .text(cx),
13046 sample_text_3
13047 );
13048
13049 workspace
13050 .go_back(workspace.active_pane().downgrade(), window, cx)
13051 .detach_and_log_err(cx);
13052 })
13053 .unwrap();
13054 cx.executor().run_until_parked();
13055 workspace
13056 .update(cx, |workspace, _, cx| {
13057 let active_item = workspace
13058 .active_item(cx)
13059 .expect("should have an active item after navigating back from the 3rd buffer");
13060 assert_eq!(
13061 active_item.item_id(),
13062 multibuffer_item_id,
13063 "Should navigate back from the 3rd buffer to the multi buffer"
13064 );
13065 assert!(!active_item.is_singleton(cx));
13066 })
13067 .unwrap();
13068}
13069
13070#[gpui::test]
13071async fn test_toggle_selected_diff_hunks(
13072 executor: BackgroundExecutor,
13073 cx: &mut gpui::TestAppContext,
13074) {
13075 init_test(cx, |_| {});
13076
13077 let mut cx = EditorTestContext::new(cx).await;
13078
13079 let diff_base = r#"
13080 use some::mod;
13081
13082 const A: u32 = 42;
13083
13084 fn main() {
13085 println!("hello");
13086
13087 println!("world");
13088 }
13089 "#
13090 .unindent();
13091
13092 cx.set_state(
13093 &r#"
13094 use some::modified;
13095
13096 ˇ
13097 fn main() {
13098 println!("hello there");
13099
13100 println!("around the");
13101 println!("world");
13102 }
13103 "#
13104 .unindent(),
13105 );
13106
13107 cx.set_diff_base(&diff_base);
13108 executor.run_until_parked();
13109
13110 cx.update_editor(|editor, window, cx| {
13111 editor.go_to_next_hunk(&GoToHunk, window, cx);
13112 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13113 });
13114 executor.run_until_parked();
13115 cx.assert_state_with_diff(
13116 r#"
13117 use some::modified;
13118
13119
13120 fn main() {
13121 - println!("hello");
13122 + ˇ println!("hello there");
13123
13124 println!("around the");
13125 println!("world");
13126 }
13127 "#
13128 .unindent(),
13129 );
13130
13131 cx.update_editor(|editor, window, cx| {
13132 for _ in 0..2 {
13133 editor.go_to_next_hunk(&GoToHunk, window, cx);
13134 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13135 }
13136 });
13137 executor.run_until_parked();
13138 cx.assert_state_with_diff(
13139 r#"
13140 - use some::mod;
13141 + ˇuse some::modified;
13142
13143
13144 fn main() {
13145 - println!("hello");
13146 + println!("hello there");
13147
13148 + println!("around the");
13149 println!("world");
13150 }
13151 "#
13152 .unindent(),
13153 );
13154
13155 cx.update_editor(|editor, window, cx| {
13156 editor.go_to_next_hunk(&GoToHunk, window, cx);
13157 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13158 });
13159 executor.run_until_parked();
13160 cx.assert_state_with_diff(
13161 r#"
13162 - use some::mod;
13163 + use some::modified;
13164
13165 - const A: u32 = 42;
13166 ˇ
13167 fn main() {
13168 - println!("hello");
13169 + println!("hello there");
13170
13171 + println!("around the");
13172 println!("world");
13173 }
13174 "#
13175 .unindent(),
13176 );
13177
13178 cx.update_editor(|editor, window, cx| {
13179 editor.cancel(&Cancel, window, cx);
13180 });
13181
13182 cx.assert_state_with_diff(
13183 r#"
13184 use some::modified;
13185
13186 ˇ
13187 fn main() {
13188 println!("hello there");
13189
13190 println!("around the");
13191 println!("world");
13192 }
13193 "#
13194 .unindent(),
13195 );
13196}
13197
13198#[gpui::test]
13199async fn test_diff_base_change_with_expanded_diff_hunks(
13200 executor: BackgroundExecutor,
13201 cx: &mut gpui::TestAppContext,
13202) {
13203 init_test(cx, |_| {});
13204
13205 let mut cx = EditorTestContext::new(cx).await;
13206
13207 let diff_base = r#"
13208 use some::mod1;
13209 use some::mod2;
13210
13211 const A: u32 = 42;
13212 const B: u32 = 42;
13213 const C: u32 = 42;
13214
13215 fn main() {
13216 println!("hello");
13217
13218 println!("world");
13219 }
13220 "#
13221 .unindent();
13222
13223 cx.set_state(
13224 &r#"
13225 use some::mod2;
13226
13227 const A: u32 = 42;
13228 const C: u32 = 42;
13229
13230 fn main(ˇ) {
13231 //println!("hello");
13232
13233 println!("world");
13234 //
13235 //
13236 }
13237 "#
13238 .unindent(),
13239 );
13240
13241 cx.set_diff_base(&diff_base);
13242 executor.run_until_parked();
13243
13244 cx.update_editor(|editor, window, cx| {
13245 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13246 });
13247 executor.run_until_parked();
13248 cx.assert_state_with_diff(
13249 r#"
13250 - use some::mod1;
13251 use some::mod2;
13252
13253 const A: u32 = 42;
13254 - const B: u32 = 42;
13255 const C: u32 = 42;
13256
13257 fn main(ˇ) {
13258 - println!("hello");
13259 + //println!("hello");
13260
13261 println!("world");
13262 + //
13263 + //
13264 }
13265 "#
13266 .unindent(),
13267 );
13268
13269 cx.set_diff_base("new diff base!");
13270 executor.run_until_parked();
13271 cx.assert_state_with_diff(
13272 r#"
13273 - new diff base!
13274 + use some::mod2;
13275 +
13276 + const A: u32 = 42;
13277 + const C: u32 = 42;
13278 +
13279 + fn main(ˇ) {
13280 + //println!("hello");
13281 +
13282 + println!("world");
13283 + //
13284 + //
13285 + }
13286 "#
13287 .unindent(),
13288 );
13289}
13290
13291#[gpui::test]
13292async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) {
13293 init_test(cx, |_| {});
13294
13295 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
13296 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
13297 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
13298 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
13299 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
13300 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
13301
13302 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
13303 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
13304 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
13305
13306 let multi_buffer = cx.new(|cx| {
13307 let mut multibuffer = MultiBuffer::new(ReadWrite);
13308 multibuffer.push_excerpts(
13309 buffer_1.clone(),
13310 [
13311 ExcerptRange {
13312 context: Point::new(0, 0)..Point::new(3, 0),
13313 primary: None,
13314 },
13315 ExcerptRange {
13316 context: Point::new(5, 0)..Point::new(7, 0),
13317 primary: None,
13318 },
13319 ExcerptRange {
13320 context: Point::new(9, 0)..Point::new(10, 3),
13321 primary: None,
13322 },
13323 ],
13324 cx,
13325 );
13326 multibuffer.push_excerpts(
13327 buffer_2.clone(),
13328 [
13329 ExcerptRange {
13330 context: Point::new(0, 0)..Point::new(3, 0),
13331 primary: None,
13332 },
13333 ExcerptRange {
13334 context: Point::new(5, 0)..Point::new(7, 0),
13335 primary: None,
13336 },
13337 ExcerptRange {
13338 context: Point::new(9, 0)..Point::new(10, 3),
13339 primary: None,
13340 },
13341 ],
13342 cx,
13343 );
13344 multibuffer.push_excerpts(
13345 buffer_3.clone(),
13346 [
13347 ExcerptRange {
13348 context: Point::new(0, 0)..Point::new(3, 0),
13349 primary: None,
13350 },
13351 ExcerptRange {
13352 context: Point::new(5, 0)..Point::new(7, 0),
13353 primary: None,
13354 },
13355 ExcerptRange {
13356 context: Point::new(9, 0)..Point::new(10, 3),
13357 primary: None,
13358 },
13359 ],
13360 cx,
13361 );
13362 multibuffer
13363 });
13364
13365 let editor = cx.add_window(|window, cx| {
13366 Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
13367 });
13368 editor
13369 .update(cx, |editor, _window, cx| {
13370 for (buffer, diff_base) in [
13371 (buffer_1.clone(), file_1_old),
13372 (buffer_2.clone(), file_2_old),
13373 (buffer_3.clone(), file_3_old),
13374 ] {
13375 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
13376 editor
13377 .buffer
13378 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
13379 }
13380 })
13381 .unwrap();
13382
13383 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13384 cx.run_until_parked();
13385
13386 cx.assert_editor_state(
13387 &"
13388 ˇaaa
13389 ccc
13390 ddd
13391
13392 ggg
13393 hhh
13394
13395
13396 lll
13397 mmm
13398 NNN
13399
13400 qqq
13401 rrr
13402
13403 uuu
13404 111
13405 222
13406 333
13407
13408 666
13409 777
13410
13411 000
13412 !!!"
13413 .unindent(),
13414 );
13415
13416 cx.update_editor(|editor, window, cx| {
13417 editor.select_all(&SelectAll, window, cx);
13418 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13419 });
13420 cx.executor().run_until_parked();
13421
13422 cx.assert_state_with_diff(
13423 "
13424 «aaa
13425 - bbb
13426 ccc
13427 ddd
13428
13429 ggg
13430 hhh
13431
13432
13433 lll
13434 mmm
13435 - nnn
13436 + NNN
13437
13438 qqq
13439 rrr
13440
13441 uuu
13442 111
13443 222
13444 333
13445
13446 + 666
13447 777
13448
13449 000
13450 !!!ˇ»"
13451 .unindent(),
13452 );
13453}
13454
13455#[gpui::test]
13456async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext) {
13457 init_test(cx, |_| {});
13458
13459 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
13460 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
13461
13462 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
13463 let multi_buffer = cx.new(|cx| {
13464 let mut multibuffer = MultiBuffer::new(ReadWrite);
13465 multibuffer.push_excerpts(
13466 buffer.clone(),
13467 [
13468 ExcerptRange {
13469 context: Point::new(0, 0)..Point::new(2, 0),
13470 primary: None,
13471 },
13472 ExcerptRange {
13473 context: Point::new(4, 0)..Point::new(7, 0),
13474 primary: None,
13475 },
13476 ExcerptRange {
13477 context: Point::new(9, 0)..Point::new(10, 0),
13478 primary: None,
13479 },
13480 ],
13481 cx,
13482 );
13483 multibuffer
13484 });
13485
13486 let editor = cx.add_window(|window, cx| {
13487 Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
13488 });
13489 editor
13490 .update(cx, |editor, _window, cx| {
13491 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
13492 editor
13493 .buffer
13494 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
13495 })
13496 .unwrap();
13497
13498 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13499 cx.run_until_parked();
13500
13501 cx.update_editor(|editor, window, cx| {
13502 editor.expand_all_diff_hunks(&Default::default(), window, cx)
13503 });
13504 cx.executor().run_until_parked();
13505
13506 // When the start of a hunk coincides with the start of its excerpt,
13507 // the hunk is expanded. When the start of a a hunk is earlier than
13508 // the start of its excerpt, the hunk is not expanded.
13509 cx.assert_state_with_diff(
13510 "
13511 ˇaaa
13512 - bbb
13513 + BBB
13514
13515 - ddd
13516 - eee
13517 + DDD
13518 + EEE
13519 fff
13520
13521 iii
13522 "
13523 .unindent(),
13524 );
13525}
13526
13527#[gpui::test]
13528async fn test_edits_around_expanded_insertion_hunks(
13529 executor: BackgroundExecutor,
13530 cx: &mut gpui::TestAppContext,
13531) {
13532 init_test(cx, |_| {});
13533
13534 let mut cx = EditorTestContext::new(cx).await;
13535
13536 let diff_base = r#"
13537 use some::mod1;
13538 use some::mod2;
13539
13540 const A: u32 = 42;
13541
13542 fn main() {
13543 println!("hello");
13544
13545 println!("world");
13546 }
13547 "#
13548 .unindent();
13549 executor.run_until_parked();
13550 cx.set_state(
13551 &r#"
13552 use some::mod1;
13553 use some::mod2;
13554
13555 const A: u32 = 42;
13556 const B: u32 = 42;
13557 const C: u32 = 42;
13558 ˇ
13559
13560 fn main() {
13561 println!("hello");
13562
13563 println!("world");
13564 }
13565 "#
13566 .unindent(),
13567 );
13568
13569 cx.set_diff_base(&diff_base);
13570 executor.run_until_parked();
13571
13572 cx.update_editor(|editor, window, cx| {
13573 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13574 });
13575 executor.run_until_parked();
13576
13577 cx.assert_state_with_diff(
13578 r#"
13579 use some::mod1;
13580 use some::mod2;
13581
13582 const A: u32 = 42;
13583 + const B: u32 = 42;
13584 + const C: u32 = 42;
13585 + ˇ
13586
13587 fn main() {
13588 println!("hello");
13589
13590 println!("world");
13591 }
13592 "#
13593 .unindent(),
13594 );
13595
13596 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
13597 executor.run_until_parked();
13598
13599 cx.assert_state_with_diff(
13600 r#"
13601 use some::mod1;
13602 use some::mod2;
13603
13604 const A: u32 = 42;
13605 + const B: u32 = 42;
13606 + const C: u32 = 42;
13607 + const D: u32 = 42;
13608 + ˇ
13609
13610 fn main() {
13611 println!("hello");
13612
13613 println!("world");
13614 }
13615 "#
13616 .unindent(),
13617 );
13618
13619 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
13620 executor.run_until_parked();
13621
13622 cx.assert_state_with_diff(
13623 r#"
13624 use some::mod1;
13625 use some::mod2;
13626
13627 const A: u32 = 42;
13628 + const B: u32 = 42;
13629 + const C: u32 = 42;
13630 + const D: u32 = 42;
13631 + const E: u32 = 42;
13632 + ˇ
13633
13634 fn main() {
13635 println!("hello");
13636
13637 println!("world");
13638 }
13639 "#
13640 .unindent(),
13641 );
13642
13643 cx.update_editor(|editor, window, cx| {
13644 editor.delete_line(&DeleteLine, window, cx);
13645 });
13646 executor.run_until_parked();
13647
13648 cx.assert_state_with_diff(
13649 r#"
13650 use some::mod1;
13651 use some::mod2;
13652
13653 const A: u32 = 42;
13654 + const B: u32 = 42;
13655 + const C: u32 = 42;
13656 + const D: u32 = 42;
13657 + const E: u32 = 42;
13658 ˇ
13659 fn main() {
13660 println!("hello");
13661
13662 println!("world");
13663 }
13664 "#
13665 .unindent(),
13666 );
13667
13668 cx.update_editor(|editor, window, cx| {
13669 editor.move_up(&MoveUp, window, cx);
13670 editor.delete_line(&DeleteLine, window, cx);
13671 editor.move_up(&MoveUp, window, cx);
13672 editor.delete_line(&DeleteLine, window, cx);
13673 editor.move_up(&MoveUp, window, cx);
13674 editor.delete_line(&DeleteLine, window, cx);
13675 });
13676 executor.run_until_parked();
13677 cx.assert_state_with_diff(
13678 r#"
13679 use some::mod1;
13680 use some::mod2;
13681
13682 const A: u32 = 42;
13683 + const B: u32 = 42;
13684 ˇ
13685 fn main() {
13686 println!("hello");
13687
13688 println!("world");
13689 }
13690 "#
13691 .unindent(),
13692 );
13693
13694 cx.update_editor(|editor, window, cx| {
13695 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
13696 editor.delete_line(&DeleteLine, window, cx);
13697 });
13698 executor.run_until_parked();
13699 cx.assert_state_with_diff(
13700 r#"
13701 ˇ
13702 fn main() {
13703 println!("hello");
13704
13705 println!("world");
13706 }
13707 "#
13708 .unindent(),
13709 );
13710}
13711
13712#[gpui::test]
13713async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
13714 init_test(cx, |_| {});
13715
13716 let mut cx = EditorTestContext::new(cx).await;
13717 cx.set_diff_base(indoc! { "
13718 one
13719 two
13720 three
13721 four
13722 five
13723 "
13724 });
13725 cx.set_state(indoc! { "
13726 one
13727 ˇthree
13728 five
13729 "});
13730 cx.run_until_parked();
13731 cx.update_editor(|editor, window, cx| {
13732 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13733 });
13734 cx.assert_state_with_diff(
13735 indoc! { "
13736 one
13737 - two
13738 ˇthree
13739 - four
13740 five
13741 "}
13742 .to_string(),
13743 );
13744 cx.update_editor(|editor, window, cx| {
13745 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13746 });
13747
13748 cx.assert_state_with_diff(
13749 indoc! { "
13750 one
13751 ˇthree
13752 five
13753 "}
13754 .to_string(),
13755 );
13756
13757 cx.set_state(indoc! { "
13758 one
13759 ˇTWO
13760 three
13761 four
13762 five
13763 "});
13764 cx.run_until_parked();
13765 cx.update_editor(|editor, window, cx| {
13766 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13767 });
13768
13769 cx.assert_state_with_diff(
13770 indoc! { "
13771 one
13772 - two
13773 + ˇTWO
13774 three
13775 four
13776 five
13777 "}
13778 .to_string(),
13779 );
13780 cx.update_editor(|editor, window, cx| {
13781 editor.move_up(&Default::default(), window, cx);
13782 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13783 });
13784 cx.assert_state_with_diff(
13785 indoc! { "
13786 one
13787 ˇTWO
13788 three
13789 four
13790 five
13791 "}
13792 .to_string(),
13793 );
13794}
13795
13796#[gpui::test]
13797async fn test_edits_around_expanded_deletion_hunks(
13798 executor: BackgroundExecutor,
13799 cx: &mut gpui::TestAppContext,
13800) {
13801 init_test(cx, |_| {});
13802
13803 let mut cx = EditorTestContext::new(cx).await;
13804
13805 let diff_base = r#"
13806 use some::mod1;
13807 use some::mod2;
13808
13809 const A: u32 = 42;
13810 const B: u32 = 42;
13811 const C: u32 = 42;
13812
13813
13814 fn main() {
13815 println!("hello");
13816
13817 println!("world");
13818 }
13819 "#
13820 .unindent();
13821 executor.run_until_parked();
13822 cx.set_state(
13823 &r#"
13824 use some::mod1;
13825 use some::mod2;
13826
13827 ˇconst B: u32 = 42;
13828 const C: u32 = 42;
13829
13830
13831 fn main() {
13832 println!("hello");
13833
13834 println!("world");
13835 }
13836 "#
13837 .unindent(),
13838 );
13839
13840 cx.set_diff_base(&diff_base);
13841 executor.run_until_parked();
13842
13843 cx.update_editor(|editor, window, cx| {
13844 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13845 });
13846 executor.run_until_parked();
13847
13848 cx.assert_state_with_diff(
13849 r#"
13850 use some::mod1;
13851 use some::mod2;
13852
13853 - const A: u32 = 42;
13854 ˇconst B: u32 = 42;
13855 const C: u32 = 42;
13856
13857
13858 fn main() {
13859 println!("hello");
13860
13861 println!("world");
13862 }
13863 "#
13864 .unindent(),
13865 );
13866
13867 cx.update_editor(|editor, window, cx| {
13868 editor.delete_line(&DeleteLine, window, cx);
13869 });
13870 executor.run_until_parked();
13871 cx.assert_state_with_diff(
13872 r#"
13873 use some::mod1;
13874 use some::mod2;
13875
13876 - const A: u32 = 42;
13877 - const B: u32 = 42;
13878 ˇconst C: u32 = 42;
13879
13880
13881 fn main() {
13882 println!("hello");
13883
13884 println!("world");
13885 }
13886 "#
13887 .unindent(),
13888 );
13889
13890 cx.update_editor(|editor, window, cx| {
13891 editor.delete_line(&DeleteLine, window, cx);
13892 });
13893 executor.run_until_parked();
13894 cx.assert_state_with_diff(
13895 r#"
13896 use some::mod1;
13897 use some::mod2;
13898
13899 - const A: u32 = 42;
13900 - const B: u32 = 42;
13901 - const C: u32 = 42;
13902 ˇ
13903
13904 fn main() {
13905 println!("hello");
13906
13907 println!("world");
13908 }
13909 "#
13910 .unindent(),
13911 );
13912
13913 cx.update_editor(|editor, window, cx| {
13914 editor.handle_input("replacement", window, cx);
13915 });
13916 executor.run_until_parked();
13917 cx.assert_state_with_diff(
13918 r#"
13919 use some::mod1;
13920 use some::mod2;
13921
13922 - const A: u32 = 42;
13923 - const B: u32 = 42;
13924 - const C: u32 = 42;
13925 -
13926 + replacementˇ
13927
13928 fn main() {
13929 println!("hello");
13930
13931 println!("world");
13932 }
13933 "#
13934 .unindent(),
13935 );
13936}
13937
13938#[gpui::test]
13939async fn test_backspace_after_deletion_hunk(
13940 executor: BackgroundExecutor,
13941 cx: &mut gpui::TestAppContext,
13942) {
13943 init_test(cx, |_| {});
13944
13945 let mut cx = EditorTestContext::new(cx).await;
13946
13947 let base_text = r#"
13948 one
13949 two
13950 three
13951 four
13952 five
13953 "#
13954 .unindent();
13955 executor.run_until_parked();
13956 cx.set_state(
13957 &r#"
13958 one
13959 two
13960 fˇour
13961 five
13962 "#
13963 .unindent(),
13964 );
13965
13966 cx.set_diff_base(&base_text);
13967 executor.run_until_parked();
13968
13969 cx.update_editor(|editor, window, cx| {
13970 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13971 });
13972 executor.run_until_parked();
13973
13974 cx.assert_state_with_diff(
13975 r#"
13976 one
13977 two
13978 - three
13979 fˇour
13980 five
13981 "#
13982 .unindent(),
13983 );
13984
13985 cx.update_editor(|editor, window, cx| {
13986 editor.backspace(&Backspace, window, cx);
13987 editor.backspace(&Backspace, window, cx);
13988 });
13989 executor.run_until_parked();
13990 cx.assert_state_with_diff(
13991 r#"
13992 one
13993 two
13994 - threeˇ
13995 - four
13996 + our
13997 five
13998 "#
13999 .unindent(),
14000 );
14001}
14002
14003#[gpui::test]
14004async fn test_edit_after_expanded_modification_hunk(
14005 executor: BackgroundExecutor,
14006 cx: &mut gpui::TestAppContext,
14007) {
14008 init_test(cx, |_| {});
14009
14010 let mut cx = EditorTestContext::new(cx).await;
14011
14012 let diff_base = r#"
14013 use some::mod1;
14014 use some::mod2;
14015
14016 const A: u32 = 42;
14017 const B: u32 = 42;
14018 const C: u32 = 42;
14019 const D: u32 = 42;
14020
14021
14022 fn main() {
14023 println!("hello");
14024
14025 println!("world");
14026 }"#
14027 .unindent();
14028
14029 cx.set_state(
14030 &r#"
14031 use some::mod1;
14032 use some::mod2;
14033
14034 const A: u32 = 42;
14035 const B: u32 = 42;
14036 const C: u32 = 43ˇ
14037 const D: u32 = 42;
14038
14039
14040 fn main() {
14041 println!("hello");
14042
14043 println!("world");
14044 }"#
14045 .unindent(),
14046 );
14047
14048 cx.set_diff_base(&diff_base);
14049 executor.run_until_parked();
14050 cx.update_editor(|editor, window, cx| {
14051 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
14052 });
14053 executor.run_until_parked();
14054
14055 cx.assert_state_with_diff(
14056 r#"
14057 use some::mod1;
14058 use some::mod2;
14059
14060 const A: u32 = 42;
14061 const B: u32 = 42;
14062 - const C: u32 = 42;
14063 + const C: u32 = 43ˇ
14064 const D: u32 = 42;
14065
14066
14067 fn main() {
14068 println!("hello");
14069
14070 println!("world");
14071 }"#
14072 .unindent(),
14073 );
14074
14075 cx.update_editor(|editor, window, cx| {
14076 editor.handle_input("\nnew_line\n", window, cx);
14077 });
14078 executor.run_until_parked();
14079
14080 cx.assert_state_with_diff(
14081 r#"
14082 use some::mod1;
14083 use some::mod2;
14084
14085 const A: u32 = 42;
14086 const B: u32 = 42;
14087 - const C: u32 = 42;
14088 + const C: u32 = 43
14089 + new_line
14090 + ˇ
14091 const D: u32 = 42;
14092
14093
14094 fn main() {
14095 println!("hello");
14096
14097 println!("world");
14098 }"#
14099 .unindent(),
14100 );
14101}
14102
14103#[gpui::test]
14104async fn test_stage_and_unstage_added_file_hunk(
14105 executor: BackgroundExecutor,
14106 cx: &mut gpui::TestAppContext,
14107) {
14108 init_test(cx, |_| {});
14109
14110 let mut cx = EditorTestContext::new(cx).await;
14111 cx.update_editor(|editor, _, cx| {
14112 editor.set_expand_all_diff_hunks(cx);
14113 });
14114
14115 let working_copy = r#"
14116 ˇfn main() {
14117 println!("hello, world!");
14118 }
14119 "#
14120 .unindent();
14121
14122 cx.set_state(&working_copy);
14123 executor.run_until_parked();
14124
14125 cx.assert_state_with_diff(
14126 r#"
14127 + ˇfn main() {
14128 + println!("hello, world!");
14129 + }
14130 "#
14131 .unindent(),
14132 );
14133 cx.assert_index_text(None);
14134
14135 cx.update_editor(|editor, window, cx| {
14136 editor.toggle_staged_selected_diff_hunks(&ToggleStagedSelectedDiffHunks, window, cx);
14137 });
14138 executor.run_until_parked();
14139 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
14140 cx.assert_state_with_diff(
14141 r#"
14142 + ˇfn main() {
14143 + println!("hello, world!");
14144 + }
14145 "#
14146 .unindent(),
14147 );
14148
14149 cx.update_editor(|editor, window, cx| {
14150 editor.toggle_staged_selected_diff_hunks(&ToggleStagedSelectedDiffHunks, window, cx);
14151 });
14152 executor.run_until_parked();
14153 cx.assert_index_text(None);
14154}
14155
14156async fn setup_indent_guides_editor(
14157 text: &str,
14158 cx: &mut gpui::TestAppContext,
14159) -> (BufferId, EditorTestContext) {
14160 init_test(cx, |_| {});
14161
14162 let mut cx = EditorTestContext::new(cx).await;
14163
14164 let buffer_id = cx.update_editor(|editor, window, cx| {
14165 editor.set_text(text, window, cx);
14166 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
14167
14168 buffer_ids[0]
14169 });
14170
14171 (buffer_id, cx)
14172}
14173
14174fn assert_indent_guides(
14175 range: Range<u32>,
14176 expected: Vec<IndentGuide>,
14177 active_indices: Option<Vec<usize>>,
14178 cx: &mut EditorTestContext,
14179) {
14180 let indent_guides = cx.update_editor(|editor, window, cx| {
14181 let snapshot = editor.snapshot(window, cx).display_snapshot;
14182 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
14183 editor,
14184 MultiBufferRow(range.start)..MultiBufferRow(range.end),
14185 true,
14186 &snapshot,
14187 cx,
14188 );
14189
14190 indent_guides.sort_by(|a, b| {
14191 a.depth.cmp(&b.depth).then(
14192 a.start_row
14193 .cmp(&b.start_row)
14194 .then(a.end_row.cmp(&b.end_row)),
14195 )
14196 });
14197 indent_guides
14198 });
14199
14200 if let Some(expected) = active_indices {
14201 let active_indices = cx.update_editor(|editor, window, cx| {
14202 let snapshot = editor.snapshot(window, cx).display_snapshot;
14203 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
14204 });
14205
14206 assert_eq!(
14207 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
14208 expected,
14209 "Active indent guide indices do not match"
14210 );
14211 }
14212
14213 assert_eq!(indent_guides, expected, "Indent guides do not match");
14214}
14215
14216fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
14217 IndentGuide {
14218 buffer_id,
14219 start_row: MultiBufferRow(start_row),
14220 end_row: MultiBufferRow(end_row),
14221 depth,
14222 tab_size: 4,
14223 settings: IndentGuideSettings {
14224 enabled: true,
14225 line_width: 1,
14226 active_line_width: 1,
14227 ..Default::default()
14228 },
14229 }
14230}
14231
14232#[gpui::test]
14233async fn test_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
14234 let (buffer_id, mut cx) = setup_indent_guides_editor(
14235 &"
14236 fn main() {
14237 let a = 1;
14238 }"
14239 .unindent(),
14240 cx,
14241 )
14242 .await;
14243
14244 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
14245}
14246
14247#[gpui::test]
14248async fn test_indent_guide_simple_block(cx: &mut gpui::TestAppContext) {
14249 let (buffer_id, mut cx) = setup_indent_guides_editor(
14250 &"
14251 fn main() {
14252 let a = 1;
14253 let b = 2;
14254 }"
14255 .unindent(),
14256 cx,
14257 )
14258 .await;
14259
14260 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
14261}
14262
14263#[gpui::test]
14264async fn test_indent_guide_nested(cx: &mut gpui::TestAppContext) {
14265 let (buffer_id, mut cx) = setup_indent_guides_editor(
14266 &"
14267 fn main() {
14268 let a = 1;
14269 if a == 3 {
14270 let b = 2;
14271 } else {
14272 let c = 3;
14273 }
14274 }"
14275 .unindent(),
14276 cx,
14277 )
14278 .await;
14279
14280 assert_indent_guides(
14281 0..8,
14282 vec![
14283 indent_guide(buffer_id, 1, 6, 0),
14284 indent_guide(buffer_id, 3, 3, 1),
14285 indent_guide(buffer_id, 5, 5, 1),
14286 ],
14287 None,
14288 &mut cx,
14289 );
14290}
14291
14292#[gpui::test]
14293async fn test_indent_guide_tab(cx: &mut gpui::TestAppContext) {
14294 let (buffer_id, mut cx) = setup_indent_guides_editor(
14295 &"
14296 fn main() {
14297 let a = 1;
14298 let b = 2;
14299 let c = 3;
14300 }"
14301 .unindent(),
14302 cx,
14303 )
14304 .await;
14305
14306 assert_indent_guides(
14307 0..5,
14308 vec![
14309 indent_guide(buffer_id, 1, 3, 0),
14310 indent_guide(buffer_id, 2, 2, 1),
14311 ],
14312 None,
14313 &mut cx,
14314 );
14315}
14316
14317#[gpui::test]
14318async fn test_indent_guide_continues_on_empty_line(cx: &mut gpui::TestAppContext) {
14319 let (buffer_id, mut cx) = setup_indent_guides_editor(
14320 &"
14321 fn main() {
14322 let a = 1;
14323
14324 let c = 3;
14325 }"
14326 .unindent(),
14327 cx,
14328 )
14329 .await;
14330
14331 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
14332}
14333
14334#[gpui::test]
14335async fn test_indent_guide_complex(cx: &mut gpui::TestAppContext) {
14336 let (buffer_id, mut cx) = setup_indent_guides_editor(
14337 &"
14338 fn main() {
14339 let a = 1;
14340
14341 let c = 3;
14342
14343 if a == 3 {
14344 let b = 2;
14345 } else {
14346 let c = 3;
14347 }
14348 }"
14349 .unindent(),
14350 cx,
14351 )
14352 .await;
14353
14354 assert_indent_guides(
14355 0..11,
14356 vec![
14357 indent_guide(buffer_id, 1, 9, 0),
14358 indent_guide(buffer_id, 6, 6, 1),
14359 indent_guide(buffer_id, 8, 8, 1),
14360 ],
14361 None,
14362 &mut cx,
14363 );
14364}
14365
14366#[gpui::test]
14367async fn test_indent_guide_starts_off_screen(cx: &mut gpui::TestAppContext) {
14368 let (buffer_id, mut cx) = setup_indent_guides_editor(
14369 &"
14370 fn main() {
14371 let a = 1;
14372
14373 let c = 3;
14374
14375 if a == 3 {
14376 let b = 2;
14377 } else {
14378 let c = 3;
14379 }
14380 }"
14381 .unindent(),
14382 cx,
14383 )
14384 .await;
14385
14386 assert_indent_guides(
14387 1..11,
14388 vec![
14389 indent_guide(buffer_id, 1, 9, 0),
14390 indent_guide(buffer_id, 6, 6, 1),
14391 indent_guide(buffer_id, 8, 8, 1),
14392 ],
14393 None,
14394 &mut cx,
14395 );
14396}
14397
14398#[gpui::test]
14399async fn test_indent_guide_ends_off_screen(cx: &mut gpui::TestAppContext) {
14400 let (buffer_id, mut cx) = setup_indent_guides_editor(
14401 &"
14402 fn main() {
14403 let a = 1;
14404
14405 let c = 3;
14406
14407 if a == 3 {
14408 let b = 2;
14409 } else {
14410 let c = 3;
14411 }
14412 }"
14413 .unindent(),
14414 cx,
14415 )
14416 .await;
14417
14418 assert_indent_guides(
14419 1..10,
14420 vec![
14421 indent_guide(buffer_id, 1, 9, 0),
14422 indent_guide(buffer_id, 6, 6, 1),
14423 indent_guide(buffer_id, 8, 8, 1),
14424 ],
14425 None,
14426 &mut cx,
14427 );
14428}
14429
14430#[gpui::test]
14431async fn test_indent_guide_without_brackets(cx: &mut gpui::TestAppContext) {
14432 let (buffer_id, mut cx) = setup_indent_guides_editor(
14433 &"
14434 block1
14435 block2
14436 block3
14437 block4
14438 block2
14439 block1
14440 block1"
14441 .unindent(),
14442 cx,
14443 )
14444 .await;
14445
14446 assert_indent_guides(
14447 1..10,
14448 vec![
14449 indent_guide(buffer_id, 1, 4, 0),
14450 indent_guide(buffer_id, 2, 3, 1),
14451 indent_guide(buffer_id, 3, 3, 2),
14452 ],
14453 None,
14454 &mut cx,
14455 );
14456}
14457
14458#[gpui::test]
14459async fn test_indent_guide_ends_before_empty_line(cx: &mut gpui::TestAppContext) {
14460 let (buffer_id, mut cx) = setup_indent_guides_editor(
14461 &"
14462 block1
14463 block2
14464 block3
14465
14466 block1
14467 block1"
14468 .unindent(),
14469 cx,
14470 )
14471 .await;
14472
14473 assert_indent_guides(
14474 0..6,
14475 vec![
14476 indent_guide(buffer_id, 1, 2, 0),
14477 indent_guide(buffer_id, 2, 2, 1),
14478 ],
14479 None,
14480 &mut cx,
14481 );
14482}
14483
14484#[gpui::test]
14485async fn test_indent_guide_continuing_off_screen(cx: &mut gpui::TestAppContext) {
14486 let (buffer_id, mut cx) = setup_indent_guides_editor(
14487 &"
14488 block1
14489
14490
14491
14492 block2
14493 "
14494 .unindent(),
14495 cx,
14496 )
14497 .await;
14498
14499 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
14500}
14501
14502#[gpui::test]
14503async fn test_indent_guide_tabs(cx: &mut gpui::TestAppContext) {
14504 let (buffer_id, mut cx) = setup_indent_guides_editor(
14505 &"
14506 def a:
14507 \tb = 3
14508 \tif True:
14509 \t\tc = 4
14510 \t\td = 5
14511 \tprint(b)
14512 "
14513 .unindent(),
14514 cx,
14515 )
14516 .await;
14517
14518 assert_indent_guides(
14519 0..6,
14520 vec![
14521 indent_guide(buffer_id, 1, 6, 0),
14522 indent_guide(buffer_id, 3, 4, 1),
14523 ],
14524 None,
14525 &mut cx,
14526 );
14527}
14528
14529#[gpui::test]
14530async fn test_active_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
14531 let (buffer_id, mut cx) = setup_indent_guides_editor(
14532 &"
14533 fn main() {
14534 let a = 1;
14535 }"
14536 .unindent(),
14537 cx,
14538 )
14539 .await;
14540
14541 cx.update_editor(|editor, window, cx| {
14542 editor.change_selections(None, window, cx, |s| {
14543 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14544 });
14545 });
14546
14547 assert_indent_guides(
14548 0..3,
14549 vec![indent_guide(buffer_id, 1, 1, 0)],
14550 Some(vec![0]),
14551 &mut cx,
14552 );
14553}
14554
14555#[gpui::test]
14556async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppContext) {
14557 let (buffer_id, mut cx) = setup_indent_guides_editor(
14558 &"
14559 fn main() {
14560 if 1 == 2 {
14561 let a = 1;
14562 }
14563 }"
14564 .unindent(),
14565 cx,
14566 )
14567 .await;
14568
14569 cx.update_editor(|editor, window, cx| {
14570 editor.change_selections(None, window, cx, |s| {
14571 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14572 });
14573 });
14574
14575 assert_indent_guides(
14576 0..4,
14577 vec![
14578 indent_guide(buffer_id, 1, 3, 0),
14579 indent_guide(buffer_id, 2, 2, 1),
14580 ],
14581 Some(vec![1]),
14582 &mut cx,
14583 );
14584
14585 cx.update_editor(|editor, window, cx| {
14586 editor.change_selections(None, window, cx, |s| {
14587 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
14588 });
14589 });
14590
14591 assert_indent_guides(
14592 0..4,
14593 vec![
14594 indent_guide(buffer_id, 1, 3, 0),
14595 indent_guide(buffer_id, 2, 2, 1),
14596 ],
14597 Some(vec![1]),
14598 &mut cx,
14599 );
14600
14601 cx.update_editor(|editor, window, cx| {
14602 editor.change_selections(None, window, cx, |s| {
14603 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
14604 });
14605 });
14606
14607 assert_indent_guides(
14608 0..4,
14609 vec![
14610 indent_guide(buffer_id, 1, 3, 0),
14611 indent_guide(buffer_id, 2, 2, 1),
14612 ],
14613 Some(vec![0]),
14614 &mut cx,
14615 );
14616}
14617
14618#[gpui::test]
14619async fn test_active_indent_guide_empty_line(cx: &mut gpui::TestAppContext) {
14620 let (buffer_id, mut cx) = setup_indent_guides_editor(
14621 &"
14622 fn main() {
14623 let a = 1;
14624
14625 let b = 2;
14626 }"
14627 .unindent(),
14628 cx,
14629 )
14630 .await;
14631
14632 cx.update_editor(|editor, window, cx| {
14633 editor.change_selections(None, window, cx, |s| {
14634 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
14635 });
14636 });
14637
14638 assert_indent_guides(
14639 0..5,
14640 vec![indent_guide(buffer_id, 1, 3, 0)],
14641 Some(vec![0]),
14642 &mut cx,
14643 );
14644}
14645
14646#[gpui::test]
14647async fn test_active_indent_guide_non_matching_indent(cx: &mut gpui::TestAppContext) {
14648 let (buffer_id, mut cx) = setup_indent_guides_editor(
14649 &"
14650 def m:
14651 a = 1
14652 pass"
14653 .unindent(),
14654 cx,
14655 )
14656 .await;
14657
14658 cx.update_editor(|editor, window, cx| {
14659 editor.change_selections(None, window, cx, |s| {
14660 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14661 });
14662 });
14663
14664 assert_indent_guides(
14665 0..3,
14666 vec![indent_guide(buffer_id, 1, 2, 0)],
14667 Some(vec![0]),
14668 &mut cx,
14669 );
14670}
14671
14672#[gpui::test]
14673async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut gpui::TestAppContext) {
14674 init_test(cx, |_| {});
14675 let mut cx = EditorTestContext::new(cx).await;
14676 let text = indoc! {
14677 "
14678 impl A {
14679 fn b() {
14680 0;
14681 3;
14682 5;
14683 6;
14684 7;
14685 }
14686 }
14687 "
14688 };
14689 let base_text = indoc! {
14690 "
14691 impl A {
14692 fn b() {
14693 0;
14694 1;
14695 2;
14696 3;
14697 4;
14698 }
14699 fn c() {
14700 5;
14701 6;
14702 7;
14703 }
14704 }
14705 "
14706 };
14707
14708 cx.update_editor(|editor, window, cx| {
14709 editor.set_text(text, window, cx);
14710
14711 editor.buffer().update(cx, |multibuffer, cx| {
14712 let buffer = multibuffer.as_singleton().unwrap();
14713 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
14714
14715 multibuffer.set_all_diff_hunks_expanded(cx);
14716 multibuffer.add_diff(diff, cx);
14717
14718 buffer.read(cx).remote_id()
14719 })
14720 });
14721 cx.run_until_parked();
14722
14723 cx.assert_state_with_diff(
14724 indoc! { "
14725 impl A {
14726 fn b() {
14727 0;
14728 - 1;
14729 - 2;
14730 3;
14731 - 4;
14732 - }
14733 - fn c() {
14734 5;
14735 6;
14736 7;
14737 }
14738 }
14739 ˇ"
14740 }
14741 .to_string(),
14742 );
14743
14744 let mut actual_guides = cx.update_editor(|editor, window, cx| {
14745 editor
14746 .snapshot(window, cx)
14747 .buffer_snapshot
14748 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
14749 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
14750 .collect::<Vec<_>>()
14751 });
14752 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
14753 assert_eq!(
14754 actual_guides,
14755 vec![
14756 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
14757 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
14758 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
14759 ]
14760 );
14761}
14762
14763#[gpui::test]
14764fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
14765 init_test(cx, |_| {});
14766
14767 let editor = cx.add_window(|window, cx| {
14768 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
14769 build_editor(buffer, window, cx)
14770 });
14771
14772 let render_args = Arc::new(Mutex::new(None));
14773 let snapshot = editor
14774 .update(cx, |editor, window, cx| {
14775 let snapshot = editor.buffer().read(cx).snapshot(cx);
14776 let range =
14777 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
14778
14779 struct RenderArgs {
14780 row: MultiBufferRow,
14781 folded: bool,
14782 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
14783 }
14784
14785 let crease = Crease::inline(
14786 range,
14787 FoldPlaceholder::test(),
14788 {
14789 let toggle_callback = render_args.clone();
14790 move |row, folded, callback, _window, _cx| {
14791 *toggle_callback.lock() = Some(RenderArgs {
14792 row,
14793 folded,
14794 callback,
14795 });
14796 div()
14797 }
14798 },
14799 |_row, _folded, _window, _cx| div(),
14800 );
14801
14802 editor.insert_creases(Some(crease), cx);
14803 let snapshot = editor.snapshot(window, cx);
14804 let _div = snapshot.render_crease_toggle(
14805 MultiBufferRow(1),
14806 false,
14807 cx.entity().clone(),
14808 window,
14809 cx,
14810 );
14811 snapshot
14812 })
14813 .unwrap();
14814
14815 let render_args = render_args.lock().take().unwrap();
14816 assert_eq!(render_args.row, MultiBufferRow(1));
14817 assert!(!render_args.folded);
14818 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
14819
14820 cx.update_window(*editor, |_, window, cx| {
14821 (render_args.callback)(true, window, cx)
14822 })
14823 .unwrap();
14824 let snapshot = editor
14825 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
14826 .unwrap();
14827 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
14828
14829 cx.update_window(*editor, |_, window, cx| {
14830 (render_args.callback)(false, window, cx)
14831 })
14832 .unwrap();
14833 let snapshot = editor
14834 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
14835 .unwrap();
14836 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
14837}
14838
14839#[gpui::test]
14840async fn test_input_text(cx: &mut gpui::TestAppContext) {
14841 init_test(cx, |_| {});
14842 let mut cx = EditorTestContext::new(cx).await;
14843
14844 cx.set_state(
14845 &r#"ˇone
14846 two
14847
14848 three
14849 fourˇ
14850 five
14851
14852 siˇx"#
14853 .unindent(),
14854 );
14855
14856 cx.dispatch_action(HandleInput(String::new()));
14857 cx.assert_editor_state(
14858 &r#"ˇone
14859 two
14860
14861 three
14862 fourˇ
14863 five
14864
14865 siˇx"#
14866 .unindent(),
14867 );
14868
14869 cx.dispatch_action(HandleInput("AAAA".to_string()));
14870 cx.assert_editor_state(
14871 &r#"AAAAˇone
14872 two
14873
14874 three
14875 fourAAAAˇ
14876 five
14877
14878 siAAAAˇx"#
14879 .unindent(),
14880 );
14881}
14882
14883#[gpui::test]
14884async fn test_scroll_cursor_center_top_bottom(cx: &mut gpui::TestAppContext) {
14885 init_test(cx, |_| {});
14886
14887 let mut cx = EditorTestContext::new(cx).await;
14888 cx.set_state(
14889 r#"let foo = 1;
14890let foo = 2;
14891let foo = 3;
14892let fooˇ = 4;
14893let foo = 5;
14894let foo = 6;
14895let foo = 7;
14896let foo = 8;
14897let foo = 9;
14898let foo = 10;
14899let foo = 11;
14900let foo = 12;
14901let foo = 13;
14902let foo = 14;
14903let foo = 15;"#,
14904 );
14905
14906 cx.update_editor(|e, window, cx| {
14907 assert_eq!(
14908 e.next_scroll_position,
14909 NextScrollCursorCenterTopBottom::Center,
14910 "Default next scroll direction is center",
14911 );
14912
14913 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14914 assert_eq!(
14915 e.next_scroll_position,
14916 NextScrollCursorCenterTopBottom::Top,
14917 "After center, next scroll direction should be top",
14918 );
14919
14920 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14921 assert_eq!(
14922 e.next_scroll_position,
14923 NextScrollCursorCenterTopBottom::Bottom,
14924 "After top, next scroll direction should be bottom",
14925 );
14926
14927 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14928 assert_eq!(
14929 e.next_scroll_position,
14930 NextScrollCursorCenterTopBottom::Center,
14931 "After bottom, scrolling should start over",
14932 );
14933
14934 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14935 assert_eq!(
14936 e.next_scroll_position,
14937 NextScrollCursorCenterTopBottom::Top,
14938 "Scrolling continues if retriggered fast enough"
14939 );
14940 });
14941
14942 cx.executor()
14943 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
14944 cx.executor().run_until_parked();
14945 cx.update_editor(|e, _, _| {
14946 assert_eq!(
14947 e.next_scroll_position,
14948 NextScrollCursorCenterTopBottom::Center,
14949 "If scrolling is not triggered fast enough, it should reset"
14950 );
14951 });
14952}
14953
14954#[gpui::test]
14955async fn test_goto_definition_with_find_all_references_fallback(cx: &mut gpui::TestAppContext) {
14956 init_test(cx, |_| {});
14957 let mut cx = EditorLspTestContext::new_rust(
14958 lsp::ServerCapabilities {
14959 definition_provider: Some(lsp::OneOf::Left(true)),
14960 references_provider: Some(lsp::OneOf::Left(true)),
14961 ..lsp::ServerCapabilities::default()
14962 },
14963 cx,
14964 )
14965 .await;
14966
14967 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
14968 let go_to_definition = cx.lsp.handle_request::<lsp::request::GotoDefinition, _, _>(
14969 move |params, _| async move {
14970 if empty_go_to_definition {
14971 Ok(None)
14972 } else {
14973 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
14974 uri: params.text_document_position_params.text_document.uri,
14975 range: lsp::Range::new(lsp::Position::new(4, 3), lsp::Position::new(4, 6)),
14976 })))
14977 }
14978 },
14979 );
14980 let references =
14981 cx.lsp
14982 .handle_request::<lsp::request::References, _, _>(move |params, _| async move {
14983 Ok(Some(vec![lsp::Location {
14984 uri: params.text_document_position.text_document.uri,
14985 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
14986 }]))
14987 });
14988 (go_to_definition, references)
14989 };
14990
14991 cx.set_state(
14992 &r#"fn one() {
14993 let mut a = ˇtwo();
14994 }
14995
14996 fn two() {}"#
14997 .unindent(),
14998 );
14999 set_up_lsp_handlers(false, &mut cx);
15000 let navigated = cx
15001 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
15002 .await
15003 .expect("Failed to navigate to definition");
15004 assert_eq!(
15005 navigated,
15006 Navigated::Yes,
15007 "Should have navigated to definition from the GetDefinition response"
15008 );
15009 cx.assert_editor_state(
15010 &r#"fn one() {
15011 let mut a = two();
15012 }
15013
15014 fn «twoˇ»() {}"#
15015 .unindent(),
15016 );
15017
15018 let editors = cx.update_workspace(|workspace, _, cx| {
15019 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
15020 });
15021 cx.update_editor(|_, _, test_editor_cx| {
15022 assert_eq!(
15023 editors.len(),
15024 1,
15025 "Initially, only one, test, editor should be open in the workspace"
15026 );
15027 assert_eq!(
15028 test_editor_cx.entity(),
15029 editors.last().expect("Asserted len is 1").clone()
15030 );
15031 });
15032
15033 set_up_lsp_handlers(true, &mut cx);
15034 let navigated = cx
15035 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
15036 .await
15037 .expect("Failed to navigate to lookup references");
15038 assert_eq!(
15039 navigated,
15040 Navigated::Yes,
15041 "Should have navigated to references as a fallback after empty GoToDefinition response"
15042 );
15043 // We should not change the selections in the existing file,
15044 // if opening another milti buffer with the references
15045 cx.assert_editor_state(
15046 &r#"fn one() {
15047 let mut a = two();
15048 }
15049
15050 fn «twoˇ»() {}"#
15051 .unindent(),
15052 );
15053 let editors = cx.update_workspace(|workspace, _, cx| {
15054 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
15055 });
15056 cx.update_editor(|_, _, test_editor_cx| {
15057 assert_eq!(
15058 editors.len(),
15059 2,
15060 "After falling back to references search, we open a new editor with the results"
15061 );
15062 let references_fallback_text = editors
15063 .into_iter()
15064 .find(|new_editor| *new_editor != test_editor_cx.entity())
15065 .expect("Should have one non-test editor now")
15066 .read(test_editor_cx)
15067 .text(test_editor_cx);
15068 assert_eq!(
15069 references_fallback_text, "fn one() {\n let mut a = two();\n}",
15070 "Should use the range from the references response and not the GoToDefinition one"
15071 );
15072 });
15073}
15074
15075#[gpui::test]
15076async fn test_find_enclosing_node_with_task(cx: &mut gpui::TestAppContext) {
15077 init_test(cx, |_| {});
15078
15079 let language = Arc::new(Language::new(
15080 LanguageConfig::default(),
15081 Some(tree_sitter_rust::LANGUAGE.into()),
15082 ));
15083
15084 let text = r#"
15085 #[cfg(test)]
15086 mod tests() {
15087 #[test]
15088 fn runnable_1() {
15089 let a = 1;
15090 }
15091
15092 #[test]
15093 fn runnable_2() {
15094 let a = 1;
15095 let b = 2;
15096 }
15097 }
15098 "#
15099 .unindent();
15100
15101 let fs = FakeFs::new(cx.executor());
15102 fs.insert_file("/file.rs", Default::default()).await;
15103
15104 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15105 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15106 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15107 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
15108 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
15109
15110 let editor = cx.new_window_entity(|window, cx| {
15111 Editor::new(
15112 EditorMode::Full,
15113 multi_buffer,
15114 Some(project.clone()),
15115 true,
15116 window,
15117 cx,
15118 )
15119 });
15120
15121 editor.update_in(cx, |editor, window, cx| {
15122 editor.tasks.insert(
15123 (buffer.read(cx).remote_id(), 3),
15124 RunnableTasks {
15125 templates: vec![],
15126 offset: MultiBufferOffset(43),
15127 column: 0,
15128 extra_variables: HashMap::default(),
15129 context_range: BufferOffset(43)..BufferOffset(85),
15130 },
15131 );
15132 editor.tasks.insert(
15133 (buffer.read(cx).remote_id(), 8),
15134 RunnableTasks {
15135 templates: vec![],
15136 offset: MultiBufferOffset(86),
15137 column: 0,
15138 extra_variables: HashMap::default(),
15139 context_range: BufferOffset(86)..BufferOffset(191),
15140 },
15141 );
15142
15143 // Test finding task when cursor is inside function body
15144 editor.change_selections(None, window, cx, |s| {
15145 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
15146 });
15147 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
15148 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
15149
15150 // Test finding task when cursor is on function name
15151 editor.change_selections(None, window, cx, |s| {
15152 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
15153 });
15154 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
15155 assert_eq!(row, 8, "Should find task when cursor is on function name");
15156 });
15157}
15158
15159#[gpui::test]
15160async fn test_multi_buffer_folding(cx: &mut gpui::TestAppContext) {
15161 init_test(cx, |_| {});
15162
15163 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
15164 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
15165 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
15166
15167 let fs = FakeFs::new(cx.executor());
15168 fs.insert_tree(
15169 path!("/a"),
15170 json!({
15171 "first.rs": sample_text_1,
15172 "second.rs": sample_text_2,
15173 "third.rs": sample_text_3,
15174 }),
15175 )
15176 .await;
15177 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15178 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15179 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15180 let worktree = project.update(cx, |project, cx| {
15181 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15182 assert_eq!(worktrees.len(), 1);
15183 worktrees.pop().unwrap()
15184 });
15185 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15186
15187 let buffer_1 = project
15188 .update(cx, |project, cx| {
15189 project.open_buffer((worktree_id, "first.rs"), cx)
15190 })
15191 .await
15192 .unwrap();
15193 let buffer_2 = project
15194 .update(cx, |project, cx| {
15195 project.open_buffer((worktree_id, "second.rs"), cx)
15196 })
15197 .await
15198 .unwrap();
15199 let buffer_3 = project
15200 .update(cx, |project, cx| {
15201 project.open_buffer((worktree_id, "third.rs"), cx)
15202 })
15203 .await
15204 .unwrap();
15205
15206 let multi_buffer = cx.new(|cx| {
15207 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15208 multi_buffer.push_excerpts(
15209 buffer_1.clone(),
15210 [
15211 ExcerptRange {
15212 context: Point::new(0, 0)..Point::new(3, 0),
15213 primary: None,
15214 },
15215 ExcerptRange {
15216 context: Point::new(5, 0)..Point::new(7, 0),
15217 primary: None,
15218 },
15219 ExcerptRange {
15220 context: Point::new(9, 0)..Point::new(10, 4),
15221 primary: None,
15222 },
15223 ],
15224 cx,
15225 );
15226 multi_buffer.push_excerpts(
15227 buffer_2.clone(),
15228 [
15229 ExcerptRange {
15230 context: Point::new(0, 0)..Point::new(3, 0),
15231 primary: None,
15232 },
15233 ExcerptRange {
15234 context: Point::new(5, 0)..Point::new(7, 0),
15235 primary: None,
15236 },
15237 ExcerptRange {
15238 context: Point::new(9, 0)..Point::new(10, 4),
15239 primary: None,
15240 },
15241 ],
15242 cx,
15243 );
15244 multi_buffer.push_excerpts(
15245 buffer_3.clone(),
15246 [
15247 ExcerptRange {
15248 context: Point::new(0, 0)..Point::new(3, 0),
15249 primary: None,
15250 },
15251 ExcerptRange {
15252 context: Point::new(5, 0)..Point::new(7, 0),
15253 primary: None,
15254 },
15255 ExcerptRange {
15256 context: Point::new(9, 0)..Point::new(10, 4),
15257 primary: None,
15258 },
15259 ],
15260 cx,
15261 );
15262 multi_buffer
15263 });
15264 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15265 Editor::new(
15266 EditorMode::Full,
15267 multi_buffer,
15268 Some(project.clone()),
15269 true,
15270 window,
15271 cx,
15272 )
15273 });
15274
15275 let full_text = "\n\n\naaaa\nbbbb\ncccc\n\n\n\nffff\ngggg\n\n\n\njjjj\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n";
15276 assert_eq!(
15277 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15278 full_text,
15279 );
15280
15281 multi_buffer_editor.update(cx, |editor, cx| {
15282 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
15283 });
15284 assert_eq!(
15285 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15286 "\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",
15287 "After folding the first buffer, its text should not be displayed"
15288 );
15289
15290 multi_buffer_editor.update(cx, |editor, cx| {
15291 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
15292 });
15293 assert_eq!(
15294 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15295 "\n\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
15296 "After folding the second buffer, its text should not be displayed"
15297 );
15298
15299 multi_buffer_editor.update(cx, |editor, cx| {
15300 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
15301 });
15302 assert_eq!(
15303 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15304 "\n\n\n\n\n",
15305 "After folding the third buffer, its text should not be displayed"
15306 );
15307
15308 // Emulate selection inside the fold logic, that should work
15309 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15310 editor
15311 .snapshot(window, cx)
15312 .next_line_boundary(Point::new(0, 4));
15313 });
15314
15315 multi_buffer_editor.update(cx, |editor, cx| {
15316 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
15317 });
15318 assert_eq!(
15319 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15320 "\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
15321 "After unfolding the second buffer, its text should be displayed"
15322 );
15323
15324 multi_buffer_editor.update(cx, |editor, cx| {
15325 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
15326 });
15327 assert_eq!(
15328 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15329 "\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",
15330 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
15331 );
15332
15333 multi_buffer_editor.update(cx, |editor, cx| {
15334 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
15335 });
15336 assert_eq!(
15337 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15338 full_text,
15339 "After unfolding the all buffers, all original text should be displayed"
15340 );
15341}
15342
15343#[gpui::test]
15344async fn test_multi_buffer_single_excerpts_folding(cx: &mut gpui::TestAppContext) {
15345 init_test(cx, |_| {});
15346
15347 let sample_text_1 = "1111\n2222\n3333".to_string();
15348 let sample_text_2 = "4444\n5555\n6666".to_string();
15349 let sample_text_3 = "7777\n8888\n9999".to_string();
15350
15351 let fs = FakeFs::new(cx.executor());
15352 fs.insert_tree(
15353 path!("/a"),
15354 json!({
15355 "first.rs": sample_text_1,
15356 "second.rs": sample_text_2,
15357 "third.rs": sample_text_3,
15358 }),
15359 )
15360 .await;
15361 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15362 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15363 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15364 let worktree = project.update(cx, |project, cx| {
15365 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15366 assert_eq!(worktrees.len(), 1);
15367 worktrees.pop().unwrap()
15368 });
15369 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15370
15371 let buffer_1 = project
15372 .update(cx, |project, cx| {
15373 project.open_buffer((worktree_id, "first.rs"), cx)
15374 })
15375 .await
15376 .unwrap();
15377 let buffer_2 = project
15378 .update(cx, |project, cx| {
15379 project.open_buffer((worktree_id, "second.rs"), cx)
15380 })
15381 .await
15382 .unwrap();
15383 let buffer_3 = project
15384 .update(cx, |project, cx| {
15385 project.open_buffer((worktree_id, "third.rs"), cx)
15386 })
15387 .await
15388 .unwrap();
15389
15390 let multi_buffer = cx.new(|cx| {
15391 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15392 multi_buffer.push_excerpts(
15393 buffer_1.clone(),
15394 [ExcerptRange {
15395 context: Point::new(0, 0)..Point::new(3, 0),
15396 primary: None,
15397 }],
15398 cx,
15399 );
15400 multi_buffer.push_excerpts(
15401 buffer_2.clone(),
15402 [ExcerptRange {
15403 context: Point::new(0, 0)..Point::new(3, 0),
15404 primary: None,
15405 }],
15406 cx,
15407 );
15408 multi_buffer.push_excerpts(
15409 buffer_3.clone(),
15410 [ExcerptRange {
15411 context: Point::new(0, 0)..Point::new(3, 0),
15412 primary: None,
15413 }],
15414 cx,
15415 );
15416 multi_buffer
15417 });
15418
15419 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15420 Editor::new(
15421 EditorMode::Full,
15422 multi_buffer,
15423 Some(project.clone()),
15424 true,
15425 window,
15426 cx,
15427 )
15428 });
15429
15430 let full_text = "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n";
15431 assert_eq!(
15432 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15433 full_text,
15434 );
15435
15436 multi_buffer_editor.update(cx, |editor, cx| {
15437 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
15438 });
15439 assert_eq!(
15440 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15441 "\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n",
15442 "After folding the first buffer, its text should not be displayed"
15443 );
15444
15445 multi_buffer_editor.update(cx, |editor, cx| {
15446 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
15447 });
15448
15449 assert_eq!(
15450 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15451 "\n\n\n\n\n\n\n7777\n8888\n9999\n",
15452 "After folding the second buffer, its text should not be displayed"
15453 );
15454
15455 multi_buffer_editor.update(cx, |editor, cx| {
15456 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
15457 });
15458 assert_eq!(
15459 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15460 "\n\n\n\n\n",
15461 "After folding the third buffer, its text should not be displayed"
15462 );
15463
15464 multi_buffer_editor.update(cx, |editor, cx| {
15465 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
15466 });
15467 assert_eq!(
15468 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15469 "\n\n\n\n\n4444\n5555\n6666\n\n\n",
15470 "After unfolding the second buffer, its text should be displayed"
15471 );
15472
15473 multi_buffer_editor.update(cx, |editor, cx| {
15474 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
15475 });
15476 assert_eq!(
15477 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15478 "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n",
15479 "After unfolding the first buffer, its text should be displayed"
15480 );
15481
15482 multi_buffer_editor.update(cx, |editor, cx| {
15483 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
15484 });
15485 assert_eq!(
15486 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15487 full_text,
15488 "After unfolding all buffers, all original text should be displayed"
15489 );
15490}
15491
15492#[gpui::test]
15493async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppContext) {
15494 init_test(cx, |_| {});
15495
15496 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
15497
15498 let fs = FakeFs::new(cx.executor());
15499 fs.insert_tree(
15500 path!("/a"),
15501 json!({
15502 "main.rs": sample_text,
15503 }),
15504 )
15505 .await;
15506 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15507 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15508 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15509 let worktree = project.update(cx, |project, cx| {
15510 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15511 assert_eq!(worktrees.len(), 1);
15512 worktrees.pop().unwrap()
15513 });
15514 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15515
15516 let buffer_1 = project
15517 .update(cx, |project, cx| {
15518 project.open_buffer((worktree_id, "main.rs"), cx)
15519 })
15520 .await
15521 .unwrap();
15522
15523 let multi_buffer = cx.new(|cx| {
15524 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15525 multi_buffer.push_excerpts(
15526 buffer_1.clone(),
15527 [ExcerptRange {
15528 context: Point::new(0, 0)
15529 ..Point::new(
15530 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
15531 0,
15532 ),
15533 primary: None,
15534 }],
15535 cx,
15536 );
15537 multi_buffer
15538 });
15539 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15540 Editor::new(
15541 EditorMode::Full,
15542 multi_buffer,
15543 Some(project.clone()),
15544 true,
15545 window,
15546 cx,
15547 )
15548 });
15549
15550 let selection_range = Point::new(1, 0)..Point::new(2, 0);
15551 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15552 enum TestHighlight {}
15553 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
15554 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
15555 editor.highlight_text::<TestHighlight>(
15556 vec![highlight_range.clone()],
15557 HighlightStyle::color(Hsla::green()),
15558 cx,
15559 );
15560 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
15561 });
15562
15563 let full_text = format!("\n\n\n{sample_text}\n");
15564 assert_eq!(
15565 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15566 full_text,
15567 );
15568}
15569
15570#[gpui::test]
15571async fn test_inline_completion_text(cx: &mut TestAppContext) {
15572 init_test(cx, |_| {});
15573
15574 // Simple insertion
15575 assert_highlighted_edits(
15576 "Hello, world!",
15577 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
15578 true,
15579 cx,
15580 |highlighted_edits, cx| {
15581 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
15582 assert_eq!(highlighted_edits.highlights.len(), 1);
15583 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
15584 assert_eq!(
15585 highlighted_edits.highlights[0].1.background_color,
15586 Some(cx.theme().status().created_background)
15587 );
15588 },
15589 )
15590 .await;
15591
15592 // Replacement
15593 assert_highlighted_edits(
15594 "This is a test.",
15595 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
15596 false,
15597 cx,
15598 |highlighted_edits, cx| {
15599 assert_eq!(highlighted_edits.text, "That is a test.");
15600 assert_eq!(highlighted_edits.highlights.len(), 1);
15601 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
15602 assert_eq!(
15603 highlighted_edits.highlights[0].1.background_color,
15604 Some(cx.theme().status().created_background)
15605 );
15606 },
15607 )
15608 .await;
15609
15610 // Multiple edits
15611 assert_highlighted_edits(
15612 "Hello, world!",
15613 vec![
15614 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
15615 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
15616 ],
15617 false,
15618 cx,
15619 |highlighted_edits, cx| {
15620 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
15621 assert_eq!(highlighted_edits.highlights.len(), 2);
15622 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
15623 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
15624 assert_eq!(
15625 highlighted_edits.highlights[0].1.background_color,
15626 Some(cx.theme().status().created_background)
15627 );
15628 assert_eq!(
15629 highlighted_edits.highlights[1].1.background_color,
15630 Some(cx.theme().status().created_background)
15631 );
15632 },
15633 )
15634 .await;
15635
15636 // Multiple lines with edits
15637 assert_highlighted_edits(
15638 "First line\nSecond line\nThird line\nFourth line",
15639 vec![
15640 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
15641 (
15642 Point::new(2, 0)..Point::new(2, 10),
15643 "New third line".to_string(),
15644 ),
15645 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
15646 ],
15647 false,
15648 cx,
15649 |highlighted_edits, cx| {
15650 assert_eq!(
15651 highlighted_edits.text,
15652 "Second modified\nNew third line\nFourth updated line"
15653 );
15654 assert_eq!(highlighted_edits.highlights.len(), 3);
15655 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
15656 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
15657 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
15658 for highlight in &highlighted_edits.highlights {
15659 assert_eq!(
15660 highlight.1.background_color,
15661 Some(cx.theme().status().created_background)
15662 );
15663 }
15664 },
15665 )
15666 .await;
15667}
15668
15669#[gpui::test]
15670async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
15671 init_test(cx, |_| {});
15672
15673 // Deletion
15674 assert_highlighted_edits(
15675 "Hello, world!",
15676 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
15677 true,
15678 cx,
15679 |highlighted_edits, cx| {
15680 assert_eq!(highlighted_edits.text, "Hello, world!");
15681 assert_eq!(highlighted_edits.highlights.len(), 1);
15682 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
15683 assert_eq!(
15684 highlighted_edits.highlights[0].1.background_color,
15685 Some(cx.theme().status().deleted_background)
15686 );
15687 },
15688 )
15689 .await;
15690
15691 // Insertion
15692 assert_highlighted_edits(
15693 "Hello, world!",
15694 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
15695 true,
15696 cx,
15697 |highlighted_edits, cx| {
15698 assert_eq!(highlighted_edits.highlights.len(), 1);
15699 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
15700 assert_eq!(
15701 highlighted_edits.highlights[0].1.background_color,
15702 Some(cx.theme().status().created_background)
15703 );
15704 },
15705 )
15706 .await;
15707}
15708
15709async fn assert_highlighted_edits(
15710 text: &str,
15711 edits: Vec<(Range<Point>, String)>,
15712 include_deletions: bool,
15713 cx: &mut TestAppContext,
15714 assertion_fn: impl Fn(HighlightedText, &App),
15715) {
15716 let window = cx.add_window(|window, cx| {
15717 let buffer = MultiBuffer::build_simple(text, cx);
15718 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
15719 });
15720 let cx = &mut VisualTestContext::from_window(*window, cx);
15721
15722 let (buffer, snapshot) = window
15723 .update(cx, |editor, _window, cx| {
15724 (
15725 editor.buffer().clone(),
15726 editor.buffer().read(cx).snapshot(cx),
15727 )
15728 })
15729 .unwrap();
15730
15731 let edits = edits
15732 .into_iter()
15733 .map(|(range, edit)| {
15734 (
15735 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
15736 edit,
15737 )
15738 })
15739 .collect::<Vec<_>>();
15740
15741 let text_anchor_edits = edits
15742 .clone()
15743 .into_iter()
15744 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
15745 .collect::<Vec<_>>();
15746
15747 let edit_preview = window
15748 .update(cx, |_, _window, cx| {
15749 buffer
15750 .read(cx)
15751 .as_singleton()
15752 .unwrap()
15753 .read(cx)
15754 .preview_edits(text_anchor_edits.into(), cx)
15755 })
15756 .unwrap()
15757 .await;
15758
15759 cx.update(|_window, cx| {
15760 let highlighted_edits = inline_completion_edit_text(
15761 &snapshot.as_singleton().unwrap().2,
15762 &edits,
15763 &edit_preview,
15764 include_deletions,
15765 cx,
15766 );
15767 assertion_fn(highlighted_edits, cx)
15768 });
15769}
15770
15771#[gpui::test]
15772async fn test_rename_with_duplicate_edits(cx: &mut gpui::TestAppContext) {
15773 init_test(cx, |_| {});
15774 let capabilities = lsp::ServerCapabilities {
15775 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
15776 prepare_provider: Some(true),
15777 work_done_progress_options: Default::default(),
15778 })),
15779 ..Default::default()
15780 };
15781 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
15782
15783 cx.set_state(indoc! {"
15784 struct Fˇoo {}
15785 "});
15786
15787 cx.update_editor(|editor, _, cx| {
15788 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
15789 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
15790 editor.highlight_background::<DocumentHighlightRead>(
15791 &[highlight_range],
15792 |c| c.editor_document_highlight_read_background,
15793 cx,
15794 );
15795 });
15796
15797 let mut prepare_rename_handler =
15798 cx.handle_request::<lsp::request::PrepareRenameRequest, _, _>(move |_, _, _| async move {
15799 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
15800 start: lsp::Position {
15801 line: 0,
15802 character: 7,
15803 },
15804 end: lsp::Position {
15805 line: 0,
15806 character: 10,
15807 },
15808 })))
15809 });
15810 let prepare_rename_task = cx
15811 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
15812 .expect("Prepare rename was not started");
15813 prepare_rename_handler.next().await.unwrap();
15814 prepare_rename_task.await.expect("Prepare rename failed");
15815
15816 let mut rename_handler =
15817 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
15818 let edit = lsp::TextEdit {
15819 range: lsp::Range {
15820 start: lsp::Position {
15821 line: 0,
15822 character: 7,
15823 },
15824 end: lsp::Position {
15825 line: 0,
15826 character: 10,
15827 },
15828 },
15829 new_text: "FooRenamed".to_string(),
15830 };
15831 Ok(Some(lsp::WorkspaceEdit::new(
15832 // Specify the same edit twice
15833 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
15834 )))
15835 });
15836 let rename_task = cx
15837 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
15838 .expect("Confirm rename was not started");
15839 rename_handler.next().await.unwrap();
15840 rename_task.await.expect("Confirm rename failed");
15841 cx.run_until_parked();
15842
15843 // Despite two edits, only one is actually applied as those are identical
15844 cx.assert_editor_state(indoc! {"
15845 struct FooRenamedˇ {}
15846 "});
15847}
15848
15849#[gpui::test]
15850async fn test_rename_without_prepare(cx: &mut gpui::TestAppContext) {
15851 init_test(cx, |_| {});
15852 // These capabilities indicate that the server does not support prepare rename.
15853 let capabilities = lsp::ServerCapabilities {
15854 rename_provider: Some(lsp::OneOf::Left(true)),
15855 ..Default::default()
15856 };
15857 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
15858
15859 cx.set_state(indoc! {"
15860 struct Fˇoo {}
15861 "});
15862
15863 cx.update_editor(|editor, _window, cx| {
15864 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
15865 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
15866 editor.highlight_background::<DocumentHighlightRead>(
15867 &[highlight_range],
15868 |c| c.editor_document_highlight_read_background,
15869 cx,
15870 );
15871 });
15872
15873 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
15874 .expect("Prepare rename was not started")
15875 .await
15876 .expect("Prepare rename failed");
15877
15878 let mut rename_handler =
15879 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
15880 let edit = lsp::TextEdit {
15881 range: lsp::Range {
15882 start: lsp::Position {
15883 line: 0,
15884 character: 7,
15885 },
15886 end: lsp::Position {
15887 line: 0,
15888 character: 10,
15889 },
15890 },
15891 new_text: "FooRenamed".to_string(),
15892 };
15893 Ok(Some(lsp::WorkspaceEdit::new(
15894 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
15895 )))
15896 });
15897 let rename_task = cx
15898 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
15899 .expect("Confirm rename was not started");
15900 rename_handler.next().await.unwrap();
15901 rename_task.await.expect("Confirm rename failed");
15902 cx.run_until_parked();
15903
15904 // Correct range is renamed, as `surrounding_word` is used to find it.
15905 cx.assert_editor_state(indoc! {"
15906 struct FooRenamedˇ {}
15907 "});
15908}
15909
15910fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
15911 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
15912 point..point
15913}
15914
15915fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
15916 let (text, ranges) = marked_text_ranges(marked_text, true);
15917 assert_eq!(editor.text(cx), text);
15918 assert_eq!(
15919 editor.selections.ranges(cx),
15920 ranges,
15921 "Assert selections are {}",
15922 marked_text
15923 );
15924}
15925
15926pub fn handle_signature_help_request(
15927 cx: &mut EditorLspTestContext,
15928 mocked_response: lsp::SignatureHelp,
15929) -> impl Future<Output = ()> {
15930 let mut request =
15931 cx.handle_request::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
15932 let mocked_response = mocked_response.clone();
15933 async move { Ok(Some(mocked_response)) }
15934 });
15935
15936 async move {
15937 request.next().await;
15938 }
15939}
15940
15941/// Handle completion request passing a marked string specifying where the completion
15942/// should be triggered from using '|' character, what range should be replaced, and what completions
15943/// should be returned using '<' and '>' to delimit the range
15944pub fn handle_completion_request(
15945 cx: &mut EditorLspTestContext,
15946 marked_string: &str,
15947 completions: Vec<&'static str>,
15948 counter: Arc<AtomicUsize>,
15949) -> impl Future<Output = ()> {
15950 let complete_from_marker: TextRangeMarker = '|'.into();
15951 let replace_range_marker: TextRangeMarker = ('<', '>').into();
15952 let (_, mut marked_ranges) = marked_text_ranges_by(
15953 marked_string,
15954 vec![complete_from_marker.clone(), replace_range_marker.clone()],
15955 );
15956
15957 let complete_from_position =
15958 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
15959 let replace_range =
15960 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
15961
15962 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
15963 let completions = completions.clone();
15964 counter.fetch_add(1, atomic::Ordering::Release);
15965 async move {
15966 assert_eq!(params.text_document_position.text_document.uri, url.clone());
15967 assert_eq!(
15968 params.text_document_position.position,
15969 complete_from_position
15970 );
15971 Ok(Some(lsp::CompletionResponse::Array(
15972 completions
15973 .iter()
15974 .map(|completion_text| lsp::CompletionItem {
15975 label: completion_text.to_string(),
15976 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15977 range: replace_range,
15978 new_text: completion_text.to_string(),
15979 })),
15980 ..Default::default()
15981 })
15982 .collect(),
15983 )))
15984 }
15985 });
15986
15987 async move {
15988 request.next().await;
15989 }
15990}
15991
15992fn handle_resolve_completion_request(
15993 cx: &mut EditorLspTestContext,
15994 edits: Option<Vec<(&'static str, &'static str)>>,
15995) -> impl Future<Output = ()> {
15996 let edits = edits.map(|edits| {
15997 edits
15998 .iter()
15999 .map(|(marked_string, new_text)| {
16000 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
16001 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
16002 lsp::TextEdit::new(replace_range, new_text.to_string())
16003 })
16004 .collect::<Vec<_>>()
16005 });
16006
16007 let mut request =
16008 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
16009 let edits = edits.clone();
16010 async move {
16011 Ok(lsp::CompletionItem {
16012 additional_text_edits: edits,
16013 ..Default::default()
16014 })
16015 }
16016 });
16017
16018 async move {
16019 request.next().await;
16020 }
16021}
16022
16023pub(crate) fn update_test_language_settings(
16024 cx: &mut TestAppContext,
16025 f: impl Fn(&mut AllLanguageSettingsContent),
16026) {
16027 cx.update(|cx| {
16028 SettingsStore::update_global(cx, |store, cx| {
16029 store.update_user_settings::<AllLanguageSettings>(cx, f);
16030 });
16031 });
16032}
16033
16034pub(crate) fn update_test_project_settings(
16035 cx: &mut TestAppContext,
16036 f: impl Fn(&mut ProjectSettings),
16037) {
16038 cx.update(|cx| {
16039 SettingsStore::update_global(cx, |store, cx| {
16040 store.update_user_settings::<ProjectSettings>(cx, f);
16041 });
16042 });
16043}
16044
16045pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
16046 cx.update(|cx| {
16047 assets::Assets.load_test_fonts(cx);
16048 let store = SettingsStore::test(cx);
16049 cx.set_global(store);
16050 theme::init(theme::LoadThemes::JustBase, cx);
16051 release_channel::init(SemanticVersion::default(), cx);
16052 client::init_settings(cx);
16053 language::init(cx);
16054 Project::init_settings(cx);
16055 workspace::init_settings(cx);
16056 crate::init(cx);
16057 });
16058
16059 update_test_language_settings(cx, f);
16060}
16061
16062#[track_caller]
16063fn assert_hunk_revert(
16064 not_reverted_text_with_selections: &str,
16065 expected_hunk_statuses_before: Vec<DiffHunkStatus>,
16066 expected_reverted_text_with_selections: &str,
16067 base_text: &str,
16068 cx: &mut EditorLspTestContext,
16069) {
16070 cx.set_state(not_reverted_text_with_selections);
16071 cx.set_diff_base(base_text);
16072 cx.executor().run_until_parked();
16073
16074 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
16075 let snapshot = editor.snapshot(window, cx);
16076 let reverted_hunk_statuses = snapshot
16077 .buffer_snapshot
16078 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
16079 .map(|hunk| hunk.status())
16080 .collect::<Vec<_>>();
16081
16082 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
16083 reverted_hunk_statuses
16084 });
16085 cx.executor().run_until_parked();
16086 cx.assert_editor_state(expected_reverted_text_with_selections);
16087 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
16088}