1use super::*;
2use crate::{
3 scroll::scroll_amount::ScrollAmount,
4 test::{
5 assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
6 editor_test_context::EditorTestContext, select_ranges,
7 },
8 JoinLines,
9};
10use futures::StreamExt;
11use gpui::{
12 div, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
13 WindowBounds, WindowOptions,
14};
15use indoc::indoc;
16use language::{
17 language_settings::{
18 AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent, PrettierSettings,
19 },
20 BracketPairConfig,
21 Capability::ReadWrite,
22 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
23 Override, ParsedMarkdown, Point,
24};
25use language_settings::{Formatter, FormatterList, IndentGuideSettings};
26use multi_buffer::IndentGuide;
27use parking_lot::Mutex;
28use pretty_assertions::{assert_eq, assert_ne};
29use project::{buffer_store::BufferChangeSet, FakeFs};
30use project::{
31 lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT,
32 project_settings::{LspSettings, ProjectSettings},
33};
34use serde_json::{self, json};
35use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
36use std::{
37 iter,
38 sync::atomic::{self, AtomicUsize},
39};
40use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
41use unindent::Unindent;
42use util::{
43 assert_set_eq,
44 test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
45};
46use workspace::{
47 item::{FollowEvent, FollowableItem, Item, ItemHandle},
48 NavigationEntry, ViewId,
49};
50
51#[gpui::test]
52fn test_edit_events(cx: &mut TestAppContext) {
53 init_test(cx, |_| {});
54
55 let buffer = cx.new(|cx| {
56 let mut buffer = language::Buffer::local("123456", cx);
57 buffer.set_group_interval(Duration::from_secs(1));
58 buffer
59 });
60
61 let events = Rc::new(RefCell::new(Vec::new()));
62 let editor1 = cx.add_window({
63 let events = events.clone();
64 |window, cx| {
65 let model = cx.entity().clone();
66 cx.subscribe_in(
67 &model,
68 window,
69 move |_, _, event: &EditorEvent, _, _| match event {
70 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
71 EditorEvent::BufferEdited => {
72 events.borrow_mut().push(("editor1", "buffer edited"))
73 }
74 _ => {}
75 },
76 )
77 .detach();
78 Editor::for_buffer(buffer.clone(), None, window, cx)
79 }
80 });
81
82 let editor2 = cx.add_window({
83 let events = events.clone();
84 |window, cx| {
85 cx.subscribe_in(
86 &cx.entity().clone(),
87 window,
88 move |_, _, event: &EditorEvent, _, _| match event {
89 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
90 EditorEvent::BufferEdited => {
91 events.borrow_mut().push(("editor2", "buffer edited"))
92 }
93 _ => {}
94 },
95 )
96 .detach();
97 Editor::for_buffer(buffer.clone(), None, window, cx)
98 }
99 });
100
101 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
102
103 // Mutating editor 1 will emit an `Edited` event only for that editor.
104 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
105 assert_eq!(
106 mem::take(&mut *events.borrow_mut()),
107 [
108 ("editor1", "edited"),
109 ("editor1", "buffer edited"),
110 ("editor2", "buffer edited"),
111 ]
112 );
113
114 // Mutating editor 2 will emit an `Edited` event only for that editor.
115 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
116 assert_eq!(
117 mem::take(&mut *events.borrow_mut()),
118 [
119 ("editor2", "edited"),
120 ("editor1", "buffer edited"),
121 ("editor2", "buffer edited"),
122 ]
123 );
124
125 // Undoing on editor 1 will emit an `Edited` event only for that editor.
126 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
127 assert_eq!(
128 mem::take(&mut *events.borrow_mut()),
129 [
130 ("editor1", "edited"),
131 ("editor1", "buffer edited"),
132 ("editor2", "buffer edited"),
133 ]
134 );
135
136 // Redoing on editor 1 will emit an `Edited` event only for that editor.
137 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
138 assert_eq!(
139 mem::take(&mut *events.borrow_mut()),
140 [
141 ("editor1", "edited"),
142 ("editor1", "buffer edited"),
143 ("editor2", "buffer edited"),
144 ]
145 );
146
147 // Undoing on editor 2 will emit an `Edited` event only for that editor.
148 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
149 assert_eq!(
150 mem::take(&mut *events.borrow_mut()),
151 [
152 ("editor2", "edited"),
153 ("editor1", "buffer edited"),
154 ("editor2", "buffer edited"),
155 ]
156 );
157
158 // Redoing on editor 2 will emit an `Edited` event only for that editor.
159 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
160 assert_eq!(
161 mem::take(&mut *events.borrow_mut()),
162 [
163 ("editor2", "edited"),
164 ("editor1", "buffer edited"),
165 ("editor2", "buffer edited"),
166 ]
167 );
168
169 // No event is emitted when the mutation is a no-op.
170 _ = editor2.update(cx, |editor, window, cx| {
171 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
172
173 editor.backspace(&Backspace, window, cx);
174 });
175 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
176}
177
178#[gpui::test]
179fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
180 init_test(cx, |_| {});
181
182 let mut now = Instant::now();
183 let group_interval = Duration::from_millis(1);
184 let buffer = cx.new(|cx| {
185 let mut buf = language::Buffer::local("123456", cx);
186 buf.set_group_interval(group_interval);
187 buf
188 });
189 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
190 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
191
192 _ = editor.update(cx, |editor, window, cx| {
193 editor.start_transaction_at(now, window, cx);
194 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
195
196 editor.insert("cd", window, cx);
197 editor.end_transaction_at(now, cx);
198 assert_eq!(editor.text(cx), "12cd56");
199 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
200
201 editor.start_transaction_at(now, window, cx);
202 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
203 editor.insert("e", window, cx);
204 editor.end_transaction_at(now, cx);
205 assert_eq!(editor.text(cx), "12cde6");
206 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
207
208 now += group_interval + Duration::from_millis(1);
209 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
210
211 // Simulate an edit in another editor
212 buffer.update(cx, |buffer, cx| {
213 buffer.start_transaction_at(now, cx);
214 buffer.edit([(0..1, "a")], None, cx);
215 buffer.edit([(1..1, "b")], None, cx);
216 buffer.end_transaction_at(now, cx);
217 });
218
219 assert_eq!(editor.text(cx), "ab2cde6");
220 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
221
222 // Last transaction happened past the group interval in a different editor.
223 // Undo it individually and don't restore selections.
224 editor.undo(&Undo, window, cx);
225 assert_eq!(editor.text(cx), "12cde6");
226 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
227
228 // First two transactions happened within the group interval in this editor.
229 // Undo them together and restore selections.
230 editor.undo(&Undo, window, cx);
231 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
232 assert_eq!(editor.text(cx), "123456");
233 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
234
235 // Redo the first two transactions together.
236 editor.redo(&Redo, window, cx);
237 assert_eq!(editor.text(cx), "12cde6");
238 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
239
240 // Redo the last transaction on its own.
241 editor.redo(&Redo, window, cx);
242 assert_eq!(editor.text(cx), "ab2cde6");
243 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
244
245 // Test empty transactions.
246 editor.start_transaction_at(now, window, cx);
247 editor.end_transaction_at(now, cx);
248 editor.undo(&Undo, window, cx);
249 assert_eq!(editor.text(cx), "12cde6");
250 });
251}
252
253#[gpui::test]
254fn test_ime_composition(cx: &mut TestAppContext) {
255 init_test(cx, |_| {});
256
257 let buffer = cx.new(|cx| {
258 let mut buffer = language::Buffer::local("abcde", cx);
259 // Ensure automatic grouping doesn't occur.
260 buffer.set_group_interval(Duration::ZERO);
261 buffer
262 });
263
264 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
265 cx.add_window(|window, cx| {
266 let mut editor = build_editor(buffer.clone(), window, cx);
267
268 // Start a new IME composition.
269 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
270 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
271 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
272 assert_eq!(editor.text(cx), "äbcde");
273 assert_eq!(
274 editor.marked_text_ranges(cx),
275 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
276 );
277
278 // Finalize IME composition.
279 editor.replace_text_in_range(None, "ā", window, cx);
280 assert_eq!(editor.text(cx), "ābcde");
281 assert_eq!(editor.marked_text_ranges(cx), None);
282
283 // IME composition edits are grouped and are undone/redone at once.
284 editor.undo(&Default::default(), window, cx);
285 assert_eq!(editor.text(cx), "abcde");
286 assert_eq!(editor.marked_text_ranges(cx), None);
287 editor.redo(&Default::default(), window, cx);
288 assert_eq!(editor.text(cx), "ābcde");
289 assert_eq!(editor.marked_text_ranges(cx), None);
290
291 // Start a new IME composition.
292 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
293 assert_eq!(
294 editor.marked_text_ranges(cx),
295 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
296 );
297
298 // Undoing during an IME composition cancels it.
299 editor.undo(&Default::default(), window, cx);
300 assert_eq!(editor.text(cx), "ābcde");
301 assert_eq!(editor.marked_text_ranges(cx), None);
302
303 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
304 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
305 assert_eq!(editor.text(cx), "ābcdè");
306 assert_eq!(
307 editor.marked_text_ranges(cx),
308 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
309 );
310
311 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
312 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
313 assert_eq!(editor.text(cx), "ābcdę");
314 assert_eq!(editor.marked_text_ranges(cx), None);
315
316 // Start a new IME composition with multiple cursors.
317 editor.change_selections(None, window, cx, |s| {
318 s.select_ranges([
319 OffsetUtf16(1)..OffsetUtf16(1),
320 OffsetUtf16(3)..OffsetUtf16(3),
321 OffsetUtf16(5)..OffsetUtf16(5),
322 ])
323 });
324 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
325 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
326 assert_eq!(
327 editor.marked_text_ranges(cx),
328 Some(vec![
329 OffsetUtf16(0)..OffsetUtf16(3),
330 OffsetUtf16(4)..OffsetUtf16(7),
331 OffsetUtf16(8)..OffsetUtf16(11)
332 ])
333 );
334
335 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
336 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
337 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
338 assert_eq!(
339 editor.marked_text_ranges(cx),
340 Some(vec![
341 OffsetUtf16(1)..OffsetUtf16(2),
342 OffsetUtf16(5)..OffsetUtf16(6),
343 OffsetUtf16(9)..OffsetUtf16(10)
344 ])
345 );
346
347 // Finalize IME composition with multiple cursors.
348 editor.replace_text_in_range(Some(9..10), "2", window, cx);
349 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
350 assert_eq!(editor.marked_text_ranges(cx), None);
351
352 editor
353 });
354}
355
356#[gpui::test]
357fn test_selection_with_mouse(cx: &mut TestAppContext) {
358 init_test(cx, |_| {});
359
360 let editor = cx.add_window(|window, cx| {
361 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
362 build_editor(buffer, window, cx)
363 });
364
365 _ = editor.update(cx, |editor, window, cx| {
366 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
367 });
368 assert_eq!(
369 editor
370 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
371 .unwrap(),
372 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
373 );
374
375 _ = editor.update(cx, |editor, window, cx| {
376 editor.update_selection(
377 DisplayPoint::new(DisplayRow(3), 3),
378 0,
379 gpui::Point::<f32>::default(),
380 window,
381 cx,
382 );
383 });
384
385 assert_eq!(
386 editor
387 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
388 .unwrap(),
389 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
390 );
391
392 _ = editor.update(cx, |editor, window, cx| {
393 editor.update_selection(
394 DisplayPoint::new(DisplayRow(1), 1),
395 0,
396 gpui::Point::<f32>::default(),
397 window,
398 cx,
399 );
400 });
401
402 assert_eq!(
403 editor
404 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
405 .unwrap(),
406 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
407 );
408
409 _ = editor.update(cx, |editor, window, cx| {
410 editor.end_selection(window, cx);
411 editor.update_selection(
412 DisplayPoint::new(DisplayRow(3), 3),
413 0,
414 gpui::Point::<f32>::default(),
415 window,
416 cx,
417 );
418 });
419
420 assert_eq!(
421 editor
422 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
423 .unwrap(),
424 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
425 );
426
427 _ = editor.update(cx, |editor, window, cx| {
428 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
429 editor.update_selection(
430 DisplayPoint::new(DisplayRow(0), 0),
431 0,
432 gpui::Point::<f32>::default(),
433 window,
434 cx,
435 );
436 });
437
438 assert_eq!(
439 editor
440 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
441 .unwrap(),
442 [
443 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
444 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
445 ]
446 );
447
448 _ = editor.update(cx, |editor, window, cx| {
449 editor.end_selection(window, cx);
450 });
451
452 assert_eq!(
453 editor
454 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
455 .unwrap(),
456 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
457 );
458}
459
460#[gpui::test]
461fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
462 init_test(cx, |_| {});
463
464 let editor = cx.add_window(|window, cx| {
465 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
466 build_editor(buffer, window, cx)
467 });
468
469 _ = editor.update(cx, |editor, window, cx| {
470 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
471 });
472
473 _ = editor.update(cx, |editor, window, cx| {
474 editor.end_selection(window, cx);
475 });
476
477 _ = editor.update(cx, |editor, window, cx| {
478 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
479 });
480
481 _ = editor.update(cx, |editor, window, cx| {
482 editor.end_selection(window, cx);
483 });
484
485 assert_eq!(
486 editor
487 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
488 .unwrap(),
489 [
490 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
491 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
492 ]
493 );
494
495 _ = editor.update(cx, |editor, window, cx| {
496 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
497 });
498
499 _ = editor.update(cx, |editor, window, cx| {
500 editor.end_selection(window, cx);
501 });
502
503 assert_eq!(
504 editor
505 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
506 .unwrap(),
507 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
508 );
509}
510
511#[gpui::test]
512fn test_canceling_pending_selection(cx: &mut TestAppContext) {
513 init_test(cx, |_| {});
514
515 let editor = cx.add_window(|window, cx| {
516 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
517 build_editor(buffer, window, cx)
518 });
519
520 _ = editor.update(cx, |editor, window, cx| {
521 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
522 assert_eq!(
523 editor.selections.display_ranges(cx),
524 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
525 );
526 });
527
528 _ = editor.update(cx, |editor, window, cx| {
529 editor.update_selection(
530 DisplayPoint::new(DisplayRow(3), 3),
531 0,
532 gpui::Point::<f32>::default(),
533 window,
534 cx,
535 );
536 assert_eq!(
537 editor.selections.display_ranges(cx),
538 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
539 );
540 });
541
542 _ = editor.update(cx, |editor, window, cx| {
543 editor.cancel(&Cancel, window, cx);
544 editor.update_selection(
545 DisplayPoint::new(DisplayRow(1), 1),
546 0,
547 gpui::Point::<f32>::default(),
548 window,
549 cx,
550 );
551 assert_eq!(
552 editor.selections.display_ranges(cx),
553 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
554 );
555 });
556}
557
558#[gpui::test]
559fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
560 init_test(cx, |_| {});
561
562 let editor = cx.add_window(|window, cx| {
563 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
564 build_editor(buffer, window, cx)
565 });
566
567 _ = editor.update(cx, |editor, window, cx| {
568 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
569 assert_eq!(
570 editor.selections.display_ranges(cx),
571 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
572 );
573
574 editor.move_down(&Default::default(), window, cx);
575 assert_eq!(
576 editor.selections.display_ranges(cx),
577 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
578 );
579
580 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
581 assert_eq!(
582 editor.selections.display_ranges(cx),
583 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
584 );
585
586 editor.move_up(&Default::default(), window, cx);
587 assert_eq!(
588 editor.selections.display_ranges(cx),
589 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
590 );
591 });
592}
593
594#[gpui::test]
595fn test_clone(cx: &mut TestAppContext) {
596 init_test(cx, |_| {});
597
598 let (text, selection_ranges) = marked_text_ranges(
599 indoc! {"
600 one
601 two
602 threeˇ
603 four
604 fiveˇ
605 "},
606 true,
607 );
608
609 let editor = cx.add_window(|window, cx| {
610 let buffer = MultiBuffer::build_simple(&text, cx);
611 build_editor(buffer, window, cx)
612 });
613
614 _ = editor.update(cx, |editor, window, cx| {
615 editor.change_selections(None, window, cx, |s| {
616 s.select_ranges(selection_ranges.clone())
617 });
618 editor.fold_creases(
619 vec![
620 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
621 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
622 ],
623 true,
624 window,
625 cx,
626 );
627 });
628
629 let cloned_editor = editor
630 .update(cx, |editor, _, cx| {
631 cx.open_window(Default::default(), |window, cx| {
632 cx.new(|cx| editor.clone(window, cx))
633 })
634 })
635 .unwrap()
636 .unwrap();
637
638 let snapshot = editor
639 .update(cx, |e, window, cx| e.snapshot(window, cx))
640 .unwrap();
641 let cloned_snapshot = cloned_editor
642 .update(cx, |e, window, cx| e.snapshot(window, cx))
643 .unwrap();
644
645 assert_eq!(
646 cloned_editor
647 .update(cx, |e, _, cx| e.display_text(cx))
648 .unwrap(),
649 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
650 );
651 assert_eq!(
652 cloned_snapshot
653 .folds_in_range(0..text.len())
654 .collect::<Vec<_>>(),
655 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
656 );
657 assert_set_eq!(
658 cloned_editor
659 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
660 .unwrap(),
661 editor
662 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
663 .unwrap()
664 );
665 assert_set_eq!(
666 cloned_editor
667 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
668 .unwrap(),
669 editor
670 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
671 .unwrap()
672 );
673}
674
675#[gpui::test]
676async fn test_navigation_history(cx: &mut TestAppContext) {
677 init_test(cx, |_| {});
678
679 use workspace::item::Item;
680
681 let fs = FakeFs::new(cx.executor());
682 let project = Project::test(fs, [], cx).await;
683 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
684 let pane = workspace
685 .update(cx, |workspace, _, _| workspace.active_pane().clone())
686 .unwrap();
687
688 _ = workspace.update(cx, |_v, window, cx| {
689 cx.new(|cx| {
690 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
691 let mut editor = build_editor(buffer.clone(), window, cx);
692 let handle = cx.entity();
693 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
694
695 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
696 editor.nav_history.as_mut().unwrap().pop_backward(cx)
697 }
698
699 // Move the cursor a small distance.
700 // Nothing is added to the navigation history.
701 editor.change_selections(None, window, cx, |s| {
702 s.select_display_ranges([
703 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
704 ])
705 });
706 editor.change_selections(None, window, cx, |s| {
707 s.select_display_ranges([
708 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
709 ])
710 });
711 assert!(pop_history(&mut editor, cx).is_none());
712
713 // Move the cursor a large distance.
714 // The history can jump back to the previous position.
715 editor.change_selections(None, window, cx, |s| {
716 s.select_display_ranges([
717 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
718 ])
719 });
720 let nav_entry = pop_history(&mut editor, cx).unwrap();
721 editor.navigate(nav_entry.data.unwrap(), window, cx);
722 assert_eq!(nav_entry.item.id(), cx.entity_id());
723 assert_eq!(
724 editor.selections.display_ranges(cx),
725 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
726 );
727 assert!(pop_history(&mut editor, cx).is_none());
728
729 // Move the cursor a small distance via the mouse.
730 // Nothing is added to the navigation history.
731 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
732 editor.end_selection(window, cx);
733 assert_eq!(
734 editor.selections.display_ranges(cx),
735 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
736 );
737 assert!(pop_history(&mut editor, cx).is_none());
738
739 // Move the cursor a large distance via the mouse.
740 // The history can jump back to the previous position.
741 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
742 editor.end_selection(window, cx);
743 assert_eq!(
744 editor.selections.display_ranges(cx),
745 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
746 );
747 let nav_entry = pop_history(&mut editor, cx).unwrap();
748 editor.navigate(nav_entry.data.unwrap(), window, cx);
749 assert_eq!(nav_entry.item.id(), cx.entity_id());
750 assert_eq!(
751 editor.selections.display_ranges(cx),
752 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
753 );
754 assert!(pop_history(&mut editor, cx).is_none());
755
756 // Set scroll position to check later
757 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
758 let original_scroll_position = editor.scroll_manager.anchor();
759
760 // Jump to the end of the document and adjust scroll
761 editor.move_to_end(&MoveToEnd, window, cx);
762 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
763 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
764
765 let nav_entry = pop_history(&mut editor, cx).unwrap();
766 editor.navigate(nav_entry.data.unwrap(), window, cx);
767 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
768
769 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
770 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
771 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
772 let invalid_point = Point::new(9999, 0);
773 editor.navigate(
774 Box::new(NavigationData {
775 cursor_anchor: invalid_anchor,
776 cursor_position: invalid_point,
777 scroll_anchor: ScrollAnchor {
778 anchor: invalid_anchor,
779 offset: Default::default(),
780 },
781 scroll_top_row: invalid_point.row,
782 }),
783 window,
784 cx,
785 );
786 assert_eq!(
787 editor.selections.display_ranges(cx),
788 &[editor.max_point(cx)..editor.max_point(cx)]
789 );
790 assert_eq!(
791 editor.scroll_position(cx),
792 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
793 );
794
795 editor
796 })
797 });
798}
799
800#[gpui::test]
801fn test_cancel(cx: &mut TestAppContext) {
802 init_test(cx, |_| {});
803
804 let editor = cx.add_window(|window, cx| {
805 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
806 build_editor(buffer, window, cx)
807 });
808
809 _ = editor.update(cx, |editor, window, cx| {
810 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
811 editor.update_selection(
812 DisplayPoint::new(DisplayRow(1), 1),
813 0,
814 gpui::Point::<f32>::default(),
815 window,
816 cx,
817 );
818 editor.end_selection(window, cx);
819
820 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
821 editor.update_selection(
822 DisplayPoint::new(DisplayRow(0), 3),
823 0,
824 gpui::Point::<f32>::default(),
825 window,
826 cx,
827 );
828 editor.end_selection(window, cx);
829 assert_eq!(
830 editor.selections.display_ranges(cx),
831 [
832 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
833 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
834 ]
835 );
836 });
837
838 _ = editor.update(cx, |editor, window, cx| {
839 editor.cancel(&Cancel, window, cx);
840 assert_eq!(
841 editor.selections.display_ranges(cx),
842 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
843 );
844 });
845
846 _ = editor.update(cx, |editor, window, cx| {
847 editor.cancel(&Cancel, window, cx);
848 assert_eq!(
849 editor.selections.display_ranges(cx),
850 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
851 );
852 });
853}
854
855#[gpui::test]
856fn test_fold_action(cx: &mut TestAppContext) {
857 init_test(cx, |_| {});
858
859 let editor = cx.add_window(|window, cx| {
860 let buffer = MultiBuffer::build_simple(
861 &"
862 impl Foo {
863 // Hello!
864
865 fn a() {
866 1
867 }
868
869 fn b() {
870 2
871 }
872
873 fn c() {
874 3
875 }
876 }
877 "
878 .unindent(),
879 cx,
880 );
881 build_editor(buffer.clone(), window, cx)
882 });
883
884 _ = editor.update(cx, |editor, window, cx| {
885 editor.change_selections(None, window, cx, |s| {
886 s.select_display_ranges([
887 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
888 ]);
889 });
890 editor.fold(&Fold, window, cx);
891 assert_eq!(
892 editor.display_text(cx),
893 "
894 impl Foo {
895 // Hello!
896
897 fn a() {
898 1
899 }
900
901 fn b() {⋯
902 }
903
904 fn c() {⋯
905 }
906 }
907 "
908 .unindent(),
909 );
910
911 editor.fold(&Fold, window, cx);
912 assert_eq!(
913 editor.display_text(cx),
914 "
915 impl Foo {⋯
916 }
917 "
918 .unindent(),
919 );
920
921 editor.unfold_lines(&UnfoldLines, window, cx);
922 assert_eq!(
923 editor.display_text(cx),
924 "
925 impl Foo {
926 // Hello!
927
928 fn a() {
929 1
930 }
931
932 fn b() {⋯
933 }
934
935 fn c() {⋯
936 }
937 }
938 "
939 .unindent(),
940 );
941
942 editor.unfold_lines(&UnfoldLines, window, cx);
943 assert_eq!(
944 editor.display_text(cx),
945 editor.buffer.read(cx).read(cx).text()
946 );
947 });
948}
949
950#[gpui::test]
951fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
952 init_test(cx, |_| {});
953
954 let editor = cx.add_window(|window, cx| {
955 let buffer = MultiBuffer::build_simple(
956 &"
957 class Foo:
958 # Hello!
959
960 def a():
961 print(1)
962
963 def b():
964 print(2)
965
966 def c():
967 print(3)
968 "
969 .unindent(),
970 cx,
971 );
972 build_editor(buffer.clone(), window, cx)
973 });
974
975 _ = editor.update(cx, |editor, window, cx| {
976 editor.change_selections(None, window, cx, |s| {
977 s.select_display_ranges([
978 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
979 ]);
980 });
981 editor.fold(&Fold, window, cx);
982 assert_eq!(
983 editor.display_text(cx),
984 "
985 class Foo:
986 # Hello!
987
988 def a():
989 print(1)
990
991 def b():⋯
992
993 def c():⋯
994 "
995 .unindent(),
996 );
997
998 editor.fold(&Fold, window, cx);
999 assert_eq!(
1000 editor.display_text(cx),
1001 "
1002 class Foo:⋯
1003 "
1004 .unindent(),
1005 );
1006
1007 editor.unfold_lines(&UnfoldLines, window, cx);
1008 assert_eq!(
1009 editor.display_text(cx),
1010 "
1011 class Foo:
1012 # Hello!
1013
1014 def a():
1015 print(1)
1016
1017 def b():⋯
1018
1019 def c():⋯
1020 "
1021 .unindent(),
1022 );
1023
1024 editor.unfold_lines(&UnfoldLines, window, cx);
1025 assert_eq!(
1026 editor.display_text(cx),
1027 editor.buffer.read(cx).read(cx).text()
1028 );
1029 });
1030}
1031
1032#[gpui::test]
1033fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1034 init_test(cx, |_| {});
1035
1036 let editor = cx.add_window(|window, cx| {
1037 let buffer = MultiBuffer::build_simple(
1038 &"
1039 class Foo:
1040 # Hello!
1041
1042 def a():
1043 print(1)
1044
1045 def b():
1046 print(2)
1047
1048
1049 def c():
1050 print(3)
1051
1052
1053 "
1054 .unindent(),
1055 cx,
1056 );
1057 build_editor(buffer.clone(), window, cx)
1058 });
1059
1060 _ = editor.update(cx, |editor, window, cx| {
1061 editor.change_selections(None, window, cx, |s| {
1062 s.select_display_ranges([
1063 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1064 ]);
1065 });
1066 editor.fold(&Fold, window, cx);
1067 assert_eq!(
1068 editor.display_text(cx),
1069 "
1070 class Foo:
1071 # Hello!
1072
1073 def a():
1074 print(1)
1075
1076 def b():⋯
1077
1078
1079 def c():⋯
1080
1081
1082 "
1083 .unindent(),
1084 );
1085
1086 editor.fold(&Fold, window, cx);
1087 assert_eq!(
1088 editor.display_text(cx),
1089 "
1090 class Foo:⋯
1091
1092
1093 "
1094 .unindent(),
1095 );
1096
1097 editor.unfold_lines(&UnfoldLines, window, cx);
1098 assert_eq!(
1099 editor.display_text(cx),
1100 "
1101 class Foo:
1102 # Hello!
1103
1104 def a():
1105 print(1)
1106
1107 def b():⋯
1108
1109
1110 def c():⋯
1111
1112
1113 "
1114 .unindent(),
1115 );
1116
1117 editor.unfold_lines(&UnfoldLines, window, cx);
1118 assert_eq!(
1119 editor.display_text(cx),
1120 editor.buffer.read(cx).read(cx).text()
1121 );
1122 });
1123}
1124
1125#[gpui::test]
1126fn test_fold_at_level(cx: &mut TestAppContext) {
1127 init_test(cx, |_| {});
1128
1129 let editor = cx.add_window(|window, cx| {
1130 let buffer = MultiBuffer::build_simple(
1131 &"
1132 class Foo:
1133 # Hello!
1134
1135 def a():
1136 print(1)
1137
1138 def b():
1139 print(2)
1140
1141
1142 class Bar:
1143 # World!
1144
1145 def a():
1146 print(1)
1147
1148 def b():
1149 print(2)
1150
1151
1152 "
1153 .unindent(),
1154 cx,
1155 );
1156 build_editor(buffer.clone(), window, cx)
1157 });
1158
1159 _ = editor.update(cx, |editor, window, cx| {
1160 editor.fold_at_level(&FoldAtLevel { level: 2 }, window, cx);
1161 assert_eq!(
1162 editor.display_text(cx),
1163 "
1164 class Foo:
1165 # Hello!
1166
1167 def a():⋯
1168
1169 def b():⋯
1170
1171
1172 class Bar:
1173 # World!
1174
1175 def a():⋯
1176
1177 def b():⋯
1178
1179
1180 "
1181 .unindent(),
1182 );
1183
1184 editor.fold_at_level(&FoldAtLevel { level: 1 }, window, cx);
1185 assert_eq!(
1186 editor.display_text(cx),
1187 "
1188 class Foo:⋯
1189
1190
1191 class Bar:⋯
1192
1193
1194 "
1195 .unindent(),
1196 );
1197
1198 editor.unfold_all(&UnfoldAll, window, cx);
1199 editor.fold_at_level(&FoldAtLevel { level: 0 }, window, cx);
1200 assert_eq!(
1201 editor.display_text(cx),
1202 "
1203 class Foo:
1204 # Hello!
1205
1206 def a():
1207 print(1)
1208
1209 def b():
1210 print(2)
1211
1212
1213 class Bar:
1214 # World!
1215
1216 def a():
1217 print(1)
1218
1219 def b():
1220 print(2)
1221
1222
1223 "
1224 .unindent(),
1225 );
1226
1227 assert_eq!(
1228 editor.display_text(cx),
1229 editor.buffer.read(cx).read(cx).text()
1230 );
1231 });
1232}
1233
1234#[gpui::test]
1235fn test_move_cursor(cx: &mut TestAppContext) {
1236 init_test(cx, |_| {});
1237
1238 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1239 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1240
1241 buffer.update(cx, |buffer, cx| {
1242 buffer.edit(
1243 vec![
1244 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1245 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1246 ],
1247 None,
1248 cx,
1249 );
1250 });
1251 _ = editor.update(cx, |editor, window, cx| {
1252 assert_eq!(
1253 editor.selections.display_ranges(cx),
1254 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1255 );
1256
1257 editor.move_down(&MoveDown, window, cx);
1258 assert_eq!(
1259 editor.selections.display_ranges(cx),
1260 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1261 );
1262
1263 editor.move_right(&MoveRight, window, cx);
1264 assert_eq!(
1265 editor.selections.display_ranges(cx),
1266 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1267 );
1268
1269 editor.move_left(&MoveLeft, window, cx);
1270 assert_eq!(
1271 editor.selections.display_ranges(cx),
1272 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1273 );
1274
1275 editor.move_up(&MoveUp, window, cx);
1276 assert_eq!(
1277 editor.selections.display_ranges(cx),
1278 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1279 );
1280
1281 editor.move_to_end(&MoveToEnd, window, cx);
1282 assert_eq!(
1283 editor.selections.display_ranges(cx),
1284 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1285 );
1286
1287 editor.move_to_beginning(&MoveToBeginning, window, cx);
1288 assert_eq!(
1289 editor.selections.display_ranges(cx),
1290 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1291 );
1292
1293 editor.change_selections(None, window, cx, |s| {
1294 s.select_display_ranges([
1295 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1296 ]);
1297 });
1298 editor.select_to_beginning(&SelectToBeginning, window, cx);
1299 assert_eq!(
1300 editor.selections.display_ranges(cx),
1301 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1302 );
1303
1304 editor.select_to_end(&SelectToEnd, window, cx);
1305 assert_eq!(
1306 editor.selections.display_ranges(cx),
1307 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1308 );
1309 });
1310}
1311
1312// TODO: Re-enable this test
1313#[cfg(target_os = "macos")]
1314#[gpui::test]
1315fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1316 init_test(cx, |_| {});
1317
1318 let editor = cx.add_window(|window, cx| {
1319 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1320 build_editor(buffer.clone(), window, cx)
1321 });
1322
1323 assert_eq!('🟥'.len_utf8(), 4);
1324 assert_eq!('α'.len_utf8(), 2);
1325
1326 _ = editor.update(cx, |editor, window, cx| {
1327 editor.fold_creases(
1328 vec![
1329 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1330 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1331 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1332 ],
1333 true,
1334 window,
1335 cx,
1336 );
1337 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1338
1339 editor.move_right(&MoveRight, window, cx);
1340 assert_eq!(
1341 editor.selections.display_ranges(cx),
1342 &[empty_range(0, "🟥".len())]
1343 );
1344 editor.move_right(&MoveRight, window, cx);
1345 assert_eq!(
1346 editor.selections.display_ranges(cx),
1347 &[empty_range(0, "🟥🟧".len())]
1348 );
1349 editor.move_right(&MoveRight, window, cx);
1350 assert_eq!(
1351 editor.selections.display_ranges(cx),
1352 &[empty_range(0, "🟥🟧⋯".len())]
1353 );
1354
1355 editor.move_down(&MoveDown, window, cx);
1356 assert_eq!(
1357 editor.selections.display_ranges(cx),
1358 &[empty_range(1, "ab⋯e".len())]
1359 );
1360 editor.move_left(&MoveLeft, window, cx);
1361 assert_eq!(
1362 editor.selections.display_ranges(cx),
1363 &[empty_range(1, "ab⋯".len())]
1364 );
1365 editor.move_left(&MoveLeft, window, cx);
1366 assert_eq!(
1367 editor.selections.display_ranges(cx),
1368 &[empty_range(1, "ab".len())]
1369 );
1370 editor.move_left(&MoveLeft, window, cx);
1371 assert_eq!(
1372 editor.selections.display_ranges(cx),
1373 &[empty_range(1, "a".len())]
1374 );
1375
1376 editor.move_down(&MoveDown, window, cx);
1377 assert_eq!(
1378 editor.selections.display_ranges(cx),
1379 &[empty_range(2, "α".len())]
1380 );
1381 editor.move_right(&MoveRight, window, cx);
1382 assert_eq!(
1383 editor.selections.display_ranges(cx),
1384 &[empty_range(2, "αβ".len())]
1385 );
1386 editor.move_right(&MoveRight, window, cx);
1387 assert_eq!(
1388 editor.selections.display_ranges(cx),
1389 &[empty_range(2, "αβ⋯".len())]
1390 );
1391 editor.move_right(&MoveRight, window, cx);
1392 assert_eq!(
1393 editor.selections.display_ranges(cx),
1394 &[empty_range(2, "αβ⋯ε".len())]
1395 );
1396
1397 editor.move_up(&MoveUp, window, cx);
1398 assert_eq!(
1399 editor.selections.display_ranges(cx),
1400 &[empty_range(1, "ab⋯e".len())]
1401 );
1402 editor.move_down(&MoveDown, window, cx);
1403 assert_eq!(
1404 editor.selections.display_ranges(cx),
1405 &[empty_range(2, "αβ⋯ε".len())]
1406 );
1407 editor.move_up(&MoveUp, window, cx);
1408 assert_eq!(
1409 editor.selections.display_ranges(cx),
1410 &[empty_range(1, "ab⋯e".len())]
1411 );
1412
1413 editor.move_up(&MoveUp, window, cx);
1414 assert_eq!(
1415 editor.selections.display_ranges(cx),
1416 &[empty_range(0, "🟥🟧".len())]
1417 );
1418 editor.move_left(&MoveLeft, window, cx);
1419 assert_eq!(
1420 editor.selections.display_ranges(cx),
1421 &[empty_range(0, "🟥".len())]
1422 );
1423 editor.move_left(&MoveLeft, window, cx);
1424 assert_eq!(
1425 editor.selections.display_ranges(cx),
1426 &[empty_range(0, "".len())]
1427 );
1428 });
1429}
1430
1431#[gpui::test]
1432fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1433 init_test(cx, |_| {});
1434
1435 let editor = cx.add_window(|window, cx| {
1436 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1437 build_editor(buffer.clone(), window, cx)
1438 });
1439 _ = editor.update(cx, |editor, window, cx| {
1440 editor.change_selections(None, window, cx, |s| {
1441 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1442 });
1443
1444 // moving above start of document should move selection to start of document,
1445 // but the next move down should still be at the original goal_x
1446 editor.move_up(&MoveUp, window, cx);
1447 assert_eq!(
1448 editor.selections.display_ranges(cx),
1449 &[empty_range(0, "".len())]
1450 );
1451
1452 editor.move_down(&MoveDown, window, cx);
1453 assert_eq!(
1454 editor.selections.display_ranges(cx),
1455 &[empty_range(1, "abcd".len())]
1456 );
1457
1458 editor.move_down(&MoveDown, window, cx);
1459 assert_eq!(
1460 editor.selections.display_ranges(cx),
1461 &[empty_range(2, "αβγ".len())]
1462 );
1463
1464 editor.move_down(&MoveDown, window, cx);
1465 assert_eq!(
1466 editor.selections.display_ranges(cx),
1467 &[empty_range(3, "abcd".len())]
1468 );
1469
1470 editor.move_down(&MoveDown, window, cx);
1471 assert_eq!(
1472 editor.selections.display_ranges(cx),
1473 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1474 );
1475
1476 // moving past end of document should not change goal_x
1477 editor.move_down(&MoveDown, window, cx);
1478 assert_eq!(
1479 editor.selections.display_ranges(cx),
1480 &[empty_range(5, "".len())]
1481 );
1482
1483 editor.move_down(&MoveDown, window, cx);
1484 assert_eq!(
1485 editor.selections.display_ranges(cx),
1486 &[empty_range(5, "".len())]
1487 );
1488
1489 editor.move_up(&MoveUp, window, cx);
1490 assert_eq!(
1491 editor.selections.display_ranges(cx),
1492 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1493 );
1494
1495 editor.move_up(&MoveUp, window, cx);
1496 assert_eq!(
1497 editor.selections.display_ranges(cx),
1498 &[empty_range(3, "abcd".len())]
1499 );
1500
1501 editor.move_up(&MoveUp, window, cx);
1502 assert_eq!(
1503 editor.selections.display_ranges(cx),
1504 &[empty_range(2, "αβγ".len())]
1505 );
1506 });
1507}
1508
1509#[gpui::test]
1510fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1511 init_test(cx, |_| {});
1512 let move_to_beg = MoveToBeginningOfLine {
1513 stop_at_soft_wraps: true,
1514 };
1515
1516 let move_to_end = MoveToEndOfLine {
1517 stop_at_soft_wraps: true,
1518 };
1519
1520 let editor = cx.add_window(|window, cx| {
1521 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1522 build_editor(buffer, window, cx)
1523 });
1524 _ = editor.update(cx, |editor, window, cx| {
1525 editor.change_selections(None, window, cx, |s| {
1526 s.select_display_ranges([
1527 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1528 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1529 ]);
1530 });
1531 });
1532
1533 _ = editor.update(cx, |editor, window, cx| {
1534 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1535 assert_eq!(
1536 editor.selections.display_ranges(cx),
1537 &[
1538 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1539 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1540 ]
1541 );
1542 });
1543
1544 _ = editor.update(cx, |editor, window, cx| {
1545 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1546 assert_eq!(
1547 editor.selections.display_ranges(cx),
1548 &[
1549 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1550 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1551 ]
1552 );
1553 });
1554
1555 _ = editor.update(cx, |editor, window, cx| {
1556 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1557 assert_eq!(
1558 editor.selections.display_ranges(cx),
1559 &[
1560 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1561 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1562 ]
1563 );
1564 });
1565
1566 _ = editor.update(cx, |editor, window, cx| {
1567 editor.move_to_end_of_line(&move_to_end, window, cx);
1568 assert_eq!(
1569 editor.selections.display_ranges(cx),
1570 &[
1571 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1572 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1573 ]
1574 );
1575 });
1576
1577 // Moving to the end of line again is a no-op.
1578 _ = editor.update(cx, |editor, window, cx| {
1579 editor.move_to_end_of_line(&move_to_end, window, cx);
1580 assert_eq!(
1581 editor.selections.display_ranges(cx),
1582 &[
1583 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1584 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1585 ]
1586 );
1587 });
1588
1589 _ = editor.update(cx, |editor, window, cx| {
1590 editor.move_left(&MoveLeft, window, cx);
1591 editor.select_to_beginning_of_line(
1592 &SelectToBeginningOfLine {
1593 stop_at_soft_wraps: true,
1594 },
1595 window,
1596 cx,
1597 );
1598 assert_eq!(
1599 editor.selections.display_ranges(cx),
1600 &[
1601 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1602 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1603 ]
1604 );
1605 });
1606
1607 _ = editor.update(cx, |editor, window, cx| {
1608 editor.select_to_beginning_of_line(
1609 &SelectToBeginningOfLine {
1610 stop_at_soft_wraps: true,
1611 },
1612 window,
1613 cx,
1614 );
1615 assert_eq!(
1616 editor.selections.display_ranges(cx),
1617 &[
1618 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1619 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1620 ]
1621 );
1622 });
1623
1624 _ = editor.update(cx, |editor, window, cx| {
1625 editor.select_to_beginning_of_line(
1626 &SelectToBeginningOfLine {
1627 stop_at_soft_wraps: true,
1628 },
1629 window,
1630 cx,
1631 );
1632 assert_eq!(
1633 editor.selections.display_ranges(cx),
1634 &[
1635 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1636 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1637 ]
1638 );
1639 });
1640
1641 _ = editor.update(cx, |editor, window, cx| {
1642 editor.select_to_end_of_line(
1643 &SelectToEndOfLine {
1644 stop_at_soft_wraps: true,
1645 },
1646 window,
1647 cx,
1648 );
1649 assert_eq!(
1650 editor.selections.display_ranges(cx),
1651 &[
1652 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1653 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1654 ]
1655 );
1656 });
1657
1658 _ = editor.update(cx, |editor, window, cx| {
1659 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1660 assert_eq!(editor.display_text(cx), "ab\n de");
1661 assert_eq!(
1662 editor.selections.display_ranges(cx),
1663 &[
1664 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1665 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1666 ]
1667 );
1668 });
1669
1670 _ = editor.update(cx, |editor, window, cx| {
1671 editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx);
1672 assert_eq!(editor.display_text(cx), "\n");
1673 assert_eq!(
1674 editor.selections.display_ranges(cx),
1675 &[
1676 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1677 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1678 ]
1679 );
1680 });
1681}
1682
1683#[gpui::test]
1684fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1685 init_test(cx, |_| {});
1686 let move_to_beg = MoveToBeginningOfLine {
1687 stop_at_soft_wraps: false,
1688 };
1689
1690 let move_to_end = MoveToEndOfLine {
1691 stop_at_soft_wraps: false,
1692 };
1693
1694 let editor = cx.add_window(|window, cx| {
1695 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1696 build_editor(buffer, window, cx)
1697 });
1698
1699 _ = editor.update(cx, |editor, window, cx| {
1700 editor.set_wrap_width(Some(140.0.into()), cx);
1701
1702 // We expect the following lines after wrapping
1703 // ```
1704 // thequickbrownfox
1705 // jumpedoverthelazydo
1706 // gs
1707 // ```
1708 // The final `gs` was soft-wrapped onto a new line.
1709 assert_eq!(
1710 "thequickbrownfox\njumpedoverthelaz\nydogs",
1711 editor.display_text(cx),
1712 );
1713
1714 // First, let's assert behavior on the first line, that was not soft-wrapped.
1715 // Start the cursor at the `k` on the first line
1716 editor.change_selections(None, window, cx, |s| {
1717 s.select_display_ranges([
1718 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1719 ]);
1720 });
1721
1722 // Moving to the beginning of the line should put us at the beginning of the line.
1723 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1724 assert_eq!(
1725 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1726 editor.selections.display_ranges(cx)
1727 );
1728
1729 // Moving to the end of the line should put us at the end of the line.
1730 editor.move_to_end_of_line(&move_to_end, window, cx);
1731 assert_eq!(
1732 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1733 editor.selections.display_ranges(cx)
1734 );
1735
1736 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1737 // Start the cursor at the last line (`y` that was wrapped to a new line)
1738 editor.change_selections(None, window, cx, |s| {
1739 s.select_display_ranges([
1740 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1741 ]);
1742 });
1743
1744 // Moving to the beginning of the line should put us at the start of the second line of
1745 // display text, i.e., the `j`.
1746 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1747 assert_eq!(
1748 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1749 editor.selections.display_ranges(cx)
1750 );
1751
1752 // Moving to the beginning of the line again should be a no-op.
1753 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1754 assert_eq!(
1755 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1756 editor.selections.display_ranges(cx)
1757 );
1758
1759 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1760 // next display line.
1761 editor.move_to_end_of_line(&move_to_end, window, cx);
1762 assert_eq!(
1763 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1764 editor.selections.display_ranges(cx)
1765 );
1766
1767 // Moving to the end of the line again should be a no-op.
1768 editor.move_to_end_of_line(&move_to_end, window, cx);
1769 assert_eq!(
1770 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1771 editor.selections.display_ranges(cx)
1772 );
1773 });
1774}
1775
1776#[gpui::test]
1777fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1778 init_test(cx, |_| {});
1779
1780 let editor = cx.add_window(|window, cx| {
1781 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1782 build_editor(buffer, window, cx)
1783 });
1784 _ = editor.update(cx, |editor, window, cx| {
1785 editor.change_selections(None, window, cx, |s| {
1786 s.select_display_ranges([
1787 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1788 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1789 ])
1790 });
1791
1792 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1793 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1794
1795 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1796 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1797
1798 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1799 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1800
1801 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1802 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1803
1804 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1805 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1806
1807 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1808 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1809
1810 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1811 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1812
1813 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1814 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1815
1816 editor.move_right(&MoveRight, window, cx);
1817 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1818 assert_selection_ranges(
1819 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1820 editor,
1821 cx,
1822 );
1823
1824 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1825 assert_selection_ranges(
1826 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1827 editor,
1828 cx,
1829 );
1830
1831 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1832 assert_selection_ranges(
1833 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1834 editor,
1835 cx,
1836 );
1837 });
1838}
1839
1840#[gpui::test]
1841fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1842 init_test(cx, |_| {});
1843
1844 let editor = cx.add_window(|window, cx| {
1845 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1846 build_editor(buffer, window, cx)
1847 });
1848
1849 _ = editor.update(cx, |editor, window, cx| {
1850 editor.set_wrap_width(Some(140.0.into()), cx);
1851 assert_eq!(
1852 editor.display_text(cx),
1853 "use one::{\n two::three::\n four::five\n};"
1854 );
1855
1856 editor.change_selections(None, window, cx, |s| {
1857 s.select_display_ranges([
1858 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1859 ]);
1860 });
1861
1862 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1863 assert_eq!(
1864 editor.selections.display_ranges(cx),
1865 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1866 );
1867
1868 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1869 assert_eq!(
1870 editor.selections.display_ranges(cx),
1871 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1872 );
1873
1874 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1875 assert_eq!(
1876 editor.selections.display_ranges(cx),
1877 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1878 );
1879
1880 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1881 assert_eq!(
1882 editor.selections.display_ranges(cx),
1883 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
1884 );
1885
1886 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1887 assert_eq!(
1888 editor.selections.display_ranges(cx),
1889 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1890 );
1891
1892 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1893 assert_eq!(
1894 editor.selections.display_ranges(cx),
1895 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1896 );
1897 });
1898}
1899
1900#[gpui::test]
1901async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1902 init_test(cx, |_| {});
1903 let mut cx = EditorTestContext::new(cx).await;
1904
1905 let line_height = cx.editor(|editor, window, _| {
1906 editor
1907 .style()
1908 .unwrap()
1909 .text
1910 .line_height_in_pixels(window.rem_size())
1911 });
1912 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
1913
1914 cx.set_state(
1915 &r#"ˇone
1916 two
1917
1918 three
1919 fourˇ
1920 five
1921
1922 six"#
1923 .unindent(),
1924 );
1925
1926 cx.update_editor(|editor, window, cx| {
1927 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1928 });
1929 cx.assert_editor_state(
1930 &r#"one
1931 two
1932 ˇ
1933 three
1934 four
1935 five
1936 ˇ
1937 six"#
1938 .unindent(),
1939 );
1940
1941 cx.update_editor(|editor, window, cx| {
1942 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1943 });
1944 cx.assert_editor_state(
1945 &r#"one
1946 two
1947
1948 three
1949 four
1950 five
1951 ˇ
1952 sixˇ"#
1953 .unindent(),
1954 );
1955
1956 cx.update_editor(|editor, window, cx| {
1957 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
1958 });
1959 cx.assert_editor_state(
1960 &r#"one
1961 two
1962
1963 three
1964 four
1965 five
1966
1967 sixˇ"#
1968 .unindent(),
1969 );
1970
1971 cx.update_editor(|editor, window, cx| {
1972 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
1973 });
1974 cx.assert_editor_state(
1975 &r#"one
1976 two
1977
1978 three
1979 four
1980 five
1981 ˇ
1982 six"#
1983 .unindent(),
1984 );
1985
1986 cx.update_editor(|editor, window, cx| {
1987 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
1988 });
1989 cx.assert_editor_state(
1990 &r#"one
1991 two
1992 ˇ
1993 three
1994 four
1995 five
1996
1997 six"#
1998 .unindent(),
1999 );
2000
2001 cx.update_editor(|editor, window, cx| {
2002 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2003 });
2004 cx.assert_editor_state(
2005 &r#"ˇone
2006 two
2007
2008 three
2009 four
2010 five
2011
2012 six"#
2013 .unindent(),
2014 );
2015}
2016
2017#[gpui::test]
2018async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
2019 init_test(cx, |_| {});
2020 let mut cx = EditorTestContext::new(cx).await;
2021 let line_height = cx.editor(|editor, window, _| {
2022 editor
2023 .style()
2024 .unwrap()
2025 .text
2026 .line_height_in_pixels(window.rem_size())
2027 });
2028 let window = cx.window;
2029 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2030
2031 cx.set_state(
2032 r#"ˇone
2033 two
2034 three
2035 four
2036 five
2037 six
2038 seven
2039 eight
2040 nine
2041 ten
2042 "#,
2043 );
2044
2045 cx.update_editor(|editor, window, cx| {
2046 assert_eq!(
2047 editor.snapshot(window, cx).scroll_position(),
2048 gpui::Point::new(0., 0.)
2049 );
2050 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2051 assert_eq!(
2052 editor.snapshot(window, cx).scroll_position(),
2053 gpui::Point::new(0., 3.)
2054 );
2055 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2056 assert_eq!(
2057 editor.snapshot(window, cx).scroll_position(),
2058 gpui::Point::new(0., 6.)
2059 );
2060 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2061 assert_eq!(
2062 editor.snapshot(window, cx).scroll_position(),
2063 gpui::Point::new(0., 3.)
2064 );
2065
2066 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2067 assert_eq!(
2068 editor.snapshot(window, cx).scroll_position(),
2069 gpui::Point::new(0., 1.)
2070 );
2071 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2072 assert_eq!(
2073 editor.snapshot(window, cx).scroll_position(),
2074 gpui::Point::new(0., 3.)
2075 );
2076 });
2077}
2078
2079#[gpui::test]
2080async fn test_autoscroll(cx: &mut gpui::TestAppContext) {
2081 init_test(cx, |_| {});
2082 let mut cx = EditorTestContext::new(cx).await;
2083
2084 let line_height = cx.update_editor(|editor, window, cx| {
2085 editor.set_vertical_scroll_margin(2, cx);
2086 editor
2087 .style()
2088 .unwrap()
2089 .text
2090 .line_height_in_pixels(window.rem_size())
2091 });
2092 let window = cx.window;
2093 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2094
2095 cx.set_state(
2096 r#"ˇone
2097 two
2098 three
2099 four
2100 five
2101 six
2102 seven
2103 eight
2104 nine
2105 ten
2106 "#,
2107 );
2108 cx.update_editor(|editor, window, cx| {
2109 assert_eq!(
2110 editor.snapshot(window, cx).scroll_position(),
2111 gpui::Point::new(0., 0.0)
2112 );
2113 });
2114
2115 // Add a cursor below the visible area. Since both cursors cannot fit
2116 // on screen, the editor autoscrolls to reveal the newest cursor, and
2117 // allows the vertical scroll margin below that cursor.
2118 cx.update_editor(|editor, window, cx| {
2119 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2120 selections.select_ranges([
2121 Point::new(0, 0)..Point::new(0, 0),
2122 Point::new(6, 0)..Point::new(6, 0),
2123 ]);
2124 })
2125 });
2126 cx.update_editor(|editor, window, cx| {
2127 assert_eq!(
2128 editor.snapshot(window, cx).scroll_position(),
2129 gpui::Point::new(0., 3.0)
2130 );
2131 });
2132
2133 // Move down. The editor cursor scrolls down to track the newest cursor.
2134 cx.update_editor(|editor, window, cx| {
2135 editor.move_down(&Default::default(), window, cx);
2136 });
2137 cx.update_editor(|editor, window, cx| {
2138 assert_eq!(
2139 editor.snapshot(window, cx).scroll_position(),
2140 gpui::Point::new(0., 4.0)
2141 );
2142 });
2143
2144 // Add a cursor above the visible area. Since both cursors fit on screen,
2145 // the editor scrolls to show both.
2146 cx.update_editor(|editor, window, cx| {
2147 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2148 selections.select_ranges([
2149 Point::new(1, 0)..Point::new(1, 0),
2150 Point::new(6, 0)..Point::new(6, 0),
2151 ]);
2152 })
2153 });
2154 cx.update_editor(|editor, window, cx| {
2155 assert_eq!(
2156 editor.snapshot(window, cx).scroll_position(),
2157 gpui::Point::new(0., 1.0)
2158 );
2159 });
2160}
2161
2162#[gpui::test]
2163async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
2164 init_test(cx, |_| {});
2165 let mut cx = EditorTestContext::new(cx).await;
2166
2167 let line_height = cx.editor(|editor, window, _cx| {
2168 editor
2169 .style()
2170 .unwrap()
2171 .text
2172 .line_height_in_pixels(window.rem_size())
2173 });
2174 let window = cx.window;
2175 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2176 cx.set_state(
2177 &r#"
2178 ˇone
2179 two
2180 threeˇ
2181 four
2182 five
2183 six
2184 seven
2185 eight
2186 nine
2187 ten
2188 "#
2189 .unindent(),
2190 );
2191
2192 cx.update_editor(|editor, window, cx| {
2193 editor.move_page_down(&MovePageDown::default(), window, cx)
2194 });
2195 cx.assert_editor_state(
2196 &r#"
2197 one
2198 two
2199 three
2200 ˇfour
2201 five
2202 sixˇ
2203 seven
2204 eight
2205 nine
2206 ten
2207 "#
2208 .unindent(),
2209 );
2210
2211 cx.update_editor(|editor, window, cx| {
2212 editor.move_page_down(&MovePageDown::default(), window, cx)
2213 });
2214 cx.assert_editor_state(
2215 &r#"
2216 one
2217 two
2218 three
2219 four
2220 five
2221 six
2222 ˇseven
2223 eight
2224 nineˇ
2225 ten
2226 "#
2227 .unindent(),
2228 );
2229
2230 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2231 cx.assert_editor_state(
2232 &r#"
2233 one
2234 two
2235 three
2236 ˇfour
2237 five
2238 sixˇ
2239 seven
2240 eight
2241 nine
2242 ten
2243 "#
2244 .unindent(),
2245 );
2246
2247 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2248 cx.assert_editor_state(
2249 &r#"
2250 ˇone
2251 two
2252 threeˇ
2253 four
2254 five
2255 six
2256 seven
2257 eight
2258 nine
2259 ten
2260 "#
2261 .unindent(),
2262 );
2263
2264 // Test select collapsing
2265 cx.update_editor(|editor, window, cx| {
2266 editor.move_page_down(&MovePageDown::default(), window, cx);
2267 editor.move_page_down(&MovePageDown::default(), window, cx);
2268 editor.move_page_down(&MovePageDown::default(), window, cx);
2269 });
2270 cx.assert_editor_state(
2271 &r#"
2272 one
2273 two
2274 three
2275 four
2276 five
2277 six
2278 seven
2279 eight
2280 nine
2281 ˇten
2282 ˇ"#
2283 .unindent(),
2284 );
2285}
2286
2287#[gpui::test]
2288async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
2289 init_test(cx, |_| {});
2290 let mut cx = EditorTestContext::new(cx).await;
2291 cx.set_state("one «two threeˇ» four");
2292 cx.update_editor(|editor, window, cx| {
2293 editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx);
2294 assert_eq!(editor.text(cx), " four");
2295 });
2296}
2297
2298#[gpui::test]
2299fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2300 init_test(cx, |_| {});
2301
2302 let editor = cx.add_window(|window, cx| {
2303 let buffer = MultiBuffer::build_simple("one two three four", cx);
2304 build_editor(buffer.clone(), window, cx)
2305 });
2306
2307 _ = editor.update(cx, |editor, window, cx| {
2308 editor.change_selections(None, window, cx, |s| {
2309 s.select_display_ranges([
2310 // an empty selection - the preceding word fragment is deleted
2311 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2312 // characters selected - they are deleted
2313 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2314 ])
2315 });
2316 editor.delete_to_previous_word_start(
2317 &DeleteToPreviousWordStart {
2318 ignore_newlines: false,
2319 },
2320 window,
2321 cx,
2322 );
2323 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2324 });
2325
2326 _ = editor.update(cx, |editor, window, cx| {
2327 editor.change_selections(None, window, cx, |s| {
2328 s.select_display_ranges([
2329 // an empty selection - the following word fragment is deleted
2330 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2331 // characters selected - they are deleted
2332 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2333 ])
2334 });
2335 editor.delete_to_next_word_end(
2336 &DeleteToNextWordEnd {
2337 ignore_newlines: false,
2338 },
2339 window,
2340 cx,
2341 );
2342 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2343 });
2344}
2345
2346#[gpui::test]
2347fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2348 init_test(cx, |_| {});
2349
2350 let editor = cx.add_window(|window, cx| {
2351 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2352 build_editor(buffer.clone(), window, cx)
2353 });
2354 let del_to_prev_word_start = DeleteToPreviousWordStart {
2355 ignore_newlines: false,
2356 };
2357 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2358 ignore_newlines: true,
2359 };
2360
2361 _ = editor.update(cx, |editor, window, cx| {
2362 editor.change_selections(None, window, cx, |s| {
2363 s.select_display_ranges([
2364 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2365 ])
2366 });
2367 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2368 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2369 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2370 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2371 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2372 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2373 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2374 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2375 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2376 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2377 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2378 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2379 });
2380}
2381
2382#[gpui::test]
2383fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2384 init_test(cx, |_| {});
2385
2386 let editor = cx.add_window(|window, cx| {
2387 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2388 build_editor(buffer.clone(), window, cx)
2389 });
2390 let del_to_next_word_end = DeleteToNextWordEnd {
2391 ignore_newlines: false,
2392 };
2393 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2394 ignore_newlines: true,
2395 };
2396
2397 _ = editor.update(cx, |editor, window, cx| {
2398 editor.change_selections(None, window, cx, |s| {
2399 s.select_display_ranges([
2400 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2401 ])
2402 });
2403 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2404 assert_eq!(
2405 editor.buffer.read(cx).read(cx).text(),
2406 "one\n two\nthree\n four"
2407 );
2408 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2409 assert_eq!(
2410 editor.buffer.read(cx).read(cx).text(),
2411 "\n two\nthree\n four"
2412 );
2413 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2414 assert_eq!(
2415 editor.buffer.read(cx).read(cx).text(),
2416 "two\nthree\n four"
2417 );
2418 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2419 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2420 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2421 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2422 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2423 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2424 });
2425}
2426
2427#[gpui::test]
2428fn test_newline(cx: &mut TestAppContext) {
2429 init_test(cx, |_| {});
2430
2431 let editor = cx.add_window(|window, cx| {
2432 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2433 build_editor(buffer.clone(), window, cx)
2434 });
2435
2436 _ = editor.update(cx, |editor, window, cx| {
2437 editor.change_selections(None, window, cx, |s| {
2438 s.select_display_ranges([
2439 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2440 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2441 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2442 ])
2443 });
2444
2445 editor.newline(&Newline, window, cx);
2446 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2447 });
2448}
2449
2450#[gpui::test]
2451fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2452 init_test(cx, |_| {});
2453
2454 let editor = cx.add_window(|window, cx| {
2455 let buffer = MultiBuffer::build_simple(
2456 "
2457 a
2458 b(
2459 X
2460 )
2461 c(
2462 X
2463 )
2464 "
2465 .unindent()
2466 .as_str(),
2467 cx,
2468 );
2469 let mut editor = build_editor(buffer.clone(), window, cx);
2470 editor.change_selections(None, window, cx, |s| {
2471 s.select_ranges([
2472 Point::new(2, 4)..Point::new(2, 5),
2473 Point::new(5, 4)..Point::new(5, 5),
2474 ])
2475 });
2476 editor
2477 });
2478
2479 _ = editor.update(cx, |editor, window, cx| {
2480 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2481 editor.buffer.update(cx, |buffer, cx| {
2482 buffer.edit(
2483 [
2484 (Point::new(1, 2)..Point::new(3, 0), ""),
2485 (Point::new(4, 2)..Point::new(6, 0), ""),
2486 ],
2487 None,
2488 cx,
2489 );
2490 assert_eq!(
2491 buffer.read(cx).text(),
2492 "
2493 a
2494 b()
2495 c()
2496 "
2497 .unindent()
2498 );
2499 });
2500 assert_eq!(
2501 editor.selections.ranges(cx),
2502 &[
2503 Point::new(1, 2)..Point::new(1, 2),
2504 Point::new(2, 2)..Point::new(2, 2),
2505 ],
2506 );
2507
2508 editor.newline(&Newline, window, cx);
2509 assert_eq!(
2510 editor.text(cx),
2511 "
2512 a
2513 b(
2514 )
2515 c(
2516 )
2517 "
2518 .unindent()
2519 );
2520
2521 // The selections are moved after the inserted newlines
2522 assert_eq!(
2523 editor.selections.ranges(cx),
2524 &[
2525 Point::new(2, 0)..Point::new(2, 0),
2526 Point::new(4, 0)..Point::new(4, 0),
2527 ],
2528 );
2529 });
2530}
2531
2532#[gpui::test]
2533async fn test_newline_above(cx: &mut gpui::TestAppContext) {
2534 init_test(cx, |settings| {
2535 settings.defaults.tab_size = NonZeroU32::new(4)
2536 });
2537
2538 let language = Arc::new(
2539 Language::new(
2540 LanguageConfig::default(),
2541 Some(tree_sitter_rust::LANGUAGE.into()),
2542 )
2543 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2544 .unwrap(),
2545 );
2546
2547 let mut cx = EditorTestContext::new(cx).await;
2548 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2549 cx.set_state(indoc! {"
2550 const a: ˇA = (
2551 (ˇ
2552 «const_functionˇ»(ˇ),
2553 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2554 )ˇ
2555 ˇ);ˇ
2556 "});
2557
2558 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2559 cx.assert_editor_state(indoc! {"
2560 ˇ
2561 const a: A = (
2562 ˇ
2563 (
2564 ˇ
2565 ˇ
2566 const_function(),
2567 ˇ
2568 ˇ
2569 ˇ
2570 ˇ
2571 something_else,
2572 ˇ
2573 )
2574 ˇ
2575 ˇ
2576 );
2577 "});
2578}
2579
2580#[gpui::test]
2581async fn test_newline_below(cx: &mut gpui::TestAppContext) {
2582 init_test(cx, |settings| {
2583 settings.defaults.tab_size = NonZeroU32::new(4)
2584 });
2585
2586 let language = Arc::new(
2587 Language::new(
2588 LanguageConfig::default(),
2589 Some(tree_sitter_rust::LANGUAGE.into()),
2590 )
2591 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2592 .unwrap(),
2593 );
2594
2595 let mut cx = EditorTestContext::new(cx).await;
2596 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2597 cx.set_state(indoc! {"
2598 const a: ˇA = (
2599 (ˇ
2600 «const_functionˇ»(ˇ),
2601 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2602 )ˇ
2603 ˇ);ˇ
2604 "});
2605
2606 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2607 cx.assert_editor_state(indoc! {"
2608 const a: A = (
2609 ˇ
2610 (
2611 ˇ
2612 const_function(),
2613 ˇ
2614 ˇ
2615 something_else,
2616 ˇ
2617 ˇ
2618 ˇ
2619 ˇ
2620 )
2621 ˇ
2622 );
2623 ˇ
2624 ˇ
2625 "});
2626}
2627
2628#[gpui::test]
2629async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
2630 init_test(cx, |settings| {
2631 settings.defaults.tab_size = NonZeroU32::new(4)
2632 });
2633
2634 let language = Arc::new(Language::new(
2635 LanguageConfig {
2636 line_comments: vec!["//".into()],
2637 ..LanguageConfig::default()
2638 },
2639 None,
2640 ));
2641 {
2642 let mut cx = EditorTestContext::new(cx).await;
2643 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2644 cx.set_state(indoc! {"
2645 // Fooˇ
2646 "});
2647
2648 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2649 cx.assert_editor_state(indoc! {"
2650 // Foo
2651 //ˇ
2652 "});
2653 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2654 cx.set_state(indoc! {"
2655 ˇ// Foo
2656 "});
2657 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2658 cx.assert_editor_state(indoc! {"
2659
2660 ˇ// Foo
2661 "});
2662 }
2663 // Ensure that comment continuations can be disabled.
2664 update_test_language_settings(cx, |settings| {
2665 settings.defaults.extend_comment_on_newline = Some(false);
2666 });
2667 let mut cx = EditorTestContext::new(cx).await;
2668 cx.set_state(indoc! {"
2669 // Fooˇ
2670 "});
2671 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2672 cx.assert_editor_state(indoc! {"
2673 // Foo
2674 ˇ
2675 "});
2676}
2677
2678#[gpui::test]
2679fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2680 init_test(cx, |_| {});
2681
2682 let editor = cx.add_window(|window, cx| {
2683 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2684 let mut editor = build_editor(buffer.clone(), window, cx);
2685 editor.change_selections(None, window, cx, |s| {
2686 s.select_ranges([3..4, 11..12, 19..20])
2687 });
2688 editor
2689 });
2690
2691 _ = editor.update(cx, |editor, window, cx| {
2692 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2693 editor.buffer.update(cx, |buffer, cx| {
2694 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2695 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2696 });
2697 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2698
2699 editor.insert("Z", window, cx);
2700 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2701
2702 // The selections are moved after the inserted characters
2703 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2704 });
2705}
2706
2707#[gpui::test]
2708async fn test_tab(cx: &mut gpui::TestAppContext) {
2709 init_test(cx, |settings| {
2710 settings.defaults.tab_size = NonZeroU32::new(3)
2711 });
2712
2713 let mut cx = EditorTestContext::new(cx).await;
2714 cx.set_state(indoc! {"
2715 ˇabˇc
2716 ˇ🏀ˇ🏀ˇefg
2717 dˇ
2718 "});
2719 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2720 cx.assert_editor_state(indoc! {"
2721 ˇab ˇc
2722 ˇ🏀 ˇ🏀 ˇefg
2723 d ˇ
2724 "});
2725
2726 cx.set_state(indoc! {"
2727 a
2728 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2729 "});
2730 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2731 cx.assert_editor_state(indoc! {"
2732 a
2733 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2734 "});
2735}
2736
2737#[gpui::test]
2738async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) {
2739 init_test(cx, |_| {});
2740
2741 let mut cx = EditorTestContext::new(cx).await;
2742 let language = Arc::new(
2743 Language::new(
2744 LanguageConfig::default(),
2745 Some(tree_sitter_rust::LANGUAGE.into()),
2746 )
2747 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2748 .unwrap(),
2749 );
2750 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2751
2752 // cursors that are already at the suggested indent level insert
2753 // a soft tab. cursors that are to the left of the suggested indent
2754 // auto-indent their line.
2755 cx.set_state(indoc! {"
2756 ˇ
2757 const a: B = (
2758 c(
2759 d(
2760 ˇ
2761 )
2762 ˇ
2763 ˇ )
2764 );
2765 "});
2766 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2767 cx.assert_editor_state(indoc! {"
2768 ˇ
2769 const a: B = (
2770 c(
2771 d(
2772 ˇ
2773 )
2774 ˇ
2775 ˇ)
2776 );
2777 "});
2778
2779 // handle auto-indent when there are multiple cursors on the same line
2780 cx.set_state(indoc! {"
2781 const a: B = (
2782 c(
2783 ˇ ˇ
2784 ˇ )
2785 );
2786 "});
2787 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2788 cx.assert_editor_state(indoc! {"
2789 const a: B = (
2790 c(
2791 ˇ
2792 ˇ)
2793 );
2794 "});
2795}
2796
2797#[gpui::test]
2798async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
2799 init_test(cx, |settings| {
2800 settings.defaults.tab_size = NonZeroU32::new(4)
2801 });
2802
2803 let language = Arc::new(
2804 Language::new(
2805 LanguageConfig::default(),
2806 Some(tree_sitter_rust::LANGUAGE.into()),
2807 )
2808 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2809 .unwrap(),
2810 );
2811
2812 let mut cx = EditorTestContext::new(cx).await;
2813 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2814 cx.set_state(indoc! {"
2815 fn a() {
2816 if b {
2817 \t ˇc
2818 }
2819 }
2820 "});
2821
2822 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2823 cx.assert_editor_state(indoc! {"
2824 fn a() {
2825 if b {
2826 ˇc
2827 }
2828 }
2829 "});
2830}
2831
2832#[gpui::test]
2833async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
2834 init_test(cx, |settings| {
2835 settings.defaults.tab_size = NonZeroU32::new(4);
2836 });
2837
2838 let mut cx = EditorTestContext::new(cx).await;
2839
2840 cx.set_state(indoc! {"
2841 «oneˇ» «twoˇ»
2842 three
2843 four
2844 "});
2845 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2846 cx.assert_editor_state(indoc! {"
2847 «oneˇ» «twoˇ»
2848 three
2849 four
2850 "});
2851
2852 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2853 cx.assert_editor_state(indoc! {"
2854 «oneˇ» «twoˇ»
2855 three
2856 four
2857 "});
2858
2859 // select across line ending
2860 cx.set_state(indoc! {"
2861 one two
2862 t«hree
2863 ˇ» four
2864 "});
2865 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2866 cx.assert_editor_state(indoc! {"
2867 one two
2868 t«hree
2869 ˇ» four
2870 "});
2871
2872 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2873 cx.assert_editor_state(indoc! {"
2874 one two
2875 t«hree
2876 ˇ» four
2877 "});
2878
2879 // Ensure that indenting/outdenting works when the cursor is at column 0.
2880 cx.set_state(indoc! {"
2881 one two
2882 ˇthree
2883 four
2884 "});
2885 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2886 cx.assert_editor_state(indoc! {"
2887 one two
2888 ˇthree
2889 four
2890 "});
2891
2892 cx.set_state(indoc! {"
2893 one two
2894 ˇ three
2895 four
2896 "});
2897 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2898 cx.assert_editor_state(indoc! {"
2899 one two
2900 ˇthree
2901 four
2902 "});
2903}
2904
2905#[gpui::test]
2906async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
2907 init_test(cx, |settings| {
2908 settings.defaults.hard_tabs = Some(true);
2909 });
2910
2911 let mut cx = EditorTestContext::new(cx).await;
2912
2913 // select two ranges on one line
2914 cx.set_state(indoc! {"
2915 «oneˇ» «twoˇ»
2916 three
2917 four
2918 "});
2919 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2920 cx.assert_editor_state(indoc! {"
2921 \t«oneˇ» «twoˇ»
2922 three
2923 four
2924 "});
2925 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2926 cx.assert_editor_state(indoc! {"
2927 \t\t«oneˇ» «twoˇ»
2928 three
2929 four
2930 "});
2931 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2932 cx.assert_editor_state(indoc! {"
2933 \t«oneˇ» «twoˇ»
2934 three
2935 four
2936 "});
2937 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2938 cx.assert_editor_state(indoc! {"
2939 «oneˇ» «twoˇ»
2940 three
2941 four
2942 "});
2943
2944 // select across a line ending
2945 cx.set_state(indoc! {"
2946 one two
2947 t«hree
2948 ˇ»four
2949 "});
2950 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2951 cx.assert_editor_state(indoc! {"
2952 one two
2953 \tt«hree
2954 ˇ»four
2955 "});
2956 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2957 cx.assert_editor_state(indoc! {"
2958 one two
2959 \t\tt«hree
2960 ˇ»four
2961 "});
2962 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2963 cx.assert_editor_state(indoc! {"
2964 one two
2965 \tt«hree
2966 ˇ»four
2967 "});
2968 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2969 cx.assert_editor_state(indoc! {"
2970 one two
2971 t«hree
2972 ˇ»four
2973 "});
2974
2975 // Ensure that indenting/outdenting works when the cursor is at column 0.
2976 cx.set_state(indoc! {"
2977 one two
2978 ˇthree
2979 four
2980 "});
2981 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2982 cx.assert_editor_state(indoc! {"
2983 one two
2984 ˇthree
2985 four
2986 "});
2987 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2988 cx.assert_editor_state(indoc! {"
2989 one two
2990 \tˇthree
2991 four
2992 "});
2993 cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx));
2994 cx.assert_editor_state(indoc! {"
2995 one two
2996 ˇthree
2997 four
2998 "});
2999}
3000
3001#[gpui::test]
3002fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3003 init_test(cx, |settings| {
3004 settings.languages.extend([
3005 (
3006 "TOML".into(),
3007 LanguageSettingsContent {
3008 tab_size: NonZeroU32::new(2),
3009 ..Default::default()
3010 },
3011 ),
3012 (
3013 "Rust".into(),
3014 LanguageSettingsContent {
3015 tab_size: NonZeroU32::new(4),
3016 ..Default::default()
3017 },
3018 ),
3019 ]);
3020 });
3021
3022 let toml_language = Arc::new(Language::new(
3023 LanguageConfig {
3024 name: "TOML".into(),
3025 ..Default::default()
3026 },
3027 None,
3028 ));
3029 let rust_language = Arc::new(Language::new(
3030 LanguageConfig {
3031 name: "Rust".into(),
3032 ..Default::default()
3033 },
3034 None,
3035 ));
3036
3037 let toml_buffer =
3038 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3039 let rust_buffer =
3040 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3041 let multibuffer = cx.new(|cx| {
3042 let mut multibuffer = MultiBuffer::new(ReadWrite);
3043 multibuffer.push_excerpts(
3044 toml_buffer.clone(),
3045 [ExcerptRange {
3046 context: Point::new(0, 0)..Point::new(2, 0),
3047 primary: None,
3048 }],
3049 cx,
3050 );
3051 multibuffer.push_excerpts(
3052 rust_buffer.clone(),
3053 [ExcerptRange {
3054 context: Point::new(0, 0)..Point::new(1, 0),
3055 primary: None,
3056 }],
3057 cx,
3058 );
3059 multibuffer
3060 });
3061
3062 cx.add_window(|window, cx| {
3063 let mut editor = build_editor(multibuffer, window, cx);
3064
3065 assert_eq!(
3066 editor.text(cx),
3067 indoc! {"
3068 a = 1
3069 b = 2
3070
3071 const c: usize = 3;
3072 "}
3073 );
3074
3075 select_ranges(
3076 &mut editor,
3077 indoc! {"
3078 «aˇ» = 1
3079 b = 2
3080
3081 «const c:ˇ» usize = 3;
3082 "},
3083 window,
3084 cx,
3085 );
3086
3087 editor.tab(&Tab, window, cx);
3088 assert_text_with_selections(
3089 &mut editor,
3090 indoc! {"
3091 «aˇ» = 1
3092 b = 2
3093
3094 «const c:ˇ» usize = 3;
3095 "},
3096 cx,
3097 );
3098 editor.tab_prev(&TabPrev, window, cx);
3099 assert_text_with_selections(
3100 &mut editor,
3101 indoc! {"
3102 «aˇ» = 1
3103 b = 2
3104
3105 «const c:ˇ» usize = 3;
3106 "},
3107 cx,
3108 );
3109
3110 editor
3111 });
3112}
3113
3114#[gpui::test]
3115async fn test_backspace(cx: &mut gpui::TestAppContext) {
3116 init_test(cx, |_| {});
3117
3118 let mut cx = EditorTestContext::new(cx).await;
3119
3120 // Basic backspace
3121 cx.set_state(indoc! {"
3122 onˇe two three
3123 fou«rˇ» five six
3124 seven «ˇeight nine
3125 »ten
3126 "});
3127 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3128 cx.assert_editor_state(indoc! {"
3129 oˇe two three
3130 fouˇ five six
3131 seven ˇten
3132 "});
3133
3134 // Test backspace inside and around indents
3135 cx.set_state(indoc! {"
3136 zero
3137 ˇone
3138 ˇtwo
3139 ˇ ˇ ˇ three
3140 ˇ ˇ four
3141 "});
3142 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3143 cx.assert_editor_state(indoc! {"
3144 zero
3145 ˇone
3146 ˇtwo
3147 ˇ threeˇ four
3148 "});
3149
3150 // Test backspace with line_mode set to true
3151 cx.update_editor(|e, _, _| e.selections.line_mode = true);
3152 cx.set_state(indoc! {"
3153 The ˇquick ˇbrown
3154 fox jumps over
3155 the lazy dog
3156 ˇThe qu«ick bˇ»rown"});
3157 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3158 cx.assert_editor_state(indoc! {"
3159 ˇfox jumps over
3160 the lazy dogˇ"});
3161}
3162
3163#[gpui::test]
3164async fn test_delete(cx: &mut gpui::TestAppContext) {
3165 init_test(cx, |_| {});
3166
3167 let mut cx = EditorTestContext::new(cx).await;
3168 cx.set_state(indoc! {"
3169 onˇe two three
3170 fou«rˇ» five six
3171 seven «ˇeight nine
3172 »ten
3173 "});
3174 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3175 cx.assert_editor_state(indoc! {"
3176 onˇ two three
3177 fouˇ five six
3178 seven ˇten
3179 "});
3180
3181 // Test backspace with line_mode set to true
3182 cx.update_editor(|e, _, _| e.selections.line_mode = true);
3183 cx.set_state(indoc! {"
3184 The ˇquick ˇbrown
3185 fox «ˇjum»ps over
3186 the lazy dog
3187 ˇThe qu«ick bˇ»rown"});
3188 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3189 cx.assert_editor_state("ˇthe lazy dogˇ");
3190}
3191
3192#[gpui::test]
3193fn test_delete_line(cx: &mut TestAppContext) {
3194 init_test(cx, |_| {});
3195
3196 let editor = cx.add_window(|window, cx| {
3197 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3198 build_editor(buffer, window, cx)
3199 });
3200 _ = editor.update(cx, |editor, window, cx| {
3201 editor.change_selections(None, window, cx, |s| {
3202 s.select_display_ranges([
3203 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3204 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3205 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3206 ])
3207 });
3208 editor.delete_line(&DeleteLine, window, cx);
3209 assert_eq!(editor.display_text(cx), "ghi");
3210 assert_eq!(
3211 editor.selections.display_ranges(cx),
3212 vec![
3213 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3214 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3215 ]
3216 );
3217 });
3218
3219 let editor = cx.add_window(|window, cx| {
3220 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3221 build_editor(buffer, window, cx)
3222 });
3223 _ = editor.update(cx, |editor, window, cx| {
3224 editor.change_selections(None, window, cx, |s| {
3225 s.select_display_ranges([
3226 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3227 ])
3228 });
3229 editor.delete_line(&DeleteLine, window, cx);
3230 assert_eq!(editor.display_text(cx), "ghi\n");
3231 assert_eq!(
3232 editor.selections.display_ranges(cx),
3233 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3234 );
3235 });
3236}
3237
3238#[gpui::test]
3239fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3240 init_test(cx, |_| {});
3241
3242 cx.add_window(|window, cx| {
3243 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3244 let mut editor = build_editor(buffer.clone(), window, cx);
3245 let buffer = buffer.read(cx).as_singleton().unwrap();
3246
3247 assert_eq!(
3248 editor.selections.ranges::<Point>(cx),
3249 &[Point::new(0, 0)..Point::new(0, 0)]
3250 );
3251
3252 // When on single line, replace newline at end by space
3253 editor.join_lines(&JoinLines, window, cx);
3254 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3255 assert_eq!(
3256 editor.selections.ranges::<Point>(cx),
3257 &[Point::new(0, 3)..Point::new(0, 3)]
3258 );
3259
3260 // When multiple lines are selected, remove newlines that are spanned by the selection
3261 editor.change_selections(None, window, cx, |s| {
3262 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3263 });
3264 editor.join_lines(&JoinLines, window, cx);
3265 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3266 assert_eq!(
3267 editor.selections.ranges::<Point>(cx),
3268 &[Point::new(0, 11)..Point::new(0, 11)]
3269 );
3270
3271 // Undo should be transactional
3272 editor.undo(&Undo, window, cx);
3273 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3274 assert_eq!(
3275 editor.selections.ranges::<Point>(cx),
3276 &[Point::new(0, 5)..Point::new(2, 2)]
3277 );
3278
3279 // When joining an empty line don't insert a space
3280 editor.change_selections(None, window, cx, |s| {
3281 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3282 });
3283 editor.join_lines(&JoinLines, window, cx);
3284 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3285 assert_eq!(
3286 editor.selections.ranges::<Point>(cx),
3287 [Point::new(2, 3)..Point::new(2, 3)]
3288 );
3289
3290 // We can remove trailing newlines
3291 editor.join_lines(&JoinLines, window, cx);
3292 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3293 assert_eq!(
3294 editor.selections.ranges::<Point>(cx),
3295 [Point::new(2, 3)..Point::new(2, 3)]
3296 );
3297
3298 // We don't blow up on the last line
3299 editor.join_lines(&JoinLines, window, cx);
3300 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3301 assert_eq!(
3302 editor.selections.ranges::<Point>(cx),
3303 [Point::new(2, 3)..Point::new(2, 3)]
3304 );
3305
3306 // reset to test indentation
3307 editor.buffer.update(cx, |buffer, cx| {
3308 buffer.edit(
3309 [
3310 (Point::new(1, 0)..Point::new(1, 2), " "),
3311 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3312 ],
3313 None,
3314 cx,
3315 )
3316 });
3317
3318 // We remove any leading spaces
3319 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3320 editor.change_selections(None, window, cx, |s| {
3321 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3322 });
3323 editor.join_lines(&JoinLines, window, cx);
3324 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3325
3326 // We don't insert a space for a line containing only spaces
3327 editor.join_lines(&JoinLines, window, cx);
3328 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3329
3330 // We ignore any leading tabs
3331 editor.join_lines(&JoinLines, window, cx);
3332 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3333
3334 editor
3335 });
3336}
3337
3338#[gpui::test]
3339fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3340 init_test(cx, |_| {});
3341
3342 cx.add_window(|window, cx| {
3343 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3344 let mut editor = build_editor(buffer.clone(), window, cx);
3345 let buffer = buffer.read(cx).as_singleton().unwrap();
3346
3347 editor.change_selections(None, window, cx, |s| {
3348 s.select_ranges([
3349 Point::new(0, 2)..Point::new(1, 1),
3350 Point::new(1, 2)..Point::new(1, 2),
3351 Point::new(3, 1)..Point::new(3, 2),
3352 ])
3353 });
3354
3355 editor.join_lines(&JoinLines, window, cx);
3356 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3357
3358 assert_eq!(
3359 editor.selections.ranges::<Point>(cx),
3360 [
3361 Point::new(0, 7)..Point::new(0, 7),
3362 Point::new(1, 3)..Point::new(1, 3)
3363 ]
3364 );
3365 editor
3366 });
3367}
3368
3369#[gpui::test]
3370async fn test_join_lines_with_git_diff_base(
3371 executor: BackgroundExecutor,
3372 cx: &mut gpui::TestAppContext,
3373) {
3374 init_test(cx, |_| {});
3375
3376 let mut cx = EditorTestContext::new(cx).await;
3377
3378 let diff_base = r#"
3379 Line 0
3380 Line 1
3381 Line 2
3382 Line 3
3383 "#
3384 .unindent();
3385
3386 cx.set_state(
3387 &r#"
3388 ˇLine 0
3389 Line 1
3390 Line 2
3391 Line 3
3392 "#
3393 .unindent(),
3394 );
3395
3396 cx.set_diff_base(&diff_base);
3397 executor.run_until_parked();
3398
3399 // Join lines
3400 cx.update_editor(|editor, window, cx| {
3401 editor.join_lines(&JoinLines, window, cx);
3402 });
3403 executor.run_until_parked();
3404
3405 cx.assert_editor_state(
3406 &r#"
3407 Line 0ˇ Line 1
3408 Line 2
3409 Line 3
3410 "#
3411 .unindent(),
3412 );
3413 // Join again
3414 cx.update_editor(|editor, window, cx| {
3415 editor.join_lines(&JoinLines, window, cx);
3416 });
3417 executor.run_until_parked();
3418
3419 cx.assert_editor_state(
3420 &r#"
3421 Line 0 Line 1ˇ Line 2
3422 Line 3
3423 "#
3424 .unindent(),
3425 );
3426}
3427
3428#[gpui::test]
3429async fn test_custom_newlines_cause_no_false_positive_diffs(
3430 executor: BackgroundExecutor,
3431 cx: &mut gpui::TestAppContext,
3432) {
3433 init_test(cx, |_| {});
3434 let mut cx = EditorTestContext::new(cx).await;
3435 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3436 cx.set_diff_base("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3437 executor.run_until_parked();
3438
3439 cx.update_editor(|editor, window, cx| {
3440 let snapshot = editor.snapshot(window, cx);
3441 assert_eq!(
3442 snapshot
3443 .buffer_snapshot
3444 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3445 .collect::<Vec<_>>(),
3446 Vec::new(),
3447 "Should not have any diffs for files with custom newlines"
3448 );
3449 });
3450}
3451
3452#[gpui::test]
3453async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3454 init_test(cx, |_| {});
3455
3456 let mut cx = EditorTestContext::new(cx).await;
3457
3458 // Test sort_lines_case_insensitive()
3459 cx.set_state(indoc! {"
3460 «z
3461 y
3462 x
3463 Z
3464 Y
3465 Xˇ»
3466 "});
3467 cx.update_editor(|e, window, cx| {
3468 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3469 });
3470 cx.assert_editor_state(indoc! {"
3471 «x
3472 X
3473 y
3474 Y
3475 z
3476 Zˇ»
3477 "});
3478
3479 // Test reverse_lines()
3480 cx.set_state(indoc! {"
3481 «5
3482 4
3483 3
3484 2
3485 1ˇ»
3486 "});
3487 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3488 cx.assert_editor_state(indoc! {"
3489 «1
3490 2
3491 3
3492 4
3493 5ˇ»
3494 "});
3495
3496 // Skip testing shuffle_line()
3497
3498 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3499 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3500
3501 // Don't manipulate when cursor is on single line, but expand the selection
3502 cx.set_state(indoc! {"
3503 ddˇdd
3504 ccc
3505 bb
3506 a
3507 "});
3508 cx.update_editor(|e, window, cx| {
3509 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3510 });
3511 cx.assert_editor_state(indoc! {"
3512 «ddddˇ»
3513 ccc
3514 bb
3515 a
3516 "});
3517
3518 // Basic manipulate case
3519 // Start selection moves to column 0
3520 // End of selection shrinks to fit shorter line
3521 cx.set_state(indoc! {"
3522 dd«d
3523 ccc
3524 bb
3525 aaaaaˇ»
3526 "});
3527 cx.update_editor(|e, window, cx| {
3528 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3529 });
3530 cx.assert_editor_state(indoc! {"
3531 «aaaaa
3532 bb
3533 ccc
3534 dddˇ»
3535 "});
3536
3537 // Manipulate case with newlines
3538 cx.set_state(indoc! {"
3539 dd«d
3540 ccc
3541
3542 bb
3543 aaaaa
3544
3545 ˇ»
3546 "});
3547 cx.update_editor(|e, window, cx| {
3548 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3549 });
3550 cx.assert_editor_state(indoc! {"
3551 «
3552
3553 aaaaa
3554 bb
3555 ccc
3556 dddˇ»
3557
3558 "});
3559
3560 // Adding new line
3561 cx.set_state(indoc! {"
3562 aa«a
3563 bbˇ»b
3564 "});
3565 cx.update_editor(|e, window, cx| {
3566 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3567 });
3568 cx.assert_editor_state(indoc! {"
3569 «aaa
3570 bbb
3571 added_lineˇ»
3572 "});
3573
3574 // Removing line
3575 cx.set_state(indoc! {"
3576 aa«a
3577 bbbˇ»
3578 "});
3579 cx.update_editor(|e, window, cx| {
3580 e.manipulate_lines(window, cx, |lines| {
3581 lines.pop();
3582 })
3583 });
3584 cx.assert_editor_state(indoc! {"
3585 «aaaˇ»
3586 "});
3587
3588 // Removing all lines
3589 cx.set_state(indoc! {"
3590 aa«a
3591 bbbˇ»
3592 "});
3593 cx.update_editor(|e, window, cx| {
3594 e.manipulate_lines(window, cx, |lines| {
3595 lines.drain(..);
3596 })
3597 });
3598 cx.assert_editor_state(indoc! {"
3599 ˇ
3600 "});
3601}
3602
3603#[gpui::test]
3604async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3605 init_test(cx, |_| {});
3606
3607 let mut cx = EditorTestContext::new(cx).await;
3608
3609 // Consider continuous selection as single selection
3610 cx.set_state(indoc! {"
3611 Aaa«aa
3612 cˇ»c«c
3613 bb
3614 aaaˇ»aa
3615 "});
3616 cx.update_editor(|e, window, cx| {
3617 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3618 });
3619 cx.assert_editor_state(indoc! {"
3620 «Aaaaa
3621 ccc
3622 bb
3623 aaaaaˇ»
3624 "});
3625
3626 cx.set_state(indoc! {"
3627 Aaa«aa
3628 cˇ»c«c
3629 bb
3630 aaaˇ»aa
3631 "});
3632 cx.update_editor(|e, window, cx| {
3633 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3634 });
3635 cx.assert_editor_state(indoc! {"
3636 «Aaaaa
3637 ccc
3638 bbˇ»
3639 "});
3640
3641 // Consider non continuous selection as distinct dedup operations
3642 cx.set_state(indoc! {"
3643 «aaaaa
3644 bb
3645 aaaaa
3646 aaaaaˇ»
3647
3648 aaa«aaˇ»
3649 "});
3650 cx.update_editor(|e, window, cx| {
3651 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3652 });
3653 cx.assert_editor_state(indoc! {"
3654 «aaaaa
3655 bbˇ»
3656
3657 «aaaaaˇ»
3658 "});
3659}
3660
3661#[gpui::test]
3662async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3663 init_test(cx, |_| {});
3664
3665 let mut cx = EditorTestContext::new(cx).await;
3666
3667 cx.set_state(indoc! {"
3668 «Aaa
3669 aAa
3670 Aaaˇ»
3671 "});
3672 cx.update_editor(|e, window, cx| {
3673 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3674 });
3675 cx.assert_editor_state(indoc! {"
3676 «Aaa
3677 aAaˇ»
3678 "});
3679
3680 cx.set_state(indoc! {"
3681 «Aaa
3682 aAa
3683 aaAˇ»
3684 "});
3685 cx.update_editor(|e, window, cx| {
3686 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3687 });
3688 cx.assert_editor_state(indoc! {"
3689 «Aaaˇ»
3690 "});
3691}
3692
3693#[gpui::test]
3694async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3695 init_test(cx, |_| {});
3696
3697 let mut cx = EditorTestContext::new(cx).await;
3698
3699 // Manipulate with multiple selections on a single line
3700 cx.set_state(indoc! {"
3701 dd«dd
3702 cˇ»c«c
3703 bb
3704 aaaˇ»aa
3705 "});
3706 cx.update_editor(|e, window, cx| {
3707 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3708 });
3709 cx.assert_editor_state(indoc! {"
3710 «aaaaa
3711 bb
3712 ccc
3713 ddddˇ»
3714 "});
3715
3716 // Manipulate with multiple disjoin selections
3717 cx.set_state(indoc! {"
3718 5«
3719 4
3720 3
3721 2
3722 1ˇ»
3723
3724 dd«dd
3725 ccc
3726 bb
3727 aaaˇ»aa
3728 "});
3729 cx.update_editor(|e, window, cx| {
3730 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3731 });
3732 cx.assert_editor_state(indoc! {"
3733 «1
3734 2
3735 3
3736 4
3737 5ˇ»
3738
3739 «aaaaa
3740 bb
3741 ccc
3742 ddddˇ»
3743 "});
3744
3745 // Adding lines on each selection
3746 cx.set_state(indoc! {"
3747 2«
3748 1ˇ»
3749
3750 bb«bb
3751 aaaˇ»aa
3752 "});
3753 cx.update_editor(|e, window, cx| {
3754 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3755 });
3756 cx.assert_editor_state(indoc! {"
3757 «2
3758 1
3759 added lineˇ»
3760
3761 «bbbb
3762 aaaaa
3763 added lineˇ»
3764 "});
3765
3766 // Removing lines on each selection
3767 cx.set_state(indoc! {"
3768 2«
3769 1ˇ»
3770
3771 bb«bb
3772 aaaˇ»aa
3773 "});
3774 cx.update_editor(|e, window, cx| {
3775 e.manipulate_lines(window, cx, |lines| {
3776 lines.pop();
3777 })
3778 });
3779 cx.assert_editor_state(indoc! {"
3780 «2ˇ»
3781
3782 «bbbbˇ»
3783 "});
3784}
3785
3786#[gpui::test]
3787async fn test_manipulate_text(cx: &mut TestAppContext) {
3788 init_test(cx, |_| {});
3789
3790 let mut cx = EditorTestContext::new(cx).await;
3791
3792 // Test convert_to_upper_case()
3793 cx.set_state(indoc! {"
3794 «hello worldˇ»
3795 "});
3796 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3797 cx.assert_editor_state(indoc! {"
3798 «HELLO WORLDˇ»
3799 "});
3800
3801 // Test convert_to_lower_case()
3802 cx.set_state(indoc! {"
3803 «HELLO WORLDˇ»
3804 "});
3805 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3806 cx.assert_editor_state(indoc! {"
3807 «hello worldˇ»
3808 "});
3809
3810 // Test multiple line, single selection case
3811 cx.set_state(indoc! {"
3812 «The quick brown
3813 fox jumps over
3814 the lazy dogˇ»
3815 "});
3816 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3817 cx.assert_editor_state(indoc! {"
3818 «The Quick Brown
3819 Fox Jumps Over
3820 The Lazy Dogˇ»
3821 "});
3822
3823 // Test multiple line, single selection case
3824 cx.set_state(indoc! {"
3825 «The quick brown
3826 fox jumps over
3827 the lazy dogˇ»
3828 "});
3829 cx.update_editor(|e, window, cx| {
3830 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
3831 });
3832 cx.assert_editor_state(indoc! {"
3833 «TheQuickBrown
3834 FoxJumpsOver
3835 TheLazyDogˇ»
3836 "});
3837
3838 // From here on out, test more complex cases of manipulate_text()
3839
3840 // Test no selection case - should affect words cursors are in
3841 // Cursor at beginning, middle, and end of word
3842 cx.set_state(indoc! {"
3843 ˇhello big beauˇtiful worldˇ
3844 "});
3845 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3846 cx.assert_editor_state(indoc! {"
3847 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
3848 "});
3849
3850 // Test multiple selections on a single line and across multiple lines
3851 cx.set_state(indoc! {"
3852 «Theˇ» quick «brown
3853 foxˇ» jumps «overˇ»
3854 the «lazyˇ» dog
3855 "});
3856 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3857 cx.assert_editor_state(indoc! {"
3858 «THEˇ» quick «BROWN
3859 FOXˇ» jumps «OVERˇ»
3860 the «LAZYˇ» dog
3861 "});
3862
3863 // Test case where text length grows
3864 cx.set_state(indoc! {"
3865 «tschüߡ»
3866 "});
3867 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3868 cx.assert_editor_state(indoc! {"
3869 «TSCHÜSSˇ»
3870 "});
3871
3872 // Test to make sure we don't crash when text shrinks
3873 cx.set_state(indoc! {"
3874 aaa_bbbˇ
3875 "});
3876 cx.update_editor(|e, window, cx| {
3877 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3878 });
3879 cx.assert_editor_state(indoc! {"
3880 «aaaBbbˇ»
3881 "});
3882
3883 // Test to make sure we all aware of the fact that each word can grow and shrink
3884 // Final selections should be aware of this fact
3885 cx.set_state(indoc! {"
3886 aaa_bˇbb bbˇb_ccc ˇccc_ddd
3887 "});
3888 cx.update_editor(|e, window, cx| {
3889 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3890 });
3891 cx.assert_editor_state(indoc! {"
3892 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
3893 "});
3894
3895 cx.set_state(indoc! {"
3896 «hElLo, WoRld!ˇ»
3897 "});
3898 cx.update_editor(|e, window, cx| {
3899 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
3900 });
3901 cx.assert_editor_state(indoc! {"
3902 «HeLlO, wOrLD!ˇ»
3903 "});
3904}
3905
3906#[gpui::test]
3907fn test_duplicate_line(cx: &mut TestAppContext) {
3908 init_test(cx, |_| {});
3909
3910 let editor = cx.add_window(|window, cx| {
3911 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3912 build_editor(buffer, window, cx)
3913 });
3914 _ = editor.update(cx, |editor, window, cx| {
3915 editor.change_selections(None, window, cx, |s| {
3916 s.select_display_ranges([
3917 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3918 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3919 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
3920 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3921 ])
3922 });
3923 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
3924 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
3925 assert_eq!(
3926 editor.selections.display_ranges(cx),
3927 vec![
3928 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3929 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3930 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3931 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
3932 ]
3933 );
3934 });
3935
3936 let editor = cx.add_window(|window, cx| {
3937 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3938 build_editor(buffer, window, cx)
3939 });
3940 _ = editor.update(cx, |editor, window, cx| {
3941 editor.change_selections(None, window, cx, |s| {
3942 s.select_display_ranges([
3943 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
3944 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
3945 ])
3946 });
3947 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
3948 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
3949 assert_eq!(
3950 editor.selections.display_ranges(cx),
3951 vec![
3952 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
3953 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
3954 ]
3955 );
3956 });
3957
3958 // With `move_upwards` the selections stay in place, except for
3959 // the lines inserted above them
3960 let editor = cx.add_window(|window, cx| {
3961 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3962 build_editor(buffer, window, cx)
3963 });
3964 _ = editor.update(cx, |editor, window, cx| {
3965 editor.change_selections(None, window, cx, |s| {
3966 s.select_display_ranges([
3967 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3968 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3969 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
3970 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3971 ])
3972 });
3973 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
3974 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
3975 assert_eq!(
3976 editor.selections.display_ranges(cx),
3977 vec![
3978 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
3979 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3980 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
3981 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
3982 ]
3983 );
3984 });
3985
3986 let editor = cx.add_window(|window, cx| {
3987 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3988 build_editor(buffer, window, cx)
3989 });
3990 _ = editor.update(cx, |editor, window, cx| {
3991 editor.change_selections(None, window, cx, |s| {
3992 s.select_display_ranges([
3993 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
3994 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
3995 ])
3996 });
3997 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
3998 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
3999 assert_eq!(
4000 editor.selections.display_ranges(cx),
4001 vec![
4002 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4003 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4004 ]
4005 );
4006 });
4007
4008 let editor = cx.add_window(|window, cx| {
4009 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4010 build_editor(buffer, window, cx)
4011 });
4012 _ = editor.update(cx, |editor, window, cx| {
4013 editor.change_selections(None, window, cx, |s| {
4014 s.select_display_ranges([
4015 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4016 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4017 ])
4018 });
4019 editor.duplicate_selection(&DuplicateSelection, window, cx);
4020 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4021 assert_eq!(
4022 editor.selections.display_ranges(cx),
4023 vec![
4024 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4025 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4026 ]
4027 );
4028 });
4029}
4030
4031#[gpui::test]
4032fn test_move_line_up_down(cx: &mut TestAppContext) {
4033 init_test(cx, |_| {});
4034
4035 let editor = cx.add_window(|window, cx| {
4036 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4037 build_editor(buffer, window, cx)
4038 });
4039 _ = editor.update(cx, |editor, window, cx| {
4040 editor.fold_creases(
4041 vec![
4042 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4043 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4044 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4045 ],
4046 true,
4047 window,
4048 cx,
4049 );
4050 editor.change_selections(None, window, cx, |s| {
4051 s.select_display_ranges([
4052 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4053 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4054 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4055 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4056 ])
4057 });
4058 assert_eq!(
4059 editor.display_text(cx),
4060 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4061 );
4062
4063 editor.move_line_up(&MoveLineUp, window, cx);
4064 assert_eq!(
4065 editor.display_text(cx),
4066 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4067 );
4068 assert_eq!(
4069 editor.selections.display_ranges(cx),
4070 vec![
4071 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4072 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4073 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4074 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4075 ]
4076 );
4077 });
4078
4079 _ = editor.update(cx, |editor, window, cx| {
4080 editor.move_line_down(&MoveLineDown, window, cx);
4081 assert_eq!(
4082 editor.display_text(cx),
4083 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4084 );
4085 assert_eq!(
4086 editor.selections.display_ranges(cx),
4087 vec![
4088 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4089 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4090 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4091 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4092 ]
4093 );
4094 });
4095
4096 _ = editor.update(cx, |editor, window, cx| {
4097 editor.move_line_down(&MoveLineDown, window, cx);
4098 assert_eq!(
4099 editor.display_text(cx),
4100 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4101 );
4102 assert_eq!(
4103 editor.selections.display_ranges(cx),
4104 vec![
4105 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4106 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4107 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4108 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4109 ]
4110 );
4111 });
4112
4113 _ = editor.update(cx, |editor, window, cx| {
4114 editor.move_line_up(&MoveLineUp, window, cx);
4115 assert_eq!(
4116 editor.display_text(cx),
4117 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4118 );
4119 assert_eq!(
4120 editor.selections.display_ranges(cx),
4121 vec![
4122 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4123 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4124 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4125 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4126 ]
4127 );
4128 });
4129}
4130
4131#[gpui::test]
4132fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4133 init_test(cx, |_| {});
4134
4135 let editor = cx.add_window(|window, cx| {
4136 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4137 build_editor(buffer, window, cx)
4138 });
4139 _ = editor.update(cx, |editor, window, cx| {
4140 let snapshot = editor.buffer.read(cx).snapshot(cx);
4141 editor.insert_blocks(
4142 [BlockProperties {
4143 style: BlockStyle::Fixed,
4144 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4145 height: 1,
4146 render: Arc::new(|_| div().into_any()),
4147 priority: 0,
4148 }],
4149 Some(Autoscroll::fit()),
4150 cx,
4151 );
4152 editor.change_selections(None, window, cx, |s| {
4153 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4154 });
4155 editor.move_line_down(&MoveLineDown, window, cx);
4156 });
4157}
4158
4159#[gpui::test]
4160async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4161 init_test(cx, |_| {});
4162
4163 let mut cx = EditorTestContext::new(cx).await;
4164 cx.set_state(
4165 &"
4166 ˇzero
4167 one
4168 two
4169 three
4170 four
4171 five
4172 "
4173 .unindent(),
4174 );
4175
4176 // Create a four-line block that replaces three lines of text.
4177 cx.update_editor(|editor, window, cx| {
4178 let snapshot = editor.snapshot(window, cx);
4179 let snapshot = &snapshot.buffer_snapshot;
4180 let placement = BlockPlacement::Replace(
4181 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4182 );
4183 editor.insert_blocks(
4184 [BlockProperties {
4185 placement,
4186 height: 4,
4187 style: BlockStyle::Sticky,
4188 render: Arc::new(|_| gpui::div().into_any_element()),
4189 priority: 0,
4190 }],
4191 None,
4192 cx,
4193 );
4194 });
4195
4196 // Move down so that the cursor touches the block.
4197 cx.update_editor(|editor, window, cx| {
4198 editor.move_down(&Default::default(), window, cx);
4199 });
4200 cx.assert_editor_state(
4201 &"
4202 zero
4203 «one
4204 two
4205 threeˇ»
4206 four
4207 five
4208 "
4209 .unindent(),
4210 );
4211
4212 // Move down past the block.
4213 cx.update_editor(|editor, window, cx| {
4214 editor.move_down(&Default::default(), window, cx);
4215 });
4216 cx.assert_editor_state(
4217 &"
4218 zero
4219 one
4220 two
4221 three
4222 ˇfour
4223 five
4224 "
4225 .unindent(),
4226 );
4227}
4228
4229#[gpui::test]
4230fn test_transpose(cx: &mut TestAppContext) {
4231 init_test(cx, |_| {});
4232
4233 _ = cx.add_window(|window, cx| {
4234 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4235 editor.set_style(EditorStyle::default(), window, cx);
4236 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4237 editor.transpose(&Default::default(), window, cx);
4238 assert_eq!(editor.text(cx), "bac");
4239 assert_eq!(editor.selections.ranges(cx), [2..2]);
4240
4241 editor.transpose(&Default::default(), window, cx);
4242 assert_eq!(editor.text(cx), "bca");
4243 assert_eq!(editor.selections.ranges(cx), [3..3]);
4244
4245 editor.transpose(&Default::default(), window, cx);
4246 assert_eq!(editor.text(cx), "bac");
4247 assert_eq!(editor.selections.ranges(cx), [3..3]);
4248
4249 editor
4250 });
4251
4252 _ = cx.add_window(|window, cx| {
4253 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4254 editor.set_style(EditorStyle::default(), window, cx);
4255 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4256 editor.transpose(&Default::default(), window, cx);
4257 assert_eq!(editor.text(cx), "acb\nde");
4258 assert_eq!(editor.selections.ranges(cx), [3..3]);
4259
4260 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4261 editor.transpose(&Default::default(), window, cx);
4262 assert_eq!(editor.text(cx), "acbd\ne");
4263 assert_eq!(editor.selections.ranges(cx), [5..5]);
4264
4265 editor.transpose(&Default::default(), window, cx);
4266 assert_eq!(editor.text(cx), "acbde\n");
4267 assert_eq!(editor.selections.ranges(cx), [6..6]);
4268
4269 editor.transpose(&Default::default(), window, cx);
4270 assert_eq!(editor.text(cx), "acbd\ne");
4271 assert_eq!(editor.selections.ranges(cx), [6..6]);
4272
4273 editor
4274 });
4275
4276 _ = cx.add_window(|window, cx| {
4277 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4278 editor.set_style(EditorStyle::default(), window, cx);
4279 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4280 editor.transpose(&Default::default(), window, cx);
4281 assert_eq!(editor.text(cx), "bacd\ne");
4282 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4283
4284 editor.transpose(&Default::default(), window, cx);
4285 assert_eq!(editor.text(cx), "bcade\n");
4286 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4287
4288 editor.transpose(&Default::default(), window, cx);
4289 assert_eq!(editor.text(cx), "bcda\ne");
4290 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4291
4292 editor.transpose(&Default::default(), window, cx);
4293 assert_eq!(editor.text(cx), "bcade\n");
4294 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4295
4296 editor.transpose(&Default::default(), window, cx);
4297 assert_eq!(editor.text(cx), "bcaed\n");
4298 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4299
4300 editor
4301 });
4302
4303 _ = cx.add_window(|window, cx| {
4304 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4305 editor.set_style(EditorStyle::default(), window, cx);
4306 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4307 editor.transpose(&Default::default(), window, cx);
4308 assert_eq!(editor.text(cx), "🏀🍐✋");
4309 assert_eq!(editor.selections.ranges(cx), [8..8]);
4310
4311 editor.transpose(&Default::default(), window, cx);
4312 assert_eq!(editor.text(cx), "🏀✋🍐");
4313 assert_eq!(editor.selections.ranges(cx), [11..11]);
4314
4315 editor.transpose(&Default::default(), window, cx);
4316 assert_eq!(editor.text(cx), "🏀🍐✋");
4317 assert_eq!(editor.selections.ranges(cx), [11..11]);
4318
4319 editor
4320 });
4321}
4322
4323#[gpui::test]
4324async fn test_rewrap(cx: &mut TestAppContext) {
4325 init_test(cx, |_| {});
4326
4327 let mut cx = EditorTestContext::new(cx).await;
4328
4329 let language_with_c_comments = Arc::new(Language::new(
4330 LanguageConfig {
4331 line_comments: vec!["// ".into()],
4332 ..LanguageConfig::default()
4333 },
4334 None,
4335 ));
4336 let language_with_pound_comments = Arc::new(Language::new(
4337 LanguageConfig {
4338 line_comments: vec!["# ".into()],
4339 ..LanguageConfig::default()
4340 },
4341 None,
4342 ));
4343 let markdown_language = Arc::new(Language::new(
4344 LanguageConfig {
4345 name: "Markdown".into(),
4346 ..LanguageConfig::default()
4347 },
4348 None,
4349 ));
4350 let language_with_doc_comments = Arc::new(Language::new(
4351 LanguageConfig {
4352 line_comments: vec!["// ".into(), "/// ".into()],
4353 ..LanguageConfig::default()
4354 },
4355 Some(tree_sitter_rust::LANGUAGE.into()),
4356 ));
4357
4358 let plaintext_language = Arc::new(Language::new(
4359 LanguageConfig {
4360 name: "Plain Text".into(),
4361 ..LanguageConfig::default()
4362 },
4363 None,
4364 ));
4365
4366 assert_rewrap(
4367 indoc! {"
4368 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4369 "},
4370 indoc! {"
4371 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4372 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4373 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4374 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4375 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4376 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4377 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4378 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4379 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4380 // porttitor id. Aliquam id accumsan eros.
4381 "},
4382 language_with_c_comments.clone(),
4383 &mut cx,
4384 );
4385
4386 // Test that rewrapping works inside of a selection
4387 assert_rewrap(
4388 indoc! {"
4389 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.ˇ»
4390 "},
4391 indoc! {"
4392 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4393 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4394 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4395 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4396 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4397 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4398 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4399 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4400 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4401 // porttitor id. Aliquam id accumsan eros.ˇ»
4402 "},
4403 language_with_c_comments.clone(),
4404 &mut cx,
4405 );
4406
4407 // Test that cursors that expand to the same region are collapsed.
4408 assert_rewrap(
4409 indoc! {"
4410 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4411 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4412 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4413 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4414 "},
4415 indoc! {"
4416 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4417 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4418 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4419 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4420 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4421 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4422 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4423 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4424 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4425 // porttitor id. Aliquam id accumsan eros.
4426 "},
4427 language_with_c_comments.clone(),
4428 &mut cx,
4429 );
4430
4431 // Test that non-contiguous selections are treated separately.
4432 assert_rewrap(
4433 indoc! {"
4434 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4435 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4436 //
4437 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4438 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4439 "},
4440 indoc! {"
4441 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4442 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4443 // auctor, eu lacinia sapien scelerisque.
4444 //
4445 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4446 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4447 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4448 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4449 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4450 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4451 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4452 "},
4453 language_with_c_comments.clone(),
4454 &mut cx,
4455 );
4456
4457 // Test that different comment prefixes are supported.
4458 assert_rewrap(
4459 indoc! {"
4460 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4461 "},
4462 indoc! {"
4463 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4464 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4465 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4466 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4467 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4468 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4469 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4470 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4471 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4472 # accumsan eros.
4473 "},
4474 language_with_pound_comments.clone(),
4475 &mut cx,
4476 );
4477
4478 // Test that rewrapping is ignored outside of comments in most languages.
4479 assert_rewrap(
4480 indoc! {"
4481 /// Adds two numbers.
4482 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4483 fn add(a: u32, b: u32) -> u32 {
4484 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
4485 }
4486 "},
4487 indoc! {"
4488 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4489 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4490 fn add(a: u32, b: u32) -> u32 {
4491 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
4492 }
4493 "},
4494 language_with_doc_comments.clone(),
4495 &mut cx,
4496 );
4497
4498 // Test that rewrapping works in Markdown and Plain Text languages.
4499 assert_rewrap(
4500 indoc! {"
4501 # Hello
4502
4503 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
4504 "},
4505 indoc! {"
4506 # Hello
4507
4508 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4509 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4510 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4511 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4512 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4513 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4514 Integer sit amet scelerisque nisi.
4515 "},
4516 markdown_language,
4517 &mut cx,
4518 );
4519
4520 assert_rewrap(
4521 indoc! {"
4522 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
4523 "},
4524 indoc! {"
4525 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4526 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4527 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4528 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4529 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4530 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4531 Integer sit amet scelerisque nisi.
4532 "},
4533 plaintext_language,
4534 &mut cx,
4535 );
4536
4537 // Test rewrapping unaligned comments in a selection.
4538 assert_rewrap(
4539 indoc! {"
4540 fn foo() {
4541 if true {
4542 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4543 // Praesent semper egestas tellus id dignissim.ˇ»
4544 do_something();
4545 } else {
4546 //
4547 }
4548 }
4549 "},
4550 indoc! {"
4551 fn foo() {
4552 if true {
4553 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4554 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4555 // egestas tellus id dignissim.ˇ»
4556 do_something();
4557 } else {
4558 //
4559 }
4560 }
4561 "},
4562 language_with_doc_comments.clone(),
4563 &mut cx,
4564 );
4565
4566 assert_rewrap(
4567 indoc! {"
4568 fn foo() {
4569 if true {
4570 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4571 // Praesent semper egestas tellus id dignissim.»
4572 do_something();
4573 } else {
4574 //
4575 }
4576
4577 }
4578 "},
4579 indoc! {"
4580 fn foo() {
4581 if true {
4582 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4583 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4584 // egestas tellus id dignissim.»
4585 do_something();
4586 } else {
4587 //
4588 }
4589
4590 }
4591 "},
4592 language_with_doc_comments.clone(),
4593 &mut cx,
4594 );
4595
4596 #[track_caller]
4597 fn assert_rewrap(
4598 unwrapped_text: &str,
4599 wrapped_text: &str,
4600 language: Arc<Language>,
4601 cx: &mut EditorTestContext,
4602 ) {
4603 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4604 cx.set_state(unwrapped_text);
4605 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4606 cx.assert_editor_state(wrapped_text);
4607 }
4608}
4609
4610#[gpui::test]
4611async fn test_clipboard(cx: &mut gpui::TestAppContext) {
4612 init_test(cx, |_| {});
4613
4614 let mut cx = EditorTestContext::new(cx).await;
4615
4616 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4617 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4618 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4619
4620 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4621 cx.set_state("two ˇfour ˇsix ˇ");
4622 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4623 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4624
4625 // Paste again but with only two cursors. Since the number of cursors doesn't
4626 // match the number of slices in the clipboard, the entire clipboard text
4627 // is pasted at each cursor.
4628 cx.set_state("ˇtwo one✅ four three six five ˇ");
4629 cx.update_editor(|e, window, cx| {
4630 e.handle_input("( ", window, cx);
4631 e.paste(&Paste, window, cx);
4632 e.handle_input(") ", window, cx);
4633 });
4634 cx.assert_editor_state(
4635 &([
4636 "( one✅ ",
4637 "three ",
4638 "five ) ˇtwo one✅ four three six five ( one✅ ",
4639 "three ",
4640 "five ) ˇ",
4641 ]
4642 .join("\n")),
4643 );
4644
4645 // Cut with three selections, one of which is full-line.
4646 cx.set_state(indoc! {"
4647 1«2ˇ»3
4648 4ˇ567
4649 «8ˇ»9"});
4650 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4651 cx.assert_editor_state(indoc! {"
4652 1ˇ3
4653 ˇ9"});
4654
4655 // Paste with three selections, noticing how the copied selection that was full-line
4656 // gets inserted before the second cursor.
4657 cx.set_state(indoc! {"
4658 1ˇ3
4659 9ˇ
4660 «oˇ»ne"});
4661 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4662 cx.assert_editor_state(indoc! {"
4663 12ˇ3
4664 4567
4665 9ˇ
4666 8ˇne"});
4667
4668 // Copy with a single cursor only, which writes the whole line into the clipboard.
4669 cx.set_state(indoc! {"
4670 The quick brown
4671 fox juˇmps over
4672 the lazy dog"});
4673 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4674 assert_eq!(
4675 cx.read_from_clipboard()
4676 .and_then(|item| item.text().as_deref().map(str::to_string)),
4677 Some("fox jumps over\n".to_string())
4678 );
4679
4680 // Paste with three selections, noticing how the copied full-line selection is inserted
4681 // before the empty selections but replaces the selection that is non-empty.
4682 cx.set_state(indoc! {"
4683 Tˇhe quick brown
4684 «foˇ»x jumps over
4685 tˇhe lazy dog"});
4686 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4687 cx.assert_editor_state(indoc! {"
4688 fox jumps over
4689 Tˇhe quick brown
4690 fox jumps over
4691 ˇx jumps over
4692 fox jumps over
4693 tˇhe lazy dog"});
4694}
4695
4696#[gpui::test]
4697async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
4698 init_test(cx, |_| {});
4699
4700 let mut cx = EditorTestContext::new(cx).await;
4701 let language = Arc::new(Language::new(
4702 LanguageConfig::default(),
4703 Some(tree_sitter_rust::LANGUAGE.into()),
4704 ));
4705 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4706
4707 // Cut an indented block, without the leading whitespace.
4708 cx.set_state(indoc! {"
4709 const a: B = (
4710 c(),
4711 «d(
4712 e,
4713 f
4714 )ˇ»
4715 );
4716 "});
4717 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4718 cx.assert_editor_state(indoc! {"
4719 const a: B = (
4720 c(),
4721 ˇ
4722 );
4723 "});
4724
4725 // Paste it at the same position.
4726 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4727 cx.assert_editor_state(indoc! {"
4728 const a: B = (
4729 c(),
4730 d(
4731 e,
4732 f
4733 )ˇ
4734 );
4735 "});
4736
4737 // Paste it at a line with a lower indent level.
4738 cx.set_state(indoc! {"
4739 ˇ
4740 const a: B = (
4741 c(),
4742 );
4743 "});
4744 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4745 cx.assert_editor_state(indoc! {"
4746 d(
4747 e,
4748 f
4749 )ˇ
4750 const a: B = (
4751 c(),
4752 );
4753 "});
4754
4755 // Cut an indented block, with the leading whitespace.
4756 cx.set_state(indoc! {"
4757 const a: B = (
4758 c(),
4759 « d(
4760 e,
4761 f
4762 )
4763 ˇ»);
4764 "});
4765 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4766 cx.assert_editor_state(indoc! {"
4767 const a: B = (
4768 c(),
4769 ˇ);
4770 "});
4771
4772 // Paste it at the same position.
4773 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4774 cx.assert_editor_state(indoc! {"
4775 const a: B = (
4776 c(),
4777 d(
4778 e,
4779 f
4780 )
4781 ˇ);
4782 "});
4783
4784 // Paste it at a line with a higher indent level.
4785 cx.set_state(indoc! {"
4786 const a: B = (
4787 c(),
4788 d(
4789 e,
4790 fˇ
4791 )
4792 );
4793 "});
4794 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4795 cx.assert_editor_state(indoc! {"
4796 const a: B = (
4797 c(),
4798 d(
4799 e,
4800 f d(
4801 e,
4802 f
4803 )
4804 ˇ
4805 )
4806 );
4807 "});
4808}
4809
4810#[gpui::test]
4811fn test_select_all(cx: &mut TestAppContext) {
4812 init_test(cx, |_| {});
4813
4814 let editor = cx.add_window(|window, cx| {
4815 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
4816 build_editor(buffer, window, cx)
4817 });
4818 _ = editor.update(cx, |editor, window, cx| {
4819 editor.select_all(&SelectAll, window, cx);
4820 assert_eq!(
4821 editor.selections.display_ranges(cx),
4822 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
4823 );
4824 });
4825}
4826
4827#[gpui::test]
4828fn test_select_line(cx: &mut TestAppContext) {
4829 init_test(cx, |_| {});
4830
4831 let editor = cx.add_window(|window, cx| {
4832 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
4833 build_editor(buffer, window, cx)
4834 });
4835 _ = editor.update(cx, |editor, window, cx| {
4836 editor.change_selections(None, window, cx, |s| {
4837 s.select_display_ranges([
4838 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4839 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4840 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4841 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
4842 ])
4843 });
4844 editor.select_line(&SelectLine, window, cx);
4845 assert_eq!(
4846 editor.selections.display_ranges(cx),
4847 vec![
4848 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
4849 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
4850 ]
4851 );
4852 });
4853
4854 _ = editor.update(cx, |editor, window, cx| {
4855 editor.select_line(&SelectLine, window, cx);
4856 assert_eq!(
4857 editor.selections.display_ranges(cx),
4858 vec![
4859 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
4860 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
4861 ]
4862 );
4863 });
4864
4865 _ = editor.update(cx, |editor, window, cx| {
4866 editor.select_line(&SelectLine, window, cx);
4867 assert_eq!(
4868 editor.selections.display_ranges(cx),
4869 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
4870 );
4871 });
4872}
4873
4874#[gpui::test]
4875fn test_split_selection_into_lines(cx: &mut TestAppContext) {
4876 init_test(cx, |_| {});
4877
4878 let editor = cx.add_window(|window, cx| {
4879 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
4880 build_editor(buffer, window, cx)
4881 });
4882 _ = editor.update(cx, |editor, window, cx| {
4883 editor.fold_creases(
4884 vec![
4885 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4886 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4887 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4888 ],
4889 true,
4890 window,
4891 cx,
4892 );
4893 editor.change_selections(None, window, cx, |s| {
4894 s.select_display_ranges([
4895 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4896 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4897 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4898 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
4899 ])
4900 });
4901 assert_eq!(
4902 editor.display_text(cx),
4903 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
4904 );
4905 });
4906
4907 _ = editor.update(cx, |editor, window, cx| {
4908 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
4909 assert_eq!(
4910 editor.display_text(cx),
4911 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
4912 );
4913 assert_eq!(
4914 editor.selections.display_ranges(cx),
4915 [
4916 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4917 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4918 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4919 DisplayPoint::new(DisplayRow(5), 4)..DisplayPoint::new(DisplayRow(5), 4)
4920 ]
4921 );
4922 });
4923
4924 _ = editor.update(cx, |editor, window, cx| {
4925 editor.change_selections(None, window, cx, |s| {
4926 s.select_display_ranges([
4927 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
4928 ])
4929 });
4930 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
4931 assert_eq!(
4932 editor.display_text(cx),
4933 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
4934 );
4935 assert_eq!(
4936 editor.selections.display_ranges(cx),
4937 [
4938 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
4939 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
4940 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
4941 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
4942 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
4943 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
4944 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5),
4945 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(7), 0)
4946 ]
4947 );
4948 });
4949}
4950
4951#[gpui::test]
4952async fn test_add_selection_above_below(cx: &mut TestAppContext) {
4953 init_test(cx, |_| {});
4954
4955 let mut cx = EditorTestContext::new(cx).await;
4956
4957 // let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx);
4958 cx.set_state(indoc!(
4959 r#"abc
4960 defˇghi
4961
4962 jk
4963 nlmo
4964 "#
4965 ));
4966
4967 cx.update_editor(|editor, window, cx| {
4968 editor.add_selection_above(&Default::default(), window, cx);
4969 });
4970
4971 cx.assert_editor_state(indoc!(
4972 r#"abcˇ
4973 defˇghi
4974
4975 jk
4976 nlmo
4977 "#
4978 ));
4979
4980 cx.update_editor(|editor, window, cx| {
4981 editor.add_selection_above(&Default::default(), window, cx);
4982 });
4983
4984 cx.assert_editor_state(indoc!(
4985 r#"abcˇ
4986 defˇghi
4987
4988 jk
4989 nlmo
4990 "#
4991 ));
4992
4993 cx.update_editor(|editor, window, cx| {
4994 editor.add_selection_below(&Default::default(), window, cx);
4995 });
4996
4997 cx.assert_editor_state(indoc!(
4998 r#"abc
4999 defˇghi
5000
5001 jk
5002 nlmo
5003 "#
5004 ));
5005
5006 cx.update_editor(|editor, window, cx| {
5007 editor.undo_selection(&Default::default(), window, cx);
5008 });
5009
5010 cx.assert_editor_state(indoc!(
5011 r#"abcˇ
5012 defˇghi
5013
5014 jk
5015 nlmo
5016 "#
5017 ));
5018
5019 cx.update_editor(|editor, window, cx| {
5020 editor.redo_selection(&Default::default(), window, cx);
5021 });
5022
5023 cx.assert_editor_state(indoc!(
5024 r#"abc
5025 defˇghi
5026
5027 jk
5028 nlmo
5029 "#
5030 ));
5031
5032 cx.update_editor(|editor, window, cx| {
5033 editor.add_selection_below(&Default::default(), window, cx);
5034 });
5035
5036 cx.assert_editor_state(indoc!(
5037 r#"abc
5038 defˇghi
5039
5040 jk
5041 nlmˇo
5042 "#
5043 ));
5044
5045 cx.update_editor(|editor, window, cx| {
5046 editor.add_selection_below(&Default::default(), window, cx);
5047 });
5048
5049 cx.assert_editor_state(indoc!(
5050 r#"abc
5051 defˇghi
5052
5053 jk
5054 nlmˇo
5055 "#
5056 ));
5057
5058 // change selections
5059 cx.set_state(indoc!(
5060 r#"abc
5061 def«ˇg»hi
5062
5063 jk
5064 nlmo
5065 "#
5066 ));
5067
5068 cx.update_editor(|editor, window, cx| {
5069 editor.add_selection_below(&Default::default(), window, cx);
5070 });
5071
5072 cx.assert_editor_state(indoc!(
5073 r#"abc
5074 def«ˇg»hi
5075
5076 jk
5077 nlm«ˇo»
5078 "#
5079 ));
5080
5081 cx.update_editor(|editor, window, cx| {
5082 editor.add_selection_below(&Default::default(), window, cx);
5083 });
5084
5085 cx.assert_editor_state(indoc!(
5086 r#"abc
5087 def«ˇg»hi
5088
5089 jk
5090 nlm«ˇo»
5091 "#
5092 ));
5093
5094 cx.update_editor(|editor, window, cx| {
5095 editor.add_selection_above(&Default::default(), window, cx);
5096 });
5097
5098 cx.assert_editor_state(indoc!(
5099 r#"abc
5100 def«ˇg»hi
5101
5102 jk
5103 nlmo
5104 "#
5105 ));
5106
5107 cx.update_editor(|editor, window, cx| {
5108 editor.add_selection_above(&Default::default(), window, cx);
5109 });
5110
5111 cx.assert_editor_state(indoc!(
5112 r#"abc
5113 def«ˇg»hi
5114
5115 jk
5116 nlmo
5117 "#
5118 ));
5119
5120 // Change selections again
5121 cx.set_state(indoc!(
5122 r#"a«bc
5123 defgˇ»hi
5124
5125 jk
5126 nlmo
5127 "#
5128 ));
5129
5130 cx.update_editor(|editor, window, cx| {
5131 editor.add_selection_below(&Default::default(), window, cx);
5132 });
5133
5134 cx.assert_editor_state(indoc!(
5135 r#"a«bcˇ»
5136 d«efgˇ»hi
5137
5138 j«kˇ»
5139 nlmo
5140 "#
5141 ));
5142
5143 cx.update_editor(|editor, window, cx| {
5144 editor.add_selection_below(&Default::default(), window, cx);
5145 });
5146 cx.assert_editor_state(indoc!(
5147 r#"a«bcˇ»
5148 d«efgˇ»hi
5149
5150 j«kˇ»
5151 n«lmoˇ»
5152 "#
5153 ));
5154 cx.update_editor(|editor, window, cx| {
5155 editor.add_selection_above(&Default::default(), window, cx);
5156 });
5157
5158 cx.assert_editor_state(indoc!(
5159 r#"a«bcˇ»
5160 d«efgˇ»hi
5161
5162 j«kˇ»
5163 nlmo
5164 "#
5165 ));
5166
5167 // Change selections again
5168 cx.set_state(indoc!(
5169 r#"abc
5170 d«ˇefghi
5171
5172 jk
5173 nlm»o
5174 "#
5175 ));
5176
5177 cx.update_editor(|editor, window, cx| {
5178 editor.add_selection_above(&Default::default(), window, cx);
5179 });
5180
5181 cx.assert_editor_state(indoc!(
5182 r#"a«ˇbc»
5183 d«ˇef»ghi
5184
5185 j«ˇk»
5186 n«ˇlm»o
5187 "#
5188 ));
5189
5190 cx.update_editor(|editor, window, cx| {
5191 editor.add_selection_below(&Default::default(), window, cx);
5192 });
5193
5194 cx.assert_editor_state(indoc!(
5195 r#"abc
5196 d«ˇef»ghi
5197
5198 j«ˇk»
5199 n«ˇlm»o
5200 "#
5201 ));
5202}
5203
5204#[gpui::test]
5205async fn test_select_next(cx: &mut gpui::TestAppContext) {
5206 init_test(cx, |_| {});
5207
5208 let mut cx = EditorTestContext::new(cx).await;
5209 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5210
5211 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5212 .unwrap();
5213 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5214
5215 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5216 .unwrap();
5217 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5218
5219 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5220 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5221
5222 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5223 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5224
5225 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5226 .unwrap();
5227 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5228
5229 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5230 .unwrap();
5231 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5232}
5233
5234#[gpui::test]
5235async fn test_select_all_matches(cx: &mut gpui::TestAppContext) {
5236 init_test(cx, |_| {});
5237
5238 let mut cx = EditorTestContext::new(cx).await;
5239 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5240
5241 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5242 .unwrap();
5243 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5244}
5245
5246#[gpui::test]
5247async fn test_select_next_with_multiple_carets(cx: &mut gpui::TestAppContext) {
5248 init_test(cx, |_| {});
5249
5250 let mut cx = EditorTestContext::new(cx).await;
5251 cx.set_state(
5252 r#"let foo = 2;
5253lˇet foo = 2;
5254let fooˇ = 2;
5255let foo = 2;
5256let foo = ˇ2;"#,
5257 );
5258
5259 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5260 .unwrap();
5261 cx.assert_editor_state(
5262 r#"let foo = 2;
5263«letˇ» foo = 2;
5264let «fooˇ» = 2;
5265let foo = 2;
5266let foo = «2ˇ»;"#,
5267 );
5268
5269 // noop for multiple selections with different contents
5270 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5271 .unwrap();
5272 cx.assert_editor_state(
5273 r#"let foo = 2;
5274«letˇ» foo = 2;
5275let «fooˇ» = 2;
5276let foo = 2;
5277let foo = «2ˇ»;"#,
5278 );
5279}
5280
5281#[gpui::test]
5282async fn test_select_previous_multibuffer(cx: &mut gpui::TestAppContext) {
5283 init_test(cx, |_| {});
5284
5285 let mut cx =
5286 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
5287
5288 cx.assert_editor_state(indoc! {"
5289 ˇbbb
5290 ccc
5291
5292 bbb
5293 ccc
5294 "});
5295 cx.dispatch_action(SelectPrevious::default());
5296 cx.assert_editor_state(indoc! {"
5297 «bbbˇ»
5298 ccc
5299
5300 bbb
5301 ccc
5302 "});
5303 cx.dispatch_action(SelectPrevious::default());
5304 cx.assert_editor_state(indoc! {"
5305 «bbbˇ»
5306 ccc
5307
5308 «bbbˇ»
5309 ccc
5310 "});
5311}
5312
5313#[gpui::test]
5314async fn test_select_previous_with_single_caret(cx: &mut gpui::TestAppContext) {
5315 init_test(cx, |_| {});
5316
5317 let mut cx = EditorTestContext::new(cx).await;
5318 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5319
5320 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5321 .unwrap();
5322 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5323
5324 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5325 .unwrap();
5326 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5327
5328 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5329 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5330
5331 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5332 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5333
5334 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5335 .unwrap();
5336 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
5337
5338 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5339 .unwrap();
5340 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
5341
5342 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5343 .unwrap();
5344 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5345}
5346
5347#[gpui::test]
5348async fn test_select_previous_with_multiple_carets(cx: &mut gpui::TestAppContext) {
5349 init_test(cx, |_| {});
5350
5351 let mut cx = EditorTestContext::new(cx).await;
5352 cx.set_state(
5353 r#"let foo = 2;
5354lˇet foo = 2;
5355let fooˇ = 2;
5356let foo = 2;
5357let foo = ˇ2;"#,
5358 );
5359
5360 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5361 .unwrap();
5362 cx.assert_editor_state(
5363 r#"let foo = 2;
5364«letˇ» foo = 2;
5365let «fooˇ» = 2;
5366let foo = 2;
5367let foo = «2ˇ»;"#,
5368 );
5369
5370 // noop for multiple selections with different contents
5371 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5372 .unwrap();
5373 cx.assert_editor_state(
5374 r#"let foo = 2;
5375«letˇ» foo = 2;
5376let «fooˇ» = 2;
5377let foo = 2;
5378let foo = «2ˇ»;"#,
5379 );
5380}
5381
5382#[gpui::test]
5383async fn test_select_previous_with_single_selection(cx: &mut gpui::TestAppContext) {
5384 init_test(cx, |_| {});
5385
5386 let mut cx = EditorTestContext::new(cx).await;
5387 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5388
5389 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5390 .unwrap();
5391 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5392
5393 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5394 .unwrap();
5395 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5396
5397 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5398 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5399
5400 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5401 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5402
5403 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5404 .unwrap();
5405 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
5406
5407 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5408 .unwrap();
5409 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5410}
5411
5412#[gpui::test]
5413async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
5414 init_test(cx, |_| {});
5415
5416 let language = Arc::new(Language::new(
5417 LanguageConfig::default(),
5418 Some(tree_sitter_rust::LANGUAGE.into()),
5419 ));
5420
5421 let text = r#"
5422 use mod1::mod2::{mod3, mod4};
5423
5424 fn fn_1(param1: bool, param2: &str) {
5425 let var1 = "text";
5426 }
5427 "#
5428 .unindent();
5429
5430 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5431 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5432 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5433
5434 editor
5435 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5436 .await;
5437
5438 editor.update_in(cx, |editor, window, cx| {
5439 editor.change_selections(None, window, cx, |s| {
5440 s.select_display_ranges([
5441 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
5442 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
5443 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
5444 ]);
5445 });
5446 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5447 });
5448 editor.update(cx, |editor, cx| {
5449 assert_text_with_selections(
5450 editor,
5451 indoc! {r#"
5452 use mod1::mod2::{mod3, «mod4ˇ»};
5453
5454 fn fn_1«ˇ(param1: bool, param2: &str)» {
5455 let var1 = "«textˇ»";
5456 }
5457 "#},
5458 cx,
5459 );
5460 });
5461
5462 editor.update_in(cx, |editor, window, cx| {
5463 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5464 });
5465 editor.update(cx, |editor, cx| {
5466 assert_text_with_selections(
5467 editor,
5468 indoc! {r#"
5469 use mod1::mod2::«{mod3, mod4}ˇ»;
5470
5471 «ˇfn fn_1(param1: bool, param2: &str) {
5472 let var1 = "text";
5473 }»
5474 "#},
5475 cx,
5476 );
5477 });
5478
5479 editor.update_in(cx, |editor, window, cx| {
5480 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5481 });
5482 assert_eq!(
5483 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
5484 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
5485 );
5486
5487 // Trying to expand the selected syntax node one more time has no effect.
5488 editor.update_in(cx, |editor, window, cx| {
5489 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5490 });
5491 assert_eq!(
5492 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
5493 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
5494 );
5495
5496 editor.update_in(cx, |editor, window, cx| {
5497 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5498 });
5499 editor.update(cx, |editor, cx| {
5500 assert_text_with_selections(
5501 editor,
5502 indoc! {r#"
5503 use mod1::mod2::«{mod3, mod4}ˇ»;
5504
5505 «ˇfn fn_1(param1: bool, param2: &str) {
5506 let var1 = "text";
5507 }»
5508 "#},
5509 cx,
5510 );
5511 });
5512
5513 editor.update_in(cx, |editor, window, cx| {
5514 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5515 });
5516 editor.update(cx, |editor, cx| {
5517 assert_text_with_selections(
5518 editor,
5519 indoc! {r#"
5520 use mod1::mod2::{mod3, «mod4ˇ»};
5521
5522 fn fn_1«ˇ(param1: bool, param2: &str)» {
5523 let var1 = "«textˇ»";
5524 }
5525 "#},
5526 cx,
5527 );
5528 });
5529
5530 editor.update_in(cx, |editor, window, cx| {
5531 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5532 });
5533 editor.update(cx, |editor, cx| {
5534 assert_text_with_selections(
5535 editor,
5536 indoc! {r#"
5537 use mod1::mod2::{mod3, mo«ˇ»d4};
5538
5539 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
5540 let var1 = "te«ˇ»xt";
5541 }
5542 "#},
5543 cx,
5544 );
5545 });
5546
5547 // Trying to shrink the selected syntax node one more time has no effect.
5548 editor.update_in(cx, |editor, window, cx| {
5549 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
5550 });
5551 editor.update_in(cx, |editor, _, cx| {
5552 assert_text_with_selections(
5553 editor,
5554 indoc! {r#"
5555 use mod1::mod2::{mod3, mo«ˇ»d4};
5556
5557 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
5558 let var1 = "te«ˇ»xt";
5559 }
5560 "#},
5561 cx,
5562 );
5563 });
5564
5565 // Ensure that we keep expanding the selection if the larger selection starts or ends within
5566 // a fold.
5567 editor.update_in(cx, |editor, window, cx| {
5568 editor.fold_creases(
5569 vec![
5570 Crease::simple(
5571 Point::new(0, 21)..Point::new(0, 24),
5572 FoldPlaceholder::test(),
5573 ),
5574 Crease::simple(
5575 Point::new(3, 20)..Point::new(3, 22),
5576 FoldPlaceholder::test(),
5577 ),
5578 ],
5579 true,
5580 window,
5581 cx,
5582 );
5583 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
5584 });
5585 editor.update(cx, |editor, cx| {
5586 assert_text_with_selections(
5587 editor,
5588 indoc! {r#"
5589 use mod1::mod2::«{mod3, mod4}ˇ»;
5590
5591 fn fn_1«ˇ(param1: bool, param2: &str)» {
5592 «let var1 = "text";ˇ»
5593 }
5594 "#},
5595 cx,
5596 );
5597 });
5598}
5599
5600#[gpui::test]
5601async fn test_fold_function_bodies(cx: &mut gpui::TestAppContext) {
5602 init_test(cx, |_| {});
5603
5604 let base_text = r#"
5605 impl A {
5606 // this is an unstaged comment
5607
5608 fn b() {
5609 c();
5610 }
5611
5612 // this is another unstaged comment
5613
5614 fn d() {
5615 // e
5616 // f
5617 }
5618 }
5619
5620 fn g() {
5621 // h
5622 }
5623 "#
5624 .unindent();
5625
5626 let text = r#"
5627 ˇimpl A {
5628
5629 fn b() {
5630 c();
5631 }
5632
5633 fn d() {
5634 // e
5635 // f
5636 }
5637 }
5638
5639 fn g() {
5640 // h
5641 }
5642 "#
5643 .unindent();
5644
5645 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
5646 cx.set_state(&text);
5647 cx.set_diff_base(&base_text);
5648 cx.update_editor(|editor, window, cx| {
5649 editor.expand_all_diff_hunks(&Default::default(), window, cx);
5650 });
5651
5652 cx.assert_state_with_diff(
5653 "
5654 ˇimpl A {
5655 - // this is an unstaged comment
5656
5657 fn b() {
5658 c();
5659 }
5660
5661 - // this is another unstaged comment
5662 -
5663 fn d() {
5664 // e
5665 // f
5666 }
5667 }
5668
5669 fn g() {
5670 // h
5671 }
5672 "
5673 .unindent(),
5674 );
5675
5676 let expected_display_text = "
5677 impl A {
5678 // this is an unstaged comment
5679
5680 fn b() {
5681 ⋯
5682 }
5683
5684 // this is another unstaged comment
5685
5686 fn d() {
5687 ⋯
5688 }
5689 }
5690
5691 fn g() {
5692 ⋯
5693 }
5694 "
5695 .unindent();
5696
5697 cx.update_editor(|editor, window, cx| {
5698 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
5699 assert_eq!(editor.display_text(cx), expected_display_text);
5700 });
5701}
5702
5703#[gpui::test]
5704async fn test_autoindent(cx: &mut gpui::TestAppContext) {
5705 init_test(cx, |_| {});
5706
5707 let language = Arc::new(
5708 Language::new(
5709 LanguageConfig {
5710 brackets: BracketPairConfig {
5711 pairs: vec![
5712 BracketPair {
5713 start: "{".to_string(),
5714 end: "}".to_string(),
5715 close: false,
5716 surround: false,
5717 newline: true,
5718 },
5719 BracketPair {
5720 start: "(".to_string(),
5721 end: ")".to_string(),
5722 close: false,
5723 surround: false,
5724 newline: true,
5725 },
5726 ],
5727 ..Default::default()
5728 },
5729 ..Default::default()
5730 },
5731 Some(tree_sitter_rust::LANGUAGE.into()),
5732 )
5733 .with_indents_query(
5734 r#"
5735 (_ "(" ")" @end) @indent
5736 (_ "{" "}" @end) @indent
5737 "#,
5738 )
5739 .unwrap(),
5740 );
5741
5742 let text = "fn a() {}";
5743
5744 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5745 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5746 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5747 editor
5748 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5749 .await;
5750
5751 editor.update_in(cx, |editor, window, cx| {
5752 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
5753 editor.newline(&Newline, window, cx);
5754 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
5755 assert_eq!(
5756 editor.selections.ranges(cx),
5757 &[
5758 Point::new(1, 4)..Point::new(1, 4),
5759 Point::new(3, 4)..Point::new(3, 4),
5760 Point::new(5, 0)..Point::new(5, 0)
5761 ]
5762 );
5763 });
5764}
5765
5766#[gpui::test]
5767async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
5768 init_test(cx, |_| {});
5769
5770 {
5771 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
5772 cx.set_state(indoc! {"
5773 impl A {
5774
5775 fn b() {}
5776
5777 «fn c() {
5778
5779 }ˇ»
5780 }
5781 "});
5782
5783 cx.update_editor(|editor, window, cx| {
5784 editor.autoindent(&Default::default(), window, cx);
5785 });
5786
5787 cx.assert_editor_state(indoc! {"
5788 impl A {
5789
5790 fn b() {}
5791
5792 «fn c() {
5793
5794 }ˇ»
5795 }
5796 "});
5797 }
5798
5799 {
5800 let mut cx = EditorTestContext::new_multibuffer(
5801 cx,
5802 [indoc! { "
5803 impl A {
5804 «
5805 // a
5806 fn b(){}
5807 »
5808 «
5809 }
5810 fn c(){}
5811 »
5812 "}],
5813 );
5814
5815 let buffer = cx.update_editor(|editor, _, cx| {
5816 let buffer = editor.buffer().update(cx, |buffer, _| {
5817 buffer.all_buffers().iter().next().unwrap().clone()
5818 });
5819 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5820 buffer
5821 });
5822
5823 cx.run_until_parked();
5824 cx.update_editor(|editor, window, cx| {
5825 editor.select_all(&Default::default(), window, cx);
5826 editor.autoindent(&Default::default(), window, cx)
5827 });
5828 cx.run_until_parked();
5829
5830 cx.update(|_, cx| {
5831 pretty_assertions::assert_eq!(
5832 buffer.read(cx).text(),
5833 indoc! { "
5834 impl A {
5835
5836 // a
5837 fn b(){}
5838
5839
5840 }
5841 fn c(){}
5842
5843 " }
5844 )
5845 });
5846 }
5847}
5848
5849#[gpui::test]
5850async fn test_autoclose_and_auto_surround_pairs(cx: &mut gpui::TestAppContext) {
5851 init_test(cx, |_| {});
5852
5853 let mut cx = EditorTestContext::new(cx).await;
5854
5855 let language = Arc::new(Language::new(
5856 LanguageConfig {
5857 brackets: BracketPairConfig {
5858 pairs: vec![
5859 BracketPair {
5860 start: "{".to_string(),
5861 end: "}".to_string(),
5862 close: true,
5863 surround: true,
5864 newline: true,
5865 },
5866 BracketPair {
5867 start: "(".to_string(),
5868 end: ")".to_string(),
5869 close: true,
5870 surround: true,
5871 newline: true,
5872 },
5873 BracketPair {
5874 start: "/*".to_string(),
5875 end: " */".to_string(),
5876 close: true,
5877 surround: true,
5878 newline: true,
5879 },
5880 BracketPair {
5881 start: "[".to_string(),
5882 end: "]".to_string(),
5883 close: false,
5884 surround: false,
5885 newline: true,
5886 },
5887 BracketPair {
5888 start: "\"".to_string(),
5889 end: "\"".to_string(),
5890 close: true,
5891 surround: true,
5892 newline: false,
5893 },
5894 BracketPair {
5895 start: "<".to_string(),
5896 end: ">".to_string(),
5897 close: false,
5898 surround: true,
5899 newline: true,
5900 },
5901 ],
5902 ..Default::default()
5903 },
5904 autoclose_before: "})]".to_string(),
5905 ..Default::default()
5906 },
5907 Some(tree_sitter_rust::LANGUAGE.into()),
5908 ));
5909
5910 cx.language_registry().add(language.clone());
5911 cx.update_buffer(|buffer, cx| {
5912 buffer.set_language(Some(language), cx);
5913 });
5914
5915 cx.set_state(
5916 &r#"
5917 🏀ˇ
5918 εˇ
5919 ❤️ˇ
5920 "#
5921 .unindent(),
5922 );
5923
5924 // autoclose multiple nested brackets at multiple cursors
5925 cx.update_editor(|editor, window, cx| {
5926 editor.handle_input("{", window, cx);
5927 editor.handle_input("{", window, cx);
5928 editor.handle_input("{", window, cx);
5929 });
5930 cx.assert_editor_state(
5931 &"
5932 🏀{{{ˇ}}}
5933 ε{{{ˇ}}}
5934 ❤️{{{ˇ}}}
5935 "
5936 .unindent(),
5937 );
5938
5939 // insert a different closing bracket
5940 cx.update_editor(|editor, window, cx| {
5941 editor.handle_input(")", window, cx);
5942 });
5943 cx.assert_editor_state(
5944 &"
5945 🏀{{{)ˇ}}}
5946 ε{{{)ˇ}}}
5947 ❤️{{{)ˇ}}}
5948 "
5949 .unindent(),
5950 );
5951
5952 // skip over the auto-closed brackets when typing a closing bracket
5953 cx.update_editor(|editor, window, cx| {
5954 editor.move_right(&MoveRight, window, cx);
5955 editor.handle_input("}", window, cx);
5956 editor.handle_input("}", window, cx);
5957 editor.handle_input("}", window, cx);
5958 });
5959 cx.assert_editor_state(
5960 &"
5961 🏀{{{)}}}}ˇ
5962 ε{{{)}}}}ˇ
5963 ❤️{{{)}}}}ˇ
5964 "
5965 .unindent(),
5966 );
5967
5968 // autoclose multi-character pairs
5969 cx.set_state(
5970 &"
5971 ˇ
5972 ˇ
5973 "
5974 .unindent(),
5975 );
5976 cx.update_editor(|editor, window, cx| {
5977 editor.handle_input("/", window, cx);
5978 editor.handle_input("*", window, cx);
5979 });
5980 cx.assert_editor_state(
5981 &"
5982 /*ˇ */
5983 /*ˇ */
5984 "
5985 .unindent(),
5986 );
5987
5988 // one cursor autocloses a multi-character pair, one cursor
5989 // does not autoclose.
5990 cx.set_state(
5991 &"
5992 /ˇ
5993 ˇ
5994 "
5995 .unindent(),
5996 );
5997 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
5998 cx.assert_editor_state(
5999 &"
6000 /*ˇ */
6001 *ˇ
6002 "
6003 .unindent(),
6004 );
6005
6006 // Don't autoclose if the next character isn't whitespace and isn't
6007 // listed in the language's "autoclose_before" section.
6008 cx.set_state("ˇa b");
6009 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6010 cx.assert_editor_state("{ˇa b");
6011
6012 // Don't autoclose if `close` is false for the bracket pair
6013 cx.set_state("ˇ");
6014 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6015 cx.assert_editor_state("[ˇ");
6016
6017 // Surround with brackets if text is selected
6018 cx.set_state("«aˇ» b");
6019 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6020 cx.assert_editor_state("{«aˇ»} b");
6021
6022 // Autclose pair where the start and end characters are the same
6023 cx.set_state("aˇ");
6024 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6025 cx.assert_editor_state("a\"ˇ\"");
6026 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6027 cx.assert_editor_state("a\"\"ˇ");
6028
6029 // Don't autoclose pair if autoclose is disabled
6030 cx.set_state("ˇ");
6031 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6032 cx.assert_editor_state("<ˇ");
6033
6034 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6035 cx.set_state("«aˇ» b");
6036 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6037 cx.assert_editor_state("<«aˇ»> b");
6038}
6039
6040#[gpui::test]
6041async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut gpui::TestAppContext) {
6042 init_test(cx, |settings| {
6043 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6044 });
6045
6046 let mut cx = EditorTestContext::new(cx).await;
6047
6048 let language = Arc::new(Language::new(
6049 LanguageConfig {
6050 brackets: BracketPairConfig {
6051 pairs: vec![
6052 BracketPair {
6053 start: "{".to_string(),
6054 end: "}".to_string(),
6055 close: true,
6056 surround: true,
6057 newline: true,
6058 },
6059 BracketPair {
6060 start: "(".to_string(),
6061 end: ")".to_string(),
6062 close: true,
6063 surround: true,
6064 newline: true,
6065 },
6066 BracketPair {
6067 start: "[".to_string(),
6068 end: "]".to_string(),
6069 close: false,
6070 surround: false,
6071 newline: true,
6072 },
6073 ],
6074 ..Default::default()
6075 },
6076 autoclose_before: "})]".to_string(),
6077 ..Default::default()
6078 },
6079 Some(tree_sitter_rust::LANGUAGE.into()),
6080 ));
6081
6082 cx.language_registry().add(language.clone());
6083 cx.update_buffer(|buffer, cx| {
6084 buffer.set_language(Some(language), cx);
6085 });
6086
6087 cx.set_state(
6088 &"
6089 ˇ
6090 ˇ
6091 ˇ
6092 "
6093 .unindent(),
6094 );
6095
6096 // ensure only matching closing brackets are skipped over
6097 cx.update_editor(|editor, window, cx| {
6098 editor.handle_input("}", window, cx);
6099 editor.move_left(&MoveLeft, window, cx);
6100 editor.handle_input(")", window, cx);
6101 editor.move_left(&MoveLeft, window, cx);
6102 });
6103 cx.assert_editor_state(
6104 &"
6105 ˇ)}
6106 ˇ)}
6107 ˇ)}
6108 "
6109 .unindent(),
6110 );
6111
6112 // skip-over closing brackets at multiple cursors
6113 cx.update_editor(|editor, window, cx| {
6114 editor.handle_input(")", window, cx);
6115 editor.handle_input("}", window, cx);
6116 });
6117 cx.assert_editor_state(
6118 &"
6119 )}ˇ
6120 )}ˇ
6121 )}ˇ
6122 "
6123 .unindent(),
6124 );
6125
6126 // ignore non-close brackets
6127 cx.update_editor(|editor, window, cx| {
6128 editor.handle_input("]", window, cx);
6129 editor.move_left(&MoveLeft, window, cx);
6130 editor.handle_input("]", window, cx);
6131 });
6132 cx.assert_editor_state(
6133 &"
6134 )}]ˇ]
6135 )}]ˇ]
6136 )}]ˇ]
6137 "
6138 .unindent(),
6139 );
6140}
6141
6142#[gpui::test]
6143async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
6144 init_test(cx, |_| {});
6145
6146 let mut cx = EditorTestContext::new(cx).await;
6147
6148 let html_language = Arc::new(
6149 Language::new(
6150 LanguageConfig {
6151 name: "HTML".into(),
6152 brackets: BracketPairConfig {
6153 pairs: vec![
6154 BracketPair {
6155 start: "<".into(),
6156 end: ">".into(),
6157 close: true,
6158 ..Default::default()
6159 },
6160 BracketPair {
6161 start: "{".into(),
6162 end: "}".into(),
6163 close: true,
6164 ..Default::default()
6165 },
6166 BracketPair {
6167 start: "(".into(),
6168 end: ")".into(),
6169 close: true,
6170 ..Default::default()
6171 },
6172 ],
6173 ..Default::default()
6174 },
6175 autoclose_before: "})]>".into(),
6176 ..Default::default()
6177 },
6178 Some(tree_sitter_html::language()),
6179 )
6180 .with_injection_query(
6181 r#"
6182 (script_element
6183 (raw_text) @injection.content
6184 (#set! injection.language "javascript"))
6185 "#,
6186 )
6187 .unwrap(),
6188 );
6189
6190 let javascript_language = Arc::new(Language::new(
6191 LanguageConfig {
6192 name: "JavaScript".into(),
6193 brackets: BracketPairConfig {
6194 pairs: vec![
6195 BracketPair {
6196 start: "/*".into(),
6197 end: " */".into(),
6198 close: true,
6199 ..Default::default()
6200 },
6201 BracketPair {
6202 start: "{".into(),
6203 end: "}".into(),
6204 close: true,
6205 ..Default::default()
6206 },
6207 BracketPair {
6208 start: "(".into(),
6209 end: ")".into(),
6210 close: true,
6211 ..Default::default()
6212 },
6213 ],
6214 ..Default::default()
6215 },
6216 autoclose_before: "})]>".into(),
6217 ..Default::default()
6218 },
6219 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6220 ));
6221
6222 cx.language_registry().add(html_language.clone());
6223 cx.language_registry().add(javascript_language.clone());
6224
6225 cx.update_buffer(|buffer, cx| {
6226 buffer.set_language(Some(html_language), cx);
6227 });
6228
6229 cx.set_state(
6230 &r#"
6231 <body>ˇ
6232 <script>
6233 var x = 1;ˇ
6234 </script>
6235 </body>ˇ
6236 "#
6237 .unindent(),
6238 );
6239
6240 // Precondition: different languages are active at different locations.
6241 cx.update_editor(|editor, window, cx| {
6242 let snapshot = editor.snapshot(window, cx);
6243 let cursors = editor.selections.ranges::<usize>(cx);
6244 let languages = cursors
6245 .iter()
6246 .map(|c| snapshot.language_at(c.start).unwrap().name())
6247 .collect::<Vec<_>>();
6248 assert_eq!(
6249 languages,
6250 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6251 );
6252 });
6253
6254 // Angle brackets autoclose in HTML, but not JavaScript.
6255 cx.update_editor(|editor, window, cx| {
6256 editor.handle_input("<", window, cx);
6257 editor.handle_input("a", window, cx);
6258 });
6259 cx.assert_editor_state(
6260 &r#"
6261 <body><aˇ>
6262 <script>
6263 var x = 1;<aˇ
6264 </script>
6265 </body><aˇ>
6266 "#
6267 .unindent(),
6268 );
6269
6270 // Curly braces and parens autoclose in both HTML and JavaScript.
6271 cx.update_editor(|editor, window, cx| {
6272 editor.handle_input(" b=", window, cx);
6273 editor.handle_input("{", window, cx);
6274 editor.handle_input("c", window, cx);
6275 editor.handle_input("(", window, cx);
6276 });
6277 cx.assert_editor_state(
6278 &r#"
6279 <body><a b={c(ˇ)}>
6280 <script>
6281 var x = 1;<a b={c(ˇ)}
6282 </script>
6283 </body><a b={c(ˇ)}>
6284 "#
6285 .unindent(),
6286 );
6287
6288 // Brackets that were already autoclosed are skipped.
6289 cx.update_editor(|editor, window, cx| {
6290 editor.handle_input(")", window, cx);
6291 editor.handle_input("d", window, cx);
6292 editor.handle_input("}", window, cx);
6293 });
6294 cx.assert_editor_state(
6295 &r#"
6296 <body><a b={c()d}ˇ>
6297 <script>
6298 var x = 1;<a b={c()d}ˇ
6299 </script>
6300 </body><a b={c()d}ˇ>
6301 "#
6302 .unindent(),
6303 );
6304 cx.update_editor(|editor, window, cx| {
6305 editor.handle_input(">", window, cx);
6306 });
6307 cx.assert_editor_state(
6308 &r#"
6309 <body><a b={c()d}>ˇ
6310 <script>
6311 var x = 1;<a b={c()d}>ˇ
6312 </script>
6313 </body><a b={c()d}>ˇ
6314 "#
6315 .unindent(),
6316 );
6317
6318 // Reset
6319 cx.set_state(
6320 &r#"
6321 <body>ˇ
6322 <script>
6323 var x = 1;ˇ
6324 </script>
6325 </body>ˇ
6326 "#
6327 .unindent(),
6328 );
6329
6330 cx.update_editor(|editor, window, cx| {
6331 editor.handle_input("<", window, cx);
6332 });
6333 cx.assert_editor_state(
6334 &r#"
6335 <body><ˇ>
6336 <script>
6337 var x = 1;<ˇ
6338 </script>
6339 </body><ˇ>
6340 "#
6341 .unindent(),
6342 );
6343
6344 // When backspacing, the closing angle brackets are removed.
6345 cx.update_editor(|editor, window, cx| {
6346 editor.backspace(&Backspace, window, cx);
6347 });
6348 cx.assert_editor_state(
6349 &r#"
6350 <body>ˇ
6351 <script>
6352 var x = 1;ˇ
6353 </script>
6354 </body>ˇ
6355 "#
6356 .unindent(),
6357 );
6358
6359 // Block comments autoclose in JavaScript, but not HTML.
6360 cx.update_editor(|editor, window, cx| {
6361 editor.handle_input("/", window, cx);
6362 editor.handle_input("*", window, cx);
6363 });
6364 cx.assert_editor_state(
6365 &r#"
6366 <body>/*ˇ
6367 <script>
6368 var x = 1;/*ˇ */
6369 </script>
6370 </body>/*ˇ
6371 "#
6372 .unindent(),
6373 );
6374}
6375
6376#[gpui::test]
6377async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
6378 init_test(cx, |_| {});
6379
6380 let mut cx = EditorTestContext::new(cx).await;
6381
6382 let rust_language = Arc::new(
6383 Language::new(
6384 LanguageConfig {
6385 name: "Rust".into(),
6386 brackets: serde_json::from_value(json!([
6387 { "start": "{", "end": "}", "close": true, "newline": true },
6388 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
6389 ]))
6390 .unwrap(),
6391 autoclose_before: "})]>".into(),
6392 ..Default::default()
6393 },
6394 Some(tree_sitter_rust::LANGUAGE.into()),
6395 )
6396 .with_override_query("(string_literal) @string")
6397 .unwrap(),
6398 );
6399
6400 cx.language_registry().add(rust_language.clone());
6401 cx.update_buffer(|buffer, cx| {
6402 buffer.set_language(Some(rust_language), cx);
6403 });
6404
6405 cx.set_state(
6406 &r#"
6407 let x = ˇ
6408 "#
6409 .unindent(),
6410 );
6411
6412 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
6413 cx.update_editor(|editor, window, cx| {
6414 editor.handle_input("\"", window, cx);
6415 });
6416 cx.assert_editor_state(
6417 &r#"
6418 let x = "ˇ"
6419 "#
6420 .unindent(),
6421 );
6422
6423 // Inserting another quotation mark. The cursor moves across the existing
6424 // automatically-inserted quotation mark.
6425 cx.update_editor(|editor, window, cx| {
6426 editor.handle_input("\"", window, cx);
6427 });
6428 cx.assert_editor_state(
6429 &r#"
6430 let x = ""ˇ
6431 "#
6432 .unindent(),
6433 );
6434
6435 // Reset
6436 cx.set_state(
6437 &r#"
6438 let x = ˇ
6439 "#
6440 .unindent(),
6441 );
6442
6443 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
6444 cx.update_editor(|editor, window, cx| {
6445 editor.handle_input("\"", window, cx);
6446 editor.handle_input(" ", window, cx);
6447 editor.move_left(&Default::default(), window, cx);
6448 editor.handle_input("\\", window, cx);
6449 editor.handle_input("\"", window, cx);
6450 });
6451 cx.assert_editor_state(
6452 &r#"
6453 let x = "\"ˇ "
6454 "#
6455 .unindent(),
6456 );
6457
6458 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
6459 // mark. Nothing is inserted.
6460 cx.update_editor(|editor, window, cx| {
6461 editor.move_right(&Default::default(), window, cx);
6462 editor.handle_input("\"", window, cx);
6463 });
6464 cx.assert_editor_state(
6465 &r#"
6466 let x = "\" "ˇ
6467 "#
6468 .unindent(),
6469 );
6470}
6471
6472#[gpui::test]
6473async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
6474 init_test(cx, |_| {});
6475
6476 let language = Arc::new(Language::new(
6477 LanguageConfig {
6478 brackets: BracketPairConfig {
6479 pairs: vec![
6480 BracketPair {
6481 start: "{".to_string(),
6482 end: "}".to_string(),
6483 close: true,
6484 surround: true,
6485 newline: true,
6486 },
6487 BracketPair {
6488 start: "/* ".to_string(),
6489 end: "*/".to_string(),
6490 close: true,
6491 surround: true,
6492 ..Default::default()
6493 },
6494 ],
6495 ..Default::default()
6496 },
6497 ..Default::default()
6498 },
6499 Some(tree_sitter_rust::LANGUAGE.into()),
6500 ));
6501
6502 let text = r#"
6503 a
6504 b
6505 c
6506 "#
6507 .unindent();
6508
6509 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6510 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6511 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6512 editor
6513 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6514 .await;
6515
6516 editor.update_in(cx, |editor, window, cx| {
6517 editor.change_selections(None, window, cx, |s| {
6518 s.select_display_ranges([
6519 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6520 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6521 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
6522 ])
6523 });
6524
6525 editor.handle_input("{", window, cx);
6526 editor.handle_input("{", window, cx);
6527 editor.handle_input("{", window, cx);
6528 assert_eq!(
6529 editor.text(cx),
6530 "
6531 {{{a}}}
6532 {{{b}}}
6533 {{{c}}}
6534 "
6535 .unindent()
6536 );
6537 assert_eq!(
6538 editor.selections.display_ranges(cx),
6539 [
6540 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
6541 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
6542 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
6543 ]
6544 );
6545
6546 editor.undo(&Undo, window, cx);
6547 editor.undo(&Undo, window, cx);
6548 editor.undo(&Undo, window, cx);
6549 assert_eq!(
6550 editor.text(cx),
6551 "
6552 a
6553 b
6554 c
6555 "
6556 .unindent()
6557 );
6558 assert_eq!(
6559 editor.selections.display_ranges(cx),
6560 [
6561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6562 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6563 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
6564 ]
6565 );
6566
6567 // Ensure inserting the first character of a multi-byte bracket pair
6568 // doesn't surround the selections with the bracket.
6569 editor.handle_input("/", window, cx);
6570 assert_eq!(
6571 editor.text(cx),
6572 "
6573 /
6574 /
6575 /
6576 "
6577 .unindent()
6578 );
6579 assert_eq!(
6580 editor.selections.display_ranges(cx),
6581 [
6582 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6583 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6584 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
6585 ]
6586 );
6587
6588 editor.undo(&Undo, window, cx);
6589 assert_eq!(
6590 editor.text(cx),
6591 "
6592 a
6593 b
6594 c
6595 "
6596 .unindent()
6597 );
6598 assert_eq!(
6599 editor.selections.display_ranges(cx),
6600 [
6601 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6602 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
6603 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
6604 ]
6605 );
6606
6607 // Ensure inserting the last character of a multi-byte bracket pair
6608 // doesn't surround the selections with the bracket.
6609 editor.handle_input("*", window, cx);
6610 assert_eq!(
6611 editor.text(cx),
6612 "
6613 *
6614 *
6615 *
6616 "
6617 .unindent()
6618 );
6619 assert_eq!(
6620 editor.selections.display_ranges(cx),
6621 [
6622 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6623 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6624 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
6625 ]
6626 );
6627 });
6628}
6629
6630#[gpui::test]
6631async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
6632 init_test(cx, |_| {});
6633
6634 let language = Arc::new(Language::new(
6635 LanguageConfig {
6636 brackets: BracketPairConfig {
6637 pairs: vec![BracketPair {
6638 start: "{".to_string(),
6639 end: "}".to_string(),
6640 close: true,
6641 surround: true,
6642 newline: true,
6643 }],
6644 ..Default::default()
6645 },
6646 autoclose_before: "}".to_string(),
6647 ..Default::default()
6648 },
6649 Some(tree_sitter_rust::LANGUAGE.into()),
6650 ));
6651
6652 let text = r#"
6653 a
6654 b
6655 c
6656 "#
6657 .unindent();
6658
6659 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6660 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6661 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6662 editor
6663 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6664 .await;
6665
6666 editor.update_in(cx, |editor, window, cx| {
6667 editor.change_selections(None, window, cx, |s| {
6668 s.select_ranges([
6669 Point::new(0, 1)..Point::new(0, 1),
6670 Point::new(1, 1)..Point::new(1, 1),
6671 Point::new(2, 1)..Point::new(2, 1),
6672 ])
6673 });
6674
6675 editor.handle_input("{", window, cx);
6676 editor.handle_input("{", window, cx);
6677 editor.handle_input("_", window, cx);
6678 assert_eq!(
6679 editor.text(cx),
6680 "
6681 a{{_}}
6682 b{{_}}
6683 c{{_}}
6684 "
6685 .unindent()
6686 );
6687 assert_eq!(
6688 editor.selections.ranges::<Point>(cx),
6689 [
6690 Point::new(0, 4)..Point::new(0, 4),
6691 Point::new(1, 4)..Point::new(1, 4),
6692 Point::new(2, 4)..Point::new(2, 4)
6693 ]
6694 );
6695
6696 editor.backspace(&Default::default(), window, cx);
6697 editor.backspace(&Default::default(), window, cx);
6698 assert_eq!(
6699 editor.text(cx),
6700 "
6701 a{}
6702 b{}
6703 c{}
6704 "
6705 .unindent()
6706 );
6707 assert_eq!(
6708 editor.selections.ranges::<Point>(cx),
6709 [
6710 Point::new(0, 2)..Point::new(0, 2),
6711 Point::new(1, 2)..Point::new(1, 2),
6712 Point::new(2, 2)..Point::new(2, 2)
6713 ]
6714 );
6715
6716 editor.delete_to_previous_word_start(&Default::default(), window, cx);
6717 assert_eq!(
6718 editor.text(cx),
6719 "
6720 a
6721 b
6722 c
6723 "
6724 .unindent()
6725 );
6726 assert_eq!(
6727 editor.selections.ranges::<Point>(cx),
6728 [
6729 Point::new(0, 1)..Point::new(0, 1),
6730 Point::new(1, 1)..Point::new(1, 1),
6731 Point::new(2, 1)..Point::new(2, 1)
6732 ]
6733 );
6734 });
6735}
6736
6737#[gpui::test]
6738async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut gpui::TestAppContext) {
6739 init_test(cx, |settings| {
6740 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6741 });
6742
6743 let mut cx = EditorTestContext::new(cx).await;
6744
6745 let language = Arc::new(Language::new(
6746 LanguageConfig {
6747 brackets: BracketPairConfig {
6748 pairs: vec![
6749 BracketPair {
6750 start: "{".to_string(),
6751 end: "}".to_string(),
6752 close: true,
6753 surround: true,
6754 newline: true,
6755 },
6756 BracketPair {
6757 start: "(".to_string(),
6758 end: ")".to_string(),
6759 close: true,
6760 surround: true,
6761 newline: true,
6762 },
6763 BracketPair {
6764 start: "[".to_string(),
6765 end: "]".to_string(),
6766 close: false,
6767 surround: true,
6768 newline: true,
6769 },
6770 ],
6771 ..Default::default()
6772 },
6773 autoclose_before: "})]".to_string(),
6774 ..Default::default()
6775 },
6776 Some(tree_sitter_rust::LANGUAGE.into()),
6777 ));
6778
6779 cx.language_registry().add(language.clone());
6780 cx.update_buffer(|buffer, cx| {
6781 buffer.set_language(Some(language), cx);
6782 });
6783
6784 cx.set_state(
6785 &"
6786 {(ˇ)}
6787 [[ˇ]]
6788 {(ˇ)}
6789 "
6790 .unindent(),
6791 );
6792
6793 cx.update_editor(|editor, window, cx| {
6794 editor.backspace(&Default::default(), window, cx);
6795 editor.backspace(&Default::default(), window, cx);
6796 });
6797
6798 cx.assert_editor_state(
6799 &"
6800 ˇ
6801 ˇ]]
6802 ˇ
6803 "
6804 .unindent(),
6805 );
6806
6807 cx.update_editor(|editor, window, cx| {
6808 editor.handle_input("{", window, cx);
6809 editor.handle_input("{", window, cx);
6810 editor.move_right(&MoveRight, window, cx);
6811 editor.move_right(&MoveRight, window, cx);
6812 editor.move_left(&MoveLeft, window, cx);
6813 editor.move_left(&MoveLeft, window, cx);
6814 editor.backspace(&Default::default(), window, cx);
6815 });
6816
6817 cx.assert_editor_state(
6818 &"
6819 {ˇ}
6820 {ˇ}]]
6821 {ˇ}
6822 "
6823 .unindent(),
6824 );
6825
6826 cx.update_editor(|editor, window, cx| {
6827 editor.backspace(&Default::default(), window, cx);
6828 });
6829
6830 cx.assert_editor_state(
6831 &"
6832 ˇ
6833 ˇ]]
6834 ˇ
6835 "
6836 .unindent(),
6837 );
6838}
6839
6840#[gpui::test]
6841async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) {
6842 init_test(cx, |_| {});
6843
6844 let language = Arc::new(Language::new(
6845 LanguageConfig::default(),
6846 Some(tree_sitter_rust::LANGUAGE.into()),
6847 ));
6848
6849 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
6850 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6851 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6852 editor
6853 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6854 .await;
6855
6856 editor.update_in(cx, |editor, window, cx| {
6857 editor.set_auto_replace_emoji_shortcode(true);
6858
6859 editor.handle_input("Hello ", window, cx);
6860 editor.handle_input(":wave", window, cx);
6861 assert_eq!(editor.text(cx), "Hello :wave".unindent());
6862
6863 editor.handle_input(":", window, cx);
6864 assert_eq!(editor.text(cx), "Hello 👋".unindent());
6865
6866 editor.handle_input(" :smile", window, cx);
6867 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
6868
6869 editor.handle_input(":", window, cx);
6870 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
6871
6872 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
6873 editor.handle_input(":wave", window, cx);
6874 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
6875
6876 editor.handle_input(":", window, cx);
6877 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
6878
6879 editor.handle_input(":1", window, cx);
6880 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
6881
6882 editor.handle_input(":", window, cx);
6883 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
6884
6885 // Ensure shortcode does not get replaced when it is part of a word
6886 editor.handle_input(" Test:wave", window, cx);
6887 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
6888
6889 editor.handle_input(":", window, cx);
6890 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
6891
6892 editor.set_auto_replace_emoji_shortcode(false);
6893
6894 // Ensure shortcode does not get replaced when auto replace is off
6895 editor.handle_input(" :wave", window, cx);
6896 assert_eq!(
6897 editor.text(cx),
6898 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
6899 );
6900
6901 editor.handle_input(":", window, cx);
6902 assert_eq!(
6903 editor.text(cx),
6904 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
6905 );
6906 });
6907}
6908
6909#[gpui::test]
6910async fn test_snippet_placeholder_choices(cx: &mut gpui::TestAppContext) {
6911 init_test(cx, |_| {});
6912
6913 let (text, insertion_ranges) = marked_text_ranges(
6914 indoc! {"
6915 ˇ
6916 "},
6917 false,
6918 );
6919
6920 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
6921 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6922
6923 _ = editor.update_in(cx, |editor, window, cx| {
6924 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
6925
6926 editor
6927 .insert_snippet(&insertion_ranges, snippet, window, cx)
6928 .unwrap();
6929
6930 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
6931 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
6932 assert_eq!(editor.text(cx), expected_text);
6933 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
6934 }
6935
6936 assert(
6937 editor,
6938 cx,
6939 indoc! {"
6940 type «» =•
6941 "},
6942 );
6943
6944 assert!(editor.context_menu_visible(), "There should be a matches");
6945 });
6946}
6947
6948#[gpui::test]
6949async fn test_snippets(cx: &mut gpui::TestAppContext) {
6950 init_test(cx, |_| {});
6951
6952 let (text, insertion_ranges) = marked_text_ranges(
6953 indoc! {"
6954 a.ˇ b
6955 a.ˇ b
6956 a.ˇ b
6957 "},
6958 false,
6959 );
6960
6961 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
6962 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6963
6964 editor.update_in(cx, |editor, window, cx| {
6965 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
6966
6967 editor
6968 .insert_snippet(&insertion_ranges, snippet, window, cx)
6969 .unwrap();
6970
6971 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
6972 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
6973 assert_eq!(editor.text(cx), expected_text);
6974 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
6975 }
6976
6977 assert(
6978 editor,
6979 cx,
6980 indoc! {"
6981 a.f(«one», two, «three») b
6982 a.f(«one», two, «three») b
6983 a.f(«one», two, «three») b
6984 "},
6985 );
6986
6987 // Can't move earlier than the first tab stop
6988 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
6989 assert(
6990 editor,
6991 cx,
6992 indoc! {"
6993 a.f(«one», two, «three») b
6994 a.f(«one», two, «three») b
6995 a.f(«one», two, «three») b
6996 "},
6997 );
6998
6999 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7000 assert(
7001 editor,
7002 cx,
7003 indoc! {"
7004 a.f(one, «two», three) b
7005 a.f(one, «two», three) b
7006 a.f(one, «two», three) b
7007 "},
7008 );
7009
7010 editor.move_to_prev_snippet_tabstop(window, cx);
7011 assert(
7012 editor,
7013 cx,
7014 indoc! {"
7015 a.f(«one», two, «three») b
7016 a.f(«one», two, «three») b
7017 a.f(«one», two, «three») b
7018 "},
7019 );
7020
7021 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7022 assert(
7023 editor,
7024 cx,
7025 indoc! {"
7026 a.f(one, «two», three) b
7027 a.f(one, «two», three) b
7028 a.f(one, «two», three) b
7029 "},
7030 );
7031 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7032 assert(
7033 editor,
7034 cx,
7035 indoc! {"
7036 a.f(one, two, three)ˇ b
7037 a.f(one, two, three)ˇ b
7038 a.f(one, two, three)ˇ b
7039 "},
7040 );
7041
7042 // As soon as the last tab stop is reached, snippet state is gone
7043 editor.move_to_prev_snippet_tabstop(window, cx);
7044 assert(
7045 editor,
7046 cx,
7047 indoc! {"
7048 a.f(one, two, three)ˇ b
7049 a.f(one, two, three)ˇ b
7050 a.f(one, two, three)ˇ b
7051 "},
7052 );
7053 });
7054}
7055
7056#[gpui::test]
7057async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
7058 init_test(cx, |_| {});
7059
7060 let fs = FakeFs::new(cx.executor());
7061 fs.insert_file("/file.rs", Default::default()).await;
7062
7063 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
7064
7065 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7066 language_registry.add(rust_lang());
7067 let mut fake_servers = language_registry.register_fake_lsp(
7068 "Rust",
7069 FakeLspAdapter {
7070 capabilities: lsp::ServerCapabilities {
7071 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7072 ..Default::default()
7073 },
7074 ..Default::default()
7075 },
7076 );
7077
7078 let buffer = project
7079 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
7080 .await
7081 .unwrap();
7082
7083 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7084 let (editor, cx) = cx.add_window_view(|window, cx| {
7085 build_editor_with_project(project.clone(), buffer, window, cx)
7086 });
7087 editor.update_in(cx, |editor, window, cx| {
7088 editor.set_text("one\ntwo\nthree\n", window, cx)
7089 });
7090 assert!(cx.read(|cx| editor.is_dirty(cx)));
7091
7092 cx.executor().start_waiting();
7093 let fake_server = fake_servers.next().await.unwrap();
7094
7095 let save = editor
7096 .update_in(cx, |editor, window, cx| {
7097 editor.save(true, project.clone(), window, cx)
7098 })
7099 .unwrap();
7100 fake_server
7101 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7102 assert_eq!(
7103 params.text_document.uri,
7104 lsp::Url::from_file_path("/file.rs").unwrap()
7105 );
7106 assert_eq!(params.options.tab_size, 4);
7107 Ok(Some(vec![lsp::TextEdit::new(
7108 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7109 ", ".to_string(),
7110 )]))
7111 })
7112 .next()
7113 .await;
7114 cx.executor().start_waiting();
7115 save.await;
7116
7117 assert_eq!(
7118 editor.update(cx, |editor, cx| editor.text(cx)),
7119 "one, two\nthree\n"
7120 );
7121 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7122
7123 editor.update_in(cx, |editor, window, cx| {
7124 editor.set_text("one\ntwo\nthree\n", window, cx)
7125 });
7126 assert!(cx.read(|cx| editor.is_dirty(cx)));
7127
7128 // Ensure we can still save even if formatting hangs.
7129 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7130 assert_eq!(
7131 params.text_document.uri,
7132 lsp::Url::from_file_path("/file.rs").unwrap()
7133 );
7134 futures::future::pending::<()>().await;
7135 unreachable!()
7136 });
7137 let save = editor
7138 .update_in(cx, |editor, window, cx| {
7139 editor.save(true, project.clone(), window, cx)
7140 })
7141 .unwrap();
7142 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7143 cx.executor().start_waiting();
7144 save.await;
7145 assert_eq!(
7146 editor.update(cx, |editor, cx| editor.text(cx)),
7147 "one\ntwo\nthree\n"
7148 );
7149 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7150
7151 // For non-dirty buffer, no formatting request should be sent
7152 let save = editor
7153 .update_in(cx, |editor, window, cx| {
7154 editor.save(true, project.clone(), window, cx)
7155 })
7156 .unwrap();
7157 let _pending_format_request = fake_server
7158 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7159 panic!("Should not be invoked on non-dirty buffer");
7160 })
7161 .next();
7162 cx.executor().start_waiting();
7163 save.await;
7164
7165 // Set rust language override and assert overridden tabsize is sent to language server
7166 update_test_language_settings(cx, |settings| {
7167 settings.languages.insert(
7168 "Rust".into(),
7169 LanguageSettingsContent {
7170 tab_size: NonZeroU32::new(8),
7171 ..Default::default()
7172 },
7173 );
7174 });
7175
7176 editor.update_in(cx, |editor, window, cx| {
7177 editor.set_text("somehting_new\n", window, cx)
7178 });
7179 assert!(cx.read(|cx| editor.is_dirty(cx)));
7180 let save = editor
7181 .update_in(cx, |editor, window, cx| {
7182 editor.save(true, project.clone(), window, cx)
7183 })
7184 .unwrap();
7185 fake_server
7186 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7187 assert_eq!(
7188 params.text_document.uri,
7189 lsp::Url::from_file_path("/file.rs").unwrap()
7190 );
7191 assert_eq!(params.options.tab_size, 8);
7192 Ok(Some(vec![]))
7193 })
7194 .next()
7195 .await;
7196 cx.executor().start_waiting();
7197 save.await;
7198}
7199
7200#[gpui::test]
7201async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) {
7202 init_test(cx, |_| {});
7203
7204 let cols = 4;
7205 let rows = 10;
7206 let sample_text_1 = sample_text(rows, cols, 'a');
7207 assert_eq!(
7208 sample_text_1,
7209 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7210 );
7211 let sample_text_2 = sample_text(rows, cols, 'l');
7212 assert_eq!(
7213 sample_text_2,
7214 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7215 );
7216 let sample_text_3 = sample_text(rows, cols, 'v');
7217 assert_eq!(
7218 sample_text_3,
7219 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7220 );
7221
7222 let fs = FakeFs::new(cx.executor());
7223 fs.insert_tree(
7224 "/a",
7225 json!({
7226 "main.rs": sample_text_1,
7227 "other.rs": sample_text_2,
7228 "lib.rs": sample_text_3,
7229 }),
7230 )
7231 .await;
7232
7233 let project = Project::test(fs, ["/a".as_ref()], cx).await;
7234 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7235 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7236
7237 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7238 language_registry.add(rust_lang());
7239 let mut fake_servers = language_registry.register_fake_lsp(
7240 "Rust",
7241 FakeLspAdapter {
7242 capabilities: lsp::ServerCapabilities {
7243 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7244 ..Default::default()
7245 },
7246 ..Default::default()
7247 },
7248 );
7249
7250 let worktree = project.update(cx, |project, cx| {
7251 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7252 assert_eq!(worktrees.len(), 1);
7253 worktrees.pop().unwrap()
7254 });
7255 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
7256
7257 let buffer_1 = project
7258 .update(cx, |project, cx| {
7259 project.open_buffer((worktree_id, "main.rs"), cx)
7260 })
7261 .await
7262 .unwrap();
7263 let buffer_2 = project
7264 .update(cx, |project, cx| {
7265 project.open_buffer((worktree_id, "other.rs"), cx)
7266 })
7267 .await
7268 .unwrap();
7269 let buffer_3 = project
7270 .update(cx, |project, cx| {
7271 project.open_buffer((worktree_id, "lib.rs"), cx)
7272 })
7273 .await
7274 .unwrap();
7275
7276 let multi_buffer = cx.new(|cx| {
7277 let mut multi_buffer = MultiBuffer::new(ReadWrite);
7278 multi_buffer.push_excerpts(
7279 buffer_1.clone(),
7280 [
7281 ExcerptRange {
7282 context: Point::new(0, 0)..Point::new(3, 0),
7283 primary: None,
7284 },
7285 ExcerptRange {
7286 context: Point::new(5, 0)..Point::new(7, 0),
7287 primary: None,
7288 },
7289 ExcerptRange {
7290 context: Point::new(9, 0)..Point::new(10, 4),
7291 primary: None,
7292 },
7293 ],
7294 cx,
7295 );
7296 multi_buffer.push_excerpts(
7297 buffer_2.clone(),
7298 [
7299 ExcerptRange {
7300 context: Point::new(0, 0)..Point::new(3, 0),
7301 primary: None,
7302 },
7303 ExcerptRange {
7304 context: Point::new(5, 0)..Point::new(7, 0),
7305 primary: None,
7306 },
7307 ExcerptRange {
7308 context: Point::new(9, 0)..Point::new(10, 4),
7309 primary: None,
7310 },
7311 ],
7312 cx,
7313 );
7314 multi_buffer.push_excerpts(
7315 buffer_3.clone(),
7316 [
7317 ExcerptRange {
7318 context: Point::new(0, 0)..Point::new(3, 0),
7319 primary: None,
7320 },
7321 ExcerptRange {
7322 context: Point::new(5, 0)..Point::new(7, 0),
7323 primary: None,
7324 },
7325 ExcerptRange {
7326 context: Point::new(9, 0)..Point::new(10, 4),
7327 primary: None,
7328 },
7329 ],
7330 cx,
7331 );
7332 multi_buffer
7333 });
7334 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
7335 Editor::new(
7336 EditorMode::Full,
7337 multi_buffer,
7338 Some(project.clone()),
7339 true,
7340 window,
7341 cx,
7342 )
7343 });
7344
7345 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7346 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7347 s.select_ranges(Some(1..2))
7348 });
7349 editor.insert("|one|two|three|", window, cx);
7350 });
7351 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7352 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7353 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7354 s.select_ranges(Some(60..70))
7355 });
7356 editor.insert("|four|five|six|", window, cx);
7357 });
7358 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7359
7360 // First two buffers should be edited, but not the third one.
7361 assert_eq!(
7362 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7363 "a|one|two|three|aa\nbbbb\ncccc\n\nffff\ngggg\n\njjjj\nllll\nmmmm\nnnnn|four|five|six|\nr\n\nuuuu\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}",
7364 );
7365 buffer_1.update(cx, |buffer, _| {
7366 assert!(buffer.is_dirty());
7367 assert_eq!(
7368 buffer.text(),
7369 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
7370 )
7371 });
7372 buffer_2.update(cx, |buffer, _| {
7373 assert!(buffer.is_dirty());
7374 assert_eq!(
7375 buffer.text(),
7376 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
7377 )
7378 });
7379 buffer_3.update(cx, |buffer, _| {
7380 assert!(!buffer.is_dirty());
7381 assert_eq!(buffer.text(), sample_text_3,)
7382 });
7383 cx.executor().run_until_parked();
7384
7385 cx.executor().start_waiting();
7386 let save = multi_buffer_editor
7387 .update_in(cx, |editor, window, cx| {
7388 editor.save(true, project.clone(), window, cx)
7389 })
7390 .unwrap();
7391
7392 let fake_server = fake_servers.next().await.unwrap();
7393 fake_server
7394 .server
7395 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7396 Ok(Some(vec![lsp::TextEdit::new(
7397 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7398 format!("[{} formatted]", params.text_document.uri),
7399 )]))
7400 })
7401 .detach();
7402 save.await;
7403
7404 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
7405 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
7406 assert_eq!(
7407 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7408 "a|o[file:///a/main.rs formatted]bbbb\ncccc\n\nffff\ngggg\n\njjjj\n\nlll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|\nr\n\nuuuu\n\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}",
7409 );
7410 buffer_1.update(cx, |buffer, _| {
7411 assert!(!buffer.is_dirty());
7412 assert_eq!(
7413 buffer.text(),
7414 "a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n",
7415 )
7416 });
7417 buffer_2.update(cx, |buffer, _| {
7418 assert!(!buffer.is_dirty());
7419 assert_eq!(
7420 buffer.text(),
7421 "lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n",
7422 )
7423 });
7424 buffer_3.update(cx, |buffer, _| {
7425 assert!(!buffer.is_dirty());
7426 assert_eq!(buffer.text(), sample_text_3,)
7427 });
7428}
7429
7430#[gpui::test]
7431async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
7432 init_test(cx, |_| {});
7433
7434 let fs = FakeFs::new(cx.executor());
7435 fs.insert_file("/file.rs", Default::default()).await;
7436
7437 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
7438
7439 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7440 language_registry.add(rust_lang());
7441 let mut fake_servers = language_registry.register_fake_lsp(
7442 "Rust",
7443 FakeLspAdapter {
7444 capabilities: lsp::ServerCapabilities {
7445 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
7446 ..Default::default()
7447 },
7448 ..Default::default()
7449 },
7450 );
7451
7452 let buffer = project
7453 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
7454 .await
7455 .unwrap();
7456
7457 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7458 let (editor, cx) = cx.add_window_view(|window, cx| {
7459 build_editor_with_project(project.clone(), buffer, window, cx)
7460 });
7461 editor.update_in(cx, |editor, window, cx| {
7462 editor.set_text("one\ntwo\nthree\n", window, cx)
7463 });
7464 assert!(cx.read(|cx| editor.is_dirty(cx)));
7465
7466 cx.executor().start_waiting();
7467 let fake_server = fake_servers.next().await.unwrap();
7468
7469 let save = editor
7470 .update_in(cx, |editor, window, cx| {
7471 editor.save(true, project.clone(), window, cx)
7472 })
7473 .unwrap();
7474 fake_server
7475 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
7476 assert_eq!(
7477 params.text_document.uri,
7478 lsp::Url::from_file_path("/file.rs").unwrap()
7479 );
7480 assert_eq!(params.options.tab_size, 4);
7481 Ok(Some(vec![lsp::TextEdit::new(
7482 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7483 ", ".to_string(),
7484 )]))
7485 })
7486 .next()
7487 .await;
7488 cx.executor().start_waiting();
7489 save.await;
7490 assert_eq!(
7491 editor.update(cx, |editor, cx| editor.text(cx)),
7492 "one, two\nthree\n"
7493 );
7494 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7495
7496 editor.update_in(cx, |editor, window, cx| {
7497 editor.set_text("one\ntwo\nthree\n", window, cx)
7498 });
7499 assert!(cx.read(|cx| editor.is_dirty(cx)));
7500
7501 // Ensure we can still save even if formatting hangs.
7502 fake_server.handle_request::<lsp::request::RangeFormatting, _, _>(
7503 move |params, _| async move {
7504 assert_eq!(
7505 params.text_document.uri,
7506 lsp::Url::from_file_path("/file.rs").unwrap()
7507 );
7508 futures::future::pending::<()>().await;
7509 unreachable!()
7510 },
7511 );
7512 let save = editor
7513 .update_in(cx, |editor, window, cx| {
7514 editor.save(true, project.clone(), window, cx)
7515 })
7516 .unwrap();
7517 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7518 cx.executor().start_waiting();
7519 save.await;
7520 assert_eq!(
7521 editor.update(cx, |editor, cx| editor.text(cx)),
7522 "one\ntwo\nthree\n"
7523 );
7524 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7525
7526 // For non-dirty buffer, no formatting request should be sent
7527 let save = editor
7528 .update_in(cx, |editor, window, cx| {
7529 editor.save(true, project.clone(), window, cx)
7530 })
7531 .unwrap();
7532 let _pending_format_request = fake_server
7533 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7534 panic!("Should not be invoked on non-dirty buffer");
7535 })
7536 .next();
7537 cx.executor().start_waiting();
7538 save.await;
7539
7540 // Set Rust language override and assert overridden tabsize is sent to language server
7541 update_test_language_settings(cx, |settings| {
7542 settings.languages.insert(
7543 "Rust".into(),
7544 LanguageSettingsContent {
7545 tab_size: NonZeroU32::new(8),
7546 ..Default::default()
7547 },
7548 );
7549 });
7550
7551 editor.update_in(cx, |editor, window, cx| {
7552 editor.set_text("somehting_new\n", window, cx)
7553 });
7554 assert!(cx.read(|cx| editor.is_dirty(cx)));
7555 let save = editor
7556 .update_in(cx, |editor, window, cx| {
7557 editor.save(true, project.clone(), window, cx)
7558 })
7559 .unwrap();
7560 fake_server
7561 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
7562 assert_eq!(
7563 params.text_document.uri,
7564 lsp::Url::from_file_path("/file.rs").unwrap()
7565 );
7566 assert_eq!(params.options.tab_size, 8);
7567 Ok(Some(vec![]))
7568 })
7569 .next()
7570 .await;
7571 cx.executor().start_waiting();
7572 save.await;
7573}
7574
7575#[gpui::test]
7576async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
7577 init_test(cx, |settings| {
7578 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
7579 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
7580 ))
7581 });
7582
7583 let fs = FakeFs::new(cx.executor());
7584 fs.insert_file("/file.rs", Default::default()).await;
7585
7586 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
7587
7588 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7589 language_registry.add(Arc::new(Language::new(
7590 LanguageConfig {
7591 name: "Rust".into(),
7592 matcher: LanguageMatcher {
7593 path_suffixes: vec!["rs".to_string()],
7594 ..Default::default()
7595 },
7596 ..LanguageConfig::default()
7597 },
7598 Some(tree_sitter_rust::LANGUAGE.into()),
7599 )));
7600 update_test_language_settings(cx, |settings| {
7601 // Enable Prettier formatting for the same buffer, and ensure
7602 // LSP is called instead of Prettier.
7603 settings.defaults.prettier = Some(PrettierSettings {
7604 allowed: true,
7605 ..PrettierSettings::default()
7606 });
7607 });
7608 let mut fake_servers = language_registry.register_fake_lsp(
7609 "Rust",
7610 FakeLspAdapter {
7611 capabilities: lsp::ServerCapabilities {
7612 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7613 ..Default::default()
7614 },
7615 ..Default::default()
7616 },
7617 );
7618
7619 let buffer = project
7620 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
7621 .await
7622 .unwrap();
7623
7624 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7625 let (editor, cx) = cx.add_window_view(|window, cx| {
7626 build_editor_with_project(project.clone(), buffer, window, cx)
7627 });
7628 editor.update_in(cx, |editor, window, cx| {
7629 editor.set_text("one\ntwo\nthree\n", window, cx)
7630 });
7631
7632 cx.executor().start_waiting();
7633 let fake_server = fake_servers.next().await.unwrap();
7634
7635 let format = editor
7636 .update_in(cx, |editor, window, cx| {
7637 editor.perform_format(
7638 project.clone(),
7639 FormatTrigger::Manual,
7640 FormatTarget::Buffers,
7641 window,
7642 cx,
7643 )
7644 })
7645 .unwrap();
7646 fake_server
7647 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7648 assert_eq!(
7649 params.text_document.uri,
7650 lsp::Url::from_file_path("/file.rs").unwrap()
7651 );
7652 assert_eq!(params.options.tab_size, 4);
7653 Ok(Some(vec![lsp::TextEdit::new(
7654 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7655 ", ".to_string(),
7656 )]))
7657 })
7658 .next()
7659 .await;
7660 cx.executor().start_waiting();
7661 format.await;
7662 assert_eq!(
7663 editor.update(cx, |editor, cx| editor.text(cx)),
7664 "one, two\nthree\n"
7665 );
7666
7667 editor.update_in(cx, |editor, window, cx| {
7668 editor.set_text("one\ntwo\nthree\n", window, cx)
7669 });
7670 // Ensure we don't lock if formatting hangs.
7671 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7672 assert_eq!(
7673 params.text_document.uri,
7674 lsp::Url::from_file_path("/file.rs").unwrap()
7675 );
7676 futures::future::pending::<()>().await;
7677 unreachable!()
7678 });
7679 let format = editor
7680 .update_in(cx, |editor, window, cx| {
7681 editor.perform_format(
7682 project,
7683 FormatTrigger::Manual,
7684 FormatTarget::Buffers,
7685 window,
7686 cx,
7687 )
7688 })
7689 .unwrap();
7690 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7691 cx.executor().start_waiting();
7692 format.await;
7693 assert_eq!(
7694 editor.update(cx, |editor, cx| editor.text(cx)),
7695 "one\ntwo\nthree\n"
7696 );
7697}
7698
7699#[gpui::test]
7700async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
7701 init_test(cx, |_| {});
7702
7703 let mut cx = EditorLspTestContext::new_rust(
7704 lsp::ServerCapabilities {
7705 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7706 ..Default::default()
7707 },
7708 cx,
7709 )
7710 .await;
7711
7712 cx.set_state(indoc! {"
7713 one.twoˇ
7714 "});
7715
7716 // The format request takes a long time. When it completes, it inserts
7717 // a newline and an indent before the `.`
7718 cx.lsp
7719 .handle_request::<lsp::request::Formatting, _, _>(move |_, cx| {
7720 let executor = cx.background_executor().clone();
7721 async move {
7722 executor.timer(Duration::from_millis(100)).await;
7723 Ok(Some(vec![lsp::TextEdit {
7724 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
7725 new_text: "\n ".into(),
7726 }]))
7727 }
7728 });
7729
7730 // Submit a format request.
7731 let format_1 = cx
7732 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7733 .unwrap();
7734 cx.executor().run_until_parked();
7735
7736 // Submit a second format request.
7737 let format_2 = cx
7738 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7739 .unwrap();
7740 cx.executor().run_until_parked();
7741
7742 // Wait for both format requests to complete
7743 cx.executor().advance_clock(Duration::from_millis(200));
7744 cx.executor().start_waiting();
7745 format_1.await.unwrap();
7746 cx.executor().start_waiting();
7747 format_2.await.unwrap();
7748
7749 // The formatting edits only happens once.
7750 cx.assert_editor_state(indoc! {"
7751 one
7752 .twoˇ
7753 "});
7754}
7755
7756#[gpui::test]
7757async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
7758 init_test(cx, |settings| {
7759 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
7760 });
7761
7762 let mut cx = EditorLspTestContext::new_rust(
7763 lsp::ServerCapabilities {
7764 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7765 ..Default::default()
7766 },
7767 cx,
7768 )
7769 .await;
7770
7771 // Set up a buffer white some trailing whitespace and no trailing newline.
7772 cx.set_state(
7773 &[
7774 "one ", //
7775 "twoˇ", //
7776 "three ", //
7777 "four", //
7778 ]
7779 .join("\n"),
7780 );
7781
7782 // Submit a format request.
7783 let format = cx
7784 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
7785 .unwrap();
7786
7787 // Record which buffer changes have been sent to the language server
7788 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
7789 cx.lsp
7790 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
7791 let buffer_changes = buffer_changes.clone();
7792 move |params, _| {
7793 buffer_changes.lock().extend(
7794 params
7795 .content_changes
7796 .into_iter()
7797 .map(|e| (e.range.unwrap(), e.text)),
7798 );
7799 }
7800 });
7801
7802 // Handle formatting requests to the language server.
7803 cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
7804 let buffer_changes = buffer_changes.clone();
7805 move |_, _| {
7806 // When formatting is requested, trailing whitespace has already been stripped,
7807 // and the trailing newline has already been added.
7808 assert_eq!(
7809 &buffer_changes.lock()[1..],
7810 &[
7811 (
7812 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
7813 "".into()
7814 ),
7815 (
7816 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
7817 "".into()
7818 ),
7819 (
7820 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
7821 "\n".into()
7822 ),
7823 ]
7824 );
7825
7826 // Insert blank lines between each line of the buffer.
7827 async move {
7828 Ok(Some(vec![
7829 lsp::TextEdit {
7830 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
7831 new_text: "\n".into(),
7832 },
7833 lsp::TextEdit {
7834 range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
7835 new_text: "\n".into(),
7836 },
7837 ]))
7838 }
7839 }
7840 });
7841
7842 // After formatting the buffer, the trailing whitespace is stripped,
7843 // a newline is appended, and the edits provided by the language server
7844 // have been applied.
7845 format.await.unwrap();
7846 cx.assert_editor_state(
7847 &[
7848 "one", //
7849 "", //
7850 "twoˇ", //
7851 "", //
7852 "three", //
7853 "four", //
7854 "", //
7855 ]
7856 .join("\n"),
7857 );
7858
7859 // Undoing the formatting undoes the trailing whitespace removal, the
7860 // trailing newline, and the LSP edits.
7861 cx.update_buffer(|buffer, cx| buffer.undo(cx));
7862 cx.assert_editor_state(
7863 &[
7864 "one ", //
7865 "twoˇ", //
7866 "three ", //
7867 "four", //
7868 ]
7869 .join("\n"),
7870 );
7871}
7872
7873#[gpui::test]
7874async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
7875 cx: &mut gpui::TestAppContext,
7876) {
7877 init_test(cx, |_| {});
7878
7879 cx.update(|cx| {
7880 cx.update_global::<SettingsStore, _>(|settings, cx| {
7881 settings.update_user_settings::<EditorSettings>(cx, |settings| {
7882 settings.auto_signature_help = Some(true);
7883 });
7884 });
7885 });
7886
7887 let mut cx = EditorLspTestContext::new_rust(
7888 lsp::ServerCapabilities {
7889 signature_help_provider: Some(lsp::SignatureHelpOptions {
7890 ..Default::default()
7891 }),
7892 ..Default::default()
7893 },
7894 cx,
7895 )
7896 .await;
7897
7898 let language = Language::new(
7899 LanguageConfig {
7900 name: "Rust".into(),
7901 brackets: BracketPairConfig {
7902 pairs: vec![
7903 BracketPair {
7904 start: "{".to_string(),
7905 end: "}".to_string(),
7906 close: true,
7907 surround: true,
7908 newline: true,
7909 },
7910 BracketPair {
7911 start: "(".to_string(),
7912 end: ")".to_string(),
7913 close: true,
7914 surround: true,
7915 newline: true,
7916 },
7917 BracketPair {
7918 start: "/*".to_string(),
7919 end: " */".to_string(),
7920 close: true,
7921 surround: true,
7922 newline: true,
7923 },
7924 BracketPair {
7925 start: "[".to_string(),
7926 end: "]".to_string(),
7927 close: false,
7928 surround: false,
7929 newline: true,
7930 },
7931 BracketPair {
7932 start: "\"".to_string(),
7933 end: "\"".to_string(),
7934 close: true,
7935 surround: true,
7936 newline: false,
7937 },
7938 BracketPair {
7939 start: "<".to_string(),
7940 end: ">".to_string(),
7941 close: false,
7942 surround: true,
7943 newline: true,
7944 },
7945 ],
7946 ..Default::default()
7947 },
7948 autoclose_before: "})]".to_string(),
7949 ..Default::default()
7950 },
7951 Some(tree_sitter_rust::LANGUAGE.into()),
7952 );
7953 let language = Arc::new(language);
7954
7955 cx.language_registry().add(language.clone());
7956 cx.update_buffer(|buffer, cx| {
7957 buffer.set_language(Some(language), cx);
7958 });
7959
7960 cx.set_state(
7961 &r#"
7962 fn main() {
7963 sampleˇ
7964 }
7965 "#
7966 .unindent(),
7967 );
7968
7969 cx.update_editor(|editor, window, cx| {
7970 editor.handle_input("(", window, cx);
7971 });
7972 cx.assert_editor_state(
7973 &"
7974 fn main() {
7975 sample(ˇ)
7976 }
7977 "
7978 .unindent(),
7979 );
7980
7981 let mocked_response = lsp::SignatureHelp {
7982 signatures: vec![lsp::SignatureInformation {
7983 label: "fn sample(param1: u8, param2: u8)".to_string(),
7984 documentation: None,
7985 parameters: Some(vec![
7986 lsp::ParameterInformation {
7987 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
7988 documentation: None,
7989 },
7990 lsp::ParameterInformation {
7991 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
7992 documentation: None,
7993 },
7994 ]),
7995 active_parameter: None,
7996 }],
7997 active_signature: Some(0),
7998 active_parameter: Some(0),
7999 };
8000 handle_signature_help_request(&mut cx, mocked_response).await;
8001
8002 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8003 .await;
8004
8005 cx.editor(|editor, _, _| {
8006 let signature_help_state = editor.signature_help_state.popover().cloned();
8007 assert!(signature_help_state.is_some());
8008 let ParsedMarkdown {
8009 text, highlights, ..
8010 } = signature_help_state.unwrap().parsed_content;
8011 assert_eq!(text, "param1: u8, param2: u8");
8012 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8013 });
8014}
8015
8016#[gpui::test]
8017async fn test_handle_input_with_different_show_signature_settings(cx: &mut gpui::TestAppContext) {
8018 init_test(cx, |_| {});
8019
8020 cx.update(|cx| {
8021 cx.update_global::<SettingsStore, _>(|settings, cx| {
8022 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8023 settings.auto_signature_help = Some(false);
8024 settings.show_signature_help_after_edits = Some(false);
8025 });
8026 });
8027 });
8028
8029 let mut cx = EditorLspTestContext::new_rust(
8030 lsp::ServerCapabilities {
8031 signature_help_provider: Some(lsp::SignatureHelpOptions {
8032 ..Default::default()
8033 }),
8034 ..Default::default()
8035 },
8036 cx,
8037 )
8038 .await;
8039
8040 let language = Language::new(
8041 LanguageConfig {
8042 name: "Rust".into(),
8043 brackets: BracketPairConfig {
8044 pairs: vec![
8045 BracketPair {
8046 start: "{".to_string(),
8047 end: "}".to_string(),
8048 close: true,
8049 surround: true,
8050 newline: true,
8051 },
8052 BracketPair {
8053 start: "(".to_string(),
8054 end: ")".to_string(),
8055 close: true,
8056 surround: true,
8057 newline: true,
8058 },
8059 BracketPair {
8060 start: "/*".to_string(),
8061 end: " */".to_string(),
8062 close: true,
8063 surround: true,
8064 newline: true,
8065 },
8066 BracketPair {
8067 start: "[".to_string(),
8068 end: "]".to_string(),
8069 close: false,
8070 surround: false,
8071 newline: true,
8072 },
8073 BracketPair {
8074 start: "\"".to_string(),
8075 end: "\"".to_string(),
8076 close: true,
8077 surround: true,
8078 newline: false,
8079 },
8080 BracketPair {
8081 start: "<".to_string(),
8082 end: ">".to_string(),
8083 close: false,
8084 surround: true,
8085 newline: true,
8086 },
8087 ],
8088 ..Default::default()
8089 },
8090 autoclose_before: "})]".to_string(),
8091 ..Default::default()
8092 },
8093 Some(tree_sitter_rust::LANGUAGE.into()),
8094 );
8095 let language = Arc::new(language);
8096
8097 cx.language_registry().add(language.clone());
8098 cx.update_buffer(|buffer, cx| {
8099 buffer.set_language(Some(language), cx);
8100 });
8101
8102 // Ensure that signature_help is not called when no signature help is enabled.
8103 cx.set_state(
8104 &r#"
8105 fn main() {
8106 sampleˇ
8107 }
8108 "#
8109 .unindent(),
8110 );
8111 cx.update_editor(|editor, window, cx| {
8112 editor.handle_input("(", window, cx);
8113 });
8114 cx.assert_editor_state(
8115 &"
8116 fn main() {
8117 sample(ˇ)
8118 }
8119 "
8120 .unindent(),
8121 );
8122 cx.editor(|editor, _, _| {
8123 assert!(editor.signature_help_state.task().is_none());
8124 });
8125
8126 let mocked_response = lsp::SignatureHelp {
8127 signatures: vec![lsp::SignatureInformation {
8128 label: "fn sample(param1: u8, param2: u8)".to_string(),
8129 documentation: None,
8130 parameters: Some(vec![
8131 lsp::ParameterInformation {
8132 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8133 documentation: None,
8134 },
8135 lsp::ParameterInformation {
8136 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8137 documentation: None,
8138 },
8139 ]),
8140 active_parameter: None,
8141 }],
8142 active_signature: Some(0),
8143 active_parameter: Some(0),
8144 };
8145
8146 // Ensure that signature_help is called when enabled afte edits
8147 cx.update(|_, cx| {
8148 cx.update_global::<SettingsStore, _>(|settings, cx| {
8149 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8150 settings.auto_signature_help = Some(false);
8151 settings.show_signature_help_after_edits = Some(true);
8152 });
8153 });
8154 });
8155 cx.set_state(
8156 &r#"
8157 fn main() {
8158 sampleˇ
8159 }
8160 "#
8161 .unindent(),
8162 );
8163 cx.update_editor(|editor, window, cx| {
8164 editor.handle_input("(", window, cx);
8165 });
8166 cx.assert_editor_state(
8167 &"
8168 fn main() {
8169 sample(ˇ)
8170 }
8171 "
8172 .unindent(),
8173 );
8174 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8175 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8176 .await;
8177 cx.update_editor(|editor, _, _| {
8178 let signature_help_state = editor.signature_help_state.popover().cloned();
8179 assert!(signature_help_state.is_some());
8180 let ParsedMarkdown {
8181 text, highlights, ..
8182 } = signature_help_state.unwrap().parsed_content;
8183 assert_eq!(text, "param1: u8, param2: u8");
8184 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8185 editor.signature_help_state = SignatureHelpState::default();
8186 });
8187
8188 // Ensure that signature_help is called when auto signature help override is enabled
8189 cx.update(|_, cx| {
8190 cx.update_global::<SettingsStore, _>(|settings, cx| {
8191 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8192 settings.auto_signature_help = Some(true);
8193 settings.show_signature_help_after_edits = Some(false);
8194 });
8195 });
8196 });
8197 cx.set_state(
8198 &r#"
8199 fn main() {
8200 sampleˇ
8201 }
8202 "#
8203 .unindent(),
8204 );
8205 cx.update_editor(|editor, window, cx| {
8206 editor.handle_input("(", window, cx);
8207 });
8208 cx.assert_editor_state(
8209 &"
8210 fn main() {
8211 sample(ˇ)
8212 }
8213 "
8214 .unindent(),
8215 );
8216 handle_signature_help_request(&mut cx, mocked_response).await;
8217 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8218 .await;
8219 cx.editor(|editor, _, _| {
8220 let signature_help_state = editor.signature_help_state.popover().cloned();
8221 assert!(signature_help_state.is_some());
8222 let ParsedMarkdown {
8223 text, highlights, ..
8224 } = signature_help_state.unwrap().parsed_content;
8225 assert_eq!(text, "param1: u8, param2: u8");
8226 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8227 });
8228}
8229
8230#[gpui::test]
8231async fn test_signature_help(cx: &mut gpui::TestAppContext) {
8232 init_test(cx, |_| {});
8233 cx.update(|cx| {
8234 cx.update_global::<SettingsStore, _>(|settings, cx| {
8235 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8236 settings.auto_signature_help = Some(true);
8237 });
8238 });
8239 });
8240
8241 let mut cx = EditorLspTestContext::new_rust(
8242 lsp::ServerCapabilities {
8243 signature_help_provider: Some(lsp::SignatureHelpOptions {
8244 ..Default::default()
8245 }),
8246 ..Default::default()
8247 },
8248 cx,
8249 )
8250 .await;
8251
8252 // A test that directly calls `show_signature_help`
8253 cx.update_editor(|editor, window, cx| {
8254 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8255 });
8256
8257 let mocked_response = lsp::SignatureHelp {
8258 signatures: vec![lsp::SignatureInformation {
8259 label: "fn sample(param1: u8, param2: u8)".to_string(),
8260 documentation: None,
8261 parameters: Some(vec![
8262 lsp::ParameterInformation {
8263 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8264 documentation: None,
8265 },
8266 lsp::ParameterInformation {
8267 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8268 documentation: None,
8269 },
8270 ]),
8271 active_parameter: None,
8272 }],
8273 active_signature: Some(0),
8274 active_parameter: Some(0),
8275 };
8276 handle_signature_help_request(&mut cx, mocked_response).await;
8277
8278 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8279 .await;
8280
8281 cx.editor(|editor, _, _| {
8282 let signature_help_state = editor.signature_help_state.popover().cloned();
8283 assert!(signature_help_state.is_some());
8284 let ParsedMarkdown {
8285 text, highlights, ..
8286 } = signature_help_state.unwrap().parsed_content;
8287 assert_eq!(text, "param1: u8, param2: u8");
8288 assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
8289 });
8290
8291 // When exiting outside from inside the brackets, `signature_help` is closed.
8292 cx.set_state(indoc! {"
8293 fn main() {
8294 sample(ˇ);
8295 }
8296
8297 fn sample(param1: u8, param2: u8) {}
8298 "});
8299
8300 cx.update_editor(|editor, window, cx| {
8301 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
8302 });
8303
8304 let mocked_response = lsp::SignatureHelp {
8305 signatures: Vec::new(),
8306 active_signature: None,
8307 active_parameter: None,
8308 };
8309 handle_signature_help_request(&mut cx, mocked_response).await;
8310
8311 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
8312 .await;
8313
8314 cx.editor(|editor, _, _| {
8315 assert!(!editor.signature_help_state.is_shown());
8316 });
8317
8318 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
8319 cx.set_state(indoc! {"
8320 fn main() {
8321 sample(ˇ);
8322 }
8323
8324 fn sample(param1: u8, param2: u8) {}
8325 "});
8326
8327 let mocked_response = lsp::SignatureHelp {
8328 signatures: vec![lsp::SignatureInformation {
8329 label: "fn sample(param1: u8, param2: u8)".to_string(),
8330 documentation: None,
8331 parameters: Some(vec![
8332 lsp::ParameterInformation {
8333 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8334 documentation: None,
8335 },
8336 lsp::ParameterInformation {
8337 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8338 documentation: None,
8339 },
8340 ]),
8341 active_parameter: None,
8342 }],
8343 active_signature: Some(0),
8344 active_parameter: Some(0),
8345 };
8346 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8347 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8348 .await;
8349 cx.editor(|editor, _, _| {
8350 assert!(editor.signature_help_state.is_shown());
8351 });
8352
8353 // Restore the popover with more parameter input
8354 cx.set_state(indoc! {"
8355 fn main() {
8356 sample(param1, param2ˇ);
8357 }
8358
8359 fn sample(param1: u8, param2: u8) {}
8360 "});
8361
8362 let mocked_response = lsp::SignatureHelp {
8363 signatures: vec![lsp::SignatureInformation {
8364 label: "fn sample(param1: u8, param2: u8)".to_string(),
8365 documentation: None,
8366 parameters: Some(vec![
8367 lsp::ParameterInformation {
8368 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8369 documentation: None,
8370 },
8371 lsp::ParameterInformation {
8372 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8373 documentation: None,
8374 },
8375 ]),
8376 active_parameter: None,
8377 }],
8378 active_signature: Some(0),
8379 active_parameter: Some(1),
8380 };
8381 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8382 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8383 .await;
8384
8385 // When selecting a range, the popover is gone.
8386 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
8387 cx.update_editor(|editor, window, cx| {
8388 editor.change_selections(None, window, cx, |s| {
8389 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
8390 })
8391 });
8392 cx.assert_editor_state(indoc! {"
8393 fn main() {
8394 sample(param1, «ˇparam2»);
8395 }
8396
8397 fn sample(param1: u8, param2: u8) {}
8398 "});
8399 cx.editor(|editor, _, _| {
8400 assert!(!editor.signature_help_state.is_shown());
8401 });
8402
8403 // When unselecting again, the popover is back if within the brackets.
8404 cx.update_editor(|editor, window, cx| {
8405 editor.change_selections(None, window, cx, |s| {
8406 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8407 })
8408 });
8409 cx.assert_editor_state(indoc! {"
8410 fn main() {
8411 sample(param1, ˇparam2);
8412 }
8413
8414 fn sample(param1: u8, param2: u8) {}
8415 "});
8416 handle_signature_help_request(&mut cx, mocked_response).await;
8417 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8418 .await;
8419 cx.editor(|editor, _, _| {
8420 assert!(editor.signature_help_state.is_shown());
8421 });
8422
8423 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
8424 cx.update_editor(|editor, window, cx| {
8425 editor.change_selections(None, window, cx, |s| {
8426 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
8427 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8428 })
8429 });
8430 cx.assert_editor_state(indoc! {"
8431 fn main() {
8432 sample(param1, ˇparam2);
8433 }
8434
8435 fn sample(param1: u8, param2: u8) {}
8436 "});
8437
8438 let mocked_response = lsp::SignatureHelp {
8439 signatures: vec![lsp::SignatureInformation {
8440 label: "fn sample(param1: u8, param2: u8)".to_string(),
8441 documentation: None,
8442 parameters: Some(vec![
8443 lsp::ParameterInformation {
8444 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8445 documentation: None,
8446 },
8447 lsp::ParameterInformation {
8448 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8449 documentation: None,
8450 },
8451 ]),
8452 active_parameter: None,
8453 }],
8454 active_signature: Some(0),
8455 active_parameter: Some(1),
8456 };
8457 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8458 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8459 .await;
8460 cx.update_editor(|editor, _, cx| {
8461 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
8462 });
8463 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
8464 .await;
8465 cx.update_editor(|editor, window, cx| {
8466 editor.change_selections(None, window, cx, |s| {
8467 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
8468 })
8469 });
8470 cx.assert_editor_state(indoc! {"
8471 fn main() {
8472 sample(param1, «ˇparam2»);
8473 }
8474
8475 fn sample(param1: u8, param2: u8) {}
8476 "});
8477 cx.update_editor(|editor, window, cx| {
8478 editor.change_selections(None, window, cx, |s| {
8479 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
8480 })
8481 });
8482 cx.assert_editor_state(indoc! {"
8483 fn main() {
8484 sample(param1, ˇparam2);
8485 }
8486
8487 fn sample(param1: u8, param2: u8) {}
8488 "});
8489 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
8490 .await;
8491}
8492
8493#[gpui::test]
8494async fn test_completion(cx: &mut gpui::TestAppContext) {
8495 init_test(cx, |_| {});
8496
8497 let mut cx = EditorLspTestContext::new_rust(
8498 lsp::ServerCapabilities {
8499 completion_provider: Some(lsp::CompletionOptions {
8500 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
8501 resolve_provider: Some(true),
8502 ..Default::default()
8503 }),
8504 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
8505 ..Default::default()
8506 },
8507 cx,
8508 )
8509 .await;
8510 let counter = Arc::new(AtomicUsize::new(0));
8511
8512 cx.set_state(indoc! {"
8513 oneˇ
8514 two
8515 three
8516 "});
8517 cx.simulate_keystroke(".");
8518 handle_completion_request(
8519 &mut cx,
8520 indoc! {"
8521 one.|<>
8522 two
8523 three
8524 "},
8525 vec!["first_completion", "second_completion"],
8526 counter.clone(),
8527 )
8528 .await;
8529 cx.condition(|editor, _| editor.context_menu_visible())
8530 .await;
8531 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
8532
8533 let _handler = handle_signature_help_request(
8534 &mut cx,
8535 lsp::SignatureHelp {
8536 signatures: vec![lsp::SignatureInformation {
8537 label: "test signature".to_string(),
8538 documentation: None,
8539 parameters: Some(vec![lsp::ParameterInformation {
8540 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
8541 documentation: None,
8542 }]),
8543 active_parameter: None,
8544 }],
8545 active_signature: None,
8546 active_parameter: None,
8547 },
8548 );
8549 cx.update_editor(|editor, window, cx| {
8550 assert!(
8551 !editor.signature_help_state.is_shown(),
8552 "No signature help was called for"
8553 );
8554 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8555 });
8556 cx.run_until_parked();
8557 cx.update_editor(|editor, _, _| {
8558 assert!(
8559 !editor.signature_help_state.is_shown(),
8560 "No signature help should be shown when completions menu is open"
8561 );
8562 });
8563
8564 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8565 editor.context_menu_next(&Default::default(), window, cx);
8566 editor
8567 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8568 .unwrap()
8569 });
8570 cx.assert_editor_state(indoc! {"
8571 one.second_completionˇ
8572 two
8573 three
8574 "});
8575
8576 handle_resolve_completion_request(
8577 &mut cx,
8578 Some(vec![
8579 (
8580 //This overlaps with the primary completion edit which is
8581 //misbehavior from the LSP spec, test that we filter it out
8582 indoc! {"
8583 one.second_ˇcompletion
8584 two
8585 threeˇ
8586 "},
8587 "overlapping additional edit",
8588 ),
8589 (
8590 indoc! {"
8591 one.second_completion
8592 two
8593 threeˇ
8594 "},
8595 "\nadditional edit",
8596 ),
8597 ]),
8598 )
8599 .await;
8600 apply_additional_edits.await.unwrap();
8601 cx.assert_editor_state(indoc! {"
8602 one.second_completionˇ
8603 two
8604 three
8605 additional edit
8606 "});
8607
8608 cx.set_state(indoc! {"
8609 one.second_completion
8610 twoˇ
8611 threeˇ
8612 additional edit
8613 "});
8614 cx.simulate_keystroke(" ");
8615 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8616 cx.simulate_keystroke("s");
8617 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8618
8619 cx.assert_editor_state(indoc! {"
8620 one.second_completion
8621 two sˇ
8622 three sˇ
8623 additional edit
8624 "});
8625 handle_completion_request(
8626 &mut cx,
8627 indoc! {"
8628 one.second_completion
8629 two s
8630 three <s|>
8631 additional edit
8632 "},
8633 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
8634 counter.clone(),
8635 )
8636 .await;
8637 cx.condition(|editor, _| editor.context_menu_visible())
8638 .await;
8639 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
8640
8641 cx.simulate_keystroke("i");
8642
8643 handle_completion_request(
8644 &mut cx,
8645 indoc! {"
8646 one.second_completion
8647 two si
8648 three <si|>
8649 additional edit
8650 "},
8651 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
8652 counter.clone(),
8653 )
8654 .await;
8655 cx.condition(|editor, _| editor.context_menu_visible())
8656 .await;
8657 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
8658
8659 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8660 editor
8661 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8662 .unwrap()
8663 });
8664 cx.assert_editor_state(indoc! {"
8665 one.second_completion
8666 two sixth_completionˇ
8667 three sixth_completionˇ
8668 additional edit
8669 "});
8670
8671 apply_additional_edits.await.unwrap();
8672
8673 update_test_language_settings(&mut cx, |settings| {
8674 settings.defaults.show_completions_on_input = Some(false);
8675 });
8676 cx.set_state("editorˇ");
8677 cx.simulate_keystroke(".");
8678 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8679 cx.simulate_keystroke("c");
8680 cx.simulate_keystroke("l");
8681 cx.simulate_keystroke("o");
8682 cx.assert_editor_state("editor.cloˇ");
8683 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
8684 cx.update_editor(|editor, window, cx| {
8685 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
8686 });
8687 handle_completion_request(
8688 &mut cx,
8689 "editor.<clo|>",
8690 vec!["close", "clobber"],
8691 counter.clone(),
8692 )
8693 .await;
8694 cx.condition(|editor, _| editor.context_menu_visible())
8695 .await;
8696 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
8697
8698 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
8699 editor
8700 .confirm_completion(&ConfirmCompletion::default(), window, cx)
8701 .unwrap()
8702 });
8703 cx.assert_editor_state("editor.closeˇ");
8704 handle_resolve_completion_request(&mut cx, None).await;
8705 apply_additional_edits.await.unwrap();
8706}
8707
8708#[gpui::test]
8709async fn test_multiline_completion(cx: &mut gpui::TestAppContext) {
8710 init_test(cx, |_| {});
8711
8712 let fs = FakeFs::new(cx.executor());
8713 fs.insert_tree(
8714 "/a",
8715 json!({
8716 "main.ts": "a",
8717 }),
8718 )
8719 .await;
8720
8721 let project = Project::test(fs, ["/a".as_ref()], cx).await;
8722 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8723 let typescript_language = Arc::new(Language::new(
8724 LanguageConfig {
8725 name: "TypeScript".into(),
8726 matcher: LanguageMatcher {
8727 path_suffixes: vec!["ts".to_string()],
8728 ..LanguageMatcher::default()
8729 },
8730 line_comments: vec!["// ".into()],
8731 ..LanguageConfig::default()
8732 },
8733 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8734 ));
8735 language_registry.add(typescript_language.clone());
8736 let mut fake_servers = language_registry.register_fake_lsp(
8737 "TypeScript",
8738 FakeLspAdapter {
8739 capabilities: lsp::ServerCapabilities {
8740 completion_provider: Some(lsp::CompletionOptions {
8741 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
8742 ..lsp::CompletionOptions::default()
8743 }),
8744 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
8745 ..lsp::ServerCapabilities::default()
8746 },
8747 // Emulate vtsls label generation
8748 label_for_completion: Some(Box::new(|item, _| {
8749 let text = if let Some(description) = item
8750 .label_details
8751 .as_ref()
8752 .and_then(|label_details| label_details.description.as_ref())
8753 {
8754 format!("{} {}", item.label, description)
8755 } else if let Some(detail) = &item.detail {
8756 format!("{} {}", item.label, detail)
8757 } else {
8758 item.label.clone()
8759 };
8760 let len = text.len();
8761 Some(language::CodeLabel {
8762 text,
8763 runs: Vec::new(),
8764 filter_range: 0..len,
8765 })
8766 })),
8767 ..FakeLspAdapter::default()
8768 },
8769 );
8770 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8771 let cx = &mut VisualTestContext::from_window(*workspace, cx);
8772 let worktree_id = workspace
8773 .update(cx, |workspace, _window, cx| {
8774 workspace.project().update(cx, |project, cx| {
8775 project.worktrees(cx).next().unwrap().read(cx).id()
8776 })
8777 })
8778 .unwrap();
8779 let _buffer = project
8780 .update(cx, |project, cx| {
8781 project.open_local_buffer_with_lsp("/a/main.ts", cx)
8782 })
8783 .await
8784 .unwrap();
8785 let editor = workspace
8786 .update(cx, |workspace, window, cx| {
8787 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
8788 })
8789 .unwrap()
8790 .await
8791 .unwrap()
8792 .downcast::<Editor>()
8793 .unwrap();
8794 let fake_server = fake_servers.next().await.unwrap();
8795
8796 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
8797 let multiline_label_2 = "a\nb\nc\n";
8798 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
8799 let multiline_description = "d\ne\nf\n";
8800 let multiline_detail_2 = "g\nh\ni\n";
8801
8802 let mut completion_handle =
8803 fake_server.handle_request::<lsp::request::Completion, _, _>(move |params, _| async move {
8804 Ok(Some(lsp::CompletionResponse::Array(vec![
8805 lsp::CompletionItem {
8806 label: multiline_label.to_string(),
8807 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8808 range: lsp::Range {
8809 start: lsp::Position {
8810 line: params.text_document_position.position.line,
8811 character: params.text_document_position.position.character,
8812 },
8813 end: lsp::Position {
8814 line: params.text_document_position.position.line,
8815 character: params.text_document_position.position.character,
8816 },
8817 },
8818 new_text: "new_text_1".to_string(),
8819 })),
8820 ..lsp::CompletionItem::default()
8821 },
8822 lsp::CompletionItem {
8823 label: "single line label 1".to_string(),
8824 detail: Some(multiline_detail.to_string()),
8825 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8826 range: lsp::Range {
8827 start: lsp::Position {
8828 line: params.text_document_position.position.line,
8829 character: params.text_document_position.position.character,
8830 },
8831 end: lsp::Position {
8832 line: params.text_document_position.position.line,
8833 character: params.text_document_position.position.character,
8834 },
8835 },
8836 new_text: "new_text_2".to_string(),
8837 })),
8838 ..lsp::CompletionItem::default()
8839 },
8840 lsp::CompletionItem {
8841 label: "single line label 2".to_string(),
8842 label_details: Some(lsp::CompletionItemLabelDetails {
8843 description: Some(multiline_description.to_string()),
8844 detail: None,
8845 }),
8846 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8847 range: lsp::Range {
8848 start: lsp::Position {
8849 line: params.text_document_position.position.line,
8850 character: params.text_document_position.position.character,
8851 },
8852 end: lsp::Position {
8853 line: params.text_document_position.position.line,
8854 character: params.text_document_position.position.character,
8855 },
8856 },
8857 new_text: "new_text_2".to_string(),
8858 })),
8859 ..lsp::CompletionItem::default()
8860 },
8861 lsp::CompletionItem {
8862 label: multiline_label_2.to_string(),
8863 detail: Some(multiline_detail_2.to_string()),
8864 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8865 range: lsp::Range {
8866 start: lsp::Position {
8867 line: params.text_document_position.position.line,
8868 character: params.text_document_position.position.character,
8869 },
8870 end: lsp::Position {
8871 line: params.text_document_position.position.line,
8872 character: params.text_document_position.position.character,
8873 },
8874 },
8875 new_text: "new_text_3".to_string(),
8876 })),
8877 ..lsp::CompletionItem::default()
8878 },
8879 lsp::CompletionItem {
8880 label: "Label with many spaces and \t but without newlines".to_string(),
8881 detail: Some(
8882 "Details with many spaces and \t but without newlines".to_string(),
8883 ),
8884 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8885 range: lsp::Range {
8886 start: lsp::Position {
8887 line: params.text_document_position.position.line,
8888 character: params.text_document_position.position.character,
8889 },
8890 end: lsp::Position {
8891 line: params.text_document_position.position.line,
8892 character: params.text_document_position.position.character,
8893 },
8894 },
8895 new_text: "new_text_4".to_string(),
8896 })),
8897 ..lsp::CompletionItem::default()
8898 },
8899 ])))
8900 });
8901
8902 editor.update_in(cx, |editor, window, cx| {
8903 cx.focus_self(window);
8904 editor.move_to_end(&MoveToEnd, window, cx);
8905 editor.handle_input(".", window, cx);
8906 });
8907 cx.run_until_parked();
8908 completion_handle.next().await.unwrap();
8909
8910 editor.update(cx, |editor, _| {
8911 assert!(editor.context_menu_visible());
8912 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
8913 {
8914 let completion_labels = menu
8915 .completions
8916 .borrow()
8917 .iter()
8918 .map(|c| c.label.text.clone())
8919 .collect::<Vec<_>>();
8920 assert_eq!(
8921 completion_labels,
8922 &[
8923 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
8924 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
8925 "single line label 2 d e f ",
8926 "a b c g h i ",
8927 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
8928 ],
8929 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
8930 );
8931
8932 for completion in menu
8933 .completions
8934 .borrow()
8935 .iter() {
8936 assert_eq!(
8937 completion.label.filter_range,
8938 0..completion.label.text.len(),
8939 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
8940 );
8941 }
8942
8943 } else {
8944 panic!("expected completion menu to be open");
8945 }
8946 });
8947}
8948
8949#[gpui::test]
8950async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
8951 init_test(cx, |_| {});
8952 let mut cx = EditorLspTestContext::new_rust(
8953 lsp::ServerCapabilities {
8954 completion_provider: Some(lsp::CompletionOptions {
8955 trigger_characters: Some(vec![".".to_string()]),
8956 ..Default::default()
8957 }),
8958 ..Default::default()
8959 },
8960 cx,
8961 )
8962 .await;
8963 cx.lsp
8964 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
8965 Ok(Some(lsp::CompletionResponse::Array(vec![
8966 lsp::CompletionItem {
8967 label: "first".into(),
8968 ..Default::default()
8969 },
8970 lsp::CompletionItem {
8971 label: "last".into(),
8972 ..Default::default()
8973 },
8974 ])))
8975 });
8976 cx.set_state("variableˇ");
8977 cx.simulate_keystroke(".");
8978 cx.executor().run_until_parked();
8979
8980 cx.update_editor(|editor, _, _| {
8981 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
8982 {
8983 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
8984 } else {
8985 panic!("expected completion menu to be open");
8986 }
8987 });
8988
8989 cx.update_editor(|editor, window, cx| {
8990 editor.move_page_down(&MovePageDown::default(), window, cx);
8991 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
8992 {
8993 assert!(
8994 menu.selected_item == 1,
8995 "expected PageDown to select the last item from the context menu"
8996 );
8997 } else {
8998 panic!("expected completion menu to stay open after PageDown");
8999 }
9000 });
9001
9002 cx.update_editor(|editor, window, cx| {
9003 editor.move_page_up(&MovePageUp::default(), window, cx);
9004 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9005 {
9006 assert!(
9007 menu.selected_item == 0,
9008 "expected PageUp to select the first item from the context menu"
9009 );
9010 } else {
9011 panic!("expected completion menu to stay open after PageUp");
9012 }
9013 });
9014}
9015
9016#[gpui::test]
9017async fn test_completion_sort(cx: &mut gpui::TestAppContext) {
9018 init_test(cx, |_| {});
9019 let mut cx = EditorLspTestContext::new_rust(
9020 lsp::ServerCapabilities {
9021 completion_provider: Some(lsp::CompletionOptions {
9022 trigger_characters: Some(vec![".".to_string()]),
9023 ..Default::default()
9024 }),
9025 ..Default::default()
9026 },
9027 cx,
9028 )
9029 .await;
9030 cx.lsp
9031 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9032 Ok(Some(lsp::CompletionResponse::Array(vec![
9033 lsp::CompletionItem {
9034 label: "Range".into(),
9035 sort_text: Some("a".into()),
9036 ..Default::default()
9037 },
9038 lsp::CompletionItem {
9039 label: "r".into(),
9040 sort_text: Some("b".into()),
9041 ..Default::default()
9042 },
9043 lsp::CompletionItem {
9044 label: "ret".into(),
9045 sort_text: Some("c".into()),
9046 ..Default::default()
9047 },
9048 lsp::CompletionItem {
9049 label: "return".into(),
9050 sort_text: Some("d".into()),
9051 ..Default::default()
9052 },
9053 lsp::CompletionItem {
9054 label: "slice".into(),
9055 sort_text: Some("d".into()),
9056 ..Default::default()
9057 },
9058 ])))
9059 });
9060 cx.set_state("rˇ");
9061 cx.executor().run_until_parked();
9062 cx.update_editor(|editor, window, cx| {
9063 editor.show_completions(
9064 &ShowCompletions {
9065 trigger: Some("r".into()),
9066 },
9067 window,
9068 cx,
9069 );
9070 });
9071 cx.executor().run_until_parked();
9072
9073 cx.update_editor(|editor, _, _| {
9074 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9075 {
9076 assert_eq!(
9077 completion_menu_entries(&menu),
9078 &["r", "ret", "Range", "return"]
9079 );
9080 } else {
9081 panic!("expected completion menu to be open");
9082 }
9083 });
9084}
9085
9086#[gpui::test]
9087async fn test_no_duplicated_completion_requests(cx: &mut gpui::TestAppContext) {
9088 init_test(cx, |_| {});
9089
9090 let mut cx = EditorLspTestContext::new_rust(
9091 lsp::ServerCapabilities {
9092 completion_provider: Some(lsp::CompletionOptions {
9093 trigger_characters: Some(vec![".".to_string()]),
9094 resolve_provider: Some(true),
9095 ..Default::default()
9096 }),
9097 ..Default::default()
9098 },
9099 cx,
9100 )
9101 .await;
9102
9103 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
9104 cx.simulate_keystroke(".");
9105 let completion_item = lsp::CompletionItem {
9106 label: "Some".into(),
9107 kind: Some(lsp::CompletionItemKind::SNIPPET),
9108 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
9109 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
9110 kind: lsp::MarkupKind::Markdown,
9111 value: "```rust\nSome(2)\n```".to_string(),
9112 })),
9113 deprecated: Some(false),
9114 sort_text: Some("Some".to_string()),
9115 filter_text: Some("Some".to_string()),
9116 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
9117 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9118 range: lsp::Range {
9119 start: lsp::Position {
9120 line: 0,
9121 character: 22,
9122 },
9123 end: lsp::Position {
9124 line: 0,
9125 character: 22,
9126 },
9127 },
9128 new_text: "Some(2)".to_string(),
9129 })),
9130 additional_text_edits: Some(vec![lsp::TextEdit {
9131 range: lsp::Range {
9132 start: lsp::Position {
9133 line: 0,
9134 character: 20,
9135 },
9136 end: lsp::Position {
9137 line: 0,
9138 character: 22,
9139 },
9140 },
9141 new_text: "".to_string(),
9142 }]),
9143 ..Default::default()
9144 };
9145
9146 let closure_completion_item = completion_item.clone();
9147 let counter = Arc::new(AtomicUsize::new(0));
9148 let counter_clone = counter.clone();
9149 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
9150 let task_completion_item = closure_completion_item.clone();
9151 counter_clone.fetch_add(1, atomic::Ordering::Release);
9152 async move {
9153 Ok(Some(lsp::CompletionResponse::Array(vec![
9154 task_completion_item,
9155 ])))
9156 }
9157 });
9158
9159 cx.condition(|editor, _| editor.context_menu_visible())
9160 .await;
9161 cx.assert_editor_state(indoc! {"fn main() { let a = 2.ˇ; }"});
9162 assert!(request.next().await.is_some());
9163 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9164
9165 cx.simulate_keystroke("S");
9166 cx.simulate_keystroke("o");
9167 cx.simulate_keystroke("m");
9168 cx.condition(|editor, _| editor.context_menu_visible())
9169 .await;
9170 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Somˇ; }"});
9171 assert!(request.next().await.is_some());
9172 assert!(request.next().await.is_some());
9173 assert!(request.next().await.is_some());
9174 request.close();
9175 assert!(request.next().await.is_none());
9176 assert_eq!(
9177 counter.load(atomic::Ordering::Acquire),
9178 4,
9179 "With the completions menu open, only one LSP request should happen per input"
9180 );
9181}
9182
9183#[gpui::test]
9184async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
9185 init_test(cx, |_| {});
9186 let mut cx = EditorTestContext::new(cx).await;
9187 let language = Arc::new(Language::new(
9188 LanguageConfig {
9189 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
9190 ..Default::default()
9191 },
9192 Some(tree_sitter_rust::LANGUAGE.into()),
9193 ));
9194 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
9195
9196 // If multiple selections intersect a line, the line is only toggled once.
9197 cx.set_state(indoc! {"
9198 fn a() {
9199 «//b();
9200 ˇ»// «c();
9201 //ˇ» d();
9202 }
9203 "});
9204
9205 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9206
9207 cx.assert_editor_state(indoc! {"
9208 fn a() {
9209 «b();
9210 c();
9211 ˇ» d();
9212 }
9213 "});
9214
9215 // The comment prefix is inserted at the same column for every line in a
9216 // selection.
9217 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9218
9219 cx.assert_editor_state(indoc! {"
9220 fn a() {
9221 // «b();
9222 // c();
9223 ˇ»// d();
9224 }
9225 "});
9226
9227 // If a selection ends at the beginning of a line, that line is not toggled.
9228 cx.set_selections_state(indoc! {"
9229 fn a() {
9230 // b();
9231 «// c();
9232 ˇ» // d();
9233 }
9234 "});
9235
9236 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9237
9238 cx.assert_editor_state(indoc! {"
9239 fn a() {
9240 // b();
9241 «c();
9242 ˇ» // d();
9243 }
9244 "});
9245
9246 // If a selection span a single line and is empty, the line is toggled.
9247 cx.set_state(indoc! {"
9248 fn a() {
9249 a();
9250 b();
9251 ˇ
9252 }
9253 "});
9254
9255 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9256
9257 cx.assert_editor_state(indoc! {"
9258 fn a() {
9259 a();
9260 b();
9261 //•ˇ
9262 }
9263 "});
9264
9265 // If a selection span multiple lines, empty lines are not toggled.
9266 cx.set_state(indoc! {"
9267 fn a() {
9268 «a();
9269
9270 c();ˇ»
9271 }
9272 "});
9273
9274 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9275
9276 cx.assert_editor_state(indoc! {"
9277 fn a() {
9278 // «a();
9279
9280 // c();ˇ»
9281 }
9282 "});
9283
9284 // If a selection includes multiple comment prefixes, all lines are uncommented.
9285 cx.set_state(indoc! {"
9286 fn a() {
9287 «// a();
9288 /// b();
9289 //! c();ˇ»
9290 }
9291 "});
9292
9293 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
9294
9295 cx.assert_editor_state(indoc! {"
9296 fn a() {
9297 «a();
9298 b();
9299 c();ˇ»
9300 }
9301 "});
9302}
9303
9304#[gpui::test]
9305async fn test_toggle_comment_ignore_indent(cx: &mut gpui::TestAppContext) {
9306 init_test(cx, |_| {});
9307 let mut cx = EditorTestContext::new(cx).await;
9308 let language = Arc::new(Language::new(
9309 LanguageConfig {
9310 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
9311 ..Default::default()
9312 },
9313 Some(tree_sitter_rust::LANGUAGE.into()),
9314 ));
9315 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
9316
9317 let toggle_comments = &ToggleComments {
9318 advance_downwards: false,
9319 ignore_indent: true,
9320 };
9321
9322 // If multiple selections intersect a line, the line is only toggled once.
9323 cx.set_state(indoc! {"
9324 fn a() {
9325 // «b();
9326 // c();
9327 // ˇ» d();
9328 }
9329 "});
9330
9331 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9332
9333 cx.assert_editor_state(indoc! {"
9334 fn a() {
9335 «b();
9336 c();
9337 ˇ» d();
9338 }
9339 "});
9340
9341 // The comment prefix is inserted at the beginning of each line
9342 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9343
9344 cx.assert_editor_state(indoc! {"
9345 fn a() {
9346 // «b();
9347 // c();
9348 // ˇ» d();
9349 }
9350 "});
9351
9352 // If a selection ends at the beginning of a line, that line is not toggled.
9353 cx.set_selections_state(indoc! {"
9354 fn a() {
9355 // b();
9356 // «c();
9357 ˇ»// d();
9358 }
9359 "});
9360
9361 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9362
9363 cx.assert_editor_state(indoc! {"
9364 fn a() {
9365 // b();
9366 «c();
9367 ˇ»// d();
9368 }
9369 "});
9370
9371 // If a selection span a single line and is empty, the line is toggled.
9372 cx.set_state(indoc! {"
9373 fn a() {
9374 a();
9375 b();
9376 ˇ
9377 }
9378 "});
9379
9380 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9381
9382 cx.assert_editor_state(indoc! {"
9383 fn a() {
9384 a();
9385 b();
9386 //ˇ
9387 }
9388 "});
9389
9390 // If a selection span multiple lines, empty lines are not toggled.
9391 cx.set_state(indoc! {"
9392 fn a() {
9393 «a();
9394
9395 c();ˇ»
9396 }
9397 "});
9398
9399 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9400
9401 cx.assert_editor_state(indoc! {"
9402 fn a() {
9403 // «a();
9404
9405 // c();ˇ»
9406 }
9407 "});
9408
9409 // If a selection includes multiple comment prefixes, all lines are uncommented.
9410 cx.set_state(indoc! {"
9411 fn a() {
9412 // «a();
9413 /// b();
9414 //! c();ˇ»
9415 }
9416 "});
9417
9418 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
9419
9420 cx.assert_editor_state(indoc! {"
9421 fn a() {
9422 «a();
9423 b();
9424 c();ˇ»
9425 }
9426 "});
9427}
9428
9429#[gpui::test]
9430async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
9431 init_test(cx, |_| {});
9432
9433 let language = Arc::new(Language::new(
9434 LanguageConfig {
9435 line_comments: vec!["// ".into()],
9436 ..Default::default()
9437 },
9438 Some(tree_sitter_rust::LANGUAGE.into()),
9439 ));
9440
9441 let mut cx = EditorTestContext::new(cx).await;
9442
9443 cx.language_registry().add(language.clone());
9444 cx.update_buffer(|buffer, cx| {
9445 buffer.set_language(Some(language), cx);
9446 });
9447
9448 let toggle_comments = &ToggleComments {
9449 advance_downwards: true,
9450 ignore_indent: false,
9451 };
9452
9453 // Single cursor on one line -> advance
9454 // Cursor moves horizontally 3 characters as well on non-blank line
9455 cx.set_state(indoc!(
9456 "fn a() {
9457 ˇdog();
9458 cat();
9459 }"
9460 ));
9461 cx.update_editor(|editor, window, cx| {
9462 editor.toggle_comments(toggle_comments, window, cx);
9463 });
9464 cx.assert_editor_state(indoc!(
9465 "fn a() {
9466 // dog();
9467 catˇ();
9468 }"
9469 ));
9470
9471 // Single selection on one line -> don't advance
9472 cx.set_state(indoc!(
9473 "fn a() {
9474 «dog()ˇ»;
9475 cat();
9476 }"
9477 ));
9478 cx.update_editor(|editor, window, cx| {
9479 editor.toggle_comments(toggle_comments, window, cx);
9480 });
9481 cx.assert_editor_state(indoc!(
9482 "fn a() {
9483 // «dog()ˇ»;
9484 cat();
9485 }"
9486 ));
9487
9488 // Multiple cursors on one line -> advance
9489 cx.set_state(indoc!(
9490 "fn a() {
9491 ˇdˇog();
9492 cat();
9493 }"
9494 ));
9495 cx.update_editor(|editor, window, cx| {
9496 editor.toggle_comments(toggle_comments, window, cx);
9497 });
9498 cx.assert_editor_state(indoc!(
9499 "fn a() {
9500 // dog();
9501 catˇ(ˇ);
9502 }"
9503 ));
9504
9505 // Multiple cursors on one line, with selection -> don't advance
9506 cx.set_state(indoc!(
9507 "fn a() {
9508 ˇdˇog«()ˇ»;
9509 cat();
9510 }"
9511 ));
9512 cx.update_editor(|editor, window, cx| {
9513 editor.toggle_comments(toggle_comments, window, cx);
9514 });
9515 cx.assert_editor_state(indoc!(
9516 "fn a() {
9517 // ˇdˇog«()ˇ»;
9518 cat();
9519 }"
9520 ));
9521
9522 // Single cursor on one line -> advance
9523 // Cursor moves to column 0 on blank line
9524 cx.set_state(indoc!(
9525 "fn a() {
9526 ˇdog();
9527
9528 cat();
9529 }"
9530 ));
9531 cx.update_editor(|editor, window, cx| {
9532 editor.toggle_comments(toggle_comments, window, cx);
9533 });
9534 cx.assert_editor_state(indoc!(
9535 "fn a() {
9536 // dog();
9537 ˇ
9538 cat();
9539 }"
9540 ));
9541
9542 // Single cursor on one line -> advance
9543 // Cursor starts and ends at column 0
9544 cx.set_state(indoc!(
9545 "fn a() {
9546 ˇ dog();
9547 cat();
9548 }"
9549 ));
9550 cx.update_editor(|editor, window, cx| {
9551 editor.toggle_comments(toggle_comments, window, cx);
9552 });
9553 cx.assert_editor_state(indoc!(
9554 "fn a() {
9555 // dog();
9556 ˇ cat();
9557 }"
9558 ));
9559}
9560
9561#[gpui::test]
9562async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
9563 init_test(cx, |_| {});
9564
9565 let mut cx = EditorTestContext::new(cx).await;
9566
9567 let html_language = Arc::new(
9568 Language::new(
9569 LanguageConfig {
9570 name: "HTML".into(),
9571 block_comment: Some(("<!-- ".into(), " -->".into())),
9572 ..Default::default()
9573 },
9574 Some(tree_sitter_html::language()),
9575 )
9576 .with_injection_query(
9577 r#"
9578 (script_element
9579 (raw_text) @injection.content
9580 (#set! injection.language "javascript"))
9581 "#,
9582 )
9583 .unwrap(),
9584 );
9585
9586 let javascript_language = Arc::new(Language::new(
9587 LanguageConfig {
9588 name: "JavaScript".into(),
9589 line_comments: vec!["// ".into()],
9590 ..Default::default()
9591 },
9592 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9593 ));
9594
9595 cx.language_registry().add(html_language.clone());
9596 cx.language_registry().add(javascript_language.clone());
9597 cx.update_buffer(|buffer, cx| {
9598 buffer.set_language(Some(html_language), cx);
9599 });
9600
9601 // Toggle comments for empty selections
9602 cx.set_state(
9603 &r#"
9604 <p>A</p>ˇ
9605 <p>B</p>ˇ
9606 <p>C</p>ˇ
9607 "#
9608 .unindent(),
9609 );
9610 cx.update_editor(|editor, window, cx| {
9611 editor.toggle_comments(&ToggleComments::default(), window, cx)
9612 });
9613 cx.assert_editor_state(
9614 &r#"
9615 <!-- <p>A</p>ˇ -->
9616 <!-- <p>B</p>ˇ -->
9617 <!-- <p>C</p>ˇ -->
9618 "#
9619 .unindent(),
9620 );
9621 cx.update_editor(|editor, window, cx| {
9622 editor.toggle_comments(&ToggleComments::default(), window, cx)
9623 });
9624 cx.assert_editor_state(
9625 &r#"
9626 <p>A</p>ˇ
9627 <p>B</p>ˇ
9628 <p>C</p>ˇ
9629 "#
9630 .unindent(),
9631 );
9632
9633 // Toggle comments for mixture of empty and non-empty selections, where
9634 // multiple selections occupy a given line.
9635 cx.set_state(
9636 &r#"
9637 <p>A«</p>
9638 <p>ˇ»B</p>ˇ
9639 <p>C«</p>
9640 <p>ˇ»D</p>ˇ
9641 "#
9642 .unindent(),
9643 );
9644
9645 cx.update_editor(|editor, window, cx| {
9646 editor.toggle_comments(&ToggleComments::default(), window, cx)
9647 });
9648 cx.assert_editor_state(
9649 &r#"
9650 <!-- <p>A«</p>
9651 <p>ˇ»B</p>ˇ -->
9652 <!-- <p>C«</p>
9653 <p>ˇ»D</p>ˇ -->
9654 "#
9655 .unindent(),
9656 );
9657 cx.update_editor(|editor, window, cx| {
9658 editor.toggle_comments(&ToggleComments::default(), window, cx)
9659 });
9660 cx.assert_editor_state(
9661 &r#"
9662 <p>A«</p>
9663 <p>ˇ»B</p>ˇ
9664 <p>C«</p>
9665 <p>ˇ»D</p>ˇ
9666 "#
9667 .unindent(),
9668 );
9669
9670 // Toggle comments when different languages are active for different
9671 // selections.
9672 cx.set_state(
9673 &r#"
9674 ˇ<script>
9675 ˇvar x = new Y();
9676 ˇ</script>
9677 "#
9678 .unindent(),
9679 );
9680 cx.executor().run_until_parked();
9681 cx.update_editor(|editor, window, cx| {
9682 editor.toggle_comments(&ToggleComments::default(), window, cx)
9683 });
9684 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
9685 // Uncommenting and commenting from this position brings in even more wrong artifacts.
9686 cx.assert_editor_state(
9687 &r#"
9688 <!-- ˇ<script> -->
9689 // ˇvar x = new Y();
9690 // ˇ</script>
9691 "#
9692 .unindent(),
9693 );
9694}
9695
9696#[gpui::test]
9697fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
9698 init_test(cx, |_| {});
9699
9700 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9701 let multibuffer = cx.new(|cx| {
9702 let mut multibuffer = MultiBuffer::new(ReadWrite);
9703 multibuffer.push_excerpts(
9704 buffer.clone(),
9705 [
9706 ExcerptRange {
9707 context: Point::new(0, 0)..Point::new(0, 4),
9708 primary: None,
9709 },
9710 ExcerptRange {
9711 context: Point::new(1, 0)..Point::new(1, 4),
9712 primary: None,
9713 },
9714 ],
9715 cx,
9716 );
9717 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
9718 multibuffer
9719 });
9720
9721 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
9722 editor.update_in(cx, |editor, window, cx| {
9723 assert_eq!(editor.text(cx), "aaaa\nbbbb");
9724 editor.change_selections(None, window, cx, |s| {
9725 s.select_ranges([
9726 Point::new(0, 0)..Point::new(0, 0),
9727 Point::new(1, 0)..Point::new(1, 0),
9728 ])
9729 });
9730
9731 editor.handle_input("X", window, cx);
9732 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
9733 assert_eq!(
9734 editor.selections.ranges(cx),
9735 [
9736 Point::new(0, 1)..Point::new(0, 1),
9737 Point::new(1, 1)..Point::new(1, 1),
9738 ]
9739 );
9740
9741 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
9742 editor.change_selections(None, window, cx, |s| {
9743 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
9744 });
9745 editor.backspace(&Default::default(), window, cx);
9746 assert_eq!(editor.text(cx), "Xa\nbbb");
9747 assert_eq!(
9748 editor.selections.ranges(cx),
9749 [Point::new(1, 0)..Point::new(1, 0)]
9750 );
9751
9752 editor.change_selections(None, window, cx, |s| {
9753 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
9754 });
9755 editor.backspace(&Default::default(), window, cx);
9756 assert_eq!(editor.text(cx), "X\nbb");
9757 assert_eq!(
9758 editor.selections.ranges(cx),
9759 [Point::new(0, 1)..Point::new(0, 1)]
9760 );
9761 });
9762}
9763
9764#[gpui::test]
9765fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
9766 init_test(cx, |_| {});
9767
9768 let markers = vec![('[', ']').into(), ('(', ')').into()];
9769 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
9770 indoc! {"
9771 [aaaa
9772 (bbbb]
9773 cccc)",
9774 },
9775 markers.clone(),
9776 );
9777 let excerpt_ranges = markers.into_iter().map(|marker| {
9778 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
9779 ExcerptRange {
9780 context,
9781 primary: None,
9782 }
9783 });
9784 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
9785 let multibuffer = cx.new(|cx| {
9786 let mut multibuffer = MultiBuffer::new(ReadWrite);
9787 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
9788 multibuffer
9789 });
9790
9791 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
9792 editor.update_in(cx, |editor, window, cx| {
9793 let (expected_text, selection_ranges) = marked_text_ranges(
9794 indoc! {"
9795 aaaa
9796 bˇbbb
9797 bˇbbˇb
9798 cccc"
9799 },
9800 true,
9801 );
9802 assert_eq!(editor.text(cx), expected_text);
9803 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
9804
9805 editor.handle_input("X", window, cx);
9806
9807 let (expected_text, expected_selections) = marked_text_ranges(
9808 indoc! {"
9809 aaaa
9810 bXˇbbXb
9811 bXˇbbXˇb
9812 cccc"
9813 },
9814 false,
9815 );
9816 assert_eq!(editor.text(cx), expected_text);
9817 assert_eq!(editor.selections.ranges(cx), expected_selections);
9818
9819 editor.newline(&Newline, window, cx);
9820 let (expected_text, expected_selections) = marked_text_ranges(
9821 indoc! {"
9822 aaaa
9823 bX
9824 ˇbbX
9825 b
9826 bX
9827 ˇbbX
9828 ˇb
9829 cccc"
9830 },
9831 false,
9832 );
9833 assert_eq!(editor.text(cx), expected_text);
9834 assert_eq!(editor.selections.ranges(cx), expected_selections);
9835 });
9836}
9837
9838#[gpui::test]
9839fn test_refresh_selections(cx: &mut TestAppContext) {
9840 init_test(cx, |_| {});
9841
9842 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9843 let mut excerpt1_id = None;
9844 let multibuffer = cx.new(|cx| {
9845 let mut multibuffer = MultiBuffer::new(ReadWrite);
9846 excerpt1_id = multibuffer
9847 .push_excerpts(
9848 buffer.clone(),
9849 [
9850 ExcerptRange {
9851 context: Point::new(0, 0)..Point::new(1, 4),
9852 primary: None,
9853 },
9854 ExcerptRange {
9855 context: Point::new(1, 0)..Point::new(2, 4),
9856 primary: None,
9857 },
9858 ],
9859 cx,
9860 )
9861 .into_iter()
9862 .next();
9863 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
9864 multibuffer
9865 });
9866
9867 let editor = cx.add_window(|window, cx| {
9868 let mut editor = build_editor(multibuffer.clone(), window, cx);
9869 let snapshot = editor.snapshot(window, cx);
9870 editor.change_selections(None, window, cx, |s| {
9871 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
9872 });
9873 editor.begin_selection(
9874 Point::new(2, 1).to_display_point(&snapshot),
9875 true,
9876 1,
9877 window,
9878 cx,
9879 );
9880 assert_eq!(
9881 editor.selections.ranges(cx),
9882 [
9883 Point::new(1, 3)..Point::new(1, 3),
9884 Point::new(2, 1)..Point::new(2, 1),
9885 ]
9886 );
9887 editor
9888 });
9889
9890 // Refreshing selections is a no-op when excerpts haven't changed.
9891 _ = editor.update(cx, |editor, window, cx| {
9892 editor.change_selections(None, window, cx, |s| s.refresh());
9893 assert_eq!(
9894 editor.selections.ranges(cx),
9895 [
9896 Point::new(1, 3)..Point::new(1, 3),
9897 Point::new(2, 1)..Point::new(2, 1),
9898 ]
9899 );
9900 });
9901
9902 multibuffer.update(cx, |multibuffer, cx| {
9903 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
9904 });
9905 _ = editor.update(cx, |editor, window, cx| {
9906 // Removing an excerpt causes the first selection to become degenerate.
9907 assert_eq!(
9908 editor.selections.ranges(cx),
9909 [
9910 Point::new(0, 0)..Point::new(0, 0),
9911 Point::new(0, 1)..Point::new(0, 1)
9912 ]
9913 );
9914
9915 // Refreshing selections will relocate the first selection to the original buffer
9916 // location.
9917 editor.change_selections(None, window, cx, |s| s.refresh());
9918 assert_eq!(
9919 editor.selections.ranges(cx),
9920 [
9921 Point::new(0, 1)..Point::new(0, 1),
9922 Point::new(0, 3)..Point::new(0, 3)
9923 ]
9924 );
9925 assert!(editor.selections.pending_anchor().is_some());
9926 });
9927}
9928
9929#[gpui::test]
9930fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
9931 init_test(cx, |_| {});
9932
9933 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
9934 let mut excerpt1_id = None;
9935 let multibuffer = cx.new(|cx| {
9936 let mut multibuffer = MultiBuffer::new(ReadWrite);
9937 excerpt1_id = multibuffer
9938 .push_excerpts(
9939 buffer.clone(),
9940 [
9941 ExcerptRange {
9942 context: Point::new(0, 0)..Point::new(1, 4),
9943 primary: None,
9944 },
9945 ExcerptRange {
9946 context: Point::new(1, 0)..Point::new(2, 4),
9947 primary: None,
9948 },
9949 ],
9950 cx,
9951 )
9952 .into_iter()
9953 .next();
9954 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
9955 multibuffer
9956 });
9957
9958 let editor = cx.add_window(|window, cx| {
9959 let mut editor = build_editor(multibuffer.clone(), window, cx);
9960 let snapshot = editor.snapshot(window, cx);
9961 editor.begin_selection(
9962 Point::new(1, 3).to_display_point(&snapshot),
9963 false,
9964 1,
9965 window,
9966 cx,
9967 );
9968 assert_eq!(
9969 editor.selections.ranges(cx),
9970 [Point::new(1, 3)..Point::new(1, 3)]
9971 );
9972 editor
9973 });
9974
9975 multibuffer.update(cx, |multibuffer, cx| {
9976 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
9977 });
9978 _ = editor.update(cx, |editor, window, cx| {
9979 assert_eq!(
9980 editor.selections.ranges(cx),
9981 [Point::new(0, 0)..Point::new(0, 0)]
9982 );
9983
9984 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
9985 editor.change_selections(None, window, cx, |s| s.refresh());
9986 assert_eq!(
9987 editor.selections.ranges(cx),
9988 [Point::new(0, 3)..Point::new(0, 3)]
9989 );
9990 assert!(editor.selections.pending_anchor().is_some());
9991 });
9992}
9993
9994#[gpui::test]
9995async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
9996 init_test(cx, |_| {});
9997
9998 let language = Arc::new(
9999 Language::new(
10000 LanguageConfig {
10001 brackets: BracketPairConfig {
10002 pairs: vec![
10003 BracketPair {
10004 start: "{".to_string(),
10005 end: "}".to_string(),
10006 close: true,
10007 surround: true,
10008 newline: true,
10009 },
10010 BracketPair {
10011 start: "/* ".to_string(),
10012 end: " */".to_string(),
10013 close: true,
10014 surround: true,
10015 newline: true,
10016 },
10017 ],
10018 ..Default::default()
10019 },
10020 ..Default::default()
10021 },
10022 Some(tree_sitter_rust::LANGUAGE.into()),
10023 )
10024 .with_indents_query("")
10025 .unwrap(),
10026 );
10027
10028 let text = concat!(
10029 "{ }\n", //
10030 " x\n", //
10031 " /* */\n", //
10032 "x\n", //
10033 "{{} }\n", //
10034 );
10035
10036 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10037 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10038 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10039 editor
10040 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10041 .await;
10042
10043 editor.update_in(cx, |editor, window, cx| {
10044 editor.change_selections(None, window, cx, |s| {
10045 s.select_display_ranges([
10046 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
10047 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
10048 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
10049 ])
10050 });
10051 editor.newline(&Newline, window, cx);
10052
10053 assert_eq!(
10054 editor.buffer().read(cx).read(cx).text(),
10055 concat!(
10056 "{ \n", // Suppress rustfmt
10057 "\n", //
10058 "}\n", //
10059 " x\n", //
10060 " /* \n", //
10061 " \n", //
10062 " */\n", //
10063 "x\n", //
10064 "{{} \n", //
10065 "}\n", //
10066 )
10067 );
10068 });
10069}
10070
10071#[gpui::test]
10072fn test_highlighted_ranges(cx: &mut TestAppContext) {
10073 init_test(cx, |_| {});
10074
10075 let editor = cx.add_window(|window, cx| {
10076 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
10077 build_editor(buffer.clone(), window, cx)
10078 });
10079
10080 _ = editor.update(cx, |editor, window, cx| {
10081 struct Type1;
10082 struct Type2;
10083
10084 let buffer = editor.buffer.read(cx).snapshot(cx);
10085
10086 let anchor_range =
10087 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
10088
10089 editor.highlight_background::<Type1>(
10090 &[
10091 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
10092 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
10093 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
10094 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
10095 ],
10096 |_| Hsla::red(),
10097 cx,
10098 );
10099 editor.highlight_background::<Type2>(
10100 &[
10101 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
10102 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
10103 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
10104 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
10105 ],
10106 |_| Hsla::green(),
10107 cx,
10108 );
10109
10110 let snapshot = editor.snapshot(window, cx);
10111 let mut highlighted_ranges = editor.background_highlights_in_range(
10112 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
10113 &snapshot,
10114 cx.theme().colors(),
10115 );
10116 // Enforce a consistent ordering based on color without relying on the ordering of the
10117 // highlight's `TypeId` which is non-executor.
10118 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
10119 assert_eq!(
10120 highlighted_ranges,
10121 &[
10122 (
10123 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
10124 Hsla::red(),
10125 ),
10126 (
10127 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
10128 Hsla::red(),
10129 ),
10130 (
10131 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
10132 Hsla::green(),
10133 ),
10134 (
10135 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
10136 Hsla::green(),
10137 ),
10138 ]
10139 );
10140 assert_eq!(
10141 editor.background_highlights_in_range(
10142 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
10143 &snapshot,
10144 cx.theme().colors(),
10145 ),
10146 &[(
10147 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
10148 Hsla::red(),
10149 )]
10150 );
10151 });
10152}
10153
10154#[gpui::test]
10155async fn test_following(cx: &mut gpui::TestAppContext) {
10156 init_test(cx, |_| {});
10157
10158 let fs = FakeFs::new(cx.executor());
10159 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
10160
10161 let buffer = project.update(cx, |project, cx| {
10162 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
10163 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
10164 });
10165 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
10166 let follower = cx.update(|cx| {
10167 cx.open_window(
10168 WindowOptions {
10169 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
10170 gpui::Point::new(px(0.), px(0.)),
10171 gpui::Point::new(px(10.), px(80.)),
10172 ))),
10173 ..Default::default()
10174 },
10175 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
10176 )
10177 .unwrap()
10178 });
10179
10180 let is_still_following = Rc::new(RefCell::new(true));
10181 let follower_edit_event_count = Rc::new(RefCell::new(0));
10182 let pending_update = Rc::new(RefCell::new(None));
10183 let leader_model = leader.root(cx).unwrap();
10184 let follower_model = follower.root(cx).unwrap();
10185 _ = follower.update(cx, {
10186 let update = pending_update.clone();
10187 let is_still_following = is_still_following.clone();
10188 let follower_edit_event_count = follower_edit_event_count.clone();
10189 |_, window, cx| {
10190 cx.subscribe_in(
10191 &leader_model,
10192 window,
10193 move |_, leader, event, window, cx| {
10194 leader.read(cx).add_event_to_update_proto(
10195 event,
10196 &mut update.borrow_mut(),
10197 window,
10198 cx,
10199 );
10200 },
10201 )
10202 .detach();
10203
10204 cx.subscribe_in(
10205 &follower_model,
10206 window,
10207 move |_, _, event: &EditorEvent, _window, _cx| {
10208 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
10209 *is_still_following.borrow_mut() = false;
10210 }
10211
10212 if let EditorEvent::BufferEdited = event {
10213 *follower_edit_event_count.borrow_mut() += 1;
10214 }
10215 },
10216 )
10217 .detach();
10218 }
10219 });
10220
10221 // Update the selections only
10222 _ = leader.update(cx, |leader, window, cx| {
10223 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
10224 });
10225 follower
10226 .update(cx, |follower, window, cx| {
10227 follower.apply_update_proto(
10228 &project,
10229 pending_update.borrow_mut().take().unwrap(),
10230 window,
10231 cx,
10232 )
10233 })
10234 .unwrap()
10235 .await
10236 .unwrap();
10237 _ = follower.update(cx, |follower, _, cx| {
10238 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
10239 });
10240 assert!(*is_still_following.borrow());
10241 assert_eq!(*follower_edit_event_count.borrow(), 0);
10242
10243 // Update the scroll position only
10244 _ = leader.update(cx, |leader, window, cx| {
10245 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
10246 });
10247 follower
10248 .update(cx, |follower, window, cx| {
10249 follower.apply_update_proto(
10250 &project,
10251 pending_update.borrow_mut().take().unwrap(),
10252 window,
10253 cx,
10254 )
10255 })
10256 .unwrap()
10257 .await
10258 .unwrap();
10259 assert_eq!(
10260 follower
10261 .update(cx, |follower, _, cx| follower.scroll_position(cx))
10262 .unwrap(),
10263 gpui::Point::new(1.5, 3.5)
10264 );
10265 assert!(*is_still_following.borrow());
10266 assert_eq!(*follower_edit_event_count.borrow(), 0);
10267
10268 // Update the selections and scroll position. The follower's scroll position is updated
10269 // via autoscroll, not via the leader's exact scroll position.
10270 _ = leader.update(cx, |leader, window, cx| {
10271 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10272 leader.request_autoscroll(Autoscroll::newest(), cx);
10273 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
10274 });
10275 follower
10276 .update(cx, |follower, window, cx| {
10277 follower.apply_update_proto(
10278 &project,
10279 pending_update.borrow_mut().take().unwrap(),
10280 window,
10281 cx,
10282 )
10283 })
10284 .unwrap()
10285 .await
10286 .unwrap();
10287 _ = follower.update(cx, |follower, _, cx| {
10288 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
10289 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
10290 });
10291 assert!(*is_still_following.borrow());
10292
10293 // Creating a pending selection that precedes another selection
10294 _ = leader.update(cx, |leader, window, cx| {
10295 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
10296 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
10297 });
10298 follower
10299 .update(cx, |follower, window, cx| {
10300 follower.apply_update_proto(
10301 &project,
10302 pending_update.borrow_mut().take().unwrap(),
10303 window,
10304 cx,
10305 )
10306 })
10307 .unwrap()
10308 .await
10309 .unwrap();
10310 _ = follower.update(cx, |follower, _, cx| {
10311 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
10312 });
10313 assert!(*is_still_following.borrow());
10314
10315 // Extend the pending selection so that it surrounds another selection
10316 _ = leader.update(cx, |leader, window, cx| {
10317 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
10318 });
10319 follower
10320 .update(cx, |follower, window, cx| {
10321 follower.apply_update_proto(
10322 &project,
10323 pending_update.borrow_mut().take().unwrap(),
10324 window,
10325 cx,
10326 )
10327 })
10328 .unwrap()
10329 .await
10330 .unwrap();
10331 _ = follower.update(cx, |follower, _, cx| {
10332 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
10333 });
10334
10335 // Scrolling locally breaks the follow
10336 _ = follower.update(cx, |follower, window, cx| {
10337 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
10338 follower.set_scroll_anchor(
10339 ScrollAnchor {
10340 anchor: top_anchor,
10341 offset: gpui::Point::new(0.0, 0.5),
10342 },
10343 window,
10344 cx,
10345 );
10346 });
10347 assert!(!(*is_still_following.borrow()));
10348}
10349
10350#[gpui::test]
10351async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
10352 init_test(cx, |_| {});
10353
10354 let fs = FakeFs::new(cx.executor());
10355 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
10356 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10357 let pane = workspace
10358 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10359 .unwrap();
10360
10361 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10362
10363 let leader = pane.update_in(cx, |_, window, cx| {
10364 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
10365 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
10366 });
10367
10368 // Start following the editor when it has no excerpts.
10369 let mut state_message =
10370 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
10371 let workspace_model = workspace.root(cx).unwrap();
10372 let follower_1 = cx
10373 .update_window(*workspace.deref(), |_, window, cx| {
10374 Editor::from_state_proto(
10375 workspace_model,
10376 ViewId {
10377 creator: Default::default(),
10378 id: 0,
10379 },
10380 &mut state_message,
10381 window,
10382 cx,
10383 )
10384 })
10385 .unwrap()
10386 .unwrap()
10387 .await
10388 .unwrap();
10389
10390 let update_message = Rc::new(RefCell::new(None));
10391 follower_1.update_in(cx, {
10392 let update = update_message.clone();
10393 |_, window, cx| {
10394 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
10395 leader.read(cx).add_event_to_update_proto(
10396 event,
10397 &mut update.borrow_mut(),
10398 window,
10399 cx,
10400 );
10401 })
10402 .detach();
10403 }
10404 });
10405
10406 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
10407 (
10408 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
10409 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
10410 )
10411 });
10412
10413 // Insert some excerpts.
10414 leader.update(cx, |leader, cx| {
10415 leader.buffer.update(cx, |multibuffer, cx| {
10416 let excerpt_ids = multibuffer.push_excerpts(
10417 buffer_1.clone(),
10418 [
10419 ExcerptRange {
10420 context: 1..6,
10421 primary: None,
10422 },
10423 ExcerptRange {
10424 context: 12..15,
10425 primary: None,
10426 },
10427 ExcerptRange {
10428 context: 0..3,
10429 primary: None,
10430 },
10431 ],
10432 cx,
10433 );
10434 multibuffer.insert_excerpts_after(
10435 excerpt_ids[0],
10436 buffer_2.clone(),
10437 [
10438 ExcerptRange {
10439 context: 8..12,
10440 primary: None,
10441 },
10442 ExcerptRange {
10443 context: 0..6,
10444 primary: None,
10445 },
10446 ],
10447 cx,
10448 );
10449 });
10450 });
10451
10452 // Apply the update of adding the excerpts.
10453 follower_1
10454 .update_in(cx, |follower, window, cx| {
10455 follower.apply_update_proto(
10456 &project,
10457 update_message.borrow().clone().unwrap(),
10458 window,
10459 cx,
10460 )
10461 })
10462 .await
10463 .unwrap();
10464 assert_eq!(
10465 follower_1.update(cx, |editor, cx| editor.text(cx)),
10466 leader.update(cx, |editor, cx| editor.text(cx))
10467 );
10468 update_message.borrow_mut().take();
10469
10470 // Start following separately after it already has excerpts.
10471 let mut state_message =
10472 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
10473 let workspace_model = workspace.root(cx).unwrap();
10474 let follower_2 = cx
10475 .update_window(*workspace.deref(), |_, window, cx| {
10476 Editor::from_state_proto(
10477 workspace_model,
10478 ViewId {
10479 creator: Default::default(),
10480 id: 0,
10481 },
10482 &mut state_message,
10483 window,
10484 cx,
10485 )
10486 })
10487 .unwrap()
10488 .unwrap()
10489 .await
10490 .unwrap();
10491 assert_eq!(
10492 follower_2.update(cx, |editor, cx| editor.text(cx)),
10493 leader.update(cx, |editor, cx| editor.text(cx))
10494 );
10495
10496 // Remove some excerpts.
10497 leader.update(cx, |leader, cx| {
10498 leader.buffer.update(cx, |multibuffer, cx| {
10499 let excerpt_ids = multibuffer.excerpt_ids();
10500 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
10501 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
10502 });
10503 });
10504
10505 // Apply the update of removing the excerpts.
10506 follower_1
10507 .update_in(cx, |follower, window, cx| {
10508 follower.apply_update_proto(
10509 &project,
10510 update_message.borrow().clone().unwrap(),
10511 window,
10512 cx,
10513 )
10514 })
10515 .await
10516 .unwrap();
10517 follower_2
10518 .update_in(cx, |follower, window, cx| {
10519 follower.apply_update_proto(
10520 &project,
10521 update_message.borrow().clone().unwrap(),
10522 window,
10523 cx,
10524 )
10525 })
10526 .await
10527 .unwrap();
10528 update_message.borrow_mut().take();
10529 assert_eq!(
10530 follower_1.update(cx, |editor, cx| editor.text(cx)),
10531 leader.update(cx, |editor, cx| editor.text(cx))
10532 );
10533}
10534
10535#[gpui::test]
10536async fn go_to_prev_overlapping_diagnostic(
10537 executor: BackgroundExecutor,
10538 cx: &mut gpui::TestAppContext,
10539) {
10540 init_test(cx, |_| {});
10541
10542 let mut cx = EditorTestContext::new(cx).await;
10543 let lsp_store =
10544 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10545
10546 cx.set_state(indoc! {"
10547 ˇfn func(abc def: i32) -> u32 {
10548 }
10549 "});
10550
10551 cx.update(|_, cx| {
10552 lsp_store.update(cx, |lsp_store, cx| {
10553 lsp_store
10554 .update_diagnostics(
10555 LanguageServerId(0),
10556 lsp::PublishDiagnosticsParams {
10557 uri: lsp::Url::from_file_path("/root/file").unwrap(),
10558 version: None,
10559 diagnostics: vec![
10560 lsp::Diagnostic {
10561 range: lsp::Range::new(
10562 lsp::Position::new(0, 11),
10563 lsp::Position::new(0, 12),
10564 ),
10565 severity: Some(lsp::DiagnosticSeverity::ERROR),
10566 ..Default::default()
10567 },
10568 lsp::Diagnostic {
10569 range: lsp::Range::new(
10570 lsp::Position::new(0, 12),
10571 lsp::Position::new(0, 15),
10572 ),
10573 severity: Some(lsp::DiagnosticSeverity::ERROR),
10574 ..Default::default()
10575 },
10576 lsp::Diagnostic {
10577 range: lsp::Range::new(
10578 lsp::Position::new(0, 25),
10579 lsp::Position::new(0, 28),
10580 ),
10581 severity: Some(lsp::DiagnosticSeverity::ERROR),
10582 ..Default::default()
10583 },
10584 ],
10585 },
10586 &[],
10587 cx,
10588 )
10589 .unwrap()
10590 });
10591 });
10592
10593 executor.run_until_parked();
10594
10595 cx.update_editor(|editor, window, cx| {
10596 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10597 });
10598
10599 cx.assert_editor_state(indoc! {"
10600 fn func(abc def: i32) -> ˇu32 {
10601 }
10602 "});
10603
10604 cx.update_editor(|editor, window, cx| {
10605 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10606 });
10607
10608 cx.assert_editor_state(indoc! {"
10609 fn func(abc ˇdef: i32) -> u32 {
10610 }
10611 "});
10612
10613 cx.update_editor(|editor, window, cx| {
10614 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10615 });
10616
10617 cx.assert_editor_state(indoc! {"
10618 fn func(abcˇ def: i32) -> u32 {
10619 }
10620 "});
10621
10622 cx.update_editor(|editor, window, cx| {
10623 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
10624 });
10625
10626 cx.assert_editor_state(indoc! {"
10627 fn func(abc def: i32) -> ˇu32 {
10628 }
10629 "});
10630}
10631
10632#[gpui::test]
10633async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
10634 init_test(cx, |_| {});
10635
10636 let mut cx = EditorTestContext::new(cx).await;
10637
10638 cx.set_state(indoc! {"
10639 fn func(abˇc def: i32) -> u32 {
10640 }
10641 "});
10642 let lsp_store =
10643 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
10644
10645 cx.update(|_, cx| {
10646 lsp_store.update(cx, |lsp_store, cx| {
10647 lsp_store.update_diagnostics(
10648 LanguageServerId(0),
10649 lsp::PublishDiagnosticsParams {
10650 uri: lsp::Url::from_file_path("/root/file").unwrap(),
10651 version: None,
10652 diagnostics: vec![lsp::Diagnostic {
10653 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
10654 severity: Some(lsp::DiagnosticSeverity::ERROR),
10655 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
10656 ..Default::default()
10657 }],
10658 },
10659 &[],
10660 cx,
10661 )
10662 })
10663 }).unwrap();
10664 cx.run_until_parked();
10665 cx.update_editor(|editor, window, cx| {
10666 hover_popover::hover(editor, &Default::default(), window, cx)
10667 });
10668 cx.run_until_parked();
10669 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
10670}
10671
10672#[gpui::test]
10673async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
10674 init_test(cx, |_| {});
10675
10676 let mut cx = EditorTestContext::new(cx).await;
10677
10678 let diff_base = r#"
10679 use some::mod;
10680
10681 const A: u32 = 42;
10682
10683 fn main() {
10684 println!("hello");
10685
10686 println!("world");
10687 }
10688 "#
10689 .unindent();
10690
10691 // Edits are modified, removed, modified, added
10692 cx.set_state(
10693 &r#"
10694 use some::modified;
10695
10696 ˇ
10697 fn main() {
10698 println!("hello there");
10699
10700 println!("around the");
10701 println!("world");
10702 }
10703 "#
10704 .unindent(),
10705 );
10706
10707 cx.set_diff_base(&diff_base);
10708 executor.run_until_parked();
10709
10710 cx.update_editor(|editor, window, cx| {
10711 //Wrap around the bottom of the buffer
10712 for _ in 0..3 {
10713 editor.go_to_next_hunk(&GoToHunk, window, cx);
10714 }
10715 });
10716
10717 cx.assert_editor_state(
10718 &r#"
10719 ˇuse some::modified;
10720
10721
10722 fn main() {
10723 println!("hello there");
10724
10725 println!("around the");
10726 println!("world");
10727 }
10728 "#
10729 .unindent(),
10730 );
10731
10732 cx.update_editor(|editor, window, cx| {
10733 //Wrap around the top of the buffer
10734 for _ in 0..2 {
10735 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10736 }
10737 });
10738
10739 cx.assert_editor_state(
10740 &r#"
10741 use some::modified;
10742
10743
10744 fn main() {
10745 ˇ println!("hello there");
10746
10747 println!("around the");
10748 println!("world");
10749 }
10750 "#
10751 .unindent(),
10752 );
10753
10754 cx.update_editor(|editor, window, cx| {
10755 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10756 });
10757
10758 cx.assert_editor_state(
10759 &r#"
10760 use some::modified;
10761
10762 ˇ
10763 fn main() {
10764 println!("hello there");
10765
10766 println!("around the");
10767 println!("world");
10768 }
10769 "#
10770 .unindent(),
10771 );
10772
10773 cx.update_editor(|editor, window, cx| {
10774 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10775 });
10776
10777 cx.assert_editor_state(
10778 &r#"
10779 ˇuse some::modified;
10780
10781
10782 fn main() {
10783 println!("hello there");
10784
10785 println!("around the");
10786 println!("world");
10787 }
10788 "#
10789 .unindent(),
10790 );
10791
10792 cx.update_editor(|editor, window, cx| {
10793 for _ in 0..2 {
10794 editor.go_to_prev_hunk(&GoToPrevHunk, window, cx);
10795 }
10796 });
10797
10798 cx.assert_editor_state(
10799 &r#"
10800 use some::modified;
10801
10802
10803 fn main() {
10804 ˇ println!("hello there");
10805
10806 println!("around the");
10807 println!("world");
10808 }
10809 "#
10810 .unindent(),
10811 );
10812
10813 cx.update_editor(|editor, window, cx| {
10814 editor.fold(&Fold, window, cx);
10815 });
10816
10817 cx.update_editor(|editor, window, cx| {
10818 editor.go_to_next_hunk(&GoToHunk, window, cx);
10819 });
10820
10821 cx.assert_editor_state(
10822 &r#"
10823 ˇuse some::modified;
10824
10825
10826 fn main() {
10827 println!("hello there");
10828
10829 println!("around the");
10830 println!("world");
10831 }
10832 "#
10833 .unindent(),
10834 );
10835}
10836
10837#[test]
10838fn test_split_words() {
10839 fn split(text: &str) -> Vec<&str> {
10840 split_words(text).collect()
10841 }
10842
10843 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
10844 assert_eq!(split("hello_world"), &["hello_", "world"]);
10845 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
10846 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
10847 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
10848 assert_eq!(split("helloworld"), &["helloworld"]);
10849
10850 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
10851}
10852
10853#[gpui::test]
10854async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
10855 init_test(cx, |_| {});
10856
10857 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
10858 let mut assert = |before, after| {
10859 let _state_context = cx.set_state(before);
10860 cx.run_until_parked();
10861 cx.update_editor(|editor, window, cx| {
10862 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
10863 });
10864 cx.assert_editor_state(after);
10865 };
10866
10867 // Outside bracket jumps to outside of matching bracket
10868 assert("console.logˇ(var);", "console.log(var)ˇ;");
10869 assert("console.log(var)ˇ;", "console.logˇ(var);");
10870
10871 // Inside bracket jumps to inside of matching bracket
10872 assert("console.log(ˇvar);", "console.log(varˇ);");
10873 assert("console.log(varˇ);", "console.log(ˇvar);");
10874
10875 // When outside a bracket and inside, favor jumping to the inside bracket
10876 assert(
10877 "console.log('foo', [1, 2, 3]ˇ);",
10878 "console.log(ˇ'foo', [1, 2, 3]);",
10879 );
10880 assert(
10881 "console.log(ˇ'foo', [1, 2, 3]);",
10882 "console.log('foo', [1, 2, 3]ˇ);",
10883 );
10884
10885 // Bias forward if two options are equally likely
10886 assert(
10887 "let result = curried_fun()ˇ();",
10888 "let result = curried_fun()()ˇ;",
10889 );
10890
10891 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
10892 assert(
10893 indoc! {"
10894 function test() {
10895 console.log('test')ˇ
10896 }"},
10897 indoc! {"
10898 function test() {
10899 console.logˇ('test')
10900 }"},
10901 );
10902}
10903
10904#[gpui::test]
10905async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
10906 init_test(cx, |_| {});
10907
10908 let fs = FakeFs::new(cx.executor());
10909 fs.insert_tree(
10910 "/a",
10911 json!({
10912 "main.rs": "fn main() { let a = 5; }",
10913 "other.rs": "// Test file",
10914 }),
10915 )
10916 .await;
10917 let project = Project::test(fs, ["/a".as_ref()], cx).await;
10918
10919 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10920 language_registry.add(Arc::new(Language::new(
10921 LanguageConfig {
10922 name: "Rust".into(),
10923 matcher: LanguageMatcher {
10924 path_suffixes: vec!["rs".to_string()],
10925 ..Default::default()
10926 },
10927 brackets: BracketPairConfig {
10928 pairs: vec![BracketPair {
10929 start: "{".to_string(),
10930 end: "}".to_string(),
10931 close: true,
10932 surround: true,
10933 newline: true,
10934 }],
10935 disabled_scopes_by_bracket_ix: Vec::new(),
10936 },
10937 ..Default::default()
10938 },
10939 Some(tree_sitter_rust::LANGUAGE.into()),
10940 )));
10941 let mut fake_servers = language_registry.register_fake_lsp(
10942 "Rust",
10943 FakeLspAdapter {
10944 capabilities: lsp::ServerCapabilities {
10945 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
10946 first_trigger_character: "{".to_string(),
10947 more_trigger_character: None,
10948 }),
10949 ..Default::default()
10950 },
10951 ..Default::default()
10952 },
10953 );
10954
10955 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10956
10957 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10958
10959 let worktree_id = workspace
10960 .update(cx, |workspace, _, cx| {
10961 workspace.project().update(cx, |project, cx| {
10962 project.worktrees(cx).next().unwrap().read(cx).id()
10963 })
10964 })
10965 .unwrap();
10966
10967 let buffer = project
10968 .update(cx, |project, cx| {
10969 project.open_local_buffer("/a/main.rs", cx)
10970 })
10971 .await
10972 .unwrap();
10973 let editor_handle = workspace
10974 .update(cx, |workspace, window, cx| {
10975 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
10976 })
10977 .unwrap()
10978 .await
10979 .unwrap()
10980 .downcast::<Editor>()
10981 .unwrap();
10982
10983 cx.executor().start_waiting();
10984 let fake_server = fake_servers.next().await.unwrap();
10985
10986 fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
10987 assert_eq!(
10988 params.text_document_position.text_document.uri,
10989 lsp::Url::from_file_path("/a/main.rs").unwrap(),
10990 );
10991 assert_eq!(
10992 params.text_document_position.position,
10993 lsp::Position::new(0, 21),
10994 );
10995
10996 Ok(Some(vec![lsp::TextEdit {
10997 new_text: "]".to_string(),
10998 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
10999 }]))
11000 });
11001
11002 editor_handle.update_in(cx, |editor, window, cx| {
11003 window.focus(&editor.focus_handle(cx));
11004 editor.change_selections(None, window, cx, |s| {
11005 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
11006 });
11007 editor.handle_input("{", window, cx);
11008 });
11009
11010 cx.executor().run_until_parked();
11011
11012 buffer.update(cx, |buffer, _| {
11013 assert_eq!(
11014 buffer.text(),
11015 "fn main() { let a = {5}; }",
11016 "No extra braces from on type formatting should appear in the buffer"
11017 )
11018 });
11019}
11020
11021#[gpui::test]
11022async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) {
11023 init_test(cx, |_| {});
11024
11025 let fs = FakeFs::new(cx.executor());
11026 fs.insert_tree(
11027 "/a",
11028 json!({
11029 "main.rs": "fn main() { let a = 5; }",
11030 "other.rs": "// Test file",
11031 }),
11032 )
11033 .await;
11034
11035 let project = Project::test(fs, ["/a".as_ref()], cx).await;
11036
11037 let server_restarts = Arc::new(AtomicUsize::new(0));
11038 let closure_restarts = Arc::clone(&server_restarts);
11039 let language_server_name = "test language server";
11040 let language_name: LanguageName = "Rust".into();
11041
11042 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11043 language_registry.add(Arc::new(Language::new(
11044 LanguageConfig {
11045 name: language_name.clone(),
11046 matcher: LanguageMatcher {
11047 path_suffixes: vec!["rs".to_string()],
11048 ..Default::default()
11049 },
11050 ..Default::default()
11051 },
11052 Some(tree_sitter_rust::LANGUAGE.into()),
11053 )));
11054 let mut fake_servers = language_registry.register_fake_lsp(
11055 "Rust",
11056 FakeLspAdapter {
11057 name: language_server_name,
11058 initialization_options: Some(json!({
11059 "testOptionValue": true
11060 })),
11061 initializer: Some(Box::new(move |fake_server| {
11062 let task_restarts = Arc::clone(&closure_restarts);
11063 fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
11064 task_restarts.fetch_add(1, atomic::Ordering::Release);
11065 futures::future::ready(Ok(()))
11066 });
11067 })),
11068 ..Default::default()
11069 },
11070 );
11071
11072 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11073 let _buffer = project
11074 .update(cx, |project, cx| {
11075 project.open_local_buffer_with_lsp("/a/main.rs", cx)
11076 })
11077 .await
11078 .unwrap();
11079 let _fake_server = fake_servers.next().await.unwrap();
11080 update_test_language_settings(cx, |language_settings| {
11081 language_settings.languages.insert(
11082 language_name.clone(),
11083 LanguageSettingsContent {
11084 tab_size: NonZeroU32::new(8),
11085 ..Default::default()
11086 },
11087 );
11088 });
11089 cx.executor().run_until_parked();
11090 assert_eq!(
11091 server_restarts.load(atomic::Ordering::Acquire),
11092 0,
11093 "Should not restart LSP server on an unrelated change"
11094 );
11095
11096 update_test_project_settings(cx, |project_settings| {
11097 project_settings.lsp.insert(
11098 "Some other server name".into(),
11099 LspSettings {
11100 binary: None,
11101 settings: None,
11102 initialization_options: Some(json!({
11103 "some other init value": false
11104 })),
11105 },
11106 );
11107 });
11108 cx.executor().run_until_parked();
11109 assert_eq!(
11110 server_restarts.load(atomic::Ordering::Acquire),
11111 0,
11112 "Should not restart LSP server on an unrelated LSP settings change"
11113 );
11114
11115 update_test_project_settings(cx, |project_settings| {
11116 project_settings.lsp.insert(
11117 language_server_name.into(),
11118 LspSettings {
11119 binary: None,
11120 settings: None,
11121 initialization_options: Some(json!({
11122 "anotherInitValue": false
11123 })),
11124 },
11125 );
11126 });
11127 cx.executor().run_until_parked();
11128 assert_eq!(
11129 server_restarts.load(atomic::Ordering::Acquire),
11130 1,
11131 "Should restart LSP server on a related LSP settings change"
11132 );
11133
11134 update_test_project_settings(cx, |project_settings| {
11135 project_settings.lsp.insert(
11136 language_server_name.into(),
11137 LspSettings {
11138 binary: None,
11139 settings: None,
11140 initialization_options: Some(json!({
11141 "anotherInitValue": false
11142 })),
11143 },
11144 );
11145 });
11146 cx.executor().run_until_parked();
11147 assert_eq!(
11148 server_restarts.load(atomic::Ordering::Acquire),
11149 1,
11150 "Should not restart LSP server on a related LSP settings change that is the same"
11151 );
11152
11153 update_test_project_settings(cx, |project_settings| {
11154 project_settings.lsp.insert(
11155 language_server_name.into(),
11156 LspSettings {
11157 binary: None,
11158 settings: None,
11159 initialization_options: None,
11160 },
11161 );
11162 });
11163 cx.executor().run_until_parked();
11164 assert_eq!(
11165 server_restarts.load(atomic::Ordering::Acquire),
11166 2,
11167 "Should restart LSP server on another related LSP settings change"
11168 );
11169}
11170
11171#[gpui::test]
11172async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
11173 init_test(cx, |_| {});
11174
11175 let mut cx = EditorLspTestContext::new_rust(
11176 lsp::ServerCapabilities {
11177 completion_provider: Some(lsp::CompletionOptions {
11178 trigger_characters: Some(vec![".".to_string()]),
11179 resolve_provider: Some(true),
11180 ..Default::default()
11181 }),
11182 ..Default::default()
11183 },
11184 cx,
11185 )
11186 .await;
11187
11188 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11189 cx.simulate_keystroke(".");
11190 let completion_item = lsp::CompletionItem {
11191 label: "some".into(),
11192 kind: Some(lsp::CompletionItemKind::SNIPPET),
11193 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11194 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11195 kind: lsp::MarkupKind::Markdown,
11196 value: "```rust\nSome(2)\n```".to_string(),
11197 })),
11198 deprecated: Some(false),
11199 sort_text: Some("fffffff2".to_string()),
11200 filter_text: Some("some".to_string()),
11201 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11202 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11203 range: lsp::Range {
11204 start: lsp::Position {
11205 line: 0,
11206 character: 22,
11207 },
11208 end: lsp::Position {
11209 line: 0,
11210 character: 22,
11211 },
11212 },
11213 new_text: "Some(2)".to_string(),
11214 })),
11215 additional_text_edits: Some(vec![lsp::TextEdit {
11216 range: lsp::Range {
11217 start: lsp::Position {
11218 line: 0,
11219 character: 20,
11220 },
11221 end: lsp::Position {
11222 line: 0,
11223 character: 22,
11224 },
11225 },
11226 new_text: "".to_string(),
11227 }]),
11228 ..Default::default()
11229 };
11230
11231 let closure_completion_item = completion_item.clone();
11232 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11233 let task_completion_item = closure_completion_item.clone();
11234 async move {
11235 Ok(Some(lsp::CompletionResponse::Array(vec![
11236 task_completion_item,
11237 ])))
11238 }
11239 });
11240
11241 request.next().await;
11242
11243 cx.condition(|editor, _| editor.context_menu_visible())
11244 .await;
11245 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11246 editor
11247 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11248 .unwrap()
11249 });
11250 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
11251
11252 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
11253 let task_completion_item = completion_item.clone();
11254 async move { Ok(task_completion_item) }
11255 })
11256 .next()
11257 .await
11258 .unwrap();
11259 apply_additional_edits.await.unwrap();
11260 cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
11261}
11262
11263#[gpui::test]
11264async fn test_completions_resolve_updates_labels_if_filter_text_matches(
11265 cx: &mut gpui::TestAppContext,
11266) {
11267 init_test(cx, |_| {});
11268
11269 let mut cx = EditorLspTestContext::new_rust(
11270 lsp::ServerCapabilities {
11271 completion_provider: Some(lsp::CompletionOptions {
11272 trigger_characters: Some(vec![".".to_string()]),
11273 resolve_provider: Some(true),
11274 ..Default::default()
11275 }),
11276 ..Default::default()
11277 },
11278 cx,
11279 )
11280 .await;
11281
11282 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11283 cx.simulate_keystroke(".");
11284
11285 let item1 = lsp::CompletionItem {
11286 label: "id".to_string(),
11287 filter_text: Some("id".to_string()),
11288 detail: None,
11289 documentation: None,
11290 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11291 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11292 new_text: ".id".to_string(),
11293 })),
11294 ..lsp::CompletionItem::default()
11295 };
11296
11297 let item2 = lsp::CompletionItem {
11298 label: "other".to_string(),
11299 filter_text: Some("other".to_string()),
11300 detail: None,
11301 documentation: None,
11302 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11303 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11304 new_text: ".other".to_string(),
11305 })),
11306 ..lsp::CompletionItem::default()
11307 };
11308
11309 let item1 = item1.clone();
11310 cx.handle_request::<lsp::request::Completion, _, _>({
11311 let item1 = item1.clone();
11312 move |_, _, _| {
11313 let item1 = item1.clone();
11314 let item2 = item2.clone();
11315 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
11316 }
11317 })
11318 .next()
11319 .await;
11320
11321 cx.condition(|editor, _| editor.context_menu_visible())
11322 .await;
11323 cx.update_editor(|editor, _, _| {
11324 let context_menu = editor.context_menu.borrow_mut();
11325 let context_menu = context_menu
11326 .as_ref()
11327 .expect("Should have the context menu deployed");
11328 match context_menu {
11329 CodeContextMenu::Completions(completions_menu) => {
11330 let completions = completions_menu.completions.borrow_mut();
11331 assert_eq!(
11332 completions
11333 .iter()
11334 .map(|completion| &completion.label.text)
11335 .collect::<Vec<_>>(),
11336 vec!["id", "other"]
11337 )
11338 }
11339 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11340 }
11341 });
11342
11343 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>({
11344 let item1 = item1.clone();
11345 move |_, item_to_resolve, _| {
11346 let item1 = item1.clone();
11347 async move {
11348 if item1 == item_to_resolve {
11349 Ok(lsp::CompletionItem {
11350 label: "method id()".to_string(),
11351 filter_text: Some("id".to_string()),
11352 detail: Some("Now resolved!".to_string()),
11353 documentation: Some(lsp::Documentation::String("Docs".to_string())),
11354 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11355 range: lsp::Range::new(
11356 lsp::Position::new(0, 22),
11357 lsp::Position::new(0, 22),
11358 ),
11359 new_text: ".id".to_string(),
11360 })),
11361 ..lsp::CompletionItem::default()
11362 })
11363 } else {
11364 Ok(item_to_resolve)
11365 }
11366 }
11367 }
11368 })
11369 .next()
11370 .await
11371 .unwrap();
11372 cx.run_until_parked();
11373
11374 cx.update_editor(|editor, window, cx| {
11375 editor.context_menu_next(&Default::default(), window, cx);
11376 });
11377
11378 cx.update_editor(|editor, _, _| {
11379 let context_menu = editor.context_menu.borrow_mut();
11380 let context_menu = context_menu
11381 .as_ref()
11382 .expect("Should have the context menu deployed");
11383 match context_menu {
11384 CodeContextMenu::Completions(completions_menu) => {
11385 let completions = completions_menu.completions.borrow_mut();
11386 assert_eq!(
11387 completions
11388 .iter()
11389 .map(|completion| &completion.label.text)
11390 .collect::<Vec<_>>(),
11391 vec!["method id()", "other"],
11392 "Should update first completion label, but not second as the filter text did not match."
11393 );
11394 }
11395 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11396 }
11397 });
11398}
11399
11400#[gpui::test]
11401async fn test_completions_resolve_happens_once(cx: &mut gpui::TestAppContext) {
11402 init_test(cx, |_| {});
11403
11404 let mut cx = EditorLspTestContext::new_rust(
11405 lsp::ServerCapabilities {
11406 completion_provider: Some(lsp::CompletionOptions {
11407 trigger_characters: Some(vec![".".to_string()]),
11408 resolve_provider: Some(true),
11409 ..Default::default()
11410 }),
11411 ..Default::default()
11412 },
11413 cx,
11414 )
11415 .await;
11416
11417 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11418 cx.simulate_keystroke(".");
11419
11420 let unresolved_item_1 = lsp::CompletionItem {
11421 label: "id".to_string(),
11422 filter_text: Some("id".to_string()),
11423 detail: None,
11424 documentation: None,
11425 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11426 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11427 new_text: ".id".to_string(),
11428 })),
11429 ..lsp::CompletionItem::default()
11430 };
11431 let resolved_item_1 = lsp::CompletionItem {
11432 additional_text_edits: Some(vec![lsp::TextEdit {
11433 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
11434 new_text: "!!".to_string(),
11435 }]),
11436 ..unresolved_item_1.clone()
11437 };
11438 let unresolved_item_2 = lsp::CompletionItem {
11439 label: "other".to_string(),
11440 filter_text: Some("other".to_string()),
11441 detail: None,
11442 documentation: None,
11443 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11444 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
11445 new_text: ".other".to_string(),
11446 })),
11447 ..lsp::CompletionItem::default()
11448 };
11449 let resolved_item_2 = lsp::CompletionItem {
11450 additional_text_edits: Some(vec![lsp::TextEdit {
11451 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
11452 new_text: "??".to_string(),
11453 }]),
11454 ..unresolved_item_2.clone()
11455 };
11456
11457 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
11458 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
11459 cx.lsp
11460 .server
11461 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
11462 let unresolved_item_1 = unresolved_item_1.clone();
11463 let resolved_item_1 = resolved_item_1.clone();
11464 let unresolved_item_2 = unresolved_item_2.clone();
11465 let resolved_item_2 = resolved_item_2.clone();
11466 let resolve_requests_1 = resolve_requests_1.clone();
11467 let resolve_requests_2 = resolve_requests_2.clone();
11468 move |unresolved_request, _| {
11469 let unresolved_item_1 = unresolved_item_1.clone();
11470 let resolved_item_1 = resolved_item_1.clone();
11471 let unresolved_item_2 = unresolved_item_2.clone();
11472 let resolved_item_2 = resolved_item_2.clone();
11473 let resolve_requests_1 = resolve_requests_1.clone();
11474 let resolve_requests_2 = resolve_requests_2.clone();
11475 async move {
11476 if unresolved_request == unresolved_item_1 {
11477 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
11478 Ok(resolved_item_1.clone())
11479 } else if unresolved_request == unresolved_item_2 {
11480 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
11481 Ok(resolved_item_2.clone())
11482 } else {
11483 panic!("Unexpected completion item {unresolved_request:?}")
11484 }
11485 }
11486 }
11487 })
11488 .detach();
11489
11490 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11491 let unresolved_item_1 = unresolved_item_1.clone();
11492 let unresolved_item_2 = unresolved_item_2.clone();
11493 async move {
11494 Ok(Some(lsp::CompletionResponse::Array(vec![
11495 unresolved_item_1,
11496 unresolved_item_2,
11497 ])))
11498 }
11499 })
11500 .next()
11501 .await;
11502
11503 cx.condition(|editor, _| editor.context_menu_visible())
11504 .await;
11505 cx.update_editor(|editor, _, _| {
11506 let context_menu = editor.context_menu.borrow_mut();
11507 let context_menu = context_menu
11508 .as_ref()
11509 .expect("Should have the context menu deployed");
11510 match context_menu {
11511 CodeContextMenu::Completions(completions_menu) => {
11512 let completions = completions_menu.completions.borrow_mut();
11513 assert_eq!(
11514 completions
11515 .iter()
11516 .map(|completion| &completion.label.text)
11517 .collect::<Vec<_>>(),
11518 vec!["id", "other"]
11519 )
11520 }
11521 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
11522 }
11523 });
11524 cx.run_until_parked();
11525
11526 cx.update_editor(|editor, window, cx| {
11527 editor.context_menu_next(&ContextMenuNext, window, cx);
11528 });
11529 cx.run_until_parked();
11530 cx.update_editor(|editor, window, cx| {
11531 editor.context_menu_prev(&ContextMenuPrev, window, cx);
11532 });
11533 cx.run_until_parked();
11534 cx.update_editor(|editor, window, cx| {
11535 editor.context_menu_next(&ContextMenuNext, window, cx);
11536 });
11537 cx.run_until_parked();
11538 cx.update_editor(|editor, window, cx| {
11539 editor
11540 .compose_completion(&ComposeCompletion::default(), window, cx)
11541 .expect("No task returned")
11542 })
11543 .await
11544 .expect("Completion failed");
11545 cx.run_until_parked();
11546
11547 cx.update_editor(|editor, _, cx| {
11548 assert_eq!(
11549 resolve_requests_1.load(atomic::Ordering::Acquire),
11550 1,
11551 "Should always resolve once despite multiple selections"
11552 );
11553 assert_eq!(
11554 resolve_requests_2.load(atomic::Ordering::Acquire),
11555 1,
11556 "Should always resolve once after multiple selections and applying the completion"
11557 );
11558 assert_eq!(
11559 editor.text(cx),
11560 "fn main() { let a = ??.other; }",
11561 "Should use resolved data when applying the completion"
11562 );
11563 });
11564}
11565
11566#[gpui::test]
11567async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
11568 init_test(cx, |_| {});
11569
11570 let item_0 = lsp::CompletionItem {
11571 label: "abs".into(),
11572 insert_text: Some("abs".into()),
11573 data: Some(json!({ "very": "special"})),
11574 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
11575 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11576 lsp::InsertReplaceEdit {
11577 new_text: "abs".to_string(),
11578 insert: lsp::Range::default(),
11579 replace: lsp::Range::default(),
11580 },
11581 )),
11582 ..lsp::CompletionItem::default()
11583 };
11584 let items = iter::once(item_0.clone())
11585 .chain((11..51).map(|i| lsp::CompletionItem {
11586 label: format!("item_{}", i),
11587 insert_text: Some(format!("item_{}", i)),
11588 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
11589 ..lsp::CompletionItem::default()
11590 }))
11591 .collect::<Vec<_>>();
11592
11593 let default_commit_characters = vec!["?".to_string()];
11594 let default_data = json!({ "default": "data"});
11595 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
11596 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
11597 let default_edit_range = lsp::Range {
11598 start: lsp::Position {
11599 line: 0,
11600 character: 5,
11601 },
11602 end: lsp::Position {
11603 line: 0,
11604 character: 5,
11605 },
11606 };
11607
11608 let item_0_out = lsp::CompletionItem {
11609 commit_characters: Some(default_commit_characters.clone()),
11610 insert_text_format: Some(default_insert_text_format),
11611 ..item_0
11612 };
11613 let items_out = iter::once(item_0_out)
11614 .chain(items[1..].iter().map(|item| lsp::CompletionItem {
11615 commit_characters: Some(default_commit_characters.clone()),
11616 data: Some(default_data.clone()),
11617 insert_text_mode: Some(default_insert_text_mode),
11618 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11619 range: default_edit_range,
11620 new_text: item.label.clone(),
11621 })),
11622 ..item.clone()
11623 }))
11624 .collect::<Vec<lsp::CompletionItem>>();
11625
11626 let mut cx = EditorLspTestContext::new_rust(
11627 lsp::ServerCapabilities {
11628 completion_provider: Some(lsp::CompletionOptions {
11629 trigger_characters: Some(vec![".".to_string()]),
11630 resolve_provider: Some(true),
11631 ..Default::default()
11632 }),
11633 ..Default::default()
11634 },
11635 cx,
11636 )
11637 .await;
11638
11639 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
11640 cx.simulate_keystroke(".");
11641
11642 let completion_data = default_data.clone();
11643 let completion_characters = default_commit_characters.clone();
11644 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
11645 let default_data = completion_data.clone();
11646 let default_commit_characters = completion_characters.clone();
11647 let items = items.clone();
11648 async move {
11649 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
11650 items,
11651 item_defaults: Some(lsp::CompletionListItemDefaults {
11652 data: Some(default_data.clone()),
11653 commit_characters: Some(default_commit_characters.clone()),
11654 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
11655 default_edit_range,
11656 )),
11657 insert_text_format: Some(default_insert_text_format),
11658 insert_text_mode: Some(default_insert_text_mode),
11659 }),
11660 ..lsp::CompletionList::default()
11661 })))
11662 }
11663 })
11664 .next()
11665 .await;
11666
11667 let resolved_items = Arc::new(Mutex::new(Vec::new()));
11668 cx.lsp
11669 .server
11670 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
11671 let closure_resolved_items = resolved_items.clone();
11672 move |item_to_resolve, _| {
11673 let closure_resolved_items = closure_resolved_items.clone();
11674 async move {
11675 closure_resolved_items.lock().push(item_to_resolve.clone());
11676 Ok(item_to_resolve)
11677 }
11678 }
11679 })
11680 .detach();
11681
11682 cx.condition(|editor, _| editor.context_menu_visible())
11683 .await;
11684 cx.run_until_parked();
11685 cx.update_editor(|editor, _, _| {
11686 let menu = editor.context_menu.borrow_mut();
11687 match menu.as_ref().expect("should have the completions menu") {
11688 CodeContextMenu::Completions(completions_menu) => {
11689 assert_eq!(
11690 completions_menu
11691 .entries
11692 .borrow()
11693 .iter()
11694 .flat_map(|c| match c {
11695 CompletionEntry::Match(mat) => Some(mat.string.clone()),
11696 _ => None,
11697 })
11698 .collect::<Vec<String>>(),
11699 items_out
11700 .iter()
11701 .map(|completion| completion.label.clone())
11702 .collect::<Vec<String>>()
11703 );
11704 }
11705 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
11706 }
11707 });
11708 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
11709 // with 4 from the end.
11710 assert_eq!(
11711 *resolved_items.lock(),
11712 [
11713 &items_out[0..16],
11714 &items_out[items_out.len() - 4..items_out.len()]
11715 ]
11716 .concat()
11717 .iter()
11718 .cloned()
11719 .collect::<Vec<lsp::CompletionItem>>()
11720 );
11721 resolved_items.lock().clear();
11722
11723 cx.update_editor(|editor, window, cx| {
11724 editor.context_menu_prev(&ContextMenuPrev, window, cx);
11725 });
11726 cx.run_until_parked();
11727 // Completions that have already been resolved are skipped.
11728 assert_eq!(
11729 *resolved_items.lock(),
11730 items_out[items_out.len() - 16..items_out.len() - 4]
11731 .iter()
11732 .cloned()
11733 .collect::<Vec<lsp::CompletionItem>>()
11734 );
11735 resolved_items.lock().clear();
11736}
11737
11738#[gpui::test]
11739async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
11740 init_test(cx, |_| {});
11741
11742 let mut cx = EditorLspTestContext::new(
11743 Language::new(
11744 LanguageConfig {
11745 matcher: LanguageMatcher {
11746 path_suffixes: vec!["jsx".into()],
11747 ..Default::default()
11748 },
11749 overrides: [(
11750 "element".into(),
11751 LanguageConfigOverride {
11752 word_characters: Override::Set(['-'].into_iter().collect()),
11753 ..Default::default()
11754 },
11755 )]
11756 .into_iter()
11757 .collect(),
11758 ..Default::default()
11759 },
11760 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
11761 )
11762 .with_override_query("(jsx_self_closing_element) @element")
11763 .unwrap(),
11764 lsp::ServerCapabilities {
11765 completion_provider: Some(lsp::CompletionOptions {
11766 trigger_characters: Some(vec![":".to_string()]),
11767 ..Default::default()
11768 }),
11769 ..Default::default()
11770 },
11771 cx,
11772 )
11773 .await;
11774
11775 cx.lsp
11776 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11777 Ok(Some(lsp::CompletionResponse::Array(vec![
11778 lsp::CompletionItem {
11779 label: "bg-blue".into(),
11780 ..Default::default()
11781 },
11782 lsp::CompletionItem {
11783 label: "bg-red".into(),
11784 ..Default::default()
11785 },
11786 lsp::CompletionItem {
11787 label: "bg-yellow".into(),
11788 ..Default::default()
11789 },
11790 ])))
11791 });
11792
11793 cx.set_state(r#"<p class="bgˇ" />"#);
11794
11795 // Trigger completion when typing a dash, because the dash is an extra
11796 // word character in the 'element' scope, which contains the cursor.
11797 cx.simulate_keystroke("-");
11798 cx.executor().run_until_parked();
11799 cx.update_editor(|editor, _, _| {
11800 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11801 {
11802 assert_eq!(
11803 completion_menu_entries(&menu),
11804 &["bg-red", "bg-blue", "bg-yellow"]
11805 );
11806 } else {
11807 panic!("expected completion menu to be open");
11808 }
11809 });
11810
11811 cx.simulate_keystroke("l");
11812 cx.executor().run_until_parked();
11813 cx.update_editor(|editor, _, _| {
11814 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11815 {
11816 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
11817 } else {
11818 panic!("expected completion menu to be open");
11819 }
11820 });
11821
11822 // When filtering completions, consider the character after the '-' to
11823 // be the start of a subword.
11824 cx.set_state(r#"<p class="yelˇ" />"#);
11825 cx.simulate_keystroke("l");
11826 cx.executor().run_until_parked();
11827 cx.update_editor(|editor, _, _| {
11828 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11829 {
11830 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
11831 } else {
11832 panic!("expected completion menu to be open");
11833 }
11834 });
11835}
11836
11837fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
11838 let entries = menu.entries.borrow();
11839 entries
11840 .iter()
11841 .flat_map(|e| match e {
11842 CompletionEntry::Match(mat) => Some(mat.string.clone()),
11843 _ => None,
11844 })
11845 .collect()
11846}
11847
11848#[gpui::test]
11849async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
11850 init_test(cx, |settings| {
11851 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
11852 FormatterList(vec![Formatter::Prettier].into()),
11853 ))
11854 });
11855
11856 let fs = FakeFs::new(cx.executor());
11857 fs.insert_file("/file.ts", Default::default()).await;
11858
11859 let project = Project::test(fs, ["/file.ts".as_ref()], cx).await;
11860 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11861
11862 language_registry.add(Arc::new(Language::new(
11863 LanguageConfig {
11864 name: "TypeScript".into(),
11865 matcher: LanguageMatcher {
11866 path_suffixes: vec!["ts".to_string()],
11867 ..Default::default()
11868 },
11869 ..Default::default()
11870 },
11871 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11872 )));
11873 update_test_language_settings(cx, |settings| {
11874 settings.defaults.prettier = Some(PrettierSettings {
11875 allowed: true,
11876 ..PrettierSettings::default()
11877 });
11878 });
11879
11880 let test_plugin = "test_plugin";
11881 let _ = language_registry.register_fake_lsp(
11882 "TypeScript",
11883 FakeLspAdapter {
11884 prettier_plugins: vec![test_plugin],
11885 ..Default::default()
11886 },
11887 );
11888
11889 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
11890 let buffer = project
11891 .update(cx, |project, cx| project.open_local_buffer("/file.ts", cx))
11892 .await
11893 .unwrap();
11894
11895 let buffer_text = "one\ntwo\nthree\n";
11896 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11897 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11898 editor.update_in(cx, |editor, window, cx| {
11899 editor.set_text(buffer_text, window, cx)
11900 });
11901
11902 editor
11903 .update_in(cx, |editor, window, cx| {
11904 editor.perform_format(
11905 project.clone(),
11906 FormatTrigger::Manual,
11907 FormatTarget::Buffers,
11908 window,
11909 cx,
11910 )
11911 })
11912 .unwrap()
11913 .await;
11914 assert_eq!(
11915 editor.update(cx, |editor, cx| editor.text(cx)),
11916 buffer_text.to_string() + prettier_format_suffix,
11917 "Test prettier formatting was not applied to the original buffer text",
11918 );
11919
11920 update_test_language_settings(cx, |settings| {
11921 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
11922 });
11923 let format = editor.update_in(cx, |editor, window, cx| {
11924 editor.perform_format(
11925 project.clone(),
11926 FormatTrigger::Manual,
11927 FormatTarget::Buffers,
11928 window,
11929 cx,
11930 )
11931 });
11932 format.await.unwrap();
11933 assert_eq!(
11934 editor.update(cx, |editor, cx| editor.text(cx)),
11935 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
11936 "Autoformatting (via test prettier) was not applied to the original buffer text",
11937 );
11938}
11939
11940#[gpui::test]
11941async fn test_addition_reverts(cx: &mut gpui::TestAppContext) {
11942 init_test(cx, |_| {});
11943 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11944 let base_text = indoc! {r#"
11945 struct Row;
11946 struct Row1;
11947 struct Row2;
11948
11949 struct Row4;
11950 struct Row5;
11951 struct Row6;
11952
11953 struct Row8;
11954 struct Row9;
11955 struct Row10;"#};
11956
11957 // When addition hunks are not adjacent to carets, no hunk revert is performed
11958 assert_hunk_revert(
11959 indoc! {r#"struct Row;
11960 struct Row1;
11961 struct Row1.1;
11962 struct Row1.2;
11963 struct Row2;ˇ
11964
11965 struct Row4;
11966 struct Row5;
11967 struct Row6;
11968
11969 struct Row8;
11970 ˇstruct Row9;
11971 struct Row9.1;
11972 struct Row9.2;
11973 struct Row9.3;
11974 struct Row10;"#},
11975 vec![DiffHunkStatus::Added, DiffHunkStatus::Added],
11976 indoc! {r#"struct Row;
11977 struct Row1;
11978 struct Row1.1;
11979 struct Row1.2;
11980 struct Row2;ˇ
11981
11982 struct Row4;
11983 struct Row5;
11984 struct Row6;
11985
11986 struct Row8;
11987 ˇstruct Row9;
11988 struct Row9.1;
11989 struct Row9.2;
11990 struct Row9.3;
11991 struct Row10;"#},
11992 base_text,
11993 &mut cx,
11994 );
11995 // Same for selections
11996 assert_hunk_revert(
11997 indoc! {r#"struct Row;
11998 struct Row1;
11999 struct Row2;
12000 struct Row2.1;
12001 struct Row2.2;
12002 «ˇ
12003 struct Row4;
12004 struct» Row5;
12005 «struct Row6;
12006 ˇ»
12007 struct Row9.1;
12008 struct Row9.2;
12009 struct Row9.3;
12010 struct Row8;
12011 struct Row9;
12012 struct Row10;"#},
12013 vec![DiffHunkStatus::Added, DiffHunkStatus::Added],
12014 indoc! {r#"struct Row;
12015 struct Row1;
12016 struct Row2;
12017 struct Row2.1;
12018 struct Row2.2;
12019 «ˇ
12020 struct Row4;
12021 struct» Row5;
12022 «struct Row6;
12023 ˇ»
12024 struct Row9.1;
12025 struct Row9.2;
12026 struct Row9.3;
12027 struct Row8;
12028 struct Row9;
12029 struct Row10;"#},
12030 base_text,
12031 &mut cx,
12032 );
12033
12034 // When carets and selections intersect the addition hunks, those are reverted.
12035 // Adjacent carets got merged.
12036 assert_hunk_revert(
12037 indoc! {r#"struct Row;
12038 ˇ// something on the top
12039 struct Row1;
12040 struct Row2;
12041 struct Roˇw3.1;
12042 struct Row2.2;
12043 struct Row2.3;ˇ
12044
12045 struct Row4;
12046 struct ˇRow5.1;
12047 struct Row5.2;
12048 struct «Rowˇ»5.3;
12049 struct Row5;
12050 struct Row6;
12051 ˇ
12052 struct Row9.1;
12053 struct «Rowˇ»9.2;
12054 struct «ˇRow»9.3;
12055 struct Row8;
12056 struct Row9;
12057 «ˇ// something on bottom»
12058 struct Row10;"#},
12059 vec![
12060 DiffHunkStatus::Added,
12061 DiffHunkStatus::Added,
12062 DiffHunkStatus::Added,
12063 DiffHunkStatus::Added,
12064 DiffHunkStatus::Added,
12065 ],
12066 indoc! {r#"struct Row;
12067 ˇstruct Row1;
12068 struct Row2;
12069 ˇ
12070 struct Row4;
12071 ˇstruct Row5;
12072 struct Row6;
12073 ˇ
12074 ˇstruct Row8;
12075 struct Row9;
12076 ˇstruct Row10;"#},
12077 base_text,
12078 &mut cx,
12079 );
12080}
12081
12082#[gpui::test]
12083async fn test_modification_reverts(cx: &mut gpui::TestAppContext) {
12084 init_test(cx, |_| {});
12085 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12086 let base_text = indoc! {r#"
12087 struct Row;
12088 struct Row1;
12089 struct Row2;
12090
12091 struct Row4;
12092 struct Row5;
12093 struct Row6;
12094
12095 struct Row8;
12096 struct Row9;
12097 struct Row10;"#};
12098
12099 // Modification hunks behave the same as the addition ones.
12100 assert_hunk_revert(
12101 indoc! {r#"struct Row;
12102 struct Row1;
12103 struct Row33;
12104 ˇ
12105 struct Row4;
12106 struct Row5;
12107 struct Row6;
12108 ˇ
12109 struct Row99;
12110 struct Row9;
12111 struct Row10;"#},
12112 vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified],
12113 indoc! {r#"struct Row;
12114 struct Row1;
12115 struct Row33;
12116 ˇ
12117 struct Row4;
12118 struct Row5;
12119 struct Row6;
12120 ˇ
12121 struct Row99;
12122 struct Row9;
12123 struct Row10;"#},
12124 base_text,
12125 &mut cx,
12126 );
12127 assert_hunk_revert(
12128 indoc! {r#"struct Row;
12129 struct Row1;
12130 struct Row33;
12131 «ˇ
12132 struct Row4;
12133 struct» Row5;
12134 «struct Row6;
12135 ˇ»
12136 struct Row99;
12137 struct Row9;
12138 struct Row10;"#},
12139 vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified],
12140 indoc! {r#"struct Row;
12141 struct Row1;
12142 struct Row33;
12143 «ˇ
12144 struct Row4;
12145 struct» Row5;
12146 «struct Row6;
12147 ˇ»
12148 struct Row99;
12149 struct Row9;
12150 struct Row10;"#},
12151 base_text,
12152 &mut cx,
12153 );
12154
12155 assert_hunk_revert(
12156 indoc! {r#"ˇstruct Row1.1;
12157 struct Row1;
12158 «ˇstr»uct Row22;
12159
12160 struct ˇRow44;
12161 struct Row5;
12162 struct «Rˇ»ow66;ˇ
12163
12164 «struˇ»ct Row88;
12165 struct Row9;
12166 struct Row1011;ˇ"#},
12167 vec![
12168 DiffHunkStatus::Modified,
12169 DiffHunkStatus::Modified,
12170 DiffHunkStatus::Modified,
12171 DiffHunkStatus::Modified,
12172 DiffHunkStatus::Modified,
12173 DiffHunkStatus::Modified,
12174 ],
12175 indoc! {r#"struct Row;
12176 ˇstruct Row1;
12177 struct Row2;
12178 ˇ
12179 struct Row4;
12180 ˇstruct Row5;
12181 struct Row6;
12182 ˇ
12183 struct Row8;
12184 ˇstruct Row9;
12185 struct Row10;ˇ"#},
12186 base_text,
12187 &mut cx,
12188 );
12189}
12190
12191#[gpui::test]
12192async fn test_deleting_over_diff_hunk(cx: &mut gpui::TestAppContext) {
12193 init_test(cx, |_| {});
12194 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12195 let base_text = indoc! {r#"
12196 one
12197
12198 two
12199 three
12200 "#};
12201
12202 cx.set_diff_base(base_text);
12203 cx.set_state("\nˇ\n");
12204 cx.executor().run_until_parked();
12205 cx.update_editor(|editor, _window, cx| {
12206 editor.expand_selected_diff_hunks(cx);
12207 });
12208 cx.executor().run_until_parked();
12209 cx.update_editor(|editor, window, cx| {
12210 editor.backspace(&Default::default(), window, cx);
12211 });
12212 cx.run_until_parked();
12213 cx.assert_state_with_diff(
12214 indoc! {r#"
12215
12216 - two
12217 - threeˇ
12218 +
12219 "#}
12220 .to_string(),
12221 );
12222}
12223
12224#[gpui::test]
12225async fn test_deletion_reverts(cx: &mut gpui::TestAppContext) {
12226 init_test(cx, |_| {});
12227 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12228 let base_text = indoc! {r#"struct Row;
12229struct Row1;
12230struct Row2;
12231
12232struct Row4;
12233struct Row5;
12234struct Row6;
12235
12236struct Row8;
12237struct Row9;
12238struct Row10;"#};
12239
12240 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
12241 assert_hunk_revert(
12242 indoc! {r#"struct Row;
12243 struct Row2;
12244
12245 ˇstruct Row4;
12246 struct Row5;
12247 struct Row6;
12248 ˇ
12249 struct Row8;
12250 struct Row10;"#},
12251 vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
12252 indoc! {r#"struct Row;
12253 struct Row2;
12254
12255 ˇstruct Row4;
12256 struct Row5;
12257 struct Row6;
12258 ˇ
12259 struct Row8;
12260 struct Row10;"#},
12261 base_text,
12262 &mut cx,
12263 );
12264 assert_hunk_revert(
12265 indoc! {r#"struct Row;
12266 struct Row2;
12267
12268 «ˇstruct Row4;
12269 struct» Row5;
12270 «struct Row6;
12271 ˇ»
12272 struct Row8;
12273 struct Row10;"#},
12274 vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
12275 indoc! {r#"struct Row;
12276 struct Row2;
12277
12278 «ˇstruct Row4;
12279 struct» Row5;
12280 «struct Row6;
12281 ˇ»
12282 struct Row8;
12283 struct Row10;"#},
12284 base_text,
12285 &mut cx,
12286 );
12287
12288 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
12289 assert_hunk_revert(
12290 indoc! {r#"struct Row;
12291 ˇstruct Row2;
12292
12293 struct Row4;
12294 struct Row5;
12295 struct Row6;
12296
12297 struct Row8;ˇ
12298 struct Row10;"#},
12299 vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
12300 indoc! {r#"struct Row;
12301 struct Row1;
12302 ˇstruct Row2;
12303
12304 struct Row4;
12305 struct Row5;
12306 struct Row6;
12307
12308 struct Row8;ˇ
12309 struct Row9;
12310 struct Row10;"#},
12311 base_text,
12312 &mut cx,
12313 );
12314 assert_hunk_revert(
12315 indoc! {r#"struct Row;
12316 struct Row2«ˇ;
12317 struct Row4;
12318 struct» Row5;
12319 «struct Row6;
12320
12321 struct Row8;ˇ»
12322 struct Row10;"#},
12323 vec![
12324 DiffHunkStatus::Removed,
12325 DiffHunkStatus::Removed,
12326 DiffHunkStatus::Removed,
12327 ],
12328 indoc! {r#"struct Row;
12329 struct Row1;
12330 struct Row2«ˇ;
12331
12332 struct Row4;
12333 struct» Row5;
12334 «struct Row6;
12335
12336 struct Row8;ˇ»
12337 struct Row9;
12338 struct Row10;"#},
12339 base_text,
12340 &mut cx,
12341 );
12342}
12343
12344#[gpui::test]
12345async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
12346 init_test(cx, |_| {});
12347
12348 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
12349 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
12350 let base_text_3 =
12351 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
12352
12353 let text_1 = edit_first_char_of_every_line(base_text_1);
12354 let text_2 = edit_first_char_of_every_line(base_text_2);
12355 let text_3 = edit_first_char_of_every_line(base_text_3);
12356
12357 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
12358 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
12359 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
12360
12361 let multibuffer = cx.new(|cx| {
12362 let mut multibuffer = MultiBuffer::new(ReadWrite);
12363 multibuffer.push_excerpts(
12364 buffer_1.clone(),
12365 [
12366 ExcerptRange {
12367 context: Point::new(0, 0)..Point::new(3, 0),
12368 primary: None,
12369 },
12370 ExcerptRange {
12371 context: Point::new(5, 0)..Point::new(7, 0),
12372 primary: None,
12373 },
12374 ExcerptRange {
12375 context: Point::new(9, 0)..Point::new(10, 4),
12376 primary: None,
12377 },
12378 ],
12379 cx,
12380 );
12381 multibuffer.push_excerpts(
12382 buffer_2.clone(),
12383 [
12384 ExcerptRange {
12385 context: Point::new(0, 0)..Point::new(3, 0),
12386 primary: None,
12387 },
12388 ExcerptRange {
12389 context: Point::new(5, 0)..Point::new(7, 0),
12390 primary: None,
12391 },
12392 ExcerptRange {
12393 context: Point::new(9, 0)..Point::new(10, 4),
12394 primary: None,
12395 },
12396 ],
12397 cx,
12398 );
12399 multibuffer.push_excerpts(
12400 buffer_3.clone(),
12401 [
12402 ExcerptRange {
12403 context: Point::new(0, 0)..Point::new(3, 0),
12404 primary: None,
12405 },
12406 ExcerptRange {
12407 context: Point::new(5, 0)..Point::new(7, 0),
12408 primary: None,
12409 },
12410 ExcerptRange {
12411 context: Point::new(9, 0)..Point::new(10, 4),
12412 primary: None,
12413 },
12414 ],
12415 cx,
12416 );
12417 multibuffer
12418 });
12419
12420 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12421 editor.update_in(cx, |editor, _window, cx| {
12422 for (buffer, diff_base) in [
12423 (buffer_1.clone(), base_text_1),
12424 (buffer_2.clone(), base_text_2),
12425 (buffer_3.clone(), base_text_3),
12426 ] {
12427 let change_set = cx
12428 .new(|cx| BufferChangeSet::new_with_base_text(diff_base.to_string(), &buffer, cx));
12429 editor
12430 .buffer
12431 .update(cx, |buffer, cx| buffer.add_change_set(change_set, cx));
12432 }
12433 });
12434 cx.executor().run_until_parked();
12435
12436 editor.update_in(cx, |editor, window, cx| {
12437 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}");
12438 editor.select_all(&SelectAll, window, cx);
12439 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
12440 });
12441 cx.executor().run_until_parked();
12442
12443 // When all ranges are selected, all buffer hunks are reverted.
12444 editor.update(cx, |editor, cx| {
12445 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");
12446 });
12447 buffer_1.update(cx, |buffer, _| {
12448 assert_eq!(buffer.text(), base_text_1);
12449 });
12450 buffer_2.update(cx, |buffer, _| {
12451 assert_eq!(buffer.text(), base_text_2);
12452 });
12453 buffer_3.update(cx, |buffer, _| {
12454 assert_eq!(buffer.text(), base_text_3);
12455 });
12456
12457 editor.update_in(cx, |editor, window, cx| {
12458 editor.undo(&Default::default(), window, cx);
12459 });
12460
12461 editor.update_in(cx, |editor, window, cx| {
12462 editor.change_selections(None, window, cx, |s| {
12463 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
12464 });
12465 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
12466 });
12467
12468 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
12469 // but not affect buffer_2 and its related excerpts.
12470 editor.update(cx, |editor, cx| {
12471 assert_eq!(
12472 editor.text(cx),
12473 "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}"
12474 );
12475 });
12476 buffer_1.update(cx, |buffer, _| {
12477 assert_eq!(buffer.text(), base_text_1);
12478 });
12479 buffer_2.update(cx, |buffer, _| {
12480 assert_eq!(
12481 buffer.text(),
12482 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
12483 );
12484 });
12485 buffer_3.update(cx, |buffer, _| {
12486 assert_eq!(
12487 buffer.text(),
12488 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
12489 );
12490 });
12491
12492 fn edit_first_char_of_every_line(text: &str) -> String {
12493 text.split('\n')
12494 .map(|line| format!("X{}", &line[1..]))
12495 .collect::<Vec<_>>()
12496 .join("\n")
12497 }
12498}
12499
12500#[gpui::test]
12501async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
12502 init_test(cx, |_| {});
12503
12504 let cols = 4;
12505 let rows = 10;
12506 let sample_text_1 = sample_text(rows, cols, 'a');
12507 assert_eq!(
12508 sample_text_1,
12509 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
12510 );
12511 let sample_text_2 = sample_text(rows, cols, 'l');
12512 assert_eq!(
12513 sample_text_2,
12514 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
12515 );
12516 let sample_text_3 = sample_text(rows, cols, 'v');
12517 assert_eq!(
12518 sample_text_3,
12519 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
12520 );
12521
12522 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
12523 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
12524 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
12525
12526 let multi_buffer = cx.new(|cx| {
12527 let mut multibuffer = MultiBuffer::new(ReadWrite);
12528 multibuffer.push_excerpts(
12529 buffer_1.clone(),
12530 [
12531 ExcerptRange {
12532 context: Point::new(0, 0)..Point::new(3, 0),
12533 primary: None,
12534 },
12535 ExcerptRange {
12536 context: Point::new(5, 0)..Point::new(7, 0),
12537 primary: None,
12538 },
12539 ExcerptRange {
12540 context: Point::new(9, 0)..Point::new(10, 4),
12541 primary: None,
12542 },
12543 ],
12544 cx,
12545 );
12546 multibuffer.push_excerpts(
12547 buffer_2.clone(),
12548 [
12549 ExcerptRange {
12550 context: Point::new(0, 0)..Point::new(3, 0),
12551 primary: None,
12552 },
12553 ExcerptRange {
12554 context: Point::new(5, 0)..Point::new(7, 0),
12555 primary: None,
12556 },
12557 ExcerptRange {
12558 context: Point::new(9, 0)..Point::new(10, 4),
12559 primary: None,
12560 },
12561 ],
12562 cx,
12563 );
12564 multibuffer.push_excerpts(
12565 buffer_3.clone(),
12566 [
12567 ExcerptRange {
12568 context: Point::new(0, 0)..Point::new(3, 0),
12569 primary: None,
12570 },
12571 ExcerptRange {
12572 context: Point::new(5, 0)..Point::new(7, 0),
12573 primary: None,
12574 },
12575 ExcerptRange {
12576 context: Point::new(9, 0)..Point::new(10, 4),
12577 primary: None,
12578 },
12579 ],
12580 cx,
12581 );
12582 multibuffer
12583 });
12584
12585 let fs = FakeFs::new(cx.executor());
12586 fs.insert_tree(
12587 "/a",
12588 json!({
12589 "main.rs": sample_text_1,
12590 "other.rs": sample_text_2,
12591 "lib.rs": sample_text_3,
12592 }),
12593 )
12594 .await;
12595 let project = Project::test(fs, ["/a".as_ref()], cx).await;
12596 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12597 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12598 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
12599 Editor::new(
12600 EditorMode::Full,
12601 multi_buffer,
12602 Some(project.clone()),
12603 true,
12604 window,
12605 cx,
12606 )
12607 });
12608 let multibuffer_item_id = workspace
12609 .update(cx, |workspace, window, cx| {
12610 assert!(
12611 workspace.active_item(cx).is_none(),
12612 "active item should be None before the first item is added"
12613 );
12614 workspace.add_item_to_active_pane(
12615 Box::new(multi_buffer_editor.clone()),
12616 None,
12617 true,
12618 window,
12619 cx,
12620 );
12621 let active_item = workspace
12622 .active_item(cx)
12623 .expect("should have an active item after adding the multi buffer");
12624 assert!(
12625 !active_item.is_singleton(cx),
12626 "A multi buffer was expected to active after adding"
12627 );
12628 active_item.item_id()
12629 })
12630 .unwrap();
12631 cx.executor().run_until_parked();
12632
12633 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12634 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12635 s.select_ranges(Some(1..2))
12636 });
12637 editor.open_excerpts(&OpenExcerpts, window, cx);
12638 });
12639 cx.executor().run_until_parked();
12640 let first_item_id = workspace
12641 .update(cx, |workspace, window, cx| {
12642 let active_item = workspace
12643 .active_item(cx)
12644 .expect("should have an active item after navigating into the 1st buffer");
12645 let first_item_id = active_item.item_id();
12646 assert_ne!(
12647 first_item_id, multibuffer_item_id,
12648 "Should navigate into the 1st buffer and activate it"
12649 );
12650 assert!(
12651 active_item.is_singleton(cx),
12652 "New active item should be a singleton buffer"
12653 );
12654 assert_eq!(
12655 active_item
12656 .act_as::<Editor>(cx)
12657 .expect("should have navigated into an editor for the 1st buffer")
12658 .read(cx)
12659 .text(cx),
12660 sample_text_1
12661 );
12662
12663 workspace
12664 .go_back(workspace.active_pane().downgrade(), window, cx)
12665 .detach_and_log_err(cx);
12666
12667 first_item_id
12668 })
12669 .unwrap();
12670 cx.executor().run_until_parked();
12671 workspace
12672 .update(cx, |workspace, _, cx| {
12673 let active_item = workspace
12674 .active_item(cx)
12675 .expect("should have an active item after navigating back");
12676 assert_eq!(
12677 active_item.item_id(),
12678 multibuffer_item_id,
12679 "Should navigate back to the multi buffer"
12680 );
12681 assert!(!active_item.is_singleton(cx));
12682 })
12683 .unwrap();
12684
12685 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12686 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12687 s.select_ranges(Some(39..40))
12688 });
12689 editor.open_excerpts(&OpenExcerpts, window, cx);
12690 });
12691 cx.executor().run_until_parked();
12692 let second_item_id = workspace
12693 .update(cx, |workspace, window, cx| {
12694 let active_item = workspace
12695 .active_item(cx)
12696 .expect("should have an active item after navigating into the 2nd buffer");
12697 let second_item_id = active_item.item_id();
12698 assert_ne!(
12699 second_item_id, multibuffer_item_id,
12700 "Should navigate away from the multibuffer"
12701 );
12702 assert_ne!(
12703 second_item_id, first_item_id,
12704 "Should navigate into the 2nd buffer and activate it"
12705 );
12706 assert!(
12707 active_item.is_singleton(cx),
12708 "New active item should be a singleton buffer"
12709 );
12710 assert_eq!(
12711 active_item
12712 .act_as::<Editor>(cx)
12713 .expect("should have navigated into an editor")
12714 .read(cx)
12715 .text(cx),
12716 sample_text_2
12717 );
12718
12719 workspace
12720 .go_back(workspace.active_pane().downgrade(), window, cx)
12721 .detach_and_log_err(cx);
12722
12723 second_item_id
12724 })
12725 .unwrap();
12726 cx.executor().run_until_parked();
12727 workspace
12728 .update(cx, |workspace, _, cx| {
12729 let active_item = workspace
12730 .active_item(cx)
12731 .expect("should have an active item after navigating back from the 2nd buffer");
12732 assert_eq!(
12733 active_item.item_id(),
12734 multibuffer_item_id,
12735 "Should navigate back from the 2nd buffer to the multi buffer"
12736 );
12737 assert!(!active_item.is_singleton(cx));
12738 })
12739 .unwrap();
12740
12741 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12742 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
12743 s.select_ranges(Some(70..70))
12744 });
12745 editor.open_excerpts(&OpenExcerpts, window, cx);
12746 });
12747 cx.executor().run_until_parked();
12748 workspace
12749 .update(cx, |workspace, window, cx| {
12750 let active_item = workspace
12751 .active_item(cx)
12752 .expect("should have an active item after navigating into the 3rd buffer");
12753 let third_item_id = active_item.item_id();
12754 assert_ne!(
12755 third_item_id, multibuffer_item_id,
12756 "Should navigate into the 3rd buffer and activate it"
12757 );
12758 assert_ne!(third_item_id, first_item_id);
12759 assert_ne!(third_item_id, second_item_id);
12760 assert!(
12761 active_item.is_singleton(cx),
12762 "New active item should be a singleton buffer"
12763 );
12764 assert_eq!(
12765 active_item
12766 .act_as::<Editor>(cx)
12767 .expect("should have navigated into an editor")
12768 .read(cx)
12769 .text(cx),
12770 sample_text_3
12771 );
12772
12773 workspace
12774 .go_back(workspace.active_pane().downgrade(), window, cx)
12775 .detach_and_log_err(cx);
12776 })
12777 .unwrap();
12778 cx.executor().run_until_parked();
12779 workspace
12780 .update(cx, |workspace, _, cx| {
12781 let active_item = workspace
12782 .active_item(cx)
12783 .expect("should have an active item after navigating back from the 3rd buffer");
12784 assert_eq!(
12785 active_item.item_id(),
12786 multibuffer_item_id,
12787 "Should navigate back from the 3rd buffer to the multi buffer"
12788 );
12789 assert!(!active_item.is_singleton(cx));
12790 })
12791 .unwrap();
12792}
12793
12794#[gpui::test]
12795async fn test_toggle_selected_diff_hunks(
12796 executor: BackgroundExecutor,
12797 cx: &mut gpui::TestAppContext,
12798) {
12799 init_test(cx, |_| {});
12800
12801 let mut cx = EditorTestContext::new(cx).await;
12802
12803 let diff_base = r#"
12804 use some::mod;
12805
12806 const A: u32 = 42;
12807
12808 fn main() {
12809 println!("hello");
12810
12811 println!("world");
12812 }
12813 "#
12814 .unindent();
12815
12816 cx.set_state(
12817 &r#"
12818 use some::modified;
12819
12820 ˇ
12821 fn main() {
12822 println!("hello there");
12823
12824 println!("around the");
12825 println!("world");
12826 }
12827 "#
12828 .unindent(),
12829 );
12830
12831 cx.set_diff_base(&diff_base);
12832 executor.run_until_parked();
12833
12834 cx.update_editor(|editor, window, cx| {
12835 editor.go_to_next_hunk(&GoToHunk, window, cx);
12836 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
12837 });
12838 executor.run_until_parked();
12839 cx.assert_state_with_diff(
12840 r#"
12841 use some::modified;
12842
12843
12844 fn main() {
12845 - println!("hello");
12846 + ˇ println!("hello there");
12847
12848 println!("around the");
12849 println!("world");
12850 }
12851 "#
12852 .unindent(),
12853 );
12854
12855 cx.update_editor(|editor, window, cx| {
12856 for _ in 0..2 {
12857 editor.go_to_next_hunk(&GoToHunk, window, cx);
12858 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
12859 }
12860 });
12861 executor.run_until_parked();
12862 cx.assert_state_with_diff(
12863 r#"
12864 - use some::mod;
12865 + ˇuse some::modified;
12866
12867
12868 fn main() {
12869 - println!("hello");
12870 + println!("hello there");
12871
12872 + println!("around the");
12873 println!("world");
12874 }
12875 "#
12876 .unindent(),
12877 );
12878
12879 cx.update_editor(|editor, window, cx| {
12880 editor.go_to_next_hunk(&GoToHunk, window, cx);
12881 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
12882 });
12883 executor.run_until_parked();
12884 cx.assert_state_with_diff(
12885 r#"
12886 - use some::mod;
12887 + use some::modified;
12888
12889 - const A: u32 = 42;
12890 ˇ
12891 fn main() {
12892 - println!("hello");
12893 + println!("hello there");
12894
12895 + println!("around the");
12896 println!("world");
12897 }
12898 "#
12899 .unindent(),
12900 );
12901
12902 cx.update_editor(|editor, window, cx| {
12903 editor.cancel(&Cancel, window, cx);
12904 });
12905
12906 cx.assert_state_with_diff(
12907 r#"
12908 use some::modified;
12909
12910 ˇ
12911 fn main() {
12912 println!("hello there");
12913
12914 println!("around the");
12915 println!("world");
12916 }
12917 "#
12918 .unindent(),
12919 );
12920}
12921
12922#[gpui::test]
12923async fn test_diff_base_change_with_expanded_diff_hunks(
12924 executor: BackgroundExecutor,
12925 cx: &mut gpui::TestAppContext,
12926) {
12927 init_test(cx, |_| {});
12928
12929 let mut cx = EditorTestContext::new(cx).await;
12930
12931 let diff_base = r#"
12932 use some::mod1;
12933 use some::mod2;
12934
12935 const A: u32 = 42;
12936 const B: u32 = 42;
12937 const C: u32 = 42;
12938
12939 fn main() {
12940 println!("hello");
12941
12942 println!("world");
12943 }
12944 "#
12945 .unindent();
12946
12947 cx.set_state(
12948 &r#"
12949 use some::mod2;
12950
12951 const A: u32 = 42;
12952 const C: u32 = 42;
12953
12954 fn main(ˇ) {
12955 //println!("hello");
12956
12957 println!("world");
12958 //
12959 //
12960 }
12961 "#
12962 .unindent(),
12963 );
12964
12965 cx.set_diff_base(&diff_base);
12966 executor.run_until_parked();
12967
12968 cx.update_editor(|editor, window, cx| {
12969 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
12970 });
12971 executor.run_until_parked();
12972 cx.assert_state_with_diff(
12973 r#"
12974 - use some::mod1;
12975 use some::mod2;
12976
12977 const A: u32 = 42;
12978 - const B: u32 = 42;
12979 const C: u32 = 42;
12980
12981 fn main(ˇ) {
12982 - println!("hello");
12983 + //println!("hello");
12984
12985 println!("world");
12986 + //
12987 + //
12988 }
12989 "#
12990 .unindent(),
12991 );
12992
12993 cx.set_diff_base("new diff base!");
12994 executor.run_until_parked();
12995 cx.assert_state_with_diff(
12996 r#"
12997 use some::mod2;
12998
12999 const A: u32 = 42;
13000 const C: u32 = 42;
13001
13002 fn main(ˇ) {
13003 //println!("hello");
13004
13005 println!("world");
13006 //
13007 //
13008 }
13009 "#
13010 .unindent(),
13011 );
13012
13013 cx.update_editor(|editor, window, cx| {
13014 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13015 });
13016 executor.run_until_parked();
13017 cx.assert_state_with_diff(
13018 r#"
13019 - new diff base!
13020 + use some::mod2;
13021 +
13022 + const A: u32 = 42;
13023 + const C: u32 = 42;
13024 +
13025 + fn main(ˇ) {
13026 + //println!("hello");
13027 +
13028 + println!("world");
13029 + //
13030 + //
13031 + }
13032 "#
13033 .unindent(),
13034 );
13035}
13036
13037#[gpui::test]
13038async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) {
13039 init_test(cx, |_| {});
13040
13041 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
13042 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
13043 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
13044 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
13045 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
13046 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
13047
13048 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
13049 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
13050 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
13051
13052 let multi_buffer = cx.new(|cx| {
13053 let mut multibuffer = MultiBuffer::new(ReadWrite);
13054 multibuffer.push_excerpts(
13055 buffer_1.clone(),
13056 [
13057 ExcerptRange {
13058 context: Point::new(0, 0)..Point::new(3, 0),
13059 primary: None,
13060 },
13061 ExcerptRange {
13062 context: Point::new(5, 0)..Point::new(7, 0),
13063 primary: None,
13064 },
13065 ExcerptRange {
13066 context: Point::new(9, 0)..Point::new(10, 3),
13067 primary: None,
13068 },
13069 ],
13070 cx,
13071 );
13072 multibuffer.push_excerpts(
13073 buffer_2.clone(),
13074 [
13075 ExcerptRange {
13076 context: Point::new(0, 0)..Point::new(3, 0),
13077 primary: None,
13078 },
13079 ExcerptRange {
13080 context: Point::new(5, 0)..Point::new(7, 0),
13081 primary: None,
13082 },
13083 ExcerptRange {
13084 context: Point::new(9, 0)..Point::new(10, 3),
13085 primary: None,
13086 },
13087 ],
13088 cx,
13089 );
13090 multibuffer.push_excerpts(
13091 buffer_3.clone(),
13092 [
13093 ExcerptRange {
13094 context: Point::new(0, 0)..Point::new(3, 0),
13095 primary: None,
13096 },
13097 ExcerptRange {
13098 context: Point::new(5, 0)..Point::new(7, 0),
13099 primary: None,
13100 },
13101 ExcerptRange {
13102 context: Point::new(9, 0)..Point::new(10, 3),
13103 primary: None,
13104 },
13105 ],
13106 cx,
13107 );
13108 multibuffer
13109 });
13110
13111 let editor = cx.add_window(|window, cx| {
13112 Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
13113 });
13114 editor
13115 .update(cx, |editor, _window, cx| {
13116 for (buffer, diff_base) in [
13117 (buffer_1.clone(), file_1_old),
13118 (buffer_2.clone(), file_2_old),
13119 (buffer_3.clone(), file_3_old),
13120 ] {
13121 let change_set = cx.new(|cx| {
13122 BufferChangeSet::new_with_base_text(diff_base.to_string(), &buffer, cx)
13123 });
13124 editor
13125 .buffer
13126 .update(cx, |buffer, cx| buffer.add_change_set(change_set, cx));
13127 }
13128 })
13129 .unwrap();
13130
13131 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13132 cx.run_until_parked();
13133
13134 cx.assert_editor_state(
13135 &"
13136 ˇaaa
13137 ccc
13138 ddd
13139
13140 ggg
13141 hhh
13142
13143
13144 lll
13145 mmm
13146 NNN
13147
13148 qqq
13149 rrr
13150
13151 uuu
13152 111
13153 222
13154 333
13155
13156 666
13157 777
13158
13159 000
13160 !!!"
13161 .unindent(),
13162 );
13163
13164 cx.update_editor(|editor, window, cx| {
13165 editor.select_all(&SelectAll, window, cx);
13166 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
13167 });
13168 cx.executor().run_until_parked();
13169
13170 cx.assert_state_with_diff(
13171 "
13172 «aaa
13173 - bbb
13174 ccc
13175 ddd
13176
13177 ggg
13178 hhh
13179
13180
13181 lll
13182 mmm
13183 - nnn
13184 + NNN
13185
13186 qqq
13187 rrr
13188
13189 uuu
13190 111
13191 222
13192 333
13193
13194 + 666
13195 777
13196
13197 000
13198 !!!ˇ»"
13199 .unindent(),
13200 );
13201}
13202
13203#[gpui::test]
13204async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext) {
13205 init_test(cx, |_| {});
13206
13207 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
13208 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\n";
13209
13210 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
13211 let multi_buffer = cx.new(|cx| {
13212 let mut multibuffer = MultiBuffer::new(ReadWrite);
13213 multibuffer.push_excerpts(
13214 buffer.clone(),
13215 [
13216 ExcerptRange {
13217 context: Point::new(0, 0)..Point::new(2, 0),
13218 primary: None,
13219 },
13220 ExcerptRange {
13221 context: Point::new(5, 0)..Point::new(7, 0),
13222 primary: None,
13223 },
13224 ],
13225 cx,
13226 );
13227 multibuffer
13228 });
13229
13230 let editor = cx.add_window(|window, cx| {
13231 Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
13232 });
13233 editor
13234 .update(cx, |editor, _window, cx| {
13235 let change_set =
13236 cx.new(|cx| BufferChangeSet::new_with_base_text(base.to_string(), &buffer, cx));
13237 editor
13238 .buffer
13239 .update(cx, |buffer, cx| buffer.add_change_set(change_set, cx))
13240 })
13241 .unwrap();
13242
13243 let mut cx = EditorTestContext::for_editor(editor, cx).await;
13244 cx.run_until_parked();
13245
13246 cx.update_editor(|editor, window, cx| {
13247 editor.expand_all_diff_hunks(&Default::default(), window, cx)
13248 });
13249 cx.executor().run_until_parked();
13250
13251 cx.assert_state_with_diff(
13252 "
13253 ˇaaa
13254 - bbb
13255 + BBB
13256
13257 + EEE
13258 fff
13259 "
13260 .unindent(),
13261 );
13262}
13263
13264#[gpui::test]
13265async fn test_edits_around_expanded_insertion_hunks(
13266 executor: BackgroundExecutor,
13267 cx: &mut gpui::TestAppContext,
13268) {
13269 init_test(cx, |_| {});
13270
13271 let mut cx = EditorTestContext::new(cx).await;
13272
13273 let diff_base = r#"
13274 use some::mod1;
13275 use some::mod2;
13276
13277 const A: u32 = 42;
13278
13279 fn main() {
13280 println!("hello");
13281
13282 println!("world");
13283 }
13284 "#
13285 .unindent();
13286 executor.run_until_parked();
13287 cx.set_state(
13288 &r#"
13289 use some::mod1;
13290 use some::mod2;
13291
13292 const A: u32 = 42;
13293 const B: u32 = 42;
13294 const C: u32 = 42;
13295 ˇ
13296
13297 fn main() {
13298 println!("hello");
13299
13300 println!("world");
13301 }
13302 "#
13303 .unindent(),
13304 );
13305
13306 cx.set_diff_base(&diff_base);
13307 executor.run_until_parked();
13308
13309 cx.update_editor(|editor, window, cx| {
13310 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13311 });
13312 executor.run_until_parked();
13313
13314 cx.assert_state_with_diff(
13315 r#"
13316 use some::mod1;
13317 use some::mod2;
13318
13319 const A: u32 = 42;
13320 + const B: u32 = 42;
13321 + const C: u32 = 42;
13322 + ˇ
13323
13324 fn main() {
13325 println!("hello");
13326
13327 println!("world");
13328 }
13329 "#
13330 .unindent(),
13331 );
13332
13333 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
13334 executor.run_until_parked();
13335
13336 cx.assert_state_with_diff(
13337 r#"
13338 use some::mod1;
13339 use some::mod2;
13340
13341 const A: u32 = 42;
13342 + const B: u32 = 42;
13343 + const C: u32 = 42;
13344 + const D: u32 = 42;
13345 + ˇ
13346
13347 fn main() {
13348 println!("hello");
13349
13350 println!("world");
13351 }
13352 "#
13353 .unindent(),
13354 );
13355
13356 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
13357 executor.run_until_parked();
13358
13359 cx.assert_state_with_diff(
13360 r#"
13361 use some::mod1;
13362 use some::mod2;
13363
13364 const A: u32 = 42;
13365 + const B: u32 = 42;
13366 + const C: u32 = 42;
13367 + const D: u32 = 42;
13368 + const E: u32 = 42;
13369 + ˇ
13370
13371 fn main() {
13372 println!("hello");
13373
13374 println!("world");
13375 }
13376 "#
13377 .unindent(),
13378 );
13379
13380 cx.update_editor(|editor, window, cx| {
13381 editor.delete_line(&DeleteLine, window, cx);
13382 });
13383 executor.run_until_parked();
13384
13385 cx.assert_state_with_diff(
13386 r#"
13387 use some::mod1;
13388 use some::mod2;
13389
13390 const A: u32 = 42;
13391 + const B: u32 = 42;
13392 + const C: u32 = 42;
13393 + const D: u32 = 42;
13394 + const E: u32 = 42;
13395 ˇ
13396 fn main() {
13397 println!("hello");
13398
13399 println!("world");
13400 }
13401 "#
13402 .unindent(),
13403 );
13404
13405 cx.update_editor(|editor, window, cx| {
13406 editor.move_up(&MoveUp, window, cx);
13407 editor.delete_line(&DeleteLine, window, cx);
13408 editor.move_up(&MoveUp, window, cx);
13409 editor.delete_line(&DeleteLine, window, cx);
13410 editor.move_up(&MoveUp, window, cx);
13411 editor.delete_line(&DeleteLine, window, cx);
13412 });
13413 executor.run_until_parked();
13414 cx.assert_state_with_diff(
13415 r#"
13416 use some::mod1;
13417 use some::mod2;
13418
13419 const A: u32 = 42;
13420 + const B: u32 = 42;
13421 ˇ
13422 fn main() {
13423 println!("hello");
13424
13425 println!("world");
13426 }
13427 "#
13428 .unindent(),
13429 );
13430
13431 cx.update_editor(|editor, window, cx| {
13432 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
13433 editor.delete_line(&DeleteLine, window, cx);
13434 });
13435 executor.run_until_parked();
13436 cx.assert_state_with_diff(
13437 r#"
13438 ˇ
13439 fn main() {
13440 println!("hello");
13441
13442 println!("world");
13443 }
13444 "#
13445 .unindent(),
13446 );
13447}
13448
13449#[gpui::test]
13450async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
13451 init_test(cx, |_| {});
13452
13453 let mut cx = EditorTestContext::new(cx).await;
13454 cx.set_diff_base(indoc! { "
13455 one
13456 two
13457 three
13458 four
13459 five
13460 "
13461 });
13462 cx.set_state(indoc! { "
13463 one
13464 ˇthree
13465 five
13466 "});
13467 cx.run_until_parked();
13468 cx.update_editor(|editor, window, cx| {
13469 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13470 });
13471 cx.assert_state_with_diff(
13472 indoc! { "
13473 one
13474 - two
13475 ˇthree
13476 - four
13477 five
13478 "}
13479 .to_string(),
13480 );
13481 cx.update_editor(|editor, window, cx| {
13482 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13483 });
13484
13485 cx.assert_state_with_diff(
13486 indoc! { "
13487 one
13488 ˇthree
13489 five
13490 "}
13491 .to_string(),
13492 );
13493
13494 cx.set_state(indoc! { "
13495 one
13496 TWO
13497 ˇthree
13498 four
13499 five
13500 "});
13501 cx.run_until_parked();
13502 cx.update_editor(|editor, window, cx| {
13503 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13504 });
13505
13506 cx.assert_state_with_diff(
13507 indoc! { "
13508 one
13509 - two
13510 + TWO
13511 ˇthree
13512 four
13513 five
13514 "}
13515 .to_string(),
13516 );
13517 cx.update_editor(|editor, window, cx| {
13518 editor.move_up(&Default::default(), window, cx);
13519 editor.move_up(&Default::default(), window, cx);
13520 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
13521 });
13522 cx.assert_state_with_diff(
13523 indoc! { "
13524 one
13525 ˇTWO
13526 three
13527 four
13528 five
13529 "}
13530 .to_string(),
13531 );
13532}
13533
13534#[gpui::test]
13535async fn test_edits_around_expanded_deletion_hunks(
13536 executor: BackgroundExecutor,
13537 cx: &mut gpui::TestAppContext,
13538) {
13539 init_test(cx, |_| {});
13540
13541 let mut cx = EditorTestContext::new(cx).await;
13542
13543 let diff_base = r#"
13544 use some::mod1;
13545 use some::mod2;
13546
13547 const A: u32 = 42;
13548 const B: u32 = 42;
13549 const C: u32 = 42;
13550
13551
13552 fn main() {
13553 println!("hello");
13554
13555 println!("world");
13556 }
13557 "#
13558 .unindent();
13559 executor.run_until_parked();
13560 cx.set_state(
13561 &r#"
13562 use some::mod1;
13563 use some::mod2;
13564
13565 ˇconst B: u32 = 42;
13566 const C: u32 = 42;
13567
13568
13569 fn main() {
13570 println!("hello");
13571
13572 println!("world");
13573 }
13574 "#
13575 .unindent(),
13576 );
13577
13578 cx.set_diff_base(&diff_base);
13579 executor.run_until_parked();
13580
13581 cx.update_editor(|editor, window, cx| {
13582 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13583 });
13584 executor.run_until_parked();
13585
13586 cx.assert_state_with_diff(
13587 r#"
13588 use some::mod1;
13589 use some::mod2;
13590
13591 - const A: u32 = 42;
13592 ˇconst B: u32 = 42;
13593 const C: u32 = 42;
13594
13595
13596 fn main() {
13597 println!("hello");
13598
13599 println!("world");
13600 }
13601 "#
13602 .unindent(),
13603 );
13604
13605 cx.update_editor(|editor, window, cx| {
13606 editor.delete_line(&DeleteLine, window, cx);
13607 });
13608 executor.run_until_parked();
13609 cx.assert_state_with_diff(
13610 r#"
13611 use some::mod1;
13612 use some::mod2;
13613
13614 - const A: u32 = 42;
13615 - const B: u32 = 42;
13616 ˇconst C: u32 = 42;
13617
13618
13619 fn main() {
13620 println!("hello");
13621
13622 println!("world");
13623 }
13624 "#
13625 .unindent(),
13626 );
13627
13628 cx.update_editor(|editor, window, cx| {
13629 editor.delete_line(&DeleteLine, window, cx);
13630 });
13631 executor.run_until_parked();
13632 cx.assert_state_with_diff(
13633 r#"
13634 use some::mod1;
13635 use some::mod2;
13636
13637 - const A: u32 = 42;
13638 - const B: u32 = 42;
13639 - const C: u32 = 42;
13640 ˇ
13641
13642 fn main() {
13643 println!("hello");
13644
13645 println!("world");
13646 }
13647 "#
13648 .unindent(),
13649 );
13650
13651 cx.update_editor(|editor, window, cx| {
13652 editor.handle_input("replacement", window, cx);
13653 });
13654 executor.run_until_parked();
13655 cx.assert_state_with_diff(
13656 r#"
13657 use some::mod1;
13658 use some::mod2;
13659
13660 - const A: u32 = 42;
13661 - const B: u32 = 42;
13662 - const C: u32 = 42;
13663 -
13664 + replacementˇ
13665
13666 fn main() {
13667 println!("hello");
13668
13669 println!("world");
13670 }
13671 "#
13672 .unindent(),
13673 );
13674}
13675
13676#[gpui::test]
13677async fn test_backspace_after_deletion_hunk(
13678 executor: BackgroundExecutor,
13679 cx: &mut gpui::TestAppContext,
13680) {
13681 init_test(cx, |_| {});
13682
13683 let mut cx = EditorTestContext::new(cx).await;
13684
13685 let base_text = r#"
13686 one
13687 two
13688 three
13689 four
13690 five
13691 "#
13692 .unindent();
13693 executor.run_until_parked();
13694 cx.set_state(
13695 &r#"
13696 one
13697 two
13698 fˇour
13699 five
13700 "#
13701 .unindent(),
13702 );
13703
13704 cx.set_diff_base(&base_text);
13705 executor.run_until_parked();
13706
13707 cx.update_editor(|editor, window, cx| {
13708 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13709 });
13710 executor.run_until_parked();
13711
13712 cx.assert_state_with_diff(
13713 r#"
13714 one
13715 two
13716 - three
13717 fˇour
13718 five
13719 "#
13720 .unindent(),
13721 );
13722
13723 cx.update_editor(|editor, window, cx| {
13724 editor.backspace(&Backspace, window, cx);
13725 editor.backspace(&Backspace, window, cx);
13726 });
13727 executor.run_until_parked();
13728 cx.assert_state_with_diff(
13729 r#"
13730 one
13731 two
13732 - threeˇ
13733 - four
13734 + our
13735 five
13736 "#
13737 .unindent(),
13738 );
13739}
13740
13741#[gpui::test]
13742async fn test_edit_after_expanded_modification_hunk(
13743 executor: BackgroundExecutor,
13744 cx: &mut gpui::TestAppContext,
13745) {
13746 init_test(cx, |_| {});
13747
13748 let mut cx = EditorTestContext::new(cx).await;
13749
13750 let diff_base = r#"
13751 use some::mod1;
13752 use some::mod2;
13753
13754 const A: u32 = 42;
13755 const B: u32 = 42;
13756 const C: u32 = 42;
13757 const D: u32 = 42;
13758
13759
13760 fn main() {
13761 println!("hello");
13762
13763 println!("world");
13764 }"#
13765 .unindent();
13766
13767 cx.set_state(
13768 &r#"
13769 use some::mod1;
13770 use some::mod2;
13771
13772 const A: u32 = 42;
13773 const B: u32 = 42;
13774 const C: u32 = 43ˇ
13775 const D: u32 = 42;
13776
13777
13778 fn main() {
13779 println!("hello");
13780
13781 println!("world");
13782 }"#
13783 .unindent(),
13784 );
13785
13786 cx.set_diff_base(&diff_base);
13787 executor.run_until_parked();
13788 cx.update_editor(|editor, window, cx| {
13789 editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, window, cx);
13790 });
13791 executor.run_until_parked();
13792
13793 cx.assert_state_with_diff(
13794 r#"
13795 use some::mod1;
13796 use some::mod2;
13797
13798 const A: u32 = 42;
13799 const B: u32 = 42;
13800 - const C: u32 = 42;
13801 + const C: u32 = 43ˇ
13802 const D: u32 = 42;
13803
13804
13805 fn main() {
13806 println!("hello");
13807
13808 println!("world");
13809 }"#
13810 .unindent(),
13811 );
13812
13813 cx.update_editor(|editor, window, cx| {
13814 editor.handle_input("\nnew_line\n", window, cx);
13815 });
13816 executor.run_until_parked();
13817
13818 cx.assert_state_with_diff(
13819 r#"
13820 use some::mod1;
13821 use some::mod2;
13822
13823 const A: u32 = 42;
13824 const B: u32 = 42;
13825 - const C: u32 = 42;
13826 + const C: u32 = 43
13827 + new_line
13828 + ˇ
13829 const D: u32 = 42;
13830
13831
13832 fn main() {
13833 println!("hello");
13834
13835 println!("world");
13836 }"#
13837 .unindent(),
13838 );
13839}
13840
13841async fn setup_indent_guides_editor(
13842 text: &str,
13843 cx: &mut gpui::TestAppContext,
13844) -> (BufferId, EditorTestContext) {
13845 init_test(cx, |_| {});
13846
13847 let mut cx = EditorTestContext::new(cx).await;
13848
13849 let buffer_id = cx.update_editor(|editor, window, cx| {
13850 editor.set_text(text, window, cx);
13851 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
13852
13853 buffer_ids[0]
13854 });
13855
13856 (buffer_id, cx)
13857}
13858
13859fn assert_indent_guides(
13860 range: Range<u32>,
13861 expected: Vec<IndentGuide>,
13862 active_indices: Option<Vec<usize>>,
13863 cx: &mut EditorTestContext,
13864) {
13865 let indent_guides = cx.update_editor(|editor, window, cx| {
13866 let snapshot = editor.snapshot(window, cx).display_snapshot;
13867 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
13868 editor,
13869 MultiBufferRow(range.start)..MultiBufferRow(range.end),
13870 true,
13871 &snapshot,
13872 cx,
13873 );
13874
13875 indent_guides.sort_by(|a, b| {
13876 a.depth.cmp(&b.depth).then(
13877 a.start_row
13878 .cmp(&b.start_row)
13879 .then(a.end_row.cmp(&b.end_row)),
13880 )
13881 });
13882 indent_guides
13883 });
13884
13885 if let Some(expected) = active_indices {
13886 let active_indices = cx.update_editor(|editor, window, cx| {
13887 let snapshot = editor.snapshot(window, cx).display_snapshot;
13888 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
13889 });
13890
13891 assert_eq!(
13892 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
13893 expected,
13894 "Active indent guide indices do not match"
13895 );
13896 }
13897
13898 assert_eq!(indent_guides, expected, "Indent guides do not match");
13899}
13900
13901fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
13902 IndentGuide {
13903 buffer_id,
13904 start_row: MultiBufferRow(start_row),
13905 end_row: MultiBufferRow(end_row),
13906 depth,
13907 tab_size: 4,
13908 settings: IndentGuideSettings {
13909 enabled: true,
13910 line_width: 1,
13911 active_line_width: 1,
13912 ..Default::default()
13913 },
13914 }
13915}
13916
13917#[gpui::test]
13918async fn test_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
13919 let (buffer_id, mut cx) = setup_indent_guides_editor(
13920 &"
13921 fn main() {
13922 let a = 1;
13923 }"
13924 .unindent(),
13925 cx,
13926 )
13927 .await;
13928
13929 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
13930}
13931
13932#[gpui::test]
13933async fn test_indent_guide_simple_block(cx: &mut gpui::TestAppContext) {
13934 let (buffer_id, mut cx) = setup_indent_guides_editor(
13935 &"
13936 fn main() {
13937 let a = 1;
13938 let b = 2;
13939 }"
13940 .unindent(),
13941 cx,
13942 )
13943 .await;
13944
13945 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
13946}
13947
13948#[gpui::test]
13949async fn test_indent_guide_nested(cx: &mut gpui::TestAppContext) {
13950 let (buffer_id, mut cx) = setup_indent_guides_editor(
13951 &"
13952 fn main() {
13953 let a = 1;
13954 if a == 3 {
13955 let b = 2;
13956 } else {
13957 let c = 3;
13958 }
13959 }"
13960 .unindent(),
13961 cx,
13962 )
13963 .await;
13964
13965 assert_indent_guides(
13966 0..8,
13967 vec![
13968 indent_guide(buffer_id, 1, 6, 0),
13969 indent_guide(buffer_id, 3, 3, 1),
13970 indent_guide(buffer_id, 5, 5, 1),
13971 ],
13972 None,
13973 &mut cx,
13974 );
13975}
13976
13977#[gpui::test]
13978async fn test_indent_guide_tab(cx: &mut gpui::TestAppContext) {
13979 let (buffer_id, mut cx) = setup_indent_guides_editor(
13980 &"
13981 fn main() {
13982 let a = 1;
13983 let b = 2;
13984 let c = 3;
13985 }"
13986 .unindent(),
13987 cx,
13988 )
13989 .await;
13990
13991 assert_indent_guides(
13992 0..5,
13993 vec![
13994 indent_guide(buffer_id, 1, 3, 0),
13995 indent_guide(buffer_id, 2, 2, 1),
13996 ],
13997 None,
13998 &mut cx,
13999 );
14000}
14001
14002#[gpui::test]
14003async fn test_indent_guide_continues_on_empty_line(cx: &mut gpui::TestAppContext) {
14004 let (buffer_id, mut cx) = setup_indent_guides_editor(
14005 &"
14006 fn main() {
14007 let a = 1;
14008
14009 let c = 3;
14010 }"
14011 .unindent(),
14012 cx,
14013 )
14014 .await;
14015
14016 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
14017}
14018
14019#[gpui::test]
14020async fn test_indent_guide_complex(cx: &mut gpui::TestAppContext) {
14021 let (buffer_id, mut cx) = setup_indent_guides_editor(
14022 &"
14023 fn main() {
14024 let a = 1;
14025
14026 let c = 3;
14027
14028 if a == 3 {
14029 let b = 2;
14030 } else {
14031 let c = 3;
14032 }
14033 }"
14034 .unindent(),
14035 cx,
14036 )
14037 .await;
14038
14039 assert_indent_guides(
14040 0..11,
14041 vec![
14042 indent_guide(buffer_id, 1, 9, 0),
14043 indent_guide(buffer_id, 6, 6, 1),
14044 indent_guide(buffer_id, 8, 8, 1),
14045 ],
14046 None,
14047 &mut cx,
14048 );
14049}
14050
14051#[gpui::test]
14052async fn test_indent_guide_starts_off_screen(cx: &mut gpui::TestAppContext) {
14053 let (buffer_id, mut cx) = setup_indent_guides_editor(
14054 &"
14055 fn main() {
14056 let a = 1;
14057
14058 let c = 3;
14059
14060 if a == 3 {
14061 let b = 2;
14062 } else {
14063 let c = 3;
14064 }
14065 }"
14066 .unindent(),
14067 cx,
14068 )
14069 .await;
14070
14071 assert_indent_guides(
14072 1..11,
14073 vec![
14074 indent_guide(buffer_id, 1, 9, 0),
14075 indent_guide(buffer_id, 6, 6, 1),
14076 indent_guide(buffer_id, 8, 8, 1),
14077 ],
14078 None,
14079 &mut cx,
14080 );
14081}
14082
14083#[gpui::test]
14084async fn test_indent_guide_ends_off_screen(cx: &mut gpui::TestAppContext) {
14085 let (buffer_id, mut cx) = setup_indent_guides_editor(
14086 &"
14087 fn main() {
14088 let a = 1;
14089
14090 let c = 3;
14091
14092 if a == 3 {
14093 let b = 2;
14094 } else {
14095 let c = 3;
14096 }
14097 }"
14098 .unindent(),
14099 cx,
14100 )
14101 .await;
14102
14103 assert_indent_guides(
14104 1..10,
14105 vec![
14106 indent_guide(buffer_id, 1, 9, 0),
14107 indent_guide(buffer_id, 6, 6, 1),
14108 indent_guide(buffer_id, 8, 8, 1),
14109 ],
14110 None,
14111 &mut cx,
14112 );
14113}
14114
14115#[gpui::test]
14116async fn test_indent_guide_without_brackets(cx: &mut gpui::TestAppContext) {
14117 let (buffer_id, mut cx) = setup_indent_guides_editor(
14118 &"
14119 block1
14120 block2
14121 block3
14122 block4
14123 block2
14124 block1
14125 block1"
14126 .unindent(),
14127 cx,
14128 )
14129 .await;
14130
14131 assert_indent_guides(
14132 1..10,
14133 vec![
14134 indent_guide(buffer_id, 1, 4, 0),
14135 indent_guide(buffer_id, 2, 3, 1),
14136 indent_guide(buffer_id, 3, 3, 2),
14137 ],
14138 None,
14139 &mut cx,
14140 );
14141}
14142
14143#[gpui::test]
14144async fn test_indent_guide_ends_before_empty_line(cx: &mut gpui::TestAppContext) {
14145 let (buffer_id, mut cx) = setup_indent_guides_editor(
14146 &"
14147 block1
14148 block2
14149 block3
14150
14151 block1
14152 block1"
14153 .unindent(),
14154 cx,
14155 )
14156 .await;
14157
14158 assert_indent_guides(
14159 0..6,
14160 vec![
14161 indent_guide(buffer_id, 1, 2, 0),
14162 indent_guide(buffer_id, 2, 2, 1),
14163 ],
14164 None,
14165 &mut cx,
14166 );
14167}
14168
14169#[gpui::test]
14170async fn test_indent_guide_continuing_off_screen(cx: &mut gpui::TestAppContext) {
14171 let (buffer_id, mut cx) = setup_indent_guides_editor(
14172 &"
14173 block1
14174
14175
14176
14177 block2
14178 "
14179 .unindent(),
14180 cx,
14181 )
14182 .await;
14183
14184 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
14185}
14186
14187#[gpui::test]
14188async fn test_indent_guide_tabs(cx: &mut gpui::TestAppContext) {
14189 let (buffer_id, mut cx) = setup_indent_guides_editor(
14190 &"
14191 def a:
14192 \tb = 3
14193 \tif True:
14194 \t\tc = 4
14195 \t\td = 5
14196 \tprint(b)
14197 "
14198 .unindent(),
14199 cx,
14200 )
14201 .await;
14202
14203 assert_indent_guides(
14204 0..6,
14205 vec![
14206 indent_guide(buffer_id, 1, 6, 0),
14207 indent_guide(buffer_id, 3, 4, 1),
14208 ],
14209 None,
14210 &mut cx,
14211 );
14212}
14213
14214#[gpui::test]
14215async fn test_active_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
14216 let (buffer_id, mut cx) = setup_indent_guides_editor(
14217 &"
14218 fn main() {
14219 let a = 1;
14220 }"
14221 .unindent(),
14222 cx,
14223 )
14224 .await;
14225
14226 cx.update_editor(|editor, window, cx| {
14227 editor.change_selections(None, window, cx, |s| {
14228 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14229 });
14230 });
14231
14232 assert_indent_guides(
14233 0..3,
14234 vec![indent_guide(buffer_id, 1, 1, 0)],
14235 Some(vec![0]),
14236 &mut cx,
14237 );
14238}
14239
14240#[gpui::test]
14241async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppContext) {
14242 let (buffer_id, mut cx) = setup_indent_guides_editor(
14243 &"
14244 fn main() {
14245 if 1 == 2 {
14246 let a = 1;
14247 }
14248 }"
14249 .unindent(),
14250 cx,
14251 )
14252 .await;
14253
14254 cx.update_editor(|editor, window, cx| {
14255 editor.change_selections(None, window, cx, |s| {
14256 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14257 });
14258 });
14259
14260 assert_indent_guides(
14261 0..4,
14262 vec![
14263 indent_guide(buffer_id, 1, 3, 0),
14264 indent_guide(buffer_id, 2, 2, 1),
14265 ],
14266 Some(vec![1]),
14267 &mut cx,
14268 );
14269
14270 cx.update_editor(|editor, window, cx| {
14271 editor.change_selections(None, window, cx, |s| {
14272 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
14273 });
14274 });
14275
14276 assert_indent_guides(
14277 0..4,
14278 vec![
14279 indent_guide(buffer_id, 1, 3, 0),
14280 indent_guide(buffer_id, 2, 2, 1),
14281 ],
14282 Some(vec![1]),
14283 &mut cx,
14284 );
14285
14286 cx.update_editor(|editor, window, cx| {
14287 editor.change_selections(None, window, cx, |s| {
14288 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
14289 });
14290 });
14291
14292 assert_indent_guides(
14293 0..4,
14294 vec![
14295 indent_guide(buffer_id, 1, 3, 0),
14296 indent_guide(buffer_id, 2, 2, 1),
14297 ],
14298 Some(vec![0]),
14299 &mut cx,
14300 );
14301}
14302
14303#[gpui::test]
14304async fn test_active_indent_guide_empty_line(cx: &mut gpui::TestAppContext) {
14305 let (buffer_id, mut cx) = setup_indent_guides_editor(
14306 &"
14307 fn main() {
14308 let a = 1;
14309
14310 let b = 2;
14311 }"
14312 .unindent(),
14313 cx,
14314 )
14315 .await;
14316
14317 cx.update_editor(|editor, window, cx| {
14318 editor.change_selections(None, window, cx, |s| {
14319 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
14320 });
14321 });
14322
14323 assert_indent_guides(
14324 0..5,
14325 vec![indent_guide(buffer_id, 1, 3, 0)],
14326 Some(vec![0]),
14327 &mut cx,
14328 );
14329}
14330
14331#[gpui::test]
14332async fn test_active_indent_guide_non_matching_indent(cx: &mut gpui::TestAppContext) {
14333 let (buffer_id, mut cx) = setup_indent_guides_editor(
14334 &"
14335 def m:
14336 a = 1
14337 pass"
14338 .unindent(),
14339 cx,
14340 )
14341 .await;
14342
14343 cx.update_editor(|editor, window, cx| {
14344 editor.change_selections(None, window, cx, |s| {
14345 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
14346 });
14347 });
14348
14349 assert_indent_guides(
14350 0..3,
14351 vec![indent_guide(buffer_id, 1, 2, 0)],
14352 Some(vec![0]),
14353 &mut cx,
14354 );
14355}
14356
14357#[gpui::test]
14358async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut gpui::TestAppContext) {
14359 init_test(cx, |_| {});
14360 let mut cx = EditorTestContext::new(cx).await;
14361 let text = indoc! {
14362 "
14363 impl A {
14364 fn b() {
14365 0;
14366 3;
14367 5;
14368 6;
14369 7;
14370 }
14371 }
14372 "
14373 };
14374 let base_text = indoc! {
14375 "
14376 impl A {
14377 fn b() {
14378 0;
14379 1;
14380 2;
14381 3;
14382 4;
14383 }
14384 fn c() {
14385 5;
14386 6;
14387 7;
14388 }
14389 }
14390 "
14391 };
14392
14393 cx.update_editor(|editor, window, cx| {
14394 editor.set_text(text, window, cx);
14395
14396 editor.buffer().update(cx, |multibuffer, cx| {
14397 let buffer = multibuffer.as_singleton().unwrap();
14398 let change_set = cx.new(|cx| {
14399 let mut change_set = BufferChangeSet::new(&buffer, cx);
14400 let _ =
14401 change_set.set_base_text(base_text.into(), buffer.read(cx).text_snapshot(), cx);
14402 change_set
14403 });
14404
14405 multibuffer.set_all_diff_hunks_expanded(cx);
14406 multibuffer.add_change_set(change_set, cx);
14407
14408 buffer.read(cx).remote_id()
14409 })
14410 });
14411 cx.run_until_parked();
14412
14413 cx.assert_state_with_diff(
14414 indoc! { "
14415 impl A {
14416 fn b() {
14417 0;
14418 - 1;
14419 - 2;
14420 3;
14421 - 4;
14422 - }
14423 - fn c() {
14424 5;
14425 6;
14426 7;
14427 }
14428 }
14429 ˇ"
14430 }
14431 .to_string(),
14432 );
14433
14434 let mut actual_guides = cx.update_editor(|editor, window, cx| {
14435 editor
14436 .snapshot(window, cx)
14437 .buffer_snapshot
14438 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
14439 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
14440 .collect::<Vec<_>>()
14441 });
14442 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
14443 assert_eq!(
14444 actual_guides,
14445 vec![
14446 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
14447 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
14448 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
14449 ]
14450 );
14451}
14452
14453#[gpui::test]
14454fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
14455 init_test(cx, |_| {});
14456
14457 let editor = cx.add_window(|window, cx| {
14458 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
14459 build_editor(buffer, window, cx)
14460 });
14461
14462 let render_args = Arc::new(Mutex::new(None));
14463 let snapshot = editor
14464 .update(cx, |editor, window, cx| {
14465 let snapshot = editor.buffer().read(cx).snapshot(cx);
14466 let range =
14467 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
14468
14469 struct RenderArgs {
14470 row: MultiBufferRow,
14471 folded: bool,
14472 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
14473 }
14474
14475 let crease = Crease::inline(
14476 range,
14477 FoldPlaceholder::test(),
14478 {
14479 let toggle_callback = render_args.clone();
14480 move |row, folded, callback, _window, _cx| {
14481 *toggle_callback.lock() = Some(RenderArgs {
14482 row,
14483 folded,
14484 callback,
14485 });
14486 div()
14487 }
14488 },
14489 |_row, _folded, _window, _cx| div(),
14490 );
14491
14492 editor.insert_creases(Some(crease), cx);
14493 let snapshot = editor.snapshot(window, cx);
14494 let _div = snapshot.render_crease_toggle(
14495 MultiBufferRow(1),
14496 false,
14497 cx.entity().clone(),
14498 window,
14499 cx,
14500 );
14501 snapshot
14502 })
14503 .unwrap();
14504
14505 let render_args = render_args.lock().take().unwrap();
14506 assert_eq!(render_args.row, MultiBufferRow(1));
14507 assert!(!render_args.folded);
14508 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
14509
14510 cx.update_window(*editor, |_, window, cx| {
14511 (render_args.callback)(true, window, cx)
14512 })
14513 .unwrap();
14514 let snapshot = editor
14515 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
14516 .unwrap();
14517 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
14518
14519 cx.update_window(*editor, |_, window, cx| {
14520 (render_args.callback)(false, window, cx)
14521 })
14522 .unwrap();
14523 let snapshot = editor
14524 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
14525 .unwrap();
14526 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
14527}
14528
14529#[gpui::test]
14530async fn test_input_text(cx: &mut gpui::TestAppContext) {
14531 init_test(cx, |_| {});
14532 let mut cx = EditorTestContext::new(cx).await;
14533
14534 cx.set_state(
14535 &r#"ˇone
14536 two
14537
14538 three
14539 fourˇ
14540 five
14541
14542 siˇx"#
14543 .unindent(),
14544 );
14545
14546 cx.dispatch_action(HandleInput(String::new()));
14547 cx.assert_editor_state(
14548 &r#"ˇone
14549 two
14550
14551 three
14552 fourˇ
14553 five
14554
14555 siˇx"#
14556 .unindent(),
14557 );
14558
14559 cx.dispatch_action(HandleInput("AAAA".to_string()));
14560 cx.assert_editor_state(
14561 &r#"AAAAˇone
14562 two
14563
14564 three
14565 fourAAAAˇ
14566 five
14567
14568 siAAAAˇx"#
14569 .unindent(),
14570 );
14571}
14572
14573#[gpui::test]
14574async fn test_scroll_cursor_center_top_bottom(cx: &mut gpui::TestAppContext) {
14575 init_test(cx, |_| {});
14576
14577 let mut cx = EditorTestContext::new(cx).await;
14578 cx.set_state(
14579 r#"let foo = 1;
14580let foo = 2;
14581let foo = 3;
14582let fooˇ = 4;
14583let foo = 5;
14584let foo = 6;
14585let foo = 7;
14586let foo = 8;
14587let foo = 9;
14588let foo = 10;
14589let foo = 11;
14590let foo = 12;
14591let foo = 13;
14592let foo = 14;
14593let foo = 15;"#,
14594 );
14595
14596 cx.update_editor(|e, window, cx| {
14597 assert_eq!(
14598 e.next_scroll_position,
14599 NextScrollCursorCenterTopBottom::Center,
14600 "Default next scroll direction is center",
14601 );
14602
14603 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14604 assert_eq!(
14605 e.next_scroll_position,
14606 NextScrollCursorCenterTopBottom::Top,
14607 "After center, next scroll direction should be top",
14608 );
14609
14610 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14611 assert_eq!(
14612 e.next_scroll_position,
14613 NextScrollCursorCenterTopBottom::Bottom,
14614 "After top, next scroll direction should be bottom",
14615 );
14616
14617 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14618 assert_eq!(
14619 e.next_scroll_position,
14620 NextScrollCursorCenterTopBottom::Center,
14621 "After bottom, scrolling should start over",
14622 );
14623
14624 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
14625 assert_eq!(
14626 e.next_scroll_position,
14627 NextScrollCursorCenterTopBottom::Top,
14628 "Scrolling continues if retriggered fast enough"
14629 );
14630 });
14631
14632 cx.executor()
14633 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
14634 cx.executor().run_until_parked();
14635 cx.update_editor(|e, _, _| {
14636 assert_eq!(
14637 e.next_scroll_position,
14638 NextScrollCursorCenterTopBottom::Center,
14639 "If scrolling is not triggered fast enough, it should reset"
14640 );
14641 });
14642}
14643
14644#[gpui::test]
14645async fn test_goto_definition_with_find_all_references_fallback(cx: &mut gpui::TestAppContext) {
14646 init_test(cx, |_| {});
14647 let mut cx = EditorLspTestContext::new_rust(
14648 lsp::ServerCapabilities {
14649 definition_provider: Some(lsp::OneOf::Left(true)),
14650 references_provider: Some(lsp::OneOf::Left(true)),
14651 ..lsp::ServerCapabilities::default()
14652 },
14653 cx,
14654 )
14655 .await;
14656
14657 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
14658 let go_to_definition = cx.lsp.handle_request::<lsp::request::GotoDefinition, _, _>(
14659 move |params, _| async move {
14660 if empty_go_to_definition {
14661 Ok(None)
14662 } else {
14663 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
14664 uri: params.text_document_position_params.text_document.uri,
14665 range: lsp::Range::new(lsp::Position::new(4, 3), lsp::Position::new(4, 6)),
14666 })))
14667 }
14668 },
14669 );
14670 let references =
14671 cx.lsp
14672 .handle_request::<lsp::request::References, _, _>(move |params, _| async move {
14673 Ok(Some(vec![lsp::Location {
14674 uri: params.text_document_position.text_document.uri,
14675 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
14676 }]))
14677 });
14678 (go_to_definition, references)
14679 };
14680
14681 cx.set_state(
14682 &r#"fn one() {
14683 let mut a = ˇtwo();
14684 }
14685
14686 fn two() {}"#
14687 .unindent(),
14688 );
14689 set_up_lsp_handlers(false, &mut cx);
14690 let navigated = cx
14691 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
14692 .await
14693 .expect("Failed to navigate to definition");
14694 assert_eq!(
14695 navigated,
14696 Navigated::Yes,
14697 "Should have navigated to definition from the GetDefinition response"
14698 );
14699 cx.assert_editor_state(
14700 &r#"fn one() {
14701 let mut a = two();
14702 }
14703
14704 fn «twoˇ»() {}"#
14705 .unindent(),
14706 );
14707
14708 let editors = cx.update_workspace(|workspace, _, cx| {
14709 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
14710 });
14711 cx.update_editor(|_, _, test_editor_cx| {
14712 assert_eq!(
14713 editors.len(),
14714 1,
14715 "Initially, only one, test, editor should be open in the workspace"
14716 );
14717 assert_eq!(
14718 test_editor_cx.entity(),
14719 editors.last().expect("Asserted len is 1").clone()
14720 );
14721 });
14722
14723 set_up_lsp_handlers(true, &mut cx);
14724 let navigated = cx
14725 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
14726 .await
14727 .expect("Failed to navigate to lookup references");
14728 assert_eq!(
14729 navigated,
14730 Navigated::Yes,
14731 "Should have navigated to references as a fallback after empty GoToDefinition response"
14732 );
14733 // We should not change the selections in the existing file,
14734 // if opening another milti buffer with the references
14735 cx.assert_editor_state(
14736 &r#"fn one() {
14737 let mut a = two();
14738 }
14739
14740 fn «twoˇ»() {}"#
14741 .unindent(),
14742 );
14743 let editors = cx.update_workspace(|workspace, _, cx| {
14744 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
14745 });
14746 cx.update_editor(|_, _, test_editor_cx| {
14747 assert_eq!(
14748 editors.len(),
14749 2,
14750 "After falling back to references search, we open a new editor with the results"
14751 );
14752 let references_fallback_text = editors
14753 .into_iter()
14754 .find(|new_editor| *new_editor != test_editor_cx.entity())
14755 .expect("Should have one non-test editor now")
14756 .read(test_editor_cx)
14757 .text(test_editor_cx);
14758 assert_eq!(
14759 references_fallback_text, "fn one() {\n let mut a = two();\n}",
14760 "Should use the range from the references response and not the GoToDefinition one"
14761 );
14762 });
14763}
14764
14765#[gpui::test]
14766async fn test_find_enclosing_node_with_task(cx: &mut gpui::TestAppContext) {
14767 init_test(cx, |_| {});
14768
14769 let language = Arc::new(Language::new(
14770 LanguageConfig::default(),
14771 Some(tree_sitter_rust::LANGUAGE.into()),
14772 ));
14773
14774 let text = r#"
14775 #[cfg(test)]
14776 mod tests() {
14777 #[test]
14778 fn runnable_1() {
14779 let a = 1;
14780 }
14781
14782 #[test]
14783 fn runnable_2() {
14784 let a = 1;
14785 let b = 2;
14786 }
14787 }
14788 "#
14789 .unindent();
14790
14791 let fs = FakeFs::new(cx.executor());
14792 fs.insert_file("/file.rs", Default::default()).await;
14793
14794 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14795 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14796 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14797 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14798 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
14799
14800 let editor = cx.new_window_entity(|window, cx| {
14801 Editor::new(
14802 EditorMode::Full,
14803 multi_buffer,
14804 Some(project.clone()),
14805 true,
14806 window,
14807 cx,
14808 )
14809 });
14810
14811 editor.update_in(cx, |editor, window, cx| {
14812 editor.tasks.insert(
14813 (buffer.read(cx).remote_id(), 3),
14814 RunnableTasks {
14815 templates: vec![],
14816 offset: MultiBufferOffset(43),
14817 column: 0,
14818 extra_variables: HashMap::default(),
14819 context_range: BufferOffset(43)..BufferOffset(85),
14820 },
14821 );
14822 editor.tasks.insert(
14823 (buffer.read(cx).remote_id(), 8),
14824 RunnableTasks {
14825 templates: vec![],
14826 offset: MultiBufferOffset(86),
14827 column: 0,
14828 extra_variables: HashMap::default(),
14829 context_range: BufferOffset(86)..BufferOffset(191),
14830 },
14831 );
14832
14833 // Test finding task when cursor is inside function body
14834 editor.change_selections(None, window, cx, |s| {
14835 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
14836 });
14837 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
14838 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
14839
14840 // Test finding task when cursor is on function name
14841 editor.change_selections(None, window, cx, |s| {
14842 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
14843 });
14844 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
14845 assert_eq!(row, 8, "Should find task when cursor is on function name");
14846 });
14847}
14848
14849#[gpui::test]
14850async fn test_multi_buffer_folding(cx: &mut gpui::TestAppContext) {
14851 init_test(cx, |_| {});
14852
14853 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
14854 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
14855 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
14856
14857 let fs = FakeFs::new(cx.executor());
14858 fs.insert_tree(
14859 "/a",
14860 json!({
14861 "first.rs": sample_text_1,
14862 "second.rs": sample_text_2,
14863 "third.rs": sample_text_3,
14864 }),
14865 )
14866 .await;
14867 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14868 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14869 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14870 let worktree = project.update(cx, |project, cx| {
14871 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
14872 assert_eq!(worktrees.len(), 1);
14873 worktrees.pop().unwrap()
14874 });
14875 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
14876
14877 let buffer_1 = project
14878 .update(cx, |project, cx| {
14879 project.open_buffer((worktree_id, "first.rs"), cx)
14880 })
14881 .await
14882 .unwrap();
14883 let buffer_2 = project
14884 .update(cx, |project, cx| {
14885 project.open_buffer((worktree_id, "second.rs"), cx)
14886 })
14887 .await
14888 .unwrap();
14889 let buffer_3 = project
14890 .update(cx, |project, cx| {
14891 project.open_buffer((worktree_id, "third.rs"), cx)
14892 })
14893 .await
14894 .unwrap();
14895
14896 let multi_buffer = cx.new(|cx| {
14897 let mut multi_buffer = MultiBuffer::new(ReadWrite);
14898 multi_buffer.push_excerpts(
14899 buffer_1.clone(),
14900 [
14901 ExcerptRange {
14902 context: Point::new(0, 0)..Point::new(3, 0),
14903 primary: None,
14904 },
14905 ExcerptRange {
14906 context: Point::new(5, 0)..Point::new(7, 0),
14907 primary: None,
14908 },
14909 ExcerptRange {
14910 context: Point::new(9, 0)..Point::new(10, 4),
14911 primary: None,
14912 },
14913 ],
14914 cx,
14915 );
14916 multi_buffer.push_excerpts(
14917 buffer_2.clone(),
14918 [
14919 ExcerptRange {
14920 context: Point::new(0, 0)..Point::new(3, 0),
14921 primary: None,
14922 },
14923 ExcerptRange {
14924 context: Point::new(5, 0)..Point::new(7, 0),
14925 primary: None,
14926 },
14927 ExcerptRange {
14928 context: Point::new(9, 0)..Point::new(10, 4),
14929 primary: None,
14930 },
14931 ],
14932 cx,
14933 );
14934 multi_buffer.push_excerpts(
14935 buffer_3.clone(),
14936 [
14937 ExcerptRange {
14938 context: Point::new(0, 0)..Point::new(3, 0),
14939 primary: None,
14940 },
14941 ExcerptRange {
14942 context: Point::new(5, 0)..Point::new(7, 0),
14943 primary: None,
14944 },
14945 ExcerptRange {
14946 context: Point::new(9, 0)..Point::new(10, 4),
14947 primary: None,
14948 },
14949 ],
14950 cx,
14951 );
14952 multi_buffer
14953 });
14954 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
14955 Editor::new(
14956 EditorMode::Full,
14957 multi_buffer,
14958 Some(project.clone()),
14959 true,
14960 window,
14961 cx,
14962 )
14963 });
14964
14965 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";
14966 assert_eq!(
14967 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
14968 full_text,
14969 );
14970
14971 multi_buffer_editor.update(cx, |editor, cx| {
14972 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
14973 });
14974 assert_eq!(
14975 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
14976 "\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",
14977 "After folding the first buffer, its text should not be displayed"
14978 );
14979
14980 multi_buffer_editor.update(cx, |editor, cx| {
14981 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
14982 });
14983 assert_eq!(
14984 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
14985 "\n\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
14986 "After folding the second buffer, its text should not be displayed"
14987 );
14988
14989 multi_buffer_editor.update(cx, |editor, cx| {
14990 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
14991 });
14992 assert_eq!(
14993 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
14994 "\n\n\n\n\n",
14995 "After folding the third buffer, its text should not be displayed"
14996 );
14997
14998 // Emulate selection inside the fold logic, that should work
14999 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15000 editor
15001 .snapshot(window, cx)
15002 .next_line_boundary(Point::new(0, 4));
15003 });
15004
15005 multi_buffer_editor.update(cx, |editor, cx| {
15006 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
15007 });
15008 assert_eq!(
15009 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15010 "\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
15011 "After unfolding the second buffer, its text should be displayed"
15012 );
15013
15014 multi_buffer_editor.update(cx, |editor, cx| {
15015 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
15016 });
15017 assert_eq!(
15018 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15019 "\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",
15020 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
15021 );
15022
15023 multi_buffer_editor.update(cx, |editor, cx| {
15024 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
15025 });
15026 assert_eq!(
15027 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15028 full_text,
15029 "After unfolding the all buffers, all original text should be displayed"
15030 );
15031}
15032
15033#[gpui::test]
15034async fn test_multi_buffer_single_excerpts_folding(cx: &mut gpui::TestAppContext) {
15035 init_test(cx, |_| {});
15036
15037 let sample_text_1 = "1111\n2222\n3333".to_string();
15038 let sample_text_2 = "4444\n5555\n6666".to_string();
15039 let sample_text_3 = "7777\n8888\n9999".to_string();
15040
15041 let fs = FakeFs::new(cx.executor());
15042 fs.insert_tree(
15043 "/a",
15044 json!({
15045 "first.rs": sample_text_1,
15046 "second.rs": sample_text_2,
15047 "third.rs": sample_text_3,
15048 }),
15049 )
15050 .await;
15051 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15052 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15053 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15054 let worktree = project.update(cx, |project, cx| {
15055 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15056 assert_eq!(worktrees.len(), 1);
15057 worktrees.pop().unwrap()
15058 });
15059 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15060
15061 let buffer_1 = project
15062 .update(cx, |project, cx| {
15063 project.open_buffer((worktree_id, "first.rs"), cx)
15064 })
15065 .await
15066 .unwrap();
15067 let buffer_2 = project
15068 .update(cx, |project, cx| {
15069 project.open_buffer((worktree_id, "second.rs"), cx)
15070 })
15071 .await
15072 .unwrap();
15073 let buffer_3 = project
15074 .update(cx, |project, cx| {
15075 project.open_buffer((worktree_id, "third.rs"), cx)
15076 })
15077 .await
15078 .unwrap();
15079
15080 let multi_buffer = cx.new(|cx| {
15081 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15082 multi_buffer.push_excerpts(
15083 buffer_1.clone(),
15084 [ExcerptRange {
15085 context: Point::new(0, 0)..Point::new(3, 0),
15086 primary: None,
15087 }],
15088 cx,
15089 );
15090 multi_buffer.push_excerpts(
15091 buffer_2.clone(),
15092 [ExcerptRange {
15093 context: Point::new(0, 0)..Point::new(3, 0),
15094 primary: None,
15095 }],
15096 cx,
15097 );
15098 multi_buffer.push_excerpts(
15099 buffer_3.clone(),
15100 [ExcerptRange {
15101 context: Point::new(0, 0)..Point::new(3, 0),
15102 primary: None,
15103 }],
15104 cx,
15105 );
15106 multi_buffer
15107 });
15108
15109 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15110 Editor::new(
15111 EditorMode::Full,
15112 multi_buffer,
15113 Some(project.clone()),
15114 true,
15115 window,
15116 cx,
15117 )
15118 });
15119
15120 let full_text = "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n";
15121 assert_eq!(
15122 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15123 full_text,
15124 );
15125
15126 multi_buffer_editor.update(cx, |editor, cx| {
15127 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
15128 });
15129 assert_eq!(
15130 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15131 "\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n",
15132 "After folding the first buffer, its text should not be displayed"
15133 );
15134
15135 multi_buffer_editor.update(cx, |editor, cx| {
15136 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
15137 });
15138
15139 assert_eq!(
15140 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15141 "\n\n\n\n\n\n\n7777\n8888\n9999\n",
15142 "After folding the second buffer, its text should not be displayed"
15143 );
15144
15145 multi_buffer_editor.update(cx, |editor, cx| {
15146 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
15147 });
15148 assert_eq!(
15149 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15150 "\n\n\n\n\n",
15151 "After folding the third buffer, its text should not be displayed"
15152 );
15153
15154 multi_buffer_editor.update(cx, |editor, cx| {
15155 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
15156 });
15157 assert_eq!(
15158 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15159 "\n\n\n\n\n4444\n5555\n6666\n\n\n",
15160 "After unfolding the second buffer, its text should be displayed"
15161 );
15162
15163 multi_buffer_editor.update(cx, |editor, cx| {
15164 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
15165 });
15166 assert_eq!(
15167 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15168 "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n",
15169 "After unfolding the first buffer, its text should be displayed"
15170 );
15171
15172 multi_buffer_editor.update(cx, |editor, cx| {
15173 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
15174 });
15175 assert_eq!(
15176 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15177 full_text,
15178 "After unfolding all buffers, all original text should be displayed"
15179 );
15180}
15181
15182#[gpui::test]
15183async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppContext) {
15184 init_test(cx, |_| {});
15185
15186 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
15187
15188 let fs = FakeFs::new(cx.executor());
15189 fs.insert_tree(
15190 "/a",
15191 json!({
15192 "main.rs": sample_text,
15193 }),
15194 )
15195 .await;
15196 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15197 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15198 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15199 let worktree = project.update(cx, |project, cx| {
15200 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
15201 assert_eq!(worktrees.len(), 1);
15202 worktrees.pop().unwrap()
15203 });
15204 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
15205
15206 let buffer_1 = project
15207 .update(cx, |project, cx| {
15208 project.open_buffer((worktree_id, "main.rs"), cx)
15209 })
15210 .await
15211 .unwrap();
15212
15213 let multi_buffer = cx.new(|cx| {
15214 let mut multi_buffer = MultiBuffer::new(ReadWrite);
15215 multi_buffer.push_excerpts(
15216 buffer_1.clone(),
15217 [ExcerptRange {
15218 context: Point::new(0, 0)
15219 ..Point::new(
15220 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
15221 0,
15222 ),
15223 primary: None,
15224 }],
15225 cx,
15226 );
15227 multi_buffer
15228 });
15229 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15230 Editor::new(
15231 EditorMode::Full,
15232 multi_buffer,
15233 Some(project.clone()),
15234 true,
15235 window,
15236 cx,
15237 )
15238 });
15239
15240 let selection_range = Point::new(1, 0)..Point::new(2, 0);
15241 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15242 enum TestHighlight {}
15243 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
15244 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
15245 editor.highlight_text::<TestHighlight>(
15246 vec![highlight_range.clone()],
15247 HighlightStyle::color(Hsla::green()),
15248 cx,
15249 );
15250 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
15251 });
15252
15253 let full_text = format!("\n\n\n{sample_text}\n");
15254 assert_eq!(
15255 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
15256 full_text,
15257 );
15258}
15259
15260#[gpui::test]
15261fn test_inline_completion_text(cx: &mut TestAppContext) {
15262 init_test(cx, |_| {});
15263
15264 // Simple insertion
15265 {
15266 let window = cx.add_window(|window, cx| {
15267 let buffer = MultiBuffer::build_simple("Hello, world!", cx);
15268 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
15269 });
15270 let cx = &mut VisualTestContext::from_window(*window, cx);
15271
15272 window
15273 .update(cx, |editor, window, cx| {
15274 let snapshot = editor.snapshot(window, cx);
15275 let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 6))
15276 ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 6));
15277 let edits = vec![(edit_range, " beautiful".to_string())];
15278
15279 let InlineCompletionText::Edit { text, highlights } =
15280 inline_completion_edit_text(&snapshot, &edits, false, cx)
15281 else {
15282 panic!("Failed to generate inline completion text");
15283 };
15284
15285 assert_eq!(text, "Hello, beautiful world!");
15286 assert_eq!(highlights.len(), 1);
15287 assert_eq!(highlights[0].0, 6..16);
15288 assert_eq!(
15289 highlights[0].1.background_color,
15290 Some(cx.theme().status().created_background)
15291 );
15292 })
15293 .unwrap();
15294 }
15295
15296 // Replacement
15297 {
15298 let window = cx.add_window(|window, cx| {
15299 let buffer = MultiBuffer::build_simple("This is a test.", cx);
15300 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
15301 });
15302 let cx = &mut VisualTestContext::from_window(*window, cx);
15303
15304 window
15305 .update(cx, |editor, window, cx| {
15306 let snapshot = editor.snapshot(window, cx);
15307 let edits = vec![(
15308 snapshot.buffer_snapshot.anchor_after(Point::new(0, 0))
15309 ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 4)),
15310 "That".to_string(),
15311 )];
15312
15313 let InlineCompletionText::Edit { text, highlights } =
15314 inline_completion_edit_text(&snapshot, &edits, false, cx)
15315 else {
15316 panic!("Failed to generate inline completion text");
15317 };
15318
15319 assert_eq!(text, "That is a test.");
15320 assert_eq!(highlights.len(), 1);
15321 assert_eq!(highlights[0].0, 0..4);
15322 assert_eq!(
15323 highlights[0].1.background_color,
15324 Some(cx.theme().status().created_background)
15325 );
15326 })
15327 .unwrap();
15328 }
15329
15330 // Multiple edits
15331 {
15332 let window = cx.add_window(|window, cx| {
15333 let buffer = MultiBuffer::build_simple("Hello, world!", cx);
15334 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
15335 });
15336 let cx = &mut VisualTestContext::from_window(*window, cx);
15337
15338 window
15339 .update(cx, |editor, window, cx| {
15340 let snapshot = editor.snapshot(window, cx);
15341 let edits = vec![
15342 (
15343 snapshot.buffer_snapshot.anchor_after(Point::new(0, 0))
15344 ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 5)),
15345 "Greetings".into(),
15346 ),
15347 (
15348 snapshot.buffer_snapshot.anchor_after(Point::new(0, 12))
15349 ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 12)),
15350 " and universe".into(),
15351 ),
15352 ];
15353
15354 let InlineCompletionText::Edit { text, highlights } =
15355 inline_completion_edit_text(&snapshot, &edits, false, cx)
15356 else {
15357 panic!("Failed to generate inline completion text");
15358 };
15359
15360 assert_eq!(text, "Greetings, world and universe!");
15361 assert_eq!(highlights.len(), 2);
15362 assert_eq!(highlights[0].0, 0..9);
15363 assert_eq!(highlights[1].0, 16..29);
15364 assert_eq!(
15365 highlights[0].1.background_color,
15366 Some(cx.theme().status().created_background)
15367 );
15368 assert_eq!(
15369 highlights[1].1.background_color,
15370 Some(cx.theme().status().created_background)
15371 );
15372 })
15373 .unwrap();
15374 }
15375
15376 // Multiple lines with edits
15377 {
15378 let window = cx.add_window(|window, cx| {
15379 let buffer =
15380 MultiBuffer::build_simple("First line\nSecond line\nThird line\nFourth line", cx);
15381 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
15382 });
15383 let cx = &mut VisualTestContext::from_window(*window, cx);
15384
15385 window
15386 .update(cx, |editor, window, cx| {
15387 let snapshot = editor.snapshot(window, cx);
15388 let edits = vec![
15389 (
15390 snapshot.buffer_snapshot.anchor_before(Point::new(1, 7))
15391 ..snapshot.buffer_snapshot.anchor_before(Point::new(1, 11)),
15392 "modified".to_string(),
15393 ),
15394 (
15395 snapshot.buffer_snapshot.anchor_before(Point::new(2, 0))
15396 ..snapshot.buffer_snapshot.anchor_before(Point::new(2, 10)),
15397 "New third line".to_string(),
15398 ),
15399 (
15400 snapshot.buffer_snapshot.anchor_before(Point::new(3, 6))
15401 ..snapshot.buffer_snapshot.anchor_before(Point::new(3, 6)),
15402 " updated".to_string(),
15403 ),
15404 ];
15405
15406 let InlineCompletionText::Edit { text, highlights } =
15407 inline_completion_edit_text(&snapshot, &edits, false, cx)
15408 else {
15409 panic!("Failed to generate inline completion text");
15410 };
15411
15412 assert_eq!(text, "Second modified\nNew third line\nFourth updated line");
15413 assert_eq!(highlights.len(), 3);
15414 assert_eq!(highlights[0].0, 7..15); // "modified"
15415 assert_eq!(highlights[1].0, 16..30); // "New third line"
15416 assert_eq!(highlights[2].0, 37..45); // " updated"
15417
15418 for highlight in &highlights {
15419 assert_eq!(
15420 highlight.1.background_color,
15421 Some(cx.theme().status().created_background)
15422 );
15423 }
15424 })
15425 .unwrap();
15426 }
15427}
15428
15429#[gpui::test]
15430fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
15431 init_test(cx, |_| {});
15432
15433 // Deletion
15434 {
15435 let window = cx.add_window(|window, cx| {
15436 let buffer = MultiBuffer::build_simple("Hello, world!", cx);
15437 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
15438 });
15439 let cx = &mut VisualTestContext::from_window(*window, cx);
15440
15441 window
15442 .update(cx, |editor, window, cx| {
15443 let snapshot = editor.snapshot(window, cx);
15444 let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 5))
15445 ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 11));
15446 let edits = vec![(edit_range, "".to_string())];
15447
15448 let InlineCompletionText::Edit { text, highlights } =
15449 inline_completion_edit_text(&snapshot, &edits, true, cx)
15450 else {
15451 panic!("Failed to generate inline completion text");
15452 };
15453
15454 assert_eq!(text, "Hello, world!");
15455 assert_eq!(highlights.len(), 1);
15456 assert_eq!(highlights[0].0, 5..11);
15457 assert_eq!(
15458 highlights[0].1.background_color,
15459 Some(cx.theme().status().deleted_background)
15460 );
15461 })
15462 .unwrap();
15463 }
15464
15465 // Insertion
15466 {
15467 let window = cx.add_window(|window, cx| {
15468 let buffer = MultiBuffer::build_simple("Hello, world!", cx);
15469 Editor::new(EditorMode::Full, buffer, None, true, window, cx)
15470 });
15471 let cx = &mut VisualTestContext::from_window(*window, cx);
15472
15473 window
15474 .update(cx, |editor, window, cx| {
15475 let snapshot = editor.snapshot(window, cx);
15476 let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 6))
15477 ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 6));
15478 let edits = vec![(edit_range, " digital".to_string())];
15479
15480 let InlineCompletionText::Edit { text, highlights } =
15481 inline_completion_edit_text(&snapshot, &edits, true, cx)
15482 else {
15483 panic!("Failed to generate inline completion text");
15484 };
15485
15486 assert_eq!(text, "Hello, digital world!");
15487 assert_eq!(highlights.len(), 1);
15488 assert_eq!(highlights[0].0, 6..14);
15489 assert_eq!(
15490 highlights[0].1.background_color,
15491 Some(cx.theme().status().created_background)
15492 );
15493 })
15494 .unwrap();
15495 }
15496}
15497
15498#[gpui::test]
15499async fn test_rename_with_duplicate_edits(cx: &mut gpui::TestAppContext) {
15500 init_test(cx, |_| {});
15501 let capabilities = lsp::ServerCapabilities {
15502 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
15503 prepare_provider: Some(true),
15504 work_done_progress_options: Default::default(),
15505 })),
15506 ..Default::default()
15507 };
15508 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
15509
15510 cx.set_state(indoc! {"
15511 struct Fˇoo {}
15512 "});
15513
15514 cx.update_editor(|editor, _, cx| {
15515 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
15516 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
15517 editor.highlight_background::<DocumentHighlightRead>(
15518 &[highlight_range],
15519 |c| c.editor_document_highlight_read_background,
15520 cx,
15521 );
15522 });
15523
15524 let mut prepare_rename_handler =
15525 cx.handle_request::<lsp::request::PrepareRenameRequest, _, _>(move |_, _, _| async move {
15526 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
15527 start: lsp::Position {
15528 line: 0,
15529 character: 7,
15530 },
15531 end: lsp::Position {
15532 line: 0,
15533 character: 10,
15534 },
15535 })))
15536 });
15537 let prepare_rename_task = cx
15538 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
15539 .expect("Prepare rename was not started");
15540 prepare_rename_handler.next().await.unwrap();
15541 prepare_rename_task.await.expect("Prepare rename failed");
15542
15543 let mut rename_handler =
15544 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
15545 let edit = lsp::TextEdit {
15546 range: lsp::Range {
15547 start: lsp::Position {
15548 line: 0,
15549 character: 7,
15550 },
15551 end: lsp::Position {
15552 line: 0,
15553 character: 10,
15554 },
15555 },
15556 new_text: "FooRenamed".to_string(),
15557 };
15558 Ok(Some(lsp::WorkspaceEdit::new(
15559 // Specify the same edit twice
15560 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
15561 )))
15562 });
15563 let rename_task = cx
15564 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
15565 .expect("Confirm rename was not started");
15566 rename_handler.next().await.unwrap();
15567 rename_task.await.expect("Confirm rename failed");
15568 cx.run_until_parked();
15569
15570 // Despite two edits, only one is actually applied as those are identical
15571 cx.assert_editor_state(indoc! {"
15572 struct FooRenamedˇ {}
15573 "});
15574}
15575
15576#[gpui::test]
15577async fn test_rename_without_prepare(cx: &mut gpui::TestAppContext) {
15578 init_test(cx, |_| {});
15579 // These capabilities indicate that the server does not support prepare rename.
15580 let capabilities = lsp::ServerCapabilities {
15581 rename_provider: Some(lsp::OneOf::Left(true)),
15582 ..Default::default()
15583 };
15584 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
15585
15586 cx.set_state(indoc! {"
15587 struct Fˇoo {}
15588 "});
15589
15590 cx.update_editor(|editor, _window, cx| {
15591 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
15592 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
15593 editor.highlight_background::<DocumentHighlightRead>(
15594 &[highlight_range],
15595 |c| c.editor_document_highlight_read_background,
15596 cx,
15597 );
15598 });
15599
15600 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
15601 .expect("Prepare rename was not started")
15602 .await
15603 .expect("Prepare rename failed");
15604
15605 let mut rename_handler =
15606 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
15607 let edit = lsp::TextEdit {
15608 range: lsp::Range {
15609 start: lsp::Position {
15610 line: 0,
15611 character: 7,
15612 },
15613 end: lsp::Position {
15614 line: 0,
15615 character: 10,
15616 },
15617 },
15618 new_text: "FooRenamed".to_string(),
15619 };
15620 Ok(Some(lsp::WorkspaceEdit::new(
15621 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
15622 )))
15623 });
15624 let rename_task = cx
15625 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
15626 .expect("Confirm rename was not started");
15627 rename_handler.next().await.unwrap();
15628 rename_task.await.expect("Confirm rename failed");
15629 cx.run_until_parked();
15630
15631 // Correct range is renamed, as `surrounding_word` is used to find it.
15632 cx.assert_editor_state(indoc! {"
15633 struct FooRenamedˇ {}
15634 "});
15635}
15636
15637fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
15638 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
15639 point..point
15640}
15641
15642fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
15643 let (text, ranges) = marked_text_ranges(marked_text, true);
15644 assert_eq!(editor.text(cx), text);
15645 assert_eq!(
15646 editor.selections.ranges(cx),
15647 ranges,
15648 "Assert selections are {}",
15649 marked_text
15650 );
15651}
15652
15653pub fn handle_signature_help_request(
15654 cx: &mut EditorLspTestContext,
15655 mocked_response: lsp::SignatureHelp,
15656) -> impl Future<Output = ()> {
15657 let mut request =
15658 cx.handle_request::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
15659 let mocked_response = mocked_response.clone();
15660 async move { Ok(Some(mocked_response)) }
15661 });
15662
15663 async move {
15664 request.next().await;
15665 }
15666}
15667
15668/// Handle completion request passing a marked string specifying where the completion
15669/// should be triggered from using '|' character, what range should be replaced, and what completions
15670/// should be returned using '<' and '>' to delimit the range
15671pub fn handle_completion_request(
15672 cx: &mut EditorLspTestContext,
15673 marked_string: &str,
15674 completions: Vec<&'static str>,
15675 counter: Arc<AtomicUsize>,
15676) -> impl Future<Output = ()> {
15677 let complete_from_marker: TextRangeMarker = '|'.into();
15678 let replace_range_marker: TextRangeMarker = ('<', '>').into();
15679 let (_, mut marked_ranges) = marked_text_ranges_by(
15680 marked_string,
15681 vec![complete_from_marker.clone(), replace_range_marker.clone()],
15682 );
15683
15684 let complete_from_position =
15685 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
15686 let replace_range =
15687 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
15688
15689 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
15690 let completions = completions.clone();
15691 counter.fetch_add(1, atomic::Ordering::Release);
15692 async move {
15693 assert_eq!(params.text_document_position.text_document.uri, url.clone());
15694 assert_eq!(
15695 params.text_document_position.position,
15696 complete_from_position
15697 );
15698 Ok(Some(lsp::CompletionResponse::Array(
15699 completions
15700 .iter()
15701 .map(|completion_text| lsp::CompletionItem {
15702 label: completion_text.to_string(),
15703 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15704 range: replace_range,
15705 new_text: completion_text.to_string(),
15706 })),
15707 ..Default::default()
15708 })
15709 .collect(),
15710 )))
15711 }
15712 });
15713
15714 async move {
15715 request.next().await;
15716 }
15717}
15718
15719fn handle_resolve_completion_request(
15720 cx: &mut EditorLspTestContext,
15721 edits: Option<Vec<(&'static str, &'static str)>>,
15722) -> impl Future<Output = ()> {
15723 let edits = edits.map(|edits| {
15724 edits
15725 .iter()
15726 .map(|(marked_string, new_text)| {
15727 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
15728 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
15729 lsp::TextEdit::new(replace_range, new_text.to_string())
15730 })
15731 .collect::<Vec<_>>()
15732 });
15733
15734 let mut request =
15735 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15736 let edits = edits.clone();
15737 async move {
15738 Ok(lsp::CompletionItem {
15739 additional_text_edits: edits,
15740 ..Default::default()
15741 })
15742 }
15743 });
15744
15745 async move {
15746 request.next().await;
15747 }
15748}
15749
15750pub(crate) fn update_test_language_settings(
15751 cx: &mut TestAppContext,
15752 f: impl Fn(&mut AllLanguageSettingsContent),
15753) {
15754 cx.update(|cx| {
15755 SettingsStore::update_global(cx, |store, cx| {
15756 store.update_user_settings::<AllLanguageSettings>(cx, f);
15757 });
15758 });
15759}
15760
15761pub(crate) fn update_test_project_settings(
15762 cx: &mut TestAppContext,
15763 f: impl Fn(&mut ProjectSettings),
15764) {
15765 cx.update(|cx| {
15766 SettingsStore::update_global(cx, |store, cx| {
15767 store.update_user_settings::<ProjectSettings>(cx, f);
15768 });
15769 });
15770}
15771
15772pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
15773 cx.update(|cx| {
15774 assets::Assets.load_test_fonts(cx);
15775 let store = SettingsStore::test(cx);
15776 cx.set_global(store);
15777 theme::init(theme::LoadThemes::JustBase, cx);
15778 release_channel::init(SemanticVersion::default(), cx);
15779 client::init_settings(cx);
15780 language::init(cx);
15781 Project::init_settings(cx);
15782 workspace::init_settings(cx);
15783 crate::init(cx);
15784 });
15785
15786 update_test_language_settings(cx, f);
15787}
15788
15789#[track_caller]
15790fn assert_hunk_revert(
15791 not_reverted_text_with_selections: &str,
15792 expected_hunk_statuses_before: Vec<DiffHunkStatus>,
15793 expected_reverted_text_with_selections: &str,
15794 base_text: &str,
15795 cx: &mut EditorLspTestContext,
15796) {
15797 cx.set_state(not_reverted_text_with_selections);
15798 cx.set_diff_base(base_text);
15799 cx.executor().run_until_parked();
15800
15801 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
15802 let snapshot = editor.snapshot(window, cx);
15803 let reverted_hunk_statuses = snapshot
15804 .buffer_snapshot
15805 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
15806 .map(|hunk| hunk.status())
15807 .collect::<Vec<_>>();
15808
15809 editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
15810 reverted_hunk_statuses
15811 });
15812 cx.executor().run_until_parked();
15813 cx.assert_editor_state(expected_reverted_text_with_selections);
15814 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
15815}