1use super::*;
2use crate::{
3 scroll::scroll_amount::ScrollAmount,
4 test::{
5 assert_text_with_selections, build_editor,
6 editor_lsp_test_context::{git_commit_lang, EditorLspTestContext},
7 editor_test_context::EditorTestContext,
8 select_ranges,
9 },
10 JoinLines,
11};
12use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
13use futures::StreamExt;
14use gpui::{
15 div, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
16 WindowBounds, WindowOptions,
17};
18use indoc::indoc;
19use language::{
20 language_settings::{
21 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
22 LanguageSettingsContent, PrettierSettings,
23 },
24 BracketPairConfig,
25 Capability::ReadWrite,
26 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
27 Override, Point,
28};
29use language_settings::{Formatter, FormatterList, IndentGuideSettings};
30use multi_buffer::{IndentGuide, PathKey};
31use parking_lot::Mutex;
32use pretty_assertions::{assert_eq, assert_ne};
33use project::{
34 debugger::breakpoint_store::{BreakpointKind, SerializedBreakpoint},
35 project_settings::{LspSettings, ProjectSettings},
36 FakeFs,
37};
38use serde_json::{self, json};
39use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
40use std::{
41 iter,
42 sync::atomic::{self, AtomicUsize},
43};
44use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
45use text::ToPoint as _;
46use unindent::Unindent;
47use util::{
48 assert_set_eq, path,
49 test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
50 uri,
51};
52use workspace::{
53 item::{FollowEvent, FollowableItem, Item, ItemHandle},
54 NavigationEntry, ViewId,
55};
56
57#[gpui::test]
58fn test_edit_events(cx: &mut TestAppContext) {
59 init_test(cx, |_| {});
60
61 let buffer = cx.new(|cx| {
62 let mut buffer = language::Buffer::local("123456", cx);
63 buffer.set_group_interval(Duration::from_secs(1));
64 buffer
65 });
66
67 let events = Rc::new(RefCell::new(Vec::new()));
68 let editor1 = cx.add_window({
69 let events = events.clone();
70 |window, cx| {
71 let entity = cx.entity().clone();
72 cx.subscribe_in(
73 &entity,
74 window,
75 move |_, _, event: &EditorEvent, _, _| match event {
76 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
77 EditorEvent::BufferEdited => {
78 events.borrow_mut().push(("editor1", "buffer edited"))
79 }
80 _ => {}
81 },
82 )
83 .detach();
84 Editor::for_buffer(buffer.clone(), None, window, cx)
85 }
86 });
87
88 let editor2 = cx.add_window({
89 let events = events.clone();
90 |window, cx| {
91 cx.subscribe_in(
92 &cx.entity().clone(),
93 window,
94 move |_, _, event: &EditorEvent, _, _| match event {
95 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
96 EditorEvent::BufferEdited => {
97 events.borrow_mut().push(("editor2", "buffer edited"))
98 }
99 _ => {}
100 },
101 )
102 .detach();
103 Editor::for_buffer(buffer.clone(), None, window, cx)
104 }
105 });
106
107 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
108
109 // Mutating editor 1 will emit an `Edited` event only for that editor.
110 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
111 assert_eq!(
112 mem::take(&mut *events.borrow_mut()),
113 [
114 ("editor1", "edited"),
115 ("editor1", "buffer edited"),
116 ("editor2", "buffer edited"),
117 ]
118 );
119
120 // Mutating editor 2 will emit an `Edited` event only for that editor.
121 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
122 assert_eq!(
123 mem::take(&mut *events.borrow_mut()),
124 [
125 ("editor2", "edited"),
126 ("editor1", "buffer edited"),
127 ("editor2", "buffer edited"),
128 ]
129 );
130
131 // Undoing on editor 1 will emit an `Edited` event only for that editor.
132 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
133 assert_eq!(
134 mem::take(&mut *events.borrow_mut()),
135 [
136 ("editor1", "edited"),
137 ("editor1", "buffer edited"),
138 ("editor2", "buffer edited"),
139 ]
140 );
141
142 // Redoing on editor 1 will emit an `Edited` event only for that editor.
143 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
144 assert_eq!(
145 mem::take(&mut *events.borrow_mut()),
146 [
147 ("editor1", "edited"),
148 ("editor1", "buffer edited"),
149 ("editor2", "buffer edited"),
150 ]
151 );
152
153 // Undoing on editor 2 will emit an `Edited` event only for that editor.
154 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
155 assert_eq!(
156 mem::take(&mut *events.borrow_mut()),
157 [
158 ("editor2", "edited"),
159 ("editor1", "buffer edited"),
160 ("editor2", "buffer edited"),
161 ]
162 );
163
164 // Redoing on editor 2 will emit an `Edited` event only for that editor.
165 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
166 assert_eq!(
167 mem::take(&mut *events.borrow_mut()),
168 [
169 ("editor2", "edited"),
170 ("editor1", "buffer edited"),
171 ("editor2", "buffer edited"),
172 ]
173 );
174
175 // No event is emitted when the mutation is a no-op.
176 _ = editor2.update(cx, |editor, window, cx| {
177 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
178
179 editor.backspace(&Backspace, window, cx);
180 });
181 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
182}
183
184#[gpui::test]
185fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
186 init_test(cx, |_| {});
187
188 let mut now = Instant::now();
189 let group_interval = Duration::from_millis(1);
190 let buffer = cx.new(|cx| {
191 let mut buf = language::Buffer::local("123456", cx);
192 buf.set_group_interval(group_interval);
193 buf
194 });
195 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
196 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
197
198 _ = editor.update(cx, |editor, window, cx| {
199 editor.start_transaction_at(now, window, cx);
200 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
201
202 editor.insert("cd", window, cx);
203 editor.end_transaction_at(now, cx);
204 assert_eq!(editor.text(cx), "12cd56");
205 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
206
207 editor.start_transaction_at(now, window, cx);
208 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
209 editor.insert("e", window, cx);
210 editor.end_transaction_at(now, cx);
211 assert_eq!(editor.text(cx), "12cde6");
212 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
213
214 now += group_interval + Duration::from_millis(1);
215 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
216
217 // Simulate an edit in another editor
218 buffer.update(cx, |buffer, cx| {
219 buffer.start_transaction_at(now, cx);
220 buffer.edit([(0..1, "a")], None, cx);
221 buffer.edit([(1..1, "b")], None, cx);
222 buffer.end_transaction_at(now, cx);
223 });
224
225 assert_eq!(editor.text(cx), "ab2cde6");
226 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
227
228 // Last transaction happened past the group interval in a different editor.
229 // Undo it individually and don't restore selections.
230 editor.undo(&Undo, window, cx);
231 assert_eq!(editor.text(cx), "12cde6");
232 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
233
234 // First two transactions happened within the group interval in this editor.
235 // Undo them together and restore selections.
236 editor.undo(&Undo, window, cx);
237 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
238 assert_eq!(editor.text(cx), "123456");
239 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
240
241 // Redo the first two transactions together.
242 editor.redo(&Redo, window, cx);
243 assert_eq!(editor.text(cx), "12cde6");
244 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
245
246 // Redo the last transaction on its own.
247 editor.redo(&Redo, window, cx);
248 assert_eq!(editor.text(cx), "ab2cde6");
249 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
250
251 // Test empty transactions.
252 editor.start_transaction_at(now, window, cx);
253 editor.end_transaction_at(now, cx);
254 editor.undo(&Undo, window, cx);
255 assert_eq!(editor.text(cx), "12cde6");
256 });
257}
258
259#[gpui::test]
260fn test_ime_composition(cx: &mut TestAppContext) {
261 init_test(cx, |_| {});
262
263 let buffer = cx.new(|cx| {
264 let mut buffer = language::Buffer::local("abcde", cx);
265 // Ensure automatic grouping doesn't occur.
266 buffer.set_group_interval(Duration::ZERO);
267 buffer
268 });
269
270 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
271 cx.add_window(|window, cx| {
272 let mut editor = build_editor(buffer.clone(), window, cx);
273
274 // Start a new IME composition.
275 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
276 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
277 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
278 assert_eq!(editor.text(cx), "äbcde");
279 assert_eq!(
280 editor.marked_text_ranges(cx),
281 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
282 );
283
284 // Finalize IME composition.
285 editor.replace_text_in_range(None, "ā", window, cx);
286 assert_eq!(editor.text(cx), "ābcde");
287 assert_eq!(editor.marked_text_ranges(cx), None);
288
289 // IME composition edits are grouped and are undone/redone at once.
290 editor.undo(&Default::default(), window, cx);
291 assert_eq!(editor.text(cx), "abcde");
292 assert_eq!(editor.marked_text_ranges(cx), None);
293 editor.redo(&Default::default(), window, cx);
294 assert_eq!(editor.text(cx), "ābcde");
295 assert_eq!(editor.marked_text_ranges(cx), None);
296
297 // Start a new IME composition.
298 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
299 assert_eq!(
300 editor.marked_text_ranges(cx),
301 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
302 );
303
304 // Undoing during an IME composition cancels it.
305 editor.undo(&Default::default(), window, cx);
306 assert_eq!(editor.text(cx), "ābcde");
307 assert_eq!(editor.marked_text_ranges(cx), None);
308
309 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
310 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
311 assert_eq!(editor.text(cx), "ābcdè");
312 assert_eq!(
313 editor.marked_text_ranges(cx),
314 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
315 );
316
317 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
318 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
319 assert_eq!(editor.text(cx), "ābcdę");
320 assert_eq!(editor.marked_text_ranges(cx), None);
321
322 // Start a new IME composition with multiple cursors.
323 editor.change_selections(None, window, cx, |s| {
324 s.select_ranges([
325 OffsetUtf16(1)..OffsetUtf16(1),
326 OffsetUtf16(3)..OffsetUtf16(3),
327 OffsetUtf16(5)..OffsetUtf16(5),
328 ])
329 });
330 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
331 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
332 assert_eq!(
333 editor.marked_text_ranges(cx),
334 Some(vec![
335 OffsetUtf16(0)..OffsetUtf16(3),
336 OffsetUtf16(4)..OffsetUtf16(7),
337 OffsetUtf16(8)..OffsetUtf16(11)
338 ])
339 );
340
341 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
342 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
343 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
344 assert_eq!(
345 editor.marked_text_ranges(cx),
346 Some(vec![
347 OffsetUtf16(1)..OffsetUtf16(2),
348 OffsetUtf16(5)..OffsetUtf16(6),
349 OffsetUtf16(9)..OffsetUtf16(10)
350 ])
351 );
352
353 // Finalize IME composition with multiple cursors.
354 editor.replace_text_in_range(Some(9..10), "2", window, cx);
355 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
356 assert_eq!(editor.marked_text_ranges(cx), None);
357
358 editor
359 });
360}
361
362#[gpui::test]
363fn test_selection_with_mouse(cx: &mut TestAppContext) {
364 init_test(cx, |_| {});
365
366 let editor = cx.add_window(|window, cx| {
367 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
368 build_editor(buffer, window, cx)
369 });
370
371 _ = editor.update(cx, |editor, window, cx| {
372 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
373 });
374 assert_eq!(
375 editor
376 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
377 .unwrap(),
378 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
379 );
380
381 _ = editor.update(cx, |editor, window, cx| {
382 editor.update_selection(
383 DisplayPoint::new(DisplayRow(3), 3),
384 0,
385 gpui::Point::<f32>::default(),
386 window,
387 cx,
388 );
389 });
390
391 assert_eq!(
392 editor
393 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
394 .unwrap(),
395 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
396 );
397
398 _ = editor.update(cx, |editor, window, cx| {
399 editor.update_selection(
400 DisplayPoint::new(DisplayRow(1), 1),
401 0,
402 gpui::Point::<f32>::default(),
403 window,
404 cx,
405 );
406 });
407
408 assert_eq!(
409 editor
410 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
411 .unwrap(),
412 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
413 );
414
415 _ = editor.update(cx, |editor, window, cx| {
416 editor.end_selection(window, cx);
417 editor.update_selection(
418 DisplayPoint::new(DisplayRow(3), 3),
419 0,
420 gpui::Point::<f32>::default(),
421 window,
422 cx,
423 );
424 });
425
426 assert_eq!(
427 editor
428 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
429 .unwrap(),
430 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
431 );
432
433 _ = editor.update(cx, |editor, window, cx| {
434 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
435 editor.update_selection(
436 DisplayPoint::new(DisplayRow(0), 0),
437 0,
438 gpui::Point::<f32>::default(),
439 window,
440 cx,
441 );
442 });
443
444 assert_eq!(
445 editor
446 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
447 .unwrap(),
448 [
449 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
450 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
451 ]
452 );
453
454 _ = editor.update(cx, |editor, window, cx| {
455 editor.end_selection(window, cx);
456 });
457
458 assert_eq!(
459 editor
460 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
461 .unwrap(),
462 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
463 );
464}
465
466#[gpui::test]
467fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
468 init_test(cx, |_| {});
469
470 let editor = cx.add_window(|window, cx| {
471 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
472 build_editor(buffer, window, cx)
473 });
474
475 _ = editor.update(cx, |editor, window, cx| {
476 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
477 });
478
479 _ = editor.update(cx, |editor, window, cx| {
480 editor.end_selection(window, cx);
481 });
482
483 _ = editor.update(cx, |editor, window, cx| {
484 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
485 });
486
487 _ = editor.update(cx, |editor, window, cx| {
488 editor.end_selection(window, cx);
489 });
490
491 assert_eq!(
492 editor
493 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
494 .unwrap(),
495 [
496 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
497 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
498 ]
499 );
500
501 _ = editor.update(cx, |editor, window, cx| {
502 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
503 });
504
505 _ = editor.update(cx, |editor, window, cx| {
506 editor.end_selection(window, cx);
507 });
508
509 assert_eq!(
510 editor
511 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
512 .unwrap(),
513 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
514 );
515}
516
517#[gpui::test]
518fn test_canceling_pending_selection(cx: &mut TestAppContext) {
519 init_test(cx, |_| {});
520
521 let editor = cx.add_window(|window, cx| {
522 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
523 build_editor(buffer, window, cx)
524 });
525
526 _ = editor.update(cx, |editor, window, cx| {
527 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
528 assert_eq!(
529 editor.selections.display_ranges(cx),
530 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
531 );
532 });
533
534 _ = editor.update(cx, |editor, window, cx| {
535 editor.update_selection(
536 DisplayPoint::new(DisplayRow(3), 3),
537 0,
538 gpui::Point::<f32>::default(),
539 window,
540 cx,
541 );
542 assert_eq!(
543 editor.selections.display_ranges(cx),
544 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
545 );
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.cancel(&Cancel, window, cx);
550 editor.update_selection(
551 DisplayPoint::new(DisplayRow(1), 1),
552 0,
553 gpui::Point::<f32>::default(),
554 window,
555 cx,
556 );
557 assert_eq!(
558 editor.selections.display_ranges(cx),
559 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
560 );
561 });
562}
563
564#[gpui::test]
565fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
566 init_test(cx, |_| {});
567
568 let editor = cx.add_window(|window, cx| {
569 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
570 build_editor(buffer, window, cx)
571 });
572
573 _ = editor.update(cx, |editor, window, cx| {
574 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
575 assert_eq!(
576 editor.selections.display_ranges(cx),
577 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
578 );
579
580 editor.move_down(&Default::default(), window, cx);
581 assert_eq!(
582 editor.selections.display_ranges(cx),
583 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
584 );
585
586 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
587 assert_eq!(
588 editor.selections.display_ranges(cx),
589 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
590 );
591
592 editor.move_up(&Default::default(), window, cx);
593 assert_eq!(
594 editor.selections.display_ranges(cx),
595 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
596 );
597 });
598}
599
600#[gpui::test]
601fn test_clone(cx: &mut TestAppContext) {
602 init_test(cx, |_| {});
603
604 let (text, selection_ranges) = marked_text_ranges(
605 indoc! {"
606 one
607 two
608 threeˇ
609 four
610 fiveˇ
611 "},
612 true,
613 );
614
615 let editor = cx.add_window(|window, cx| {
616 let buffer = MultiBuffer::build_simple(&text, cx);
617 build_editor(buffer, window, cx)
618 });
619
620 _ = editor.update(cx, |editor, window, cx| {
621 editor.change_selections(None, window, cx, |s| {
622 s.select_ranges(selection_ranges.clone())
623 });
624 editor.fold_creases(
625 vec![
626 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
627 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
628 ],
629 true,
630 window,
631 cx,
632 );
633 });
634
635 let cloned_editor = editor
636 .update(cx, |editor, _, cx| {
637 cx.open_window(Default::default(), |window, cx| {
638 cx.new(|cx| editor.clone(window, cx))
639 })
640 })
641 .unwrap()
642 .unwrap();
643
644 let snapshot = editor
645 .update(cx, |e, window, cx| e.snapshot(window, cx))
646 .unwrap();
647 let cloned_snapshot = cloned_editor
648 .update(cx, |e, window, cx| e.snapshot(window, cx))
649 .unwrap();
650
651 assert_eq!(
652 cloned_editor
653 .update(cx, |e, _, cx| e.display_text(cx))
654 .unwrap(),
655 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
656 );
657 assert_eq!(
658 cloned_snapshot
659 .folds_in_range(0..text.len())
660 .collect::<Vec<_>>(),
661 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
662 );
663 assert_set_eq!(
664 cloned_editor
665 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
666 .unwrap(),
667 editor
668 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
669 .unwrap()
670 );
671 assert_set_eq!(
672 cloned_editor
673 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
674 .unwrap(),
675 editor
676 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
677 .unwrap()
678 );
679}
680
681#[gpui::test]
682async fn test_navigation_history(cx: &mut TestAppContext) {
683 init_test(cx, |_| {});
684
685 use workspace::item::Item;
686
687 let fs = FakeFs::new(cx.executor());
688 let project = Project::test(fs, [], cx).await;
689 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
690 let pane = workspace
691 .update(cx, |workspace, _, _| workspace.active_pane().clone())
692 .unwrap();
693
694 _ = workspace.update(cx, |_v, window, cx| {
695 cx.new(|cx| {
696 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
697 let mut editor = build_editor(buffer.clone(), window, cx);
698 let handle = cx.entity();
699 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
700
701 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
702 editor.nav_history.as_mut().unwrap().pop_backward(cx)
703 }
704
705 // Move the cursor a small distance.
706 // Nothing is added to the navigation history.
707 editor.change_selections(None, window, cx, |s| {
708 s.select_display_ranges([
709 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
710 ])
711 });
712 editor.change_selections(None, window, cx, |s| {
713 s.select_display_ranges([
714 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
715 ])
716 });
717 assert!(pop_history(&mut editor, cx).is_none());
718
719 // Move the cursor a large distance.
720 // The history can jump back to the previous position.
721 editor.change_selections(None, window, cx, |s| {
722 s.select_display_ranges([
723 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
724 ])
725 });
726 let nav_entry = pop_history(&mut editor, cx).unwrap();
727 editor.navigate(nav_entry.data.unwrap(), window, cx);
728 assert_eq!(nav_entry.item.id(), cx.entity_id());
729 assert_eq!(
730 editor.selections.display_ranges(cx),
731 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
732 );
733 assert!(pop_history(&mut editor, cx).is_none());
734
735 // Move the cursor a small distance via the mouse.
736 // Nothing is added to the navigation history.
737 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
738 editor.end_selection(window, cx);
739 assert_eq!(
740 editor.selections.display_ranges(cx),
741 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
742 );
743 assert!(pop_history(&mut editor, cx).is_none());
744
745 // Move the cursor a large distance via the mouse.
746 // The history can jump back to the previous position.
747 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
748 editor.end_selection(window, cx);
749 assert_eq!(
750 editor.selections.display_ranges(cx),
751 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
752 );
753 let nav_entry = pop_history(&mut editor, cx).unwrap();
754 editor.navigate(nav_entry.data.unwrap(), window, cx);
755 assert_eq!(nav_entry.item.id(), cx.entity_id());
756 assert_eq!(
757 editor.selections.display_ranges(cx),
758 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
759 );
760 assert!(pop_history(&mut editor, cx).is_none());
761
762 // Set scroll position to check later
763 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
764 let original_scroll_position = editor.scroll_manager.anchor();
765
766 // Jump to the end of the document and adjust scroll
767 editor.move_to_end(&MoveToEnd, window, cx);
768 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
769 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
770
771 let nav_entry = pop_history(&mut editor, cx).unwrap();
772 editor.navigate(nav_entry.data.unwrap(), window, cx);
773 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
774
775 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
776 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
777 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
778 let invalid_point = Point::new(9999, 0);
779 editor.navigate(
780 Box::new(NavigationData {
781 cursor_anchor: invalid_anchor,
782 cursor_position: invalid_point,
783 scroll_anchor: ScrollAnchor {
784 anchor: invalid_anchor,
785 offset: Default::default(),
786 },
787 scroll_top_row: invalid_point.row,
788 }),
789 window,
790 cx,
791 );
792 assert_eq!(
793 editor.selections.display_ranges(cx),
794 &[editor.max_point(cx)..editor.max_point(cx)]
795 );
796 assert_eq!(
797 editor.scroll_position(cx),
798 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
799 );
800
801 editor
802 })
803 });
804}
805
806#[gpui::test]
807fn test_cancel(cx: &mut TestAppContext) {
808 init_test(cx, |_| {});
809
810 let editor = cx.add_window(|window, cx| {
811 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
812 build_editor(buffer, window, cx)
813 });
814
815 _ = editor.update(cx, |editor, window, cx| {
816 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
817 editor.update_selection(
818 DisplayPoint::new(DisplayRow(1), 1),
819 0,
820 gpui::Point::<f32>::default(),
821 window,
822 cx,
823 );
824 editor.end_selection(window, cx);
825
826 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
827 editor.update_selection(
828 DisplayPoint::new(DisplayRow(0), 3),
829 0,
830 gpui::Point::<f32>::default(),
831 window,
832 cx,
833 );
834 editor.end_selection(window, cx);
835 assert_eq!(
836 editor.selections.display_ranges(cx),
837 [
838 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
839 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
840 ]
841 );
842 });
843
844 _ = editor.update(cx, |editor, window, cx| {
845 editor.cancel(&Cancel, window, cx);
846 assert_eq!(
847 editor.selections.display_ranges(cx),
848 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
849 );
850 });
851
852 _ = editor.update(cx, |editor, window, cx| {
853 editor.cancel(&Cancel, window, cx);
854 assert_eq!(
855 editor.selections.display_ranges(cx),
856 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
857 );
858 });
859}
860
861#[gpui::test]
862fn test_fold_action(cx: &mut TestAppContext) {
863 init_test(cx, |_| {});
864
865 let editor = cx.add_window(|window, cx| {
866 let buffer = MultiBuffer::build_simple(
867 &"
868 impl Foo {
869 // Hello!
870
871 fn a() {
872 1
873 }
874
875 fn b() {
876 2
877 }
878
879 fn c() {
880 3
881 }
882 }
883 "
884 .unindent(),
885 cx,
886 );
887 build_editor(buffer.clone(), window, cx)
888 });
889
890 _ = editor.update(cx, |editor, window, cx| {
891 editor.change_selections(None, window, cx, |s| {
892 s.select_display_ranges([
893 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
894 ]);
895 });
896 editor.fold(&Fold, window, cx);
897 assert_eq!(
898 editor.display_text(cx),
899 "
900 impl Foo {
901 // Hello!
902
903 fn a() {
904 1
905 }
906
907 fn b() {⋯
908 }
909
910 fn c() {⋯
911 }
912 }
913 "
914 .unindent(),
915 );
916
917 editor.fold(&Fold, window, cx);
918 assert_eq!(
919 editor.display_text(cx),
920 "
921 impl Foo {⋯
922 }
923 "
924 .unindent(),
925 );
926
927 editor.unfold_lines(&UnfoldLines, window, cx);
928 assert_eq!(
929 editor.display_text(cx),
930 "
931 impl Foo {
932 // Hello!
933
934 fn a() {
935 1
936 }
937
938 fn b() {⋯
939 }
940
941 fn c() {⋯
942 }
943 }
944 "
945 .unindent(),
946 );
947
948 editor.unfold_lines(&UnfoldLines, window, cx);
949 assert_eq!(
950 editor.display_text(cx),
951 editor.buffer.read(cx).read(cx).text()
952 );
953 });
954}
955
956#[gpui::test]
957fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
958 init_test(cx, |_| {});
959
960 let editor = cx.add_window(|window, cx| {
961 let buffer = MultiBuffer::build_simple(
962 &"
963 class Foo:
964 # Hello!
965
966 def a():
967 print(1)
968
969 def b():
970 print(2)
971
972 def c():
973 print(3)
974 "
975 .unindent(),
976 cx,
977 );
978 build_editor(buffer.clone(), window, cx)
979 });
980
981 _ = editor.update(cx, |editor, window, cx| {
982 editor.change_selections(None, window, cx, |s| {
983 s.select_display_ranges([
984 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
985 ]);
986 });
987 editor.fold(&Fold, window, cx);
988 assert_eq!(
989 editor.display_text(cx),
990 "
991 class Foo:
992 # Hello!
993
994 def a():
995 print(1)
996
997 def b():⋯
998
999 def c():⋯
1000 "
1001 .unindent(),
1002 );
1003
1004 editor.fold(&Fold, window, cx);
1005 assert_eq!(
1006 editor.display_text(cx),
1007 "
1008 class Foo:⋯
1009 "
1010 .unindent(),
1011 );
1012
1013 editor.unfold_lines(&UnfoldLines, window, cx);
1014 assert_eq!(
1015 editor.display_text(cx),
1016 "
1017 class Foo:
1018 # Hello!
1019
1020 def a():
1021 print(1)
1022
1023 def b():⋯
1024
1025 def c():⋯
1026 "
1027 .unindent(),
1028 );
1029
1030 editor.unfold_lines(&UnfoldLines, window, cx);
1031 assert_eq!(
1032 editor.display_text(cx),
1033 editor.buffer.read(cx).read(cx).text()
1034 );
1035 });
1036}
1037
1038#[gpui::test]
1039fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1040 init_test(cx, |_| {});
1041
1042 let editor = cx.add_window(|window, cx| {
1043 let buffer = MultiBuffer::build_simple(
1044 &"
1045 class Foo:
1046 # Hello!
1047
1048 def a():
1049 print(1)
1050
1051 def b():
1052 print(2)
1053
1054
1055 def c():
1056 print(3)
1057
1058
1059 "
1060 .unindent(),
1061 cx,
1062 );
1063 build_editor(buffer.clone(), window, cx)
1064 });
1065
1066 _ = editor.update(cx, |editor, window, cx| {
1067 editor.change_selections(None, window, cx, |s| {
1068 s.select_display_ranges([
1069 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1070 ]);
1071 });
1072 editor.fold(&Fold, window, cx);
1073 assert_eq!(
1074 editor.display_text(cx),
1075 "
1076 class Foo:
1077 # Hello!
1078
1079 def a():
1080 print(1)
1081
1082 def b():⋯
1083
1084
1085 def c():⋯
1086
1087
1088 "
1089 .unindent(),
1090 );
1091
1092 editor.fold(&Fold, window, cx);
1093 assert_eq!(
1094 editor.display_text(cx),
1095 "
1096 class Foo:⋯
1097
1098
1099 "
1100 .unindent(),
1101 );
1102
1103 editor.unfold_lines(&UnfoldLines, window, cx);
1104 assert_eq!(
1105 editor.display_text(cx),
1106 "
1107 class Foo:
1108 # Hello!
1109
1110 def a():
1111 print(1)
1112
1113 def b():⋯
1114
1115
1116 def c():⋯
1117
1118
1119 "
1120 .unindent(),
1121 );
1122
1123 editor.unfold_lines(&UnfoldLines, window, cx);
1124 assert_eq!(
1125 editor.display_text(cx),
1126 editor.buffer.read(cx).read(cx).text()
1127 );
1128 });
1129}
1130
1131#[gpui::test]
1132fn test_fold_at_level(cx: &mut TestAppContext) {
1133 init_test(cx, |_| {});
1134
1135 let editor = cx.add_window(|window, cx| {
1136 let buffer = MultiBuffer::build_simple(
1137 &"
1138 class Foo:
1139 # Hello!
1140
1141 def a():
1142 print(1)
1143
1144 def b():
1145 print(2)
1146
1147
1148 class Bar:
1149 # World!
1150
1151 def a():
1152 print(1)
1153
1154 def b():
1155 print(2)
1156
1157
1158 "
1159 .unindent(),
1160 cx,
1161 );
1162 build_editor(buffer.clone(), window, cx)
1163 });
1164
1165 _ = editor.update(cx, |editor, window, cx| {
1166 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1167 assert_eq!(
1168 editor.display_text(cx),
1169 "
1170 class Foo:
1171 # Hello!
1172
1173 def a():⋯
1174
1175 def b():⋯
1176
1177
1178 class Bar:
1179 # World!
1180
1181 def a():⋯
1182
1183 def b():⋯
1184
1185
1186 "
1187 .unindent(),
1188 );
1189
1190 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1191 assert_eq!(
1192 editor.display_text(cx),
1193 "
1194 class Foo:⋯
1195
1196
1197 class Bar:⋯
1198
1199
1200 "
1201 .unindent(),
1202 );
1203
1204 editor.unfold_all(&UnfoldAll, window, cx);
1205 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1206 assert_eq!(
1207 editor.display_text(cx),
1208 "
1209 class Foo:
1210 # Hello!
1211
1212 def a():
1213 print(1)
1214
1215 def b():
1216 print(2)
1217
1218
1219 class Bar:
1220 # World!
1221
1222 def a():
1223 print(1)
1224
1225 def b():
1226 print(2)
1227
1228
1229 "
1230 .unindent(),
1231 );
1232
1233 assert_eq!(
1234 editor.display_text(cx),
1235 editor.buffer.read(cx).read(cx).text()
1236 );
1237 });
1238}
1239
1240#[gpui::test]
1241fn test_move_cursor(cx: &mut TestAppContext) {
1242 init_test(cx, |_| {});
1243
1244 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1245 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1246
1247 buffer.update(cx, |buffer, cx| {
1248 buffer.edit(
1249 vec![
1250 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1251 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1252 ],
1253 None,
1254 cx,
1255 );
1256 });
1257 _ = editor.update(cx, |editor, window, cx| {
1258 assert_eq!(
1259 editor.selections.display_ranges(cx),
1260 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1261 );
1262
1263 editor.move_down(&MoveDown, window, cx);
1264 assert_eq!(
1265 editor.selections.display_ranges(cx),
1266 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1267 );
1268
1269 editor.move_right(&MoveRight, window, cx);
1270 assert_eq!(
1271 editor.selections.display_ranges(cx),
1272 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1273 );
1274
1275 editor.move_left(&MoveLeft, window, cx);
1276 assert_eq!(
1277 editor.selections.display_ranges(cx),
1278 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1279 );
1280
1281 editor.move_up(&MoveUp, window, cx);
1282 assert_eq!(
1283 editor.selections.display_ranges(cx),
1284 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1285 );
1286
1287 editor.move_to_end(&MoveToEnd, window, cx);
1288 assert_eq!(
1289 editor.selections.display_ranges(cx),
1290 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1291 );
1292
1293 editor.move_to_beginning(&MoveToBeginning, window, cx);
1294 assert_eq!(
1295 editor.selections.display_ranges(cx),
1296 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1297 );
1298
1299 editor.change_selections(None, window, cx, |s| {
1300 s.select_display_ranges([
1301 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1302 ]);
1303 });
1304 editor.select_to_beginning(&SelectToBeginning, window, cx);
1305 assert_eq!(
1306 editor.selections.display_ranges(cx),
1307 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1308 );
1309
1310 editor.select_to_end(&SelectToEnd, window, cx);
1311 assert_eq!(
1312 editor.selections.display_ranges(cx),
1313 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1314 );
1315 });
1316}
1317
1318// TODO: Re-enable this test
1319#[cfg(target_os = "macos")]
1320#[gpui::test]
1321fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1322 init_test(cx, |_| {});
1323
1324 let editor = cx.add_window(|window, cx| {
1325 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1326 build_editor(buffer.clone(), window, cx)
1327 });
1328
1329 assert_eq!('🟥'.len_utf8(), 4);
1330 assert_eq!('α'.len_utf8(), 2);
1331
1332 _ = editor.update(cx, |editor, window, cx| {
1333 editor.fold_creases(
1334 vec![
1335 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1336 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1337 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1338 ],
1339 true,
1340 window,
1341 cx,
1342 );
1343 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1344
1345 editor.move_right(&MoveRight, window, cx);
1346 assert_eq!(
1347 editor.selections.display_ranges(cx),
1348 &[empty_range(0, "🟥".len())]
1349 );
1350 editor.move_right(&MoveRight, window, cx);
1351 assert_eq!(
1352 editor.selections.display_ranges(cx),
1353 &[empty_range(0, "🟥🟧".len())]
1354 );
1355 editor.move_right(&MoveRight, window, cx);
1356 assert_eq!(
1357 editor.selections.display_ranges(cx),
1358 &[empty_range(0, "🟥🟧⋯".len())]
1359 );
1360
1361 editor.move_down(&MoveDown, window, cx);
1362 assert_eq!(
1363 editor.selections.display_ranges(cx),
1364 &[empty_range(1, "ab⋯e".len())]
1365 );
1366 editor.move_left(&MoveLeft, window, cx);
1367 assert_eq!(
1368 editor.selections.display_ranges(cx),
1369 &[empty_range(1, "ab⋯".len())]
1370 );
1371 editor.move_left(&MoveLeft, window, cx);
1372 assert_eq!(
1373 editor.selections.display_ranges(cx),
1374 &[empty_range(1, "ab".len())]
1375 );
1376 editor.move_left(&MoveLeft, window, cx);
1377 assert_eq!(
1378 editor.selections.display_ranges(cx),
1379 &[empty_range(1, "a".len())]
1380 );
1381
1382 editor.move_down(&MoveDown, window, cx);
1383 assert_eq!(
1384 editor.selections.display_ranges(cx),
1385 &[empty_range(2, "α".len())]
1386 );
1387 editor.move_right(&MoveRight, window, cx);
1388 assert_eq!(
1389 editor.selections.display_ranges(cx),
1390 &[empty_range(2, "αβ".len())]
1391 );
1392 editor.move_right(&MoveRight, window, cx);
1393 assert_eq!(
1394 editor.selections.display_ranges(cx),
1395 &[empty_range(2, "αβ⋯".len())]
1396 );
1397 editor.move_right(&MoveRight, window, cx);
1398 assert_eq!(
1399 editor.selections.display_ranges(cx),
1400 &[empty_range(2, "αβ⋯ε".len())]
1401 );
1402
1403 editor.move_up(&MoveUp, window, cx);
1404 assert_eq!(
1405 editor.selections.display_ranges(cx),
1406 &[empty_range(1, "ab⋯e".len())]
1407 );
1408 editor.move_down(&MoveDown, window, cx);
1409 assert_eq!(
1410 editor.selections.display_ranges(cx),
1411 &[empty_range(2, "αβ⋯ε".len())]
1412 );
1413 editor.move_up(&MoveUp, window, cx);
1414 assert_eq!(
1415 editor.selections.display_ranges(cx),
1416 &[empty_range(1, "ab⋯e".len())]
1417 );
1418
1419 editor.move_up(&MoveUp, window, cx);
1420 assert_eq!(
1421 editor.selections.display_ranges(cx),
1422 &[empty_range(0, "🟥🟧".len())]
1423 );
1424 editor.move_left(&MoveLeft, window, cx);
1425 assert_eq!(
1426 editor.selections.display_ranges(cx),
1427 &[empty_range(0, "🟥".len())]
1428 );
1429 editor.move_left(&MoveLeft, window, cx);
1430 assert_eq!(
1431 editor.selections.display_ranges(cx),
1432 &[empty_range(0, "".len())]
1433 );
1434 });
1435}
1436
1437#[gpui::test]
1438fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1439 init_test(cx, |_| {});
1440
1441 let editor = cx.add_window(|window, cx| {
1442 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1443 build_editor(buffer.clone(), window, cx)
1444 });
1445 _ = editor.update(cx, |editor, window, cx| {
1446 editor.change_selections(None, window, cx, |s| {
1447 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1448 });
1449
1450 // moving above start of document should move selection to start of document,
1451 // but the next move down should still be at the original goal_x
1452 editor.move_up(&MoveUp, window, cx);
1453 assert_eq!(
1454 editor.selections.display_ranges(cx),
1455 &[empty_range(0, "".len())]
1456 );
1457
1458 editor.move_down(&MoveDown, window, cx);
1459 assert_eq!(
1460 editor.selections.display_ranges(cx),
1461 &[empty_range(1, "abcd".len())]
1462 );
1463
1464 editor.move_down(&MoveDown, window, cx);
1465 assert_eq!(
1466 editor.selections.display_ranges(cx),
1467 &[empty_range(2, "αβγ".len())]
1468 );
1469
1470 editor.move_down(&MoveDown, window, cx);
1471 assert_eq!(
1472 editor.selections.display_ranges(cx),
1473 &[empty_range(3, "abcd".len())]
1474 );
1475
1476 editor.move_down(&MoveDown, window, cx);
1477 assert_eq!(
1478 editor.selections.display_ranges(cx),
1479 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1480 );
1481
1482 // moving past end of document should not change goal_x
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_down(&MoveDown, window, cx);
1490 assert_eq!(
1491 editor.selections.display_ranges(cx),
1492 &[empty_range(5, "".len())]
1493 );
1494
1495 editor.move_up(&MoveUp, window, cx);
1496 assert_eq!(
1497 editor.selections.display_ranges(cx),
1498 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1499 );
1500
1501 editor.move_up(&MoveUp, window, cx);
1502 assert_eq!(
1503 editor.selections.display_ranges(cx),
1504 &[empty_range(3, "abcd".len())]
1505 );
1506
1507 editor.move_up(&MoveUp, window, cx);
1508 assert_eq!(
1509 editor.selections.display_ranges(cx),
1510 &[empty_range(2, "αβγ".len())]
1511 );
1512 });
1513}
1514
1515#[gpui::test]
1516fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1517 init_test(cx, |_| {});
1518 let move_to_beg = MoveToBeginningOfLine {
1519 stop_at_soft_wraps: true,
1520 stop_at_indent: true,
1521 };
1522
1523 let delete_to_beg = DeleteToBeginningOfLine {
1524 stop_at_indent: false,
1525 };
1526
1527 let move_to_end = MoveToEndOfLine {
1528 stop_at_soft_wraps: true,
1529 };
1530
1531 let editor = cx.add_window(|window, cx| {
1532 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1533 build_editor(buffer, window, cx)
1534 });
1535 _ = editor.update(cx, |editor, window, cx| {
1536 editor.change_selections(None, window, cx, |s| {
1537 s.select_display_ranges([
1538 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1539 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
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), 2)..DisplayPoint::new(DisplayRow(1), 2),
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), 0)..DisplayPoint::new(DisplayRow(1), 0),
1562 ]
1563 );
1564 });
1565
1566 _ = editor.update(cx, |editor, window, cx| {
1567 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1568 assert_eq!(
1569 editor.selections.display_ranges(cx),
1570 &[
1571 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1572 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1573 ]
1574 );
1575 });
1576
1577 _ = editor.update(cx, |editor, window, cx| {
1578 editor.move_to_end_of_line(&move_to_end, window, cx);
1579 assert_eq!(
1580 editor.selections.display_ranges(cx),
1581 &[
1582 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1583 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1584 ]
1585 );
1586 });
1587
1588 // Moving to the end of line again is a no-op.
1589 _ = editor.update(cx, |editor, window, cx| {
1590 editor.move_to_end_of_line(&move_to_end, window, cx);
1591 assert_eq!(
1592 editor.selections.display_ranges(cx),
1593 &[
1594 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1595 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1596 ]
1597 );
1598 });
1599
1600 _ = editor.update(cx, |editor, window, cx| {
1601 editor.move_left(&MoveLeft, window, cx);
1602 editor.select_to_beginning_of_line(
1603 &SelectToBeginningOfLine {
1604 stop_at_soft_wraps: true,
1605 stop_at_indent: true,
1606 },
1607 window,
1608 cx,
1609 );
1610 assert_eq!(
1611 editor.selections.display_ranges(cx),
1612 &[
1613 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1614 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1615 ]
1616 );
1617 });
1618
1619 _ = editor.update(cx, |editor, window, cx| {
1620 editor.select_to_beginning_of_line(
1621 &SelectToBeginningOfLine {
1622 stop_at_soft_wraps: true,
1623 stop_at_indent: true,
1624 },
1625 window,
1626 cx,
1627 );
1628 assert_eq!(
1629 editor.selections.display_ranges(cx),
1630 &[
1631 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1632 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1633 ]
1634 );
1635 });
1636
1637 _ = editor.update(cx, |editor, window, cx| {
1638 editor.select_to_beginning_of_line(
1639 &SelectToBeginningOfLine {
1640 stop_at_soft_wraps: true,
1641 stop_at_indent: true,
1642 },
1643 window,
1644 cx,
1645 );
1646 assert_eq!(
1647 editor.selections.display_ranges(cx),
1648 &[
1649 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1650 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1651 ]
1652 );
1653 });
1654
1655 _ = editor.update(cx, |editor, window, cx| {
1656 editor.select_to_end_of_line(
1657 &SelectToEndOfLine {
1658 stop_at_soft_wraps: true,
1659 },
1660 window,
1661 cx,
1662 );
1663 assert_eq!(
1664 editor.selections.display_ranges(cx),
1665 &[
1666 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1667 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1668 ]
1669 );
1670 });
1671
1672 _ = editor.update(cx, |editor, window, cx| {
1673 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1674 assert_eq!(editor.display_text(cx), "ab\n de");
1675 assert_eq!(
1676 editor.selections.display_ranges(cx),
1677 &[
1678 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1679 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1680 ]
1681 );
1682 });
1683
1684 _ = editor.update(cx, |editor, window, cx| {
1685 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1686 assert_eq!(editor.display_text(cx), "\n");
1687 assert_eq!(
1688 editor.selections.display_ranges(cx),
1689 &[
1690 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1691 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1692 ]
1693 );
1694 });
1695}
1696
1697#[gpui::test]
1698fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1699 init_test(cx, |_| {});
1700 let move_to_beg = MoveToBeginningOfLine {
1701 stop_at_soft_wraps: false,
1702 stop_at_indent: false,
1703 };
1704
1705 let move_to_end = MoveToEndOfLine {
1706 stop_at_soft_wraps: false,
1707 };
1708
1709 let editor = cx.add_window(|window, cx| {
1710 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1711 build_editor(buffer, window, cx)
1712 });
1713
1714 _ = editor.update(cx, |editor, window, cx| {
1715 editor.set_wrap_width(Some(140.0.into()), cx);
1716
1717 // We expect the following lines after wrapping
1718 // ```
1719 // thequickbrownfox
1720 // jumpedoverthelazydo
1721 // gs
1722 // ```
1723 // The final `gs` was soft-wrapped onto a new line.
1724 assert_eq!(
1725 "thequickbrownfox\njumpedoverthelaz\nydogs",
1726 editor.display_text(cx),
1727 );
1728
1729 // First, let's assert behavior on the first line, that was not soft-wrapped.
1730 // Start the cursor at the `k` on the first line
1731 editor.change_selections(None, window, cx, |s| {
1732 s.select_display_ranges([
1733 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1734 ]);
1735 });
1736
1737 // Moving to the beginning of the line should put us at the beginning of the line.
1738 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1739 assert_eq!(
1740 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1741 editor.selections.display_ranges(cx)
1742 );
1743
1744 // Moving to the end of the line should put us at the end of the line.
1745 editor.move_to_end_of_line(&move_to_end, window, cx);
1746 assert_eq!(
1747 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1748 editor.selections.display_ranges(cx)
1749 );
1750
1751 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1752 // Start the cursor at the last line (`y` that was wrapped to a new line)
1753 editor.change_selections(None, window, cx, |s| {
1754 s.select_display_ranges([
1755 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1756 ]);
1757 });
1758
1759 // Moving to the beginning of the line should put us at the start of the second line of
1760 // display text, i.e., the `j`.
1761 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1762 assert_eq!(
1763 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1764 editor.selections.display_ranges(cx)
1765 );
1766
1767 // Moving to the beginning of the line again should be a no-op.
1768 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1769 assert_eq!(
1770 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1771 editor.selections.display_ranges(cx)
1772 );
1773
1774 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1775 // next display line.
1776 editor.move_to_end_of_line(&move_to_end, window, cx);
1777 assert_eq!(
1778 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1779 editor.selections.display_ranges(cx)
1780 );
1781
1782 // Moving to the end of the line again should be a no-op.
1783 editor.move_to_end_of_line(&move_to_end, window, cx);
1784 assert_eq!(
1785 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1786 editor.selections.display_ranges(cx)
1787 );
1788 });
1789}
1790
1791#[gpui::test]
1792fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1793 init_test(cx, |_| {});
1794
1795 let move_to_beg = MoveToBeginningOfLine {
1796 stop_at_soft_wraps: true,
1797 stop_at_indent: true,
1798 };
1799
1800 let select_to_beg = SelectToBeginningOfLine {
1801 stop_at_soft_wraps: true,
1802 stop_at_indent: true,
1803 };
1804
1805 let delete_to_beg = DeleteToBeginningOfLine {
1806 stop_at_indent: true,
1807 };
1808
1809 let move_to_end = MoveToEndOfLine {
1810 stop_at_soft_wraps: false,
1811 };
1812
1813 let editor = cx.add_window(|window, cx| {
1814 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1815 build_editor(buffer, window, cx)
1816 });
1817
1818 _ = editor.update(cx, |editor, window, cx| {
1819 editor.change_selections(None, window, cx, |s| {
1820 s.select_display_ranges([
1821 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1822 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1823 ]);
1824 });
1825
1826 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1827 // and the second cursor at the first non-whitespace character in the line.
1828 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1829 assert_eq!(
1830 editor.selections.display_ranges(cx),
1831 &[
1832 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1833 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1834 ]
1835 );
1836
1837 // Moving to the beginning of the line again should be a no-op for the first cursor,
1838 // and should move the second cursor to the beginning of the line.
1839 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1840 assert_eq!(
1841 editor.selections.display_ranges(cx),
1842 &[
1843 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1844 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1845 ]
1846 );
1847
1848 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1849 // and should move the second cursor back to the first non-whitespace character in the line.
1850 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1851 assert_eq!(
1852 editor.selections.display_ranges(cx),
1853 &[
1854 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1855 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1856 ]
1857 );
1858
1859 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1860 // and to the first non-whitespace character in the line for the second cursor.
1861 editor.move_to_end_of_line(&move_to_end, window, cx);
1862 editor.move_left(&MoveLeft, window, cx);
1863 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1864 assert_eq!(
1865 editor.selections.display_ranges(cx),
1866 &[
1867 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1868 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1869 ]
1870 );
1871
1872 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1873 // and should select to the beginning of the line for the second cursor.
1874 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1875 assert_eq!(
1876 editor.selections.display_ranges(cx),
1877 &[
1878 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1879 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1880 ]
1881 );
1882
1883 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1884 // and should delete to the first non-whitespace character in the line for the second cursor.
1885 editor.move_to_end_of_line(&move_to_end, window, cx);
1886 editor.move_left(&MoveLeft, window, cx);
1887 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1888 assert_eq!(editor.text(cx), "c\n f");
1889 });
1890}
1891
1892#[gpui::test]
1893fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1894 init_test(cx, |_| {});
1895
1896 let editor = cx.add_window(|window, cx| {
1897 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1898 build_editor(buffer, window, cx)
1899 });
1900 _ = editor.update(cx, |editor, window, cx| {
1901 editor.change_selections(None, window, cx, |s| {
1902 s.select_display_ranges([
1903 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1904 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1905 ])
1906 });
1907
1908 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1909 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1910
1911 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1912 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1913
1914 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1915 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1916
1917 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1918 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1919
1920 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1921 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1922
1923 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1924 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1925
1926 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1927 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1928
1929 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1930 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1931
1932 editor.move_right(&MoveRight, window, cx);
1933 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1934 assert_selection_ranges(
1935 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1936 editor,
1937 cx,
1938 );
1939
1940 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1941 assert_selection_ranges(
1942 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1943 editor,
1944 cx,
1945 );
1946
1947 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1948 assert_selection_ranges(
1949 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1950 editor,
1951 cx,
1952 );
1953 });
1954}
1955
1956#[gpui::test]
1957fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1958 init_test(cx, |_| {});
1959
1960 let editor = cx.add_window(|window, cx| {
1961 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1962 build_editor(buffer, window, cx)
1963 });
1964
1965 _ = editor.update(cx, |editor, window, cx| {
1966 editor.set_wrap_width(Some(140.0.into()), cx);
1967 assert_eq!(
1968 editor.display_text(cx),
1969 "use one::{\n two::three::\n four::five\n};"
1970 );
1971
1972 editor.change_selections(None, window, cx, |s| {
1973 s.select_display_ranges([
1974 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1975 ]);
1976 });
1977
1978 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1979 assert_eq!(
1980 editor.selections.display_ranges(cx),
1981 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1982 );
1983
1984 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1985 assert_eq!(
1986 editor.selections.display_ranges(cx),
1987 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1988 );
1989
1990 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1991 assert_eq!(
1992 editor.selections.display_ranges(cx),
1993 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1994 );
1995
1996 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1997 assert_eq!(
1998 editor.selections.display_ranges(cx),
1999 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2000 );
2001
2002 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2003 assert_eq!(
2004 editor.selections.display_ranges(cx),
2005 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2006 );
2007
2008 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2009 assert_eq!(
2010 editor.selections.display_ranges(cx),
2011 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2012 );
2013 });
2014}
2015
2016#[gpui::test]
2017async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2018 init_test(cx, |_| {});
2019 let mut cx = EditorTestContext::new(cx).await;
2020
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 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2029
2030 cx.set_state(
2031 &r#"ˇone
2032 two
2033
2034 three
2035 fourˇ
2036 five
2037
2038 six"#
2039 .unindent(),
2040 );
2041
2042 cx.update_editor(|editor, window, cx| {
2043 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2044 });
2045 cx.assert_editor_state(
2046 &r#"one
2047 two
2048 ˇ
2049 three
2050 four
2051 five
2052 ˇ
2053 six"#
2054 .unindent(),
2055 );
2056
2057 cx.update_editor(|editor, window, cx| {
2058 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2059 });
2060 cx.assert_editor_state(
2061 &r#"one
2062 two
2063
2064 three
2065 four
2066 five
2067 ˇ
2068 sixˇ"#
2069 .unindent(),
2070 );
2071
2072 cx.update_editor(|editor, window, cx| {
2073 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2074 });
2075 cx.assert_editor_state(
2076 &r#"one
2077 two
2078
2079 three
2080 four
2081 five
2082
2083 sixˇ"#
2084 .unindent(),
2085 );
2086
2087 cx.update_editor(|editor, window, cx| {
2088 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2089 });
2090 cx.assert_editor_state(
2091 &r#"one
2092 two
2093
2094 three
2095 four
2096 five
2097 ˇ
2098 six"#
2099 .unindent(),
2100 );
2101
2102 cx.update_editor(|editor, window, cx| {
2103 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2104 });
2105 cx.assert_editor_state(
2106 &r#"one
2107 two
2108 ˇ
2109 three
2110 four
2111 five
2112
2113 six"#
2114 .unindent(),
2115 );
2116
2117 cx.update_editor(|editor, window, cx| {
2118 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2119 });
2120 cx.assert_editor_state(
2121 &r#"ˇone
2122 two
2123
2124 three
2125 four
2126 five
2127
2128 six"#
2129 .unindent(),
2130 );
2131}
2132
2133#[gpui::test]
2134async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2135 init_test(cx, |_| {});
2136 let mut cx = EditorTestContext::new(cx).await;
2137 let line_height = cx.editor(|editor, window, _| {
2138 editor
2139 .style()
2140 .unwrap()
2141 .text
2142 .line_height_in_pixels(window.rem_size())
2143 });
2144 let window = cx.window;
2145 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2146
2147 cx.set_state(
2148 r#"ˇone
2149 two
2150 three
2151 four
2152 five
2153 six
2154 seven
2155 eight
2156 nine
2157 ten
2158 "#,
2159 );
2160
2161 cx.update_editor(|editor, window, cx| {
2162 assert_eq!(
2163 editor.snapshot(window, cx).scroll_position(),
2164 gpui::Point::new(0., 0.)
2165 );
2166 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2167 assert_eq!(
2168 editor.snapshot(window, cx).scroll_position(),
2169 gpui::Point::new(0., 3.)
2170 );
2171 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2172 assert_eq!(
2173 editor.snapshot(window, cx).scroll_position(),
2174 gpui::Point::new(0., 6.)
2175 );
2176 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2177 assert_eq!(
2178 editor.snapshot(window, cx).scroll_position(),
2179 gpui::Point::new(0., 3.)
2180 );
2181
2182 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2183 assert_eq!(
2184 editor.snapshot(window, cx).scroll_position(),
2185 gpui::Point::new(0., 1.)
2186 );
2187 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2188 assert_eq!(
2189 editor.snapshot(window, cx).scroll_position(),
2190 gpui::Point::new(0., 3.)
2191 );
2192 });
2193}
2194
2195#[gpui::test]
2196async fn test_autoscroll(cx: &mut TestAppContext) {
2197 init_test(cx, |_| {});
2198 let mut cx = EditorTestContext::new(cx).await;
2199
2200 let line_height = cx.update_editor(|editor, window, cx| {
2201 editor.set_vertical_scroll_margin(2, cx);
2202 editor
2203 .style()
2204 .unwrap()
2205 .text
2206 .line_height_in_pixels(window.rem_size())
2207 });
2208 let window = cx.window;
2209 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2210
2211 cx.set_state(
2212 r#"ˇone
2213 two
2214 three
2215 four
2216 five
2217 six
2218 seven
2219 eight
2220 nine
2221 ten
2222 "#,
2223 );
2224 cx.update_editor(|editor, window, cx| {
2225 assert_eq!(
2226 editor.snapshot(window, cx).scroll_position(),
2227 gpui::Point::new(0., 0.0)
2228 );
2229 });
2230
2231 // Add a cursor below the visible area. Since both cursors cannot fit
2232 // on screen, the editor autoscrolls to reveal the newest cursor, and
2233 // allows the vertical scroll margin below that cursor.
2234 cx.update_editor(|editor, window, cx| {
2235 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2236 selections.select_ranges([
2237 Point::new(0, 0)..Point::new(0, 0),
2238 Point::new(6, 0)..Point::new(6, 0),
2239 ]);
2240 })
2241 });
2242 cx.update_editor(|editor, window, cx| {
2243 assert_eq!(
2244 editor.snapshot(window, cx).scroll_position(),
2245 gpui::Point::new(0., 3.0)
2246 );
2247 });
2248
2249 // Move down. The editor cursor scrolls down to track the newest cursor.
2250 cx.update_editor(|editor, window, cx| {
2251 editor.move_down(&Default::default(), window, cx);
2252 });
2253 cx.update_editor(|editor, window, cx| {
2254 assert_eq!(
2255 editor.snapshot(window, cx).scroll_position(),
2256 gpui::Point::new(0., 4.0)
2257 );
2258 });
2259
2260 // Add a cursor above the visible area. Since both cursors fit on screen,
2261 // the editor scrolls to show both.
2262 cx.update_editor(|editor, window, cx| {
2263 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2264 selections.select_ranges([
2265 Point::new(1, 0)..Point::new(1, 0),
2266 Point::new(6, 0)..Point::new(6, 0),
2267 ]);
2268 })
2269 });
2270 cx.update_editor(|editor, window, cx| {
2271 assert_eq!(
2272 editor.snapshot(window, cx).scroll_position(),
2273 gpui::Point::new(0., 1.0)
2274 );
2275 });
2276}
2277
2278#[gpui::test]
2279async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2280 init_test(cx, |_| {});
2281 let mut cx = EditorTestContext::new(cx).await;
2282
2283 let line_height = cx.editor(|editor, window, _cx| {
2284 editor
2285 .style()
2286 .unwrap()
2287 .text
2288 .line_height_in_pixels(window.rem_size())
2289 });
2290 let window = cx.window;
2291 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2292 cx.set_state(
2293 &r#"
2294 ˇone
2295 two
2296 threeˇ
2297 four
2298 five
2299 six
2300 seven
2301 eight
2302 nine
2303 ten
2304 "#
2305 .unindent(),
2306 );
2307
2308 cx.update_editor(|editor, window, cx| {
2309 editor.move_page_down(&MovePageDown::default(), window, cx)
2310 });
2311 cx.assert_editor_state(
2312 &r#"
2313 one
2314 two
2315 three
2316 ˇfour
2317 five
2318 sixˇ
2319 seven
2320 eight
2321 nine
2322 ten
2323 "#
2324 .unindent(),
2325 );
2326
2327 cx.update_editor(|editor, window, cx| {
2328 editor.move_page_down(&MovePageDown::default(), window, cx)
2329 });
2330 cx.assert_editor_state(
2331 &r#"
2332 one
2333 two
2334 three
2335 four
2336 five
2337 six
2338 ˇseven
2339 eight
2340 nineˇ
2341 ten
2342 "#
2343 .unindent(),
2344 );
2345
2346 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2347 cx.assert_editor_state(
2348 &r#"
2349 one
2350 two
2351 three
2352 ˇfour
2353 five
2354 sixˇ
2355 seven
2356 eight
2357 nine
2358 ten
2359 "#
2360 .unindent(),
2361 );
2362
2363 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2364 cx.assert_editor_state(
2365 &r#"
2366 ˇone
2367 two
2368 threeˇ
2369 four
2370 five
2371 six
2372 seven
2373 eight
2374 nine
2375 ten
2376 "#
2377 .unindent(),
2378 );
2379
2380 // Test select collapsing
2381 cx.update_editor(|editor, window, cx| {
2382 editor.move_page_down(&MovePageDown::default(), window, cx);
2383 editor.move_page_down(&MovePageDown::default(), window, cx);
2384 editor.move_page_down(&MovePageDown::default(), window, cx);
2385 });
2386 cx.assert_editor_state(
2387 &r#"
2388 one
2389 two
2390 three
2391 four
2392 five
2393 six
2394 seven
2395 eight
2396 nine
2397 ˇten
2398 ˇ"#
2399 .unindent(),
2400 );
2401}
2402
2403#[gpui::test]
2404async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2405 init_test(cx, |_| {});
2406 let mut cx = EditorTestContext::new(cx).await;
2407 cx.set_state("one «two threeˇ» four");
2408 cx.update_editor(|editor, window, cx| {
2409 editor.delete_to_beginning_of_line(
2410 &DeleteToBeginningOfLine {
2411 stop_at_indent: false,
2412 },
2413 window,
2414 cx,
2415 );
2416 assert_eq!(editor.text(cx), " four");
2417 });
2418}
2419
2420#[gpui::test]
2421fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2422 init_test(cx, |_| {});
2423
2424 let editor = cx.add_window(|window, cx| {
2425 let buffer = MultiBuffer::build_simple("one two three four", cx);
2426 build_editor(buffer.clone(), window, cx)
2427 });
2428
2429 _ = editor.update(cx, |editor, window, cx| {
2430 editor.change_selections(None, window, cx, |s| {
2431 s.select_display_ranges([
2432 // an empty selection - the preceding word fragment is deleted
2433 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2434 // characters selected - they are deleted
2435 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2436 ])
2437 });
2438 editor.delete_to_previous_word_start(
2439 &DeleteToPreviousWordStart {
2440 ignore_newlines: false,
2441 },
2442 window,
2443 cx,
2444 );
2445 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2446 });
2447
2448 _ = editor.update(cx, |editor, window, cx| {
2449 editor.change_selections(None, window, cx, |s| {
2450 s.select_display_ranges([
2451 // an empty selection - the following word fragment is deleted
2452 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2453 // characters selected - they are deleted
2454 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2455 ])
2456 });
2457 editor.delete_to_next_word_end(
2458 &DeleteToNextWordEnd {
2459 ignore_newlines: false,
2460 },
2461 window,
2462 cx,
2463 );
2464 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2465 });
2466}
2467
2468#[gpui::test]
2469fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2470 init_test(cx, |_| {});
2471
2472 let editor = cx.add_window(|window, cx| {
2473 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2474 build_editor(buffer.clone(), window, cx)
2475 });
2476 let del_to_prev_word_start = DeleteToPreviousWordStart {
2477 ignore_newlines: false,
2478 };
2479 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2480 ignore_newlines: true,
2481 };
2482
2483 _ = editor.update(cx, |editor, window, cx| {
2484 editor.change_selections(None, window, cx, |s| {
2485 s.select_display_ranges([
2486 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2487 ])
2488 });
2489 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2490 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2491 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2492 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2493 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2494 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2495 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2496 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2497 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2498 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2499 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2500 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2501 });
2502}
2503
2504#[gpui::test]
2505fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2506 init_test(cx, |_| {});
2507
2508 let editor = cx.add_window(|window, cx| {
2509 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2510 build_editor(buffer.clone(), window, cx)
2511 });
2512 let del_to_next_word_end = DeleteToNextWordEnd {
2513 ignore_newlines: false,
2514 };
2515 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2516 ignore_newlines: true,
2517 };
2518
2519 _ = editor.update(cx, |editor, window, cx| {
2520 editor.change_selections(None, window, cx, |s| {
2521 s.select_display_ranges([
2522 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2523 ])
2524 });
2525 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2526 assert_eq!(
2527 editor.buffer.read(cx).read(cx).text(),
2528 "one\n two\nthree\n four"
2529 );
2530 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2531 assert_eq!(
2532 editor.buffer.read(cx).read(cx).text(),
2533 "\n two\nthree\n four"
2534 );
2535 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2536 assert_eq!(
2537 editor.buffer.read(cx).read(cx).text(),
2538 "two\nthree\n four"
2539 );
2540 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2541 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2542 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2543 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2544 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2545 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2546 });
2547}
2548
2549#[gpui::test]
2550fn test_newline(cx: &mut TestAppContext) {
2551 init_test(cx, |_| {});
2552
2553 let editor = cx.add_window(|window, cx| {
2554 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2555 build_editor(buffer.clone(), window, cx)
2556 });
2557
2558 _ = editor.update(cx, |editor, window, cx| {
2559 editor.change_selections(None, window, cx, |s| {
2560 s.select_display_ranges([
2561 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2562 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2563 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2564 ])
2565 });
2566
2567 editor.newline(&Newline, window, cx);
2568 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2569 });
2570}
2571
2572#[gpui::test]
2573fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2574 init_test(cx, |_| {});
2575
2576 let editor = cx.add_window(|window, cx| {
2577 let buffer = MultiBuffer::build_simple(
2578 "
2579 a
2580 b(
2581 X
2582 )
2583 c(
2584 X
2585 )
2586 "
2587 .unindent()
2588 .as_str(),
2589 cx,
2590 );
2591 let mut editor = build_editor(buffer.clone(), window, cx);
2592 editor.change_selections(None, window, cx, |s| {
2593 s.select_ranges([
2594 Point::new(2, 4)..Point::new(2, 5),
2595 Point::new(5, 4)..Point::new(5, 5),
2596 ])
2597 });
2598 editor
2599 });
2600
2601 _ = editor.update(cx, |editor, window, cx| {
2602 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2603 editor.buffer.update(cx, |buffer, cx| {
2604 buffer.edit(
2605 [
2606 (Point::new(1, 2)..Point::new(3, 0), ""),
2607 (Point::new(4, 2)..Point::new(6, 0), ""),
2608 ],
2609 None,
2610 cx,
2611 );
2612 assert_eq!(
2613 buffer.read(cx).text(),
2614 "
2615 a
2616 b()
2617 c()
2618 "
2619 .unindent()
2620 );
2621 });
2622 assert_eq!(
2623 editor.selections.ranges(cx),
2624 &[
2625 Point::new(1, 2)..Point::new(1, 2),
2626 Point::new(2, 2)..Point::new(2, 2),
2627 ],
2628 );
2629
2630 editor.newline(&Newline, window, cx);
2631 assert_eq!(
2632 editor.text(cx),
2633 "
2634 a
2635 b(
2636 )
2637 c(
2638 )
2639 "
2640 .unindent()
2641 );
2642
2643 // The selections are moved after the inserted newlines
2644 assert_eq!(
2645 editor.selections.ranges(cx),
2646 &[
2647 Point::new(2, 0)..Point::new(2, 0),
2648 Point::new(4, 0)..Point::new(4, 0),
2649 ],
2650 );
2651 });
2652}
2653
2654#[gpui::test]
2655async fn test_newline_above(cx: &mut TestAppContext) {
2656 init_test(cx, |settings| {
2657 settings.defaults.tab_size = NonZeroU32::new(4)
2658 });
2659
2660 let language = Arc::new(
2661 Language::new(
2662 LanguageConfig::default(),
2663 Some(tree_sitter_rust::LANGUAGE.into()),
2664 )
2665 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2666 .unwrap(),
2667 );
2668
2669 let mut cx = EditorTestContext::new(cx).await;
2670 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2671 cx.set_state(indoc! {"
2672 const a: ˇA = (
2673 (ˇ
2674 «const_functionˇ»(ˇ),
2675 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2676 )ˇ
2677 ˇ);ˇ
2678 "});
2679
2680 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2681 cx.assert_editor_state(indoc! {"
2682 ˇ
2683 const a: A = (
2684 ˇ
2685 (
2686 ˇ
2687 ˇ
2688 const_function(),
2689 ˇ
2690 ˇ
2691 ˇ
2692 ˇ
2693 something_else,
2694 ˇ
2695 )
2696 ˇ
2697 ˇ
2698 );
2699 "});
2700}
2701
2702#[gpui::test]
2703async fn test_newline_below(cx: &mut TestAppContext) {
2704 init_test(cx, |settings| {
2705 settings.defaults.tab_size = NonZeroU32::new(4)
2706 });
2707
2708 let language = Arc::new(
2709 Language::new(
2710 LanguageConfig::default(),
2711 Some(tree_sitter_rust::LANGUAGE.into()),
2712 )
2713 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2714 .unwrap(),
2715 );
2716
2717 let mut cx = EditorTestContext::new(cx).await;
2718 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2719 cx.set_state(indoc! {"
2720 const a: ˇA = (
2721 (ˇ
2722 «const_functionˇ»(ˇ),
2723 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2724 )ˇ
2725 ˇ);ˇ
2726 "});
2727
2728 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2729 cx.assert_editor_state(indoc! {"
2730 const a: A = (
2731 ˇ
2732 (
2733 ˇ
2734 const_function(),
2735 ˇ
2736 ˇ
2737 something_else,
2738 ˇ
2739 ˇ
2740 ˇ
2741 ˇ
2742 )
2743 ˇ
2744 );
2745 ˇ
2746 ˇ
2747 "});
2748}
2749
2750#[gpui::test]
2751async fn test_newline_comments(cx: &mut TestAppContext) {
2752 init_test(cx, |settings| {
2753 settings.defaults.tab_size = NonZeroU32::new(4)
2754 });
2755
2756 let language = Arc::new(Language::new(
2757 LanguageConfig {
2758 line_comments: vec!["//".into()],
2759 ..LanguageConfig::default()
2760 },
2761 None,
2762 ));
2763 {
2764 let mut cx = EditorTestContext::new(cx).await;
2765 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2766 cx.set_state(indoc! {"
2767 // Fooˇ
2768 "});
2769
2770 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2771 cx.assert_editor_state(indoc! {"
2772 // Foo
2773 //ˇ
2774 "});
2775 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2776 cx.set_state(indoc! {"
2777 ˇ// Foo
2778 "});
2779 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2780 cx.assert_editor_state(indoc! {"
2781
2782 ˇ// Foo
2783 "});
2784 }
2785 // Ensure that comment continuations can be disabled.
2786 update_test_language_settings(cx, |settings| {
2787 settings.defaults.extend_comment_on_newline = Some(false);
2788 });
2789 let mut cx = EditorTestContext::new(cx).await;
2790 cx.set_state(indoc! {"
2791 // Fooˇ
2792 "});
2793 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2794 cx.assert_editor_state(indoc! {"
2795 // Foo
2796 ˇ
2797 "});
2798}
2799
2800#[gpui::test]
2801fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2802 init_test(cx, |_| {});
2803
2804 let editor = cx.add_window(|window, cx| {
2805 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2806 let mut editor = build_editor(buffer.clone(), window, cx);
2807 editor.change_selections(None, window, cx, |s| {
2808 s.select_ranges([3..4, 11..12, 19..20])
2809 });
2810 editor
2811 });
2812
2813 _ = editor.update(cx, |editor, window, cx| {
2814 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2815 editor.buffer.update(cx, |buffer, cx| {
2816 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2817 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2818 });
2819 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2820
2821 editor.insert("Z", window, cx);
2822 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2823
2824 // The selections are moved after the inserted characters
2825 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2826 });
2827}
2828
2829#[gpui::test]
2830async fn test_tab(cx: &mut TestAppContext) {
2831 init_test(cx, |settings| {
2832 settings.defaults.tab_size = NonZeroU32::new(3)
2833 });
2834
2835 let mut cx = EditorTestContext::new(cx).await;
2836 cx.set_state(indoc! {"
2837 ˇabˇc
2838 ˇ🏀ˇ🏀ˇefg
2839 dˇ
2840 "});
2841 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2842 cx.assert_editor_state(indoc! {"
2843 ˇab ˇc
2844 ˇ🏀 ˇ🏀 ˇefg
2845 d ˇ
2846 "});
2847
2848 cx.set_state(indoc! {"
2849 a
2850 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2851 "});
2852 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2853 cx.assert_editor_state(indoc! {"
2854 a
2855 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2856 "});
2857}
2858
2859#[gpui::test]
2860async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
2861 init_test(cx, |_| {});
2862
2863 let mut cx = EditorTestContext::new(cx).await;
2864 let language = Arc::new(
2865 Language::new(
2866 LanguageConfig::default(),
2867 Some(tree_sitter_rust::LANGUAGE.into()),
2868 )
2869 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2870 .unwrap(),
2871 );
2872 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2873
2874 // cursors that are already at the suggested indent level insert
2875 // a soft tab. cursors that are to the left of the suggested indent
2876 // auto-indent their line.
2877 cx.set_state(indoc! {"
2878 ˇ
2879 const a: B = (
2880 c(
2881 d(
2882 ˇ
2883 )
2884 ˇ
2885 ˇ )
2886 );
2887 "});
2888 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2889 cx.assert_editor_state(indoc! {"
2890 ˇ
2891 const a: B = (
2892 c(
2893 d(
2894 ˇ
2895 )
2896 ˇ
2897 ˇ)
2898 );
2899 "});
2900
2901 // handle auto-indent when there are multiple cursors on the same line
2902 cx.set_state(indoc! {"
2903 const a: B = (
2904 c(
2905 ˇ ˇ
2906 ˇ )
2907 );
2908 "});
2909 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2910 cx.assert_editor_state(indoc! {"
2911 const a: B = (
2912 c(
2913 ˇ
2914 ˇ)
2915 );
2916 "});
2917}
2918
2919#[gpui::test]
2920async fn test_tab_with_mixed_whitespace(cx: &mut TestAppContext) {
2921 init_test(cx, |settings| {
2922 settings.defaults.tab_size = NonZeroU32::new(4)
2923 });
2924
2925 let language = Arc::new(
2926 Language::new(
2927 LanguageConfig::default(),
2928 Some(tree_sitter_rust::LANGUAGE.into()),
2929 )
2930 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2931 .unwrap(),
2932 );
2933
2934 let mut cx = EditorTestContext::new(cx).await;
2935 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2936 cx.set_state(indoc! {"
2937 fn a() {
2938 if b {
2939 \t ˇc
2940 }
2941 }
2942 "});
2943
2944 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2945 cx.assert_editor_state(indoc! {"
2946 fn a() {
2947 if b {
2948 ˇc
2949 }
2950 }
2951 "});
2952}
2953
2954#[gpui::test]
2955async fn test_indent_outdent(cx: &mut TestAppContext) {
2956 init_test(cx, |settings| {
2957 settings.defaults.tab_size = NonZeroU32::new(4);
2958 });
2959
2960 let mut cx = EditorTestContext::new(cx).await;
2961
2962 cx.set_state(indoc! {"
2963 «oneˇ» «twoˇ»
2964 three
2965 four
2966 "});
2967 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2968 cx.assert_editor_state(indoc! {"
2969 «oneˇ» «twoˇ»
2970 three
2971 four
2972 "});
2973
2974 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
2975 cx.assert_editor_state(indoc! {"
2976 «oneˇ» «twoˇ»
2977 three
2978 four
2979 "});
2980
2981 // select across line ending
2982 cx.set_state(indoc! {"
2983 one two
2984 t«hree
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«hree
2991 ˇ» four
2992 "});
2993
2994 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
2995 cx.assert_editor_state(indoc! {"
2996 one two
2997 t«hree
2998 ˇ» four
2999 "});
3000
3001 // Ensure that indenting/outdenting works when the cursor is at column 0.
3002 cx.set_state(indoc! {"
3003 one two
3004 ˇthree
3005 four
3006 "});
3007 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3008 cx.assert_editor_state(indoc! {"
3009 one two
3010 ˇthree
3011 four
3012 "});
3013
3014 cx.set_state(indoc! {"
3015 one two
3016 ˇ three
3017 four
3018 "});
3019 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3020 cx.assert_editor_state(indoc! {"
3021 one two
3022 ˇthree
3023 four
3024 "});
3025}
3026
3027#[gpui::test]
3028async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3029 init_test(cx, |settings| {
3030 settings.defaults.hard_tabs = Some(true);
3031 });
3032
3033 let mut cx = EditorTestContext::new(cx).await;
3034
3035 // select two ranges on one line
3036 cx.set_state(indoc! {"
3037 «oneˇ» «twoˇ»
3038 three
3039 four
3040 "});
3041 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3042 cx.assert_editor_state(indoc! {"
3043 \t«oneˇ» «twoˇ»
3044 three
3045 four
3046 "});
3047 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3048 cx.assert_editor_state(indoc! {"
3049 \t\t«oneˇ» «twoˇ»
3050 three
3051 four
3052 "});
3053 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3054 cx.assert_editor_state(indoc! {"
3055 \t«oneˇ» «twoˇ»
3056 three
3057 four
3058 "});
3059 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3060 cx.assert_editor_state(indoc! {"
3061 «oneˇ» «twoˇ»
3062 three
3063 four
3064 "});
3065
3066 // select across a line ending
3067 cx.set_state(indoc! {"
3068 one two
3069 t«hree
3070 ˇ»four
3071 "});
3072 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3073 cx.assert_editor_state(indoc! {"
3074 one two
3075 \tt«hree
3076 ˇ»four
3077 "});
3078 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3079 cx.assert_editor_state(indoc! {"
3080 one two
3081 \t\tt«hree
3082 ˇ»four
3083 "});
3084 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3085 cx.assert_editor_state(indoc! {"
3086 one two
3087 \tt«hree
3088 ˇ»four
3089 "});
3090 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3091 cx.assert_editor_state(indoc! {"
3092 one two
3093 t«hree
3094 ˇ»four
3095 "});
3096
3097 // Ensure that indenting/outdenting works when the cursor is at column 0.
3098 cx.set_state(indoc! {"
3099 one two
3100 ˇthree
3101 four
3102 "});
3103 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3104 cx.assert_editor_state(indoc! {"
3105 one two
3106 ˇthree
3107 four
3108 "});
3109 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3110 cx.assert_editor_state(indoc! {"
3111 one two
3112 \tˇthree
3113 four
3114 "});
3115 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3116 cx.assert_editor_state(indoc! {"
3117 one two
3118 ˇthree
3119 four
3120 "});
3121}
3122
3123#[gpui::test]
3124fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3125 init_test(cx, |settings| {
3126 settings.languages.extend([
3127 (
3128 "TOML".into(),
3129 LanguageSettingsContent {
3130 tab_size: NonZeroU32::new(2),
3131 ..Default::default()
3132 },
3133 ),
3134 (
3135 "Rust".into(),
3136 LanguageSettingsContent {
3137 tab_size: NonZeroU32::new(4),
3138 ..Default::default()
3139 },
3140 ),
3141 ]);
3142 });
3143
3144 let toml_language = Arc::new(Language::new(
3145 LanguageConfig {
3146 name: "TOML".into(),
3147 ..Default::default()
3148 },
3149 None,
3150 ));
3151 let rust_language = Arc::new(Language::new(
3152 LanguageConfig {
3153 name: "Rust".into(),
3154 ..Default::default()
3155 },
3156 None,
3157 ));
3158
3159 let toml_buffer =
3160 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3161 let rust_buffer =
3162 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3163 let multibuffer = cx.new(|cx| {
3164 let mut multibuffer = MultiBuffer::new(ReadWrite);
3165 multibuffer.push_excerpts(
3166 toml_buffer.clone(),
3167 [ExcerptRange {
3168 context: Point::new(0, 0)..Point::new(2, 0),
3169 primary: None,
3170 }],
3171 cx,
3172 );
3173 multibuffer.push_excerpts(
3174 rust_buffer.clone(),
3175 [ExcerptRange {
3176 context: Point::new(0, 0)..Point::new(1, 0),
3177 primary: None,
3178 }],
3179 cx,
3180 );
3181 multibuffer
3182 });
3183
3184 cx.add_window(|window, cx| {
3185 let mut editor = build_editor(multibuffer, window, cx);
3186
3187 assert_eq!(
3188 editor.text(cx),
3189 indoc! {"
3190 a = 1
3191 b = 2
3192
3193 const c: usize = 3;
3194 "}
3195 );
3196
3197 select_ranges(
3198 &mut editor,
3199 indoc! {"
3200 «aˇ» = 1
3201 b = 2
3202
3203 «const c:ˇ» usize = 3;
3204 "},
3205 window,
3206 cx,
3207 );
3208
3209 editor.tab(&Tab, window, cx);
3210 assert_text_with_selections(
3211 &mut editor,
3212 indoc! {"
3213 «aˇ» = 1
3214 b = 2
3215
3216 «const c:ˇ» usize = 3;
3217 "},
3218 cx,
3219 );
3220 editor.backtab(&Backtab, window, cx);
3221 assert_text_with_selections(
3222 &mut editor,
3223 indoc! {"
3224 «aˇ» = 1
3225 b = 2
3226
3227 «const c:ˇ» usize = 3;
3228 "},
3229 cx,
3230 );
3231
3232 editor
3233 });
3234}
3235
3236#[gpui::test]
3237async fn test_backspace(cx: &mut TestAppContext) {
3238 init_test(cx, |_| {});
3239
3240 let mut cx = EditorTestContext::new(cx).await;
3241
3242 // Basic backspace
3243 cx.set_state(indoc! {"
3244 onˇe two three
3245 fou«rˇ» five six
3246 seven «ˇeight nine
3247 »ten
3248 "});
3249 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3250 cx.assert_editor_state(indoc! {"
3251 oˇe two three
3252 fouˇ five six
3253 seven ˇten
3254 "});
3255
3256 // Test backspace inside and around indents
3257 cx.set_state(indoc! {"
3258 zero
3259 ˇone
3260 ˇtwo
3261 ˇ ˇ ˇ three
3262 ˇ ˇ four
3263 "});
3264 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3265 cx.assert_editor_state(indoc! {"
3266 zero
3267 ˇone
3268 ˇtwo
3269 ˇ threeˇ four
3270 "});
3271
3272 // Test backspace with line_mode set to true
3273 cx.update_editor(|e, _, _| e.selections.line_mode = true);
3274 cx.set_state(indoc! {"
3275 The ˇquick ˇbrown
3276 fox jumps over
3277 the lazy dog
3278 ˇThe qu«ick bˇ»rown"});
3279 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3280 cx.assert_editor_state(indoc! {"
3281 ˇfox jumps over
3282 the lazy dogˇ"});
3283}
3284
3285#[gpui::test]
3286async fn test_delete(cx: &mut TestAppContext) {
3287 init_test(cx, |_| {});
3288
3289 let mut cx = EditorTestContext::new(cx).await;
3290 cx.set_state(indoc! {"
3291 onˇe two three
3292 fou«rˇ» five six
3293 seven «ˇeight nine
3294 »ten
3295 "});
3296 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3297 cx.assert_editor_state(indoc! {"
3298 onˇ two three
3299 fouˇ five six
3300 seven ˇten
3301 "});
3302
3303 // Test backspace with line_mode set to true
3304 cx.update_editor(|e, _, _| e.selections.line_mode = true);
3305 cx.set_state(indoc! {"
3306 The ˇquick ˇbrown
3307 fox «ˇjum»ps over
3308 the lazy dog
3309 ˇThe qu«ick bˇ»rown"});
3310 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3311 cx.assert_editor_state("ˇthe lazy dogˇ");
3312}
3313
3314#[gpui::test]
3315fn test_delete_line(cx: &mut TestAppContext) {
3316 init_test(cx, |_| {});
3317
3318 let editor = cx.add_window(|window, cx| {
3319 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3320 build_editor(buffer, window, cx)
3321 });
3322 _ = editor.update(cx, |editor, window, cx| {
3323 editor.change_selections(None, window, cx, |s| {
3324 s.select_display_ranges([
3325 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3326 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3327 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3328 ])
3329 });
3330 editor.delete_line(&DeleteLine, window, cx);
3331 assert_eq!(editor.display_text(cx), "ghi");
3332 assert_eq!(
3333 editor.selections.display_ranges(cx),
3334 vec![
3335 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3336 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3337 ]
3338 );
3339 });
3340
3341 let editor = cx.add_window(|window, cx| {
3342 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3343 build_editor(buffer, window, cx)
3344 });
3345 _ = editor.update(cx, |editor, window, cx| {
3346 editor.change_selections(None, window, cx, |s| {
3347 s.select_display_ranges([
3348 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3349 ])
3350 });
3351 editor.delete_line(&DeleteLine, window, cx);
3352 assert_eq!(editor.display_text(cx), "ghi\n");
3353 assert_eq!(
3354 editor.selections.display_ranges(cx),
3355 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3356 );
3357 });
3358}
3359
3360#[gpui::test]
3361fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3362 init_test(cx, |_| {});
3363
3364 cx.add_window(|window, cx| {
3365 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3366 let mut editor = build_editor(buffer.clone(), window, cx);
3367 let buffer = buffer.read(cx).as_singleton().unwrap();
3368
3369 assert_eq!(
3370 editor.selections.ranges::<Point>(cx),
3371 &[Point::new(0, 0)..Point::new(0, 0)]
3372 );
3373
3374 // When on single line, replace newline at end by space
3375 editor.join_lines(&JoinLines, window, cx);
3376 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3377 assert_eq!(
3378 editor.selections.ranges::<Point>(cx),
3379 &[Point::new(0, 3)..Point::new(0, 3)]
3380 );
3381
3382 // When multiple lines are selected, remove newlines that are spanned by the selection
3383 editor.change_selections(None, window, cx, |s| {
3384 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3385 });
3386 editor.join_lines(&JoinLines, window, cx);
3387 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3388 assert_eq!(
3389 editor.selections.ranges::<Point>(cx),
3390 &[Point::new(0, 11)..Point::new(0, 11)]
3391 );
3392
3393 // Undo should be transactional
3394 editor.undo(&Undo, window, cx);
3395 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3396 assert_eq!(
3397 editor.selections.ranges::<Point>(cx),
3398 &[Point::new(0, 5)..Point::new(2, 2)]
3399 );
3400
3401 // When joining an empty line don't insert a space
3402 editor.change_selections(None, window, cx, |s| {
3403 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3404 });
3405 editor.join_lines(&JoinLines, window, cx);
3406 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3407 assert_eq!(
3408 editor.selections.ranges::<Point>(cx),
3409 [Point::new(2, 3)..Point::new(2, 3)]
3410 );
3411
3412 // We can remove trailing newlines
3413 editor.join_lines(&JoinLines, window, cx);
3414 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3415 assert_eq!(
3416 editor.selections.ranges::<Point>(cx),
3417 [Point::new(2, 3)..Point::new(2, 3)]
3418 );
3419
3420 // We don't blow up on the last line
3421 editor.join_lines(&JoinLines, window, cx);
3422 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3423 assert_eq!(
3424 editor.selections.ranges::<Point>(cx),
3425 [Point::new(2, 3)..Point::new(2, 3)]
3426 );
3427
3428 // reset to test indentation
3429 editor.buffer.update(cx, |buffer, cx| {
3430 buffer.edit(
3431 [
3432 (Point::new(1, 0)..Point::new(1, 2), " "),
3433 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3434 ],
3435 None,
3436 cx,
3437 )
3438 });
3439
3440 // We remove any leading spaces
3441 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3442 editor.change_selections(None, window, cx, |s| {
3443 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3444 });
3445 editor.join_lines(&JoinLines, window, cx);
3446 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3447
3448 // We don't insert a space for a line containing only spaces
3449 editor.join_lines(&JoinLines, window, cx);
3450 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3451
3452 // We ignore any leading tabs
3453 editor.join_lines(&JoinLines, window, cx);
3454 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3455
3456 editor
3457 });
3458}
3459
3460#[gpui::test]
3461fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3462 init_test(cx, |_| {});
3463
3464 cx.add_window(|window, cx| {
3465 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3466 let mut editor = build_editor(buffer.clone(), window, cx);
3467 let buffer = buffer.read(cx).as_singleton().unwrap();
3468
3469 editor.change_selections(None, window, cx, |s| {
3470 s.select_ranges([
3471 Point::new(0, 2)..Point::new(1, 1),
3472 Point::new(1, 2)..Point::new(1, 2),
3473 Point::new(3, 1)..Point::new(3, 2),
3474 ])
3475 });
3476
3477 editor.join_lines(&JoinLines, window, cx);
3478 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3479
3480 assert_eq!(
3481 editor.selections.ranges::<Point>(cx),
3482 [
3483 Point::new(0, 7)..Point::new(0, 7),
3484 Point::new(1, 3)..Point::new(1, 3)
3485 ]
3486 );
3487 editor
3488 });
3489}
3490
3491#[gpui::test]
3492async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3493 init_test(cx, |_| {});
3494
3495 let mut cx = EditorTestContext::new(cx).await;
3496
3497 let diff_base = r#"
3498 Line 0
3499 Line 1
3500 Line 2
3501 Line 3
3502 "#
3503 .unindent();
3504
3505 cx.set_state(
3506 &r#"
3507 ˇLine 0
3508 Line 1
3509 Line 2
3510 Line 3
3511 "#
3512 .unindent(),
3513 );
3514
3515 cx.set_head_text(&diff_base);
3516 executor.run_until_parked();
3517
3518 // Join lines
3519 cx.update_editor(|editor, window, cx| {
3520 editor.join_lines(&JoinLines, window, cx);
3521 });
3522 executor.run_until_parked();
3523
3524 cx.assert_editor_state(
3525 &r#"
3526 Line 0ˇ Line 1
3527 Line 2
3528 Line 3
3529 "#
3530 .unindent(),
3531 );
3532 // Join again
3533 cx.update_editor(|editor, window, cx| {
3534 editor.join_lines(&JoinLines, window, cx);
3535 });
3536 executor.run_until_parked();
3537
3538 cx.assert_editor_state(
3539 &r#"
3540 Line 0 Line 1ˇ Line 2
3541 Line 3
3542 "#
3543 .unindent(),
3544 );
3545}
3546
3547#[gpui::test]
3548async fn test_custom_newlines_cause_no_false_positive_diffs(
3549 executor: BackgroundExecutor,
3550 cx: &mut TestAppContext,
3551) {
3552 init_test(cx, |_| {});
3553 let mut cx = EditorTestContext::new(cx).await;
3554 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3555 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3556 executor.run_until_parked();
3557
3558 cx.update_editor(|editor, window, cx| {
3559 let snapshot = editor.snapshot(window, cx);
3560 assert_eq!(
3561 snapshot
3562 .buffer_snapshot
3563 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3564 .collect::<Vec<_>>(),
3565 Vec::new(),
3566 "Should not have any diffs for files with custom newlines"
3567 );
3568 });
3569}
3570
3571#[gpui::test]
3572async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3573 init_test(cx, |_| {});
3574
3575 let mut cx = EditorTestContext::new(cx).await;
3576
3577 // Test sort_lines_case_insensitive()
3578 cx.set_state(indoc! {"
3579 «z
3580 y
3581 x
3582 Z
3583 Y
3584 Xˇ»
3585 "});
3586 cx.update_editor(|e, window, cx| {
3587 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3588 });
3589 cx.assert_editor_state(indoc! {"
3590 «x
3591 X
3592 y
3593 Y
3594 z
3595 Zˇ»
3596 "});
3597
3598 // Test reverse_lines()
3599 cx.set_state(indoc! {"
3600 «5
3601 4
3602 3
3603 2
3604 1ˇ»
3605 "});
3606 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3607 cx.assert_editor_state(indoc! {"
3608 «1
3609 2
3610 3
3611 4
3612 5ˇ»
3613 "});
3614
3615 // Skip testing shuffle_line()
3616
3617 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3618 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3619
3620 // Don't manipulate when cursor is on single line, but expand the selection
3621 cx.set_state(indoc! {"
3622 ddˇdd
3623 ccc
3624 bb
3625 a
3626 "});
3627 cx.update_editor(|e, window, cx| {
3628 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3629 });
3630 cx.assert_editor_state(indoc! {"
3631 «ddddˇ»
3632 ccc
3633 bb
3634 a
3635 "});
3636
3637 // Basic manipulate case
3638 // Start selection moves to column 0
3639 // End of selection shrinks to fit shorter line
3640 cx.set_state(indoc! {"
3641 dd«d
3642 ccc
3643 bb
3644 aaaaaˇ»
3645 "});
3646 cx.update_editor(|e, window, cx| {
3647 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3648 });
3649 cx.assert_editor_state(indoc! {"
3650 «aaaaa
3651 bb
3652 ccc
3653 dddˇ»
3654 "});
3655
3656 // Manipulate case with newlines
3657 cx.set_state(indoc! {"
3658 dd«d
3659 ccc
3660
3661 bb
3662 aaaaa
3663
3664 ˇ»
3665 "});
3666 cx.update_editor(|e, window, cx| {
3667 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3668 });
3669 cx.assert_editor_state(indoc! {"
3670 «
3671
3672 aaaaa
3673 bb
3674 ccc
3675 dddˇ»
3676
3677 "});
3678
3679 // Adding new line
3680 cx.set_state(indoc! {"
3681 aa«a
3682 bbˇ»b
3683 "});
3684 cx.update_editor(|e, window, cx| {
3685 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3686 });
3687 cx.assert_editor_state(indoc! {"
3688 «aaa
3689 bbb
3690 added_lineˇ»
3691 "});
3692
3693 // Removing line
3694 cx.set_state(indoc! {"
3695 aa«a
3696 bbbˇ»
3697 "});
3698 cx.update_editor(|e, window, cx| {
3699 e.manipulate_lines(window, cx, |lines| {
3700 lines.pop();
3701 })
3702 });
3703 cx.assert_editor_state(indoc! {"
3704 «aaaˇ»
3705 "});
3706
3707 // Removing all lines
3708 cx.set_state(indoc! {"
3709 aa«a
3710 bbbˇ»
3711 "});
3712 cx.update_editor(|e, window, cx| {
3713 e.manipulate_lines(window, cx, |lines| {
3714 lines.drain(..);
3715 })
3716 });
3717 cx.assert_editor_state(indoc! {"
3718 ˇ
3719 "});
3720}
3721
3722#[gpui::test]
3723async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3724 init_test(cx, |_| {});
3725
3726 let mut cx = EditorTestContext::new(cx).await;
3727
3728 // Consider continuous selection as single selection
3729 cx.set_state(indoc! {"
3730 Aaa«aa
3731 cˇ»c«c
3732 bb
3733 aaaˇ»aa
3734 "});
3735 cx.update_editor(|e, window, cx| {
3736 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3737 });
3738 cx.assert_editor_state(indoc! {"
3739 «Aaaaa
3740 ccc
3741 bb
3742 aaaaaˇ»
3743 "});
3744
3745 cx.set_state(indoc! {"
3746 Aaa«aa
3747 cˇ»c«c
3748 bb
3749 aaaˇ»aa
3750 "});
3751 cx.update_editor(|e, window, cx| {
3752 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3753 });
3754 cx.assert_editor_state(indoc! {"
3755 «Aaaaa
3756 ccc
3757 bbˇ»
3758 "});
3759
3760 // Consider non continuous selection as distinct dedup operations
3761 cx.set_state(indoc! {"
3762 «aaaaa
3763 bb
3764 aaaaa
3765 aaaaaˇ»
3766
3767 aaa«aaˇ»
3768 "});
3769 cx.update_editor(|e, window, cx| {
3770 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3771 });
3772 cx.assert_editor_state(indoc! {"
3773 «aaaaa
3774 bbˇ»
3775
3776 «aaaaaˇ»
3777 "});
3778}
3779
3780#[gpui::test]
3781async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3782 init_test(cx, |_| {});
3783
3784 let mut cx = EditorTestContext::new(cx).await;
3785
3786 cx.set_state(indoc! {"
3787 «Aaa
3788 aAa
3789 Aaaˇ»
3790 "});
3791 cx.update_editor(|e, window, cx| {
3792 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3793 });
3794 cx.assert_editor_state(indoc! {"
3795 «Aaa
3796 aAaˇ»
3797 "});
3798
3799 cx.set_state(indoc! {"
3800 «Aaa
3801 aAa
3802 aaAˇ»
3803 "});
3804 cx.update_editor(|e, window, cx| {
3805 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3806 });
3807 cx.assert_editor_state(indoc! {"
3808 «Aaaˇ»
3809 "});
3810}
3811
3812#[gpui::test]
3813async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3814 init_test(cx, |_| {});
3815
3816 let mut cx = EditorTestContext::new(cx).await;
3817
3818 // Manipulate with multiple selections on a single line
3819 cx.set_state(indoc! {"
3820 dd«dd
3821 cˇ»c«c
3822 bb
3823 aaaˇ»aa
3824 "});
3825 cx.update_editor(|e, window, cx| {
3826 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3827 });
3828 cx.assert_editor_state(indoc! {"
3829 «aaaaa
3830 bb
3831 ccc
3832 ddddˇ»
3833 "});
3834
3835 // Manipulate with multiple disjoin selections
3836 cx.set_state(indoc! {"
3837 5«
3838 4
3839 3
3840 2
3841 1ˇ»
3842
3843 dd«dd
3844 ccc
3845 bb
3846 aaaˇ»aa
3847 "});
3848 cx.update_editor(|e, window, cx| {
3849 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3850 });
3851 cx.assert_editor_state(indoc! {"
3852 «1
3853 2
3854 3
3855 4
3856 5ˇ»
3857
3858 «aaaaa
3859 bb
3860 ccc
3861 ddddˇ»
3862 "});
3863
3864 // Adding lines on each selection
3865 cx.set_state(indoc! {"
3866 2«
3867 1ˇ»
3868
3869 bb«bb
3870 aaaˇ»aa
3871 "});
3872 cx.update_editor(|e, window, cx| {
3873 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3874 });
3875 cx.assert_editor_state(indoc! {"
3876 «2
3877 1
3878 added lineˇ»
3879
3880 «bbbb
3881 aaaaa
3882 added lineˇ»
3883 "});
3884
3885 // Removing lines on each selection
3886 cx.set_state(indoc! {"
3887 2«
3888 1ˇ»
3889
3890 bb«bb
3891 aaaˇ»aa
3892 "});
3893 cx.update_editor(|e, window, cx| {
3894 e.manipulate_lines(window, cx, |lines| {
3895 lines.pop();
3896 })
3897 });
3898 cx.assert_editor_state(indoc! {"
3899 «2ˇ»
3900
3901 «bbbbˇ»
3902 "});
3903}
3904
3905#[gpui::test]
3906async fn test_manipulate_text(cx: &mut TestAppContext) {
3907 init_test(cx, |_| {});
3908
3909 let mut cx = EditorTestContext::new(cx).await;
3910
3911 // Test convert_to_upper_case()
3912 cx.set_state(indoc! {"
3913 «hello worldˇ»
3914 "});
3915 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3916 cx.assert_editor_state(indoc! {"
3917 «HELLO WORLDˇ»
3918 "});
3919
3920 // Test convert_to_lower_case()
3921 cx.set_state(indoc! {"
3922 «HELLO WORLDˇ»
3923 "});
3924 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3925 cx.assert_editor_state(indoc! {"
3926 «hello worldˇ»
3927 "});
3928
3929 // Test multiple line, single selection case
3930 cx.set_state(indoc! {"
3931 «The quick brown
3932 fox jumps over
3933 the lazy dogˇ»
3934 "});
3935 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3936 cx.assert_editor_state(indoc! {"
3937 «The Quick Brown
3938 Fox Jumps Over
3939 The Lazy Dogˇ»
3940 "});
3941
3942 // Test multiple line, single selection case
3943 cx.set_state(indoc! {"
3944 «The quick brown
3945 fox jumps over
3946 the lazy dogˇ»
3947 "});
3948 cx.update_editor(|e, window, cx| {
3949 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
3950 });
3951 cx.assert_editor_state(indoc! {"
3952 «TheQuickBrown
3953 FoxJumpsOver
3954 TheLazyDogˇ»
3955 "});
3956
3957 // From here on out, test more complex cases of manipulate_text()
3958
3959 // Test no selection case - should affect words cursors are in
3960 // Cursor at beginning, middle, and end of word
3961 cx.set_state(indoc! {"
3962 ˇhello big beauˇtiful worldˇ
3963 "});
3964 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3965 cx.assert_editor_state(indoc! {"
3966 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
3967 "});
3968
3969 // Test multiple selections on a single line and across multiple lines
3970 cx.set_state(indoc! {"
3971 «Theˇ» quick «brown
3972 foxˇ» jumps «overˇ»
3973 the «lazyˇ» dog
3974 "});
3975 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3976 cx.assert_editor_state(indoc! {"
3977 «THEˇ» quick «BROWN
3978 FOXˇ» jumps «OVERˇ»
3979 the «LAZYˇ» dog
3980 "});
3981
3982 // Test case where text length grows
3983 cx.set_state(indoc! {"
3984 «tschüߡ»
3985 "});
3986 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3987 cx.assert_editor_state(indoc! {"
3988 «TSCHÜSSˇ»
3989 "});
3990
3991 // Test to make sure we don't crash when text shrinks
3992 cx.set_state(indoc! {"
3993 aaa_bbbˇ
3994 "});
3995 cx.update_editor(|e, window, cx| {
3996 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3997 });
3998 cx.assert_editor_state(indoc! {"
3999 «aaaBbbˇ»
4000 "});
4001
4002 // Test to make sure we all aware of the fact that each word can grow and shrink
4003 // Final selections should be aware of this fact
4004 cx.set_state(indoc! {"
4005 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4006 "});
4007 cx.update_editor(|e, window, cx| {
4008 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4009 });
4010 cx.assert_editor_state(indoc! {"
4011 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4012 "});
4013
4014 cx.set_state(indoc! {"
4015 «hElLo, WoRld!ˇ»
4016 "});
4017 cx.update_editor(|e, window, cx| {
4018 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4019 });
4020 cx.assert_editor_state(indoc! {"
4021 «HeLlO, wOrLD!ˇ»
4022 "});
4023}
4024
4025#[gpui::test]
4026fn test_duplicate_line(cx: &mut TestAppContext) {
4027 init_test(cx, |_| {});
4028
4029 let editor = cx.add_window(|window, cx| {
4030 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4031 build_editor(buffer, window, cx)
4032 });
4033 _ = editor.update(cx, |editor, window, cx| {
4034 editor.change_selections(None, window, cx, |s| {
4035 s.select_display_ranges([
4036 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4037 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4038 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4039 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4040 ])
4041 });
4042 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4043 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4044 assert_eq!(
4045 editor.selections.display_ranges(cx),
4046 vec![
4047 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4048 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4049 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4050 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4051 ]
4052 );
4053 });
4054
4055 let editor = cx.add_window(|window, cx| {
4056 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4057 build_editor(buffer, window, cx)
4058 });
4059 _ = editor.update(cx, |editor, window, cx| {
4060 editor.change_selections(None, window, cx, |s| {
4061 s.select_display_ranges([
4062 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4063 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4064 ])
4065 });
4066 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4067 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4068 assert_eq!(
4069 editor.selections.display_ranges(cx),
4070 vec![
4071 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4072 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4073 ]
4074 );
4075 });
4076
4077 // With `move_upwards` the selections stay in place, except for
4078 // the lines inserted above them
4079 let editor = cx.add_window(|window, cx| {
4080 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4081 build_editor(buffer, window, cx)
4082 });
4083 _ = editor.update(cx, |editor, window, cx| {
4084 editor.change_selections(None, window, cx, |s| {
4085 s.select_display_ranges([
4086 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4087 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4088 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4089 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4090 ])
4091 });
4092 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4093 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4094 assert_eq!(
4095 editor.selections.display_ranges(cx),
4096 vec![
4097 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4098 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4099 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4100 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4101 ]
4102 );
4103 });
4104
4105 let editor = cx.add_window(|window, cx| {
4106 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4107 build_editor(buffer, window, cx)
4108 });
4109 _ = editor.update(cx, |editor, window, cx| {
4110 editor.change_selections(None, window, cx, |s| {
4111 s.select_display_ranges([
4112 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4113 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4114 ])
4115 });
4116 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4117 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4118 assert_eq!(
4119 editor.selections.display_ranges(cx),
4120 vec![
4121 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4122 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4123 ]
4124 );
4125 });
4126
4127 let editor = cx.add_window(|window, cx| {
4128 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4129 build_editor(buffer, window, cx)
4130 });
4131 _ = editor.update(cx, |editor, window, cx| {
4132 editor.change_selections(None, window, cx, |s| {
4133 s.select_display_ranges([
4134 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4135 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4136 ])
4137 });
4138 editor.duplicate_selection(&DuplicateSelection, window, cx);
4139 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4140 assert_eq!(
4141 editor.selections.display_ranges(cx),
4142 vec![
4143 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4144 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4145 ]
4146 );
4147 });
4148}
4149
4150#[gpui::test]
4151fn test_move_line_up_down(cx: &mut TestAppContext) {
4152 init_test(cx, |_| {});
4153
4154 let editor = cx.add_window(|window, cx| {
4155 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4156 build_editor(buffer, window, cx)
4157 });
4158 _ = editor.update(cx, |editor, window, cx| {
4159 editor.fold_creases(
4160 vec![
4161 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4162 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4163 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4164 ],
4165 true,
4166 window,
4167 cx,
4168 );
4169 editor.change_selections(None, window, cx, |s| {
4170 s.select_display_ranges([
4171 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4172 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4173 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4174 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4175 ])
4176 });
4177 assert_eq!(
4178 editor.display_text(cx),
4179 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4180 );
4181
4182 editor.move_line_up(&MoveLineUp, window, cx);
4183 assert_eq!(
4184 editor.display_text(cx),
4185 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4186 );
4187 assert_eq!(
4188 editor.selections.display_ranges(cx),
4189 vec![
4190 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4191 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4192 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4193 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4194 ]
4195 );
4196 });
4197
4198 _ = editor.update(cx, |editor, window, cx| {
4199 editor.move_line_down(&MoveLineDown, window, cx);
4200 assert_eq!(
4201 editor.display_text(cx),
4202 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4203 );
4204 assert_eq!(
4205 editor.selections.display_ranges(cx),
4206 vec![
4207 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4208 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4209 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4210 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4211 ]
4212 );
4213 });
4214
4215 _ = editor.update(cx, |editor, window, cx| {
4216 editor.move_line_down(&MoveLineDown, window, cx);
4217 assert_eq!(
4218 editor.display_text(cx),
4219 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4220 );
4221 assert_eq!(
4222 editor.selections.display_ranges(cx),
4223 vec![
4224 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4225 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4226 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4227 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4228 ]
4229 );
4230 });
4231
4232 _ = editor.update(cx, |editor, window, cx| {
4233 editor.move_line_up(&MoveLineUp, window, cx);
4234 assert_eq!(
4235 editor.display_text(cx),
4236 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4237 );
4238 assert_eq!(
4239 editor.selections.display_ranges(cx),
4240 vec![
4241 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4242 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4243 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4244 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4245 ]
4246 );
4247 });
4248}
4249
4250#[gpui::test]
4251fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4252 init_test(cx, |_| {});
4253
4254 let editor = cx.add_window(|window, cx| {
4255 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4256 build_editor(buffer, window, cx)
4257 });
4258 _ = editor.update(cx, |editor, window, cx| {
4259 let snapshot = editor.buffer.read(cx).snapshot(cx);
4260 editor.insert_blocks(
4261 [BlockProperties {
4262 style: BlockStyle::Fixed,
4263 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4264 height: 1,
4265 render: Arc::new(|_| div().into_any()),
4266 priority: 0,
4267 }],
4268 Some(Autoscroll::fit()),
4269 cx,
4270 );
4271 editor.change_selections(None, window, cx, |s| {
4272 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4273 });
4274 editor.move_line_down(&MoveLineDown, window, cx);
4275 });
4276}
4277
4278#[gpui::test]
4279async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4280 init_test(cx, |_| {});
4281
4282 let mut cx = EditorTestContext::new(cx).await;
4283 cx.set_state(
4284 &"
4285 ˇzero
4286 one
4287 two
4288 three
4289 four
4290 five
4291 "
4292 .unindent(),
4293 );
4294
4295 // Create a four-line block that replaces three lines of text.
4296 cx.update_editor(|editor, window, cx| {
4297 let snapshot = editor.snapshot(window, cx);
4298 let snapshot = &snapshot.buffer_snapshot;
4299 let placement = BlockPlacement::Replace(
4300 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4301 );
4302 editor.insert_blocks(
4303 [BlockProperties {
4304 placement,
4305 height: 4,
4306 style: BlockStyle::Sticky,
4307 render: Arc::new(|_| gpui::div().into_any_element()),
4308 priority: 0,
4309 }],
4310 None,
4311 cx,
4312 );
4313 });
4314
4315 // Move down so that the cursor touches the block.
4316 cx.update_editor(|editor, window, cx| {
4317 editor.move_down(&Default::default(), window, cx);
4318 });
4319 cx.assert_editor_state(
4320 &"
4321 zero
4322 «one
4323 two
4324 threeˇ»
4325 four
4326 five
4327 "
4328 .unindent(),
4329 );
4330
4331 // Move down past the block.
4332 cx.update_editor(|editor, window, cx| {
4333 editor.move_down(&Default::default(), window, cx);
4334 });
4335 cx.assert_editor_state(
4336 &"
4337 zero
4338 one
4339 two
4340 three
4341 ˇfour
4342 five
4343 "
4344 .unindent(),
4345 );
4346}
4347
4348#[gpui::test]
4349fn test_transpose(cx: &mut TestAppContext) {
4350 init_test(cx, |_| {});
4351
4352 _ = cx.add_window(|window, cx| {
4353 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4354 editor.set_style(EditorStyle::default(), window, cx);
4355 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4356 editor.transpose(&Default::default(), window, cx);
4357 assert_eq!(editor.text(cx), "bac");
4358 assert_eq!(editor.selections.ranges(cx), [2..2]);
4359
4360 editor.transpose(&Default::default(), window, cx);
4361 assert_eq!(editor.text(cx), "bca");
4362 assert_eq!(editor.selections.ranges(cx), [3..3]);
4363
4364 editor.transpose(&Default::default(), window, cx);
4365 assert_eq!(editor.text(cx), "bac");
4366 assert_eq!(editor.selections.ranges(cx), [3..3]);
4367
4368 editor
4369 });
4370
4371 _ = cx.add_window(|window, cx| {
4372 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4373 editor.set_style(EditorStyle::default(), window, cx);
4374 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4375 editor.transpose(&Default::default(), window, cx);
4376 assert_eq!(editor.text(cx), "acb\nde");
4377 assert_eq!(editor.selections.ranges(cx), [3..3]);
4378
4379 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4380 editor.transpose(&Default::default(), window, cx);
4381 assert_eq!(editor.text(cx), "acbd\ne");
4382 assert_eq!(editor.selections.ranges(cx), [5..5]);
4383
4384 editor.transpose(&Default::default(), window, cx);
4385 assert_eq!(editor.text(cx), "acbde\n");
4386 assert_eq!(editor.selections.ranges(cx), [6..6]);
4387
4388 editor.transpose(&Default::default(), window, cx);
4389 assert_eq!(editor.text(cx), "acbd\ne");
4390 assert_eq!(editor.selections.ranges(cx), [6..6]);
4391
4392 editor
4393 });
4394
4395 _ = cx.add_window(|window, cx| {
4396 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4397 editor.set_style(EditorStyle::default(), window, cx);
4398 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4399 editor.transpose(&Default::default(), window, cx);
4400 assert_eq!(editor.text(cx), "bacd\ne");
4401 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4402
4403 editor.transpose(&Default::default(), window, cx);
4404 assert_eq!(editor.text(cx), "bcade\n");
4405 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4406
4407 editor.transpose(&Default::default(), window, cx);
4408 assert_eq!(editor.text(cx), "bcda\ne");
4409 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4410
4411 editor.transpose(&Default::default(), window, cx);
4412 assert_eq!(editor.text(cx), "bcade\n");
4413 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4414
4415 editor.transpose(&Default::default(), window, cx);
4416 assert_eq!(editor.text(cx), "bcaed\n");
4417 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4418
4419 editor
4420 });
4421
4422 _ = cx.add_window(|window, cx| {
4423 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4424 editor.set_style(EditorStyle::default(), window, cx);
4425 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4426 editor.transpose(&Default::default(), window, cx);
4427 assert_eq!(editor.text(cx), "🏀🍐✋");
4428 assert_eq!(editor.selections.ranges(cx), [8..8]);
4429
4430 editor.transpose(&Default::default(), window, cx);
4431 assert_eq!(editor.text(cx), "🏀✋🍐");
4432 assert_eq!(editor.selections.ranges(cx), [11..11]);
4433
4434 editor.transpose(&Default::default(), window, cx);
4435 assert_eq!(editor.text(cx), "🏀🍐✋");
4436 assert_eq!(editor.selections.ranges(cx), [11..11]);
4437
4438 editor
4439 });
4440}
4441
4442#[gpui::test]
4443async fn test_rewrap(cx: &mut TestAppContext) {
4444 init_test(cx, |settings| {
4445 settings.languages.extend([
4446 (
4447 "Markdown".into(),
4448 LanguageSettingsContent {
4449 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4450 ..Default::default()
4451 },
4452 ),
4453 (
4454 "Plain Text".into(),
4455 LanguageSettingsContent {
4456 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4457 ..Default::default()
4458 },
4459 ),
4460 ])
4461 });
4462
4463 let mut cx = EditorTestContext::new(cx).await;
4464
4465 let language_with_c_comments = Arc::new(Language::new(
4466 LanguageConfig {
4467 line_comments: vec!["// ".into()],
4468 ..LanguageConfig::default()
4469 },
4470 None,
4471 ));
4472 let language_with_pound_comments = Arc::new(Language::new(
4473 LanguageConfig {
4474 line_comments: vec!["# ".into()],
4475 ..LanguageConfig::default()
4476 },
4477 None,
4478 ));
4479 let markdown_language = Arc::new(Language::new(
4480 LanguageConfig {
4481 name: "Markdown".into(),
4482 ..LanguageConfig::default()
4483 },
4484 None,
4485 ));
4486 let language_with_doc_comments = Arc::new(Language::new(
4487 LanguageConfig {
4488 line_comments: vec!["// ".into(), "/// ".into()],
4489 ..LanguageConfig::default()
4490 },
4491 Some(tree_sitter_rust::LANGUAGE.into()),
4492 ));
4493
4494 let plaintext_language = Arc::new(Language::new(
4495 LanguageConfig {
4496 name: "Plain Text".into(),
4497 ..LanguageConfig::default()
4498 },
4499 None,
4500 ));
4501
4502 assert_rewrap(
4503 indoc! {"
4504 // ˇ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.
4505 "},
4506 indoc! {"
4507 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4508 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4509 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4510 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4511 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4512 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4513 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4514 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4515 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4516 // porttitor id. Aliquam id accumsan eros.
4517 "},
4518 language_with_c_comments.clone(),
4519 &mut cx,
4520 );
4521
4522 // Test that rewrapping works inside of a selection
4523 assert_rewrap(
4524 indoc! {"
4525 «// 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.ˇ»
4526 "},
4527 indoc! {"
4528 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4529 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4530 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4531 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4532 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4533 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4534 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4535 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4536 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4537 // porttitor id. Aliquam id accumsan eros.ˇ»
4538 "},
4539 language_with_c_comments.clone(),
4540 &mut cx,
4541 );
4542
4543 // Test that cursors that expand to the same region are collapsed.
4544 assert_rewrap(
4545 indoc! {"
4546 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4547 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4548 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4549 // ˇ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.
4550 "},
4551 indoc! {"
4552 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4553 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4554 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4555 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4556 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4557 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4558 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4559 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4560 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4561 // porttitor id. Aliquam id accumsan eros.
4562 "},
4563 language_with_c_comments.clone(),
4564 &mut cx,
4565 );
4566
4567 // Test that non-contiguous selections are treated separately.
4568 assert_rewrap(
4569 indoc! {"
4570 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4571 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4572 //
4573 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4574 // ˇ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.
4575 "},
4576 indoc! {"
4577 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4578 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4579 // auctor, eu lacinia sapien scelerisque.
4580 //
4581 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4582 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4583 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4584 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4585 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4586 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4587 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4588 "},
4589 language_with_c_comments.clone(),
4590 &mut cx,
4591 );
4592
4593 // Test that different comment prefixes are supported.
4594 assert_rewrap(
4595 indoc! {"
4596 # ˇ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.
4597 "},
4598 indoc! {"
4599 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4600 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4601 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4602 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4603 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4604 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4605 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4606 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4607 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4608 # accumsan eros.
4609 "},
4610 language_with_pound_comments.clone(),
4611 &mut cx,
4612 );
4613
4614 // Test that rewrapping is ignored outside of comments in most languages.
4615 assert_rewrap(
4616 indoc! {"
4617 /// Adds two numbers.
4618 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4619 fn add(a: u32, b: u32) -> u32 {
4620 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ˇ
4621 }
4622 "},
4623 indoc! {"
4624 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4625 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4626 fn add(a: u32, b: u32) -> u32 {
4627 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ˇ
4628 }
4629 "},
4630 language_with_doc_comments.clone(),
4631 &mut cx,
4632 );
4633
4634 // Test that rewrapping works in Markdown and Plain Text languages.
4635 assert_rewrap(
4636 indoc! {"
4637 # Hello
4638
4639 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.
4640 "},
4641 indoc! {"
4642 # Hello
4643
4644 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4645 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4646 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4647 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4648 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4649 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4650 Integer sit amet scelerisque nisi.
4651 "},
4652 markdown_language,
4653 &mut cx,
4654 );
4655
4656 assert_rewrap(
4657 indoc! {"
4658 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.
4659 "},
4660 indoc! {"
4661 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4662 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4663 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4664 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4665 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4666 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4667 Integer sit amet scelerisque nisi.
4668 "},
4669 plaintext_language,
4670 &mut cx,
4671 );
4672
4673 // Test rewrapping unaligned comments in a selection.
4674 assert_rewrap(
4675 indoc! {"
4676 fn foo() {
4677 if true {
4678 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4679 // Praesent semper egestas tellus id dignissim.ˇ»
4680 do_something();
4681 } else {
4682 //
4683 }
4684 }
4685 "},
4686 indoc! {"
4687 fn foo() {
4688 if true {
4689 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4690 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4691 // egestas tellus id dignissim.ˇ»
4692 do_something();
4693 } else {
4694 //
4695 }
4696 }
4697 "},
4698 language_with_doc_comments.clone(),
4699 &mut cx,
4700 );
4701
4702 assert_rewrap(
4703 indoc! {"
4704 fn foo() {
4705 if true {
4706 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4707 // Praesent semper egestas tellus id dignissim.»
4708 do_something();
4709 } else {
4710 //
4711 }
4712
4713 }
4714 "},
4715 indoc! {"
4716 fn foo() {
4717 if true {
4718 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4719 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4720 // egestas tellus id dignissim.»
4721 do_something();
4722 } else {
4723 //
4724 }
4725
4726 }
4727 "},
4728 language_with_doc_comments.clone(),
4729 &mut cx,
4730 );
4731
4732 #[track_caller]
4733 fn assert_rewrap(
4734 unwrapped_text: &str,
4735 wrapped_text: &str,
4736 language: Arc<Language>,
4737 cx: &mut EditorTestContext,
4738 ) {
4739 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4740 cx.set_state(unwrapped_text);
4741 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4742 cx.assert_editor_state(wrapped_text);
4743 }
4744}
4745
4746#[gpui::test]
4747async fn test_hard_wrap(cx: &mut TestAppContext) {
4748 init_test(cx, |_| {});
4749 let mut cx = EditorTestContext::new(cx).await;
4750
4751 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
4752 cx.update_editor(|editor, _, cx| {
4753 editor.set_hard_wrap(Some(14), cx);
4754 });
4755
4756 cx.set_state(indoc!(
4757 "
4758 one two three ˇ
4759 "
4760 ));
4761 cx.simulate_input("four");
4762 cx.run_until_parked();
4763
4764 cx.assert_editor_state(indoc!(
4765 "
4766 one two three
4767 fourˇ
4768 "
4769 ));
4770
4771 cx.update_editor(|editor, window, cx| {
4772 editor.newline(&Default::default(), window, cx);
4773 });
4774 cx.run_until_parked();
4775 cx.assert_editor_state(indoc!(
4776 "
4777 one two three
4778 four
4779 ˇ
4780 "
4781 ));
4782
4783 cx.simulate_input("five");
4784 cx.run_until_parked();
4785 cx.assert_editor_state(indoc!(
4786 "
4787 one two three
4788 four
4789 fiveˇ
4790 "
4791 ));
4792
4793 cx.update_editor(|editor, window, cx| {
4794 editor.newline(&Default::default(), window, cx);
4795 });
4796 cx.run_until_parked();
4797 cx.simulate_input("# ");
4798 cx.run_until_parked();
4799 cx.assert_editor_state(indoc!(
4800 "
4801 one two three
4802 four
4803 five
4804 # ˇ
4805 "
4806 ));
4807
4808 cx.update_editor(|editor, window, cx| {
4809 editor.newline(&Default::default(), window, cx);
4810 });
4811 cx.run_until_parked();
4812 cx.assert_editor_state(indoc!(
4813 "
4814 one two three
4815 four
4816 five
4817 #\x20
4818 #ˇ
4819 "
4820 ));
4821
4822 cx.simulate_input(" 6");
4823 cx.run_until_parked();
4824 cx.assert_editor_state(indoc!(
4825 "
4826 one two three
4827 four
4828 five
4829 #
4830 # 6ˇ
4831 "
4832 ));
4833}
4834
4835#[gpui::test]
4836async fn test_clipboard(cx: &mut TestAppContext) {
4837 init_test(cx, |_| {});
4838
4839 let mut cx = EditorTestContext::new(cx).await;
4840
4841 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4842 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4843 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4844
4845 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4846 cx.set_state("two ˇfour ˇsix ˇ");
4847 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4848 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4849
4850 // Paste again but with only two cursors. Since the number of cursors doesn't
4851 // match the number of slices in the clipboard, the entire clipboard text
4852 // is pasted at each cursor.
4853 cx.set_state("ˇtwo one✅ four three six five ˇ");
4854 cx.update_editor(|e, window, cx| {
4855 e.handle_input("( ", window, cx);
4856 e.paste(&Paste, window, cx);
4857 e.handle_input(") ", window, cx);
4858 });
4859 cx.assert_editor_state(
4860 &([
4861 "( one✅ ",
4862 "three ",
4863 "five ) ˇtwo one✅ four three six five ( one✅ ",
4864 "three ",
4865 "five ) ˇ",
4866 ]
4867 .join("\n")),
4868 );
4869
4870 // Cut with three selections, one of which is full-line.
4871 cx.set_state(indoc! {"
4872 1«2ˇ»3
4873 4ˇ567
4874 «8ˇ»9"});
4875 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4876 cx.assert_editor_state(indoc! {"
4877 1ˇ3
4878 ˇ9"});
4879
4880 // Paste with three selections, noticing how the copied selection that was full-line
4881 // gets inserted before the second cursor.
4882 cx.set_state(indoc! {"
4883 1ˇ3
4884 9ˇ
4885 «oˇ»ne"});
4886 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4887 cx.assert_editor_state(indoc! {"
4888 12ˇ3
4889 4567
4890 9ˇ
4891 8ˇne"});
4892
4893 // Copy with a single cursor only, which writes the whole line into the clipboard.
4894 cx.set_state(indoc! {"
4895 The quick brown
4896 fox juˇmps over
4897 the lazy dog"});
4898 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4899 assert_eq!(
4900 cx.read_from_clipboard()
4901 .and_then(|item| item.text().as_deref().map(str::to_string)),
4902 Some("fox jumps over\n".to_string())
4903 );
4904
4905 // Paste with three selections, noticing how the copied full-line selection is inserted
4906 // before the empty selections but replaces the selection that is non-empty.
4907 cx.set_state(indoc! {"
4908 Tˇhe quick brown
4909 «foˇ»x jumps over
4910 tˇhe lazy dog"});
4911 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4912 cx.assert_editor_state(indoc! {"
4913 fox jumps over
4914 Tˇhe quick brown
4915 fox jumps over
4916 ˇx jumps over
4917 fox jumps over
4918 tˇhe lazy dog"});
4919}
4920
4921#[gpui::test]
4922async fn test_copy_trim(cx: &mut TestAppContext) {
4923 init_test(cx, |_| {});
4924
4925 let mut cx = EditorTestContext::new(cx).await;
4926 cx.set_state(
4927 r#" «for selection in selections.iter() {
4928 let mut start = selection.start;
4929 let mut end = selection.end;
4930 let is_entire_line = selection.is_empty() || self.selections.line_mode;
4931 if is_entire_line {
4932 start = Point::new(start.row, 0);ˇ»
4933 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4934 }
4935 "#,
4936 );
4937 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4938 assert_eq!(
4939 cx.read_from_clipboard()
4940 .and_then(|item| item.text().as_deref().map(str::to_string)),
4941 Some(
4942 "for selection in selections.iter() {
4943 let mut start = selection.start;
4944 let mut end = selection.end;
4945 let is_entire_line = selection.is_empty() || self.selections.line_mode;
4946 if is_entire_line {
4947 start = Point::new(start.row, 0);"
4948 .to_string()
4949 ),
4950 "Regular copying preserves all indentation selected",
4951 );
4952 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
4953 assert_eq!(
4954 cx.read_from_clipboard()
4955 .and_then(|item| item.text().as_deref().map(str::to_string)),
4956 Some(
4957 "for selection in selections.iter() {
4958let mut start = selection.start;
4959let mut end = selection.end;
4960let is_entire_line = selection.is_empty() || self.selections.line_mode;
4961if is_entire_line {
4962 start = Point::new(start.row, 0);"
4963 .to_string()
4964 ),
4965 "Copying with stripping should strip all leading whitespaces"
4966 );
4967
4968 cx.set_state(
4969 r#" « for selection in selections.iter() {
4970 let mut start = selection.start;
4971 let mut end = selection.end;
4972 let is_entire_line = selection.is_empty() || self.selections.line_mode;
4973 if is_entire_line {
4974 start = Point::new(start.row, 0);ˇ»
4975 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4976 }
4977 "#,
4978 );
4979 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4980 assert_eq!(
4981 cx.read_from_clipboard()
4982 .and_then(|item| item.text().as_deref().map(str::to_string)),
4983 Some(
4984 " for selection in selections.iter() {
4985 let mut start = selection.start;
4986 let mut end = selection.end;
4987 let is_entire_line = selection.is_empty() || self.selections.line_mode;
4988 if is_entire_line {
4989 start = Point::new(start.row, 0);"
4990 .to_string()
4991 ),
4992 "Regular copying preserves all indentation selected",
4993 );
4994 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
4995 assert_eq!(
4996 cx.read_from_clipboard()
4997 .and_then(|item| item.text().as_deref().map(str::to_string)),
4998 Some(
4999 "for selection in selections.iter() {
5000let mut start = selection.start;
5001let mut end = selection.end;
5002let is_entire_line = selection.is_empty() || self.selections.line_mode;
5003if is_entire_line {
5004 start = Point::new(start.row, 0);"
5005 .to_string()
5006 ),
5007 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5008 );
5009
5010 cx.set_state(
5011 r#" «ˇ for selection in selections.iter() {
5012 let mut start = selection.start;
5013 let mut end = selection.end;
5014 let is_entire_line = selection.is_empty() || self.selections.line_mode;
5015 if is_entire_line {
5016 start = Point::new(start.row, 0);»
5017 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5018 }
5019 "#,
5020 );
5021 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5022 assert_eq!(
5023 cx.read_from_clipboard()
5024 .and_then(|item| item.text().as_deref().map(str::to_string)),
5025 Some(
5026 " for selection in selections.iter() {
5027 let mut start = selection.start;
5028 let mut end = selection.end;
5029 let is_entire_line = selection.is_empty() || self.selections.line_mode;
5030 if is_entire_line {
5031 start = Point::new(start.row, 0);"
5032 .to_string()
5033 ),
5034 "Regular copying for reverse selection works the same",
5035 );
5036 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5037 assert_eq!(
5038 cx.read_from_clipboard()
5039 .and_then(|item| item.text().as_deref().map(str::to_string)),
5040 Some(
5041 "for selection in selections.iter() {
5042let mut start = selection.start;
5043let mut end = selection.end;
5044let is_entire_line = selection.is_empty() || self.selections.line_mode;
5045if is_entire_line {
5046 start = Point::new(start.row, 0);"
5047 .to_string()
5048 ),
5049 "Copying with stripping for reverse selection works the same"
5050 );
5051
5052 cx.set_state(
5053 r#" for selection «in selections.iter() {
5054 let mut start = selection.start;
5055 let mut end = selection.end;
5056 let is_entire_line = selection.is_empty() || self.selections.line_mode;
5057 if is_entire_line {
5058 start = Point::new(start.row, 0);ˇ»
5059 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5060 }
5061 "#,
5062 );
5063 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5064 assert_eq!(
5065 cx.read_from_clipboard()
5066 .and_then(|item| item.text().as_deref().map(str::to_string)),
5067 Some(
5068 "in selections.iter() {
5069 let mut start = selection.start;
5070 let mut end = selection.end;
5071 let is_entire_line = selection.is_empty() || self.selections.line_mode;
5072 if is_entire_line {
5073 start = Point::new(start.row, 0);"
5074 .to_string()
5075 ),
5076 "When selecting past the indent, the copying works as usual",
5077 );
5078 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5079 assert_eq!(
5080 cx.read_from_clipboard()
5081 .and_then(|item| item.text().as_deref().map(str::to_string)),
5082 Some(
5083 "in selections.iter() {
5084 let mut start = selection.start;
5085 let mut end = selection.end;
5086 let is_entire_line = selection.is_empty() || self.selections.line_mode;
5087 if is_entire_line {
5088 start = Point::new(start.row, 0);"
5089 .to_string()
5090 ),
5091 "When selecting past the indent, nothing is trimmed"
5092 );
5093}
5094
5095#[gpui::test]
5096async fn test_paste_multiline(cx: &mut TestAppContext) {
5097 init_test(cx, |_| {});
5098
5099 let mut cx = EditorTestContext::new(cx).await;
5100 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5101
5102 // Cut an indented block, without the leading whitespace.
5103 cx.set_state(indoc! {"
5104 const a: B = (
5105 c(),
5106 «d(
5107 e,
5108 f
5109 )ˇ»
5110 );
5111 "});
5112 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5113 cx.assert_editor_state(indoc! {"
5114 const a: B = (
5115 c(),
5116 ˇ
5117 );
5118 "});
5119
5120 // Paste it at the same position.
5121 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5122 cx.assert_editor_state(indoc! {"
5123 const a: B = (
5124 c(),
5125 d(
5126 e,
5127 f
5128 )ˇ
5129 );
5130 "});
5131
5132 // Paste it at a line with a lower indent level.
5133 cx.set_state(indoc! {"
5134 ˇ
5135 const a: B = (
5136 c(),
5137 );
5138 "});
5139 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5140 cx.assert_editor_state(indoc! {"
5141 d(
5142 e,
5143 f
5144 )ˇ
5145 const a: B = (
5146 c(),
5147 );
5148 "});
5149
5150 // Cut an indented block, with the leading whitespace.
5151 cx.set_state(indoc! {"
5152 const a: B = (
5153 c(),
5154 « d(
5155 e,
5156 f
5157 )
5158 ˇ»);
5159 "});
5160 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5161 cx.assert_editor_state(indoc! {"
5162 const a: B = (
5163 c(),
5164 ˇ);
5165 "});
5166
5167 // Paste it at the same position.
5168 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5169 cx.assert_editor_state(indoc! {"
5170 const a: B = (
5171 c(),
5172 d(
5173 e,
5174 f
5175 )
5176 ˇ);
5177 "});
5178
5179 // Paste it at a line with a higher indent level.
5180 cx.set_state(indoc! {"
5181 const a: B = (
5182 c(),
5183 d(
5184 e,
5185 fˇ
5186 )
5187 );
5188 "});
5189 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5190 cx.assert_editor_state(indoc! {"
5191 const a: B = (
5192 c(),
5193 d(
5194 e,
5195 f d(
5196 e,
5197 f
5198 )
5199 ˇ
5200 )
5201 );
5202 "});
5203
5204 // Copy an indented block, starting mid-line
5205 cx.set_state(indoc! {"
5206 const a: B = (
5207 c(),
5208 somethin«g(
5209 e,
5210 f
5211 )ˇ»
5212 );
5213 "});
5214 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5215
5216 // Paste it on a line with a lower indent level
5217 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5218 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5219 cx.assert_editor_state(indoc! {"
5220 const a: B = (
5221 c(),
5222 something(
5223 e,
5224 f
5225 )
5226 );
5227 g(
5228 e,
5229 f
5230 )ˇ"});
5231}
5232
5233#[gpui::test]
5234async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5235 init_test(cx, |_| {});
5236
5237 cx.write_to_clipboard(ClipboardItem::new_string(
5238 " d(\n e\n );\n".into(),
5239 ));
5240
5241 let mut cx = EditorTestContext::new(cx).await;
5242 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5243
5244 cx.set_state(indoc! {"
5245 fn a() {
5246 b();
5247 if c() {
5248 ˇ
5249 }
5250 }
5251 "});
5252
5253 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5254 cx.assert_editor_state(indoc! {"
5255 fn a() {
5256 b();
5257 if c() {
5258 d(
5259 e
5260 );
5261 ˇ
5262 }
5263 }
5264 "});
5265
5266 cx.set_state(indoc! {"
5267 fn a() {
5268 b();
5269 ˇ
5270 }
5271 "});
5272
5273 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5274 cx.assert_editor_state(indoc! {"
5275 fn a() {
5276 b();
5277 d(
5278 e
5279 );
5280 ˇ
5281 }
5282 "});
5283}
5284
5285#[gpui::test]
5286fn test_select_all(cx: &mut TestAppContext) {
5287 init_test(cx, |_| {});
5288
5289 let editor = cx.add_window(|window, cx| {
5290 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5291 build_editor(buffer, window, cx)
5292 });
5293 _ = editor.update(cx, |editor, window, cx| {
5294 editor.select_all(&SelectAll, window, cx);
5295 assert_eq!(
5296 editor.selections.display_ranges(cx),
5297 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5298 );
5299 });
5300}
5301
5302#[gpui::test]
5303fn test_select_line(cx: &mut TestAppContext) {
5304 init_test(cx, |_| {});
5305
5306 let editor = cx.add_window(|window, cx| {
5307 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5308 build_editor(buffer, window, cx)
5309 });
5310 _ = editor.update(cx, |editor, window, cx| {
5311 editor.change_selections(None, window, cx, |s| {
5312 s.select_display_ranges([
5313 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5314 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5315 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5316 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5317 ])
5318 });
5319 editor.select_line(&SelectLine, window, cx);
5320 assert_eq!(
5321 editor.selections.display_ranges(cx),
5322 vec![
5323 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5324 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5325 ]
5326 );
5327 });
5328
5329 _ = editor.update(cx, |editor, window, cx| {
5330 editor.select_line(&SelectLine, window, cx);
5331 assert_eq!(
5332 editor.selections.display_ranges(cx),
5333 vec![
5334 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5335 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5336 ]
5337 );
5338 });
5339
5340 _ = editor.update(cx, |editor, window, cx| {
5341 editor.select_line(&SelectLine, window, cx);
5342 assert_eq!(
5343 editor.selections.display_ranges(cx),
5344 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5345 );
5346 });
5347}
5348
5349#[gpui::test]
5350async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5351 init_test(cx, |_| {});
5352 let mut cx = EditorTestContext::new(cx).await;
5353
5354 #[track_caller]
5355 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5356 cx.set_state(initial_state);
5357 cx.update_editor(|e, window, cx| {
5358 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5359 });
5360 cx.assert_editor_state(expected_state);
5361 }
5362
5363 // Selection starts and ends at the middle of lines, left-to-right
5364 test(
5365 &mut cx,
5366 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5367 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5368 );
5369 // Same thing, right-to-left
5370 test(
5371 &mut cx,
5372 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5373 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5374 );
5375
5376 // Whole buffer, left-to-right, last line *doesn't* end with newline
5377 test(
5378 &mut cx,
5379 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5380 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5381 );
5382 // Same thing, right-to-left
5383 test(
5384 &mut cx,
5385 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5386 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5387 );
5388
5389 // Whole buffer, left-to-right, last line ends with newline
5390 test(
5391 &mut cx,
5392 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5393 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5394 );
5395 // Same thing, right-to-left
5396 test(
5397 &mut cx,
5398 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5399 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5400 );
5401
5402 // Starts at the end of a line, ends at the start of another
5403 test(
5404 &mut cx,
5405 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5406 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5407 );
5408}
5409
5410#[gpui::test]
5411async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5412 init_test(cx, |_| {});
5413
5414 let editor = cx.add_window(|window, cx| {
5415 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5416 build_editor(buffer, window, cx)
5417 });
5418
5419 // setup
5420 _ = editor.update(cx, |editor, window, cx| {
5421 editor.fold_creases(
5422 vec![
5423 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5424 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5425 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5426 ],
5427 true,
5428 window,
5429 cx,
5430 );
5431 assert_eq!(
5432 editor.display_text(cx),
5433 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5434 );
5435 });
5436
5437 _ = editor.update(cx, |editor, window, cx| {
5438 editor.change_selections(None, window, cx, |s| {
5439 s.select_display_ranges([
5440 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5441 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5442 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5443 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5444 ])
5445 });
5446 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5447 assert_eq!(
5448 editor.display_text(cx),
5449 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5450 );
5451 });
5452 EditorTestContext::for_editor(editor, cx)
5453 .await
5454 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5455
5456 _ = editor.update(cx, |editor, window, cx| {
5457 editor.change_selections(None, window, cx, |s| {
5458 s.select_display_ranges([
5459 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5460 ])
5461 });
5462 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5463 assert_eq!(
5464 editor.display_text(cx),
5465 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5466 );
5467 assert_eq!(
5468 editor.selections.display_ranges(cx),
5469 [
5470 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5471 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5472 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5473 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5474 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5475 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5476 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5477 ]
5478 );
5479 });
5480 EditorTestContext::for_editor(editor, cx)
5481 .await
5482 .assert_editor_state(
5483 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5484 );
5485}
5486
5487#[gpui::test]
5488async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5489 init_test(cx, |_| {});
5490
5491 let mut cx = EditorTestContext::new(cx).await;
5492
5493 cx.set_state(indoc!(
5494 r#"abc
5495 defˇghi
5496
5497 jk
5498 nlmo
5499 "#
5500 ));
5501
5502 cx.update_editor(|editor, window, cx| {
5503 editor.add_selection_above(&Default::default(), window, cx);
5504 });
5505
5506 cx.assert_editor_state(indoc!(
5507 r#"abcˇ
5508 defˇghi
5509
5510 jk
5511 nlmo
5512 "#
5513 ));
5514
5515 cx.update_editor(|editor, window, cx| {
5516 editor.add_selection_above(&Default::default(), window, cx);
5517 });
5518
5519 cx.assert_editor_state(indoc!(
5520 r#"abcˇ
5521 defˇghi
5522
5523 jk
5524 nlmo
5525 "#
5526 ));
5527
5528 cx.update_editor(|editor, window, cx| {
5529 editor.add_selection_below(&Default::default(), window, cx);
5530 });
5531
5532 cx.assert_editor_state(indoc!(
5533 r#"abc
5534 defˇghi
5535
5536 jk
5537 nlmo
5538 "#
5539 ));
5540
5541 cx.update_editor(|editor, window, cx| {
5542 editor.undo_selection(&Default::default(), window, cx);
5543 });
5544
5545 cx.assert_editor_state(indoc!(
5546 r#"abcˇ
5547 defˇghi
5548
5549 jk
5550 nlmo
5551 "#
5552 ));
5553
5554 cx.update_editor(|editor, window, cx| {
5555 editor.redo_selection(&Default::default(), window, cx);
5556 });
5557
5558 cx.assert_editor_state(indoc!(
5559 r#"abc
5560 defˇghi
5561
5562 jk
5563 nlmo
5564 "#
5565 ));
5566
5567 cx.update_editor(|editor, window, cx| {
5568 editor.add_selection_below(&Default::default(), window, cx);
5569 });
5570
5571 cx.assert_editor_state(indoc!(
5572 r#"abc
5573 defˇghi
5574
5575 jk
5576 nlmˇo
5577 "#
5578 ));
5579
5580 cx.update_editor(|editor, window, cx| {
5581 editor.add_selection_below(&Default::default(), window, cx);
5582 });
5583
5584 cx.assert_editor_state(indoc!(
5585 r#"abc
5586 defˇghi
5587
5588 jk
5589 nlmˇo
5590 "#
5591 ));
5592
5593 // change selections
5594 cx.set_state(indoc!(
5595 r#"abc
5596 def«ˇg»hi
5597
5598 jk
5599 nlmo
5600 "#
5601 ));
5602
5603 cx.update_editor(|editor, window, cx| {
5604 editor.add_selection_below(&Default::default(), window, cx);
5605 });
5606
5607 cx.assert_editor_state(indoc!(
5608 r#"abc
5609 def«ˇg»hi
5610
5611 jk
5612 nlm«ˇo»
5613 "#
5614 ));
5615
5616 cx.update_editor(|editor, window, cx| {
5617 editor.add_selection_below(&Default::default(), window, cx);
5618 });
5619
5620 cx.assert_editor_state(indoc!(
5621 r#"abc
5622 def«ˇg»hi
5623
5624 jk
5625 nlm«ˇo»
5626 "#
5627 ));
5628
5629 cx.update_editor(|editor, window, cx| {
5630 editor.add_selection_above(&Default::default(), window, cx);
5631 });
5632
5633 cx.assert_editor_state(indoc!(
5634 r#"abc
5635 def«ˇg»hi
5636
5637 jk
5638 nlmo
5639 "#
5640 ));
5641
5642 cx.update_editor(|editor, window, cx| {
5643 editor.add_selection_above(&Default::default(), window, cx);
5644 });
5645
5646 cx.assert_editor_state(indoc!(
5647 r#"abc
5648 def«ˇg»hi
5649
5650 jk
5651 nlmo
5652 "#
5653 ));
5654
5655 // Change selections again
5656 cx.set_state(indoc!(
5657 r#"a«bc
5658 defgˇ»hi
5659
5660 jk
5661 nlmo
5662 "#
5663 ));
5664
5665 cx.update_editor(|editor, window, cx| {
5666 editor.add_selection_below(&Default::default(), window, cx);
5667 });
5668
5669 cx.assert_editor_state(indoc!(
5670 r#"a«bcˇ»
5671 d«efgˇ»hi
5672
5673 j«kˇ»
5674 nlmo
5675 "#
5676 ));
5677
5678 cx.update_editor(|editor, window, cx| {
5679 editor.add_selection_below(&Default::default(), window, cx);
5680 });
5681 cx.assert_editor_state(indoc!(
5682 r#"a«bcˇ»
5683 d«efgˇ»hi
5684
5685 j«kˇ»
5686 n«lmoˇ»
5687 "#
5688 ));
5689 cx.update_editor(|editor, window, cx| {
5690 editor.add_selection_above(&Default::default(), window, cx);
5691 });
5692
5693 cx.assert_editor_state(indoc!(
5694 r#"a«bcˇ»
5695 d«efgˇ»hi
5696
5697 j«kˇ»
5698 nlmo
5699 "#
5700 ));
5701
5702 // Change selections again
5703 cx.set_state(indoc!(
5704 r#"abc
5705 d«ˇefghi
5706
5707 jk
5708 nlm»o
5709 "#
5710 ));
5711
5712 cx.update_editor(|editor, window, cx| {
5713 editor.add_selection_above(&Default::default(), window, cx);
5714 });
5715
5716 cx.assert_editor_state(indoc!(
5717 r#"a«ˇbc»
5718 d«ˇef»ghi
5719
5720 j«ˇk»
5721 n«ˇlm»o
5722 "#
5723 ));
5724
5725 cx.update_editor(|editor, window, cx| {
5726 editor.add_selection_below(&Default::default(), window, cx);
5727 });
5728
5729 cx.assert_editor_state(indoc!(
5730 r#"abc
5731 d«ˇef»ghi
5732
5733 j«ˇk»
5734 n«ˇlm»o
5735 "#
5736 ));
5737}
5738
5739#[gpui::test]
5740async fn test_select_next(cx: &mut TestAppContext) {
5741 init_test(cx, |_| {});
5742
5743 let mut cx = EditorTestContext::new(cx).await;
5744 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5745
5746 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5747 .unwrap();
5748 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5749
5750 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5751 .unwrap();
5752 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5753
5754 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5755 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5756
5757 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5758 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5759
5760 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5761 .unwrap();
5762 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5763
5764 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5765 .unwrap();
5766 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5767}
5768
5769#[gpui::test]
5770async fn test_select_all_matches(cx: &mut TestAppContext) {
5771 init_test(cx, |_| {});
5772
5773 let mut cx = EditorTestContext::new(cx).await;
5774
5775 // Test caret-only selections
5776 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5777 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5778 .unwrap();
5779 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5780
5781 // Test left-to-right selections
5782 cx.set_state("abc\n«abcˇ»\nabc");
5783 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5784 .unwrap();
5785 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5786
5787 // Test right-to-left selections
5788 cx.set_state("abc\n«ˇabc»\nabc");
5789 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5790 .unwrap();
5791 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5792
5793 // Test selecting whitespace with caret selection
5794 cx.set_state("abc\nˇ abc\nabc");
5795 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5796 .unwrap();
5797 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5798
5799 // Test selecting whitespace with left-to-right selection
5800 cx.set_state("abc\n«ˇ »abc\nabc");
5801 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5802 .unwrap();
5803 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
5804
5805 // Test no matches with right-to-left selection
5806 cx.set_state("abc\n« ˇ»abc\nabc");
5807 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5808 .unwrap();
5809 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5810}
5811
5812#[gpui::test]
5813async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
5814 init_test(cx, |_| {});
5815
5816 let mut cx = EditorTestContext::new(cx).await;
5817 cx.set_state(
5818 r#"let foo = 2;
5819lˇet foo = 2;
5820let fooˇ = 2;
5821let foo = 2;
5822let foo = ˇ2;"#,
5823 );
5824
5825 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5826 .unwrap();
5827 cx.assert_editor_state(
5828 r#"let foo = 2;
5829«letˇ» foo = 2;
5830let «fooˇ» = 2;
5831let foo = 2;
5832let foo = «2ˇ»;"#,
5833 );
5834
5835 // noop for multiple selections with different contents
5836 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5837 .unwrap();
5838 cx.assert_editor_state(
5839 r#"let foo = 2;
5840«letˇ» foo = 2;
5841let «fooˇ» = 2;
5842let foo = 2;
5843let foo = «2ˇ»;"#,
5844 );
5845}
5846
5847#[gpui::test]
5848async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
5849 init_test(cx, |_| {});
5850
5851 let mut cx =
5852 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
5853
5854 cx.assert_editor_state(indoc! {"
5855 ˇbbb
5856 ccc
5857
5858 bbb
5859 ccc
5860 "});
5861 cx.dispatch_action(SelectPrevious::default());
5862 cx.assert_editor_state(indoc! {"
5863 «bbbˇ»
5864 ccc
5865
5866 bbb
5867 ccc
5868 "});
5869 cx.dispatch_action(SelectPrevious::default());
5870 cx.assert_editor_state(indoc! {"
5871 «bbbˇ»
5872 ccc
5873
5874 «bbbˇ»
5875 ccc
5876 "});
5877}
5878
5879#[gpui::test]
5880async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
5881 init_test(cx, |_| {});
5882
5883 let mut cx = EditorTestContext::new(cx).await;
5884 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5885
5886 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5887 .unwrap();
5888 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5889
5890 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5891 .unwrap();
5892 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5893
5894 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5895 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5896
5897 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5898 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5899
5900 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5901 .unwrap();
5902 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
5903
5904 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5905 .unwrap();
5906 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
5907
5908 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5909 .unwrap();
5910 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5911}
5912
5913#[gpui::test]
5914async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
5915 init_test(cx, |_| {});
5916
5917 let mut cx = EditorTestContext::new(cx).await;
5918 cx.set_state("aˇ");
5919
5920 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5921 .unwrap();
5922 cx.assert_editor_state("«aˇ»");
5923 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5924 .unwrap();
5925 cx.assert_editor_state("«aˇ»");
5926}
5927
5928#[gpui::test]
5929async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
5930 init_test(cx, |_| {});
5931
5932 let mut cx = EditorTestContext::new(cx).await;
5933 cx.set_state(
5934 r#"let foo = 2;
5935lˇet foo = 2;
5936let fooˇ = 2;
5937let foo = 2;
5938let foo = ˇ2;"#,
5939 );
5940
5941 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5942 .unwrap();
5943 cx.assert_editor_state(
5944 r#"let foo = 2;
5945«letˇ» foo = 2;
5946let «fooˇ» = 2;
5947let foo = 2;
5948let foo = «2ˇ»;"#,
5949 );
5950
5951 // noop for multiple selections with different contents
5952 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5953 .unwrap();
5954 cx.assert_editor_state(
5955 r#"let foo = 2;
5956«letˇ» foo = 2;
5957let «fooˇ» = 2;
5958let foo = 2;
5959let foo = «2ˇ»;"#,
5960 );
5961}
5962
5963#[gpui::test]
5964async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
5965 init_test(cx, |_| {});
5966
5967 let mut cx = EditorTestContext::new(cx).await;
5968 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5969
5970 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5971 .unwrap();
5972 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5973
5974 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5975 .unwrap();
5976 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5977
5978 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5979 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5980
5981 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5982 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5983
5984 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5985 .unwrap();
5986 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
5987
5988 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5989 .unwrap();
5990 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5991}
5992
5993#[gpui::test]
5994async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
5995 init_test(cx, |_| {});
5996
5997 let language = Arc::new(Language::new(
5998 LanguageConfig::default(),
5999 Some(tree_sitter_rust::LANGUAGE.into()),
6000 ));
6001
6002 let text = r#"
6003 use mod1::mod2::{mod3, mod4};
6004
6005 fn fn_1(param1: bool, param2: &str) {
6006 let var1 = "text";
6007 }
6008 "#
6009 .unindent();
6010
6011 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6012 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6013 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6014
6015 editor
6016 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6017 .await;
6018
6019 editor.update_in(cx, |editor, window, cx| {
6020 editor.change_selections(None, window, cx, |s| {
6021 s.select_display_ranges([
6022 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6023 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6024 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6025 ]);
6026 });
6027 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6028 });
6029 editor.update(cx, |editor, cx| {
6030 assert_text_with_selections(
6031 editor,
6032 indoc! {r#"
6033 use mod1::mod2::{mod3, «mod4ˇ»};
6034
6035 fn fn_1«ˇ(param1: bool, param2: &str)» {
6036 let var1 = "«textˇ»";
6037 }
6038 "#},
6039 cx,
6040 );
6041 });
6042
6043 editor.update_in(cx, |editor, window, cx| {
6044 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6045 });
6046 editor.update(cx, |editor, cx| {
6047 assert_text_with_selections(
6048 editor,
6049 indoc! {r#"
6050 use mod1::mod2::«{mod3, mod4}ˇ»;
6051
6052 «ˇfn fn_1(param1: bool, param2: &str) {
6053 let var1 = "text";
6054 }»
6055 "#},
6056 cx,
6057 );
6058 });
6059
6060 editor.update_in(cx, |editor, window, cx| {
6061 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6062 });
6063 assert_eq!(
6064 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6065 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6066 );
6067
6068 // Trying to expand the selected syntax node one more time has no effect.
6069 editor.update_in(cx, |editor, window, cx| {
6070 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6071 });
6072 assert_eq!(
6073 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6074 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6075 );
6076
6077 editor.update_in(cx, |editor, window, cx| {
6078 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6079 });
6080 editor.update(cx, |editor, cx| {
6081 assert_text_with_selections(
6082 editor,
6083 indoc! {r#"
6084 use mod1::mod2::«{mod3, mod4}ˇ»;
6085
6086 «ˇfn fn_1(param1: bool, param2: &str) {
6087 let var1 = "text";
6088 }»
6089 "#},
6090 cx,
6091 );
6092 });
6093
6094 editor.update_in(cx, |editor, window, cx| {
6095 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6096 });
6097 editor.update(cx, |editor, cx| {
6098 assert_text_with_selections(
6099 editor,
6100 indoc! {r#"
6101 use mod1::mod2::{mod3, «mod4ˇ»};
6102
6103 fn fn_1«ˇ(param1: bool, param2: &str)» {
6104 let var1 = "«textˇ»";
6105 }
6106 "#},
6107 cx,
6108 );
6109 });
6110
6111 editor.update_in(cx, |editor, window, cx| {
6112 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6113 });
6114 editor.update(cx, |editor, cx| {
6115 assert_text_with_selections(
6116 editor,
6117 indoc! {r#"
6118 use mod1::mod2::{mod3, mo«ˇ»d4};
6119
6120 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6121 let var1 = "te«ˇ»xt";
6122 }
6123 "#},
6124 cx,
6125 );
6126 });
6127
6128 // Trying to shrink the selected syntax node one more time has no effect.
6129 editor.update_in(cx, |editor, window, cx| {
6130 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6131 });
6132 editor.update_in(cx, |editor, _, cx| {
6133 assert_text_with_selections(
6134 editor,
6135 indoc! {r#"
6136 use mod1::mod2::{mod3, mo«ˇ»d4};
6137
6138 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6139 let var1 = "te«ˇ»xt";
6140 }
6141 "#},
6142 cx,
6143 );
6144 });
6145
6146 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6147 // a fold.
6148 editor.update_in(cx, |editor, window, cx| {
6149 editor.fold_creases(
6150 vec![
6151 Crease::simple(
6152 Point::new(0, 21)..Point::new(0, 24),
6153 FoldPlaceholder::test(),
6154 ),
6155 Crease::simple(
6156 Point::new(3, 20)..Point::new(3, 22),
6157 FoldPlaceholder::test(),
6158 ),
6159 ],
6160 true,
6161 window,
6162 cx,
6163 );
6164 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6165 });
6166 editor.update(cx, |editor, cx| {
6167 assert_text_with_selections(
6168 editor,
6169 indoc! {r#"
6170 use mod1::mod2::«{mod3, mod4}ˇ»;
6171
6172 fn fn_1«ˇ(param1: bool, param2: &str)» {
6173 «let var1 = "text";ˇ»
6174 }
6175 "#},
6176 cx,
6177 );
6178 });
6179}
6180
6181#[gpui::test]
6182async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6183 init_test(cx, |_| {});
6184
6185 let base_text = r#"
6186 impl A {
6187 // this is an uncommitted comment
6188
6189 fn b() {
6190 c();
6191 }
6192
6193 // this is another uncommitted comment
6194
6195 fn d() {
6196 // e
6197 // f
6198 }
6199 }
6200
6201 fn g() {
6202 // h
6203 }
6204 "#
6205 .unindent();
6206
6207 let text = r#"
6208 ˇimpl A {
6209
6210 fn b() {
6211 c();
6212 }
6213
6214 fn d() {
6215 // e
6216 // f
6217 }
6218 }
6219
6220 fn g() {
6221 // h
6222 }
6223 "#
6224 .unindent();
6225
6226 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6227 cx.set_state(&text);
6228 cx.set_head_text(&base_text);
6229 cx.update_editor(|editor, window, cx| {
6230 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6231 });
6232
6233 cx.assert_state_with_diff(
6234 "
6235 ˇimpl A {
6236 - // this is an uncommitted comment
6237
6238 fn b() {
6239 c();
6240 }
6241
6242 - // this is another uncommitted comment
6243 -
6244 fn d() {
6245 // e
6246 // f
6247 }
6248 }
6249
6250 fn g() {
6251 // h
6252 }
6253 "
6254 .unindent(),
6255 );
6256
6257 let expected_display_text = "
6258 impl A {
6259 // this is an uncommitted comment
6260
6261 fn b() {
6262 ⋯
6263 }
6264
6265 // this is another uncommitted comment
6266
6267 fn d() {
6268 ⋯
6269 }
6270 }
6271
6272 fn g() {
6273 ⋯
6274 }
6275 "
6276 .unindent();
6277
6278 cx.update_editor(|editor, window, cx| {
6279 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
6280 assert_eq!(editor.display_text(cx), expected_display_text);
6281 });
6282}
6283
6284#[gpui::test]
6285async fn test_autoindent(cx: &mut TestAppContext) {
6286 init_test(cx, |_| {});
6287
6288 let language = Arc::new(
6289 Language::new(
6290 LanguageConfig {
6291 brackets: BracketPairConfig {
6292 pairs: vec![
6293 BracketPair {
6294 start: "{".to_string(),
6295 end: "}".to_string(),
6296 close: false,
6297 surround: false,
6298 newline: true,
6299 },
6300 BracketPair {
6301 start: "(".to_string(),
6302 end: ")".to_string(),
6303 close: false,
6304 surround: false,
6305 newline: true,
6306 },
6307 ],
6308 ..Default::default()
6309 },
6310 ..Default::default()
6311 },
6312 Some(tree_sitter_rust::LANGUAGE.into()),
6313 )
6314 .with_indents_query(
6315 r#"
6316 (_ "(" ")" @end) @indent
6317 (_ "{" "}" @end) @indent
6318 "#,
6319 )
6320 .unwrap(),
6321 );
6322
6323 let text = "fn a() {}";
6324
6325 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6326 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6327 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6328 editor
6329 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6330 .await;
6331
6332 editor.update_in(cx, |editor, window, cx| {
6333 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
6334 editor.newline(&Newline, window, cx);
6335 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
6336 assert_eq!(
6337 editor.selections.ranges(cx),
6338 &[
6339 Point::new(1, 4)..Point::new(1, 4),
6340 Point::new(3, 4)..Point::new(3, 4),
6341 Point::new(5, 0)..Point::new(5, 0)
6342 ]
6343 );
6344 });
6345}
6346
6347#[gpui::test]
6348async fn test_autoindent_selections(cx: &mut TestAppContext) {
6349 init_test(cx, |_| {});
6350
6351 {
6352 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6353 cx.set_state(indoc! {"
6354 impl A {
6355
6356 fn b() {}
6357
6358 «fn c() {
6359
6360 }ˇ»
6361 }
6362 "});
6363
6364 cx.update_editor(|editor, window, cx| {
6365 editor.autoindent(&Default::default(), window, cx);
6366 });
6367
6368 cx.assert_editor_state(indoc! {"
6369 impl A {
6370
6371 fn b() {}
6372
6373 «fn c() {
6374
6375 }ˇ»
6376 }
6377 "});
6378 }
6379
6380 {
6381 let mut cx = EditorTestContext::new_multibuffer(
6382 cx,
6383 [indoc! { "
6384 impl A {
6385 «
6386 // a
6387 fn b(){}
6388 »
6389 «
6390 }
6391 fn c(){}
6392 »
6393 "}],
6394 );
6395
6396 let buffer = cx.update_editor(|editor, _, cx| {
6397 let buffer = editor.buffer().update(cx, |buffer, _| {
6398 buffer.all_buffers().iter().next().unwrap().clone()
6399 });
6400 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6401 buffer
6402 });
6403
6404 cx.run_until_parked();
6405 cx.update_editor(|editor, window, cx| {
6406 editor.select_all(&Default::default(), window, cx);
6407 editor.autoindent(&Default::default(), window, cx)
6408 });
6409 cx.run_until_parked();
6410
6411 cx.update(|_, cx| {
6412 pretty_assertions::assert_eq!(
6413 buffer.read(cx).text(),
6414 indoc! { "
6415 impl A {
6416
6417 // a
6418 fn b(){}
6419
6420
6421 }
6422 fn c(){}
6423
6424 " }
6425 )
6426 });
6427 }
6428}
6429
6430#[gpui::test]
6431async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
6432 init_test(cx, |_| {});
6433
6434 let mut cx = EditorTestContext::new(cx).await;
6435
6436 let language = Arc::new(Language::new(
6437 LanguageConfig {
6438 brackets: BracketPairConfig {
6439 pairs: vec![
6440 BracketPair {
6441 start: "{".to_string(),
6442 end: "}".to_string(),
6443 close: true,
6444 surround: true,
6445 newline: true,
6446 },
6447 BracketPair {
6448 start: "(".to_string(),
6449 end: ")".to_string(),
6450 close: true,
6451 surround: true,
6452 newline: true,
6453 },
6454 BracketPair {
6455 start: "/*".to_string(),
6456 end: " */".to_string(),
6457 close: true,
6458 surround: true,
6459 newline: true,
6460 },
6461 BracketPair {
6462 start: "[".to_string(),
6463 end: "]".to_string(),
6464 close: false,
6465 surround: false,
6466 newline: true,
6467 },
6468 BracketPair {
6469 start: "\"".to_string(),
6470 end: "\"".to_string(),
6471 close: true,
6472 surround: true,
6473 newline: false,
6474 },
6475 BracketPair {
6476 start: "<".to_string(),
6477 end: ">".to_string(),
6478 close: false,
6479 surround: true,
6480 newline: true,
6481 },
6482 ],
6483 ..Default::default()
6484 },
6485 autoclose_before: "})]".to_string(),
6486 ..Default::default()
6487 },
6488 Some(tree_sitter_rust::LANGUAGE.into()),
6489 ));
6490
6491 cx.language_registry().add(language.clone());
6492 cx.update_buffer(|buffer, cx| {
6493 buffer.set_language(Some(language), cx);
6494 });
6495
6496 cx.set_state(
6497 &r#"
6498 🏀ˇ
6499 εˇ
6500 ❤️ˇ
6501 "#
6502 .unindent(),
6503 );
6504
6505 // autoclose multiple nested brackets at multiple cursors
6506 cx.update_editor(|editor, window, cx| {
6507 editor.handle_input("{", window, cx);
6508 editor.handle_input("{", window, cx);
6509 editor.handle_input("{", window, cx);
6510 });
6511 cx.assert_editor_state(
6512 &"
6513 🏀{{{ˇ}}}
6514 ε{{{ˇ}}}
6515 ❤️{{{ˇ}}}
6516 "
6517 .unindent(),
6518 );
6519
6520 // insert a different closing bracket
6521 cx.update_editor(|editor, window, cx| {
6522 editor.handle_input(")", window, cx);
6523 });
6524 cx.assert_editor_state(
6525 &"
6526 🏀{{{)ˇ}}}
6527 ε{{{)ˇ}}}
6528 ❤️{{{)ˇ}}}
6529 "
6530 .unindent(),
6531 );
6532
6533 // skip over the auto-closed brackets when typing a closing bracket
6534 cx.update_editor(|editor, window, cx| {
6535 editor.move_right(&MoveRight, window, cx);
6536 editor.handle_input("}", window, cx);
6537 editor.handle_input("}", window, cx);
6538 editor.handle_input("}", window, cx);
6539 });
6540 cx.assert_editor_state(
6541 &"
6542 🏀{{{)}}}}ˇ
6543 ε{{{)}}}}ˇ
6544 ❤️{{{)}}}}ˇ
6545 "
6546 .unindent(),
6547 );
6548
6549 // autoclose multi-character pairs
6550 cx.set_state(
6551 &"
6552 ˇ
6553 ˇ
6554 "
6555 .unindent(),
6556 );
6557 cx.update_editor(|editor, window, cx| {
6558 editor.handle_input("/", window, cx);
6559 editor.handle_input("*", window, cx);
6560 });
6561 cx.assert_editor_state(
6562 &"
6563 /*ˇ */
6564 /*ˇ */
6565 "
6566 .unindent(),
6567 );
6568
6569 // one cursor autocloses a multi-character pair, one cursor
6570 // does not autoclose.
6571 cx.set_state(
6572 &"
6573 /ˇ
6574 ˇ
6575 "
6576 .unindent(),
6577 );
6578 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6579 cx.assert_editor_state(
6580 &"
6581 /*ˇ */
6582 *ˇ
6583 "
6584 .unindent(),
6585 );
6586
6587 // Don't autoclose if the next character isn't whitespace and isn't
6588 // listed in the language's "autoclose_before" section.
6589 cx.set_state("ˇa b");
6590 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6591 cx.assert_editor_state("{ˇa b");
6592
6593 // Don't autoclose if `close` is false for the bracket pair
6594 cx.set_state("ˇ");
6595 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6596 cx.assert_editor_state("[ˇ");
6597
6598 // Surround with brackets if text is selected
6599 cx.set_state("«aˇ» b");
6600 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6601 cx.assert_editor_state("{«aˇ»} b");
6602
6603 // Autoclose when not immediately after a word character
6604 cx.set_state("a ˇ");
6605 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6606 cx.assert_editor_state("a \"ˇ\"");
6607
6608 // Autoclose pair where the start and end characters are the same
6609 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6610 cx.assert_editor_state("a \"\"ˇ");
6611
6612 // Don't autoclose when immediately after a word character
6613 cx.set_state("aˇ");
6614 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6615 cx.assert_editor_state("a\"ˇ");
6616
6617 // Do autoclose when after a non-word character
6618 cx.set_state("{ˇ");
6619 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6620 cx.assert_editor_state("{\"ˇ\"");
6621
6622 // Non identical pairs autoclose regardless of preceding character
6623 cx.set_state("aˇ");
6624 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6625 cx.assert_editor_state("a{ˇ}");
6626
6627 // Don't autoclose pair if autoclose is disabled
6628 cx.set_state("ˇ");
6629 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6630 cx.assert_editor_state("<ˇ");
6631
6632 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6633 cx.set_state("«aˇ» b");
6634 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6635 cx.assert_editor_state("<«aˇ»> b");
6636}
6637
6638#[gpui::test]
6639async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
6640 init_test(cx, |settings| {
6641 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6642 });
6643
6644 let mut cx = EditorTestContext::new(cx).await;
6645
6646 let language = Arc::new(Language::new(
6647 LanguageConfig {
6648 brackets: BracketPairConfig {
6649 pairs: vec![
6650 BracketPair {
6651 start: "{".to_string(),
6652 end: "}".to_string(),
6653 close: true,
6654 surround: true,
6655 newline: true,
6656 },
6657 BracketPair {
6658 start: "(".to_string(),
6659 end: ")".to_string(),
6660 close: true,
6661 surround: true,
6662 newline: true,
6663 },
6664 BracketPair {
6665 start: "[".to_string(),
6666 end: "]".to_string(),
6667 close: false,
6668 surround: false,
6669 newline: true,
6670 },
6671 ],
6672 ..Default::default()
6673 },
6674 autoclose_before: "})]".to_string(),
6675 ..Default::default()
6676 },
6677 Some(tree_sitter_rust::LANGUAGE.into()),
6678 ));
6679
6680 cx.language_registry().add(language.clone());
6681 cx.update_buffer(|buffer, cx| {
6682 buffer.set_language(Some(language), cx);
6683 });
6684
6685 cx.set_state(
6686 &"
6687 ˇ
6688 ˇ
6689 ˇ
6690 "
6691 .unindent(),
6692 );
6693
6694 // ensure only matching closing brackets are skipped over
6695 cx.update_editor(|editor, window, cx| {
6696 editor.handle_input("}", window, cx);
6697 editor.move_left(&MoveLeft, window, cx);
6698 editor.handle_input(")", window, cx);
6699 editor.move_left(&MoveLeft, window, cx);
6700 });
6701 cx.assert_editor_state(
6702 &"
6703 ˇ)}
6704 ˇ)}
6705 ˇ)}
6706 "
6707 .unindent(),
6708 );
6709
6710 // skip-over closing brackets at multiple cursors
6711 cx.update_editor(|editor, window, cx| {
6712 editor.handle_input(")", window, cx);
6713 editor.handle_input("}", window, cx);
6714 });
6715 cx.assert_editor_state(
6716 &"
6717 )}ˇ
6718 )}ˇ
6719 )}ˇ
6720 "
6721 .unindent(),
6722 );
6723
6724 // ignore non-close brackets
6725 cx.update_editor(|editor, window, cx| {
6726 editor.handle_input("]", window, cx);
6727 editor.move_left(&MoveLeft, window, cx);
6728 editor.handle_input("]", window, cx);
6729 });
6730 cx.assert_editor_state(
6731 &"
6732 )}]ˇ]
6733 )}]ˇ]
6734 )}]ˇ]
6735 "
6736 .unindent(),
6737 );
6738}
6739
6740#[gpui::test]
6741async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
6742 init_test(cx, |_| {});
6743
6744 let mut cx = EditorTestContext::new(cx).await;
6745
6746 let html_language = Arc::new(
6747 Language::new(
6748 LanguageConfig {
6749 name: "HTML".into(),
6750 brackets: BracketPairConfig {
6751 pairs: vec![
6752 BracketPair {
6753 start: "<".into(),
6754 end: ">".into(),
6755 close: true,
6756 ..Default::default()
6757 },
6758 BracketPair {
6759 start: "{".into(),
6760 end: "}".into(),
6761 close: true,
6762 ..Default::default()
6763 },
6764 BracketPair {
6765 start: "(".into(),
6766 end: ")".into(),
6767 close: true,
6768 ..Default::default()
6769 },
6770 ],
6771 ..Default::default()
6772 },
6773 autoclose_before: "})]>".into(),
6774 ..Default::default()
6775 },
6776 Some(tree_sitter_html::LANGUAGE.into()),
6777 )
6778 .with_injection_query(
6779 r#"
6780 (script_element
6781 (raw_text) @injection.content
6782 (#set! injection.language "javascript"))
6783 "#,
6784 )
6785 .unwrap(),
6786 );
6787
6788 let javascript_language = Arc::new(Language::new(
6789 LanguageConfig {
6790 name: "JavaScript".into(),
6791 brackets: BracketPairConfig {
6792 pairs: vec![
6793 BracketPair {
6794 start: "/*".into(),
6795 end: " */".into(),
6796 close: true,
6797 ..Default::default()
6798 },
6799 BracketPair {
6800 start: "{".into(),
6801 end: "}".into(),
6802 close: true,
6803 ..Default::default()
6804 },
6805 BracketPair {
6806 start: "(".into(),
6807 end: ")".into(),
6808 close: true,
6809 ..Default::default()
6810 },
6811 ],
6812 ..Default::default()
6813 },
6814 autoclose_before: "})]>".into(),
6815 ..Default::default()
6816 },
6817 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6818 ));
6819
6820 cx.language_registry().add(html_language.clone());
6821 cx.language_registry().add(javascript_language.clone());
6822
6823 cx.update_buffer(|buffer, cx| {
6824 buffer.set_language(Some(html_language), cx);
6825 });
6826
6827 cx.set_state(
6828 &r#"
6829 <body>ˇ
6830 <script>
6831 var x = 1;ˇ
6832 </script>
6833 </body>ˇ
6834 "#
6835 .unindent(),
6836 );
6837
6838 // Precondition: different languages are active at different locations.
6839 cx.update_editor(|editor, window, cx| {
6840 let snapshot = editor.snapshot(window, cx);
6841 let cursors = editor.selections.ranges::<usize>(cx);
6842 let languages = cursors
6843 .iter()
6844 .map(|c| snapshot.language_at(c.start).unwrap().name())
6845 .collect::<Vec<_>>();
6846 assert_eq!(
6847 languages,
6848 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6849 );
6850 });
6851
6852 // Angle brackets autoclose in HTML, but not JavaScript.
6853 cx.update_editor(|editor, window, cx| {
6854 editor.handle_input("<", window, cx);
6855 editor.handle_input("a", window, cx);
6856 });
6857 cx.assert_editor_state(
6858 &r#"
6859 <body><aˇ>
6860 <script>
6861 var x = 1;<aˇ
6862 </script>
6863 </body><aˇ>
6864 "#
6865 .unindent(),
6866 );
6867
6868 // Curly braces and parens autoclose in both HTML and JavaScript.
6869 cx.update_editor(|editor, window, cx| {
6870 editor.handle_input(" b=", window, cx);
6871 editor.handle_input("{", window, cx);
6872 editor.handle_input("c", window, cx);
6873 editor.handle_input("(", window, cx);
6874 });
6875 cx.assert_editor_state(
6876 &r#"
6877 <body><a b={c(ˇ)}>
6878 <script>
6879 var x = 1;<a b={c(ˇ)}
6880 </script>
6881 </body><a b={c(ˇ)}>
6882 "#
6883 .unindent(),
6884 );
6885
6886 // Brackets that were already autoclosed are skipped.
6887 cx.update_editor(|editor, window, cx| {
6888 editor.handle_input(")", window, cx);
6889 editor.handle_input("d", window, cx);
6890 editor.handle_input("}", window, cx);
6891 });
6892 cx.assert_editor_state(
6893 &r#"
6894 <body><a b={c()d}ˇ>
6895 <script>
6896 var x = 1;<a b={c()d}ˇ
6897 </script>
6898 </body><a b={c()d}ˇ>
6899 "#
6900 .unindent(),
6901 );
6902 cx.update_editor(|editor, window, cx| {
6903 editor.handle_input(">", window, cx);
6904 });
6905 cx.assert_editor_state(
6906 &r#"
6907 <body><a b={c()d}>ˇ
6908 <script>
6909 var x = 1;<a b={c()d}>ˇ
6910 </script>
6911 </body><a b={c()d}>ˇ
6912 "#
6913 .unindent(),
6914 );
6915
6916 // Reset
6917 cx.set_state(
6918 &r#"
6919 <body>ˇ
6920 <script>
6921 var x = 1;ˇ
6922 </script>
6923 </body>ˇ
6924 "#
6925 .unindent(),
6926 );
6927
6928 cx.update_editor(|editor, window, cx| {
6929 editor.handle_input("<", window, cx);
6930 });
6931 cx.assert_editor_state(
6932 &r#"
6933 <body><ˇ>
6934 <script>
6935 var x = 1;<ˇ
6936 </script>
6937 </body><ˇ>
6938 "#
6939 .unindent(),
6940 );
6941
6942 // When backspacing, the closing angle brackets are removed.
6943 cx.update_editor(|editor, window, cx| {
6944 editor.backspace(&Backspace, window, cx);
6945 });
6946 cx.assert_editor_state(
6947 &r#"
6948 <body>ˇ
6949 <script>
6950 var x = 1;ˇ
6951 </script>
6952 </body>ˇ
6953 "#
6954 .unindent(),
6955 );
6956
6957 // Block comments autoclose in JavaScript, but not HTML.
6958 cx.update_editor(|editor, window, cx| {
6959 editor.handle_input("/", window, cx);
6960 editor.handle_input("*", window, cx);
6961 });
6962 cx.assert_editor_state(
6963 &r#"
6964 <body>/*ˇ
6965 <script>
6966 var x = 1;/*ˇ */
6967 </script>
6968 </body>/*ˇ
6969 "#
6970 .unindent(),
6971 );
6972}
6973
6974#[gpui::test]
6975async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
6976 init_test(cx, |_| {});
6977
6978 let mut cx = EditorTestContext::new(cx).await;
6979
6980 let rust_language = Arc::new(
6981 Language::new(
6982 LanguageConfig {
6983 name: "Rust".into(),
6984 brackets: serde_json::from_value(json!([
6985 { "start": "{", "end": "}", "close": true, "newline": true },
6986 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
6987 ]))
6988 .unwrap(),
6989 autoclose_before: "})]>".into(),
6990 ..Default::default()
6991 },
6992 Some(tree_sitter_rust::LANGUAGE.into()),
6993 )
6994 .with_override_query("(string_literal) @string")
6995 .unwrap(),
6996 );
6997
6998 cx.language_registry().add(rust_language.clone());
6999 cx.update_buffer(|buffer, cx| {
7000 buffer.set_language(Some(rust_language), cx);
7001 });
7002
7003 cx.set_state(
7004 &r#"
7005 let x = ˇ
7006 "#
7007 .unindent(),
7008 );
7009
7010 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7011 cx.update_editor(|editor, window, cx| {
7012 editor.handle_input("\"", window, cx);
7013 });
7014 cx.assert_editor_state(
7015 &r#"
7016 let x = "ˇ"
7017 "#
7018 .unindent(),
7019 );
7020
7021 // Inserting another quotation mark. The cursor moves across the existing
7022 // automatically-inserted quotation mark.
7023 cx.update_editor(|editor, window, cx| {
7024 editor.handle_input("\"", window, cx);
7025 });
7026 cx.assert_editor_state(
7027 &r#"
7028 let x = ""ˇ
7029 "#
7030 .unindent(),
7031 );
7032
7033 // Reset
7034 cx.set_state(
7035 &r#"
7036 let x = ˇ
7037 "#
7038 .unindent(),
7039 );
7040
7041 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7042 cx.update_editor(|editor, window, cx| {
7043 editor.handle_input("\"", window, cx);
7044 editor.handle_input(" ", window, cx);
7045 editor.move_left(&Default::default(), window, cx);
7046 editor.handle_input("\\", window, cx);
7047 editor.handle_input("\"", window, cx);
7048 });
7049 cx.assert_editor_state(
7050 &r#"
7051 let x = "\"ˇ "
7052 "#
7053 .unindent(),
7054 );
7055
7056 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7057 // mark. Nothing is inserted.
7058 cx.update_editor(|editor, window, cx| {
7059 editor.move_right(&Default::default(), window, cx);
7060 editor.handle_input("\"", window, cx);
7061 });
7062 cx.assert_editor_state(
7063 &r#"
7064 let x = "\" "ˇ
7065 "#
7066 .unindent(),
7067 );
7068}
7069
7070#[gpui::test]
7071async fn test_surround_with_pair(cx: &mut TestAppContext) {
7072 init_test(cx, |_| {});
7073
7074 let language = Arc::new(Language::new(
7075 LanguageConfig {
7076 brackets: BracketPairConfig {
7077 pairs: vec![
7078 BracketPair {
7079 start: "{".to_string(),
7080 end: "}".to_string(),
7081 close: true,
7082 surround: true,
7083 newline: true,
7084 },
7085 BracketPair {
7086 start: "/* ".to_string(),
7087 end: "*/".to_string(),
7088 close: true,
7089 surround: true,
7090 ..Default::default()
7091 },
7092 ],
7093 ..Default::default()
7094 },
7095 ..Default::default()
7096 },
7097 Some(tree_sitter_rust::LANGUAGE.into()),
7098 ));
7099
7100 let text = r#"
7101 a
7102 b
7103 c
7104 "#
7105 .unindent();
7106
7107 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7108 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7109 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7110 editor
7111 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7112 .await;
7113
7114 editor.update_in(cx, |editor, window, cx| {
7115 editor.change_selections(None, window, cx, |s| {
7116 s.select_display_ranges([
7117 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7118 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7119 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7120 ])
7121 });
7122
7123 editor.handle_input("{", window, cx);
7124 editor.handle_input("{", window, cx);
7125 editor.handle_input("{", window, cx);
7126 assert_eq!(
7127 editor.text(cx),
7128 "
7129 {{{a}}}
7130 {{{b}}}
7131 {{{c}}}
7132 "
7133 .unindent()
7134 );
7135 assert_eq!(
7136 editor.selections.display_ranges(cx),
7137 [
7138 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7139 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7140 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7141 ]
7142 );
7143
7144 editor.undo(&Undo, window, cx);
7145 editor.undo(&Undo, window, cx);
7146 editor.undo(&Undo, window, cx);
7147 assert_eq!(
7148 editor.text(cx),
7149 "
7150 a
7151 b
7152 c
7153 "
7154 .unindent()
7155 );
7156 assert_eq!(
7157 editor.selections.display_ranges(cx),
7158 [
7159 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7160 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7161 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7162 ]
7163 );
7164
7165 // Ensure inserting the first character of a multi-byte bracket pair
7166 // doesn't surround the selections with the bracket.
7167 editor.handle_input("/", window, cx);
7168 assert_eq!(
7169 editor.text(cx),
7170 "
7171 /
7172 /
7173 /
7174 "
7175 .unindent()
7176 );
7177 assert_eq!(
7178 editor.selections.display_ranges(cx),
7179 [
7180 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7181 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7182 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7183 ]
7184 );
7185
7186 editor.undo(&Undo, window, cx);
7187 assert_eq!(
7188 editor.text(cx),
7189 "
7190 a
7191 b
7192 c
7193 "
7194 .unindent()
7195 );
7196 assert_eq!(
7197 editor.selections.display_ranges(cx),
7198 [
7199 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7200 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7201 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7202 ]
7203 );
7204
7205 // Ensure inserting the last character of a multi-byte bracket pair
7206 // doesn't surround the selections with the bracket.
7207 editor.handle_input("*", window, cx);
7208 assert_eq!(
7209 editor.text(cx),
7210 "
7211 *
7212 *
7213 *
7214 "
7215 .unindent()
7216 );
7217 assert_eq!(
7218 editor.selections.display_ranges(cx),
7219 [
7220 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7221 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7222 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7223 ]
7224 );
7225 });
7226}
7227
7228#[gpui::test]
7229async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7230 init_test(cx, |_| {});
7231
7232 let language = Arc::new(Language::new(
7233 LanguageConfig {
7234 brackets: BracketPairConfig {
7235 pairs: vec![BracketPair {
7236 start: "{".to_string(),
7237 end: "}".to_string(),
7238 close: true,
7239 surround: true,
7240 newline: true,
7241 }],
7242 ..Default::default()
7243 },
7244 autoclose_before: "}".to_string(),
7245 ..Default::default()
7246 },
7247 Some(tree_sitter_rust::LANGUAGE.into()),
7248 ));
7249
7250 let text = r#"
7251 a
7252 b
7253 c
7254 "#
7255 .unindent();
7256
7257 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7258 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7259 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7260 editor
7261 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7262 .await;
7263
7264 editor.update_in(cx, |editor, window, cx| {
7265 editor.change_selections(None, window, cx, |s| {
7266 s.select_ranges([
7267 Point::new(0, 1)..Point::new(0, 1),
7268 Point::new(1, 1)..Point::new(1, 1),
7269 Point::new(2, 1)..Point::new(2, 1),
7270 ])
7271 });
7272
7273 editor.handle_input("{", window, cx);
7274 editor.handle_input("{", window, cx);
7275 editor.handle_input("_", window, cx);
7276 assert_eq!(
7277 editor.text(cx),
7278 "
7279 a{{_}}
7280 b{{_}}
7281 c{{_}}
7282 "
7283 .unindent()
7284 );
7285 assert_eq!(
7286 editor.selections.ranges::<Point>(cx),
7287 [
7288 Point::new(0, 4)..Point::new(0, 4),
7289 Point::new(1, 4)..Point::new(1, 4),
7290 Point::new(2, 4)..Point::new(2, 4)
7291 ]
7292 );
7293
7294 editor.backspace(&Default::default(), window, cx);
7295 editor.backspace(&Default::default(), window, cx);
7296 assert_eq!(
7297 editor.text(cx),
7298 "
7299 a{}
7300 b{}
7301 c{}
7302 "
7303 .unindent()
7304 );
7305 assert_eq!(
7306 editor.selections.ranges::<Point>(cx),
7307 [
7308 Point::new(0, 2)..Point::new(0, 2),
7309 Point::new(1, 2)..Point::new(1, 2),
7310 Point::new(2, 2)..Point::new(2, 2)
7311 ]
7312 );
7313
7314 editor.delete_to_previous_word_start(&Default::default(), window, cx);
7315 assert_eq!(
7316 editor.text(cx),
7317 "
7318 a
7319 b
7320 c
7321 "
7322 .unindent()
7323 );
7324 assert_eq!(
7325 editor.selections.ranges::<Point>(cx),
7326 [
7327 Point::new(0, 1)..Point::new(0, 1),
7328 Point::new(1, 1)..Point::new(1, 1),
7329 Point::new(2, 1)..Point::new(2, 1)
7330 ]
7331 );
7332 });
7333}
7334
7335#[gpui::test]
7336async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
7337 init_test(cx, |settings| {
7338 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7339 });
7340
7341 let mut cx = EditorTestContext::new(cx).await;
7342
7343 let language = Arc::new(Language::new(
7344 LanguageConfig {
7345 brackets: BracketPairConfig {
7346 pairs: vec![
7347 BracketPair {
7348 start: "{".to_string(),
7349 end: "}".to_string(),
7350 close: true,
7351 surround: true,
7352 newline: true,
7353 },
7354 BracketPair {
7355 start: "(".to_string(),
7356 end: ")".to_string(),
7357 close: true,
7358 surround: true,
7359 newline: true,
7360 },
7361 BracketPair {
7362 start: "[".to_string(),
7363 end: "]".to_string(),
7364 close: false,
7365 surround: true,
7366 newline: true,
7367 },
7368 ],
7369 ..Default::default()
7370 },
7371 autoclose_before: "})]".to_string(),
7372 ..Default::default()
7373 },
7374 Some(tree_sitter_rust::LANGUAGE.into()),
7375 ));
7376
7377 cx.language_registry().add(language.clone());
7378 cx.update_buffer(|buffer, cx| {
7379 buffer.set_language(Some(language), cx);
7380 });
7381
7382 cx.set_state(
7383 &"
7384 {(ˇ)}
7385 [[ˇ]]
7386 {(ˇ)}
7387 "
7388 .unindent(),
7389 );
7390
7391 cx.update_editor(|editor, window, cx| {
7392 editor.backspace(&Default::default(), window, cx);
7393 editor.backspace(&Default::default(), window, cx);
7394 });
7395
7396 cx.assert_editor_state(
7397 &"
7398 ˇ
7399 ˇ]]
7400 ˇ
7401 "
7402 .unindent(),
7403 );
7404
7405 cx.update_editor(|editor, window, cx| {
7406 editor.handle_input("{", window, cx);
7407 editor.handle_input("{", window, cx);
7408 editor.move_right(&MoveRight, window, cx);
7409 editor.move_right(&MoveRight, window, cx);
7410 editor.move_left(&MoveLeft, window, cx);
7411 editor.move_left(&MoveLeft, window, cx);
7412 editor.backspace(&Default::default(), window, cx);
7413 });
7414
7415 cx.assert_editor_state(
7416 &"
7417 {ˇ}
7418 {ˇ}]]
7419 {ˇ}
7420 "
7421 .unindent(),
7422 );
7423
7424 cx.update_editor(|editor, window, cx| {
7425 editor.backspace(&Default::default(), window, cx);
7426 });
7427
7428 cx.assert_editor_state(
7429 &"
7430 ˇ
7431 ˇ]]
7432 ˇ
7433 "
7434 .unindent(),
7435 );
7436}
7437
7438#[gpui::test]
7439async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
7440 init_test(cx, |_| {});
7441
7442 let language = Arc::new(Language::new(
7443 LanguageConfig::default(),
7444 Some(tree_sitter_rust::LANGUAGE.into()),
7445 ));
7446
7447 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
7448 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7449 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7450 editor
7451 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7452 .await;
7453
7454 editor.update_in(cx, |editor, window, cx| {
7455 editor.set_auto_replace_emoji_shortcode(true);
7456
7457 editor.handle_input("Hello ", window, cx);
7458 editor.handle_input(":wave", window, cx);
7459 assert_eq!(editor.text(cx), "Hello :wave".unindent());
7460
7461 editor.handle_input(":", window, cx);
7462 assert_eq!(editor.text(cx), "Hello 👋".unindent());
7463
7464 editor.handle_input(" :smile", window, cx);
7465 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
7466
7467 editor.handle_input(":", window, cx);
7468 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
7469
7470 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
7471 editor.handle_input(":wave", window, cx);
7472 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
7473
7474 editor.handle_input(":", window, cx);
7475 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
7476
7477 editor.handle_input(":1", window, cx);
7478 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
7479
7480 editor.handle_input(":", window, cx);
7481 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
7482
7483 // Ensure shortcode does not get replaced when it is part of a word
7484 editor.handle_input(" Test:wave", window, cx);
7485 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
7486
7487 editor.handle_input(":", window, cx);
7488 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
7489
7490 editor.set_auto_replace_emoji_shortcode(false);
7491
7492 // Ensure shortcode does not get replaced when auto replace is off
7493 editor.handle_input(" :wave", window, cx);
7494 assert_eq!(
7495 editor.text(cx),
7496 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
7497 );
7498
7499 editor.handle_input(":", window, cx);
7500 assert_eq!(
7501 editor.text(cx),
7502 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
7503 );
7504 });
7505}
7506
7507#[gpui::test]
7508async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
7509 init_test(cx, |_| {});
7510
7511 let (text, insertion_ranges) = marked_text_ranges(
7512 indoc! {"
7513 ˇ
7514 "},
7515 false,
7516 );
7517
7518 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7519 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7520
7521 _ = editor.update_in(cx, |editor, window, cx| {
7522 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7523
7524 editor
7525 .insert_snippet(&insertion_ranges, snippet, window, cx)
7526 .unwrap();
7527
7528 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7529 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7530 assert_eq!(editor.text(cx), expected_text);
7531 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7532 }
7533
7534 assert(
7535 editor,
7536 cx,
7537 indoc! {"
7538 type «» =•
7539 "},
7540 );
7541
7542 assert!(editor.context_menu_visible(), "There should be a matches");
7543 });
7544}
7545
7546#[gpui::test]
7547async fn test_snippets(cx: &mut TestAppContext) {
7548 init_test(cx, |_| {});
7549
7550 let (text, insertion_ranges) = marked_text_ranges(
7551 indoc! {"
7552 a.ˇ b
7553 a.ˇ b
7554 a.ˇ b
7555 "},
7556 false,
7557 );
7558
7559 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7560 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7561
7562 editor.update_in(cx, |editor, window, cx| {
7563 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
7564
7565 editor
7566 .insert_snippet(&insertion_ranges, snippet, window, cx)
7567 .unwrap();
7568
7569 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7570 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7571 assert_eq!(editor.text(cx), expected_text);
7572 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7573 }
7574
7575 assert(
7576 editor,
7577 cx,
7578 indoc! {"
7579 a.f(«one», two, «three») b
7580 a.f(«one», two, «three») b
7581 a.f(«one», two, «three») b
7582 "},
7583 );
7584
7585 // Can't move earlier than the first tab stop
7586 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7587 assert(
7588 editor,
7589 cx,
7590 indoc! {"
7591 a.f(«one», two, «three») b
7592 a.f(«one», two, «three») b
7593 a.f(«one», two, «three») b
7594 "},
7595 );
7596
7597 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7598 assert(
7599 editor,
7600 cx,
7601 indoc! {"
7602 a.f(one, «two», three) b
7603 a.f(one, «two», three) b
7604 a.f(one, «two», three) b
7605 "},
7606 );
7607
7608 editor.move_to_prev_snippet_tabstop(window, cx);
7609 assert(
7610 editor,
7611 cx,
7612 indoc! {"
7613 a.f(«one», two, «three») b
7614 a.f(«one», two, «three») b
7615 a.f(«one», two, «three») b
7616 "},
7617 );
7618
7619 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7620 assert(
7621 editor,
7622 cx,
7623 indoc! {"
7624 a.f(one, «two», three) b
7625 a.f(one, «two», three) b
7626 a.f(one, «two», three) b
7627 "},
7628 );
7629 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7630 assert(
7631 editor,
7632 cx,
7633 indoc! {"
7634 a.f(one, two, three)ˇ b
7635 a.f(one, two, three)ˇ b
7636 a.f(one, two, three)ˇ b
7637 "},
7638 );
7639
7640 // As soon as the last tab stop is reached, snippet state is gone
7641 editor.move_to_prev_snippet_tabstop(window, cx);
7642 assert(
7643 editor,
7644 cx,
7645 indoc! {"
7646 a.f(one, two, three)ˇ b
7647 a.f(one, two, three)ˇ b
7648 a.f(one, two, three)ˇ b
7649 "},
7650 );
7651 });
7652}
7653
7654#[gpui::test]
7655async fn test_document_format_during_save(cx: &mut TestAppContext) {
7656 init_test(cx, |_| {});
7657
7658 let fs = FakeFs::new(cx.executor());
7659 fs.insert_file(path!("/file.rs"), Default::default()).await;
7660
7661 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
7662
7663 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7664 language_registry.add(rust_lang());
7665 let mut fake_servers = language_registry.register_fake_lsp(
7666 "Rust",
7667 FakeLspAdapter {
7668 capabilities: lsp::ServerCapabilities {
7669 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7670 ..Default::default()
7671 },
7672 ..Default::default()
7673 },
7674 );
7675
7676 let buffer = project
7677 .update(cx, |project, cx| {
7678 project.open_local_buffer(path!("/file.rs"), cx)
7679 })
7680 .await
7681 .unwrap();
7682
7683 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7684 let (editor, cx) = cx.add_window_view(|window, cx| {
7685 build_editor_with_project(project.clone(), buffer, window, cx)
7686 });
7687 editor.update_in(cx, |editor, window, cx| {
7688 editor.set_text("one\ntwo\nthree\n", window, cx)
7689 });
7690 assert!(cx.read(|cx| editor.is_dirty(cx)));
7691
7692 cx.executor().start_waiting();
7693 let fake_server = fake_servers.next().await.unwrap();
7694
7695 let save = editor
7696 .update_in(cx, |editor, window, cx| {
7697 editor.save(true, project.clone(), window, cx)
7698 })
7699 .unwrap();
7700 fake_server
7701 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7702 assert_eq!(
7703 params.text_document.uri,
7704 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7705 );
7706 assert_eq!(params.options.tab_size, 4);
7707 Ok(Some(vec![lsp::TextEdit::new(
7708 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7709 ", ".to_string(),
7710 )]))
7711 })
7712 .next()
7713 .await;
7714 cx.executor().start_waiting();
7715 save.await;
7716
7717 assert_eq!(
7718 editor.update(cx, |editor, cx| editor.text(cx)),
7719 "one, two\nthree\n"
7720 );
7721 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7722
7723 editor.update_in(cx, |editor, window, cx| {
7724 editor.set_text("one\ntwo\nthree\n", window, cx)
7725 });
7726 assert!(cx.read(|cx| editor.is_dirty(cx)));
7727
7728 // Ensure we can still save even if formatting hangs.
7729 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7730 assert_eq!(
7731 params.text_document.uri,
7732 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7733 );
7734 futures::future::pending::<()>().await;
7735 unreachable!()
7736 });
7737 let save = editor
7738 .update_in(cx, |editor, window, cx| {
7739 editor.save(true, project.clone(), window, cx)
7740 })
7741 .unwrap();
7742 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7743 cx.executor().start_waiting();
7744 save.await;
7745 assert_eq!(
7746 editor.update(cx, |editor, cx| editor.text(cx)),
7747 "one\ntwo\nthree\n"
7748 );
7749 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7750
7751 // For non-dirty buffer, no formatting request should be sent
7752 let save = editor
7753 .update_in(cx, |editor, window, cx| {
7754 editor.save(true, project.clone(), window, cx)
7755 })
7756 .unwrap();
7757 let _pending_format_request = fake_server
7758 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7759 panic!("Should not be invoked on non-dirty buffer");
7760 })
7761 .next();
7762 cx.executor().start_waiting();
7763 save.await;
7764
7765 // Set rust language override and assert overridden tabsize is sent to language server
7766 update_test_language_settings(cx, |settings| {
7767 settings.languages.insert(
7768 "Rust".into(),
7769 LanguageSettingsContent {
7770 tab_size: NonZeroU32::new(8),
7771 ..Default::default()
7772 },
7773 );
7774 });
7775
7776 editor.update_in(cx, |editor, window, cx| {
7777 editor.set_text("somehting_new\n", window, cx)
7778 });
7779 assert!(cx.read(|cx| editor.is_dirty(cx)));
7780 let save = editor
7781 .update_in(cx, |editor, window, cx| {
7782 editor.save(true, project.clone(), window, cx)
7783 })
7784 .unwrap();
7785 fake_server
7786 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7787 assert_eq!(
7788 params.text_document.uri,
7789 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7790 );
7791 assert_eq!(params.options.tab_size, 8);
7792 Ok(Some(vec![]))
7793 })
7794 .next()
7795 .await;
7796 cx.executor().start_waiting();
7797 save.await;
7798}
7799
7800#[gpui::test]
7801async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
7802 init_test(cx, |_| {});
7803
7804 let cols = 4;
7805 let rows = 10;
7806 let sample_text_1 = sample_text(rows, cols, 'a');
7807 assert_eq!(
7808 sample_text_1,
7809 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7810 );
7811 let sample_text_2 = sample_text(rows, cols, 'l');
7812 assert_eq!(
7813 sample_text_2,
7814 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7815 );
7816 let sample_text_3 = sample_text(rows, cols, 'v');
7817 assert_eq!(
7818 sample_text_3,
7819 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7820 );
7821
7822 let fs = FakeFs::new(cx.executor());
7823 fs.insert_tree(
7824 path!("/a"),
7825 json!({
7826 "main.rs": sample_text_1,
7827 "other.rs": sample_text_2,
7828 "lib.rs": sample_text_3,
7829 }),
7830 )
7831 .await;
7832
7833 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
7834 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7835 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7836
7837 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7838 language_registry.add(rust_lang());
7839 let mut fake_servers = language_registry.register_fake_lsp(
7840 "Rust",
7841 FakeLspAdapter {
7842 capabilities: lsp::ServerCapabilities {
7843 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7844 ..Default::default()
7845 },
7846 ..Default::default()
7847 },
7848 );
7849
7850 let worktree = project.update(cx, |project, cx| {
7851 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7852 assert_eq!(worktrees.len(), 1);
7853 worktrees.pop().unwrap()
7854 });
7855 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
7856
7857 let buffer_1 = project
7858 .update(cx, |project, cx| {
7859 project.open_buffer((worktree_id, "main.rs"), cx)
7860 })
7861 .await
7862 .unwrap();
7863 let buffer_2 = project
7864 .update(cx, |project, cx| {
7865 project.open_buffer((worktree_id, "other.rs"), cx)
7866 })
7867 .await
7868 .unwrap();
7869 let buffer_3 = project
7870 .update(cx, |project, cx| {
7871 project.open_buffer((worktree_id, "lib.rs"), cx)
7872 })
7873 .await
7874 .unwrap();
7875
7876 let multi_buffer = cx.new(|cx| {
7877 let mut multi_buffer = MultiBuffer::new(ReadWrite);
7878 multi_buffer.push_excerpts(
7879 buffer_1.clone(),
7880 [
7881 ExcerptRange {
7882 context: Point::new(0, 0)..Point::new(3, 0),
7883 primary: None,
7884 },
7885 ExcerptRange {
7886 context: Point::new(5, 0)..Point::new(7, 0),
7887 primary: None,
7888 },
7889 ExcerptRange {
7890 context: Point::new(9, 0)..Point::new(10, 4),
7891 primary: None,
7892 },
7893 ],
7894 cx,
7895 );
7896 multi_buffer.push_excerpts(
7897 buffer_2.clone(),
7898 [
7899 ExcerptRange {
7900 context: Point::new(0, 0)..Point::new(3, 0),
7901 primary: None,
7902 },
7903 ExcerptRange {
7904 context: Point::new(5, 0)..Point::new(7, 0),
7905 primary: None,
7906 },
7907 ExcerptRange {
7908 context: Point::new(9, 0)..Point::new(10, 4),
7909 primary: None,
7910 },
7911 ],
7912 cx,
7913 );
7914 multi_buffer.push_excerpts(
7915 buffer_3.clone(),
7916 [
7917 ExcerptRange {
7918 context: Point::new(0, 0)..Point::new(3, 0),
7919 primary: None,
7920 },
7921 ExcerptRange {
7922 context: Point::new(5, 0)..Point::new(7, 0),
7923 primary: None,
7924 },
7925 ExcerptRange {
7926 context: Point::new(9, 0)..Point::new(10, 4),
7927 primary: None,
7928 },
7929 ],
7930 cx,
7931 );
7932 multi_buffer
7933 });
7934 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
7935 Editor::new(
7936 EditorMode::Full,
7937 multi_buffer,
7938 Some(project.clone()),
7939 window,
7940 cx,
7941 )
7942 });
7943
7944 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7945 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7946 s.select_ranges(Some(1..2))
7947 });
7948 editor.insert("|one|two|three|", window, cx);
7949 });
7950 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7951 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7952 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7953 s.select_ranges(Some(60..70))
7954 });
7955 editor.insert("|four|five|six|", window, cx);
7956 });
7957 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7958
7959 // First two buffers should be edited, but not the third one.
7960 assert_eq!(
7961 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7962 "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}",
7963 );
7964 buffer_1.update(cx, |buffer, _| {
7965 assert!(buffer.is_dirty());
7966 assert_eq!(
7967 buffer.text(),
7968 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
7969 )
7970 });
7971 buffer_2.update(cx, |buffer, _| {
7972 assert!(buffer.is_dirty());
7973 assert_eq!(
7974 buffer.text(),
7975 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
7976 )
7977 });
7978 buffer_3.update(cx, |buffer, _| {
7979 assert!(!buffer.is_dirty());
7980 assert_eq!(buffer.text(), sample_text_3,)
7981 });
7982 cx.executor().run_until_parked();
7983
7984 cx.executor().start_waiting();
7985 let save = multi_buffer_editor
7986 .update_in(cx, |editor, window, cx| {
7987 editor.save(true, project.clone(), window, cx)
7988 })
7989 .unwrap();
7990
7991 let fake_server = fake_servers.next().await.unwrap();
7992 fake_server
7993 .server
7994 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7995 Ok(Some(vec![lsp::TextEdit::new(
7996 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7997 format!("[{} formatted]", params.text_document.uri),
7998 )]))
7999 })
8000 .detach();
8001 save.await;
8002
8003 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8004 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8005 assert_eq!(
8006 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8007 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\n\nffff\ngggg\n\njjjj\n\nlll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|\nr\n\nuuuu\n\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}"),
8008 );
8009 buffer_1.update(cx, |buffer, _| {
8010 assert!(!buffer.is_dirty());
8011 assert_eq!(
8012 buffer.text(),
8013 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8014 )
8015 });
8016 buffer_2.update(cx, |buffer, _| {
8017 assert!(!buffer.is_dirty());
8018 assert_eq!(
8019 buffer.text(),
8020 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8021 )
8022 });
8023 buffer_3.update(cx, |buffer, _| {
8024 assert!(!buffer.is_dirty());
8025 assert_eq!(buffer.text(), sample_text_3,)
8026 });
8027}
8028
8029#[gpui::test]
8030async fn test_range_format_during_save(cx: &mut TestAppContext) {
8031 init_test(cx, |_| {});
8032
8033 let fs = FakeFs::new(cx.executor());
8034 fs.insert_file(path!("/file.rs"), Default::default()).await;
8035
8036 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8037
8038 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8039 language_registry.add(rust_lang());
8040 let mut fake_servers = language_registry.register_fake_lsp(
8041 "Rust",
8042 FakeLspAdapter {
8043 capabilities: lsp::ServerCapabilities {
8044 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8045 ..Default::default()
8046 },
8047 ..Default::default()
8048 },
8049 );
8050
8051 let buffer = project
8052 .update(cx, |project, cx| {
8053 project.open_local_buffer(path!("/file.rs"), cx)
8054 })
8055 .await
8056 .unwrap();
8057
8058 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8059 let (editor, cx) = cx.add_window_view(|window, cx| {
8060 build_editor_with_project(project.clone(), buffer, window, cx)
8061 });
8062 editor.update_in(cx, |editor, window, cx| {
8063 editor.set_text("one\ntwo\nthree\n", window, cx)
8064 });
8065 assert!(cx.read(|cx| editor.is_dirty(cx)));
8066
8067 cx.executor().start_waiting();
8068 let fake_server = fake_servers.next().await.unwrap();
8069
8070 let save = editor
8071 .update_in(cx, |editor, window, cx| {
8072 editor.save(true, project.clone(), window, cx)
8073 })
8074 .unwrap();
8075 fake_server
8076 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8077 assert_eq!(
8078 params.text_document.uri,
8079 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8080 );
8081 assert_eq!(params.options.tab_size, 4);
8082 Ok(Some(vec![lsp::TextEdit::new(
8083 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8084 ", ".to_string(),
8085 )]))
8086 })
8087 .next()
8088 .await;
8089 cx.executor().start_waiting();
8090 save.await;
8091 assert_eq!(
8092 editor.update(cx, |editor, cx| editor.text(cx)),
8093 "one, two\nthree\n"
8094 );
8095 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8096
8097 editor.update_in(cx, |editor, window, cx| {
8098 editor.set_text("one\ntwo\nthree\n", window, cx)
8099 });
8100 assert!(cx.read(|cx| editor.is_dirty(cx)));
8101
8102 // Ensure we can still save even if formatting hangs.
8103 fake_server.handle_request::<lsp::request::RangeFormatting, _, _>(
8104 move |params, _| async move {
8105 assert_eq!(
8106 params.text_document.uri,
8107 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8108 );
8109 futures::future::pending::<()>().await;
8110 unreachable!()
8111 },
8112 );
8113 let save = editor
8114 .update_in(cx, |editor, window, cx| {
8115 editor.save(true, project.clone(), window, cx)
8116 })
8117 .unwrap();
8118 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8119 cx.executor().start_waiting();
8120 save.await;
8121 assert_eq!(
8122 editor.update(cx, |editor, cx| editor.text(cx)),
8123 "one\ntwo\nthree\n"
8124 );
8125 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8126
8127 // For non-dirty buffer, no formatting request should be sent
8128 let save = editor
8129 .update_in(cx, |editor, window, cx| {
8130 editor.save(true, project.clone(), window, cx)
8131 })
8132 .unwrap();
8133 let _pending_format_request = fake_server
8134 .handle_request::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8135 panic!("Should not be invoked on non-dirty buffer");
8136 })
8137 .next();
8138 cx.executor().start_waiting();
8139 save.await;
8140
8141 // Set Rust language override and assert overridden tabsize is sent to language server
8142 update_test_language_settings(cx, |settings| {
8143 settings.languages.insert(
8144 "Rust".into(),
8145 LanguageSettingsContent {
8146 tab_size: NonZeroU32::new(8),
8147 ..Default::default()
8148 },
8149 );
8150 });
8151
8152 editor.update_in(cx, |editor, window, cx| {
8153 editor.set_text("somehting_new\n", window, cx)
8154 });
8155 assert!(cx.read(|cx| editor.is_dirty(cx)));
8156 let save = editor
8157 .update_in(cx, |editor, window, cx| {
8158 editor.save(true, project.clone(), window, cx)
8159 })
8160 .unwrap();
8161 fake_server
8162 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8163 assert_eq!(
8164 params.text_document.uri,
8165 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8166 );
8167 assert_eq!(params.options.tab_size, 8);
8168 Ok(Some(vec![]))
8169 })
8170 .next()
8171 .await;
8172 cx.executor().start_waiting();
8173 save.await;
8174}
8175
8176#[gpui::test]
8177async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8178 init_test(cx, |settings| {
8179 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8180 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8181 ))
8182 });
8183
8184 let fs = FakeFs::new(cx.executor());
8185 fs.insert_file(path!("/file.rs"), Default::default()).await;
8186
8187 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8188
8189 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8190 language_registry.add(Arc::new(Language::new(
8191 LanguageConfig {
8192 name: "Rust".into(),
8193 matcher: LanguageMatcher {
8194 path_suffixes: vec!["rs".to_string()],
8195 ..Default::default()
8196 },
8197 ..LanguageConfig::default()
8198 },
8199 Some(tree_sitter_rust::LANGUAGE.into()),
8200 )));
8201 update_test_language_settings(cx, |settings| {
8202 // Enable Prettier formatting for the same buffer, and ensure
8203 // LSP is called instead of Prettier.
8204 settings.defaults.prettier = Some(PrettierSettings {
8205 allowed: true,
8206 ..PrettierSettings::default()
8207 });
8208 });
8209 let mut fake_servers = language_registry.register_fake_lsp(
8210 "Rust",
8211 FakeLspAdapter {
8212 capabilities: lsp::ServerCapabilities {
8213 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8214 ..Default::default()
8215 },
8216 ..Default::default()
8217 },
8218 );
8219
8220 let buffer = project
8221 .update(cx, |project, cx| {
8222 project.open_local_buffer(path!("/file.rs"), cx)
8223 })
8224 .await
8225 .unwrap();
8226
8227 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8228 let (editor, cx) = cx.add_window_view(|window, cx| {
8229 build_editor_with_project(project.clone(), buffer, window, cx)
8230 });
8231 editor.update_in(cx, |editor, window, cx| {
8232 editor.set_text("one\ntwo\nthree\n", window, cx)
8233 });
8234
8235 cx.executor().start_waiting();
8236 let fake_server = fake_servers.next().await.unwrap();
8237
8238 let format = editor
8239 .update_in(cx, |editor, window, cx| {
8240 editor.perform_format(
8241 project.clone(),
8242 FormatTrigger::Manual,
8243 FormatTarget::Buffers,
8244 window,
8245 cx,
8246 )
8247 })
8248 .unwrap();
8249 fake_server
8250 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8251 assert_eq!(
8252 params.text_document.uri,
8253 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8254 );
8255 assert_eq!(params.options.tab_size, 4);
8256 Ok(Some(vec![lsp::TextEdit::new(
8257 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8258 ", ".to_string(),
8259 )]))
8260 })
8261 .next()
8262 .await;
8263 cx.executor().start_waiting();
8264 format.await;
8265 assert_eq!(
8266 editor.update(cx, |editor, cx| editor.text(cx)),
8267 "one, two\nthree\n"
8268 );
8269
8270 editor.update_in(cx, |editor, window, cx| {
8271 editor.set_text("one\ntwo\nthree\n", window, cx)
8272 });
8273 // Ensure we don't lock if formatting hangs.
8274 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8275 assert_eq!(
8276 params.text_document.uri,
8277 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8278 );
8279 futures::future::pending::<()>().await;
8280 unreachable!()
8281 });
8282 let format = editor
8283 .update_in(cx, |editor, window, cx| {
8284 editor.perform_format(
8285 project,
8286 FormatTrigger::Manual,
8287 FormatTarget::Buffers,
8288 window,
8289 cx,
8290 )
8291 })
8292 .unwrap();
8293 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8294 cx.executor().start_waiting();
8295 format.await;
8296 assert_eq!(
8297 editor.update(cx, |editor, cx| editor.text(cx)),
8298 "one\ntwo\nthree\n"
8299 );
8300}
8301
8302#[gpui::test]
8303async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
8304 init_test(cx, |settings| {
8305 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8306 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8307 ))
8308 });
8309
8310 let fs = FakeFs::new(cx.executor());
8311 fs.insert_file(path!("/file.ts"), Default::default()).await;
8312
8313 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8314
8315 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8316 language_registry.add(Arc::new(Language::new(
8317 LanguageConfig {
8318 name: "TypeScript".into(),
8319 matcher: LanguageMatcher {
8320 path_suffixes: vec!["ts".to_string()],
8321 ..Default::default()
8322 },
8323 ..LanguageConfig::default()
8324 },
8325 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8326 )));
8327 update_test_language_settings(cx, |settings| {
8328 settings.defaults.prettier = Some(PrettierSettings {
8329 allowed: true,
8330 ..PrettierSettings::default()
8331 });
8332 });
8333 let mut fake_servers = language_registry.register_fake_lsp(
8334 "TypeScript",
8335 FakeLspAdapter {
8336 capabilities: lsp::ServerCapabilities {
8337 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8338 ..Default::default()
8339 },
8340 ..Default::default()
8341 },
8342 );
8343
8344 let buffer = project
8345 .update(cx, |project, cx| {
8346 project.open_local_buffer(path!("/file.ts"), cx)
8347 })
8348 .await
8349 .unwrap();
8350
8351 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8352 let (editor, cx) = cx.add_window_view(|window, cx| {
8353 build_editor_with_project(project.clone(), buffer, window, cx)
8354 });
8355 editor.update_in(cx, |editor, window, cx| {
8356 editor.set_text(
8357 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8358 window,
8359 cx,
8360 )
8361 });
8362
8363 cx.executor().start_waiting();
8364 let fake_server = fake_servers.next().await.unwrap();
8365
8366 let format = editor
8367 .update_in(cx, |editor, window, cx| {
8368 editor.perform_code_action_kind(
8369 project.clone(),
8370 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8371 window,
8372 cx,
8373 )
8374 })
8375 .unwrap();
8376 fake_server
8377 .handle_request::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
8378 assert_eq!(
8379 params.text_document.uri,
8380 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8381 );
8382 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
8383 lsp::CodeAction {
8384 title: "Organize Imports".to_string(),
8385 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
8386 edit: Some(lsp::WorkspaceEdit {
8387 changes: Some(
8388 [(
8389 params.text_document.uri.clone(),
8390 vec![lsp::TextEdit::new(
8391 lsp::Range::new(
8392 lsp::Position::new(1, 0),
8393 lsp::Position::new(2, 0),
8394 ),
8395 "".to_string(),
8396 )],
8397 )]
8398 .into_iter()
8399 .collect(),
8400 ),
8401 ..Default::default()
8402 }),
8403 ..Default::default()
8404 },
8405 )]))
8406 })
8407 .next()
8408 .await;
8409 cx.executor().start_waiting();
8410 format.await;
8411 assert_eq!(
8412 editor.update(cx, |editor, cx| editor.text(cx)),
8413 "import { a } from 'module';\n\nconst x = a;\n"
8414 );
8415
8416 editor.update_in(cx, |editor, window, cx| {
8417 editor.set_text(
8418 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8419 window,
8420 cx,
8421 )
8422 });
8423 // Ensure we don't lock if code action hangs.
8424 fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
8425 move |params, _| async move {
8426 assert_eq!(
8427 params.text_document.uri,
8428 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8429 );
8430 futures::future::pending::<()>().await;
8431 unreachable!()
8432 },
8433 );
8434 let format = editor
8435 .update_in(cx, |editor, window, cx| {
8436 editor.perform_code_action_kind(
8437 project,
8438 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8439 window,
8440 cx,
8441 )
8442 })
8443 .unwrap();
8444 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
8445 cx.executor().start_waiting();
8446 format.await;
8447 assert_eq!(
8448 editor.update(cx, |editor, cx| editor.text(cx)),
8449 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
8450 );
8451}
8452
8453#[gpui::test]
8454async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
8455 init_test(cx, |_| {});
8456
8457 let mut cx = EditorLspTestContext::new_rust(
8458 lsp::ServerCapabilities {
8459 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8460 ..Default::default()
8461 },
8462 cx,
8463 )
8464 .await;
8465
8466 cx.set_state(indoc! {"
8467 one.twoˇ
8468 "});
8469
8470 // The format request takes a long time. When it completes, it inserts
8471 // a newline and an indent before the `.`
8472 cx.lsp
8473 .handle_request::<lsp::request::Formatting, _, _>(move |_, cx| {
8474 let executor = cx.background_executor().clone();
8475 async move {
8476 executor.timer(Duration::from_millis(100)).await;
8477 Ok(Some(vec![lsp::TextEdit {
8478 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
8479 new_text: "\n ".into(),
8480 }]))
8481 }
8482 });
8483
8484 // Submit a format request.
8485 let format_1 = cx
8486 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8487 .unwrap();
8488 cx.executor().run_until_parked();
8489
8490 // Submit a second format request.
8491 let format_2 = cx
8492 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8493 .unwrap();
8494 cx.executor().run_until_parked();
8495
8496 // Wait for both format requests to complete
8497 cx.executor().advance_clock(Duration::from_millis(200));
8498 cx.executor().start_waiting();
8499 format_1.await.unwrap();
8500 cx.executor().start_waiting();
8501 format_2.await.unwrap();
8502
8503 // The formatting edits only happens once.
8504 cx.assert_editor_state(indoc! {"
8505 one
8506 .twoˇ
8507 "});
8508}
8509
8510#[gpui::test]
8511async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
8512 init_test(cx, |settings| {
8513 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
8514 });
8515
8516 let mut cx = EditorLspTestContext::new_rust(
8517 lsp::ServerCapabilities {
8518 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8519 ..Default::default()
8520 },
8521 cx,
8522 )
8523 .await;
8524
8525 // Set up a buffer white some trailing whitespace and no trailing newline.
8526 cx.set_state(
8527 &[
8528 "one ", //
8529 "twoˇ", //
8530 "three ", //
8531 "four", //
8532 ]
8533 .join("\n"),
8534 );
8535
8536 // Submit a format request.
8537 let format = cx
8538 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8539 .unwrap();
8540
8541 // Record which buffer changes have been sent to the language server
8542 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
8543 cx.lsp
8544 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
8545 let buffer_changes = buffer_changes.clone();
8546 move |params, _| {
8547 buffer_changes.lock().extend(
8548 params
8549 .content_changes
8550 .into_iter()
8551 .map(|e| (e.range.unwrap(), e.text)),
8552 );
8553 }
8554 });
8555
8556 // Handle formatting requests to the language server.
8557 cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
8558 let buffer_changes = buffer_changes.clone();
8559 move |_, _| {
8560 // When formatting is requested, trailing whitespace has already been stripped,
8561 // and the trailing newline has already been added.
8562 assert_eq!(
8563 &buffer_changes.lock()[1..],
8564 &[
8565 (
8566 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
8567 "".into()
8568 ),
8569 (
8570 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
8571 "".into()
8572 ),
8573 (
8574 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
8575 "\n".into()
8576 ),
8577 ]
8578 );
8579
8580 // Insert blank lines between each line of the buffer.
8581 async move {
8582 Ok(Some(vec![
8583 lsp::TextEdit {
8584 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
8585 new_text: "\n".into(),
8586 },
8587 lsp::TextEdit {
8588 range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
8589 new_text: "\n".into(),
8590 },
8591 ]))
8592 }
8593 }
8594 });
8595
8596 // After formatting the buffer, the trailing whitespace is stripped,
8597 // a newline is appended, and the edits provided by the language server
8598 // have been applied.
8599 format.await.unwrap();
8600 cx.assert_editor_state(
8601 &[
8602 "one", //
8603 "", //
8604 "twoˇ", //
8605 "", //
8606 "three", //
8607 "four", //
8608 "", //
8609 ]
8610 .join("\n"),
8611 );
8612
8613 // Undoing the formatting undoes the trailing whitespace removal, the
8614 // trailing newline, and the LSP edits.
8615 cx.update_buffer(|buffer, cx| buffer.undo(cx));
8616 cx.assert_editor_state(
8617 &[
8618 "one ", //
8619 "twoˇ", //
8620 "three ", //
8621 "four", //
8622 ]
8623 .join("\n"),
8624 );
8625}
8626
8627#[gpui::test]
8628async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
8629 cx: &mut TestAppContext,
8630) {
8631 init_test(cx, |_| {});
8632
8633 cx.update(|cx| {
8634 cx.update_global::<SettingsStore, _>(|settings, cx| {
8635 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8636 settings.auto_signature_help = Some(true);
8637 });
8638 });
8639 });
8640
8641 let mut cx = EditorLspTestContext::new_rust(
8642 lsp::ServerCapabilities {
8643 signature_help_provider: Some(lsp::SignatureHelpOptions {
8644 ..Default::default()
8645 }),
8646 ..Default::default()
8647 },
8648 cx,
8649 )
8650 .await;
8651
8652 let language = Language::new(
8653 LanguageConfig {
8654 name: "Rust".into(),
8655 brackets: BracketPairConfig {
8656 pairs: vec![
8657 BracketPair {
8658 start: "{".to_string(),
8659 end: "}".to_string(),
8660 close: true,
8661 surround: true,
8662 newline: true,
8663 },
8664 BracketPair {
8665 start: "(".to_string(),
8666 end: ")".to_string(),
8667 close: true,
8668 surround: true,
8669 newline: true,
8670 },
8671 BracketPair {
8672 start: "/*".to_string(),
8673 end: " */".to_string(),
8674 close: true,
8675 surround: true,
8676 newline: true,
8677 },
8678 BracketPair {
8679 start: "[".to_string(),
8680 end: "]".to_string(),
8681 close: false,
8682 surround: false,
8683 newline: true,
8684 },
8685 BracketPair {
8686 start: "\"".to_string(),
8687 end: "\"".to_string(),
8688 close: true,
8689 surround: true,
8690 newline: false,
8691 },
8692 BracketPair {
8693 start: "<".to_string(),
8694 end: ">".to_string(),
8695 close: false,
8696 surround: true,
8697 newline: true,
8698 },
8699 ],
8700 ..Default::default()
8701 },
8702 autoclose_before: "})]".to_string(),
8703 ..Default::default()
8704 },
8705 Some(tree_sitter_rust::LANGUAGE.into()),
8706 );
8707 let language = Arc::new(language);
8708
8709 cx.language_registry().add(language.clone());
8710 cx.update_buffer(|buffer, cx| {
8711 buffer.set_language(Some(language), cx);
8712 });
8713
8714 cx.set_state(
8715 &r#"
8716 fn main() {
8717 sampleˇ
8718 }
8719 "#
8720 .unindent(),
8721 );
8722
8723 cx.update_editor(|editor, window, cx| {
8724 editor.handle_input("(", window, cx);
8725 });
8726 cx.assert_editor_state(
8727 &"
8728 fn main() {
8729 sample(ˇ)
8730 }
8731 "
8732 .unindent(),
8733 );
8734
8735 let mocked_response = lsp::SignatureHelp {
8736 signatures: vec![lsp::SignatureInformation {
8737 label: "fn sample(param1: u8, param2: u8)".to_string(),
8738 documentation: None,
8739 parameters: Some(vec![
8740 lsp::ParameterInformation {
8741 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8742 documentation: None,
8743 },
8744 lsp::ParameterInformation {
8745 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8746 documentation: None,
8747 },
8748 ]),
8749 active_parameter: None,
8750 }],
8751 active_signature: Some(0),
8752 active_parameter: Some(0),
8753 };
8754 handle_signature_help_request(&mut cx, mocked_response).await;
8755
8756 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8757 .await;
8758
8759 cx.editor(|editor, _, _| {
8760 let signature_help_state = editor.signature_help_state.popover().cloned();
8761 assert_eq!(
8762 signature_help_state.unwrap().label,
8763 "param1: u8, param2: u8"
8764 );
8765 });
8766}
8767
8768#[gpui::test]
8769async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
8770 init_test(cx, |_| {});
8771
8772 cx.update(|cx| {
8773 cx.update_global::<SettingsStore, _>(|settings, cx| {
8774 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8775 settings.auto_signature_help = Some(false);
8776 settings.show_signature_help_after_edits = Some(false);
8777 });
8778 });
8779 });
8780
8781 let mut cx = EditorLspTestContext::new_rust(
8782 lsp::ServerCapabilities {
8783 signature_help_provider: Some(lsp::SignatureHelpOptions {
8784 ..Default::default()
8785 }),
8786 ..Default::default()
8787 },
8788 cx,
8789 )
8790 .await;
8791
8792 let language = Language::new(
8793 LanguageConfig {
8794 name: "Rust".into(),
8795 brackets: BracketPairConfig {
8796 pairs: vec![
8797 BracketPair {
8798 start: "{".to_string(),
8799 end: "}".to_string(),
8800 close: true,
8801 surround: true,
8802 newline: true,
8803 },
8804 BracketPair {
8805 start: "(".to_string(),
8806 end: ")".to_string(),
8807 close: true,
8808 surround: true,
8809 newline: true,
8810 },
8811 BracketPair {
8812 start: "/*".to_string(),
8813 end: " */".to_string(),
8814 close: true,
8815 surround: true,
8816 newline: true,
8817 },
8818 BracketPair {
8819 start: "[".to_string(),
8820 end: "]".to_string(),
8821 close: false,
8822 surround: false,
8823 newline: true,
8824 },
8825 BracketPair {
8826 start: "\"".to_string(),
8827 end: "\"".to_string(),
8828 close: true,
8829 surround: true,
8830 newline: false,
8831 },
8832 BracketPair {
8833 start: "<".to_string(),
8834 end: ">".to_string(),
8835 close: false,
8836 surround: true,
8837 newline: true,
8838 },
8839 ],
8840 ..Default::default()
8841 },
8842 autoclose_before: "})]".to_string(),
8843 ..Default::default()
8844 },
8845 Some(tree_sitter_rust::LANGUAGE.into()),
8846 );
8847 let language = Arc::new(language);
8848
8849 cx.language_registry().add(language.clone());
8850 cx.update_buffer(|buffer, cx| {
8851 buffer.set_language(Some(language), cx);
8852 });
8853
8854 // Ensure that signature_help is not called when no signature help is enabled.
8855 cx.set_state(
8856 &r#"
8857 fn main() {
8858 sampleˇ
8859 }
8860 "#
8861 .unindent(),
8862 );
8863 cx.update_editor(|editor, window, cx| {
8864 editor.handle_input("(", window, cx);
8865 });
8866 cx.assert_editor_state(
8867 &"
8868 fn main() {
8869 sample(ˇ)
8870 }
8871 "
8872 .unindent(),
8873 );
8874 cx.editor(|editor, _, _| {
8875 assert!(editor.signature_help_state.task().is_none());
8876 });
8877
8878 let mocked_response = lsp::SignatureHelp {
8879 signatures: vec![lsp::SignatureInformation {
8880 label: "fn sample(param1: u8, param2: u8)".to_string(),
8881 documentation: None,
8882 parameters: Some(vec![
8883 lsp::ParameterInformation {
8884 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8885 documentation: None,
8886 },
8887 lsp::ParameterInformation {
8888 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8889 documentation: None,
8890 },
8891 ]),
8892 active_parameter: None,
8893 }],
8894 active_signature: Some(0),
8895 active_parameter: Some(0),
8896 };
8897
8898 // Ensure that signature_help is called when enabled afte edits
8899 cx.update(|_, cx| {
8900 cx.update_global::<SettingsStore, _>(|settings, cx| {
8901 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8902 settings.auto_signature_help = Some(false);
8903 settings.show_signature_help_after_edits = Some(true);
8904 });
8905 });
8906 });
8907 cx.set_state(
8908 &r#"
8909 fn main() {
8910 sampleˇ
8911 }
8912 "#
8913 .unindent(),
8914 );
8915 cx.update_editor(|editor, window, cx| {
8916 editor.handle_input("(", window, cx);
8917 });
8918 cx.assert_editor_state(
8919 &"
8920 fn main() {
8921 sample(ˇ)
8922 }
8923 "
8924 .unindent(),
8925 );
8926 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8927 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8928 .await;
8929 cx.update_editor(|editor, _, _| {
8930 let signature_help_state = editor.signature_help_state.popover().cloned();
8931 assert!(signature_help_state.is_some());
8932 assert_eq!(
8933 signature_help_state.unwrap().label,
8934 "param1: u8, param2: u8"
8935 );
8936 editor.signature_help_state = SignatureHelpState::default();
8937 });
8938
8939 // Ensure that signature_help is called when auto signature help override is enabled
8940 cx.update(|_, cx| {
8941 cx.update_global::<SettingsStore, _>(|settings, cx| {
8942 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8943 settings.auto_signature_help = Some(true);
8944 settings.show_signature_help_after_edits = Some(false);
8945 });
8946 });
8947 });
8948 cx.set_state(
8949 &r#"
8950 fn main() {
8951 sampleˇ
8952 }
8953 "#
8954 .unindent(),
8955 );
8956 cx.update_editor(|editor, window, cx| {
8957 editor.handle_input("(", window, cx);
8958 });
8959 cx.assert_editor_state(
8960 &"
8961 fn main() {
8962 sample(ˇ)
8963 }
8964 "
8965 .unindent(),
8966 );
8967 handle_signature_help_request(&mut cx, mocked_response).await;
8968 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8969 .await;
8970 cx.editor(|editor, _, _| {
8971 let signature_help_state = editor.signature_help_state.popover().cloned();
8972 assert!(signature_help_state.is_some());
8973 assert_eq!(
8974 signature_help_state.unwrap().label,
8975 "param1: u8, param2: u8"
8976 );
8977 });
8978}
8979
8980#[gpui::test]
8981async fn test_signature_help(cx: &mut TestAppContext) {
8982 init_test(cx, |_| {});
8983 cx.update(|cx| {
8984 cx.update_global::<SettingsStore, _>(|settings, cx| {
8985 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8986 settings.auto_signature_help = Some(true);
8987 });
8988 });
8989 });
8990
8991 let mut cx = EditorLspTestContext::new_rust(
8992 lsp::ServerCapabilities {
8993 signature_help_provider: Some(lsp::SignatureHelpOptions {
8994 ..Default::default()
8995 }),
8996 ..Default::default()
8997 },
8998 cx,
8999 )
9000 .await;
9001
9002 // A test that directly calls `show_signature_help`
9003 cx.update_editor(|editor, window, cx| {
9004 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9005 });
9006
9007 let mocked_response = lsp::SignatureHelp {
9008 signatures: vec![lsp::SignatureInformation {
9009 label: "fn sample(param1: u8, param2: u8)".to_string(),
9010 documentation: None,
9011 parameters: Some(vec![
9012 lsp::ParameterInformation {
9013 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9014 documentation: None,
9015 },
9016 lsp::ParameterInformation {
9017 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9018 documentation: None,
9019 },
9020 ]),
9021 active_parameter: None,
9022 }],
9023 active_signature: Some(0),
9024 active_parameter: Some(0),
9025 };
9026 handle_signature_help_request(&mut cx, mocked_response).await;
9027
9028 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9029 .await;
9030
9031 cx.editor(|editor, _, _| {
9032 let signature_help_state = editor.signature_help_state.popover().cloned();
9033 assert!(signature_help_state.is_some());
9034 assert_eq!(
9035 signature_help_state.unwrap().label,
9036 "param1: u8, param2: u8"
9037 );
9038 });
9039
9040 // When exiting outside from inside the brackets, `signature_help` is closed.
9041 cx.set_state(indoc! {"
9042 fn main() {
9043 sample(ˇ);
9044 }
9045
9046 fn sample(param1: u8, param2: u8) {}
9047 "});
9048
9049 cx.update_editor(|editor, window, cx| {
9050 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
9051 });
9052
9053 let mocked_response = lsp::SignatureHelp {
9054 signatures: Vec::new(),
9055 active_signature: None,
9056 active_parameter: None,
9057 };
9058 handle_signature_help_request(&mut cx, mocked_response).await;
9059
9060 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9061 .await;
9062
9063 cx.editor(|editor, _, _| {
9064 assert!(!editor.signature_help_state.is_shown());
9065 });
9066
9067 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
9068 cx.set_state(indoc! {"
9069 fn main() {
9070 sample(ˇ);
9071 }
9072
9073 fn sample(param1: u8, param2: u8) {}
9074 "});
9075
9076 let mocked_response = lsp::SignatureHelp {
9077 signatures: vec![lsp::SignatureInformation {
9078 label: "fn sample(param1: u8, param2: u8)".to_string(),
9079 documentation: None,
9080 parameters: Some(vec![
9081 lsp::ParameterInformation {
9082 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9083 documentation: None,
9084 },
9085 lsp::ParameterInformation {
9086 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9087 documentation: None,
9088 },
9089 ]),
9090 active_parameter: None,
9091 }],
9092 active_signature: Some(0),
9093 active_parameter: Some(0),
9094 };
9095 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9096 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9097 .await;
9098 cx.editor(|editor, _, _| {
9099 assert!(editor.signature_help_state.is_shown());
9100 });
9101
9102 // Restore the popover with more parameter input
9103 cx.set_state(indoc! {"
9104 fn main() {
9105 sample(param1, param2ˇ);
9106 }
9107
9108 fn sample(param1: u8, param2: u8) {}
9109 "});
9110
9111 let mocked_response = lsp::SignatureHelp {
9112 signatures: vec![lsp::SignatureInformation {
9113 label: "fn sample(param1: u8, param2: u8)".to_string(),
9114 documentation: None,
9115 parameters: Some(vec![
9116 lsp::ParameterInformation {
9117 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9118 documentation: None,
9119 },
9120 lsp::ParameterInformation {
9121 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9122 documentation: None,
9123 },
9124 ]),
9125 active_parameter: None,
9126 }],
9127 active_signature: Some(0),
9128 active_parameter: Some(1),
9129 };
9130 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9131 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9132 .await;
9133
9134 // When selecting a range, the popover is gone.
9135 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
9136 cx.update_editor(|editor, window, cx| {
9137 editor.change_selections(None, window, cx, |s| {
9138 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9139 })
9140 });
9141 cx.assert_editor_state(indoc! {"
9142 fn main() {
9143 sample(param1, «ˇparam2»);
9144 }
9145
9146 fn sample(param1: u8, param2: u8) {}
9147 "});
9148 cx.editor(|editor, _, _| {
9149 assert!(!editor.signature_help_state.is_shown());
9150 });
9151
9152 // When unselecting again, the popover is back if within the brackets.
9153 cx.update_editor(|editor, window, cx| {
9154 editor.change_selections(None, window, cx, |s| {
9155 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9156 })
9157 });
9158 cx.assert_editor_state(indoc! {"
9159 fn main() {
9160 sample(param1, ˇparam2);
9161 }
9162
9163 fn sample(param1: u8, param2: u8) {}
9164 "});
9165 handle_signature_help_request(&mut cx, mocked_response).await;
9166 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9167 .await;
9168 cx.editor(|editor, _, _| {
9169 assert!(editor.signature_help_state.is_shown());
9170 });
9171
9172 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
9173 cx.update_editor(|editor, window, cx| {
9174 editor.change_selections(None, window, cx, |s| {
9175 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
9176 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9177 })
9178 });
9179 cx.assert_editor_state(indoc! {"
9180 fn main() {
9181 sample(param1, ˇparam2);
9182 }
9183
9184 fn sample(param1: u8, param2: u8) {}
9185 "});
9186
9187 let mocked_response = lsp::SignatureHelp {
9188 signatures: vec![lsp::SignatureInformation {
9189 label: "fn sample(param1: u8, param2: u8)".to_string(),
9190 documentation: None,
9191 parameters: Some(vec![
9192 lsp::ParameterInformation {
9193 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9194 documentation: None,
9195 },
9196 lsp::ParameterInformation {
9197 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9198 documentation: None,
9199 },
9200 ]),
9201 active_parameter: None,
9202 }],
9203 active_signature: Some(0),
9204 active_parameter: Some(1),
9205 };
9206 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9207 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9208 .await;
9209 cx.update_editor(|editor, _, cx| {
9210 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
9211 });
9212 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9213 .await;
9214 cx.update_editor(|editor, window, cx| {
9215 editor.change_selections(None, window, cx, |s| {
9216 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9217 })
9218 });
9219 cx.assert_editor_state(indoc! {"
9220 fn main() {
9221 sample(param1, «ˇparam2»);
9222 }
9223
9224 fn sample(param1: u8, param2: u8) {}
9225 "});
9226 cx.update_editor(|editor, window, cx| {
9227 editor.change_selections(None, window, cx, |s| {
9228 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9229 })
9230 });
9231 cx.assert_editor_state(indoc! {"
9232 fn main() {
9233 sample(param1, ˇparam2);
9234 }
9235
9236 fn sample(param1: u8, param2: u8) {}
9237 "});
9238 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
9239 .await;
9240}
9241
9242#[gpui::test]
9243async fn test_completion(cx: &mut TestAppContext) {
9244 init_test(cx, |_| {});
9245
9246 let mut cx = EditorLspTestContext::new_rust(
9247 lsp::ServerCapabilities {
9248 completion_provider: Some(lsp::CompletionOptions {
9249 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9250 resolve_provider: Some(true),
9251 ..Default::default()
9252 }),
9253 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9254 ..Default::default()
9255 },
9256 cx,
9257 )
9258 .await;
9259 let counter = Arc::new(AtomicUsize::new(0));
9260
9261 cx.set_state(indoc! {"
9262 oneˇ
9263 two
9264 three
9265 "});
9266 cx.simulate_keystroke(".");
9267 handle_completion_request(
9268 &mut cx,
9269 indoc! {"
9270 one.|<>
9271 two
9272 three
9273 "},
9274 vec!["first_completion", "second_completion"],
9275 counter.clone(),
9276 )
9277 .await;
9278 cx.condition(|editor, _| editor.context_menu_visible())
9279 .await;
9280 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9281
9282 let _handler = handle_signature_help_request(
9283 &mut cx,
9284 lsp::SignatureHelp {
9285 signatures: vec![lsp::SignatureInformation {
9286 label: "test signature".to_string(),
9287 documentation: None,
9288 parameters: Some(vec![lsp::ParameterInformation {
9289 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
9290 documentation: None,
9291 }]),
9292 active_parameter: None,
9293 }],
9294 active_signature: None,
9295 active_parameter: None,
9296 },
9297 );
9298 cx.update_editor(|editor, window, cx| {
9299 assert!(
9300 !editor.signature_help_state.is_shown(),
9301 "No signature help was called for"
9302 );
9303 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9304 });
9305 cx.run_until_parked();
9306 cx.update_editor(|editor, _, _| {
9307 assert!(
9308 !editor.signature_help_state.is_shown(),
9309 "No signature help should be shown when completions menu is open"
9310 );
9311 });
9312
9313 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9314 editor.context_menu_next(&Default::default(), window, cx);
9315 editor
9316 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9317 .unwrap()
9318 });
9319 cx.assert_editor_state(indoc! {"
9320 one.second_completionˇ
9321 two
9322 three
9323 "});
9324
9325 handle_resolve_completion_request(
9326 &mut cx,
9327 Some(vec![
9328 (
9329 //This overlaps with the primary completion edit which is
9330 //misbehavior from the LSP spec, test that we filter it out
9331 indoc! {"
9332 one.second_ˇcompletion
9333 two
9334 threeˇ
9335 "},
9336 "overlapping additional edit",
9337 ),
9338 (
9339 indoc! {"
9340 one.second_completion
9341 two
9342 threeˇ
9343 "},
9344 "\nadditional edit",
9345 ),
9346 ]),
9347 )
9348 .await;
9349 apply_additional_edits.await.unwrap();
9350 cx.assert_editor_state(indoc! {"
9351 one.second_completionˇ
9352 two
9353 three
9354 additional edit
9355 "});
9356
9357 cx.set_state(indoc! {"
9358 one.second_completion
9359 twoˇ
9360 threeˇ
9361 additional edit
9362 "});
9363 cx.simulate_keystroke(" ");
9364 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9365 cx.simulate_keystroke("s");
9366 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9367
9368 cx.assert_editor_state(indoc! {"
9369 one.second_completion
9370 two sˇ
9371 three sˇ
9372 additional edit
9373 "});
9374 handle_completion_request(
9375 &mut cx,
9376 indoc! {"
9377 one.second_completion
9378 two s
9379 three <s|>
9380 additional edit
9381 "},
9382 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
9383 counter.clone(),
9384 )
9385 .await;
9386 cx.condition(|editor, _| editor.context_menu_visible())
9387 .await;
9388 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
9389
9390 cx.simulate_keystroke("i");
9391
9392 handle_completion_request(
9393 &mut cx,
9394 indoc! {"
9395 one.second_completion
9396 two si
9397 three <si|>
9398 additional edit
9399 "},
9400 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
9401 counter.clone(),
9402 )
9403 .await;
9404 cx.condition(|editor, _| editor.context_menu_visible())
9405 .await;
9406 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
9407
9408 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9409 editor
9410 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9411 .unwrap()
9412 });
9413 cx.assert_editor_state(indoc! {"
9414 one.second_completion
9415 two sixth_completionˇ
9416 three sixth_completionˇ
9417 additional edit
9418 "});
9419
9420 apply_additional_edits.await.unwrap();
9421
9422 update_test_language_settings(&mut cx, |settings| {
9423 settings.defaults.show_completions_on_input = Some(false);
9424 });
9425 cx.set_state("editorˇ");
9426 cx.simulate_keystroke(".");
9427 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9428 cx.simulate_keystroke("c");
9429 cx.simulate_keystroke("l");
9430 cx.simulate_keystroke("o");
9431 cx.assert_editor_state("editor.cloˇ");
9432 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9433 cx.update_editor(|editor, window, cx| {
9434 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9435 });
9436 handle_completion_request(
9437 &mut cx,
9438 "editor.<clo|>",
9439 vec!["close", "clobber"],
9440 counter.clone(),
9441 )
9442 .await;
9443 cx.condition(|editor, _| editor.context_menu_visible())
9444 .await;
9445 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
9446
9447 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9448 editor
9449 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9450 .unwrap()
9451 });
9452 cx.assert_editor_state("editor.closeˇ");
9453 handle_resolve_completion_request(&mut cx, None).await;
9454 apply_additional_edits.await.unwrap();
9455}
9456
9457#[gpui::test]
9458async fn test_word_completion(cx: &mut TestAppContext) {
9459 let lsp_fetch_timeout_ms = 10;
9460 init_test(cx, |language_settings| {
9461 language_settings.defaults.completions = Some(CompletionSettings {
9462 words: WordsCompletionMode::Fallback,
9463 lsp: true,
9464 lsp_fetch_timeout_ms: 10,
9465 });
9466 });
9467
9468 let mut cx = EditorLspTestContext::new_rust(
9469 lsp::ServerCapabilities {
9470 completion_provider: Some(lsp::CompletionOptions {
9471 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9472 ..lsp::CompletionOptions::default()
9473 }),
9474 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9475 ..lsp::ServerCapabilities::default()
9476 },
9477 cx,
9478 )
9479 .await;
9480
9481 let throttle_completions = Arc::new(AtomicBool::new(false));
9482
9483 let lsp_throttle_completions = throttle_completions.clone();
9484 let _completion_requests_handler =
9485 cx.lsp
9486 .server
9487 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
9488 let lsp_throttle_completions = lsp_throttle_completions.clone();
9489 let cx = cx.clone();
9490 async move {
9491 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
9492 cx.background_executor()
9493 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
9494 .await;
9495 }
9496 Ok(Some(lsp::CompletionResponse::Array(vec![
9497 lsp::CompletionItem {
9498 label: "first".into(),
9499 ..lsp::CompletionItem::default()
9500 },
9501 lsp::CompletionItem {
9502 label: "last".into(),
9503 ..lsp::CompletionItem::default()
9504 },
9505 ])))
9506 }
9507 });
9508
9509 cx.set_state(indoc! {"
9510 oneˇ
9511 two
9512 three
9513 "});
9514 cx.simulate_keystroke(".");
9515 cx.executor().run_until_parked();
9516 cx.condition(|editor, _| editor.context_menu_visible())
9517 .await;
9518 cx.update_editor(|editor, window, cx| {
9519 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9520 {
9521 assert_eq!(
9522 completion_menu_entries(&menu),
9523 &["first", "last"],
9524 "When LSP server is fast to reply, no fallback word completions are used"
9525 );
9526 } else {
9527 panic!("expected completion menu to be open");
9528 }
9529 editor.cancel(&Cancel, window, cx);
9530 });
9531 cx.executor().run_until_parked();
9532 cx.condition(|editor, _| !editor.context_menu_visible())
9533 .await;
9534
9535 throttle_completions.store(true, atomic::Ordering::Release);
9536 cx.simulate_keystroke(".");
9537 cx.executor()
9538 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
9539 cx.executor().run_until_parked();
9540 cx.condition(|editor, _| editor.context_menu_visible())
9541 .await;
9542 cx.update_editor(|editor, _, _| {
9543 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9544 {
9545 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
9546 "When LSP server is slow, document words can be shown instead, if configured accordingly");
9547 } else {
9548 panic!("expected completion menu to be open");
9549 }
9550 });
9551}
9552
9553#[gpui::test]
9554async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
9555 init_test(cx, |language_settings| {
9556 language_settings.defaults.completions = Some(CompletionSettings {
9557 words: WordsCompletionMode::Enabled,
9558 lsp: true,
9559 lsp_fetch_timeout_ms: 0,
9560 });
9561 });
9562
9563 let mut cx = EditorLspTestContext::new_rust(
9564 lsp::ServerCapabilities {
9565 completion_provider: Some(lsp::CompletionOptions {
9566 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9567 ..lsp::CompletionOptions::default()
9568 }),
9569 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9570 ..lsp::ServerCapabilities::default()
9571 },
9572 cx,
9573 )
9574 .await;
9575
9576 let _completion_requests_handler =
9577 cx.lsp
9578 .server
9579 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9580 Ok(Some(lsp::CompletionResponse::Array(vec![
9581 lsp::CompletionItem {
9582 label: "first".into(),
9583 ..lsp::CompletionItem::default()
9584 },
9585 lsp::CompletionItem {
9586 label: "last".into(),
9587 ..lsp::CompletionItem::default()
9588 },
9589 ])))
9590 });
9591
9592 cx.set_state(indoc! {"ˇ
9593 first
9594 last
9595 second
9596 "});
9597 cx.simulate_keystroke(".");
9598 cx.executor().run_until_parked();
9599 cx.condition(|editor, _| editor.context_menu_visible())
9600 .await;
9601 cx.update_editor(|editor, _, _| {
9602 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9603 {
9604 assert_eq!(
9605 completion_menu_entries(&menu),
9606 &["first", "last", "second"],
9607 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
9608 );
9609 } else {
9610 panic!("expected completion menu to be open");
9611 }
9612 });
9613}
9614
9615#[gpui::test]
9616async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
9617 init_test(cx, |language_settings| {
9618 language_settings.defaults.completions = Some(CompletionSettings {
9619 words: WordsCompletionMode::Disabled,
9620 lsp: true,
9621 lsp_fetch_timeout_ms: 0,
9622 });
9623 });
9624
9625 let mut cx = EditorLspTestContext::new_rust(
9626 lsp::ServerCapabilities {
9627 completion_provider: Some(lsp::CompletionOptions {
9628 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9629 ..lsp::CompletionOptions::default()
9630 }),
9631 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9632 ..lsp::ServerCapabilities::default()
9633 },
9634 cx,
9635 )
9636 .await;
9637
9638 let _completion_requests_handler =
9639 cx.lsp
9640 .server
9641 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9642 panic!("LSP completions should not be queried when dealing with word completions")
9643 });
9644
9645 cx.set_state(indoc! {"ˇ
9646 first
9647 last
9648 second
9649 "});
9650 cx.update_editor(|editor, window, cx| {
9651 editor.show_word_completions(&ShowWordCompletions, window, cx);
9652 });
9653 cx.executor().run_until_parked();
9654 cx.condition(|editor, _| editor.context_menu_visible())
9655 .await;
9656 cx.update_editor(|editor, _, _| {
9657 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9658 {
9659 assert_eq!(
9660 completion_menu_entries(&menu),
9661 &["first", "last", "second"],
9662 "`ShowWordCompletions` action should show word completions"
9663 );
9664 } else {
9665 panic!("expected completion menu to be open");
9666 }
9667 });
9668
9669 cx.simulate_keystroke("l");
9670 cx.executor().run_until_parked();
9671 cx.condition(|editor, _| editor.context_menu_visible())
9672 .await;
9673 cx.update_editor(|editor, _, _| {
9674 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9675 {
9676 assert_eq!(
9677 completion_menu_entries(&menu),
9678 &["last"],
9679 "After showing word completions, further editing should filter them and not query the LSP"
9680 );
9681 } else {
9682 panic!("expected completion menu to be open");
9683 }
9684 });
9685}
9686
9687#[gpui::test]
9688async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
9689 init_test(cx, |language_settings| {
9690 language_settings.defaults.completions = Some(CompletionSettings {
9691 words: WordsCompletionMode::Fallback,
9692 lsp: false,
9693 lsp_fetch_timeout_ms: 0,
9694 });
9695 });
9696
9697 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
9698
9699 cx.set_state(indoc! {"ˇ
9700 0_usize
9701 let
9702 33
9703 4.5f32
9704 "});
9705 cx.update_editor(|editor, window, cx| {
9706 editor.show_completions(&ShowCompletions::default(), window, cx);
9707 });
9708 cx.executor().run_until_parked();
9709 cx.condition(|editor, _| editor.context_menu_visible())
9710 .await;
9711 cx.update_editor(|editor, window, cx| {
9712 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9713 {
9714 assert_eq!(
9715 completion_menu_entries(&menu),
9716 &["let"],
9717 "With no digits in the completion query, no digits should be in the word completions"
9718 );
9719 } else {
9720 panic!("expected completion menu to be open");
9721 }
9722 editor.cancel(&Cancel, window, cx);
9723 });
9724
9725 cx.set_state(indoc! {"3ˇ
9726 0_usize
9727 let
9728 3
9729 33.35f32
9730 "});
9731 cx.update_editor(|editor, window, cx| {
9732 editor.show_completions(&ShowCompletions::default(), window, cx);
9733 });
9734 cx.executor().run_until_parked();
9735 cx.condition(|editor, _| editor.context_menu_visible())
9736 .await;
9737 cx.update_editor(|editor, _, _| {
9738 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9739 {
9740 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
9741 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
9742 } else {
9743 panic!("expected completion menu to be open");
9744 }
9745 });
9746}
9747
9748#[gpui::test]
9749async fn test_multiline_completion(cx: &mut TestAppContext) {
9750 init_test(cx, |_| {});
9751
9752 let fs = FakeFs::new(cx.executor());
9753 fs.insert_tree(
9754 path!("/a"),
9755 json!({
9756 "main.ts": "a",
9757 }),
9758 )
9759 .await;
9760
9761 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9762 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9763 let typescript_language = Arc::new(Language::new(
9764 LanguageConfig {
9765 name: "TypeScript".into(),
9766 matcher: LanguageMatcher {
9767 path_suffixes: vec!["ts".to_string()],
9768 ..LanguageMatcher::default()
9769 },
9770 line_comments: vec!["// ".into()],
9771 ..LanguageConfig::default()
9772 },
9773 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9774 ));
9775 language_registry.add(typescript_language.clone());
9776 let mut fake_servers = language_registry.register_fake_lsp(
9777 "TypeScript",
9778 FakeLspAdapter {
9779 capabilities: lsp::ServerCapabilities {
9780 completion_provider: Some(lsp::CompletionOptions {
9781 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9782 ..lsp::CompletionOptions::default()
9783 }),
9784 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9785 ..lsp::ServerCapabilities::default()
9786 },
9787 // Emulate vtsls label generation
9788 label_for_completion: Some(Box::new(|item, _| {
9789 let text = if let Some(description) = item
9790 .label_details
9791 .as_ref()
9792 .and_then(|label_details| label_details.description.as_ref())
9793 {
9794 format!("{} {}", item.label, description)
9795 } else if let Some(detail) = &item.detail {
9796 format!("{} {}", item.label, detail)
9797 } else {
9798 item.label.clone()
9799 };
9800 let len = text.len();
9801 Some(language::CodeLabel {
9802 text,
9803 runs: Vec::new(),
9804 filter_range: 0..len,
9805 })
9806 })),
9807 ..FakeLspAdapter::default()
9808 },
9809 );
9810 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9811 let cx = &mut VisualTestContext::from_window(*workspace, cx);
9812 let worktree_id = workspace
9813 .update(cx, |workspace, _window, cx| {
9814 workspace.project().update(cx, |project, cx| {
9815 project.worktrees(cx).next().unwrap().read(cx).id()
9816 })
9817 })
9818 .unwrap();
9819 let _buffer = project
9820 .update(cx, |project, cx| {
9821 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
9822 })
9823 .await
9824 .unwrap();
9825 let editor = workspace
9826 .update(cx, |workspace, window, cx| {
9827 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
9828 })
9829 .unwrap()
9830 .await
9831 .unwrap()
9832 .downcast::<Editor>()
9833 .unwrap();
9834 let fake_server = fake_servers.next().await.unwrap();
9835
9836 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
9837 let multiline_label_2 = "a\nb\nc\n";
9838 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
9839 let multiline_description = "d\ne\nf\n";
9840 let multiline_detail_2 = "g\nh\ni\n";
9841
9842 let mut completion_handle =
9843 fake_server.handle_request::<lsp::request::Completion, _, _>(move |params, _| async move {
9844 Ok(Some(lsp::CompletionResponse::Array(vec![
9845 lsp::CompletionItem {
9846 label: multiline_label.to_string(),
9847 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9848 range: lsp::Range {
9849 start: lsp::Position {
9850 line: params.text_document_position.position.line,
9851 character: params.text_document_position.position.character,
9852 },
9853 end: lsp::Position {
9854 line: params.text_document_position.position.line,
9855 character: params.text_document_position.position.character,
9856 },
9857 },
9858 new_text: "new_text_1".to_string(),
9859 })),
9860 ..lsp::CompletionItem::default()
9861 },
9862 lsp::CompletionItem {
9863 label: "single line label 1".to_string(),
9864 detail: Some(multiline_detail.to_string()),
9865 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9866 range: lsp::Range {
9867 start: lsp::Position {
9868 line: params.text_document_position.position.line,
9869 character: params.text_document_position.position.character,
9870 },
9871 end: lsp::Position {
9872 line: params.text_document_position.position.line,
9873 character: params.text_document_position.position.character,
9874 },
9875 },
9876 new_text: "new_text_2".to_string(),
9877 })),
9878 ..lsp::CompletionItem::default()
9879 },
9880 lsp::CompletionItem {
9881 label: "single line label 2".to_string(),
9882 label_details: Some(lsp::CompletionItemLabelDetails {
9883 description: Some(multiline_description.to_string()),
9884 detail: None,
9885 }),
9886 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9887 range: lsp::Range {
9888 start: lsp::Position {
9889 line: params.text_document_position.position.line,
9890 character: params.text_document_position.position.character,
9891 },
9892 end: lsp::Position {
9893 line: params.text_document_position.position.line,
9894 character: params.text_document_position.position.character,
9895 },
9896 },
9897 new_text: "new_text_2".to_string(),
9898 })),
9899 ..lsp::CompletionItem::default()
9900 },
9901 lsp::CompletionItem {
9902 label: multiline_label_2.to_string(),
9903 detail: Some(multiline_detail_2.to_string()),
9904 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9905 range: lsp::Range {
9906 start: lsp::Position {
9907 line: params.text_document_position.position.line,
9908 character: params.text_document_position.position.character,
9909 },
9910 end: lsp::Position {
9911 line: params.text_document_position.position.line,
9912 character: params.text_document_position.position.character,
9913 },
9914 },
9915 new_text: "new_text_3".to_string(),
9916 })),
9917 ..lsp::CompletionItem::default()
9918 },
9919 lsp::CompletionItem {
9920 label: "Label with many spaces and \t but without newlines".to_string(),
9921 detail: Some(
9922 "Details with many spaces and \t but without newlines".to_string(),
9923 ),
9924 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9925 range: lsp::Range {
9926 start: lsp::Position {
9927 line: params.text_document_position.position.line,
9928 character: params.text_document_position.position.character,
9929 },
9930 end: lsp::Position {
9931 line: params.text_document_position.position.line,
9932 character: params.text_document_position.position.character,
9933 },
9934 },
9935 new_text: "new_text_4".to_string(),
9936 })),
9937 ..lsp::CompletionItem::default()
9938 },
9939 ])))
9940 });
9941
9942 editor.update_in(cx, |editor, window, cx| {
9943 cx.focus_self(window);
9944 editor.move_to_end(&MoveToEnd, window, cx);
9945 editor.handle_input(".", window, cx);
9946 });
9947 cx.run_until_parked();
9948 completion_handle.next().await.unwrap();
9949
9950 editor.update(cx, |editor, _| {
9951 assert!(editor.context_menu_visible());
9952 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9953 {
9954 let completion_labels = menu
9955 .completions
9956 .borrow()
9957 .iter()
9958 .map(|c| c.label.text.clone())
9959 .collect::<Vec<_>>();
9960 assert_eq!(
9961 completion_labels,
9962 &[
9963 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
9964 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
9965 "single line label 2 d e f ",
9966 "a b c g h i ",
9967 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
9968 ],
9969 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
9970 );
9971
9972 for completion in menu
9973 .completions
9974 .borrow()
9975 .iter() {
9976 assert_eq!(
9977 completion.label.filter_range,
9978 0..completion.label.text.len(),
9979 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
9980 );
9981 }
9982
9983 } else {
9984 panic!("expected completion menu to be open");
9985 }
9986 });
9987}
9988
9989#[gpui::test]
9990async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
9991 init_test(cx, |_| {});
9992 let mut cx = EditorLspTestContext::new_rust(
9993 lsp::ServerCapabilities {
9994 completion_provider: Some(lsp::CompletionOptions {
9995 trigger_characters: Some(vec![".".to_string()]),
9996 ..Default::default()
9997 }),
9998 ..Default::default()
9999 },
10000 cx,
10001 )
10002 .await;
10003 cx.lsp
10004 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
10005 Ok(Some(lsp::CompletionResponse::Array(vec![
10006 lsp::CompletionItem {
10007 label: "first".into(),
10008 ..Default::default()
10009 },
10010 lsp::CompletionItem {
10011 label: "last".into(),
10012 ..Default::default()
10013 },
10014 ])))
10015 });
10016 cx.set_state("variableˇ");
10017 cx.simulate_keystroke(".");
10018 cx.executor().run_until_parked();
10019
10020 cx.update_editor(|editor, _, _| {
10021 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10022 {
10023 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
10024 } else {
10025 panic!("expected completion menu to be open");
10026 }
10027 });
10028
10029 cx.update_editor(|editor, window, cx| {
10030 editor.move_page_down(&MovePageDown::default(), window, cx);
10031 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10032 {
10033 assert!(
10034 menu.selected_item == 1,
10035 "expected PageDown to select the last item from the context menu"
10036 );
10037 } else {
10038 panic!("expected completion menu to stay open after PageDown");
10039 }
10040 });
10041
10042 cx.update_editor(|editor, window, cx| {
10043 editor.move_page_up(&MovePageUp::default(), window, cx);
10044 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10045 {
10046 assert!(
10047 menu.selected_item == 0,
10048 "expected PageUp to select the first item from the context menu"
10049 );
10050 } else {
10051 panic!("expected completion menu to stay open after PageUp");
10052 }
10053 });
10054}
10055
10056#[gpui::test]
10057async fn test_completion_sort(cx: &mut TestAppContext) {
10058 init_test(cx, |_| {});
10059 let mut cx = EditorLspTestContext::new_rust(
10060 lsp::ServerCapabilities {
10061 completion_provider: Some(lsp::CompletionOptions {
10062 trigger_characters: Some(vec![".".to_string()]),
10063 ..Default::default()
10064 }),
10065 ..Default::default()
10066 },
10067 cx,
10068 )
10069 .await;
10070 cx.lsp
10071 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
10072 Ok(Some(lsp::CompletionResponse::Array(vec![
10073 lsp::CompletionItem {
10074 label: "Range".into(),
10075 sort_text: Some("a".into()),
10076 ..Default::default()
10077 },
10078 lsp::CompletionItem {
10079 label: "r".into(),
10080 sort_text: Some("b".into()),
10081 ..Default::default()
10082 },
10083 lsp::CompletionItem {
10084 label: "ret".into(),
10085 sort_text: Some("c".into()),
10086 ..Default::default()
10087 },
10088 lsp::CompletionItem {
10089 label: "return".into(),
10090 sort_text: Some("d".into()),
10091 ..Default::default()
10092 },
10093 lsp::CompletionItem {
10094 label: "slice".into(),
10095 sort_text: Some("d".into()),
10096 ..Default::default()
10097 },
10098 ])))
10099 });
10100 cx.set_state("rˇ");
10101 cx.executor().run_until_parked();
10102 cx.update_editor(|editor, window, cx| {
10103 editor.show_completions(
10104 &ShowCompletions {
10105 trigger: Some("r".into()),
10106 },
10107 window,
10108 cx,
10109 );
10110 });
10111 cx.executor().run_until_parked();
10112
10113 cx.update_editor(|editor, _, _| {
10114 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10115 {
10116 assert_eq!(
10117 completion_menu_entries(&menu),
10118 &["r", "ret", "Range", "return"]
10119 );
10120 } else {
10121 panic!("expected completion menu to be open");
10122 }
10123 });
10124}
10125
10126#[gpui::test]
10127async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
10128 init_test(cx, |_| {});
10129
10130 let mut cx = EditorLspTestContext::new_rust(
10131 lsp::ServerCapabilities {
10132 completion_provider: Some(lsp::CompletionOptions {
10133 trigger_characters: Some(vec![".".to_string()]),
10134 resolve_provider: Some(true),
10135 ..Default::default()
10136 }),
10137 ..Default::default()
10138 },
10139 cx,
10140 )
10141 .await;
10142
10143 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
10144 cx.simulate_keystroke(".");
10145 let completion_item = lsp::CompletionItem {
10146 label: "Some".into(),
10147 kind: Some(lsp::CompletionItemKind::SNIPPET),
10148 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
10149 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
10150 kind: lsp::MarkupKind::Markdown,
10151 value: "```rust\nSome(2)\n```".to_string(),
10152 })),
10153 deprecated: Some(false),
10154 sort_text: Some("Some".to_string()),
10155 filter_text: Some("Some".to_string()),
10156 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
10157 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10158 range: lsp::Range {
10159 start: lsp::Position {
10160 line: 0,
10161 character: 22,
10162 },
10163 end: lsp::Position {
10164 line: 0,
10165 character: 22,
10166 },
10167 },
10168 new_text: "Some(2)".to_string(),
10169 })),
10170 additional_text_edits: Some(vec![lsp::TextEdit {
10171 range: lsp::Range {
10172 start: lsp::Position {
10173 line: 0,
10174 character: 20,
10175 },
10176 end: lsp::Position {
10177 line: 0,
10178 character: 22,
10179 },
10180 },
10181 new_text: "".to_string(),
10182 }]),
10183 ..Default::default()
10184 };
10185
10186 let closure_completion_item = completion_item.clone();
10187 let counter = Arc::new(AtomicUsize::new(0));
10188 let counter_clone = counter.clone();
10189 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
10190 let task_completion_item = closure_completion_item.clone();
10191 counter_clone.fetch_add(1, atomic::Ordering::Release);
10192 async move {
10193 Ok(Some(lsp::CompletionResponse::Array(vec![
10194 task_completion_item,
10195 ])))
10196 }
10197 });
10198
10199 cx.condition(|editor, _| editor.context_menu_visible())
10200 .await;
10201 cx.assert_editor_state(indoc! {"fn main() { let a = 2.ˇ; }"});
10202 assert!(request.next().await.is_some());
10203 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10204
10205 cx.simulate_keystroke("S");
10206 cx.simulate_keystroke("o");
10207 cx.simulate_keystroke("m");
10208 cx.condition(|editor, _| editor.context_menu_visible())
10209 .await;
10210 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Somˇ; }"});
10211 assert!(request.next().await.is_some());
10212 assert!(request.next().await.is_some());
10213 assert!(request.next().await.is_some());
10214 request.close();
10215 assert!(request.next().await.is_none());
10216 assert_eq!(
10217 counter.load(atomic::Ordering::Acquire),
10218 4,
10219 "With the completions menu open, only one LSP request should happen per input"
10220 );
10221}
10222
10223#[gpui::test]
10224async fn test_toggle_comment(cx: &mut TestAppContext) {
10225 init_test(cx, |_| {});
10226 let mut cx = EditorTestContext::new(cx).await;
10227 let language = Arc::new(Language::new(
10228 LanguageConfig {
10229 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10230 ..Default::default()
10231 },
10232 Some(tree_sitter_rust::LANGUAGE.into()),
10233 ));
10234 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10235
10236 // If multiple selections intersect a line, the line is only toggled once.
10237 cx.set_state(indoc! {"
10238 fn a() {
10239 «//b();
10240 ˇ»// «c();
10241 //ˇ» d();
10242 }
10243 "});
10244
10245 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10246
10247 cx.assert_editor_state(indoc! {"
10248 fn a() {
10249 «b();
10250 c();
10251 ˇ» d();
10252 }
10253 "});
10254
10255 // The comment prefix is inserted at the same column for every line in a
10256 // selection.
10257 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10258
10259 cx.assert_editor_state(indoc! {"
10260 fn a() {
10261 // «b();
10262 // c();
10263 ˇ»// d();
10264 }
10265 "});
10266
10267 // If a selection ends at the beginning of a line, that line is not toggled.
10268 cx.set_selections_state(indoc! {"
10269 fn a() {
10270 // b();
10271 «// c();
10272 ˇ» // d();
10273 }
10274 "});
10275
10276 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10277
10278 cx.assert_editor_state(indoc! {"
10279 fn a() {
10280 // b();
10281 «c();
10282 ˇ» // d();
10283 }
10284 "});
10285
10286 // If a selection span a single line and is empty, the line is toggled.
10287 cx.set_state(indoc! {"
10288 fn a() {
10289 a();
10290 b();
10291 ˇ
10292 }
10293 "});
10294
10295 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10296
10297 cx.assert_editor_state(indoc! {"
10298 fn a() {
10299 a();
10300 b();
10301 //•ˇ
10302 }
10303 "});
10304
10305 // If a selection span multiple lines, empty lines are not toggled.
10306 cx.set_state(indoc! {"
10307 fn a() {
10308 «a();
10309
10310 c();ˇ»
10311 }
10312 "});
10313
10314 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10315
10316 cx.assert_editor_state(indoc! {"
10317 fn a() {
10318 // «a();
10319
10320 // c();ˇ»
10321 }
10322 "});
10323
10324 // If a selection includes multiple comment prefixes, all lines are uncommented.
10325 cx.set_state(indoc! {"
10326 fn a() {
10327 «// a();
10328 /// b();
10329 //! c();ˇ»
10330 }
10331 "});
10332
10333 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10334
10335 cx.assert_editor_state(indoc! {"
10336 fn a() {
10337 «a();
10338 b();
10339 c();ˇ»
10340 }
10341 "});
10342}
10343
10344#[gpui::test]
10345async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
10346 init_test(cx, |_| {});
10347 let mut cx = EditorTestContext::new(cx).await;
10348 let language = Arc::new(Language::new(
10349 LanguageConfig {
10350 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10351 ..Default::default()
10352 },
10353 Some(tree_sitter_rust::LANGUAGE.into()),
10354 ));
10355 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10356
10357 let toggle_comments = &ToggleComments {
10358 advance_downwards: false,
10359 ignore_indent: true,
10360 };
10361
10362 // If multiple selections intersect a line, the line is only toggled once.
10363 cx.set_state(indoc! {"
10364 fn a() {
10365 // «b();
10366 // c();
10367 // ˇ» d();
10368 }
10369 "});
10370
10371 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10372
10373 cx.assert_editor_state(indoc! {"
10374 fn a() {
10375 «b();
10376 c();
10377 ˇ» d();
10378 }
10379 "});
10380
10381 // The comment prefix is inserted at the beginning of each line
10382 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10383
10384 cx.assert_editor_state(indoc! {"
10385 fn a() {
10386 // «b();
10387 // c();
10388 // ˇ» d();
10389 }
10390 "});
10391
10392 // If a selection ends at the beginning of a line, that line is not toggled.
10393 cx.set_selections_state(indoc! {"
10394 fn a() {
10395 // b();
10396 // «c();
10397 ˇ»// d();
10398 }
10399 "});
10400
10401 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10402
10403 cx.assert_editor_state(indoc! {"
10404 fn a() {
10405 // b();
10406 «c();
10407 ˇ»// d();
10408 }
10409 "});
10410
10411 // If a selection span a single line and is empty, the line is toggled.
10412 cx.set_state(indoc! {"
10413 fn a() {
10414 a();
10415 b();
10416 ˇ
10417 }
10418 "});
10419
10420 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10421
10422 cx.assert_editor_state(indoc! {"
10423 fn a() {
10424 a();
10425 b();
10426 //ˇ
10427 }
10428 "});
10429
10430 // If a selection span multiple lines, empty lines are not toggled.
10431 cx.set_state(indoc! {"
10432 fn a() {
10433 «a();
10434
10435 c();ˇ»
10436 }
10437 "});
10438
10439 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10440
10441 cx.assert_editor_state(indoc! {"
10442 fn a() {
10443 // «a();
10444
10445 // c();ˇ»
10446 }
10447 "});
10448
10449 // If a selection includes multiple comment prefixes, all lines are uncommented.
10450 cx.set_state(indoc! {"
10451 fn a() {
10452 // «a();
10453 /// b();
10454 //! c();ˇ»
10455 }
10456 "});
10457
10458 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10459
10460 cx.assert_editor_state(indoc! {"
10461 fn a() {
10462 «a();
10463 b();
10464 c();ˇ»
10465 }
10466 "});
10467}
10468
10469#[gpui::test]
10470async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
10471 init_test(cx, |_| {});
10472
10473 let language = Arc::new(Language::new(
10474 LanguageConfig {
10475 line_comments: vec!["// ".into()],
10476 ..Default::default()
10477 },
10478 Some(tree_sitter_rust::LANGUAGE.into()),
10479 ));
10480
10481 let mut cx = EditorTestContext::new(cx).await;
10482
10483 cx.language_registry().add(language.clone());
10484 cx.update_buffer(|buffer, cx| {
10485 buffer.set_language(Some(language), cx);
10486 });
10487
10488 let toggle_comments = &ToggleComments {
10489 advance_downwards: true,
10490 ignore_indent: false,
10491 };
10492
10493 // Single cursor on one line -> advance
10494 // Cursor moves horizontally 3 characters as well on non-blank line
10495 cx.set_state(indoc!(
10496 "fn a() {
10497 ˇdog();
10498 cat();
10499 }"
10500 ));
10501 cx.update_editor(|editor, window, cx| {
10502 editor.toggle_comments(toggle_comments, window, cx);
10503 });
10504 cx.assert_editor_state(indoc!(
10505 "fn a() {
10506 // dog();
10507 catˇ();
10508 }"
10509 ));
10510
10511 // Single selection on one line -> don't advance
10512 cx.set_state(indoc!(
10513 "fn a() {
10514 «dog()ˇ»;
10515 cat();
10516 }"
10517 ));
10518 cx.update_editor(|editor, window, cx| {
10519 editor.toggle_comments(toggle_comments, window, cx);
10520 });
10521 cx.assert_editor_state(indoc!(
10522 "fn a() {
10523 // «dog()ˇ»;
10524 cat();
10525 }"
10526 ));
10527
10528 // Multiple cursors on one line -> advance
10529 cx.set_state(indoc!(
10530 "fn a() {
10531 ˇdˇog();
10532 cat();
10533 }"
10534 ));
10535 cx.update_editor(|editor, window, cx| {
10536 editor.toggle_comments(toggle_comments, window, cx);
10537 });
10538 cx.assert_editor_state(indoc!(
10539 "fn a() {
10540 // dog();
10541 catˇ(ˇ);
10542 }"
10543 ));
10544
10545 // Multiple cursors on one line, with selection -> don't advance
10546 cx.set_state(indoc!(
10547 "fn a() {
10548 ˇdˇog«()ˇ»;
10549 cat();
10550 }"
10551 ));
10552 cx.update_editor(|editor, window, cx| {
10553 editor.toggle_comments(toggle_comments, window, cx);
10554 });
10555 cx.assert_editor_state(indoc!(
10556 "fn a() {
10557 // ˇdˇog«()ˇ»;
10558 cat();
10559 }"
10560 ));
10561
10562 // Single cursor on one line -> advance
10563 // Cursor moves to column 0 on blank line
10564 cx.set_state(indoc!(
10565 "fn a() {
10566 ˇdog();
10567
10568 cat();
10569 }"
10570 ));
10571 cx.update_editor(|editor, window, cx| {
10572 editor.toggle_comments(toggle_comments, window, cx);
10573 });
10574 cx.assert_editor_state(indoc!(
10575 "fn a() {
10576 // dog();
10577 ˇ
10578 cat();
10579 }"
10580 ));
10581
10582 // Single cursor on one line -> advance
10583 // Cursor starts and ends at column 0
10584 cx.set_state(indoc!(
10585 "fn a() {
10586 ˇ dog();
10587 cat();
10588 }"
10589 ));
10590 cx.update_editor(|editor, window, cx| {
10591 editor.toggle_comments(toggle_comments, window, cx);
10592 });
10593 cx.assert_editor_state(indoc!(
10594 "fn a() {
10595 // dog();
10596 ˇ cat();
10597 }"
10598 ));
10599}
10600
10601#[gpui::test]
10602async fn test_toggle_block_comment(cx: &mut TestAppContext) {
10603 init_test(cx, |_| {});
10604
10605 let mut cx = EditorTestContext::new(cx).await;
10606
10607 let html_language = Arc::new(
10608 Language::new(
10609 LanguageConfig {
10610 name: "HTML".into(),
10611 block_comment: Some(("<!-- ".into(), " -->".into())),
10612 ..Default::default()
10613 },
10614 Some(tree_sitter_html::LANGUAGE.into()),
10615 )
10616 .with_injection_query(
10617 r#"
10618 (script_element
10619 (raw_text) @injection.content
10620 (#set! injection.language "javascript"))
10621 "#,
10622 )
10623 .unwrap(),
10624 );
10625
10626 let javascript_language = Arc::new(Language::new(
10627 LanguageConfig {
10628 name: "JavaScript".into(),
10629 line_comments: vec!["// ".into()],
10630 ..Default::default()
10631 },
10632 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10633 ));
10634
10635 cx.language_registry().add(html_language.clone());
10636 cx.language_registry().add(javascript_language.clone());
10637 cx.update_buffer(|buffer, cx| {
10638 buffer.set_language(Some(html_language), cx);
10639 });
10640
10641 // Toggle comments for empty selections
10642 cx.set_state(
10643 &r#"
10644 <p>A</p>ˇ
10645 <p>B</p>ˇ
10646 <p>C</p>ˇ
10647 "#
10648 .unindent(),
10649 );
10650 cx.update_editor(|editor, window, cx| {
10651 editor.toggle_comments(&ToggleComments::default(), window, cx)
10652 });
10653 cx.assert_editor_state(
10654 &r#"
10655 <!-- <p>A</p>ˇ -->
10656 <!-- <p>B</p>ˇ -->
10657 <!-- <p>C</p>ˇ -->
10658 "#
10659 .unindent(),
10660 );
10661 cx.update_editor(|editor, window, cx| {
10662 editor.toggle_comments(&ToggleComments::default(), window, cx)
10663 });
10664 cx.assert_editor_state(
10665 &r#"
10666 <p>A</p>ˇ
10667 <p>B</p>ˇ
10668 <p>C</p>ˇ
10669 "#
10670 .unindent(),
10671 );
10672
10673 // Toggle comments for mixture of empty and non-empty selections, where
10674 // multiple selections occupy a given line.
10675 cx.set_state(
10676 &r#"
10677 <p>A«</p>
10678 <p>ˇ»B</p>ˇ
10679 <p>C«</p>
10680 <p>ˇ»D</p>ˇ
10681 "#
10682 .unindent(),
10683 );
10684
10685 cx.update_editor(|editor, window, cx| {
10686 editor.toggle_comments(&ToggleComments::default(), window, cx)
10687 });
10688 cx.assert_editor_state(
10689 &r#"
10690 <!-- <p>A«</p>
10691 <p>ˇ»B</p>ˇ -->
10692 <!-- <p>C«</p>
10693 <p>ˇ»D</p>ˇ -->
10694 "#
10695 .unindent(),
10696 );
10697 cx.update_editor(|editor, window, cx| {
10698 editor.toggle_comments(&ToggleComments::default(), window, cx)
10699 });
10700 cx.assert_editor_state(
10701 &r#"
10702 <p>A«</p>
10703 <p>ˇ»B</p>ˇ
10704 <p>C«</p>
10705 <p>ˇ»D</p>ˇ
10706 "#
10707 .unindent(),
10708 );
10709
10710 // Toggle comments when different languages are active for different
10711 // selections.
10712 cx.set_state(
10713 &r#"
10714 ˇ<script>
10715 ˇvar x = new Y();
10716 ˇ</script>
10717 "#
10718 .unindent(),
10719 );
10720 cx.executor().run_until_parked();
10721 cx.update_editor(|editor, window, cx| {
10722 editor.toggle_comments(&ToggleComments::default(), window, cx)
10723 });
10724 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
10725 // Uncommenting and commenting from this position brings in even more wrong artifacts.
10726 cx.assert_editor_state(
10727 &r#"
10728 <!-- ˇ<script> -->
10729 // ˇvar x = new Y();
10730 <!-- ˇ</script> -->
10731 "#
10732 .unindent(),
10733 );
10734}
10735
10736#[gpui::test]
10737fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
10738 init_test(cx, |_| {});
10739
10740 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10741 let multibuffer = cx.new(|cx| {
10742 let mut multibuffer = MultiBuffer::new(ReadWrite);
10743 multibuffer.push_excerpts(
10744 buffer.clone(),
10745 [
10746 ExcerptRange {
10747 context: Point::new(0, 0)..Point::new(0, 4),
10748 primary: None,
10749 },
10750 ExcerptRange {
10751 context: Point::new(1, 0)..Point::new(1, 4),
10752 primary: None,
10753 },
10754 ],
10755 cx,
10756 );
10757 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
10758 multibuffer
10759 });
10760
10761 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
10762 editor.update_in(cx, |editor, window, cx| {
10763 assert_eq!(editor.text(cx), "aaaa\nbbbb");
10764 editor.change_selections(None, window, cx, |s| {
10765 s.select_ranges([
10766 Point::new(0, 0)..Point::new(0, 0),
10767 Point::new(1, 0)..Point::new(1, 0),
10768 ])
10769 });
10770
10771 editor.handle_input("X", window, cx);
10772 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
10773 assert_eq!(
10774 editor.selections.ranges(cx),
10775 [
10776 Point::new(0, 1)..Point::new(0, 1),
10777 Point::new(1, 1)..Point::new(1, 1),
10778 ]
10779 );
10780
10781 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
10782 editor.change_selections(None, window, cx, |s| {
10783 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
10784 });
10785 editor.backspace(&Default::default(), window, cx);
10786 assert_eq!(editor.text(cx), "Xa\nbbb");
10787 assert_eq!(
10788 editor.selections.ranges(cx),
10789 [Point::new(1, 0)..Point::new(1, 0)]
10790 );
10791
10792 editor.change_selections(None, window, cx, |s| {
10793 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
10794 });
10795 editor.backspace(&Default::default(), window, cx);
10796 assert_eq!(editor.text(cx), "X\nbb");
10797 assert_eq!(
10798 editor.selections.ranges(cx),
10799 [Point::new(0, 1)..Point::new(0, 1)]
10800 );
10801 });
10802}
10803
10804#[gpui::test]
10805fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
10806 init_test(cx, |_| {});
10807
10808 let markers = vec![('[', ']').into(), ('(', ')').into()];
10809 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
10810 indoc! {"
10811 [aaaa
10812 (bbbb]
10813 cccc)",
10814 },
10815 markers.clone(),
10816 );
10817 let excerpt_ranges = markers.into_iter().map(|marker| {
10818 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
10819 ExcerptRange {
10820 context,
10821 primary: None,
10822 }
10823 });
10824 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
10825 let multibuffer = cx.new(|cx| {
10826 let mut multibuffer = MultiBuffer::new(ReadWrite);
10827 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
10828 multibuffer
10829 });
10830
10831 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
10832 editor.update_in(cx, |editor, window, cx| {
10833 let (expected_text, selection_ranges) = marked_text_ranges(
10834 indoc! {"
10835 aaaa
10836 bˇbbb
10837 bˇbbˇb
10838 cccc"
10839 },
10840 true,
10841 );
10842 assert_eq!(editor.text(cx), expected_text);
10843 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
10844
10845 editor.handle_input("X", window, cx);
10846
10847 let (expected_text, expected_selections) = marked_text_ranges(
10848 indoc! {"
10849 aaaa
10850 bXˇbbXb
10851 bXˇbbXˇb
10852 cccc"
10853 },
10854 false,
10855 );
10856 assert_eq!(editor.text(cx), expected_text);
10857 assert_eq!(editor.selections.ranges(cx), expected_selections);
10858
10859 editor.newline(&Newline, window, cx);
10860 let (expected_text, expected_selections) = marked_text_ranges(
10861 indoc! {"
10862 aaaa
10863 bX
10864 ˇbbX
10865 b
10866 bX
10867 ˇbbX
10868 ˇb
10869 cccc"
10870 },
10871 false,
10872 );
10873 assert_eq!(editor.text(cx), expected_text);
10874 assert_eq!(editor.selections.ranges(cx), expected_selections);
10875 });
10876}
10877
10878#[gpui::test]
10879fn test_refresh_selections(cx: &mut TestAppContext) {
10880 init_test(cx, |_| {});
10881
10882 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10883 let mut excerpt1_id = None;
10884 let multibuffer = cx.new(|cx| {
10885 let mut multibuffer = MultiBuffer::new(ReadWrite);
10886 excerpt1_id = multibuffer
10887 .push_excerpts(
10888 buffer.clone(),
10889 [
10890 ExcerptRange {
10891 context: Point::new(0, 0)..Point::new(1, 4),
10892 primary: None,
10893 },
10894 ExcerptRange {
10895 context: Point::new(1, 0)..Point::new(2, 4),
10896 primary: None,
10897 },
10898 ],
10899 cx,
10900 )
10901 .into_iter()
10902 .next();
10903 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
10904 multibuffer
10905 });
10906
10907 let editor = cx.add_window(|window, cx| {
10908 let mut editor = build_editor(multibuffer.clone(), window, cx);
10909 let snapshot = editor.snapshot(window, cx);
10910 editor.change_selections(None, window, cx, |s| {
10911 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
10912 });
10913 editor.begin_selection(
10914 Point::new(2, 1).to_display_point(&snapshot),
10915 true,
10916 1,
10917 window,
10918 cx,
10919 );
10920 assert_eq!(
10921 editor.selections.ranges(cx),
10922 [
10923 Point::new(1, 3)..Point::new(1, 3),
10924 Point::new(2, 1)..Point::new(2, 1),
10925 ]
10926 );
10927 editor
10928 });
10929
10930 // Refreshing selections is a no-op when excerpts haven't changed.
10931 _ = editor.update(cx, |editor, window, cx| {
10932 editor.change_selections(None, window, cx, |s| s.refresh());
10933 assert_eq!(
10934 editor.selections.ranges(cx),
10935 [
10936 Point::new(1, 3)..Point::new(1, 3),
10937 Point::new(2, 1)..Point::new(2, 1),
10938 ]
10939 );
10940 });
10941
10942 multibuffer.update(cx, |multibuffer, cx| {
10943 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
10944 });
10945 _ = editor.update(cx, |editor, window, cx| {
10946 // Removing an excerpt causes the first selection to become degenerate.
10947 assert_eq!(
10948 editor.selections.ranges(cx),
10949 [
10950 Point::new(0, 0)..Point::new(0, 0),
10951 Point::new(0, 1)..Point::new(0, 1)
10952 ]
10953 );
10954
10955 // Refreshing selections will relocate the first selection to the original buffer
10956 // location.
10957 editor.change_selections(None, window, cx, |s| s.refresh());
10958 assert_eq!(
10959 editor.selections.ranges(cx),
10960 [
10961 Point::new(0, 1)..Point::new(0, 1),
10962 Point::new(0, 3)..Point::new(0, 3)
10963 ]
10964 );
10965 assert!(editor.selections.pending_anchor().is_some());
10966 });
10967}
10968
10969#[gpui::test]
10970fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
10971 init_test(cx, |_| {});
10972
10973 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10974 let mut excerpt1_id = None;
10975 let multibuffer = cx.new(|cx| {
10976 let mut multibuffer = MultiBuffer::new(ReadWrite);
10977 excerpt1_id = multibuffer
10978 .push_excerpts(
10979 buffer.clone(),
10980 [
10981 ExcerptRange {
10982 context: Point::new(0, 0)..Point::new(1, 4),
10983 primary: None,
10984 },
10985 ExcerptRange {
10986 context: Point::new(1, 0)..Point::new(2, 4),
10987 primary: None,
10988 },
10989 ],
10990 cx,
10991 )
10992 .into_iter()
10993 .next();
10994 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
10995 multibuffer
10996 });
10997
10998 let editor = cx.add_window(|window, cx| {
10999 let mut editor = build_editor(multibuffer.clone(), window, cx);
11000 let snapshot = editor.snapshot(window, cx);
11001 editor.begin_selection(
11002 Point::new(1, 3).to_display_point(&snapshot),
11003 false,
11004 1,
11005 window,
11006 cx,
11007 );
11008 assert_eq!(
11009 editor.selections.ranges(cx),
11010 [Point::new(1, 3)..Point::new(1, 3)]
11011 );
11012 editor
11013 });
11014
11015 multibuffer.update(cx, |multibuffer, cx| {
11016 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
11017 });
11018 _ = editor.update(cx, |editor, window, cx| {
11019 assert_eq!(
11020 editor.selections.ranges(cx),
11021 [Point::new(0, 0)..Point::new(0, 0)]
11022 );
11023
11024 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
11025 editor.change_selections(None, window, cx, |s| s.refresh());
11026 assert_eq!(
11027 editor.selections.ranges(cx),
11028 [Point::new(0, 3)..Point::new(0, 3)]
11029 );
11030 assert!(editor.selections.pending_anchor().is_some());
11031 });
11032}
11033
11034#[gpui::test]
11035async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
11036 init_test(cx, |_| {});
11037
11038 let language = Arc::new(
11039 Language::new(
11040 LanguageConfig {
11041 brackets: BracketPairConfig {
11042 pairs: vec![
11043 BracketPair {
11044 start: "{".to_string(),
11045 end: "}".to_string(),
11046 close: true,
11047 surround: true,
11048 newline: true,
11049 },
11050 BracketPair {
11051 start: "/* ".to_string(),
11052 end: " */".to_string(),
11053 close: true,
11054 surround: true,
11055 newline: true,
11056 },
11057 ],
11058 ..Default::default()
11059 },
11060 ..Default::default()
11061 },
11062 Some(tree_sitter_rust::LANGUAGE.into()),
11063 )
11064 .with_indents_query("")
11065 .unwrap(),
11066 );
11067
11068 let text = concat!(
11069 "{ }\n", //
11070 " x\n", //
11071 " /* */\n", //
11072 "x\n", //
11073 "{{} }\n", //
11074 );
11075
11076 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11077 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11078 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11079 editor
11080 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11081 .await;
11082
11083 editor.update_in(cx, |editor, window, cx| {
11084 editor.change_selections(None, window, cx, |s| {
11085 s.select_display_ranges([
11086 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
11087 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
11088 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
11089 ])
11090 });
11091 editor.newline(&Newline, window, cx);
11092
11093 assert_eq!(
11094 editor.buffer().read(cx).read(cx).text(),
11095 concat!(
11096 "{ \n", // Suppress rustfmt
11097 "\n", //
11098 "}\n", //
11099 " x\n", //
11100 " /* \n", //
11101 " \n", //
11102 " */\n", //
11103 "x\n", //
11104 "{{} \n", //
11105 "}\n", //
11106 )
11107 );
11108 });
11109}
11110
11111#[gpui::test]
11112fn test_highlighted_ranges(cx: &mut TestAppContext) {
11113 init_test(cx, |_| {});
11114
11115 let editor = cx.add_window(|window, cx| {
11116 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
11117 build_editor(buffer.clone(), window, cx)
11118 });
11119
11120 _ = editor.update(cx, |editor, window, cx| {
11121 struct Type1;
11122 struct Type2;
11123
11124 let buffer = editor.buffer.read(cx).snapshot(cx);
11125
11126 let anchor_range =
11127 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
11128
11129 editor.highlight_background::<Type1>(
11130 &[
11131 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
11132 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
11133 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
11134 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
11135 ],
11136 |_| Hsla::red(),
11137 cx,
11138 );
11139 editor.highlight_background::<Type2>(
11140 &[
11141 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
11142 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
11143 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
11144 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
11145 ],
11146 |_| Hsla::green(),
11147 cx,
11148 );
11149
11150 let snapshot = editor.snapshot(window, cx);
11151 let mut highlighted_ranges = editor.background_highlights_in_range(
11152 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
11153 &snapshot,
11154 cx.theme().colors(),
11155 );
11156 // Enforce a consistent ordering based on color without relying on the ordering of the
11157 // highlight's `TypeId` which is non-executor.
11158 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
11159 assert_eq!(
11160 highlighted_ranges,
11161 &[
11162 (
11163 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
11164 Hsla::red(),
11165 ),
11166 (
11167 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11168 Hsla::red(),
11169 ),
11170 (
11171 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
11172 Hsla::green(),
11173 ),
11174 (
11175 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
11176 Hsla::green(),
11177 ),
11178 ]
11179 );
11180 assert_eq!(
11181 editor.background_highlights_in_range(
11182 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
11183 &snapshot,
11184 cx.theme().colors(),
11185 ),
11186 &[(
11187 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11188 Hsla::red(),
11189 )]
11190 );
11191 });
11192}
11193
11194#[gpui::test]
11195async fn test_following(cx: &mut TestAppContext) {
11196 init_test(cx, |_| {});
11197
11198 let fs = FakeFs::new(cx.executor());
11199 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11200
11201 let buffer = project.update(cx, |project, cx| {
11202 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
11203 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
11204 });
11205 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
11206 let follower = cx.update(|cx| {
11207 cx.open_window(
11208 WindowOptions {
11209 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
11210 gpui::Point::new(px(0.), px(0.)),
11211 gpui::Point::new(px(10.), px(80.)),
11212 ))),
11213 ..Default::default()
11214 },
11215 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
11216 )
11217 .unwrap()
11218 });
11219
11220 let is_still_following = Rc::new(RefCell::new(true));
11221 let follower_edit_event_count = Rc::new(RefCell::new(0));
11222 let pending_update = Rc::new(RefCell::new(None));
11223 let leader_entity = leader.root(cx).unwrap();
11224 let follower_entity = follower.root(cx).unwrap();
11225 _ = follower.update(cx, {
11226 let update = pending_update.clone();
11227 let is_still_following = is_still_following.clone();
11228 let follower_edit_event_count = follower_edit_event_count.clone();
11229 |_, window, cx| {
11230 cx.subscribe_in(
11231 &leader_entity,
11232 window,
11233 move |_, leader, event, window, cx| {
11234 leader.read(cx).add_event_to_update_proto(
11235 event,
11236 &mut update.borrow_mut(),
11237 window,
11238 cx,
11239 );
11240 },
11241 )
11242 .detach();
11243
11244 cx.subscribe_in(
11245 &follower_entity,
11246 window,
11247 move |_, _, event: &EditorEvent, _window, _cx| {
11248 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
11249 *is_still_following.borrow_mut() = false;
11250 }
11251
11252 if let EditorEvent::BufferEdited = event {
11253 *follower_edit_event_count.borrow_mut() += 1;
11254 }
11255 },
11256 )
11257 .detach();
11258 }
11259 });
11260
11261 // Update the selections only
11262 _ = leader.update(cx, |leader, window, cx| {
11263 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11264 });
11265 follower
11266 .update(cx, |follower, window, cx| {
11267 follower.apply_update_proto(
11268 &project,
11269 pending_update.borrow_mut().take().unwrap(),
11270 window,
11271 cx,
11272 )
11273 })
11274 .unwrap()
11275 .await
11276 .unwrap();
11277 _ = follower.update(cx, |follower, _, cx| {
11278 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
11279 });
11280 assert!(*is_still_following.borrow());
11281 assert_eq!(*follower_edit_event_count.borrow(), 0);
11282
11283 // Update the scroll position only
11284 _ = leader.update(cx, |leader, window, cx| {
11285 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11286 });
11287 follower
11288 .update(cx, |follower, window, cx| {
11289 follower.apply_update_proto(
11290 &project,
11291 pending_update.borrow_mut().take().unwrap(),
11292 window,
11293 cx,
11294 )
11295 })
11296 .unwrap()
11297 .await
11298 .unwrap();
11299 assert_eq!(
11300 follower
11301 .update(cx, |follower, _, cx| follower.scroll_position(cx))
11302 .unwrap(),
11303 gpui::Point::new(1.5, 3.5)
11304 );
11305 assert!(*is_still_following.borrow());
11306 assert_eq!(*follower_edit_event_count.borrow(), 0);
11307
11308 // Update the selections and scroll position. The follower's scroll position is updated
11309 // via autoscroll, not via the leader's exact scroll position.
11310 _ = leader.update(cx, |leader, window, cx| {
11311 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
11312 leader.request_autoscroll(Autoscroll::newest(), cx);
11313 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11314 });
11315 follower
11316 .update(cx, |follower, window, cx| {
11317 follower.apply_update_proto(
11318 &project,
11319 pending_update.borrow_mut().take().unwrap(),
11320 window,
11321 cx,
11322 )
11323 })
11324 .unwrap()
11325 .await
11326 .unwrap();
11327 _ = follower.update(cx, |follower, _, cx| {
11328 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
11329 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
11330 });
11331 assert!(*is_still_following.borrow());
11332
11333 // Creating a pending selection that precedes another selection
11334 _ = leader.update(cx, |leader, window, cx| {
11335 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11336 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
11337 });
11338 follower
11339 .update(cx, |follower, window, cx| {
11340 follower.apply_update_proto(
11341 &project,
11342 pending_update.borrow_mut().take().unwrap(),
11343 window,
11344 cx,
11345 )
11346 })
11347 .unwrap()
11348 .await
11349 .unwrap();
11350 _ = follower.update(cx, |follower, _, cx| {
11351 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
11352 });
11353 assert!(*is_still_following.borrow());
11354
11355 // Extend the pending selection so that it surrounds another selection
11356 _ = leader.update(cx, |leader, window, cx| {
11357 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
11358 });
11359 follower
11360 .update(cx, |follower, window, cx| {
11361 follower.apply_update_proto(
11362 &project,
11363 pending_update.borrow_mut().take().unwrap(),
11364 window,
11365 cx,
11366 )
11367 })
11368 .unwrap()
11369 .await
11370 .unwrap();
11371 _ = follower.update(cx, |follower, _, cx| {
11372 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
11373 });
11374
11375 // Scrolling locally breaks the follow
11376 _ = follower.update(cx, |follower, window, cx| {
11377 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
11378 follower.set_scroll_anchor(
11379 ScrollAnchor {
11380 anchor: top_anchor,
11381 offset: gpui::Point::new(0.0, 0.5),
11382 },
11383 window,
11384 cx,
11385 );
11386 });
11387 assert!(!(*is_still_following.borrow()));
11388}
11389
11390#[gpui::test]
11391async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
11392 init_test(cx, |_| {});
11393
11394 let fs = FakeFs::new(cx.executor());
11395 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11396 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11397 let pane = workspace
11398 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11399 .unwrap();
11400
11401 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11402
11403 let leader = pane.update_in(cx, |_, window, cx| {
11404 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
11405 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
11406 });
11407
11408 // Start following the editor when it has no excerpts.
11409 let mut state_message =
11410 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
11411 let workspace_entity = workspace.root(cx).unwrap();
11412 let follower_1 = cx
11413 .update_window(*workspace.deref(), |_, window, cx| {
11414 Editor::from_state_proto(
11415 workspace_entity,
11416 ViewId {
11417 creator: Default::default(),
11418 id: 0,
11419 },
11420 &mut state_message,
11421 window,
11422 cx,
11423 )
11424 })
11425 .unwrap()
11426 .unwrap()
11427 .await
11428 .unwrap();
11429
11430 let update_message = Rc::new(RefCell::new(None));
11431 follower_1.update_in(cx, {
11432 let update = update_message.clone();
11433 |_, window, cx| {
11434 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
11435 leader.read(cx).add_event_to_update_proto(
11436 event,
11437 &mut update.borrow_mut(),
11438 window,
11439 cx,
11440 );
11441 })
11442 .detach();
11443 }
11444 });
11445
11446 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
11447 (
11448 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
11449 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
11450 )
11451 });
11452
11453 // Insert some excerpts.
11454 leader.update(cx, |leader, cx| {
11455 leader.buffer.update(cx, |multibuffer, cx| {
11456 let excerpt_ids = multibuffer.push_excerpts(
11457 buffer_1.clone(),
11458 [
11459 ExcerptRange {
11460 context: 1..6,
11461 primary: None,
11462 },
11463 ExcerptRange {
11464 context: 12..15,
11465 primary: None,
11466 },
11467 ExcerptRange {
11468 context: 0..3,
11469 primary: None,
11470 },
11471 ],
11472 cx,
11473 );
11474 multibuffer.insert_excerpts_after(
11475 excerpt_ids[0],
11476 buffer_2.clone(),
11477 [
11478 ExcerptRange {
11479 context: 8..12,
11480 primary: None,
11481 },
11482 ExcerptRange {
11483 context: 0..6,
11484 primary: None,
11485 },
11486 ],
11487 cx,
11488 );
11489 });
11490 });
11491
11492 // Apply the update of adding the excerpts.
11493 follower_1
11494 .update_in(cx, |follower, window, cx| {
11495 follower.apply_update_proto(
11496 &project,
11497 update_message.borrow().clone().unwrap(),
11498 window,
11499 cx,
11500 )
11501 })
11502 .await
11503 .unwrap();
11504 assert_eq!(
11505 follower_1.update(cx, |editor, cx| editor.text(cx)),
11506 leader.update(cx, |editor, cx| editor.text(cx))
11507 );
11508 update_message.borrow_mut().take();
11509
11510 // Start following separately after it already has excerpts.
11511 let mut state_message =
11512 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
11513 let workspace_entity = workspace.root(cx).unwrap();
11514 let follower_2 = cx
11515 .update_window(*workspace.deref(), |_, window, cx| {
11516 Editor::from_state_proto(
11517 workspace_entity,
11518 ViewId {
11519 creator: Default::default(),
11520 id: 0,
11521 },
11522 &mut state_message,
11523 window,
11524 cx,
11525 )
11526 })
11527 .unwrap()
11528 .unwrap()
11529 .await
11530 .unwrap();
11531 assert_eq!(
11532 follower_2.update(cx, |editor, cx| editor.text(cx)),
11533 leader.update(cx, |editor, cx| editor.text(cx))
11534 );
11535
11536 // Remove some excerpts.
11537 leader.update(cx, |leader, cx| {
11538 leader.buffer.update(cx, |multibuffer, cx| {
11539 let excerpt_ids = multibuffer.excerpt_ids();
11540 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
11541 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
11542 });
11543 });
11544
11545 // Apply the update of removing the excerpts.
11546 follower_1
11547 .update_in(cx, |follower, window, cx| {
11548 follower.apply_update_proto(
11549 &project,
11550 update_message.borrow().clone().unwrap(),
11551 window,
11552 cx,
11553 )
11554 })
11555 .await
11556 .unwrap();
11557 follower_2
11558 .update_in(cx, |follower, window, cx| {
11559 follower.apply_update_proto(
11560 &project,
11561 update_message.borrow().clone().unwrap(),
11562 window,
11563 cx,
11564 )
11565 })
11566 .await
11567 .unwrap();
11568 update_message.borrow_mut().take();
11569 assert_eq!(
11570 follower_1.update(cx, |editor, cx| editor.text(cx)),
11571 leader.update(cx, |editor, cx| editor.text(cx))
11572 );
11573}
11574
11575#[gpui::test]
11576async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
11577 init_test(cx, |_| {});
11578
11579 let mut cx = EditorTestContext::new(cx).await;
11580 let lsp_store =
11581 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11582
11583 cx.set_state(indoc! {"
11584 ˇfn func(abc def: i32) -> u32 {
11585 }
11586 "});
11587
11588 cx.update(|_, cx| {
11589 lsp_store.update(cx, |lsp_store, cx| {
11590 lsp_store
11591 .update_diagnostics(
11592 LanguageServerId(0),
11593 lsp::PublishDiagnosticsParams {
11594 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11595 version: None,
11596 diagnostics: vec![
11597 lsp::Diagnostic {
11598 range: lsp::Range::new(
11599 lsp::Position::new(0, 11),
11600 lsp::Position::new(0, 12),
11601 ),
11602 severity: Some(lsp::DiagnosticSeverity::ERROR),
11603 ..Default::default()
11604 },
11605 lsp::Diagnostic {
11606 range: lsp::Range::new(
11607 lsp::Position::new(0, 12),
11608 lsp::Position::new(0, 15),
11609 ),
11610 severity: Some(lsp::DiagnosticSeverity::ERROR),
11611 ..Default::default()
11612 },
11613 lsp::Diagnostic {
11614 range: lsp::Range::new(
11615 lsp::Position::new(0, 25),
11616 lsp::Position::new(0, 28),
11617 ),
11618 severity: Some(lsp::DiagnosticSeverity::ERROR),
11619 ..Default::default()
11620 },
11621 ],
11622 },
11623 &[],
11624 cx,
11625 )
11626 .unwrap()
11627 });
11628 });
11629
11630 executor.run_until_parked();
11631
11632 cx.update_editor(|editor, window, cx| {
11633 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11634 });
11635
11636 cx.assert_editor_state(indoc! {"
11637 fn func(abc def: i32) -> ˇu32 {
11638 }
11639 "});
11640
11641 cx.update_editor(|editor, window, cx| {
11642 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11643 });
11644
11645 cx.assert_editor_state(indoc! {"
11646 fn func(abc ˇdef: i32) -> u32 {
11647 }
11648 "});
11649
11650 cx.update_editor(|editor, window, cx| {
11651 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11652 });
11653
11654 cx.assert_editor_state(indoc! {"
11655 fn func(abcˇ def: i32) -> u32 {
11656 }
11657 "});
11658
11659 cx.update_editor(|editor, window, cx| {
11660 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11661 });
11662
11663 cx.assert_editor_state(indoc! {"
11664 fn func(abc def: i32) -> ˇu32 {
11665 }
11666 "});
11667}
11668
11669#[gpui::test]
11670async fn cycle_through_same_place_diagnostics(
11671 executor: BackgroundExecutor,
11672 cx: &mut TestAppContext,
11673) {
11674 init_test(cx, |_| {});
11675
11676 let mut cx = EditorTestContext::new(cx).await;
11677 let lsp_store =
11678 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11679
11680 cx.set_state(indoc! {"
11681 ˇfn func(abc def: i32) -> u32 {
11682 }
11683 "});
11684
11685 cx.update(|_, cx| {
11686 lsp_store.update(cx, |lsp_store, cx| {
11687 lsp_store
11688 .update_diagnostics(
11689 LanguageServerId(0),
11690 lsp::PublishDiagnosticsParams {
11691 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11692 version: None,
11693 diagnostics: vec![
11694 lsp::Diagnostic {
11695 range: lsp::Range::new(
11696 lsp::Position::new(0, 11),
11697 lsp::Position::new(0, 12),
11698 ),
11699 severity: Some(lsp::DiagnosticSeverity::ERROR),
11700 ..Default::default()
11701 },
11702 lsp::Diagnostic {
11703 range: lsp::Range::new(
11704 lsp::Position::new(0, 12),
11705 lsp::Position::new(0, 15),
11706 ),
11707 severity: Some(lsp::DiagnosticSeverity::ERROR),
11708 ..Default::default()
11709 },
11710 lsp::Diagnostic {
11711 range: lsp::Range::new(
11712 lsp::Position::new(0, 12),
11713 lsp::Position::new(0, 15),
11714 ),
11715 severity: Some(lsp::DiagnosticSeverity::ERROR),
11716 ..Default::default()
11717 },
11718 lsp::Diagnostic {
11719 range: lsp::Range::new(
11720 lsp::Position::new(0, 25),
11721 lsp::Position::new(0, 28),
11722 ),
11723 severity: Some(lsp::DiagnosticSeverity::ERROR),
11724 ..Default::default()
11725 },
11726 ],
11727 },
11728 &[],
11729 cx,
11730 )
11731 .unwrap()
11732 });
11733 });
11734 executor.run_until_parked();
11735
11736 //// Backward
11737
11738 // Fourth diagnostic
11739 cx.update_editor(|editor, window, cx| {
11740 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11741 });
11742 cx.assert_editor_state(indoc! {"
11743 fn func(abc def: i32) -> ˇu32 {
11744 }
11745 "});
11746
11747 // Third diagnostic
11748 cx.update_editor(|editor, window, cx| {
11749 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11750 });
11751 cx.assert_editor_state(indoc! {"
11752 fn func(abc ˇdef: i32) -> u32 {
11753 }
11754 "});
11755
11756 // Second diagnostic, same place
11757 cx.update_editor(|editor, window, cx| {
11758 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11759 });
11760 cx.assert_editor_state(indoc! {"
11761 fn func(abc ˇdef: i32) -> u32 {
11762 }
11763 "});
11764
11765 // First diagnostic
11766 cx.update_editor(|editor, window, cx| {
11767 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11768 });
11769 cx.assert_editor_state(indoc! {"
11770 fn func(abcˇ def: i32) -> u32 {
11771 }
11772 "});
11773
11774 // Wrapped over, fourth diagnostic
11775 cx.update_editor(|editor, window, cx| {
11776 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11777 });
11778 cx.assert_editor_state(indoc! {"
11779 fn func(abc def: i32) -> ˇu32 {
11780 }
11781 "});
11782
11783 cx.update_editor(|editor, window, cx| {
11784 editor.move_to_beginning(&MoveToBeginning, window, cx);
11785 });
11786 cx.assert_editor_state(indoc! {"
11787 ˇfn func(abc def: i32) -> u32 {
11788 }
11789 "});
11790
11791 //// Forward
11792
11793 // First diagnostic
11794 cx.update_editor(|editor, window, cx| {
11795 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11796 });
11797 cx.assert_editor_state(indoc! {"
11798 fn func(abcˇ def: i32) -> u32 {
11799 }
11800 "});
11801
11802 // Second diagnostic
11803 cx.update_editor(|editor, window, cx| {
11804 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11805 });
11806 cx.assert_editor_state(indoc! {"
11807 fn func(abc ˇdef: i32) -> u32 {
11808 }
11809 "});
11810
11811 // Third diagnostic, same place
11812 cx.update_editor(|editor, window, cx| {
11813 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11814 });
11815 cx.assert_editor_state(indoc! {"
11816 fn func(abc ˇdef: i32) -> u32 {
11817 }
11818 "});
11819
11820 // Fourth diagnostic
11821 cx.update_editor(|editor, window, cx| {
11822 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11823 });
11824 cx.assert_editor_state(indoc! {"
11825 fn func(abc def: i32) -> ˇu32 {
11826 }
11827 "});
11828
11829 // Wrapped around, first diagnostic
11830 cx.update_editor(|editor, window, cx| {
11831 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11832 });
11833 cx.assert_editor_state(indoc! {"
11834 fn func(abcˇ def: i32) -> u32 {
11835 }
11836 "});
11837}
11838
11839#[gpui::test]
11840async fn active_diagnostics_dismiss_after_invalidation(
11841 executor: BackgroundExecutor,
11842 cx: &mut TestAppContext,
11843) {
11844 init_test(cx, |_| {});
11845
11846 let mut cx = EditorTestContext::new(cx).await;
11847 let lsp_store =
11848 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11849
11850 cx.set_state(indoc! {"
11851 ˇfn func(abc def: i32) -> u32 {
11852 }
11853 "});
11854
11855 let message = "Something's wrong!";
11856 cx.update(|_, cx| {
11857 lsp_store.update(cx, |lsp_store, cx| {
11858 lsp_store
11859 .update_diagnostics(
11860 LanguageServerId(0),
11861 lsp::PublishDiagnosticsParams {
11862 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11863 version: None,
11864 diagnostics: vec![lsp::Diagnostic {
11865 range: lsp::Range::new(
11866 lsp::Position::new(0, 11),
11867 lsp::Position::new(0, 12),
11868 ),
11869 severity: Some(lsp::DiagnosticSeverity::ERROR),
11870 message: message.to_string(),
11871 ..Default::default()
11872 }],
11873 },
11874 &[],
11875 cx,
11876 )
11877 .unwrap()
11878 });
11879 });
11880 executor.run_until_parked();
11881
11882 cx.update_editor(|editor, window, cx| {
11883 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11884 assert_eq!(
11885 editor
11886 .active_diagnostics
11887 .as_ref()
11888 .map(|diagnostics_group| diagnostics_group.primary_message.as_str()),
11889 Some(message),
11890 "Should have a diagnostics group activated"
11891 );
11892 });
11893 cx.assert_editor_state(indoc! {"
11894 fn func(abcˇ def: i32) -> u32 {
11895 }
11896 "});
11897
11898 cx.update(|_, cx| {
11899 lsp_store.update(cx, |lsp_store, cx| {
11900 lsp_store
11901 .update_diagnostics(
11902 LanguageServerId(0),
11903 lsp::PublishDiagnosticsParams {
11904 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11905 version: None,
11906 diagnostics: Vec::new(),
11907 },
11908 &[],
11909 cx,
11910 )
11911 .unwrap()
11912 });
11913 });
11914 executor.run_until_parked();
11915 cx.update_editor(|editor, _, _| {
11916 assert_eq!(
11917 editor.active_diagnostics, None,
11918 "After no diagnostics set to the editor, no diagnostics should be active"
11919 );
11920 });
11921 cx.assert_editor_state(indoc! {"
11922 fn func(abcˇ def: i32) -> u32 {
11923 }
11924 "});
11925
11926 cx.update_editor(|editor, window, cx| {
11927 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11928 assert_eq!(
11929 editor.active_diagnostics, None,
11930 "Should be no diagnostics to go to and activate"
11931 );
11932 });
11933 cx.assert_editor_state(indoc! {"
11934 fn func(abcˇ def: i32) -> u32 {
11935 }
11936 "});
11937}
11938
11939#[gpui::test]
11940async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
11941 init_test(cx, |_| {});
11942
11943 let mut cx = EditorTestContext::new(cx).await;
11944
11945 cx.set_state(indoc! {"
11946 fn func(abˇc def: i32) -> u32 {
11947 }
11948 "});
11949 let lsp_store =
11950 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11951
11952 cx.update(|_, cx| {
11953 lsp_store.update(cx, |lsp_store, cx| {
11954 lsp_store.update_diagnostics(
11955 LanguageServerId(0),
11956 lsp::PublishDiagnosticsParams {
11957 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11958 version: None,
11959 diagnostics: vec![lsp::Diagnostic {
11960 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
11961 severity: Some(lsp::DiagnosticSeverity::ERROR),
11962 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
11963 ..Default::default()
11964 }],
11965 },
11966 &[],
11967 cx,
11968 )
11969 })
11970 }).unwrap();
11971 cx.run_until_parked();
11972 cx.update_editor(|editor, window, cx| {
11973 hover_popover::hover(editor, &Default::default(), window, cx)
11974 });
11975 cx.run_until_parked();
11976 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
11977}
11978
11979#[gpui::test]
11980async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
11981 init_test(cx, |_| {});
11982
11983 let mut cx = EditorTestContext::new(cx).await;
11984
11985 let diff_base = r#"
11986 use some::mod;
11987
11988 const A: u32 = 42;
11989
11990 fn main() {
11991 println!("hello");
11992
11993 println!("world");
11994 }
11995 "#
11996 .unindent();
11997
11998 // Edits are modified, removed, modified, added
11999 cx.set_state(
12000 &r#"
12001 use some::modified;
12002
12003 ˇ
12004 fn main() {
12005 println!("hello there");
12006
12007 println!("around the");
12008 println!("world");
12009 }
12010 "#
12011 .unindent(),
12012 );
12013
12014 cx.set_head_text(&diff_base);
12015 executor.run_until_parked();
12016
12017 cx.update_editor(|editor, window, cx| {
12018 //Wrap around the bottom of the buffer
12019 for _ in 0..3 {
12020 editor.go_to_next_hunk(&GoToHunk, window, cx);
12021 }
12022 });
12023
12024 cx.assert_editor_state(
12025 &r#"
12026 ˇuse some::modified;
12027
12028
12029 fn main() {
12030 println!("hello there");
12031
12032 println!("around the");
12033 println!("world");
12034 }
12035 "#
12036 .unindent(),
12037 );
12038
12039 cx.update_editor(|editor, window, cx| {
12040 //Wrap around the top of the buffer
12041 for _ in 0..2 {
12042 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12043 }
12044 });
12045
12046 cx.assert_editor_state(
12047 &r#"
12048 use some::modified;
12049
12050
12051 fn main() {
12052 ˇ println!("hello there");
12053
12054 println!("around the");
12055 println!("world");
12056 }
12057 "#
12058 .unindent(),
12059 );
12060
12061 cx.update_editor(|editor, window, cx| {
12062 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12063 });
12064
12065 cx.assert_editor_state(
12066 &r#"
12067 use some::modified;
12068
12069 ˇ
12070 fn main() {
12071 println!("hello there");
12072
12073 println!("around the");
12074 println!("world");
12075 }
12076 "#
12077 .unindent(),
12078 );
12079
12080 cx.update_editor(|editor, window, cx| {
12081 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12082 });
12083
12084 cx.assert_editor_state(
12085 &r#"
12086 ˇuse some::modified;
12087
12088
12089 fn main() {
12090 println!("hello there");
12091
12092 println!("around the");
12093 println!("world");
12094 }
12095 "#
12096 .unindent(),
12097 );
12098
12099 cx.update_editor(|editor, window, cx| {
12100 for _ in 0..2 {
12101 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12102 }
12103 });
12104
12105 cx.assert_editor_state(
12106 &r#"
12107 use some::modified;
12108
12109
12110 fn main() {
12111 ˇ println!("hello there");
12112
12113 println!("around the");
12114 println!("world");
12115 }
12116 "#
12117 .unindent(),
12118 );
12119
12120 cx.update_editor(|editor, window, cx| {
12121 editor.fold(&Fold, window, cx);
12122 });
12123
12124 cx.update_editor(|editor, window, cx| {
12125 editor.go_to_next_hunk(&GoToHunk, window, cx);
12126 });
12127
12128 cx.assert_editor_state(
12129 &r#"
12130 ˇuse some::modified;
12131
12132
12133 fn main() {
12134 println!("hello there");
12135
12136 println!("around the");
12137 println!("world");
12138 }
12139 "#
12140 .unindent(),
12141 );
12142}
12143
12144#[test]
12145fn test_split_words() {
12146 fn split(text: &str) -> Vec<&str> {
12147 split_words(text).collect()
12148 }
12149
12150 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
12151 assert_eq!(split("hello_world"), &["hello_", "world"]);
12152 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
12153 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
12154 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
12155 assert_eq!(split("helloworld"), &["helloworld"]);
12156
12157 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
12158}
12159
12160#[gpui::test]
12161async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
12162 init_test(cx, |_| {});
12163
12164 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
12165 let mut assert = |before, after| {
12166 let _state_context = cx.set_state(before);
12167 cx.run_until_parked();
12168 cx.update_editor(|editor, window, cx| {
12169 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
12170 });
12171 cx.run_until_parked();
12172 cx.assert_editor_state(after);
12173 };
12174
12175 // Outside bracket jumps to outside of matching bracket
12176 assert("console.logˇ(var);", "console.log(var)ˇ;");
12177 assert("console.log(var)ˇ;", "console.logˇ(var);");
12178
12179 // Inside bracket jumps to inside of matching bracket
12180 assert("console.log(ˇvar);", "console.log(varˇ);");
12181 assert("console.log(varˇ);", "console.log(ˇvar);");
12182
12183 // When outside a bracket and inside, favor jumping to the inside bracket
12184 assert(
12185 "console.log('foo', [1, 2, 3]ˇ);",
12186 "console.log(ˇ'foo', [1, 2, 3]);",
12187 );
12188 assert(
12189 "console.log(ˇ'foo', [1, 2, 3]);",
12190 "console.log('foo', [1, 2, 3]ˇ);",
12191 );
12192
12193 // Bias forward if two options are equally likely
12194 assert(
12195 "let result = curried_fun()ˇ();",
12196 "let result = curried_fun()()ˇ;",
12197 );
12198
12199 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
12200 assert(
12201 indoc! {"
12202 function test() {
12203 console.log('test')ˇ
12204 }"},
12205 indoc! {"
12206 function test() {
12207 console.logˇ('test')
12208 }"},
12209 );
12210}
12211
12212#[gpui::test]
12213async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
12214 init_test(cx, |_| {});
12215
12216 let fs = FakeFs::new(cx.executor());
12217 fs.insert_tree(
12218 path!("/a"),
12219 json!({
12220 "main.rs": "fn main() { let a = 5; }",
12221 "other.rs": "// Test file",
12222 }),
12223 )
12224 .await;
12225 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12226
12227 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12228 language_registry.add(Arc::new(Language::new(
12229 LanguageConfig {
12230 name: "Rust".into(),
12231 matcher: LanguageMatcher {
12232 path_suffixes: vec!["rs".to_string()],
12233 ..Default::default()
12234 },
12235 brackets: BracketPairConfig {
12236 pairs: vec![BracketPair {
12237 start: "{".to_string(),
12238 end: "}".to_string(),
12239 close: true,
12240 surround: true,
12241 newline: true,
12242 }],
12243 disabled_scopes_by_bracket_ix: Vec::new(),
12244 },
12245 ..Default::default()
12246 },
12247 Some(tree_sitter_rust::LANGUAGE.into()),
12248 )));
12249 let mut fake_servers = language_registry.register_fake_lsp(
12250 "Rust",
12251 FakeLspAdapter {
12252 capabilities: lsp::ServerCapabilities {
12253 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
12254 first_trigger_character: "{".to_string(),
12255 more_trigger_character: None,
12256 }),
12257 ..Default::default()
12258 },
12259 ..Default::default()
12260 },
12261 );
12262
12263 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12264
12265 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12266
12267 let worktree_id = workspace
12268 .update(cx, |workspace, _, cx| {
12269 workspace.project().update(cx, |project, cx| {
12270 project.worktrees(cx).next().unwrap().read(cx).id()
12271 })
12272 })
12273 .unwrap();
12274
12275 let buffer = project
12276 .update(cx, |project, cx| {
12277 project.open_local_buffer(path!("/a/main.rs"), cx)
12278 })
12279 .await
12280 .unwrap();
12281 let editor_handle = workspace
12282 .update(cx, |workspace, window, cx| {
12283 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
12284 })
12285 .unwrap()
12286 .await
12287 .unwrap()
12288 .downcast::<Editor>()
12289 .unwrap();
12290
12291 cx.executor().start_waiting();
12292 let fake_server = fake_servers.next().await.unwrap();
12293
12294 fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
12295 assert_eq!(
12296 params.text_document_position.text_document.uri,
12297 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
12298 );
12299 assert_eq!(
12300 params.text_document_position.position,
12301 lsp::Position::new(0, 21),
12302 );
12303
12304 Ok(Some(vec![lsp::TextEdit {
12305 new_text: "]".to_string(),
12306 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12307 }]))
12308 });
12309
12310 editor_handle.update_in(cx, |editor, window, cx| {
12311 window.focus(&editor.focus_handle(cx));
12312 editor.change_selections(None, window, cx, |s| {
12313 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
12314 });
12315 editor.handle_input("{", window, cx);
12316 });
12317
12318 cx.executor().run_until_parked();
12319
12320 buffer.update(cx, |buffer, _| {
12321 assert_eq!(
12322 buffer.text(),
12323 "fn main() { let a = {5}; }",
12324 "No extra braces from on type formatting should appear in the buffer"
12325 )
12326 });
12327}
12328
12329#[gpui::test]
12330async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
12331 init_test(cx, |_| {});
12332
12333 let fs = FakeFs::new(cx.executor());
12334 fs.insert_tree(
12335 path!("/a"),
12336 json!({
12337 "main.rs": "fn main() { let a = 5; }",
12338 "other.rs": "// Test file",
12339 }),
12340 )
12341 .await;
12342
12343 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12344
12345 let server_restarts = Arc::new(AtomicUsize::new(0));
12346 let closure_restarts = Arc::clone(&server_restarts);
12347 let language_server_name = "test language server";
12348 let language_name: LanguageName = "Rust".into();
12349
12350 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12351 language_registry.add(Arc::new(Language::new(
12352 LanguageConfig {
12353 name: language_name.clone(),
12354 matcher: LanguageMatcher {
12355 path_suffixes: vec!["rs".to_string()],
12356 ..Default::default()
12357 },
12358 ..Default::default()
12359 },
12360 Some(tree_sitter_rust::LANGUAGE.into()),
12361 )));
12362 let mut fake_servers = language_registry.register_fake_lsp(
12363 "Rust",
12364 FakeLspAdapter {
12365 name: language_server_name,
12366 initialization_options: Some(json!({
12367 "testOptionValue": true
12368 })),
12369 initializer: Some(Box::new(move |fake_server| {
12370 let task_restarts = Arc::clone(&closure_restarts);
12371 fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
12372 task_restarts.fetch_add(1, atomic::Ordering::Release);
12373 futures::future::ready(Ok(()))
12374 });
12375 })),
12376 ..Default::default()
12377 },
12378 );
12379
12380 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12381 let _buffer = project
12382 .update(cx, |project, cx| {
12383 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
12384 })
12385 .await
12386 .unwrap();
12387 let _fake_server = fake_servers.next().await.unwrap();
12388 update_test_language_settings(cx, |language_settings| {
12389 language_settings.languages.insert(
12390 language_name.clone(),
12391 LanguageSettingsContent {
12392 tab_size: NonZeroU32::new(8),
12393 ..Default::default()
12394 },
12395 );
12396 });
12397 cx.executor().run_until_parked();
12398 assert_eq!(
12399 server_restarts.load(atomic::Ordering::Acquire),
12400 0,
12401 "Should not restart LSP server on an unrelated change"
12402 );
12403
12404 update_test_project_settings(cx, |project_settings| {
12405 project_settings.lsp.insert(
12406 "Some other server name".into(),
12407 LspSettings {
12408 binary: None,
12409 settings: None,
12410 initialization_options: Some(json!({
12411 "some other init value": false
12412 })),
12413 },
12414 );
12415 });
12416 cx.executor().run_until_parked();
12417 assert_eq!(
12418 server_restarts.load(atomic::Ordering::Acquire),
12419 0,
12420 "Should not restart LSP server on an unrelated LSP settings change"
12421 );
12422
12423 update_test_project_settings(cx, |project_settings| {
12424 project_settings.lsp.insert(
12425 language_server_name.into(),
12426 LspSettings {
12427 binary: None,
12428 settings: None,
12429 initialization_options: Some(json!({
12430 "anotherInitValue": false
12431 })),
12432 },
12433 );
12434 });
12435 cx.executor().run_until_parked();
12436 assert_eq!(
12437 server_restarts.load(atomic::Ordering::Acquire),
12438 1,
12439 "Should restart LSP server on a related LSP settings change"
12440 );
12441
12442 update_test_project_settings(cx, |project_settings| {
12443 project_settings.lsp.insert(
12444 language_server_name.into(),
12445 LspSettings {
12446 binary: None,
12447 settings: None,
12448 initialization_options: Some(json!({
12449 "anotherInitValue": false
12450 })),
12451 },
12452 );
12453 });
12454 cx.executor().run_until_parked();
12455 assert_eq!(
12456 server_restarts.load(atomic::Ordering::Acquire),
12457 1,
12458 "Should not restart LSP server on a related LSP settings change that is the same"
12459 );
12460
12461 update_test_project_settings(cx, |project_settings| {
12462 project_settings.lsp.insert(
12463 language_server_name.into(),
12464 LspSettings {
12465 binary: None,
12466 settings: None,
12467 initialization_options: None,
12468 },
12469 );
12470 });
12471 cx.executor().run_until_parked();
12472 assert_eq!(
12473 server_restarts.load(atomic::Ordering::Acquire),
12474 2,
12475 "Should restart LSP server on another related LSP settings change"
12476 );
12477}
12478
12479#[gpui::test]
12480async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
12481 init_test(cx, |_| {});
12482
12483 let mut cx = EditorLspTestContext::new_rust(
12484 lsp::ServerCapabilities {
12485 completion_provider: Some(lsp::CompletionOptions {
12486 trigger_characters: Some(vec![".".to_string()]),
12487 resolve_provider: Some(true),
12488 ..Default::default()
12489 }),
12490 ..Default::default()
12491 },
12492 cx,
12493 )
12494 .await;
12495
12496 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
12497 cx.simulate_keystroke(".");
12498 let completion_item = lsp::CompletionItem {
12499 label: "some".into(),
12500 kind: Some(lsp::CompletionItemKind::SNIPPET),
12501 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12502 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12503 kind: lsp::MarkupKind::Markdown,
12504 value: "```rust\nSome(2)\n```".to_string(),
12505 })),
12506 deprecated: Some(false),
12507 sort_text: Some("fffffff2".to_string()),
12508 filter_text: Some("some".to_string()),
12509 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12510 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12511 range: lsp::Range {
12512 start: lsp::Position {
12513 line: 0,
12514 character: 22,
12515 },
12516 end: lsp::Position {
12517 line: 0,
12518 character: 22,
12519 },
12520 },
12521 new_text: "Some(2)".to_string(),
12522 })),
12523 additional_text_edits: Some(vec![lsp::TextEdit {
12524 range: lsp::Range {
12525 start: lsp::Position {
12526 line: 0,
12527 character: 20,
12528 },
12529 end: lsp::Position {
12530 line: 0,
12531 character: 22,
12532 },
12533 },
12534 new_text: "".to_string(),
12535 }]),
12536 ..Default::default()
12537 };
12538
12539 let closure_completion_item = completion_item.clone();
12540 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
12541 let task_completion_item = closure_completion_item.clone();
12542 async move {
12543 Ok(Some(lsp::CompletionResponse::Array(vec![
12544 task_completion_item,
12545 ])))
12546 }
12547 });
12548
12549 request.next().await;
12550
12551 cx.condition(|editor, _| editor.context_menu_visible())
12552 .await;
12553 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12554 editor
12555 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12556 .unwrap()
12557 });
12558 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
12559
12560 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
12561 let task_completion_item = completion_item.clone();
12562 async move { Ok(task_completion_item) }
12563 })
12564 .next()
12565 .await
12566 .unwrap();
12567 apply_additional_edits.await.unwrap();
12568 cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
12569}
12570
12571#[gpui::test]
12572async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
12573 init_test(cx, |_| {});
12574
12575 let mut cx = EditorLspTestContext::new_rust(
12576 lsp::ServerCapabilities {
12577 completion_provider: Some(lsp::CompletionOptions {
12578 trigger_characters: Some(vec![".".to_string()]),
12579 resolve_provider: Some(true),
12580 ..Default::default()
12581 }),
12582 ..Default::default()
12583 },
12584 cx,
12585 )
12586 .await;
12587
12588 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
12589 cx.simulate_keystroke(".");
12590
12591 let item1 = lsp::CompletionItem {
12592 label: "method id()".to_string(),
12593 filter_text: Some("id".to_string()),
12594 detail: None,
12595 documentation: None,
12596 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12597 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12598 new_text: ".id".to_string(),
12599 })),
12600 ..lsp::CompletionItem::default()
12601 };
12602
12603 let item2 = lsp::CompletionItem {
12604 label: "other".to_string(),
12605 filter_text: Some("other".to_string()),
12606 detail: None,
12607 documentation: None,
12608 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12609 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12610 new_text: ".other".to_string(),
12611 })),
12612 ..lsp::CompletionItem::default()
12613 };
12614
12615 let item1 = item1.clone();
12616 cx.handle_request::<lsp::request::Completion, _, _>({
12617 let item1 = item1.clone();
12618 move |_, _, _| {
12619 let item1 = item1.clone();
12620 let item2 = item2.clone();
12621 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
12622 }
12623 })
12624 .next()
12625 .await;
12626
12627 cx.condition(|editor, _| editor.context_menu_visible())
12628 .await;
12629 cx.update_editor(|editor, _, _| {
12630 let context_menu = editor.context_menu.borrow_mut();
12631 let context_menu = context_menu
12632 .as_ref()
12633 .expect("Should have the context menu deployed");
12634 match context_menu {
12635 CodeContextMenu::Completions(completions_menu) => {
12636 let completions = completions_menu.completions.borrow_mut();
12637 assert_eq!(
12638 completions
12639 .iter()
12640 .map(|completion| &completion.label.text)
12641 .collect::<Vec<_>>(),
12642 vec!["method id()", "other"]
12643 )
12644 }
12645 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12646 }
12647 });
12648
12649 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>({
12650 let item1 = item1.clone();
12651 move |_, item_to_resolve, _| {
12652 let item1 = item1.clone();
12653 async move {
12654 if item1 == item_to_resolve {
12655 Ok(lsp::CompletionItem {
12656 label: "method id()".to_string(),
12657 filter_text: Some("id".to_string()),
12658 detail: Some("Now resolved!".to_string()),
12659 documentation: Some(lsp::Documentation::String("Docs".to_string())),
12660 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12661 range: lsp::Range::new(
12662 lsp::Position::new(0, 22),
12663 lsp::Position::new(0, 22),
12664 ),
12665 new_text: ".id".to_string(),
12666 })),
12667 ..lsp::CompletionItem::default()
12668 })
12669 } else {
12670 Ok(item_to_resolve)
12671 }
12672 }
12673 }
12674 })
12675 .next()
12676 .await
12677 .unwrap();
12678 cx.run_until_parked();
12679
12680 cx.update_editor(|editor, window, cx| {
12681 editor.context_menu_next(&Default::default(), window, cx);
12682 });
12683
12684 cx.update_editor(|editor, _, _| {
12685 let context_menu = editor.context_menu.borrow_mut();
12686 let context_menu = context_menu
12687 .as_ref()
12688 .expect("Should have the context menu deployed");
12689 match context_menu {
12690 CodeContextMenu::Completions(completions_menu) => {
12691 let completions = completions_menu.completions.borrow_mut();
12692 assert_eq!(
12693 completions
12694 .iter()
12695 .map(|completion| &completion.label.text)
12696 .collect::<Vec<_>>(),
12697 vec!["method id() Now resolved!", "other"],
12698 "Should update first completion label, but not second as the filter text did not match."
12699 );
12700 }
12701 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12702 }
12703 });
12704}
12705
12706#[gpui::test]
12707async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
12708 init_test(cx, |_| {});
12709
12710 let mut cx = EditorLspTestContext::new_rust(
12711 lsp::ServerCapabilities {
12712 completion_provider: Some(lsp::CompletionOptions {
12713 trigger_characters: Some(vec![".".to_string()]),
12714 resolve_provider: Some(true),
12715 ..Default::default()
12716 }),
12717 ..Default::default()
12718 },
12719 cx,
12720 )
12721 .await;
12722
12723 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
12724 cx.simulate_keystroke(".");
12725
12726 let unresolved_item_1 = lsp::CompletionItem {
12727 label: "id".to_string(),
12728 filter_text: Some("id".to_string()),
12729 detail: None,
12730 documentation: None,
12731 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12732 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12733 new_text: ".id".to_string(),
12734 })),
12735 ..lsp::CompletionItem::default()
12736 };
12737 let resolved_item_1 = lsp::CompletionItem {
12738 additional_text_edits: Some(vec![lsp::TextEdit {
12739 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
12740 new_text: "!!".to_string(),
12741 }]),
12742 ..unresolved_item_1.clone()
12743 };
12744 let unresolved_item_2 = lsp::CompletionItem {
12745 label: "other".to_string(),
12746 filter_text: Some("other".to_string()),
12747 detail: None,
12748 documentation: None,
12749 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12750 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12751 new_text: ".other".to_string(),
12752 })),
12753 ..lsp::CompletionItem::default()
12754 };
12755 let resolved_item_2 = lsp::CompletionItem {
12756 additional_text_edits: Some(vec![lsp::TextEdit {
12757 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
12758 new_text: "??".to_string(),
12759 }]),
12760 ..unresolved_item_2.clone()
12761 };
12762
12763 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
12764 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
12765 cx.lsp
12766 .server
12767 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
12768 let unresolved_item_1 = unresolved_item_1.clone();
12769 let resolved_item_1 = resolved_item_1.clone();
12770 let unresolved_item_2 = unresolved_item_2.clone();
12771 let resolved_item_2 = resolved_item_2.clone();
12772 let resolve_requests_1 = resolve_requests_1.clone();
12773 let resolve_requests_2 = resolve_requests_2.clone();
12774 move |unresolved_request, _| {
12775 let unresolved_item_1 = unresolved_item_1.clone();
12776 let resolved_item_1 = resolved_item_1.clone();
12777 let unresolved_item_2 = unresolved_item_2.clone();
12778 let resolved_item_2 = resolved_item_2.clone();
12779 let resolve_requests_1 = resolve_requests_1.clone();
12780 let resolve_requests_2 = resolve_requests_2.clone();
12781 async move {
12782 if unresolved_request == unresolved_item_1 {
12783 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
12784 Ok(resolved_item_1.clone())
12785 } else if unresolved_request == unresolved_item_2 {
12786 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
12787 Ok(resolved_item_2.clone())
12788 } else {
12789 panic!("Unexpected completion item {unresolved_request:?}")
12790 }
12791 }
12792 }
12793 })
12794 .detach();
12795
12796 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
12797 let unresolved_item_1 = unresolved_item_1.clone();
12798 let unresolved_item_2 = unresolved_item_2.clone();
12799 async move {
12800 Ok(Some(lsp::CompletionResponse::Array(vec![
12801 unresolved_item_1,
12802 unresolved_item_2,
12803 ])))
12804 }
12805 })
12806 .next()
12807 .await;
12808
12809 cx.condition(|editor, _| editor.context_menu_visible())
12810 .await;
12811 cx.update_editor(|editor, _, _| {
12812 let context_menu = editor.context_menu.borrow_mut();
12813 let context_menu = context_menu
12814 .as_ref()
12815 .expect("Should have the context menu deployed");
12816 match context_menu {
12817 CodeContextMenu::Completions(completions_menu) => {
12818 let completions = completions_menu.completions.borrow_mut();
12819 assert_eq!(
12820 completions
12821 .iter()
12822 .map(|completion| &completion.label.text)
12823 .collect::<Vec<_>>(),
12824 vec!["id", "other"]
12825 )
12826 }
12827 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12828 }
12829 });
12830 cx.run_until_parked();
12831
12832 cx.update_editor(|editor, window, cx| {
12833 editor.context_menu_next(&ContextMenuNext, window, cx);
12834 });
12835 cx.run_until_parked();
12836 cx.update_editor(|editor, window, cx| {
12837 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
12838 });
12839 cx.run_until_parked();
12840 cx.update_editor(|editor, window, cx| {
12841 editor.context_menu_next(&ContextMenuNext, window, cx);
12842 });
12843 cx.run_until_parked();
12844 cx.update_editor(|editor, window, cx| {
12845 editor
12846 .compose_completion(&ComposeCompletion::default(), window, cx)
12847 .expect("No task returned")
12848 })
12849 .await
12850 .expect("Completion failed");
12851 cx.run_until_parked();
12852
12853 cx.update_editor(|editor, _, cx| {
12854 assert_eq!(
12855 resolve_requests_1.load(atomic::Ordering::Acquire),
12856 1,
12857 "Should always resolve once despite multiple selections"
12858 );
12859 assert_eq!(
12860 resolve_requests_2.load(atomic::Ordering::Acquire),
12861 1,
12862 "Should always resolve once after multiple selections and applying the completion"
12863 );
12864 assert_eq!(
12865 editor.text(cx),
12866 "fn main() { let a = ??.other; }",
12867 "Should use resolved data when applying the completion"
12868 );
12869 });
12870}
12871
12872#[gpui::test]
12873async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
12874 init_test(cx, |_| {});
12875
12876 let item_0 = lsp::CompletionItem {
12877 label: "abs".into(),
12878 insert_text: Some("abs".into()),
12879 data: Some(json!({ "very": "special"})),
12880 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
12881 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12882 lsp::InsertReplaceEdit {
12883 new_text: "abs".to_string(),
12884 insert: lsp::Range::default(),
12885 replace: lsp::Range::default(),
12886 },
12887 )),
12888 ..lsp::CompletionItem::default()
12889 };
12890 let items = iter::once(item_0.clone())
12891 .chain((11..51).map(|i| lsp::CompletionItem {
12892 label: format!("item_{}", i),
12893 insert_text: Some(format!("item_{}", i)),
12894 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
12895 ..lsp::CompletionItem::default()
12896 }))
12897 .collect::<Vec<_>>();
12898
12899 let default_commit_characters = vec!["?".to_string()];
12900 let default_data = json!({ "default": "data"});
12901 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
12902 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
12903 let default_edit_range = lsp::Range {
12904 start: lsp::Position {
12905 line: 0,
12906 character: 5,
12907 },
12908 end: lsp::Position {
12909 line: 0,
12910 character: 5,
12911 },
12912 };
12913
12914 let mut cx = EditorLspTestContext::new_rust(
12915 lsp::ServerCapabilities {
12916 completion_provider: Some(lsp::CompletionOptions {
12917 trigger_characters: Some(vec![".".to_string()]),
12918 resolve_provider: Some(true),
12919 ..Default::default()
12920 }),
12921 ..Default::default()
12922 },
12923 cx,
12924 )
12925 .await;
12926
12927 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
12928 cx.simulate_keystroke(".");
12929
12930 let completion_data = default_data.clone();
12931 let completion_characters = default_commit_characters.clone();
12932 let completion_items = items.clone();
12933 cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
12934 let default_data = completion_data.clone();
12935 let default_commit_characters = completion_characters.clone();
12936 let items = completion_items.clone();
12937 async move {
12938 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
12939 items,
12940 item_defaults: Some(lsp::CompletionListItemDefaults {
12941 data: Some(default_data.clone()),
12942 commit_characters: Some(default_commit_characters.clone()),
12943 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
12944 default_edit_range,
12945 )),
12946 insert_text_format: Some(default_insert_text_format),
12947 insert_text_mode: Some(default_insert_text_mode),
12948 }),
12949 ..lsp::CompletionList::default()
12950 })))
12951 }
12952 })
12953 .next()
12954 .await;
12955
12956 let resolved_items = Arc::new(Mutex::new(Vec::new()));
12957 cx.lsp
12958 .server
12959 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
12960 let closure_resolved_items = resolved_items.clone();
12961 move |item_to_resolve, _| {
12962 let closure_resolved_items = closure_resolved_items.clone();
12963 async move {
12964 closure_resolved_items.lock().push(item_to_resolve.clone());
12965 Ok(item_to_resolve)
12966 }
12967 }
12968 })
12969 .detach();
12970
12971 cx.condition(|editor, _| editor.context_menu_visible())
12972 .await;
12973 cx.run_until_parked();
12974 cx.update_editor(|editor, _, _| {
12975 let menu = editor.context_menu.borrow_mut();
12976 match menu.as_ref().expect("should have the completions menu") {
12977 CodeContextMenu::Completions(completions_menu) => {
12978 assert_eq!(
12979 completions_menu
12980 .entries
12981 .borrow()
12982 .iter()
12983 .map(|mat| mat.string.clone())
12984 .collect::<Vec<String>>(),
12985 items
12986 .iter()
12987 .map(|completion| completion.label.clone())
12988 .collect::<Vec<String>>()
12989 );
12990 }
12991 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
12992 }
12993 });
12994 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
12995 // with 4 from the end.
12996 assert_eq!(
12997 *resolved_items.lock(),
12998 [&items[0..16], &items[items.len() - 4..items.len()]]
12999 .concat()
13000 .iter()
13001 .cloned()
13002 .map(|mut item| {
13003 if item.data.is_none() {
13004 item.data = Some(default_data.clone());
13005 }
13006 item
13007 })
13008 .collect::<Vec<lsp::CompletionItem>>(),
13009 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
13010 );
13011 resolved_items.lock().clear();
13012
13013 cx.update_editor(|editor, window, cx| {
13014 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13015 });
13016 cx.run_until_parked();
13017 // Completions that have already been resolved are skipped.
13018 assert_eq!(
13019 *resolved_items.lock(),
13020 items[items.len() - 16..items.len() - 4]
13021 .iter()
13022 .cloned()
13023 .map(|mut item| {
13024 if item.data.is_none() {
13025 item.data = Some(default_data.clone());
13026 }
13027 item
13028 })
13029 .collect::<Vec<lsp::CompletionItem>>()
13030 );
13031 resolved_items.lock().clear();
13032}
13033
13034#[gpui::test]
13035async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
13036 init_test(cx, |_| {});
13037
13038 let mut cx = EditorLspTestContext::new(
13039 Language::new(
13040 LanguageConfig {
13041 matcher: LanguageMatcher {
13042 path_suffixes: vec!["jsx".into()],
13043 ..Default::default()
13044 },
13045 overrides: [(
13046 "element".into(),
13047 LanguageConfigOverride {
13048 completion_query_characters: Override::Set(['-'].into_iter().collect()),
13049 ..Default::default()
13050 },
13051 )]
13052 .into_iter()
13053 .collect(),
13054 ..Default::default()
13055 },
13056 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13057 )
13058 .with_override_query("(jsx_self_closing_element) @element")
13059 .unwrap(),
13060 lsp::ServerCapabilities {
13061 completion_provider: Some(lsp::CompletionOptions {
13062 trigger_characters: Some(vec![":".to_string()]),
13063 ..Default::default()
13064 }),
13065 ..Default::default()
13066 },
13067 cx,
13068 )
13069 .await;
13070
13071 cx.lsp
13072 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
13073 Ok(Some(lsp::CompletionResponse::Array(vec![
13074 lsp::CompletionItem {
13075 label: "bg-blue".into(),
13076 ..Default::default()
13077 },
13078 lsp::CompletionItem {
13079 label: "bg-red".into(),
13080 ..Default::default()
13081 },
13082 lsp::CompletionItem {
13083 label: "bg-yellow".into(),
13084 ..Default::default()
13085 },
13086 ])))
13087 });
13088
13089 cx.set_state(r#"<p class="bgˇ" />"#);
13090
13091 // Trigger completion when typing a dash, because the dash is an extra
13092 // word character in the 'element' scope, which contains the cursor.
13093 cx.simulate_keystroke("-");
13094 cx.executor().run_until_parked();
13095 cx.update_editor(|editor, _, _| {
13096 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13097 {
13098 assert_eq!(
13099 completion_menu_entries(&menu),
13100 &["bg-red", "bg-blue", "bg-yellow"]
13101 );
13102 } else {
13103 panic!("expected completion menu to be open");
13104 }
13105 });
13106
13107 cx.simulate_keystroke("l");
13108 cx.executor().run_until_parked();
13109 cx.update_editor(|editor, _, _| {
13110 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13111 {
13112 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
13113 } else {
13114 panic!("expected completion menu to be open");
13115 }
13116 });
13117
13118 // When filtering completions, consider the character after the '-' to
13119 // be the start of a subword.
13120 cx.set_state(r#"<p class="yelˇ" />"#);
13121 cx.simulate_keystroke("l");
13122 cx.executor().run_until_parked();
13123 cx.update_editor(|editor, _, _| {
13124 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13125 {
13126 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
13127 } else {
13128 panic!("expected completion menu to be open");
13129 }
13130 });
13131}
13132
13133fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
13134 let entries = menu.entries.borrow();
13135 entries.iter().map(|mat| mat.string.clone()).collect()
13136}
13137
13138#[gpui::test]
13139async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
13140 init_test(cx, |settings| {
13141 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
13142 FormatterList(vec![Formatter::Prettier].into()),
13143 ))
13144 });
13145
13146 let fs = FakeFs::new(cx.executor());
13147 fs.insert_file(path!("/file.ts"), Default::default()).await;
13148
13149 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
13150 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13151
13152 language_registry.add(Arc::new(Language::new(
13153 LanguageConfig {
13154 name: "TypeScript".into(),
13155 matcher: LanguageMatcher {
13156 path_suffixes: vec!["ts".to_string()],
13157 ..Default::default()
13158 },
13159 ..Default::default()
13160 },
13161 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13162 )));
13163 update_test_language_settings(cx, |settings| {
13164 settings.defaults.prettier = Some(PrettierSettings {
13165 allowed: true,
13166 ..PrettierSettings::default()
13167 });
13168 });
13169
13170 let test_plugin = "test_plugin";
13171 let _ = language_registry.register_fake_lsp(
13172 "TypeScript",
13173 FakeLspAdapter {
13174 prettier_plugins: vec![test_plugin],
13175 ..Default::default()
13176 },
13177 );
13178
13179 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
13180 let buffer = project
13181 .update(cx, |project, cx| {
13182 project.open_local_buffer(path!("/file.ts"), cx)
13183 })
13184 .await
13185 .unwrap();
13186
13187 let buffer_text = "one\ntwo\nthree\n";
13188 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13189 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13190 editor.update_in(cx, |editor, window, cx| {
13191 editor.set_text(buffer_text, window, cx)
13192 });
13193
13194 editor
13195 .update_in(cx, |editor, window, cx| {
13196 editor.perform_format(
13197 project.clone(),
13198 FormatTrigger::Manual,
13199 FormatTarget::Buffers,
13200 window,
13201 cx,
13202 )
13203 })
13204 .unwrap()
13205 .await;
13206 assert_eq!(
13207 editor.update(cx, |editor, cx| editor.text(cx)),
13208 buffer_text.to_string() + prettier_format_suffix,
13209 "Test prettier formatting was not applied to the original buffer text",
13210 );
13211
13212 update_test_language_settings(cx, |settings| {
13213 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
13214 });
13215 let format = editor.update_in(cx, |editor, window, cx| {
13216 editor.perform_format(
13217 project.clone(),
13218 FormatTrigger::Manual,
13219 FormatTarget::Buffers,
13220 window,
13221 cx,
13222 )
13223 });
13224 format.await.unwrap();
13225 assert_eq!(
13226 editor.update(cx, |editor, cx| editor.text(cx)),
13227 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
13228 "Autoformatting (via test prettier) was not applied to the original buffer text",
13229 );
13230}
13231
13232#[gpui::test]
13233async fn test_addition_reverts(cx: &mut TestAppContext) {
13234 init_test(cx, |_| {});
13235 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13236 let base_text = indoc! {r#"
13237 struct Row;
13238 struct Row1;
13239 struct Row2;
13240
13241 struct Row4;
13242 struct Row5;
13243 struct Row6;
13244
13245 struct Row8;
13246 struct Row9;
13247 struct Row10;"#};
13248
13249 // When addition hunks are not adjacent to carets, no hunk revert is performed
13250 assert_hunk_revert(
13251 indoc! {r#"struct Row;
13252 struct Row1;
13253 struct Row1.1;
13254 struct Row1.2;
13255 struct Row2;ˇ
13256
13257 struct Row4;
13258 struct Row5;
13259 struct Row6;
13260
13261 struct Row8;
13262 ˇstruct Row9;
13263 struct Row9.1;
13264 struct Row9.2;
13265 struct Row9.3;
13266 struct Row10;"#},
13267 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13268 indoc! {r#"struct Row;
13269 struct Row1;
13270 struct Row1.1;
13271 struct Row1.2;
13272 struct Row2;ˇ
13273
13274 struct Row4;
13275 struct Row5;
13276 struct Row6;
13277
13278 struct Row8;
13279 ˇstruct Row9;
13280 struct Row9.1;
13281 struct Row9.2;
13282 struct Row9.3;
13283 struct Row10;"#},
13284 base_text,
13285 &mut cx,
13286 );
13287 // Same for selections
13288 assert_hunk_revert(
13289 indoc! {r#"struct Row;
13290 struct Row1;
13291 struct Row2;
13292 struct Row2.1;
13293 struct Row2.2;
13294 «ˇ
13295 struct Row4;
13296 struct» Row5;
13297 «struct Row6;
13298 ˇ»
13299 struct Row9.1;
13300 struct Row9.2;
13301 struct Row9.3;
13302 struct Row8;
13303 struct Row9;
13304 struct Row10;"#},
13305 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13306 indoc! {r#"struct Row;
13307 struct Row1;
13308 struct Row2;
13309 struct Row2.1;
13310 struct Row2.2;
13311 «ˇ
13312 struct Row4;
13313 struct» Row5;
13314 «struct Row6;
13315 ˇ»
13316 struct Row9.1;
13317 struct Row9.2;
13318 struct Row9.3;
13319 struct Row8;
13320 struct Row9;
13321 struct Row10;"#},
13322 base_text,
13323 &mut cx,
13324 );
13325
13326 // When carets and selections intersect the addition hunks, those are reverted.
13327 // Adjacent carets got merged.
13328 assert_hunk_revert(
13329 indoc! {r#"struct Row;
13330 ˇ// something on the top
13331 struct Row1;
13332 struct Row2;
13333 struct Roˇw3.1;
13334 struct Row2.2;
13335 struct Row2.3;ˇ
13336
13337 struct Row4;
13338 struct ˇRow5.1;
13339 struct Row5.2;
13340 struct «Rowˇ»5.3;
13341 struct Row5;
13342 struct Row6;
13343 ˇ
13344 struct Row9.1;
13345 struct «Rowˇ»9.2;
13346 struct «ˇRow»9.3;
13347 struct Row8;
13348 struct Row9;
13349 «ˇ// something on bottom»
13350 struct Row10;"#},
13351 vec![
13352 DiffHunkStatusKind::Added,
13353 DiffHunkStatusKind::Added,
13354 DiffHunkStatusKind::Added,
13355 DiffHunkStatusKind::Added,
13356 DiffHunkStatusKind::Added,
13357 ],
13358 indoc! {r#"struct Row;
13359 ˇstruct Row1;
13360 struct Row2;
13361 ˇ
13362 struct Row4;
13363 ˇstruct Row5;
13364 struct Row6;
13365 ˇ
13366 ˇstruct Row8;
13367 struct Row9;
13368 ˇstruct Row10;"#},
13369 base_text,
13370 &mut cx,
13371 );
13372}
13373
13374#[gpui::test]
13375async fn test_modification_reverts(cx: &mut TestAppContext) {
13376 init_test(cx, |_| {});
13377 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13378 let base_text = indoc! {r#"
13379 struct Row;
13380 struct Row1;
13381 struct Row2;
13382
13383 struct Row4;
13384 struct Row5;
13385 struct Row6;
13386
13387 struct Row8;
13388 struct Row9;
13389 struct Row10;"#};
13390
13391 // Modification hunks behave the same as the addition ones.
13392 assert_hunk_revert(
13393 indoc! {r#"struct Row;
13394 struct Row1;
13395 struct Row33;
13396 ˇ
13397 struct Row4;
13398 struct Row5;
13399 struct Row6;
13400 ˇ
13401 struct Row99;
13402 struct Row9;
13403 struct Row10;"#},
13404 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13405 indoc! {r#"struct Row;
13406 struct Row1;
13407 struct Row33;
13408 ˇ
13409 struct Row4;
13410 struct Row5;
13411 struct Row6;
13412 ˇ
13413 struct Row99;
13414 struct Row9;
13415 struct Row10;"#},
13416 base_text,
13417 &mut cx,
13418 );
13419 assert_hunk_revert(
13420 indoc! {r#"struct Row;
13421 struct Row1;
13422 struct Row33;
13423 «ˇ
13424 struct Row4;
13425 struct» Row5;
13426 «struct Row6;
13427 ˇ»
13428 struct Row99;
13429 struct Row9;
13430 struct Row10;"#},
13431 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13432 indoc! {r#"struct Row;
13433 struct Row1;
13434 struct Row33;
13435 «ˇ
13436 struct Row4;
13437 struct» Row5;
13438 «struct Row6;
13439 ˇ»
13440 struct Row99;
13441 struct Row9;
13442 struct Row10;"#},
13443 base_text,
13444 &mut cx,
13445 );
13446
13447 assert_hunk_revert(
13448 indoc! {r#"ˇstruct Row1.1;
13449 struct Row1;
13450 «ˇstr»uct Row22;
13451
13452 struct ˇRow44;
13453 struct Row5;
13454 struct «Rˇ»ow66;ˇ
13455
13456 «struˇ»ct Row88;
13457 struct Row9;
13458 struct Row1011;ˇ"#},
13459 vec![
13460 DiffHunkStatusKind::Modified,
13461 DiffHunkStatusKind::Modified,
13462 DiffHunkStatusKind::Modified,
13463 DiffHunkStatusKind::Modified,
13464 DiffHunkStatusKind::Modified,
13465 DiffHunkStatusKind::Modified,
13466 ],
13467 indoc! {r#"struct Row;
13468 ˇstruct Row1;
13469 struct Row2;
13470 ˇ
13471 struct Row4;
13472 ˇstruct Row5;
13473 struct Row6;
13474 ˇ
13475 struct Row8;
13476 ˇstruct Row9;
13477 struct Row10;ˇ"#},
13478 base_text,
13479 &mut cx,
13480 );
13481}
13482
13483#[gpui::test]
13484async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
13485 init_test(cx, |_| {});
13486 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13487 let base_text = indoc! {r#"
13488 one
13489
13490 two
13491 three
13492 "#};
13493
13494 cx.set_head_text(base_text);
13495 cx.set_state("\nˇ\n");
13496 cx.executor().run_until_parked();
13497 cx.update_editor(|editor, _window, cx| {
13498 editor.expand_selected_diff_hunks(cx);
13499 });
13500 cx.executor().run_until_parked();
13501 cx.update_editor(|editor, window, cx| {
13502 editor.backspace(&Default::default(), window, cx);
13503 });
13504 cx.run_until_parked();
13505 cx.assert_state_with_diff(
13506 indoc! {r#"
13507
13508 - two
13509 - threeˇ
13510 +
13511 "#}
13512 .to_string(),
13513 );
13514}
13515
13516#[gpui::test]
13517async fn test_deletion_reverts(cx: &mut TestAppContext) {
13518 init_test(cx, |_| {});
13519 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13520 let base_text = indoc! {r#"struct Row;
13521struct Row1;
13522struct Row2;
13523
13524struct Row4;
13525struct Row5;
13526struct Row6;
13527
13528struct Row8;
13529struct Row9;
13530struct Row10;"#};
13531
13532 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
13533 assert_hunk_revert(
13534 indoc! {r#"struct Row;
13535 struct Row2;
13536
13537 ˇstruct Row4;
13538 struct Row5;
13539 struct Row6;
13540 ˇ
13541 struct Row8;
13542 struct Row10;"#},
13543 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13544 indoc! {r#"struct Row;
13545 struct Row2;
13546
13547 ˇstruct Row4;
13548 struct Row5;
13549 struct Row6;
13550 ˇ
13551 struct Row8;
13552 struct Row10;"#},
13553 base_text,
13554 &mut cx,
13555 );
13556 assert_hunk_revert(
13557 indoc! {r#"struct Row;
13558 struct Row2;
13559
13560 «ˇstruct Row4;
13561 struct» Row5;
13562 «struct Row6;
13563 ˇ»
13564 struct Row8;
13565 struct Row10;"#},
13566 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13567 indoc! {r#"struct Row;
13568 struct Row2;
13569
13570 «ˇstruct Row4;
13571 struct» Row5;
13572 «struct Row6;
13573 ˇ»
13574 struct Row8;
13575 struct Row10;"#},
13576 base_text,
13577 &mut cx,
13578 );
13579
13580 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
13581 assert_hunk_revert(
13582 indoc! {r#"struct Row;
13583 ˇstruct Row2;
13584
13585 struct Row4;
13586 struct Row5;
13587 struct Row6;
13588
13589 struct Row8;ˇ
13590 struct Row10;"#},
13591 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13592 indoc! {r#"struct Row;
13593 struct Row1;
13594 ˇstruct Row2;
13595
13596 struct Row4;
13597 struct Row5;
13598 struct Row6;
13599
13600 struct Row8;ˇ
13601 struct Row9;
13602 struct Row10;"#},
13603 base_text,
13604 &mut cx,
13605 );
13606 assert_hunk_revert(
13607 indoc! {r#"struct Row;
13608 struct Row2«ˇ;
13609 struct Row4;
13610 struct» Row5;
13611 «struct Row6;
13612
13613 struct Row8;ˇ»
13614 struct Row10;"#},
13615 vec![
13616 DiffHunkStatusKind::Deleted,
13617 DiffHunkStatusKind::Deleted,
13618 DiffHunkStatusKind::Deleted,
13619 ],
13620 indoc! {r#"struct Row;
13621 struct Row1;
13622 struct Row2«ˇ;
13623
13624 struct Row4;
13625 struct» Row5;
13626 «struct Row6;
13627
13628 struct Row8;ˇ»
13629 struct Row9;
13630 struct Row10;"#},
13631 base_text,
13632 &mut cx,
13633 );
13634}
13635
13636#[gpui::test]
13637async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
13638 init_test(cx, |_| {});
13639
13640 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
13641 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
13642 let base_text_3 =
13643 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
13644
13645 let text_1 = edit_first_char_of_every_line(base_text_1);
13646 let text_2 = edit_first_char_of_every_line(base_text_2);
13647 let text_3 = edit_first_char_of_every_line(base_text_3);
13648
13649 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
13650 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
13651 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
13652
13653 let multibuffer = cx.new(|cx| {
13654 let mut multibuffer = MultiBuffer::new(ReadWrite);
13655 multibuffer.push_excerpts(
13656 buffer_1.clone(),
13657 [
13658 ExcerptRange {
13659 context: Point::new(0, 0)..Point::new(3, 0),
13660 primary: None,
13661 },
13662 ExcerptRange {
13663 context: Point::new(5, 0)..Point::new(7, 0),
13664 primary: None,
13665 },
13666 ExcerptRange {
13667 context: Point::new(9, 0)..Point::new(10, 4),
13668 primary: None,
13669 },
13670 ],
13671 cx,
13672 );
13673 multibuffer.push_excerpts(
13674 buffer_2.clone(),
13675 [
13676 ExcerptRange {
13677 context: Point::new(0, 0)..Point::new(3, 0),
13678 primary: None,
13679 },
13680 ExcerptRange {
13681 context: Point::new(5, 0)..Point::new(7, 0),
13682 primary: None,
13683 },
13684 ExcerptRange {
13685 context: Point::new(9, 0)..Point::new(10, 4),
13686 primary: None,
13687 },
13688 ],
13689 cx,
13690 );
13691 multibuffer.push_excerpts(
13692 buffer_3.clone(),
13693 [
13694 ExcerptRange {
13695 context: Point::new(0, 0)..Point::new(3, 0),
13696 primary: None,
13697 },
13698 ExcerptRange {
13699 context: Point::new(5, 0)..Point::new(7, 0),
13700 primary: None,
13701 },
13702 ExcerptRange {
13703 context: Point::new(9, 0)..Point::new(10, 4),
13704 primary: None,
13705 },
13706 ],
13707 cx,
13708 );
13709 multibuffer
13710 });
13711
13712 let fs = FakeFs::new(cx.executor());
13713 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13714 let (editor, cx) = cx
13715 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
13716 editor.update_in(cx, |editor, _window, cx| {
13717 for (buffer, diff_base) in [
13718 (buffer_1.clone(), base_text_1),
13719 (buffer_2.clone(), base_text_2),
13720 (buffer_3.clone(), base_text_3),
13721 ] {
13722 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
13723 editor
13724 .buffer
13725 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
13726 }
13727 });
13728 cx.executor().run_until_parked();
13729
13730 editor.update_in(cx, |editor, window, cx| {
13731 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}");
13732 editor.select_all(&SelectAll, window, cx);
13733 editor.git_restore(&Default::default(), window, cx);
13734 });
13735 cx.executor().run_until_parked();
13736
13737 // When all ranges are selected, all buffer hunks are reverted.
13738 editor.update(cx, |editor, cx| {
13739 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");
13740 });
13741 buffer_1.update(cx, |buffer, _| {
13742 assert_eq!(buffer.text(), base_text_1);
13743 });
13744 buffer_2.update(cx, |buffer, _| {
13745 assert_eq!(buffer.text(), base_text_2);
13746 });
13747 buffer_3.update(cx, |buffer, _| {
13748 assert_eq!(buffer.text(), base_text_3);
13749 });
13750
13751 editor.update_in(cx, |editor, window, cx| {
13752 editor.undo(&Default::default(), window, cx);
13753 });
13754
13755 editor.update_in(cx, |editor, window, cx| {
13756 editor.change_selections(None, window, cx, |s| {
13757 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
13758 });
13759 editor.git_restore(&Default::default(), window, cx);
13760 });
13761
13762 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
13763 // but not affect buffer_2 and its related excerpts.
13764 editor.update(cx, |editor, cx| {
13765 assert_eq!(
13766 editor.text(cx),
13767 "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}"
13768 );
13769 });
13770 buffer_1.update(cx, |buffer, _| {
13771 assert_eq!(buffer.text(), base_text_1);
13772 });
13773 buffer_2.update(cx, |buffer, _| {
13774 assert_eq!(
13775 buffer.text(),
13776 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
13777 );
13778 });
13779 buffer_3.update(cx, |buffer, _| {
13780 assert_eq!(
13781 buffer.text(),
13782 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
13783 );
13784 });
13785
13786 fn edit_first_char_of_every_line(text: &str) -> String {
13787 text.split('\n')
13788 .map(|line| format!("X{}", &line[1..]))
13789 .collect::<Vec<_>>()
13790 .join("\n")
13791 }
13792}
13793
13794#[gpui::test]
13795async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
13796 init_test(cx, |_| {});
13797
13798 let cols = 4;
13799 let rows = 10;
13800 let sample_text_1 = sample_text(rows, cols, 'a');
13801 assert_eq!(
13802 sample_text_1,
13803 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
13804 );
13805 let sample_text_2 = sample_text(rows, cols, 'l');
13806 assert_eq!(
13807 sample_text_2,
13808 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
13809 );
13810 let sample_text_3 = sample_text(rows, cols, 'v');
13811 assert_eq!(
13812 sample_text_3,
13813 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
13814 );
13815
13816 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
13817 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
13818 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
13819
13820 let multi_buffer = cx.new(|cx| {
13821 let mut multibuffer = MultiBuffer::new(ReadWrite);
13822 multibuffer.push_excerpts(
13823 buffer_1.clone(),
13824 [
13825 ExcerptRange {
13826 context: Point::new(0, 0)..Point::new(3, 0),
13827 primary: None,
13828 },
13829 ExcerptRange {
13830 context: Point::new(5, 0)..Point::new(7, 0),
13831 primary: None,
13832 },
13833 ExcerptRange {
13834 context: Point::new(9, 0)..Point::new(10, 4),
13835 primary: None,
13836 },
13837 ],
13838 cx,
13839 );
13840 multibuffer.push_excerpts(
13841 buffer_2.clone(),
13842 [
13843 ExcerptRange {
13844 context: Point::new(0, 0)..Point::new(3, 0),
13845 primary: None,
13846 },
13847 ExcerptRange {
13848 context: Point::new(5, 0)..Point::new(7, 0),
13849 primary: None,
13850 },
13851 ExcerptRange {
13852 context: Point::new(9, 0)..Point::new(10, 4),
13853 primary: None,
13854 },
13855 ],
13856 cx,
13857 );
13858 multibuffer.push_excerpts(
13859 buffer_3.clone(),
13860 [
13861 ExcerptRange {
13862 context: Point::new(0, 0)..Point::new(3, 0),
13863 primary: None,
13864 },
13865 ExcerptRange {
13866 context: Point::new(5, 0)..Point::new(7, 0),
13867 primary: None,
13868 },
13869 ExcerptRange {
13870 context: Point::new(9, 0)..Point::new(10, 4),
13871 primary: None,
13872 },
13873 ],
13874 cx,
13875 );
13876 multibuffer
13877 });
13878
13879 let fs = FakeFs::new(cx.executor());
13880 fs.insert_tree(
13881 "/a",
13882 json!({
13883 "main.rs": sample_text_1,
13884 "other.rs": sample_text_2,
13885 "lib.rs": sample_text_3,
13886 }),
13887 )
13888 .await;
13889 let project = Project::test(fs, ["/a".as_ref()], cx).await;
13890 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13891 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13892 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
13893 Editor::new(
13894 EditorMode::Full,
13895 multi_buffer,
13896 Some(project.clone()),
13897 window,
13898 cx,
13899 )
13900 });
13901 let multibuffer_item_id = workspace
13902 .update(cx, |workspace, window, cx| {
13903 assert!(
13904 workspace.active_item(cx).is_none(),
13905 "active item should be None before the first item is added"
13906 );
13907 workspace.add_item_to_active_pane(
13908 Box::new(multi_buffer_editor.clone()),
13909 None,
13910 true,
13911 window,
13912 cx,
13913 );
13914 let active_item = workspace
13915 .active_item(cx)
13916 .expect("should have an active item after adding the multi buffer");
13917 assert!(
13918 !active_item.is_singleton(cx),
13919 "A multi buffer was expected to active after adding"
13920 );
13921 active_item.item_id()
13922 })
13923 .unwrap();
13924 cx.executor().run_until_parked();
13925
13926 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13927 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
13928 s.select_ranges(Some(1..2))
13929 });
13930 editor.open_excerpts(&OpenExcerpts, window, cx);
13931 });
13932 cx.executor().run_until_parked();
13933 let first_item_id = workspace
13934 .update(cx, |workspace, window, cx| {
13935 let active_item = workspace
13936 .active_item(cx)
13937 .expect("should have an active item after navigating into the 1st buffer");
13938 let first_item_id = active_item.item_id();
13939 assert_ne!(
13940 first_item_id, multibuffer_item_id,
13941 "Should navigate into the 1st buffer and activate it"
13942 );
13943 assert!(
13944 active_item.is_singleton(cx),
13945 "New active item should be a singleton buffer"
13946 );
13947 assert_eq!(
13948 active_item
13949 .act_as::<Editor>(cx)
13950 .expect("should have navigated into an editor for the 1st buffer")
13951 .read(cx)
13952 .text(cx),
13953 sample_text_1
13954 );
13955
13956 workspace
13957 .go_back(workspace.active_pane().downgrade(), window, cx)
13958 .detach_and_log_err(cx);
13959
13960 first_item_id
13961 })
13962 .unwrap();
13963 cx.executor().run_until_parked();
13964 workspace
13965 .update(cx, |workspace, _, cx| {
13966 let active_item = workspace
13967 .active_item(cx)
13968 .expect("should have an active item after navigating back");
13969 assert_eq!(
13970 active_item.item_id(),
13971 multibuffer_item_id,
13972 "Should navigate back to the multi buffer"
13973 );
13974 assert!(!active_item.is_singleton(cx));
13975 })
13976 .unwrap();
13977
13978 multi_buffer_editor.update_in(cx, |editor, window, cx| {
13979 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
13980 s.select_ranges(Some(39..40))
13981 });
13982 editor.open_excerpts(&OpenExcerpts, window, cx);
13983 });
13984 cx.executor().run_until_parked();
13985 let second_item_id = workspace
13986 .update(cx, |workspace, window, cx| {
13987 let active_item = workspace
13988 .active_item(cx)
13989 .expect("should have an active item after navigating into the 2nd buffer");
13990 let second_item_id = active_item.item_id();
13991 assert_ne!(
13992 second_item_id, multibuffer_item_id,
13993 "Should navigate away from the multibuffer"
13994 );
13995 assert_ne!(
13996 second_item_id, first_item_id,
13997 "Should navigate into the 2nd buffer and activate it"
13998 );
13999 assert!(
14000 active_item.is_singleton(cx),
14001 "New active item should be a singleton buffer"
14002 );
14003 assert_eq!(
14004 active_item
14005 .act_as::<Editor>(cx)
14006 .expect("should have navigated into an editor")
14007 .read(cx)
14008 .text(cx),
14009 sample_text_2
14010 );
14011
14012 workspace
14013 .go_back(workspace.active_pane().downgrade(), window, cx)
14014 .detach_and_log_err(cx);
14015
14016 second_item_id
14017 })
14018 .unwrap();
14019 cx.executor().run_until_parked();
14020 workspace
14021 .update(cx, |workspace, _, cx| {
14022 let active_item = workspace
14023 .active_item(cx)
14024 .expect("should have an active item after navigating back from the 2nd buffer");
14025 assert_eq!(
14026 active_item.item_id(),
14027 multibuffer_item_id,
14028 "Should navigate back from the 2nd buffer to the multi buffer"
14029 );
14030 assert!(!active_item.is_singleton(cx));
14031 })
14032 .unwrap();
14033
14034 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14035 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14036 s.select_ranges(Some(70..70))
14037 });
14038 editor.open_excerpts(&OpenExcerpts, window, cx);
14039 });
14040 cx.executor().run_until_parked();
14041 workspace
14042 .update(cx, |workspace, window, cx| {
14043 let active_item = workspace
14044 .active_item(cx)
14045 .expect("should have an active item after navigating into the 3rd buffer");
14046 let third_item_id = active_item.item_id();
14047 assert_ne!(
14048 third_item_id, multibuffer_item_id,
14049 "Should navigate into the 3rd buffer and activate it"
14050 );
14051 assert_ne!(third_item_id, first_item_id);
14052 assert_ne!(third_item_id, second_item_id);
14053 assert!(
14054 active_item.is_singleton(cx),
14055 "New active item should be a singleton buffer"
14056 );
14057 assert_eq!(
14058 active_item
14059 .act_as::<Editor>(cx)
14060 .expect("should have navigated into an editor")
14061 .read(cx)
14062 .text(cx),
14063 sample_text_3
14064 );
14065
14066 workspace
14067 .go_back(workspace.active_pane().downgrade(), window, cx)
14068 .detach_and_log_err(cx);
14069 })
14070 .unwrap();
14071 cx.executor().run_until_parked();
14072 workspace
14073 .update(cx, |workspace, _, cx| {
14074 let active_item = workspace
14075 .active_item(cx)
14076 .expect("should have an active item after navigating back from the 3rd buffer");
14077 assert_eq!(
14078 active_item.item_id(),
14079 multibuffer_item_id,
14080 "Should navigate back from the 3rd buffer to the multi buffer"
14081 );
14082 assert!(!active_item.is_singleton(cx));
14083 })
14084 .unwrap();
14085}
14086
14087#[gpui::test]
14088async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14089 init_test(cx, |_| {});
14090
14091 let mut cx = EditorTestContext::new(cx).await;
14092
14093 let diff_base = r#"
14094 use some::mod;
14095
14096 const A: u32 = 42;
14097
14098 fn main() {
14099 println!("hello");
14100
14101 println!("world");
14102 }
14103 "#
14104 .unindent();
14105
14106 cx.set_state(
14107 &r#"
14108 use some::modified;
14109
14110 ˇ
14111 fn main() {
14112 println!("hello there");
14113
14114 println!("around the");
14115 println!("world");
14116 }
14117 "#
14118 .unindent(),
14119 );
14120
14121 cx.set_head_text(&diff_base);
14122 executor.run_until_parked();
14123
14124 cx.update_editor(|editor, window, cx| {
14125 editor.go_to_next_hunk(&GoToHunk, window, cx);
14126 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14127 });
14128 executor.run_until_parked();
14129 cx.assert_state_with_diff(
14130 r#"
14131 use some::modified;
14132
14133
14134 fn main() {
14135 - println!("hello");
14136 + ˇ println!("hello there");
14137
14138 println!("around the");
14139 println!("world");
14140 }
14141 "#
14142 .unindent(),
14143 );
14144
14145 cx.update_editor(|editor, window, cx| {
14146 for _ in 0..2 {
14147 editor.go_to_next_hunk(&GoToHunk, window, cx);
14148 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14149 }
14150 });
14151 executor.run_until_parked();
14152 cx.assert_state_with_diff(
14153 r#"
14154 - use some::mod;
14155 + ˇuse some::modified;
14156
14157
14158 fn main() {
14159 - println!("hello");
14160 + println!("hello there");
14161
14162 + println!("around the");
14163 println!("world");
14164 }
14165 "#
14166 .unindent(),
14167 );
14168
14169 cx.update_editor(|editor, window, cx| {
14170 editor.go_to_next_hunk(&GoToHunk, window, cx);
14171 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14172 });
14173 executor.run_until_parked();
14174 cx.assert_state_with_diff(
14175 r#"
14176 - use some::mod;
14177 + use some::modified;
14178
14179 - const A: u32 = 42;
14180 ˇ
14181 fn main() {
14182 - println!("hello");
14183 + println!("hello there");
14184
14185 + println!("around the");
14186 println!("world");
14187 }
14188 "#
14189 .unindent(),
14190 );
14191
14192 cx.update_editor(|editor, window, cx| {
14193 editor.cancel(&Cancel, window, cx);
14194 });
14195
14196 cx.assert_state_with_diff(
14197 r#"
14198 use some::modified;
14199
14200 ˇ
14201 fn main() {
14202 println!("hello there");
14203
14204 println!("around the");
14205 println!("world");
14206 }
14207 "#
14208 .unindent(),
14209 );
14210}
14211
14212#[gpui::test]
14213async fn test_diff_base_change_with_expanded_diff_hunks(
14214 executor: BackgroundExecutor,
14215 cx: &mut TestAppContext,
14216) {
14217 init_test(cx, |_| {});
14218
14219 let mut cx = EditorTestContext::new(cx).await;
14220
14221 let diff_base = r#"
14222 use some::mod1;
14223 use some::mod2;
14224
14225 const A: u32 = 42;
14226 const B: u32 = 42;
14227 const C: u32 = 42;
14228
14229 fn main() {
14230 println!("hello");
14231
14232 println!("world");
14233 }
14234 "#
14235 .unindent();
14236
14237 cx.set_state(
14238 &r#"
14239 use some::mod2;
14240
14241 const A: u32 = 42;
14242 const C: u32 = 42;
14243
14244 fn main(ˇ) {
14245 //println!("hello");
14246
14247 println!("world");
14248 //
14249 //
14250 }
14251 "#
14252 .unindent(),
14253 );
14254
14255 cx.set_head_text(&diff_base);
14256 executor.run_until_parked();
14257
14258 cx.update_editor(|editor, window, cx| {
14259 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14260 });
14261 executor.run_until_parked();
14262 cx.assert_state_with_diff(
14263 r#"
14264 - use some::mod1;
14265 use some::mod2;
14266
14267 const A: u32 = 42;
14268 - const B: u32 = 42;
14269 const C: u32 = 42;
14270
14271 fn main(ˇ) {
14272 - println!("hello");
14273 + //println!("hello");
14274
14275 println!("world");
14276 + //
14277 + //
14278 }
14279 "#
14280 .unindent(),
14281 );
14282
14283 cx.set_head_text("new diff base!");
14284 executor.run_until_parked();
14285 cx.assert_state_with_diff(
14286 r#"
14287 - new diff base!
14288 + use some::mod2;
14289 +
14290 + const A: u32 = 42;
14291 + const C: u32 = 42;
14292 +
14293 + fn main(ˇ) {
14294 + //println!("hello");
14295 +
14296 + println!("world");
14297 + //
14298 + //
14299 + }
14300 "#
14301 .unindent(),
14302 );
14303}
14304
14305#[gpui::test]
14306async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
14307 init_test(cx, |_| {});
14308
14309 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14310 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14311 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14312 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14313 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
14314 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
14315
14316 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
14317 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
14318 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
14319
14320 let multi_buffer = cx.new(|cx| {
14321 let mut multibuffer = MultiBuffer::new(ReadWrite);
14322 multibuffer.push_excerpts(
14323 buffer_1.clone(),
14324 [
14325 ExcerptRange {
14326 context: Point::new(0, 0)..Point::new(3, 0),
14327 primary: None,
14328 },
14329 ExcerptRange {
14330 context: Point::new(5, 0)..Point::new(7, 0),
14331 primary: None,
14332 },
14333 ExcerptRange {
14334 context: Point::new(9, 0)..Point::new(10, 3),
14335 primary: None,
14336 },
14337 ],
14338 cx,
14339 );
14340 multibuffer.push_excerpts(
14341 buffer_2.clone(),
14342 [
14343 ExcerptRange {
14344 context: Point::new(0, 0)..Point::new(3, 0),
14345 primary: None,
14346 },
14347 ExcerptRange {
14348 context: Point::new(5, 0)..Point::new(7, 0),
14349 primary: None,
14350 },
14351 ExcerptRange {
14352 context: Point::new(9, 0)..Point::new(10, 3),
14353 primary: None,
14354 },
14355 ],
14356 cx,
14357 );
14358 multibuffer.push_excerpts(
14359 buffer_3.clone(),
14360 [
14361 ExcerptRange {
14362 context: Point::new(0, 0)..Point::new(3, 0),
14363 primary: None,
14364 },
14365 ExcerptRange {
14366 context: Point::new(5, 0)..Point::new(7, 0),
14367 primary: None,
14368 },
14369 ExcerptRange {
14370 context: Point::new(9, 0)..Point::new(10, 3),
14371 primary: None,
14372 },
14373 ],
14374 cx,
14375 );
14376 multibuffer
14377 });
14378
14379 let editor =
14380 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14381 editor
14382 .update(cx, |editor, _window, cx| {
14383 for (buffer, diff_base) in [
14384 (buffer_1.clone(), file_1_old),
14385 (buffer_2.clone(), file_2_old),
14386 (buffer_3.clone(), file_3_old),
14387 ] {
14388 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14389 editor
14390 .buffer
14391 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14392 }
14393 })
14394 .unwrap();
14395
14396 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14397 cx.run_until_parked();
14398
14399 cx.assert_editor_state(
14400 &"
14401 ˇaaa
14402 ccc
14403 ddd
14404
14405 ggg
14406 hhh
14407
14408
14409 lll
14410 mmm
14411 NNN
14412
14413 qqq
14414 rrr
14415
14416 uuu
14417 111
14418 222
14419 333
14420
14421 666
14422 777
14423
14424 000
14425 !!!"
14426 .unindent(),
14427 );
14428
14429 cx.update_editor(|editor, window, cx| {
14430 editor.select_all(&SelectAll, window, cx);
14431 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14432 });
14433 cx.executor().run_until_parked();
14434
14435 cx.assert_state_with_diff(
14436 "
14437 «aaa
14438 - bbb
14439 ccc
14440 ddd
14441
14442 ggg
14443 hhh
14444
14445
14446 lll
14447 mmm
14448 - nnn
14449 + NNN
14450
14451 qqq
14452 rrr
14453
14454 uuu
14455 111
14456 222
14457 333
14458
14459 + 666
14460 777
14461
14462 000
14463 !!!ˇ»"
14464 .unindent(),
14465 );
14466}
14467
14468#[gpui::test]
14469async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
14470 init_test(cx, |_| {});
14471
14472 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
14473 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
14474
14475 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
14476 let multi_buffer = cx.new(|cx| {
14477 let mut multibuffer = MultiBuffer::new(ReadWrite);
14478 multibuffer.push_excerpts(
14479 buffer.clone(),
14480 [
14481 ExcerptRange {
14482 context: Point::new(0, 0)..Point::new(2, 0),
14483 primary: None,
14484 },
14485 ExcerptRange {
14486 context: Point::new(4, 0)..Point::new(7, 0),
14487 primary: None,
14488 },
14489 ExcerptRange {
14490 context: Point::new(9, 0)..Point::new(10, 0),
14491 primary: None,
14492 },
14493 ],
14494 cx,
14495 );
14496 multibuffer
14497 });
14498
14499 let editor =
14500 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14501 editor
14502 .update(cx, |editor, _window, cx| {
14503 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
14504 editor
14505 .buffer
14506 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
14507 })
14508 .unwrap();
14509
14510 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14511 cx.run_until_parked();
14512
14513 cx.update_editor(|editor, window, cx| {
14514 editor.expand_all_diff_hunks(&Default::default(), window, cx)
14515 });
14516 cx.executor().run_until_parked();
14517
14518 // When the start of a hunk coincides with the start of its excerpt,
14519 // the hunk is expanded. When the start of a a hunk is earlier than
14520 // the start of its excerpt, the hunk is not expanded.
14521 cx.assert_state_with_diff(
14522 "
14523 ˇaaa
14524 - bbb
14525 + BBB
14526
14527 - ddd
14528 - eee
14529 + DDD
14530 + EEE
14531 fff
14532
14533 iii
14534 "
14535 .unindent(),
14536 );
14537}
14538
14539#[gpui::test]
14540async fn test_edits_around_expanded_insertion_hunks(
14541 executor: BackgroundExecutor,
14542 cx: &mut TestAppContext,
14543) {
14544 init_test(cx, |_| {});
14545
14546 let mut cx = EditorTestContext::new(cx).await;
14547
14548 let diff_base = r#"
14549 use some::mod1;
14550 use some::mod2;
14551
14552 const A: u32 = 42;
14553
14554 fn main() {
14555 println!("hello");
14556
14557 println!("world");
14558 }
14559 "#
14560 .unindent();
14561 executor.run_until_parked();
14562 cx.set_state(
14563 &r#"
14564 use some::mod1;
14565 use some::mod2;
14566
14567 const A: u32 = 42;
14568 const B: u32 = 42;
14569 const C: u32 = 42;
14570 ˇ
14571
14572 fn main() {
14573 println!("hello");
14574
14575 println!("world");
14576 }
14577 "#
14578 .unindent(),
14579 );
14580
14581 cx.set_head_text(&diff_base);
14582 executor.run_until_parked();
14583
14584 cx.update_editor(|editor, window, cx| {
14585 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14586 });
14587 executor.run_until_parked();
14588
14589 cx.assert_state_with_diff(
14590 r#"
14591 use some::mod1;
14592 use some::mod2;
14593
14594 const A: u32 = 42;
14595 + const B: u32 = 42;
14596 + const C: u32 = 42;
14597 + ˇ
14598
14599 fn main() {
14600 println!("hello");
14601
14602 println!("world");
14603 }
14604 "#
14605 .unindent(),
14606 );
14607
14608 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
14609 executor.run_until_parked();
14610
14611 cx.assert_state_with_diff(
14612 r#"
14613 use some::mod1;
14614 use some::mod2;
14615
14616 const A: u32 = 42;
14617 + const B: u32 = 42;
14618 + const C: u32 = 42;
14619 + const D: u32 = 42;
14620 + ˇ
14621
14622 fn main() {
14623 println!("hello");
14624
14625 println!("world");
14626 }
14627 "#
14628 .unindent(),
14629 );
14630
14631 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
14632 executor.run_until_parked();
14633
14634 cx.assert_state_with_diff(
14635 r#"
14636 use some::mod1;
14637 use some::mod2;
14638
14639 const A: u32 = 42;
14640 + const B: u32 = 42;
14641 + const C: u32 = 42;
14642 + const D: u32 = 42;
14643 + const E: u32 = 42;
14644 + ˇ
14645
14646 fn main() {
14647 println!("hello");
14648
14649 println!("world");
14650 }
14651 "#
14652 .unindent(),
14653 );
14654
14655 cx.update_editor(|editor, window, cx| {
14656 editor.delete_line(&DeleteLine, window, cx);
14657 });
14658 executor.run_until_parked();
14659
14660 cx.assert_state_with_diff(
14661 r#"
14662 use some::mod1;
14663 use some::mod2;
14664
14665 const A: u32 = 42;
14666 + const B: u32 = 42;
14667 + const C: u32 = 42;
14668 + const D: u32 = 42;
14669 + const E: u32 = 42;
14670 ˇ
14671 fn main() {
14672 println!("hello");
14673
14674 println!("world");
14675 }
14676 "#
14677 .unindent(),
14678 );
14679
14680 cx.update_editor(|editor, window, cx| {
14681 editor.move_up(&MoveUp, window, cx);
14682 editor.delete_line(&DeleteLine, window, cx);
14683 editor.move_up(&MoveUp, window, cx);
14684 editor.delete_line(&DeleteLine, window, cx);
14685 editor.move_up(&MoveUp, window, cx);
14686 editor.delete_line(&DeleteLine, window, cx);
14687 });
14688 executor.run_until_parked();
14689 cx.assert_state_with_diff(
14690 r#"
14691 use some::mod1;
14692 use some::mod2;
14693
14694 const A: u32 = 42;
14695 + const B: u32 = 42;
14696 ˇ
14697 fn main() {
14698 println!("hello");
14699
14700 println!("world");
14701 }
14702 "#
14703 .unindent(),
14704 );
14705
14706 cx.update_editor(|editor, window, cx| {
14707 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
14708 editor.delete_line(&DeleteLine, window, cx);
14709 });
14710 executor.run_until_parked();
14711 cx.assert_state_with_diff(
14712 r#"
14713 ˇ
14714 fn main() {
14715 println!("hello");
14716
14717 println!("world");
14718 }
14719 "#
14720 .unindent(),
14721 );
14722}
14723
14724#[gpui::test]
14725async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
14726 init_test(cx, |_| {});
14727
14728 let mut cx = EditorTestContext::new(cx).await;
14729 cx.set_head_text(indoc! { "
14730 one
14731 two
14732 three
14733 four
14734 five
14735 "
14736 });
14737 cx.set_state(indoc! { "
14738 one
14739 ˇthree
14740 five
14741 "});
14742 cx.run_until_parked();
14743 cx.update_editor(|editor, window, cx| {
14744 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14745 });
14746 cx.assert_state_with_diff(
14747 indoc! { "
14748 one
14749 - two
14750 ˇthree
14751 - four
14752 five
14753 "}
14754 .to_string(),
14755 );
14756 cx.update_editor(|editor, window, cx| {
14757 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14758 });
14759
14760 cx.assert_state_with_diff(
14761 indoc! { "
14762 one
14763 ˇthree
14764 five
14765 "}
14766 .to_string(),
14767 );
14768
14769 cx.set_state(indoc! { "
14770 one
14771 ˇTWO
14772 three
14773 four
14774 five
14775 "});
14776 cx.run_until_parked();
14777 cx.update_editor(|editor, window, cx| {
14778 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14779 });
14780
14781 cx.assert_state_with_diff(
14782 indoc! { "
14783 one
14784 - two
14785 + ˇTWO
14786 three
14787 four
14788 five
14789 "}
14790 .to_string(),
14791 );
14792 cx.update_editor(|editor, window, cx| {
14793 editor.move_up(&Default::default(), window, cx);
14794 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14795 });
14796 cx.assert_state_with_diff(
14797 indoc! { "
14798 one
14799 ˇTWO
14800 three
14801 four
14802 five
14803 "}
14804 .to_string(),
14805 );
14806}
14807
14808#[gpui::test]
14809async fn test_edits_around_expanded_deletion_hunks(
14810 executor: BackgroundExecutor,
14811 cx: &mut TestAppContext,
14812) {
14813 init_test(cx, |_| {});
14814
14815 let mut cx = EditorTestContext::new(cx).await;
14816
14817 let diff_base = r#"
14818 use some::mod1;
14819 use some::mod2;
14820
14821 const A: u32 = 42;
14822 const B: u32 = 42;
14823 const C: u32 = 42;
14824
14825
14826 fn main() {
14827 println!("hello");
14828
14829 println!("world");
14830 }
14831 "#
14832 .unindent();
14833 executor.run_until_parked();
14834 cx.set_state(
14835 &r#"
14836 use some::mod1;
14837 use some::mod2;
14838
14839 ˇconst B: u32 = 42;
14840 const C: u32 = 42;
14841
14842
14843 fn main() {
14844 println!("hello");
14845
14846 println!("world");
14847 }
14848 "#
14849 .unindent(),
14850 );
14851
14852 cx.set_head_text(&diff_base);
14853 executor.run_until_parked();
14854
14855 cx.update_editor(|editor, window, cx| {
14856 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14857 });
14858 executor.run_until_parked();
14859
14860 cx.assert_state_with_diff(
14861 r#"
14862 use some::mod1;
14863 use some::mod2;
14864
14865 - const A: u32 = 42;
14866 ˇconst B: u32 = 42;
14867 const C: u32 = 42;
14868
14869
14870 fn main() {
14871 println!("hello");
14872
14873 println!("world");
14874 }
14875 "#
14876 .unindent(),
14877 );
14878
14879 cx.update_editor(|editor, window, cx| {
14880 editor.delete_line(&DeleteLine, window, cx);
14881 });
14882 executor.run_until_parked();
14883 cx.assert_state_with_diff(
14884 r#"
14885 use some::mod1;
14886 use some::mod2;
14887
14888 - const A: u32 = 42;
14889 - const B: u32 = 42;
14890 ˇconst C: u32 = 42;
14891
14892
14893 fn main() {
14894 println!("hello");
14895
14896 println!("world");
14897 }
14898 "#
14899 .unindent(),
14900 );
14901
14902 cx.update_editor(|editor, window, cx| {
14903 editor.delete_line(&DeleteLine, window, cx);
14904 });
14905 executor.run_until_parked();
14906 cx.assert_state_with_diff(
14907 r#"
14908 use some::mod1;
14909 use some::mod2;
14910
14911 - const A: u32 = 42;
14912 - const B: u32 = 42;
14913 - const C: u32 = 42;
14914 ˇ
14915
14916 fn main() {
14917 println!("hello");
14918
14919 println!("world");
14920 }
14921 "#
14922 .unindent(),
14923 );
14924
14925 cx.update_editor(|editor, window, cx| {
14926 editor.handle_input("replacement", window, cx);
14927 });
14928 executor.run_until_parked();
14929 cx.assert_state_with_diff(
14930 r#"
14931 use some::mod1;
14932 use some::mod2;
14933
14934 - const A: u32 = 42;
14935 - const B: u32 = 42;
14936 - const C: u32 = 42;
14937 -
14938 + replacementˇ
14939
14940 fn main() {
14941 println!("hello");
14942
14943 println!("world");
14944 }
14945 "#
14946 .unindent(),
14947 );
14948}
14949
14950#[gpui::test]
14951async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14952 init_test(cx, |_| {});
14953
14954 let mut cx = EditorTestContext::new(cx).await;
14955
14956 let base_text = r#"
14957 one
14958 two
14959 three
14960 four
14961 five
14962 "#
14963 .unindent();
14964 executor.run_until_parked();
14965 cx.set_state(
14966 &r#"
14967 one
14968 two
14969 fˇour
14970 five
14971 "#
14972 .unindent(),
14973 );
14974
14975 cx.set_head_text(&base_text);
14976 executor.run_until_parked();
14977
14978 cx.update_editor(|editor, window, cx| {
14979 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14980 });
14981 executor.run_until_parked();
14982
14983 cx.assert_state_with_diff(
14984 r#"
14985 one
14986 two
14987 - three
14988 fˇour
14989 five
14990 "#
14991 .unindent(),
14992 );
14993
14994 cx.update_editor(|editor, window, cx| {
14995 editor.backspace(&Backspace, window, cx);
14996 editor.backspace(&Backspace, window, cx);
14997 });
14998 executor.run_until_parked();
14999 cx.assert_state_with_diff(
15000 r#"
15001 one
15002 two
15003 - threeˇ
15004 - four
15005 + our
15006 five
15007 "#
15008 .unindent(),
15009 );
15010}
15011
15012#[gpui::test]
15013async fn test_edit_after_expanded_modification_hunk(
15014 executor: BackgroundExecutor,
15015 cx: &mut TestAppContext,
15016) {
15017 init_test(cx, |_| {});
15018
15019 let mut cx = EditorTestContext::new(cx).await;
15020
15021 let diff_base = r#"
15022 use some::mod1;
15023 use some::mod2;
15024
15025 const A: u32 = 42;
15026 const B: u32 = 42;
15027 const C: u32 = 42;
15028 const D: u32 = 42;
15029
15030
15031 fn main() {
15032 println!("hello");
15033
15034 println!("world");
15035 }"#
15036 .unindent();
15037
15038 cx.set_state(
15039 &r#"
15040 use some::mod1;
15041 use some::mod2;
15042
15043 const A: u32 = 42;
15044 const B: u32 = 42;
15045 const C: u32 = 43ˇ
15046 const D: u32 = 42;
15047
15048
15049 fn main() {
15050 println!("hello");
15051
15052 println!("world");
15053 }"#
15054 .unindent(),
15055 );
15056
15057 cx.set_head_text(&diff_base);
15058 executor.run_until_parked();
15059 cx.update_editor(|editor, window, cx| {
15060 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15061 });
15062 executor.run_until_parked();
15063
15064 cx.assert_state_with_diff(
15065 r#"
15066 use some::mod1;
15067 use some::mod2;
15068
15069 const A: u32 = 42;
15070 const B: u32 = 42;
15071 - const C: u32 = 42;
15072 + const C: u32 = 43ˇ
15073 const D: u32 = 42;
15074
15075
15076 fn main() {
15077 println!("hello");
15078
15079 println!("world");
15080 }"#
15081 .unindent(),
15082 );
15083
15084 cx.update_editor(|editor, window, cx| {
15085 editor.handle_input("\nnew_line\n", window, cx);
15086 });
15087 executor.run_until_parked();
15088
15089 cx.assert_state_with_diff(
15090 r#"
15091 use some::mod1;
15092 use some::mod2;
15093
15094 const A: u32 = 42;
15095 const B: u32 = 42;
15096 - const C: u32 = 42;
15097 + const C: u32 = 43
15098 + new_line
15099 + ˇ
15100 const D: u32 = 42;
15101
15102
15103 fn main() {
15104 println!("hello");
15105
15106 println!("world");
15107 }"#
15108 .unindent(),
15109 );
15110}
15111
15112#[gpui::test]
15113async fn test_stage_and_unstage_added_file_hunk(
15114 executor: BackgroundExecutor,
15115 cx: &mut TestAppContext,
15116) {
15117 init_test(cx, |_| {});
15118
15119 let mut cx = EditorTestContext::new(cx).await;
15120 cx.update_editor(|editor, _, cx| {
15121 editor.set_expand_all_diff_hunks(cx);
15122 });
15123
15124 let working_copy = r#"
15125 ˇfn main() {
15126 println!("hello, world!");
15127 }
15128 "#
15129 .unindent();
15130
15131 cx.set_state(&working_copy);
15132 executor.run_until_parked();
15133
15134 cx.assert_state_with_diff(
15135 r#"
15136 + ˇfn main() {
15137 + println!("hello, world!");
15138 + }
15139 "#
15140 .unindent(),
15141 );
15142 cx.assert_index_text(None);
15143
15144 cx.update_editor(|editor, window, cx| {
15145 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15146 });
15147 executor.run_until_parked();
15148 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
15149 cx.assert_state_with_diff(
15150 r#"
15151 + ˇfn main() {
15152 + println!("hello, world!");
15153 + }
15154 "#
15155 .unindent(),
15156 );
15157
15158 cx.update_editor(|editor, window, cx| {
15159 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15160 });
15161 executor.run_until_parked();
15162 cx.assert_index_text(None);
15163}
15164
15165async fn setup_indent_guides_editor(
15166 text: &str,
15167 cx: &mut TestAppContext,
15168) -> (BufferId, EditorTestContext) {
15169 init_test(cx, |_| {});
15170
15171 let mut cx = EditorTestContext::new(cx).await;
15172
15173 let buffer_id = cx.update_editor(|editor, window, cx| {
15174 editor.set_text(text, window, cx);
15175 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
15176
15177 buffer_ids[0]
15178 });
15179
15180 (buffer_id, cx)
15181}
15182
15183fn assert_indent_guides(
15184 range: Range<u32>,
15185 expected: Vec<IndentGuide>,
15186 active_indices: Option<Vec<usize>>,
15187 cx: &mut EditorTestContext,
15188) {
15189 let indent_guides = cx.update_editor(|editor, window, cx| {
15190 let snapshot = editor.snapshot(window, cx).display_snapshot;
15191 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
15192 editor,
15193 MultiBufferRow(range.start)..MultiBufferRow(range.end),
15194 true,
15195 &snapshot,
15196 cx,
15197 );
15198
15199 indent_guides.sort_by(|a, b| {
15200 a.depth.cmp(&b.depth).then(
15201 a.start_row
15202 .cmp(&b.start_row)
15203 .then(a.end_row.cmp(&b.end_row)),
15204 )
15205 });
15206 indent_guides
15207 });
15208
15209 if let Some(expected) = active_indices {
15210 let active_indices = cx.update_editor(|editor, window, cx| {
15211 let snapshot = editor.snapshot(window, cx).display_snapshot;
15212 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
15213 });
15214
15215 assert_eq!(
15216 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
15217 expected,
15218 "Active indent guide indices do not match"
15219 );
15220 }
15221
15222 assert_eq!(indent_guides, expected, "Indent guides do not match");
15223}
15224
15225fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
15226 IndentGuide {
15227 buffer_id,
15228 start_row: MultiBufferRow(start_row),
15229 end_row: MultiBufferRow(end_row),
15230 depth,
15231 tab_size: 4,
15232 settings: IndentGuideSettings {
15233 enabled: true,
15234 line_width: 1,
15235 active_line_width: 1,
15236 ..Default::default()
15237 },
15238 }
15239}
15240
15241#[gpui::test]
15242async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
15243 let (buffer_id, mut cx) = setup_indent_guides_editor(
15244 &"
15245 fn main() {
15246 let a = 1;
15247 }"
15248 .unindent(),
15249 cx,
15250 )
15251 .await;
15252
15253 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15254}
15255
15256#[gpui::test]
15257async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
15258 let (buffer_id, mut cx) = setup_indent_guides_editor(
15259 &"
15260 fn main() {
15261 let a = 1;
15262 let b = 2;
15263 }"
15264 .unindent(),
15265 cx,
15266 )
15267 .await;
15268
15269 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
15270}
15271
15272#[gpui::test]
15273async fn test_indent_guide_nested(cx: &mut TestAppContext) {
15274 let (buffer_id, mut cx) = setup_indent_guides_editor(
15275 &"
15276 fn main() {
15277 let a = 1;
15278 if a == 3 {
15279 let b = 2;
15280 } else {
15281 let c = 3;
15282 }
15283 }"
15284 .unindent(),
15285 cx,
15286 )
15287 .await;
15288
15289 assert_indent_guides(
15290 0..8,
15291 vec![
15292 indent_guide(buffer_id, 1, 6, 0),
15293 indent_guide(buffer_id, 3, 3, 1),
15294 indent_guide(buffer_id, 5, 5, 1),
15295 ],
15296 None,
15297 &mut cx,
15298 );
15299}
15300
15301#[gpui::test]
15302async fn test_indent_guide_tab(cx: &mut TestAppContext) {
15303 let (buffer_id, mut cx) = setup_indent_guides_editor(
15304 &"
15305 fn main() {
15306 let a = 1;
15307 let b = 2;
15308 let c = 3;
15309 }"
15310 .unindent(),
15311 cx,
15312 )
15313 .await;
15314
15315 assert_indent_guides(
15316 0..5,
15317 vec![
15318 indent_guide(buffer_id, 1, 3, 0),
15319 indent_guide(buffer_id, 2, 2, 1),
15320 ],
15321 None,
15322 &mut cx,
15323 );
15324}
15325
15326#[gpui::test]
15327async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
15328 let (buffer_id, mut cx) = setup_indent_guides_editor(
15329 &"
15330 fn main() {
15331 let a = 1;
15332
15333 let c = 3;
15334 }"
15335 .unindent(),
15336 cx,
15337 )
15338 .await;
15339
15340 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
15341}
15342
15343#[gpui::test]
15344async fn test_indent_guide_complex(cx: &mut TestAppContext) {
15345 let (buffer_id, mut cx) = setup_indent_guides_editor(
15346 &"
15347 fn main() {
15348 let a = 1;
15349
15350 let c = 3;
15351
15352 if a == 3 {
15353 let b = 2;
15354 } else {
15355 let c = 3;
15356 }
15357 }"
15358 .unindent(),
15359 cx,
15360 )
15361 .await;
15362
15363 assert_indent_guides(
15364 0..11,
15365 vec![
15366 indent_guide(buffer_id, 1, 9, 0),
15367 indent_guide(buffer_id, 6, 6, 1),
15368 indent_guide(buffer_id, 8, 8, 1),
15369 ],
15370 None,
15371 &mut cx,
15372 );
15373}
15374
15375#[gpui::test]
15376async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
15377 let (buffer_id, mut cx) = setup_indent_guides_editor(
15378 &"
15379 fn main() {
15380 let a = 1;
15381
15382 let c = 3;
15383
15384 if a == 3 {
15385 let b = 2;
15386 } else {
15387 let c = 3;
15388 }
15389 }"
15390 .unindent(),
15391 cx,
15392 )
15393 .await;
15394
15395 assert_indent_guides(
15396 1..11,
15397 vec![
15398 indent_guide(buffer_id, 1, 9, 0),
15399 indent_guide(buffer_id, 6, 6, 1),
15400 indent_guide(buffer_id, 8, 8, 1),
15401 ],
15402 None,
15403 &mut cx,
15404 );
15405}
15406
15407#[gpui::test]
15408async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
15409 let (buffer_id, mut cx) = setup_indent_guides_editor(
15410 &"
15411 fn main() {
15412 let a = 1;
15413
15414 let c = 3;
15415
15416 if a == 3 {
15417 let b = 2;
15418 } else {
15419 let c = 3;
15420 }
15421 }"
15422 .unindent(),
15423 cx,
15424 )
15425 .await;
15426
15427 assert_indent_guides(
15428 1..10,
15429 vec![
15430 indent_guide(buffer_id, 1, 9, 0),
15431 indent_guide(buffer_id, 6, 6, 1),
15432 indent_guide(buffer_id, 8, 8, 1),
15433 ],
15434 None,
15435 &mut cx,
15436 );
15437}
15438
15439#[gpui::test]
15440async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
15441 let (buffer_id, mut cx) = setup_indent_guides_editor(
15442 &"
15443 block1
15444 block2
15445 block3
15446 block4
15447 block2
15448 block1
15449 block1"
15450 .unindent(),
15451 cx,
15452 )
15453 .await;
15454
15455 assert_indent_guides(
15456 1..10,
15457 vec![
15458 indent_guide(buffer_id, 1, 4, 0),
15459 indent_guide(buffer_id, 2, 3, 1),
15460 indent_guide(buffer_id, 3, 3, 2),
15461 ],
15462 None,
15463 &mut cx,
15464 );
15465}
15466
15467#[gpui::test]
15468async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
15469 let (buffer_id, mut cx) = setup_indent_guides_editor(
15470 &"
15471 block1
15472 block2
15473 block3
15474
15475 block1
15476 block1"
15477 .unindent(),
15478 cx,
15479 )
15480 .await;
15481
15482 assert_indent_guides(
15483 0..6,
15484 vec![
15485 indent_guide(buffer_id, 1, 2, 0),
15486 indent_guide(buffer_id, 2, 2, 1),
15487 ],
15488 None,
15489 &mut cx,
15490 );
15491}
15492
15493#[gpui::test]
15494async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
15495 let (buffer_id, mut cx) = setup_indent_guides_editor(
15496 &"
15497 block1
15498
15499
15500
15501 block2
15502 "
15503 .unindent(),
15504 cx,
15505 )
15506 .await;
15507
15508 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15509}
15510
15511#[gpui::test]
15512async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
15513 let (buffer_id, mut cx) = setup_indent_guides_editor(
15514 &"
15515 def a:
15516 \tb = 3
15517 \tif True:
15518 \t\tc = 4
15519 \t\td = 5
15520 \tprint(b)
15521 "
15522 .unindent(),
15523 cx,
15524 )
15525 .await;
15526
15527 assert_indent_guides(
15528 0..6,
15529 vec![
15530 indent_guide(buffer_id, 1, 6, 0),
15531 indent_guide(buffer_id, 3, 4, 1),
15532 ],
15533 None,
15534 &mut cx,
15535 );
15536}
15537
15538#[gpui::test]
15539async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
15540 let (buffer_id, mut cx) = setup_indent_guides_editor(
15541 &"
15542 fn main() {
15543 let a = 1;
15544 }"
15545 .unindent(),
15546 cx,
15547 )
15548 .await;
15549
15550 cx.update_editor(|editor, window, cx| {
15551 editor.change_selections(None, window, cx, |s| {
15552 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15553 });
15554 });
15555
15556 assert_indent_guides(
15557 0..3,
15558 vec![indent_guide(buffer_id, 1, 1, 0)],
15559 Some(vec![0]),
15560 &mut cx,
15561 );
15562}
15563
15564#[gpui::test]
15565async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
15566 let (buffer_id, mut cx) = setup_indent_guides_editor(
15567 &"
15568 fn main() {
15569 if 1 == 2 {
15570 let a = 1;
15571 }
15572 }"
15573 .unindent(),
15574 cx,
15575 )
15576 .await;
15577
15578 cx.update_editor(|editor, window, cx| {
15579 editor.change_selections(None, window, cx, |s| {
15580 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15581 });
15582 });
15583
15584 assert_indent_guides(
15585 0..4,
15586 vec![
15587 indent_guide(buffer_id, 1, 3, 0),
15588 indent_guide(buffer_id, 2, 2, 1),
15589 ],
15590 Some(vec![1]),
15591 &mut cx,
15592 );
15593
15594 cx.update_editor(|editor, window, cx| {
15595 editor.change_selections(None, window, cx, |s| {
15596 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15597 });
15598 });
15599
15600 assert_indent_guides(
15601 0..4,
15602 vec![
15603 indent_guide(buffer_id, 1, 3, 0),
15604 indent_guide(buffer_id, 2, 2, 1),
15605 ],
15606 Some(vec![1]),
15607 &mut cx,
15608 );
15609
15610 cx.update_editor(|editor, window, cx| {
15611 editor.change_selections(None, window, cx, |s| {
15612 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
15613 });
15614 });
15615
15616 assert_indent_guides(
15617 0..4,
15618 vec![
15619 indent_guide(buffer_id, 1, 3, 0),
15620 indent_guide(buffer_id, 2, 2, 1),
15621 ],
15622 Some(vec![0]),
15623 &mut cx,
15624 );
15625}
15626
15627#[gpui::test]
15628async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
15629 let (buffer_id, mut cx) = setup_indent_guides_editor(
15630 &"
15631 fn main() {
15632 let a = 1;
15633
15634 let b = 2;
15635 }"
15636 .unindent(),
15637 cx,
15638 )
15639 .await;
15640
15641 cx.update_editor(|editor, window, cx| {
15642 editor.change_selections(None, window, cx, |s| {
15643 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15644 });
15645 });
15646
15647 assert_indent_guides(
15648 0..5,
15649 vec![indent_guide(buffer_id, 1, 3, 0)],
15650 Some(vec![0]),
15651 &mut cx,
15652 );
15653}
15654
15655#[gpui::test]
15656async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
15657 let (buffer_id, mut cx) = setup_indent_guides_editor(
15658 &"
15659 def m:
15660 a = 1
15661 pass"
15662 .unindent(),
15663 cx,
15664 )
15665 .await;
15666
15667 cx.update_editor(|editor, window, cx| {
15668 editor.change_selections(None, window, cx, |s| {
15669 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15670 });
15671 });
15672
15673 assert_indent_guides(
15674 0..3,
15675 vec![indent_guide(buffer_id, 1, 2, 0)],
15676 Some(vec![0]),
15677 &mut cx,
15678 );
15679}
15680
15681#[gpui::test]
15682async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
15683 init_test(cx, |_| {});
15684 let mut cx = EditorTestContext::new(cx).await;
15685 let text = indoc! {
15686 "
15687 impl A {
15688 fn b() {
15689 0;
15690 3;
15691 5;
15692 6;
15693 7;
15694 }
15695 }
15696 "
15697 };
15698 let base_text = indoc! {
15699 "
15700 impl A {
15701 fn b() {
15702 0;
15703 1;
15704 2;
15705 3;
15706 4;
15707 }
15708 fn c() {
15709 5;
15710 6;
15711 7;
15712 }
15713 }
15714 "
15715 };
15716
15717 cx.update_editor(|editor, window, cx| {
15718 editor.set_text(text, window, cx);
15719
15720 editor.buffer().update(cx, |multibuffer, cx| {
15721 let buffer = multibuffer.as_singleton().unwrap();
15722 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
15723
15724 multibuffer.set_all_diff_hunks_expanded(cx);
15725 multibuffer.add_diff(diff, cx);
15726
15727 buffer.read(cx).remote_id()
15728 })
15729 });
15730 cx.run_until_parked();
15731
15732 cx.assert_state_with_diff(
15733 indoc! { "
15734 impl A {
15735 fn b() {
15736 0;
15737 - 1;
15738 - 2;
15739 3;
15740 - 4;
15741 - }
15742 - fn c() {
15743 5;
15744 6;
15745 7;
15746 }
15747 }
15748 ˇ"
15749 }
15750 .to_string(),
15751 );
15752
15753 let mut actual_guides = cx.update_editor(|editor, window, cx| {
15754 editor
15755 .snapshot(window, cx)
15756 .buffer_snapshot
15757 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
15758 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
15759 .collect::<Vec<_>>()
15760 });
15761 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
15762 assert_eq!(
15763 actual_guides,
15764 vec![
15765 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
15766 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
15767 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
15768 ]
15769 );
15770}
15771
15772#[gpui::test]
15773async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15774 init_test(cx, |_| {});
15775 let mut cx = EditorTestContext::new(cx).await;
15776
15777 let diff_base = r#"
15778 a
15779 b
15780 c
15781 "#
15782 .unindent();
15783
15784 cx.set_state(
15785 &r#"
15786 ˇA
15787 b
15788 C
15789 "#
15790 .unindent(),
15791 );
15792 cx.set_head_text(&diff_base);
15793 cx.update_editor(|editor, window, cx| {
15794 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15795 });
15796 executor.run_until_parked();
15797
15798 let both_hunks_expanded = r#"
15799 - a
15800 + ˇA
15801 b
15802 - c
15803 + C
15804 "#
15805 .unindent();
15806
15807 cx.assert_state_with_diff(both_hunks_expanded.clone());
15808
15809 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15810 let snapshot = editor.snapshot(window, cx);
15811 let hunks = editor
15812 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15813 .collect::<Vec<_>>();
15814 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15815 let buffer_id = hunks[0].buffer_id;
15816 hunks
15817 .into_iter()
15818 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15819 .collect::<Vec<_>>()
15820 });
15821 assert_eq!(hunk_ranges.len(), 2);
15822
15823 cx.update_editor(|editor, _, cx| {
15824 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15825 });
15826 executor.run_until_parked();
15827
15828 let second_hunk_expanded = r#"
15829 ˇA
15830 b
15831 - c
15832 + C
15833 "#
15834 .unindent();
15835
15836 cx.assert_state_with_diff(second_hunk_expanded);
15837
15838 cx.update_editor(|editor, _, cx| {
15839 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15840 });
15841 executor.run_until_parked();
15842
15843 cx.assert_state_with_diff(both_hunks_expanded.clone());
15844
15845 cx.update_editor(|editor, _, cx| {
15846 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15847 });
15848 executor.run_until_parked();
15849
15850 let first_hunk_expanded = r#"
15851 - a
15852 + ˇA
15853 b
15854 C
15855 "#
15856 .unindent();
15857
15858 cx.assert_state_with_diff(first_hunk_expanded);
15859
15860 cx.update_editor(|editor, _, cx| {
15861 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15862 });
15863 executor.run_until_parked();
15864
15865 cx.assert_state_with_diff(both_hunks_expanded);
15866
15867 cx.set_state(
15868 &r#"
15869 ˇA
15870 b
15871 "#
15872 .unindent(),
15873 );
15874 cx.run_until_parked();
15875
15876 // TODO this cursor position seems bad
15877 cx.assert_state_with_diff(
15878 r#"
15879 - ˇa
15880 + A
15881 b
15882 "#
15883 .unindent(),
15884 );
15885
15886 cx.update_editor(|editor, window, cx| {
15887 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15888 });
15889
15890 cx.assert_state_with_diff(
15891 r#"
15892 - ˇa
15893 + A
15894 b
15895 - c
15896 "#
15897 .unindent(),
15898 );
15899
15900 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15901 let snapshot = editor.snapshot(window, cx);
15902 let hunks = editor
15903 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15904 .collect::<Vec<_>>();
15905 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15906 let buffer_id = hunks[0].buffer_id;
15907 hunks
15908 .into_iter()
15909 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15910 .collect::<Vec<_>>()
15911 });
15912 assert_eq!(hunk_ranges.len(), 2);
15913
15914 cx.update_editor(|editor, _, cx| {
15915 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15916 });
15917 executor.run_until_parked();
15918
15919 cx.assert_state_with_diff(
15920 r#"
15921 - ˇa
15922 + A
15923 b
15924 "#
15925 .unindent(),
15926 );
15927}
15928
15929#[gpui::test]
15930async fn test_toggle_deletion_hunk_at_start_of_file(
15931 executor: BackgroundExecutor,
15932 cx: &mut TestAppContext,
15933) {
15934 init_test(cx, |_| {});
15935 let mut cx = EditorTestContext::new(cx).await;
15936
15937 let diff_base = r#"
15938 a
15939 b
15940 c
15941 "#
15942 .unindent();
15943
15944 cx.set_state(
15945 &r#"
15946 ˇb
15947 c
15948 "#
15949 .unindent(),
15950 );
15951 cx.set_head_text(&diff_base);
15952 cx.update_editor(|editor, window, cx| {
15953 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15954 });
15955 executor.run_until_parked();
15956
15957 let hunk_expanded = r#"
15958 - a
15959 ˇb
15960 c
15961 "#
15962 .unindent();
15963
15964 cx.assert_state_with_diff(hunk_expanded.clone());
15965
15966 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15967 let snapshot = editor.snapshot(window, cx);
15968 let hunks = editor
15969 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15970 .collect::<Vec<_>>();
15971 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15972 let buffer_id = hunks[0].buffer_id;
15973 hunks
15974 .into_iter()
15975 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15976 .collect::<Vec<_>>()
15977 });
15978 assert_eq!(hunk_ranges.len(), 1);
15979
15980 cx.update_editor(|editor, _, cx| {
15981 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15982 });
15983 executor.run_until_parked();
15984
15985 let hunk_collapsed = r#"
15986 ˇb
15987 c
15988 "#
15989 .unindent();
15990
15991 cx.assert_state_with_diff(hunk_collapsed);
15992
15993 cx.update_editor(|editor, _, cx| {
15994 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15995 });
15996 executor.run_until_parked();
15997
15998 cx.assert_state_with_diff(hunk_expanded.clone());
15999}
16000
16001#[gpui::test]
16002async fn test_display_diff_hunks(cx: &mut TestAppContext) {
16003 init_test(cx, |_| {});
16004
16005 let fs = FakeFs::new(cx.executor());
16006 fs.insert_tree(
16007 path!("/test"),
16008 json!({
16009 ".git": {},
16010 "file-1": "ONE\n",
16011 "file-2": "TWO\n",
16012 "file-3": "THREE\n",
16013 }),
16014 )
16015 .await;
16016
16017 fs.set_head_for_repo(
16018 path!("/test/.git").as_ref(),
16019 &[
16020 ("file-1".into(), "one\n".into()),
16021 ("file-2".into(), "two\n".into()),
16022 ("file-3".into(), "three\n".into()),
16023 ],
16024 );
16025
16026 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
16027 let mut buffers = vec![];
16028 for i in 1..=3 {
16029 let buffer = project
16030 .update(cx, |project, cx| {
16031 let path = format!(path!("/test/file-{}"), i);
16032 project.open_local_buffer(path, cx)
16033 })
16034 .await
16035 .unwrap();
16036 buffers.push(buffer);
16037 }
16038
16039 let multibuffer = cx.new(|cx| {
16040 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
16041 multibuffer.set_all_diff_hunks_expanded(cx);
16042 for buffer in &buffers {
16043 let snapshot = buffer.read(cx).snapshot();
16044 multibuffer.set_excerpts_for_path(
16045 PathKey::namespaced("", buffer.read(cx).file().unwrap().path().clone()),
16046 buffer.clone(),
16047 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
16048 DEFAULT_MULTIBUFFER_CONTEXT,
16049 cx,
16050 );
16051 }
16052 multibuffer
16053 });
16054
16055 let editor = cx.add_window(|window, cx| {
16056 Editor::new(EditorMode::Full, multibuffer, Some(project), window, cx)
16057 });
16058 cx.run_until_parked();
16059
16060 let snapshot = editor
16061 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16062 .unwrap();
16063 let hunks = snapshot
16064 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
16065 .map(|hunk| match hunk {
16066 DisplayDiffHunk::Unfolded {
16067 display_row_range, ..
16068 } => display_row_range,
16069 DisplayDiffHunk::Folded { .. } => unreachable!(),
16070 })
16071 .collect::<Vec<_>>();
16072 assert_eq!(
16073 hunks,
16074 [
16075 DisplayRow(2)..DisplayRow(4),
16076 DisplayRow(7)..DisplayRow(9),
16077 DisplayRow(12)..DisplayRow(14),
16078 ]
16079 );
16080}
16081
16082#[gpui::test]
16083async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
16084 init_test(cx, |_| {});
16085
16086 let mut cx = EditorTestContext::new(cx).await;
16087 cx.set_head_text(indoc! { "
16088 one
16089 two
16090 three
16091 four
16092 five
16093 "
16094 });
16095 cx.set_index_text(indoc! { "
16096 one
16097 two
16098 three
16099 four
16100 five
16101 "
16102 });
16103 cx.set_state(indoc! {"
16104 one
16105 TWO
16106 ˇTHREE
16107 FOUR
16108 five
16109 "});
16110 cx.run_until_parked();
16111 cx.update_editor(|editor, window, cx| {
16112 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16113 });
16114 cx.run_until_parked();
16115 cx.assert_index_text(Some(indoc! {"
16116 one
16117 TWO
16118 THREE
16119 FOUR
16120 five
16121 "}));
16122 cx.set_state(indoc! { "
16123 one
16124 TWO
16125 ˇTHREE-HUNDRED
16126 FOUR
16127 five
16128 "});
16129 cx.run_until_parked();
16130 cx.update_editor(|editor, window, cx| {
16131 let snapshot = editor.snapshot(window, cx);
16132 let hunks = editor
16133 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16134 .collect::<Vec<_>>();
16135 assert_eq!(hunks.len(), 1);
16136 assert_eq!(
16137 hunks[0].status(),
16138 DiffHunkStatus {
16139 kind: DiffHunkStatusKind::Modified,
16140 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
16141 }
16142 );
16143
16144 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16145 });
16146 cx.run_until_parked();
16147 cx.assert_index_text(Some(indoc! {"
16148 one
16149 TWO
16150 THREE-HUNDRED
16151 FOUR
16152 five
16153 "}));
16154}
16155
16156#[gpui::test]
16157fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
16158 init_test(cx, |_| {});
16159
16160 let editor = cx.add_window(|window, cx| {
16161 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
16162 build_editor(buffer, window, cx)
16163 });
16164
16165 let render_args = Arc::new(Mutex::new(None));
16166 let snapshot = editor
16167 .update(cx, |editor, window, cx| {
16168 let snapshot = editor.buffer().read(cx).snapshot(cx);
16169 let range =
16170 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
16171
16172 struct RenderArgs {
16173 row: MultiBufferRow,
16174 folded: bool,
16175 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
16176 }
16177
16178 let crease = Crease::inline(
16179 range,
16180 FoldPlaceholder::test(),
16181 {
16182 let toggle_callback = render_args.clone();
16183 move |row, folded, callback, _window, _cx| {
16184 *toggle_callback.lock() = Some(RenderArgs {
16185 row,
16186 folded,
16187 callback,
16188 });
16189 div()
16190 }
16191 },
16192 |_row, _folded, _window, _cx| div(),
16193 );
16194
16195 editor.insert_creases(Some(crease), cx);
16196 let snapshot = editor.snapshot(window, cx);
16197 let _div = snapshot.render_crease_toggle(
16198 MultiBufferRow(1),
16199 false,
16200 cx.entity().clone(),
16201 window,
16202 cx,
16203 );
16204 snapshot
16205 })
16206 .unwrap();
16207
16208 let render_args = render_args.lock().take().unwrap();
16209 assert_eq!(render_args.row, MultiBufferRow(1));
16210 assert!(!render_args.folded);
16211 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16212
16213 cx.update_window(*editor, |_, window, cx| {
16214 (render_args.callback)(true, window, cx)
16215 })
16216 .unwrap();
16217 let snapshot = editor
16218 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16219 .unwrap();
16220 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
16221
16222 cx.update_window(*editor, |_, window, cx| {
16223 (render_args.callback)(false, window, cx)
16224 })
16225 .unwrap();
16226 let snapshot = editor
16227 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16228 .unwrap();
16229 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16230}
16231
16232#[gpui::test]
16233async fn test_input_text(cx: &mut TestAppContext) {
16234 init_test(cx, |_| {});
16235 let mut cx = EditorTestContext::new(cx).await;
16236
16237 cx.set_state(
16238 &r#"ˇone
16239 two
16240
16241 three
16242 fourˇ
16243 five
16244
16245 siˇx"#
16246 .unindent(),
16247 );
16248
16249 cx.dispatch_action(HandleInput(String::new()));
16250 cx.assert_editor_state(
16251 &r#"ˇone
16252 two
16253
16254 three
16255 fourˇ
16256 five
16257
16258 siˇx"#
16259 .unindent(),
16260 );
16261
16262 cx.dispatch_action(HandleInput("AAAA".to_string()));
16263 cx.assert_editor_state(
16264 &r#"AAAAˇone
16265 two
16266
16267 three
16268 fourAAAAˇ
16269 five
16270
16271 siAAAAˇx"#
16272 .unindent(),
16273 );
16274}
16275
16276#[gpui::test]
16277async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
16278 init_test(cx, |_| {});
16279
16280 let mut cx = EditorTestContext::new(cx).await;
16281 cx.set_state(
16282 r#"let foo = 1;
16283let foo = 2;
16284let foo = 3;
16285let fooˇ = 4;
16286let foo = 5;
16287let foo = 6;
16288let foo = 7;
16289let foo = 8;
16290let foo = 9;
16291let foo = 10;
16292let foo = 11;
16293let foo = 12;
16294let foo = 13;
16295let foo = 14;
16296let foo = 15;"#,
16297 );
16298
16299 cx.update_editor(|e, window, cx| {
16300 assert_eq!(
16301 e.next_scroll_position,
16302 NextScrollCursorCenterTopBottom::Center,
16303 "Default next scroll direction is center",
16304 );
16305
16306 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16307 assert_eq!(
16308 e.next_scroll_position,
16309 NextScrollCursorCenterTopBottom::Top,
16310 "After center, next scroll direction should be top",
16311 );
16312
16313 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16314 assert_eq!(
16315 e.next_scroll_position,
16316 NextScrollCursorCenterTopBottom::Bottom,
16317 "After top, next scroll direction should be bottom",
16318 );
16319
16320 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16321 assert_eq!(
16322 e.next_scroll_position,
16323 NextScrollCursorCenterTopBottom::Center,
16324 "After bottom, scrolling should start over",
16325 );
16326
16327 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16328 assert_eq!(
16329 e.next_scroll_position,
16330 NextScrollCursorCenterTopBottom::Top,
16331 "Scrolling continues if retriggered fast enough"
16332 );
16333 });
16334
16335 cx.executor()
16336 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
16337 cx.executor().run_until_parked();
16338 cx.update_editor(|e, _, _| {
16339 assert_eq!(
16340 e.next_scroll_position,
16341 NextScrollCursorCenterTopBottom::Center,
16342 "If scrolling is not triggered fast enough, it should reset"
16343 );
16344 });
16345}
16346
16347#[gpui::test]
16348async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
16349 init_test(cx, |_| {});
16350 let mut cx = EditorLspTestContext::new_rust(
16351 lsp::ServerCapabilities {
16352 definition_provider: Some(lsp::OneOf::Left(true)),
16353 references_provider: Some(lsp::OneOf::Left(true)),
16354 ..lsp::ServerCapabilities::default()
16355 },
16356 cx,
16357 )
16358 .await;
16359
16360 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
16361 let go_to_definition = cx.lsp.handle_request::<lsp::request::GotoDefinition, _, _>(
16362 move |params, _| async move {
16363 if empty_go_to_definition {
16364 Ok(None)
16365 } else {
16366 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
16367 uri: params.text_document_position_params.text_document.uri,
16368 range: lsp::Range::new(lsp::Position::new(4, 3), lsp::Position::new(4, 6)),
16369 })))
16370 }
16371 },
16372 );
16373 let references =
16374 cx.lsp
16375 .handle_request::<lsp::request::References, _, _>(move |params, _| async move {
16376 Ok(Some(vec![lsp::Location {
16377 uri: params.text_document_position.text_document.uri,
16378 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
16379 }]))
16380 });
16381 (go_to_definition, references)
16382 };
16383
16384 cx.set_state(
16385 &r#"fn one() {
16386 let mut a = ˇtwo();
16387 }
16388
16389 fn two() {}"#
16390 .unindent(),
16391 );
16392 set_up_lsp_handlers(false, &mut cx);
16393 let navigated = cx
16394 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16395 .await
16396 .expect("Failed to navigate to definition");
16397 assert_eq!(
16398 navigated,
16399 Navigated::Yes,
16400 "Should have navigated to definition from the GetDefinition response"
16401 );
16402 cx.assert_editor_state(
16403 &r#"fn one() {
16404 let mut a = two();
16405 }
16406
16407 fn «twoˇ»() {}"#
16408 .unindent(),
16409 );
16410
16411 let editors = cx.update_workspace(|workspace, _, cx| {
16412 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16413 });
16414 cx.update_editor(|_, _, test_editor_cx| {
16415 assert_eq!(
16416 editors.len(),
16417 1,
16418 "Initially, only one, test, editor should be open in the workspace"
16419 );
16420 assert_eq!(
16421 test_editor_cx.entity(),
16422 editors.last().expect("Asserted len is 1").clone()
16423 );
16424 });
16425
16426 set_up_lsp_handlers(true, &mut cx);
16427 let navigated = cx
16428 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16429 .await
16430 .expect("Failed to navigate to lookup references");
16431 assert_eq!(
16432 navigated,
16433 Navigated::Yes,
16434 "Should have navigated to references as a fallback after empty GoToDefinition response"
16435 );
16436 // We should not change the selections in the existing file,
16437 // if opening another milti buffer with the references
16438 cx.assert_editor_state(
16439 &r#"fn one() {
16440 let mut a = two();
16441 }
16442
16443 fn «twoˇ»() {}"#
16444 .unindent(),
16445 );
16446 let editors = cx.update_workspace(|workspace, _, cx| {
16447 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16448 });
16449 cx.update_editor(|_, _, test_editor_cx| {
16450 assert_eq!(
16451 editors.len(),
16452 2,
16453 "After falling back to references search, we open a new editor with the results"
16454 );
16455 let references_fallback_text = editors
16456 .into_iter()
16457 .find(|new_editor| *new_editor != test_editor_cx.entity())
16458 .expect("Should have one non-test editor now")
16459 .read(test_editor_cx)
16460 .text(test_editor_cx);
16461 assert_eq!(
16462 references_fallback_text, "fn one() {\n let mut a = two();\n}",
16463 "Should use the range from the references response and not the GoToDefinition one"
16464 );
16465 });
16466}
16467
16468#[gpui::test]
16469async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
16470 init_test(cx, |_| {});
16471
16472 let language = Arc::new(Language::new(
16473 LanguageConfig::default(),
16474 Some(tree_sitter_rust::LANGUAGE.into()),
16475 ));
16476
16477 let text = r#"
16478 #[cfg(test)]
16479 mod tests() {
16480 #[test]
16481 fn runnable_1() {
16482 let a = 1;
16483 }
16484
16485 #[test]
16486 fn runnable_2() {
16487 let a = 1;
16488 let b = 2;
16489 }
16490 }
16491 "#
16492 .unindent();
16493
16494 let fs = FakeFs::new(cx.executor());
16495 fs.insert_file("/file.rs", Default::default()).await;
16496
16497 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16498 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16499 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16500 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16501 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
16502
16503 let editor = cx.new_window_entity(|window, cx| {
16504 Editor::new(
16505 EditorMode::Full,
16506 multi_buffer,
16507 Some(project.clone()),
16508 window,
16509 cx,
16510 )
16511 });
16512
16513 editor.update_in(cx, |editor, window, cx| {
16514 let snapshot = editor.buffer().read(cx).snapshot(cx);
16515 editor.tasks.insert(
16516 (buffer.read(cx).remote_id(), 3),
16517 RunnableTasks {
16518 templates: vec![],
16519 offset: snapshot.anchor_before(43),
16520 column: 0,
16521 extra_variables: HashMap::default(),
16522 context_range: BufferOffset(43)..BufferOffset(85),
16523 },
16524 );
16525 editor.tasks.insert(
16526 (buffer.read(cx).remote_id(), 8),
16527 RunnableTasks {
16528 templates: vec![],
16529 offset: snapshot.anchor_before(86),
16530 column: 0,
16531 extra_variables: HashMap::default(),
16532 context_range: BufferOffset(86)..BufferOffset(191),
16533 },
16534 );
16535
16536 // Test finding task when cursor is inside function body
16537 editor.change_selections(None, window, cx, |s| {
16538 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
16539 });
16540 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16541 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
16542
16543 // Test finding task when cursor is on function name
16544 editor.change_selections(None, window, cx, |s| {
16545 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
16546 });
16547 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16548 assert_eq!(row, 8, "Should find task when cursor is on function name");
16549 });
16550}
16551
16552#[gpui::test]
16553async fn test_folding_buffers(cx: &mut TestAppContext) {
16554 init_test(cx, |_| {});
16555
16556 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
16557 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
16558 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
16559
16560 let fs = FakeFs::new(cx.executor());
16561 fs.insert_tree(
16562 path!("/a"),
16563 json!({
16564 "first.rs": sample_text_1,
16565 "second.rs": sample_text_2,
16566 "third.rs": sample_text_3,
16567 }),
16568 )
16569 .await;
16570 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16571 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16572 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16573 let worktree = project.update(cx, |project, cx| {
16574 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16575 assert_eq!(worktrees.len(), 1);
16576 worktrees.pop().unwrap()
16577 });
16578 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16579
16580 let buffer_1 = project
16581 .update(cx, |project, cx| {
16582 project.open_buffer((worktree_id, "first.rs"), cx)
16583 })
16584 .await
16585 .unwrap();
16586 let buffer_2 = project
16587 .update(cx, |project, cx| {
16588 project.open_buffer((worktree_id, "second.rs"), cx)
16589 })
16590 .await
16591 .unwrap();
16592 let buffer_3 = project
16593 .update(cx, |project, cx| {
16594 project.open_buffer((worktree_id, "third.rs"), cx)
16595 })
16596 .await
16597 .unwrap();
16598
16599 let multi_buffer = cx.new(|cx| {
16600 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16601 multi_buffer.push_excerpts(
16602 buffer_1.clone(),
16603 [
16604 ExcerptRange {
16605 context: Point::new(0, 0)..Point::new(3, 0),
16606 primary: None,
16607 },
16608 ExcerptRange {
16609 context: Point::new(5, 0)..Point::new(7, 0),
16610 primary: None,
16611 },
16612 ExcerptRange {
16613 context: Point::new(9, 0)..Point::new(10, 4),
16614 primary: None,
16615 },
16616 ],
16617 cx,
16618 );
16619 multi_buffer.push_excerpts(
16620 buffer_2.clone(),
16621 [
16622 ExcerptRange {
16623 context: Point::new(0, 0)..Point::new(3, 0),
16624 primary: None,
16625 },
16626 ExcerptRange {
16627 context: Point::new(5, 0)..Point::new(7, 0),
16628 primary: None,
16629 },
16630 ExcerptRange {
16631 context: Point::new(9, 0)..Point::new(10, 4),
16632 primary: None,
16633 },
16634 ],
16635 cx,
16636 );
16637 multi_buffer.push_excerpts(
16638 buffer_3.clone(),
16639 [
16640 ExcerptRange {
16641 context: Point::new(0, 0)..Point::new(3, 0),
16642 primary: None,
16643 },
16644 ExcerptRange {
16645 context: Point::new(5, 0)..Point::new(7, 0),
16646 primary: None,
16647 },
16648 ExcerptRange {
16649 context: Point::new(9, 0)..Point::new(10, 4),
16650 primary: None,
16651 },
16652 ],
16653 cx,
16654 );
16655 multi_buffer
16656 });
16657 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16658 Editor::new(
16659 EditorMode::Full,
16660 multi_buffer.clone(),
16661 Some(project.clone()),
16662 window,
16663 cx,
16664 )
16665 });
16666
16667 assert_eq!(
16668 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16669 "\n\naaaa\nbbbb\ncccc\n\n\nffff\ngggg\n\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16670 );
16671
16672 multi_buffer_editor.update(cx, |editor, cx| {
16673 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
16674 });
16675 assert_eq!(
16676 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16677 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16678 "After folding the first buffer, its text should not be displayed"
16679 );
16680
16681 multi_buffer_editor.update(cx, |editor, cx| {
16682 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
16683 });
16684 assert_eq!(
16685 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16686 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16687 "After folding the second buffer, its text should not be displayed"
16688 );
16689
16690 multi_buffer_editor.update(cx, |editor, cx| {
16691 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
16692 });
16693 assert_eq!(
16694 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16695 "\n\n\n\n\n",
16696 "After folding the third buffer, its text should not be displayed"
16697 );
16698
16699 // Emulate selection inside the fold logic, that should work
16700 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16701 editor
16702 .snapshot(window, cx)
16703 .next_line_boundary(Point::new(0, 4));
16704 });
16705
16706 multi_buffer_editor.update(cx, |editor, cx| {
16707 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
16708 });
16709 assert_eq!(
16710 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16711 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
16712 "After unfolding the second buffer, its text should be displayed"
16713 );
16714
16715 // Typing inside of buffer 1 causes that buffer to be unfolded.
16716 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16717 assert_eq!(
16718 multi_buffer
16719 .read(cx)
16720 .snapshot(cx)
16721 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
16722 .collect::<String>(),
16723 "bbbb"
16724 );
16725 editor.change_selections(None, window, cx, |selections| {
16726 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
16727 });
16728 editor.handle_input("B", window, cx);
16729 });
16730
16731 assert_eq!(
16732 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16733 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
16734 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
16735 );
16736
16737 multi_buffer_editor.update(cx, |editor, cx| {
16738 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
16739 });
16740 assert_eq!(
16741 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16742 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16743 "After unfolding the all buffers, all original text should be displayed"
16744 );
16745}
16746
16747#[gpui::test]
16748async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
16749 init_test(cx, |_| {});
16750
16751 let sample_text_1 = "1111\n2222\n3333".to_string();
16752 let sample_text_2 = "4444\n5555\n6666".to_string();
16753 let sample_text_3 = "7777\n8888\n9999".to_string();
16754
16755 let fs = FakeFs::new(cx.executor());
16756 fs.insert_tree(
16757 path!("/a"),
16758 json!({
16759 "first.rs": sample_text_1,
16760 "second.rs": sample_text_2,
16761 "third.rs": sample_text_3,
16762 }),
16763 )
16764 .await;
16765 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16766 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16767 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16768 let worktree = project.update(cx, |project, cx| {
16769 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16770 assert_eq!(worktrees.len(), 1);
16771 worktrees.pop().unwrap()
16772 });
16773 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16774
16775 let buffer_1 = project
16776 .update(cx, |project, cx| {
16777 project.open_buffer((worktree_id, "first.rs"), cx)
16778 })
16779 .await
16780 .unwrap();
16781 let buffer_2 = project
16782 .update(cx, |project, cx| {
16783 project.open_buffer((worktree_id, "second.rs"), cx)
16784 })
16785 .await
16786 .unwrap();
16787 let buffer_3 = project
16788 .update(cx, |project, cx| {
16789 project.open_buffer((worktree_id, "third.rs"), cx)
16790 })
16791 .await
16792 .unwrap();
16793
16794 let multi_buffer = cx.new(|cx| {
16795 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16796 multi_buffer.push_excerpts(
16797 buffer_1.clone(),
16798 [ExcerptRange {
16799 context: Point::new(0, 0)..Point::new(3, 0),
16800 primary: None,
16801 }],
16802 cx,
16803 );
16804 multi_buffer.push_excerpts(
16805 buffer_2.clone(),
16806 [ExcerptRange {
16807 context: Point::new(0, 0)..Point::new(3, 0),
16808 primary: None,
16809 }],
16810 cx,
16811 );
16812 multi_buffer.push_excerpts(
16813 buffer_3.clone(),
16814 [ExcerptRange {
16815 context: Point::new(0, 0)..Point::new(3, 0),
16816 primary: None,
16817 }],
16818 cx,
16819 );
16820 multi_buffer
16821 });
16822
16823 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16824 Editor::new(
16825 EditorMode::Full,
16826 multi_buffer,
16827 Some(project.clone()),
16828 window,
16829 cx,
16830 )
16831 });
16832
16833 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
16834 assert_eq!(
16835 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16836 full_text,
16837 );
16838
16839 multi_buffer_editor.update(cx, |editor, cx| {
16840 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
16841 });
16842 assert_eq!(
16843 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16844 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
16845 "After folding the first buffer, its text should not be displayed"
16846 );
16847
16848 multi_buffer_editor.update(cx, |editor, cx| {
16849 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
16850 });
16851
16852 assert_eq!(
16853 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16854 "\n\n\n\n\n\n7777\n8888\n9999",
16855 "After folding the second buffer, its text should not be displayed"
16856 );
16857
16858 multi_buffer_editor.update(cx, |editor, cx| {
16859 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
16860 });
16861 assert_eq!(
16862 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16863 "\n\n\n\n\n",
16864 "After folding the third buffer, its text should not be displayed"
16865 );
16866
16867 multi_buffer_editor.update(cx, |editor, cx| {
16868 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
16869 });
16870 assert_eq!(
16871 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16872 "\n\n\n\n4444\n5555\n6666\n\n",
16873 "After unfolding the second buffer, its text should be displayed"
16874 );
16875
16876 multi_buffer_editor.update(cx, |editor, cx| {
16877 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
16878 });
16879 assert_eq!(
16880 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16881 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
16882 "After unfolding the first buffer, its text should be displayed"
16883 );
16884
16885 multi_buffer_editor.update(cx, |editor, cx| {
16886 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
16887 });
16888 assert_eq!(
16889 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16890 full_text,
16891 "After unfolding all buffers, all original text should be displayed"
16892 );
16893}
16894
16895#[gpui::test]
16896async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
16897 init_test(cx, |_| {});
16898
16899 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
16900
16901 let fs = FakeFs::new(cx.executor());
16902 fs.insert_tree(
16903 path!("/a"),
16904 json!({
16905 "main.rs": sample_text,
16906 }),
16907 )
16908 .await;
16909 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16910 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16911 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16912 let worktree = project.update(cx, |project, cx| {
16913 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16914 assert_eq!(worktrees.len(), 1);
16915 worktrees.pop().unwrap()
16916 });
16917 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16918
16919 let buffer_1 = project
16920 .update(cx, |project, cx| {
16921 project.open_buffer((worktree_id, "main.rs"), cx)
16922 })
16923 .await
16924 .unwrap();
16925
16926 let multi_buffer = cx.new(|cx| {
16927 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16928 multi_buffer.push_excerpts(
16929 buffer_1.clone(),
16930 [ExcerptRange {
16931 context: Point::new(0, 0)
16932 ..Point::new(
16933 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
16934 0,
16935 ),
16936 primary: None,
16937 }],
16938 cx,
16939 );
16940 multi_buffer
16941 });
16942 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16943 Editor::new(
16944 EditorMode::Full,
16945 multi_buffer,
16946 Some(project.clone()),
16947 window,
16948 cx,
16949 )
16950 });
16951
16952 let selection_range = Point::new(1, 0)..Point::new(2, 0);
16953 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16954 enum TestHighlight {}
16955 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
16956 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
16957 editor.highlight_text::<TestHighlight>(
16958 vec![highlight_range.clone()],
16959 HighlightStyle::color(Hsla::green()),
16960 cx,
16961 );
16962 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
16963 });
16964
16965 let full_text = format!("\n\n{sample_text}");
16966 assert_eq!(
16967 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16968 full_text,
16969 );
16970}
16971
16972#[gpui::test]
16973async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
16974 init_test(cx, |_| {});
16975 cx.update(|cx| {
16976 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
16977 "keymaps/default-linux.json",
16978 cx,
16979 )
16980 .unwrap();
16981 cx.bind_keys(default_key_bindings);
16982 });
16983
16984 let (editor, cx) = cx.add_window_view(|window, cx| {
16985 let multi_buffer = MultiBuffer::build_multi(
16986 [
16987 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
16988 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
16989 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
16990 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
16991 ],
16992 cx,
16993 );
16994 let mut editor = Editor::new(EditorMode::Full, multi_buffer.clone(), None, window, cx);
16995
16996 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
16997 // fold all but the second buffer, so that we test navigating between two
16998 // adjacent folded buffers, as well as folded buffers at the start and
16999 // end the multibuffer
17000 editor.fold_buffer(buffer_ids[0], cx);
17001 editor.fold_buffer(buffer_ids[2], cx);
17002 editor.fold_buffer(buffer_ids[3], cx);
17003
17004 editor
17005 });
17006 cx.simulate_resize(size(px(1000.), px(1000.)));
17007
17008 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
17009 cx.assert_excerpts_with_selections(indoc! {"
17010 [EXCERPT]
17011 ˇ[FOLDED]
17012 [EXCERPT]
17013 a1
17014 b1
17015 [EXCERPT]
17016 [FOLDED]
17017 [EXCERPT]
17018 [FOLDED]
17019 "
17020 });
17021 cx.simulate_keystroke("down");
17022 cx.assert_excerpts_with_selections(indoc! {"
17023 [EXCERPT]
17024 [FOLDED]
17025 [EXCERPT]
17026 ˇa1
17027 b1
17028 [EXCERPT]
17029 [FOLDED]
17030 [EXCERPT]
17031 [FOLDED]
17032 "
17033 });
17034 cx.simulate_keystroke("down");
17035 cx.assert_excerpts_with_selections(indoc! {"
17036 [EXCERPT]
17037 [FOLDED]
17038 [EXCERPT]
17039 a1
17040 ˇb1
17041 [EXCERPT]
17042 [FOLDED]
17043 [EXCERPT]
17044 [FOLDED]
17045 "
17046 });
17047 cx.simulate_keystroke("down");
17048 cx.assert_excerpts_with_selections(indoc! {"
17049 [EXCERPT]
17050 [FOLDED]
17051 [EXCERPT]
17052 a1
17053 b1
17054 ˇ[EXCERPT]
17055 [FOLDED]
17056 [EXCERPT]
17057 [FOLDED]
17058 "
17059 });
17060 cx.simulate_keystroke("down");
17061 cx.assert_excerpts_with_selections(indoc! {"
17062 [EXCERPT]
17063 [FOLDED]
17064 [EXCERPT]
17065 a1
17066 b1
17067 [EXCERPT]
17068 ˇ[FOLDED]
17069 [EXCERPT]
17070 [FOLDED]
17071 "
17072 });
17073 for _ in 0..5 {
17074 cx.simulate_keystroke("down");
17075 cx.assert_excerpts_with_selections(indoc! {"
17076 [EXCERPT]
17077 [FOLDED]
17078 [EXCERPT]
17079 a1
17080 b1
17081 [EXCERPT]
17082 [FOLDED]
17083 [EXCERPT]
17084 ˇ[FOLDED]
17085 "
17086 });
17087 }
17088
17089 cx.simulate_keystroke("up");
17090 cx.assert_excerpts_with_selections(indoc! {"
17091 [EXCERPT]
17092 [FOLDED]
17093 [EXCERPT]
17094 a1
17095 b1
17096 [EXCERPT]
17097 ˇ[FOLDED]
17098 [EXCERPT]
17099 [FOLDED]
17100 "
17101 });
17102 cx.simulate_keystroke("up");
17103 cx.assert_excerpts_with_selections(indoc! {"
17104 [EXCERPT]
17105 [FOLDED]
17106 [EXCERPT]
17107 a1
17108 b1
17109 ˇ[EXCERPT]
17110 [FOLDED]
17111 [EXCERPT]
17112 [FOLDED]
17113 "
17114 });
17115 cx.simulate_keystroke("up");
17116 cx.assert_excerpts_with_selections(indoc! {"
17117 [EXCERPT]
17118 [FOLDED]
17119 [EXCERPT]
17120 a1
17121 ˇb1
17122 [EXCERPT]
17123 [FOLDED]
17124 [EXCERPT]
17125 [FOLDED]
17126 "
17127 });
17128 cx.simulate_keystroke("up");
17129 cx.assert_excerpts_with_selections(indoc! {"
17130 [EXCERPT]
17131 [FOLDED]
17132 [EXCERPT]
17133 ˇa1
17134 b1
17135 [EXCERPT]
17136 [FOLDED]
17137 [EXCERPT]
17138 [FOLDED]
17139 "
17140 });
17141 for _ in 0..5 {
17142 cx.simulate_keystroke("up");
17143 cx.assert_excerpts_with_selections(indoc! {"
17144 [EXCERPT]
17145 ˇ[FOLDED]
17146 [EXCERPT]
17147 a1
17148 b1
17149 [EXCERPT]
17150 [FOLDED]
17151 [EXCERPT]
17152 [FOLDED]
17153 "
17154 });
17155 }
17156}
17157
17158#[gpui::test]
17159async fn test_inline_completion_text(cx: &mut TestAppContext) {
17160 init_test(cx, |_| {});
17161
17162 // Simple insertion
17163 assert_highlighted_edits(
17164 "Hello, world!",
17165 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
17166 true,
17167 cx,
17168 |highlighted_edits, cx| {
17169 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
17170 assert_eq!(highlighted_edits.highlights.len(), 1);
17171 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
17172 assert_eq!(
17173 highlighted_edits.highlights[0].1.background_color,
17174 Some(cx.theme().status().created_background)
17175 );
17176 },
17177 )
17178 .await;
17179
17180 // Replacement
17181 assert_highlighted_edits(
17182 "This is a test.",
17183 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
17184 false,
17185 cx,
17186 |highlighted_edits, cx| {
17187 assert_eq!(highlighted_edits.text, "That is a test.");
17188 assert_eq!(highlighted_edits.highlights.len(), 1);
17189 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
17190 assert_eq!(
17191 highlighted_edits.highlights[0].1.background_color,
17192 Some(cx.theme().status().created_background)
17193 );
17194 },
17195 )
17196 .await;
17197
17198 // Multiple edits
17199 assert_highlighted_edits(
17200 "Hello, world!",
17201 vec![
17202 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
17203 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
17204 ],
17205 false,
17206 cx,
17207 |highlighted_edits, cx| {
17208 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
17209 assert_eq!(highlighted_edits.highlights.len(), 2);
17210 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
17211 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
17212 assert_eq!(
17213 highlighted_edits.highlights[0].1.background_color,
17214 Some(cx.theme().status().created_background)
17215 );
17216 assert_eq!(
17217 highlighted_edits.highlights[1].1.background_color,
17218 Some(cx.theme().status().created_background)
17219 );
17220 },
17221 )
17222 .await;
17223
17224 // Multiple lines with edits
17225 assert_highlighted_edits(
17226 "First line\nSecond line\nThird line\nFourth line",
17227 vec![
17228 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
17229 (
17230 Point::new(2, 0)..Point::new(2, 10),
17231 "New third line".to_string(),
17232 ),
17233 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
17234 ],
17235 false,
17236 cx,
17237 |highlighted_edits, cx| {
17238 assert_eq!(
17239 highlighted_edits.text,
17240 "Second modified\nNew third line\nFourth updated line"
17241 );
17242 assert_eq!(highlighted_edits.highlights.len(), 3);
17243 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
17244 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
17245 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
17246 for highlight in &highlighted_edits.highlights {
17247 assert_eq!(
17248 highlight.1.background_color,
17249 Some(cx.theme().status().created_background)
17250 );
17251 }
17252 },
17253 )
17254 .await;
17255}
17256
17257#[gpui::test]
17258async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
17259 init_test(cx, |_| {});
17260
17261 // Deletion
17262 assert_highlighted_edits(
17263 "Hello, world!",
17264 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
17265 true,
17266 cx,
17267 |highlighted_edits, cx| {
17268 assert_eq!(highlighted_edits.text, "Hello, world!");
17269 assert_eq!(highlighted_edits.highlights.len(), 1);
17270 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
17271 assert_eq!(
17272 highlighted_edits.highlights[0].1.background_color,
17273 Some(cx.theme().status().deleted_background)
17274 );
17275 },
17276 )
17277 .await;
17278
17279 // Insertion
17280 assert_highlighted_edits(
17281 "Hello, world!",
17282 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
17283 true,
17284 cx,
17285 |highlighted_edits, cx| {
17286 assert_eq!(highlighted_edits.highlights.len(), 1);
17287 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
17288 assert_eq!(
17289 highlighted_edits.highlights[0].1.background_color,
17290 Some(cx.theme().status().created_background)
17291 );
17292 },
17293 )
17294 .await;
17295}
17296
17297async fn assert_highlighted_edits(
17298 text: &str,
17299 edits: Vec<(Range<Point>, String)>,
17300 include_deletions: bool,
17301 cx: &mut TestAppContext,
17302 assertion_fn: impl Fn(HighlightedText, &App),
17303) {
17304 let window = cx.add_window(|window, cx| {
17305 let buffer = MultiBuffer::build_simple(text, cx);
17306 Editor::new(EditorMode::Full, buffer, None, window, cx)
17307 });
17308 let cx = &mut VisualTestContext::from_window(*window, cx);
17309
17310 let (buffer, snapshot) = window
17311 .update(cx, |editor, _window, cx| {
17312 (
17313 editor.buffer().clone(),
17314 editor.buffer().read(cx).snapshot(cx),
17315 )
17316 })
17317 .unwrap();
17318
17319 let edits = edits
17320 .into_iter()
17321 .map(|(range, edit)| {
17322 (
17323 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
17324 edit,
17325 )
17326 })
17327 .collect::<Vec<_>>();
17328
17329 let text_anchor_edits = edits
17330 .clone()
17331 .into_iter()
17332 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
17333 .collect::<Vec<_>>();
17334
17335 let edit_preview = window
17336 .update(cx, |_, _window, cx| {
17337 buffer
17338 .read(cx)
17339 .as_singleton()
17340 .unwrap()
17341 .read(cx)
17342 .preview_edits(text_anchor_edits.into(), cx)
17343 })
17344 .unwrap()
17345 .await;
17346
17347 cx.update(|_window, cx| {
17348 let highlighted_edits = inline_completion_edit_text(
17349 &snapshot.as_singleton().unwrap().2,
17350 &edits,
17351 &edit_preview,
17352 include_deletions,
17353 cx,
17354 );
17355 assertion_fn(highlighted_edits, cx)
17356 });
17357}
17358
17359#[track_caller]
17360fn assert_breakpoint(
17361 breakpoints: &BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>>,
17362 path: &Arc<Path>,
17363 expected: Vec<(u32, BreakpointKind)>,
17364) {
17365 if expected.len() == 0usize {
17366 assert!(!breakpoints.contains_key(path));
17367 } else {
17368 let mut breakpoint = breakpoints
17369 .get(path)
17370 .unwrap()
17371 .into_iter()
17372 .map(|breakpoint| (breakpoint.position, breakpoint.kind.clone()))
17373 .collect::<Vec<_>>();
17374
17375 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
17376
17377 assert_eq!(expected, breakpoint);
17378 }
17379}
17380
17381fn add_log_breakpoint_at_cursor(
17382 editor: &mut Editor,
17383 log_message: &str,
17384 window: &mut Window,
17385 cx: &mut Context<Editor>,
17386) {
17387 let (anchor, bp) = editor
17388 .breakpoint_at_cursor_head(window, cx)
17389 .unwrap_or_else(|| {
17390 let cursor_position: Point = editor.selections.newest(cx).head();
17391
17392 let breakpoint_position = editor
17393 .snapshot(window, cx)
17394 .display_snapshot
17395 .buffer_snapshot
17396 .anchor_before(Point::new(cursor_position.row, 0));
17397
17398 let kind = BreakpointKind::Log(Arc::from(log_message));
17399
17400 (breakpoint_position, Breakpoint { kind })
17401 });
17402
17403 editor.edit_breakpoint_at_anchor(
17404 anchor,
17405 bp.kind,
17406 BreakpointEditAction::EditLogMessage(log_message.into()),
17407 cx,
17408 );
17409}
17410
17411#[gpui::test]
17412async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
17413 init_test(cx, |_| {});
17414
17415 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17416 let fs = FakeFs::new(cx.executor());
17417 fs.insert_tree(
17418 path!("/a"),
17419 json!({
17420 "main.rs": sample_text,
17421 }),
17422 )
17423 .await;
17424 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17425 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17426 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17427
17428 let fs = FakeFs::new(cx.executor());
17429 fs.insert_tree(
17430 path!("/a"),
17431 json!({
17432 "main.rs": sample_text,
17433 }),
17434 )
17435 .await;
17436 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17437 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17438 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17439 let worktree_id = workspace
17440 .update(cx, |workspace, _window, cx| {
17441 workspace.project().update(cx, |project, cx| {
17442 project.worktrees(cx).next().unwrap().read(cx).id()
17443 })
17444 })
17445 .unwrap();
17446
17447 let buffer = project
17448 .update(cx, |project, cx| {
17449 project.open_buffer((worktree_id, "main.rs"), cx)
17450 })
17451 .await
17452 .unwrap();
17453
17454 let (editor, cx) = cx.add_window_view(|window, cx| {
17455 Editor::new(
17456 EditorMode::Full,
17457 MultiBuffer::build_from_buffer(buffer, cx),
17458 Some(project.clone()),
17459 window,
17460 cx,
17461 )
17462 });
17463
17464 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17465 let abs_path = project.read_with(cx, |project, cx| {
17466 project
17467 .absolute_path(&project_path, cx)
17468 .map(|path_buf| Arc::from(path_buf.to_owned()))
17469 .unwrap()
17470 });
17471
17472 // assert we can add breakpoint on the first line
17473 editor.update_in(cx, |editor, window, cx| {
17474 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17475 editor.move_to_end(&MoveToEnd, window, cx);
17476 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17477 });
17478
17479 let breakpoints = editor.update(cx, |editor, cx| {
17480 editor
17481 .breakpoint_store()
17482 .as_ref()
17483 .unwrap()
17484 .read(cx)
17485 .all_breakpoints(cx)
17486 .clone()
17487 });
17488
17489 assert_eq!(1, breakpoints.len());
17490 assert_breakpoint(
17491 &breakpoints,
17492 &abs_path,
17493 vec![(0, BreakpointKind::Standard), (3, BreakpointKind::Standard)],
17494 );
17495
17496 editor.update_in(cx, |editor, window, cx| {
17497 editor.move_to_beginning(&MoveToBeginning, window, cx);
17498 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17499 });
17500
17501 let breakpoints = editor.update(cx, |editor, cx| {
17502 editor
17503 .breakpoint_store()
17504 .as_ref()
17505 .unwrap()
17506 .read(cx)
17507 .all_breakpoints(cx)
17508 .clone()
17509 });
17510
17511 assert_eq!(1, breakpoints.len());
17512 assert_breakpoint(&breakpoints, &abs_path, vec![(3, BreakpointKind::Standard)]);
17513
17514 editor.update_in(cx, |editor, window, cx| {
17515 editor.move_to_end(&MoveToEnd, window, cx);
17516 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17517 });
17518
17519 let breakpoints = editor.update(cx, |editor, cx| {
17520 editor
17521 .breakpoint_store()
17522 .as_ref()
17523 .unwrap()
17524 .read(cx)
17525 .all_breakpoints(cx)
17526 .clone()
17527 });
17528
17529 assert_eq!(0, breakpoints.len());
17530 assert_breakpoint(&breakpoints, &abs_path, vec![]);
17531}
17532
17533#[gpui::test]
17534async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
17535 init_test(cx, |_| {});
17536
17537 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17538
17539 let fs = FakeFs::new(cx.executor());
17540 fs.insert_tree(
17541 path!("/a"),
17542 json!({
17543 "main.rs": sample_text,
17544 }),
17545 )
17546 .await;
17547 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17548 let (workspace, cx) =
17549 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
17550
17551 let worktree_id = workspace.update(cx, |workspace, cx| {
17552 workspace.project().update(cx, |project, cx| {
17553 project.worktrees(cx).next().unwrap().read(cx).id()
17554 })
17555 });
17556
17557 let buffer = project
17558 .update(cx, |project, cx| {
17559 project.open_buffer((worktree_id, "main.rs"), cx)
17560 })
17561 .await
17562 .unwrap();
17563
17564 let (editor, cx) = cx.add_window_view(|window, cx| {
17565 Editor::new(
17566 EditorMode::Full,
17567 MultiBuffer::build_from_buffer(buffer, cx),
17568 Some(project.clone()),
17569 window,
17570 cx,
17571 )
17572 });
17573
17574 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17575 let abs_path = project.read_with(cx, |project, cx| {
17576 project
17577 .absolute_path(&project_path, cx)
17578 .map(|path_buf| Arc::from(path_buf.to_owned()))
17579 .unwrap()
17580 });
17581
17582 editor.update_in(cx, |editor, window, cx| {
17583 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17584 });
17585
17586 let breakpoints = editor.update(cx, |editor, cx| {
17587 editor
17588 .breakpoint_store()
17589 .as_ref()
17590 .unwrap()
17591 .read(cx)
17592 .all_breakpoints(cx)
17593 .clone()
17594 });
17595
17596 assert_breakpoint(
17597 &breakpoints,
17598 &abs_path,
17599 vec![(0, BreakpointKind::Log("hello world".into()))],
17600 );
17601
17602 // Removing a log message from a log breakpoint should remove it
17603 editor.update_in(cx, |editor, window, cx| {
17604 add_log_breakpoint_at_cursor(editor, "", window, cx);
17605 });
17606
17607 let breakpoints = editor.update(cx, |editor, cx| {
17608 editor
17609 .breakpoint_store()
17610 .as_ref()
17611 .unwrap()
17612 .read(cx)
17613 .all_breakpoints(cx)
17614 .clone()
17615 });
17616
17617 assert_breakpoint(&breakpoints, &abs_path, vec![]);
17618
17619 editor.update_in(cx, |editor, window, cx| {
17620 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17621 editor.move_to_end(&MoveToEnd, window, cx);
17622 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17623 // Not adding a log message to a standard breakpoint shouldn't remove it
17624 add_log_breakpoint_at_cursor(editor, "", window, cx);
17625 });
17626
17627 let breakpoints = editor.update(cx, |editor, cx| {
17628 editor
17629 .breakpoint_store()
17630 .as_ref()
17631 .unwrap()
17632 .read(cx)
17633 .all_breakpoints(cx)
17634 .clone()
17635 });
17636
17637 assert_breakpoint(
17638 &breakpoints,
17639 &abs_path,
17640 vec![(0, BreakpointKind::Standard), (3, BreakpointKind::Standard)],
17641 );
17642
17643 editor.update_in(cx, |editor, window, cx| {
17644 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17645 });
17646
17647 let breakpoints = editor.update(cx, |editor, cx| {
17648 editor
17649 .breakpoint_store()
17650 .as_ref()
17651 .unwrap()
17652 .read(cx)
17653 .all_breakpoints(cx)
17654 .clone()
17655 });
17656
17657 assert_breakpoint(
17658 &breakpoints,
17659 &abs_path,
17660 vec![
17661 (0, BreakpointKind::Standard),
17662 (3, BreakpointKind::Log("hello world".into())),
17663 ],
17664 );
17665
17666 editor.update_in(cx, |editor, window, cx| {
17667 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
17668 });
17669
17670 let breakpoints = editor.update(cx, |editor, cx| {
17671 editor
17672 .breakpoint_store()
17673 .as_ref()
17674 .unwrap()
17675 .read(cx)
17676 .all_breakpoints(cx)
17677 .clone()
17678 });
17679
17680 assert_breakpoint(
17681 &breakpoints,
17682 &abs_path,
17683 vec![
17684 (0, BreakpointKind::Standard),
17685 (3, BreakpointKind::Log("hello Earth !!".into())),
17686 ],
17687 );
17688}
17689
17690#[gpui::test]
17691async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
17692 init_test(cx, |_| {});
17693 let capabilities = lsp::ServerCapabilities {
17694 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
17695 prepare_provider: Some(true),
17696 work_done_progress_options: Default::default(),
17697 })),
17698 ..Default::default()
17699 };
17700 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
17701
17702 cx.set_state(indoc! {"
17703 struct Fˇoo {}
17704 "});
17705
17706 cx.update_editor(|editor, _, cx| {
17707 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
17708 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
17709 editor.highlight_background::<DocumentHighlightRead>(
17710 &[highlight_range],
17711 |c| c.editor_document_highlight_read_background,
17712 cx,
17713 );
17714 });
17715
17716 let mut prepare_rename_handler =
17717 cx.handle_request::<lsp::request::PrepareRenameRequest, _, _>(move |_, _, _| async move {
17718 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
17719 start: lsp::Position {
17720 line: 0,
17721 character: 7,
17722 },
17723 end: lsp::Position {
17724 line: 0,
17725 character: 10,
17726 },
17727 })))
17728 });
17729 let prepare_rename_task = cx
17730 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
17731 .expect("Prepare rename was not started");
17732 prepare_rename_handler.next().await.unwrap();
17733 prepare_rename_task.await.expect("Prepare rename failed");
17734
17735 let mut rename_handler =
17736 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
17737 let edit = lsp::TextEdit {
17738 range: lsp::Range {
17739 start: lsp::Position {
17740 line: 0,
17741 character: 7,
17742 },
17743 end: lsp::Position {
17744 line: 0,
17745 character: 10,
17746 },
17747 },
17748 new_text: "FooRenamed".to_string(),
17749 };
17750 Ok(Some(lsp::WorkspaceEdit::new(
17751 // Specify the same edit twice
17752 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
17753 )))
17754 });
17755 let rename_task = cx
17756 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
17757 .expect("Confirm rename was not started");
17758 rename_handler.next().await.unwrap();
17759 rename_task.await.expect("Confirm rename failed");
17760 cx.run_until_parked();
17761
17762 // Despite two edits, only one is actually applied as those are identical
17763 cx.assert_editor_state(indoc! {"
17764 struct FooRenamedˇ {}
17765 "});
17766}
17767
17768#[gpui::test]
17769async fn test_rename_without_prepare(cx: &mut TestAppContext) {
17770 init_test(cx, |_| {});
17771 // These capabilities indicate that the server does not support prepare rename.
17772 let capabilities = lsp::ServerCapabilities {
17773 rename_provider: Some(lsp::OneOf::Left(true)),
17774 ..Default::default()
17775 };
17776 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
17777
17778 cx.set_state(indoc! {"
17779 struct Fˇoo {}
17780 "});
17781
17782 cx.update_editor(|editor, _window, cx| {
17783 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
17784 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
17785 editor.highlight_background::<DocumentHighlightRead>(
17786 &[highlight_range],
17787 |c| c.editor_document_highlight_read_background,
17788 cx,
17789 );
17790 });
17791
17792 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
17793 .expect("Prepare rename was not started")
17794 .await
17795 .expect("Prepare rename failed");
17796
17797 let mut rename_handler =
17798 cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
17799 let edit = lsp::TextEdit {
17800 range: lsp::Range {
17801 start: lsp::Position {
17802 line: 0,
17803 character: 7,
17804 },
17805 end: lsp::Position {
17806 line: 0,
17807 character: 10,
17808 },
17809 },
17810 new_text: "FooRenamed".to_string(),
17811 };
17812 Ok(Some(lsp::WorkspaceEdit::new(
17813 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
17814 )))
17815 });
17816 let rename_task = cx
17817 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
17818 .expect("Confirm rename was not started");
17819 rename_handler.next().await.unwrap();
17820 rename_task.await.expect("Confirm rename failed");
17821 cx.run_until_parked();
17822
17823 // Correct range is renamed, as `surrounding_word` is used to find it.
17824 cx.assert_editor_state(indoc! {"
17825 struct FooRenamedˇ {}
17826 "});
17827}
17828
17829#[gpui::test]
17830async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
17831 init_test(cx, |_| {});
17832 let mut cx = EditorTestContext::new(cx).await;
17833
17834 let language = Arc::new(
17835 Language::new(
17836 LanguageConfig::default(),
17837 Some(tree_sitter_html::LANGUAGE.into()),
17838 )
17839 .with_brackets_query(
17840 r#"
17841 ("<" @open "/>" @close)
17842 ("</" @open ">" @close)
17843 ("<" @open ">" @close)
17844 ("\"" @open "\"" @close)
17845 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
17846 "#,
17847 )
17848 .unwrap(),
17849 );
17850 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
17851
17852 cx.set_state(indoc! {"
17853 <span>ˇ</span>
17854 "});
17855 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
17856 cx.assert_editor_state(indoc! {"
17857 <span>
17858 ˇ
17859 </span>
17860 "});
17861
17862 cx.set_state(indoc! {"
17863 <span><span></span>ˇ</span>
17864 "});
17865 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
17866 cx.assert_editor_state(indoc! {"
17867 <span><span></span>
17868 ˇ</span>
17869 "});
17870
17871 cx.set_state(indoc! {"
17872 <span>ˇ
17873 </span>
17874 "});
17875 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
17876 cx.assert_editor_state(indoc! {"
17877 <span>
17878 ˇ
17879 </span>
17880 "});
17881}
17882
17883#[gpui::test(iterations = 10)]
17884async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
17885 init_test(cx, |_| {});
17886
17887 let fs = FakeFs::new(cx.executor());
17888 fs.insert_tree(
17889 path!("/dir"),
17890 json!({
17891 "a.ts": "a",
17892 }),
17893 )
17894 .await;
17895
17896 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
17897 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17898 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17899
17900 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17901 language_registry.add(Arc::new(Language::new(
17902 LanguageConfig {
17903 name: "TypeScript".into(),
17904 matcher: LanguageMatcher {
17905 path_suffixes: vec!["ts".to_string()],
17906 ..Default::default()
17907 },
17908 ..Default::default()
17909 },
17910 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
17911 )));
17912 let mut fake_language_servers = language_registry.register_fake_lsp(
17913 "TypeScript",
17914 FakeLspAdapter {
17915 capabilities: lsp::ServerCapabilities {
17916 code_lens_provider: Some(lsp::CodeLensOptions {
17917 resolve_provider: Some(true),
17918 }),
17919 execute_command_provider: Some(lsp::ExecuteCommandOptions {
17920 commands: vec!["_the/command".to_string()],
17921 ..lsp::ExecuteCommandOptions::default()
17922 }),
17923 ..lsp::ServerCapabilities::default()
17924 },
17925 ..FakeLspAdapter::default()
17926 },
17927 );
17928
17929 let (buffer, _handle) = project
17930 .update(cx, |p, cx| {
17931 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
17932 })
17933 .await
17934 .unwrap();
17935 cx.executor().run_until_parked();
17936
17937 let fake_server = fake_language_servers.next().await.unwrap();
17938
17939 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
17940 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
17941 drop(buffer_snapshot);
17942 let actions = cx
17943 .update_window(*workspace, |_, window, cx| {
17944 project.code_actions(&buffer, anchor..anchor, window, cx)
17945 })
17946 .unwrap();
17947
17948 fake_server
17949 .handle_request::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
17950 Ok(Some(vec![
17951 lsp::CodeLens {
17952 range: lsp::Range::default(),
17953 command: Some(lsp::Command {
17954 title: "Code lens command".to_owned(),
17955 command: "_the/command".to_owned(),
17956 arguments: None,
17957 }),
17958 data: None,
17959 },
17960 lsp::CodeLens {
17961 range: lsp::Range::default(),
17962 command: Some(lsp::Command {
17963 title: "Command not in capabilities".to_owned(),
17964 command: "not in capabilities".to_owned(),
17965 arguments: None,
17966 }),
17967 data: None,
17968 },
17969 lsp::CodeLens {
17970 range: lsp::Range {
17971 start: lsp::Position {
17972 line: 1,
17973 character: 1,
17974 },
17975 end: lsp::Position {
17976 line: 1,
17977 character: 1,
17978 },
17979 },
17980 command: Some(lsp::Command {
17981 title: "Command not in range".to_owned(),
17982 command: "_the/command".to_owned(),
17983 arguments: None,
17984 }),
17985 data: None,
17986 },
17987 ]))
17988 })
17989 .next()
17990 .await;
17991
17992 let actions = actions.await.unwrap();
17993 assert_eq!(
17994 actions.len(),
17995 1,
17996 "Should have only one valid action for the 0..0 range"
17997 );
17998 let action = actions[0].clone();
17999 let apply = project.update(cx, |project, cx| {
18000 project.apply_code_action(buffer.clone(), action, true, cx)
18001 });
18002
18003 // Resolving the code action does not populate its edits. In absence of
18004 // edits, we must execute the given command.
18005 fake_server.handle_request::<lsp::request::CodeLensResolve, _, _>(|mut lens, _| async move {
18006 let lens_command = lens.command.as_mut().expect("should have a command");
18007 assert_eq!(lens_command.title, "Code lens command");
18008 lens_command.arguments = Some(vec![json!("the-argument")]);
18009 Ok(lens)
18010 });
18011
18012 // While executing the command, the language server sends the editor
18013 // a `workspaceEdit` request.
18014 fake_server
18015 .handle_request::<lsp::request::ExecuteCommand, _, _>({
18016 let fake = fake_server.clone();
18017 move |params, _| {
18018 assert_eq!(params.command, "_the/command");
18019 let fake = fake.clone();
18020 async move {
18021 fake.server
18022 .request::<lsp::request::ApplyWorkspaceEdit>(
18023 lsp::ApplyWorkspaceEditParams {
18024 label: None,
18025 edit: lsp::WorkspaceEdit {
18026 changes: Some(
18027 [(
18028 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
18029 vec![lsp::TextEdit {
18030 range: lsp::Range::new(
18031 lsp::Position::new(0, 0),
18032 lsp::Position::new(0, 0),
18033 ),
18034 new_text: "X".into(),
18035 }],
18036 )]
18037 .into_iter()
18038 .collect(),
18039 ),
18040 ..Default::default()
18041 },
18042 },
18043 )
18044 .await
18045 .unwrap();
18046 Ok(Some(json!(null)))
18047 }
18048 }
18049 })
18050 .next()
18051 .await;
18052
18053 // Applying the code lens command returns a project transaction containing the edits
18054 // sent by the language server in its `workspaceEdit` request.
18055 let transaction = apply.await.unwrap();
18056 assert!(transaction.0.contains_key(&buffer));
18057 buffer.update(cx, |buffer, cx| {
18058 assert_eq!(buffer.text(), "Xa");
18059 buffer.undo(cx);
18060 assert_eq!(buffer.text(), "a");
18061 });
18062}
18063
18064mod autoclose_tags {
18065 use super::*;
18066 use language::language_settings::JsxTagAutoCloseSettings;
18067 use languages::language;
18068
18069 async fn test_setup(cx: &mut TestAppContext) -> EditorTestContext {
18070 init_test(cx, |settings| {
18071 settings.defaults.jsx_tag_auto_close = Some(JsxTagAutoCloseSettings { enabled: true });
18072 });
18073
18074 let mut cx = EditorTestContext::new(cx).await;
18075 cx.update_buffer(|buffer, cx| {
18076 let language = language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into());
18077
18078 buffer.set_language(Some(language), cx)
18079 });
18080
18081 cx
18082 }
18083
18084 macro_rules! check {
18085 ($name:ident, $initial:literal + $input:literal => $expected:expr) => {
18086 #[gpui::test]
18087 async fn $name(cx: &mut TestAppContext) {
18088 let mut cx = test_setup(cx).await;
18089 cx.set_state($initial);
18090 cx.run_until_parked();
18091
18092 cx.update_editor(|editor, window, cx| {
18093 editor.handle_input($input, window, cx);
18094 });
18095 cx.run_until_parked();
18096 cx.assert_editor_state($expected);
18097 }
18098 };
18099 }
18100
18101 check!(
18102 test_basic,
18103 "<divˇ" + ">" => "<div>ˇ</div>"
18104 );
18105
18106 check!(
18107 test_basic_nested,
18108 "<div><divˇ</div>" + ">" => "<div><div>ˇ</div></div>"
18109 );
18110
18111 check!(
18112 test_basic_ignore_already_closed,
18113 "<div><divˇ</div></div>" + ">" => "<div><div>ˇ</div></div>"
18114 );
18115
18116 check!(
18117 test_doesnt_autoclose_closing_tag,
18118 "</divˇ" + ">" => "</div>ˇ"
18119 );
18120
18121 check!(
18122 test_jsx_attr,
18123 "<div attr={</div>}ˇ" + ">" => "<div attr={</div>}>ˇ</div>"
18124 );
18125
18126 check!(
18127 test_ignores_closing_tags_in_expr_block,
18128 "<div><divˇ{</div>}</div>" + ">" => "<div><div>ˇ</div>{</div>}</div>"
18129 );
18130
18131 check!(
18132 test_doesnt_autoclose_on_gt_in_expr,
18133 "<div attr={1 ˇ" + ">" => "<div attr={1 >ˇ"
18134 );
18135
18136 check!(
18137 test_ignores_closing_tags_with_different_tag_names,
18138 "<div><divˇ</div></span>" + ">" => "<div><div>ˇ</div></div></span>"
18139 );
18140
18141 check!(
18142 test_autocloses_in_jsx_expression,
18143 "<div>{<divˇ}</div>" + ">" => "<div>{<div>ˇ</div>}</div>"
18144 );
18145
18146 check!(
18147 test_doesnt_autoclose_already_closed_in_jsx_expression,
18148 "<div>{<divˇ</div>}</div>" + ">" => "<div>{<div>ˇ</div>}</div>"
18149 );
18150
18151 check!(
18152 test_autocloses_fragment,
18153 "<ˇ" + ">" => "<>ˇ</>"
18154 );
18155
18156 check!(
18157 test_does_not_include_type_argument_in_autoclose_tag_name,
18158 "<Component<T> attr={boolean_value}ˇ" + ">" => "<Component<T> attr={boolean_value}>ˇ</Component>"
18159 );
18160
18161 check!(
18162 test_does_not_autoclose_doctype,
18163 "<!DOCTYPE htmlˇ" + ">" => "<!DOCTYPE html>ˇ"
18164 );
18165
18166 check!(
18167 test_does_not_autoclose_comment,
18168 "<!-- comment --ˇ" + ">" => "<!-- comment -->ˇ"
18169 );
18170
18171 check!(
18172 test_multi_cursor_autoclose_same_tag,
18173 r#"
18174 <divˇ
18175 <divˇ
18176 "#
18177 + ">" =>
18178 r#"
18179 <div>ˇ</div>
18180 <div>ˇ</div>
18181 "#
18182 );
18183
18184 check!(
18185 test_multi_cursor_autoclose_different_tags,
18186 r#"
18187 <divˇ
18188 <spanˇ
18189 "#
18190 + ">" =>
18191 r#"
18192 <div>ˇ</div>
18193 <span>ˇ</span>
18194 "#
18195 );
18196
18197 check!(
18198 test_multi_cursor_autoclose_some_dont_autoclose_others,
18199 r#"
18200 <divˇ
18201 <div /ˇ
18202 <spanˇ</span>
18203 <!DOCTYPE htmlˇ
18204 </headˇ
18205 <Component<T>ˇ
18206 ˇ
18207 "#
18208 + ">" =>
18209 r#"
18210 <div>ˇ</div>
18211 <div />ˇ
18212 <span>ˇ</span>
18213 <!DOCTYPE html>ˇ
18214 </head>ˇ
18215 <Component<T>>ˇ</Component>
18216 >ˇ
18217 "#
18218 );
18219
18220 check!(
18221 test_doesnt_mess_up_trailing_text,
18222 "<divˇfoobar" + ">" => "<div>ˇ</div>foobar"
18223 );
18224
18225 #[gpui::test]
18226 async fn test_multibuffer(cx: &mut TestAppContext) {
18227 init_test(cx, |settings| {
18228 settings.defaults.jsx_tag_auto_close = Some(JsxTagAutoCloseSettings { enabled: true });
18229 });
18230
18231 let buffer_a = cx.new(|cx| {
18232 let mut buf = language::Buffer::local("<div", cx);
18233 buf.set_language(
18234 Some(language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into())),
18235 cx,
18236 );
18237 buf
18238 });
18239 let buffer_b = cx.new(|cx| {
18240 let mut buf = language::Buffer::local("<pre", cx);
18241 buf.set_language(
18242 Some(language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into())),
18243 cx,
18244 );
18245 buf
18246 });
18247 let buffer_c = cx.new(|cx| {
18248 let buf = language::Buffer::local("<span", cx);
18249 buf
18250 });
18251 let buffer = cx.new(|cx| {
18252 let mut buf = MultiBuffer::new(language::Capability::ReadWrite);
18253 buf.push_excerpts(
18254 buffer_a,
18255 [ExcerptRange {
18256 context: text::Anchor::MIN..text::Anchor::MAX,
18257 primary: None,
18258 }],
18259 cx,
18260 );
18261 buf.push_excerpts(
18262 buffer_b,
18263 [ExcerptRange {
18264 context: text::Anchor::MIN..text::Anchor::MAX,
18265 primary: None,
18266 }],
18267 cx,
18268 );
18269 buf.push_excerpts(
18270 buffer_c,
18271 [ExcerptRange {
18272 context: text::Anchor::MIN..text::Anchor::MAX,
18273 primary: None,
18274 }],
18275 cx,
18276 );
18277 buf
18278 });
18279 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
18280
18281 let mut cx = EditorTestContext::for_editor(editor, cx).await;
18282
18283 cx.update_editor(|editor, window, cx| {
18284 editor.change_selections(None, window, cx, |selections| {
18285 selections.select(vec![
18286 Selection::from_offset(4),
18287 Selection::from_offset(9),
18288 Selection::from_offset(15),
18289 ])
18290 })
18291 });
18292 cx.run_until_parked();
18293
18294 cx.update_editor(|editor, window, cx| {
18295 editor.handle_input(">", window, cx);
18296 });
18297 cx.run_until_parked();
18298
18299 cx.assert_editor_state("<div>ˇ</div>\n<pre>ˇ</pre>\n<span>ˇ");
18300 }
18301}
18302
18303fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
18304 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
18305 point..point
18306}
18307
18308fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
18309 let (text, ranges) = marked_text_ranges(marked_text, true);
18310 assert_eq!(editor.text(cx), text);
18311 assert_eq!(
18312 editor.selections.ranges(cx),
18313 ranges,
18314 "Assert selections are {}",
18315 marked_text
18316 );
18317}
18318
18319pub fn handle_signature_help_request(
18320 cx: &mut EditorLspTestContext,
18321 mocked_response: lsp::SignatureHelp,
18322) -> impl Future<Output = ()> {
18323 let mut request =
18324 cx.handle_request::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
18325 let mocked_response = mocked_response.clone();
18326 async move { Ok(Some(mocked_response)) }
18327 });
18328
18329 async move {
18330 request.next().await;
18331 }
18332}
18333
18334/// Handle completion request passing a marked string specifying where the completion
18335/// should be triggered from using '|' character, what range should be replaced, and what completions
18336/// should be returned using '<' and '>' to delimit the range
18337pub fn handle_completion_request(
18338 cx: &mut EditorLspTestContext,
18339 marked_string: &str,
18340 completions: Vec<&'static str>,
18341 counter: Arc<AtomicUsize>,
18342) -> impl Future<Output = ()> {
18343 let complete_from_marker: TextRangeMarker = '|'.into();
18344 let replace_range_marker: TextRangeMarker = ('<', '>').into();
18345 let (_, mut marked_ranges) = marked_text_ranges_by(
18346 marked_string,
18347 vec![complete_from_marker.clone(), replace_range_marker.clone()],
18348 );
18349
18350 let complete_from_position =
18351 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
18352 let replace_range =
18353 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
18354
18355 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
18356 let completions = completions.clone();
18357 counter.fetch_add(1, atomic::Ordering::Release);
18358 async move {
18359 assert_eq!(params.text_document_position.text_document.uri, url.clone());
18360 assert_eq!(
18361 params.text_document_position.position,
18362 complete_from_position
18363 );
18364 Ok(Some(lsp::CompletionResponse::Array(
18365 completions
18366 .iter()
18367 .map(|completion_text| lsp::CompletionItem {
18368 label: completion_text.to_string(),
18369 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18370 range: replace_range,
18371 new_text: completion_text.to_string(),
18372 })),
18373 ..Default::default()
18374 })
18375 .collect(),
18376 )))
18377 }
18378 });
18379
18380 async move {
18381 request.next().await;
18382 }
18383}
18384
18385fn handle_resolve_completion_request(
18386 cx: &mut EditorLspTestContext,
18387 edits: Option<Vec<(&'static str, &'static str)>>,
18388) -> impl Future<Output = ()> {
18389 let edits = edits.map(|edits| {
18390 edits
18391 .iter()
18392 .map(|(marked_string, new_text)| {
18393 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
18394 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
18395 lsp::TextEdit::new(replace_range, new_text.to_string())
18396 })
18397 .collect::<Vec<_>>()
18398 });
18399
18400 let mut request =
18401 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18402 let edits = edits.clone();
18403 async move {
18404 Ok(lsp::CompletionItem {
18405 additional_text_edits: edits,
18406 ..Default::default()
18407 })
18408 }
18409 });
18410
18411 async move {
18412 request.next().await;
18413 }
18414}
18415
18416pub(crate) fn update_test_language_settings(
18417 cx: &mut TestAppContext,
18418 f: impl Fn(&mut AllLanguageSettingsContent),
18419) {
18420 cx.update(|cx| {
18421 SettingsStore::update_global(cx, |store, cx| {
18422 store.update_user_settings::<AllLanguageSettings>(cx, f);
18423 });
18424 });
18425}
18426
18427pub(crate) fn update_test_project_settings(
18428 cx: &mut TestAppContext,
18429 f: impl Fn(&mut ProjectSettings),
18430) {
18431 cx.update(|cx| {
18432 SettingsStore::update_global(cx, |store, cx| {
18433 store.update_user_settings::<ProjectSettings>(cx, f);
18434 });
18435 });
18436}
18437
18438pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
18439 cx.update(|cx| {
18440 assets::Assets.load_test_fonts(cx);
18441 let store = SettingsStore::test(cx);
18442 cx.set_global(store);
18443 theme::init(theme::LoadThemes::JustBase, cx);
18444 release_channel::init(SemanticVersion::default(), cx);
18445 client::init_settings(cx);
18446 language::init(cx);
18447 Project::init_settings(cx);
18448 workspace::init_settings(cx);
18449 crate::init(cx);
18450 });
18451
18452 update_test_language_settings(cx, f);
18453}
18454
18455#[track_caller]
18456fn assert_hunk_revert(
18457 not_reverted_text_with_selections: &str,
18458 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
18459 expected_reverted_text_with_selections: &str,
18460 base_text: &str,
18461 cx: &mut EditorLspTestContext,
18462) {
18463 cx.set_state(not_reverted_text_with_selections);
18464 cx.set_head_text(base_text);
18465 cx.executor().run_until_parked();
18466
18467 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
18468 let snapshot = editor.snapshot(window, cx);
18469 let reverted_hunk_statuses = snapshot
18470 .buffer_snapshot
18471 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
18472 .map(|hunk| hunk.status().kind)
18473 .collect::<Vec<_>>();
18474
18475 editor.git_restore(&Default::default(), window, cx);
18476 reverted_hunk_statuses
18477 });
18478 cx.executor().run_until_parked();
18479 cx.assert_editor_state(expected_reverted_text_with_selections);
18480 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
18481}