1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 inline_completion_tests::FakeInlineCompletionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use futures::StreamExt;
17use gpui::{
18 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
19 VisualTestContext, WindowBounds, WindowOptions, div,
20};
21use indoc::indoc;
22use language::{
23 BracketPairConfig,
24 Capability::ReadWrite,
25 DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
26 LanguageName, Override, Point,
27 language_settings::{
28 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
29 LanguageSettingsContent, LspInsertMode, PrettierSettings,
30 },
31 tree_sitter_python,
32};
33use language_settings::{Formatter, IndentGuideSettings};
34use lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::{LspSettings, ProjectSettings},
42};
43use serde_json::{self, json};
44use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
45use std::{
46 iter,
47 sync::atomic::{self, AtomicUsize},
48};
49use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
50use text::ToPoint as _;
51use unindent::Unindent;
52use util::{
53 assert_set_eq, path,
54 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
55 uri,
56};
57use workspace::{
58 CloseActiveItem, CloseAllItems, CloseInactiveItems, MoveItemToPaneInDirection, NavigationEntry,
59 OpenOptions, ViewId,
60 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
61};
62
63#[gpui::test]
64fn test_edit_events(cx: &mut TestAppContext) {
65 init_test(cx, |_| {});
66
67 let buffer = cx.new(|cx| {
68 let mut buffer = language::Buffer::local("123456", cx);
69 buffer.set_group_interval(Duration::from_secs(1));
70 buffer
71 });
72
73 let events = Rc::new(RefCell::new(Vec::new()));
74 let editor1 = cx.add_window({
75 let events = events.clone();
76 |window, cx| {
77 let entity = cx.entity().clone();
78 cx.subscribe_in(
79 &entity,
80 window,
81 move |_, _, event: &EditorEvent, _, _| match event {
82 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
83 EditorEvent::BufferEdited => {
84 events.borrow_mut().push(("editor1", "buffer edited"))
85 }
86 _ => {}
87 },
88 )
89 .detach();
90 Editor::for_buffer(buffer.clone(), None, window, cx)
91 }
92 });
93
94 let editor2 = cx.add_window({
95 let events = events.clone();
96 |window, cx| {
97 cx.subscribe_in(
98 &cx.entity().clone(),
99 window,
100 move |_, _, event: &EditorEvent, _, _| match event {
101 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
102 EditorEvent::BufferEdited => {
103 events.borrow_mut().push(("editor2", "buffer edited"))
104 }
105 _ => {}
106 },
107 )
108 .detach();
109 Editor::for_buffer(buffer.clone(), None, window, cx)
110 }
111 });
112
113 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
114
115 // Mutating editor 1 will emit an `Edited` event only for that editor.
116 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
117 assert_eq!(
118 mem::take(&mut *events.borrow_mut()),
119 [
120 ("editor1", "edited"),
121 ("editor1", "buffer edited"),
122 ("editor2", "buffer edited"),
123 ]
124 );
125
126 // Mutating editor 2 will emit an `Edited` event only for that editor.
127 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
128 assert_eq!(
129 mem::take(&mut *events.borrow_mut()),
130 [
131 ("editor2", "edited"),
132 ("editor1", "buffer edited"),
133 ("editor2", "buffer edited"),
134 ]
135 );
136
137 // Undoing on editor 1 will emit an `Edited` event only for that editor.
138 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
139 assert_eq!(
140 mem::take(&mut *events.borrow_mut()),
141 [
142 ("editor1", "edited"),
143 ("editor1", "buffer edited"),
144 ("editor2", "buffer edited"),
145 ]
146 );
147
148 // Redoing on editor 1 will emit an `Edited` event only for that editor.
149 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
150 assert_eq!(
151 mem::take(&mut *events.borrow_mut()),
152 [
153 ("editor1", "edited"),
154 ("editor1", "buffer edited"),
155 ("editor2", "buffer edited"),
156 ]
157 );
158
159 // Undoing on editor 2 will emit an `Edited` event only for that editor.
160 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
161 assert_eq!(
162 mem::take(&mut *events.borrow_mut()),
163 [
164 ("editor2", "edited"),
165 ("editor1", "buffer edited"),
166 ("editor2", "buffer edited"),
167 ]
168 );
169
170 // Redoing on editor 2 will emit an `Edited` event only for that editor.
171 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
172 assert_eq!(
173 mem::take(&mut *events.borrow_mut()),
174 [
175 ("editor2", "edited"),
176 ("editor1", "buffer edited"),
177 ("editor2", "buffer edited"),
178 ]
179 );
180
181 // No event is emitted when the mutation is a no-op.
182 _ = editor2.update(cx, |editor, window, cx| {
183 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
184 s.select_ranges([0..0])
185 });
186
187 editor.backspace(&Backspace, window, cx);
188 });
189 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
190}
191
192#[gpui::test]
193fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
194 init_test(cx, |_| {});
195
196 let mut now = Instant::now();
197 let group_interval = Duration::from_millis(1);
198 let buffer = cx.new(|cx| {
199 let mut buf = language::Buffer::local("123456", cx);
200 buf.set_group_interval(group_interval);
201 buf
202 });
203 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
204 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
205
206 _ = editor.update(cx, |editor, window, cx| {
207 editor.start_transaction_at(now, window, cx);
208 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
209 s.select_ranges([2..4])
210 });
211
212 editor.insert("cd", window, cx);
213 editor.end_transaction_at(now, cx);
214 assert_eq!(editor.text(cx), "12cd56");
215 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
216
217 editor.start_transaction_at(now, window, cx);
218 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
219 s.select_ranges([4..5])
220 });
221 editor.insert("e", window, cx);
222 editor.end_transaction_at(now, cx);
223 assert_eq!(editor.text(cx), "12cde6");
224 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
225
226 now += group_interval + Duration::from_millis(1);
227 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
228 s.select_ranges([2..2])
229 });
230
231 // Simulate an edit in another editor
232 buffer.update(cx, |buffer, cx| {
233 buffer.start_transaction_at(now, cx);
234 buffer.edit([(0..1, "a")], None, cx);
235 buffer.edit([(1..1, "b")], None, cx);
236 buffer.end_transaction_at(now, cx);
237 });
238
239 assert_eq!(editor.text(cx), "ab2cde6");
240 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
241
242 // Last transaction happened past the group interval in a different editor.
243 // Undo it individually and don't restore selections.
244 editor.undo(&Undo, window, cx);
245 assert_eq!(editor.text(cx), "12cde6");
246 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
247
248 // First two transactions happened within the group interval in this editor.
249 // Undo them together and restore selections.
250 editor.undo(&Undo, window, cx);
251 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
252 assert_eq!(editor.text(cx), "123456");
253 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
254
255 // Redo the first two transactions together.
256 editor.redo(&Redo, window, cx);
257 assert_eq!(editor.text(cx), "12cde6");
258 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
259
260 // Redo the last transaction on its own.
261 editor.redo(&Redo, window, cx);
262 assert_eq!(editor.text(cx), "ab2cde6");
263 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
264
265 // Test empty transactions.
266 editor.start_transaction_at(now, window, cx);
267 editor.end_transaction_at(now, cx);
268 editor.undo(&Undo, window, cx);
269 assert_eq!(editor.text(cx), "12cde6");
270 });
271}
272
273#[gpui::test]
274fn test_ime_composition(cx: &mut TestAppContext) {
275 init_test(cx, |_| {});
276
277 let buffer = cx.new(|cx| {
278 let mut buffer = language::Buffer::local("abcde", cx);
279 // Ensure automatic grouping doesn't occur.
280 buffer.set_group_interval(Duration::ZERO);
281 buffer
282 });
283
284 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
285 cx.add_window(|window, cx| {
286 let mut editor = build_editor(buffer.clone(), window, cx);
287
288 // Start a new IME composition.
289 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
290 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
291 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
292 assert_eq!(editor.text(cx), "äbcde");
293 assert_eq!(
294 editor.marked_text_ranges(cx),
295 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
296 );
297
298 // Finalize IME composition.
299 editor.replace_text_in_range(None, "ā", window, cx);
300 assert_eq!(editor.text(cx), "ābcde");
301 assert_eq!(editor.marked_text_ranges(cx), None);
302
303 // IME composition edits are grouped and are undone/redone at once.
304 editor.undo(&Default::default(), window, cx);
305 assert_eq!(editor.text(cx), "abcde");
306 assert_eq!(editor.marked_text_ranges(cx), None);
307 editor.redo(&Default::default(), window, cx);
308 assert_eq!(editor.text(cx), "ābcde");
309 assert_eq!(editor.marked_text_ranges(cx), None);
310
311 // Start a new IME composition.
312 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
313 assert_eq!(
314 editor.marked_text_ranges(cx),
315 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
316 );
317
318 // Undoing during an IME composition cancels it.
319 editor.undo(&Default::default(), window, cx);
320 assert_eq!(editor.text(cx), "ābcde");
321 assert_eq!(editor.marked_text_ranges(cx), None);
322
323 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
324 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
325 assert_eq!(editor.text(cx), "ābcdè");
326 assert_eq!(
327 editor.marked_text_ranges(cx),
328 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
329 );
330
331 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
332 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
333 assert_eq!(editor.text(cx), "ābcdę");
334 assert_eq!(editor.marked_text_ranges(cx), None);
335
336 // Start a new IME composition with multiple cursors.
337 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
338 s.select_ranges([
339 OffsetUtf16(1)..OffsetUtf16(1),
340 OffsetUtf16(3)..OffsetUtf16(3),
341 OffsetUtf16(5)..OffsetUtf16(5),
342 ])
343 });
344 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
345 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
346 assert_eq!(
347 editor.marked_text_ranges(cx),
348 Some(vec![
349 OffsetUtf16(0)..OffsetUtf16(3),
350 OffsetUtf16(4)..OffsetUtf16(7),
351 OffsetUtf16(8)..OffsetUtf16(11)
352 ])
353 );
354
355 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
356 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
357 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
358 assert_eq!(
359 editor.marked_text_ranges(cx),
360 Some(vec![
361 OffsetUtf16(1)..OffsetUtf16(2),
362 OffsetUtf16(5)..OffsetUtf16(6),
363 OffsetUtf16(9)..OffsetUtf16(10)
364 ])
365 );
366
367 // Finalize IME composition with multiple cursors.
368 editor.replace_text_in_range(Some(9..10), "2", window, cx);
369 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
370 assert_eq!(editor.marked_text_ranges(cx), None);
371
372 editor
373 });
374}
375
376#[gpui::test]
377fn test_selection_with_mouse(cx: &mut TestAppContext) {
378 init_test(cx, |_| {});
379
380 let editor = cx.add_window(|window, cx| {
381 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
382 build_editor(buffer, window, cx)
383 });
384
385 _ = editor.update(cx, |editor, window, cx| {
386 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
387 });
388 assert_eq!(
389 editor
390 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
391 .unwrap(),
392 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
393 );
394
395 _ = editor.update(cx, |editor, window, cx| {
396 editor.update_selection(
397 DisplayPoint::new(DisplayRow(3), 3),
398 0,
399 gpui::Point::<f32>::default(),
400 window,
401 cx,
402 );
403 });
404
405 assert_eq!(
406 editor
407 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
408 .unwrap(),
409 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
410 );
411
412 _ = editor.update(cx, |editor, window, cx| {
413 editor.update_selection(
414 DisplayPoint::new(DisplayRow(1), 1),
415 0,
416 gpui::Point::<f32>::default(),
417 window,
418 cx,
419 );
420 });
421
422 assert_eq!(
423 editor
424 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
425 .unwrap(),
426 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
427 );
428
429 _ = editor.update(cx, |editor, window, cx| {
430 editor.end_selection(window, cx);
431 editor.update_selection(
432 DisplayPoint::new(DisplayRow(3), 3),
433 0,
434 gpui::Point::<f32>::default(),
435 window,
436 cx,
437 );
438 });
439
440 assert_eq!(
441 editor
442 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
443 .unwrap(),
444 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
445 );
446
447 _ = editor.update(cx, |editor, window, cx| {
448 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
449 editor.update_selection(
450 DisplayPoint::new(DisplayRow(0), 0),
451 0,
452 gpui::Point::<f32>::default(),
453 window,
454 cx,
455 );
456 });
457
458 assert_eq!(
459 editor
460 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
461 .unwrap(),
462 [
463 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
464 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
465 ]
466 );
467
468 _ = editor.update(cx, |editor, window, cx| {
469 editor.end_selection(window, cx);
470 });
471
472 assert_eq!(
473 editor
474 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
475 .unwrap(),
476 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
477 );
478}
479
480#[gpui::test]
481fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
482 init_test(cx, |_| {});
483
484 let editor = cx.add_window(|window, cx| {
485 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
486 build_editor(buffer, window, cx)
487 });
488
489 _ = editor.update(cx, |editor, window, cx| {
490 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
491 });
492
493 _ = editor.update(cx, |editor, window, cx| {
494 editor.end_selection(window, cx);
495 });
496
497 _ = editor.update(cx, |editor, window, cx| {
498 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
499 });
500
501 _ = editor.update(cx, |editor, window, cx| {
502 editor.end_selection(window, cx);
503 });
504
505 assert_eq!(
506 editor
507 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
508 .unwrap(),
509 [
510 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
511 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
512 ]
513 );
514
515 _ = editor.update(cx, |editor, window, cx| {
516 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
517 });
518
519 _ = editor.update(cx, |editor, window, cx| {
520 editor.end_selection(window, cx);
521 });
522
523 assert_eq!(
524 editor
525 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
526 .unwrap(),
527 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
528 );
529}
530
531#[gpui::test]
532fn test_canceling_pending_selection(cx: &mut TestAppContext) {
533 init_test(cx, |_| {});
534
535 let editor = cx.add_window(|window, cx| {
536 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
537 build_editor(buffer, window, cx)
538 });
539
540 _ = editor.update(cx, |editor, window, cx| {
541 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
542 assert_eq!(
543 editor.selections.display_ranges(cx),
544 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
545 );
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.update_selection(
550 DisplayPoint::new(DisplayRow(3), 3),
551 0,
552 gpui::Point::<f32>::default(),
553 window,
554 cx,
555 );
556 assert_eq!(
557 editor.selections.display_ranges(cx),
558 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
559 );
560 });
561
562 _ = editor.update(cx, |editor, window, cx| {
563 editor.cancel(&Cancel, window, cx);
564 editor.update_selection(
565 DisplayPoint::new(DisplayRow(1), 1),
566 0,
567 gpui::Point::<f32>::default(),
568 window,
569 cx,
570 );
571 assert_eq!(
572 editor.selections.display_ranges(cx),
573 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
574 );
575 });
576}
577
578#[gpui::test]
579fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
580 init_test(cx, |_| {});
581
582 let editor = cx.add_window(|window, cx| {
583 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
584 build_editor(buffer, window, cx)
585 });
586
587 _ = editor.update(cx, |editor, window, cx| {
588 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
589 assert_eq!(
590 editor.selections.display_ranges(cx),
591 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
592 );
593
594 editor.move_down(&Default::default(), window, cx);
595 assert_eq!(
596 editor.selections.display_ranges(cx),
597 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
598 );
599
600 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
601 assert_eq!(
602 editor.selections.display_ranges(cx),
603 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
604 );
605
606 editor.move_up(&Default::default(), window, cx);
607 assert_eq!(
608 editor.selections.display_ranges(cx),
609 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
610 );
611 });
612}
613
614#[gpui::test]
615fn test_clone(cx: &mut TestAppContext) {
616 init_test(cx, |_| {});
617
618 let (text, selection_ranges) = marked_text_ranges(
619 indoc! {"
620 one
621 two
622 threeˇ
623 four
624 fiveˇ
625 "},
626 true,
627 );
628
629 let editor = cx.add_window(|window, cx| {
630 let buffer = MultiBuffer::build_simple(&text, cx);
631 build_editor(buffer, window, cx)
632 });
633
634 _ = editor.update(cx, |editor, window, cx| {
635 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
636 s.select_ranges(selection_ranges.clone())
637 });
638 editor.fold_creases(
639 vec![
640 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
641 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
642 ],
643 true,
644 window,
645 cx,
646 );
647 });
648
649 let cloned_editor = editor
650 .update(cx, |editor, _, cx| {
651 cx.open_window(Default::default(), |window, cx| {
652 cx.new(|cx| editor.clone(window, cx))
653 })
654 })
655 .unwrap()
656 .unwrap();
657
658 let snapshot = editor
659 .update(cx, |e, window, cx| e.snapshot(window, cx))
660 .unwrap();
661 let cloned_snapshot = cloned_editor
662 .update(cx, |e, window, cx| e.snapshot(window, cx))
663 .unwrap();
664
665 assert_eq!(
666 cloned_editor
667 .update(cx, |e, _, cx| e.display_text(cx))
668 .unwrap(),
669 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
670 );
671 assert_eq!(
672 cloned_snapshot
673 .folds_in_range(0..text.len())
674 .collect::<Vec<_>>(),
675 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
676 );
677 assert_set_eq!(
678 cloned_editor
679 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
680 .unwrap(),
681 editor
682 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
683 .unwrap()
684 );
685 assert_set_eq!(
686 cloned_editor
687 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
688 .unwrap(),
689 editor
690 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
691 .unwrap()
692 );
693}
694
695#[gpui::test]
696async fn test_navigation_history(cx: &mut TestAppContext) {
697 init_test(cx, |_| {});
698
699 use workspace::item::Item;
700
701 let fs = FakeFs::new(cx.executor());
702 let project = Project::test(fs, [], cx).await;
703 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
704 let pane = workspace
705 .update(cx, |workspace, _, _| workspace.active_pane().clone())
706 .unwrap();
707
708 _ = workspace.update(cx, |_v, window, cx| {
709 cx.new(|cx| {
710 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
711 let mut editor = build_editor(buffer.clone(), window, cx);
712 let handle = cx.entity();
713 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
714
715 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
716 editor.nav_history.as_mut().unwrap().pop_backward(cx)
717 }
718
719 // Move the cursor a small distance.
720 // Nothing is added to the navigation history.
721 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
722 s.select_display_ranges([
723 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
724 ])
725 });
726 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
727 s.select_display_ranges([
728 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
729 ])
730 });
731 assert!(pop_history(&mut editor, cx).is_none());
732
733 // Move the cursor a large distance.
734 // The history can jump back to the previous position.
735 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
736 s.select_display_ranges([
737 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
738 ])
739 });
740 let nav_entry = pop_history(&mut editor, cx).unwrap();
741 editor.navigate(nav_entry.data.unwrap(), window, cx);
742 assert_eq!(nav_entry.item.id(), cx.entity_id());
743 assert_eq!(
744 editor.selections.display_ranges(cx),
745 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
746 );
747 assert!(pop_history(&mut editor, cx).is_none());
748
749 // Move the cursor a small distance via the mouse.
750 // Nothing is added to the navigation history.
751 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
752 editor.end_selection(window, cx);
753 assert_eq!(
754 editor.selections.display_ranges(cx),
755 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
756 );
757 assert!(pop_history(&mut editor, cx).is_none());
758
759 // Move the cursor a large distance via the mouse.
760 // The history can jump back to the previous position.
761 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
762 editor.end_selection(window, cx);
763 assert_eq!(
764 editor.selections.display_ranges(cx),
765 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
766 );
767 let nav_entry = pop_history(&mut editor, cx).unwrap();
768 editor.navigate(nav_entry.data.unwrap(), window, cx);
769 assert_eq!(nav_entry.item.id(), cx.entity_id());
770 assert_eq!(
771 editor.selections.display_ranges(cx),
772 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
773 );
774 assert!(pop_history(&mut editor, cx).is_none());
775
776 // Set scroll position to check later
777 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
778 let original_scroll_position = editor.scroll_manager.anchor();
779
780 // Jump to the end of the document and adjust scroll
781 editor.move_to_end(&MoveToEnd, window, cx);
782 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
783 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
784
785 let nav_entry = pop_history(&mut editor, cx).unwrap();
786 editor.navigate(nav_entry.data.unwrap(), window, cx);
787 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
788
789 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
790 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
791 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
792 let invalid_point = Point::new(9999, 0);
793 editor.navigate(
794 Box::new(NavigationData {
795 cursor_anchor: invalid_anchor,
796 cursor_position: invalid_point,
797 scroll_anchor: ScrollAnchor {
798 anchor: invalid_anchor,
799 offset: Default::default(),
800 },
801 scroll_top_row: invalid_point.row,
802 }),
803 window,
804 cx,
805 );
806 assert_eq!(
807 editor.selections.display_ranges(cx),
808 &[editor.max_point(cx)..editor.max_point(cx)]
809 );
810 assert_eq!(
811 editor.scroll_position(cx),
812 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
813 );
814
815 editor
816 })
817 });
818}
819
820#[gpui::test]
821fn test_cancel(cx: &mut TestAppContext) {
822 init_test(cx, |_| {});
823
824 let editor = cx.add_window(|window, cx| {
825 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
826 build_editor(buffer, window, cx)
827 });
828
829 _ = editor.update(cx, |editor, window, cx| {
830 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
831 editor.update_selection(
832 DisplayPoint::new(DisplayRow(1), 1),
833 0,
834 gpui::Point::<f32>::default(),
835 window,
836 cx,
837 );
838 editor.end_selection(window, cx);
839
840 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
841 editor.update_selection(
842 DisplayPoint::new(DisplayRow(0), 3),
843 0,
844 gpui::Point::<f32>::default(),
845 window,
846 cx,
847 );
848 editor.end_selection(window, cx);
849 assert_eq!(
850 editor.selections.display_ranges(cx),
851 [
852 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
853 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
854 ]
855 );
856 });
857
858 _ = editor.update(cx, |editor, window, cx| {
859 editor.cancel(&Cancel, window, cx);
860 assert_eq!(
861 editor.selections.display_ranges(cx),
862 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
863 );
864 });
865
866 _ = editor.update(cx, |editor, window, cx| {
867 editor.cancel(&Cancel, window, cx);
868 assert_eq!(
869 editor.selections.display_ranges(cx),
870 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
871 );
872 });
873}
874
875#[gpui::test]
876fn test_fold_action(cx: &mut TestAppContext) {
877 init_test(cx, |_| {});
878
879 let editor = cx.add_window(|window, cx| {
880 let buffer = MultiBuffer::build_simple(
881 &"
882 impl Foo {
883 // Hello!
884
885 fn a() {
886 1
887 }
888
889 fn b() {
890 2
891 }
892
893 fn c() {
894 3
895 }
896 }
897 "
898 .unindent(),
899 cx,
900 );
901 build_editor(buffer.clone(), window, cx)
902 });
903
904 _ = editor.update(cx, |editor, window, cx| {
905 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
906 s.select_display_ranges([
907 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
908 ]);
909 });
910 editor.fold(&Fold, window, cx);
911 assert_eq!(
912 editor.display_text(cx),
913 "
914 impl Foo {
915 // Hello!
916
917 fn a() {
918 1
919 }
920
921 fn b() {⋯
922 }
923
924 fn c() {⋯
925 }
926 }
927 "
928 .unindent(),
929 );
930
931 editor.fold(&Fold, window, cx);
932 assert_eq!(
933 editor.display_text(cx),
934 "
935 impl Foo {⋯
936 }
937 "
938 .unindent(),
939 );
940
941 editor.unfold_lines(&UnfoldLines, window, cx);
942 assert_eq!(
943 editor.display_text(cx),
944 "
945 impl Foo {
946 // Hello!
947
948 fn a() {
949 1
950 }
951
952 fn b() {⋯
953 }
954
955 fn c() {⋯
956 }
957 }
958 "
959 .unindent(),
960 );
961
962 editor.unfold_lines(&UnfoldLines, window, cx);
963 assert_eq!(
964 editor.display_text(cx),
965 editor.buffer.read(cx).read(cx).text()
966 );
967 });
968}
969
970#[gpui::test]
971fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
972 init_test(cx, |_| {});
973
974 let editor = cx.add_window(|window, cx| {
975 let buffer = MultiBuffer::build_simple(
976 &"
977 class Foo:
978 # Hello!
979
980 def a():
981 print(1)
982
983 def b():
984 print(2)
985
986 def c():
987 print(3)
988 "
989 .unindent(),
990 cx,
991 );
992 build_editor(buffer.clone(), window, cx)
993 });
994
995 _ = editor.update(cx, |editor, window, cx| {
996 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
997 s.select_display_ranges([
998 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
999 ]);
1000 });
1001 editor.fold(&Fold, window, cx);
1002 assert_eq!(
1003 editor.display_text(cx),
1004 "
1005 class Foo:
1006 # Hello!
1007
1008 def a():
1009 print(1)
1010
1011 def b():⋯
1012
1013 def c():⋯
1014 "
1015 .unindent(),
1016 );
1017
1018 editor.fold(&Fold, window, cx);
1019 assert_eq!(
1020 editor.display_text(cx),
1021 "
1022 class Foo:⋯
1023 "
1024 .unindent(),
1025 );
1026
1027 editor.unfold_lines(&UnfoldLines, window, cx);
1028 assert_eq!(
1029 editor.display_text(cx),
1030 "
1031 class Foo:
1032 # Hello!
1033
1034 def a():
1035 print(1)
1036
1037 def b():⋯
1038
1039 def c():⋯
1040 "
1041 .unindent(),
1042 );
1043
1044 editor.unfold_lines(&UnfoldLines, window, cx);
1045 assert_eq!(
1046 editor.display_text(cx),
1047 editor.buffer.read(cx).read(cx).text()
1048 );
1049 });
1050}
1051
1052#[gpui::test]
1053fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1054 init_test(cx, |_| {});
1055
1056 let editor = cx.add_window(|window, cx| {
1057 let buffer = MultiBuffer::build_simple(
1058 &"
1059 class Foo:
1060 # Hello!
1061
1062 def a():
1063 print(1)
1064
1065 def b():
1066 print(2)
1067
1068
1069 def c():
1070 print(3)
1071
1072
1073 "
1074 .unindent(),
1075 cx,
1076 );
1077 build_editor(buffer.clone(), window, cx)
1078 });
1079
1080 _ = editor.update(cx, |editor, window, cx| {
1081 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1082 s.select_display_ranges([
1083 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1084 ]);
1085 });
1086 editor.fold(&Fold, window, cx);
1087 assert_eq!(
1088 editor.display_text(cx),
1089 "
1090 class Foo:
1091 # Hello!
1092
1093 def a():
1094 print(1)
1095
1096 def b():⋯
1097
1098
1099 def c():⋯
1100
1101
1102 "
1103 .unindent(),
1104 );
1105
1106 editor.fold(&Fold, window, cx);
1107 assert_eq!(
1108 editor.display_text(cx),
1109 "
1110 class Foo:⋯
1111
1112
1113 "
1114 .unindent(),
1115 );
1116
1117 editor.unfold_lines(&UnfoldLines, window, cx);
1118 assert_eq!(
1119 editor.display_text(cx),
1120 "
1121 class Foo:
1122 # Hello!
1123
1124 def a():
1125 print(1)
1126
1127 def b():⋯
1128
1129
1130 def c():⋯
1131
1132
1133 "
1134 .unindent(),
1135 );
1136
1137 editor.unfold_lines(&UnfoldLines, window, cx);
1138 assert_eq!(
1139 editor.display_text(cx),
1140 editor.buffer.read(cx).read(cx).text()
1141 );
1142 });
1143}
1144
1145#[gpui::test]
1146fn test_fold_at_level(cx: &mut TestAppContext) {
1147 init_test(cx, |_| {});
1148
1149 let editor = cx.add_window(|window, cx| {
1150 let buffer = MultiBuffer::build_simple(
1151 &"
1152 class Foo:
1153 # Hello!
1154
1155 def a():
1156 print(1)
1157
1158 def b():
1159 print(2)
1160
1161
1162 class Bar:
1163 # World!
1164
1165 def a():
1166 print(1)
1167
1168 def b():
1169 print(2)
1170
1171
1172 "
1173 .unindent(),
1174 cx,
1175 );
1176 build_editor(buffer.clone(), window, cx)
1177 });
1178
1179 _ = editor.update(cx, |editor, window, cx| {
1180 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1181 assert_eq!(
1182 editor.display_text(cx),
1183 "
1184 class Foo:
1185 # Hello!
1186
1187 def a():⋯
1188
1189 def b():⋯
1190
1191
1192 class Bar:
1193 # World!
1194
1195 def a():⋯
1196
1197 def b():⋯
1198
1199
1200 "
1201 .unindent(),
1202 );
1203
1204 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1205 assert_eq!(
1206 editor.display_text(cx),
1207 "
1208 class Foo:⋯
1209
1210
1211 class Bar:⋯
1212
1213
1214 "
1215 .unindent(),
1216 );
1217
1218 editor.unfold_all(&UnfoldAll, window, cx);
1219 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1220 assert_eq!(
1221 editor.display_text(cx),
1222 "
1223 class Foo:
1224 # Hello!
1225
1226 def a():
1227 print(1)
1228
1229 def b():
1230 print(2)
1231
1232
1233 class Bar:
1234 # World!
1235
1236 def a():
1237 print(1)
1238
1239 def b():
1240 print(2)
1241
1242
1243 "
1244 .unindent(),
1245 );
1246
1247 assert_eq!(
1248 editor.display_text(cx),
1249 editor.buffer.read(cx).read(cx).text()
1250 );
1251 });
1252}
1253
1254#[gpui::test]
1255fn test_move_cursor(cx: &mut TestAppContext) {
1256 init_test(cx, |_| {});
1257
1258 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1259 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1260
1261 buffer.update(cx, |buffer, cx| {
1262 buffer.edit(
1263 vec![
1264 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1265 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1266 ],
1267 None,
1268 cx,
1269 );
1270 });
1271 _ = editor.update(cx, |editor, window, cx| {
1272 assert_eq!(
1273 editor.selections.display_ranges(cx),
1274 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1275 );
1276
1277 editor.move_down(&MoveDown, window, cx);
1278 assert_eq!(
1279 editor.selections.display_ranges(cx),
1280 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1281 );
1282
1283 editor.move_right(&MoveRight, window, cx);
1284 assert_eq!(
1285 editor.selections.display_ranges(cx),
1286 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1287 );
1288
1289 editor.move_left(&MoveLeft, window, cx);
1290 assert_eq!(
1291 editor.selections.display_ranges(cx),
1292 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1293 );
1294
1295 editor.move_up(&MoveUp, window, cx);
1296 assert_eq!(
1297 editor.selections.display_ranges(cx),
1298 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1299 );
1300
1301 editor.move_to_end(&MoveToEnd, window, cx);
1302 assert_eq!(
1303 editor.selections.display_ranges(cx),
1304 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1305 );
1306
1307 editor.move_to_beginning(&MoveToBeginning, window, cx);
1308 assert_eq!(
1309 editor.selections.display_ranges(cx),
1310 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1311 );
1312
1313 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1314 s.select_display_ranges([
1315 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1316 ]);
1317 });
1318 editor.select_to_beginning(&SelectToBeginning, window, cx);
1319 assert_eq!(
1320 editor.selections.display_ranges(cx),
1321 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1322 );
1323
1324 editor.select_to_end(&SelectToEnd, window, cx);
1325 assert_eq!(
1326 editor.selections.display_ranges(cx),
1327 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1328 );
1329 });
1330}
1331
1332#[gpui::test]
1333fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1334 init_test(cx, |_| {});
1335
1336 let editor = cx.add_window(|window, cx| {
1337 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1338 build_editor(buffer.clone(), window, cx)
1339 });
1340
1341 assert_eq!('🟥'.len_utf8(), 4);
1342 assert_eq!('α'.len_utf8(), 2);
1343
1344 _ = editor.update(cx, |editor, window, cx| {
1345 editor.fold_creases(
1346 vec![
1347 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1348 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1349 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1350 ],
1351 true,
1352 window,
1353 cx,
1354 );
1355 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1356
1357 editor.move_right(&MoveRight, window, cx);
1358 assert_eq!(
1359 editor.selections.display_ranges(cx),
1360 &[empty_range(0, "🟥".len())]
1361 );
1362 editor.move_right(&MoveRight, window, cx);
1363 assert_eq!(
1364 editor.selections.display_ranges(cx),
1365 &[empty_range(0, "🟥🟧".len())]
1366 );
1367 editor.move_right(&MoveRight, window, cx);
1368 assert_eq!(
1369 editor.selections.display_ranges(cx),
1370 &[empty_range(0, "🟥🟧⋯".len())]
1371 );
1372
1373 editor.move_down(&MoveDown, window, cx);
1374 assert_eq!(
1375 editor.selections.display_ranges(cx),
1376 &[empty_range(1, "ab⋯e".len())]
1377 );
1378 editor.move_left(&MoveLeft, window, cx);
1379 assert_eq!(
1380 editor.selections.display_ranges(cx),
1381 &[empty_range(1, "ab⋯".len())]
1382 );
1383 editor.move_left(&MoveLeft, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[empty_range(1, "ab".len())]
1387 );
1388 editor.move_left(&MoveLeft, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[empty_range(1, "a".len())]
1392 );
1393
1394 editor.move_down(&MoveDown, window, cx);
1395 assert_eq!(
1396 editor.selections.display_ranges(cx),
1397 &[empty_range(2, "α".len())]
1398 );
1399 editor.move_right(&MoveRight, window, cx);
1400 assert_eq!(
1401 editor.selections.display_ranges(cx),
1402 &[empty_range(2, "αβ".len())]
1403 );
1404 editor.move_right(&MoveRight, window, cx);
1405 assert_eq!(
1406 editor.selections.display_ranges(cx),
1407 &[empty_range(2, "αβ⋯".len())]
1408 );
1409 editor.move_right(&MoveRight, window, cx);
1410 assert_eq!(
1411 editor.selections.display_ranges(cx),
1412 &[empty_range(2, "αβ⋯ε".len())]
1413 );
1414
1415 editor.move_up(&MoveUp, window, cx);
1416 assert_eq!(
1417 editor.selections.display_ranges(cx),
1418 &[empty_range(1, "ab⋯e".len())]
1419 );
1420 editor.move_down(&MoveDown, window, cx);
1421 assert_eq!(
1422 editor.selections.display_ranges(cx),
1423 &[empty_range(2, "αβ⋯ε".len())]
1424 );
1425 editor.move_up(&MoveUp, window, cx);
1426 assert_eq!(
1427 editor.selections.display_ranges(cx),
1428 &[empty_range(1, "ab⋯e".len())]
1429 );
1430
1431 editor.move_up(&MoveUp, window, cx);
1432 assert_eq!(
1433 editor.selections.display_ranges(cx),
1434 &[empty_range(0, "🟥🟧".len())]
1435 );
1436 editor.move_left(&MoveLeft, window, cx);
1437 assert_eq!(
1438 editor.selections.display_ranges(cx),
1439 &[empty_range(0, "🟥".len())]
1440 );
1441 editor.move_left(&MoveLeft, window, cx);
1442 assert_eq!(
1443 editor.selections.display_ranges(cx),
1444 &[empty_range(0, "".len())]
1445 );
1446 });
1447}
1448
1449#[gpui::test]
1450fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1451 init_test(cx, |_| {});
1452
1453 let editor = cx.add_window(|window, cx| {
1454 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1455 build_editor(buffer.clone(), window, cx)
1456 });
1457 _ = editor.update(cx, |editor, window, cx| {
1458 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1459 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1460 });
1461
1462 // moving above start of document should move selection to start of document,
1463 // but the next move down should still be at the original goal_x
1464 editor.move_up(&MoveUp, window, cx);
1465 assert_eq!(
1466 editor.selections.display_ranges(cx),
1467 &[empty_range(0, "".len())]
1468 );
1469
1470 editor.move_down(&MoveDown, window, cx);
1471 assert_eq!(
1472 editor.selections.display_ranges(cx),
1473 &[empty_range(1, "abcd".len())]
1474 );
1475
1476 editor.move_down(&MoveDown, window, cx);
1477 assert_eq!(
1478 editor.selections.display_ranges(cx),
1479 &[empty_range(2, "αβγ".len())]
1480 );
1481
1482 editor.move_down(&MoveDown, window, cx);
1483 assert_eq!(
1484 editor.selections.display_ranges(cx),
1485 &[empty_range(3, "abcd".len())]
1486 );
1487
1488 editor.move_down(&MoveDown, window, cx);
1489 assert_eq!(
1490 editor.selections.display_ranges(cx),
1491 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1492 );
1493
1494 // moving past end of document should not change goal_x
1495 editor.move_down(&MoveDown, window, cx);
1496 assert_eq!(
1497 editor.selections.display_ranges(cx),
1498 &[empty_range(5, "".len())]
1499 );
1500
1501 editor.move_down(&MoveDown, window, cx);
1502 assert_eq!(
1503 editor.selections.display_ranges(cx),
1504 &[empty_range(5, "".len())]
1505 );
1506
1507 editor.move_up(&MoveUp, window, cx);
1508 assert_eq!(
1509 editor.selections.display_ranges(cx),
1510 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1511 );
1512
1513 editor.move_up(&MoveUp, window, cx);
1514 assert_eq!(
1515 editor.selections.display_ranges(cx),
1516 &[empty_range(3, "abcd".len())]
1517 );
1518
1519 editor.move_up(&MoveUp, window, cx);
1520 assert_eq!(
1521 editor.selections.display_ranges(cx),
1522 &[empty_range(2, "αβγ".len())]
1523 );
1524 });
1525}
1526
1527#[gpui::test]
1528fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1529 init_test(cx, |_| {});
1530 let move_to_beg = MoveToBeginningOfLine {
1531 stop_at_soft_wraps: true,
1532 stop_at_indent: true,
1533 };
1534
1535 let delete_to_beg = DeleteToBeginningOfLine {
1536 stop_at_indent: false,
1537 };
1538
1539 let move_to_end = MoveToEndOfLine {
1540 stop_at_soft_wraps: true,
1541 };
1542
1543 let editor = cx.add_window(|window, cx| {
1544 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1545 build_editor(buffer, window, cx)
1546 });
1547 _ = editor.update(cx, |editor, window, cx| {
1548 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1549 s.select_display_ranges([
1550 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1551 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1552 ]);
1553 });
1554 });
1555
1556 _ = editor.update(cx, |editor, window, cx| {
1557 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1558 assert_eq!(
1559 editor.selections.display_ranges(cx),
1560 &[
1561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1562 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1563 ]
1564 );
1565 });
1566
1567 _ = editor.update(cx, |editor, window, cx| {
1568 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1569 assert_eq!(
1570 editor.selections.display_ranges(cx),
1571 &[
1572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1573 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1574 ]
1575 );
1576 });
1577
1578 _ = editor.update(cx, |editor, window, cx| {
1579 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1580 assert_eq!(
1581 editor.selections.display_ranges(cx),
1582 &[
1583 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1584 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1585 ]
1586 );
1587 });
1588
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 // Moving to the end of line again is a no-op.
1601 _ = editor.update(cx, |editor, window, cx| {
1602 editor.move_to_end_of_line(&move_to_end, window, cx);
1603 assert_eq!(
1604 editor.selections.display_ranges(cx),
1605 &[
1606 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1607 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1608 ]
1609 );
1610 });
1611
1612 _ = editor.update(cx, |editor, window, cx| {
1613 editor.move_left(&MoveLeft, window, cx);
1614 editor.select_to_beginning_of_line(
1615 &SelectToBeginningOfLine {
1616 stop_at_soft_wraps: true,
1617 stop_at_indent: true,
1618 },
1619 window,
1620 cx,
1621 );
1622 assert_eq!(
1623 editor.selections.display_ranges(cx),
1624 &[
1625 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1626 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1627 ]
1628 );
1629 });
1630
1631 _ = editor.update(cx, |editor, window, cx| {
1632 editor.select_to_beginning_of_line(
1633 &SelectToBeginningOfLine {
1634 stop_at_soft_wraps: true,
1635 stop_at_indent: true,
1636 },
1637 window,
1638 cx,
1639 );
1640 assert_eq!(
1641 editor.selections.display_ranges(cx),
1642 &[
1643 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1644 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1645 ]
1646 );
1647 });
1648
1649 _ = editor.update(cx, |editor, window, cx| {
1650 editor.select_to_beginning_of_line(
1651 &SelectToBeginningOfLine {
1652 stop_at_soft_wraps: true,
1653 stop_at_indent: true,
1654 },
1655 window,
1656 cx,
1657 );
1658 assert_eq!(
1659 editor.selections.display_ranges(cx),
1660 &[
1661 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1662 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1663 ]
1664 );
1665 });
1666
1667 _ = editor.update(cx, |editor, window, cx| {
1668 editor.select_to_end_of_line(
1669 &SelectToEndOfLine {
1670 stop_at_soft_wraps: true,
1671 },
1672 window,
1673 cx,
1674 );
1675 assert_eq!(
1676 editor.selections.display_ranges(cx),
1677 &[
1678 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1679 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1680 ]
1681 );
1682 });
1683
1684 _ = editor.update(cx, |editor, window, cx| {
1685 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1686 assert_eq!(editor.display_text(cx), "ab\n de");
1687 assert_eq!(
1688 editor.selections.display_ranges(cx),
1689 &[
1690 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1691 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1692 ]
1693 );
1694 });
1695
1696 _ = editor.update(cx, |editor, window, cx| {
1697 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1698 assert_eq!(editor.display_text(cx), "\n");
1699 assert_eq!(
1700 editor.selections.display_ranges(cx),
1701 &[
1702 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1703 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1704 ]
1705 );
1706 });
1707}
1708
1709#[gpui::test]
1710fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1711 init_test(cx, |_| {});
1712 let move_to_beg = MoveToBeginningOfLine {
1713 stop_at_soft_wraps: false,
1714 stop_at_indent: false,
1715 };
1716
1717 let move_to_end = MoveToEndOfLine {
1718 stop_at_soft_wraps: false,
1719 };
1720
1721 let editor = cx.add_window(|window, cx| {
1722 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1723 build_editor(buffer, window, cx)
1724 });
1725
1726 _ = editor.update(cx, |editor, window, cx| {
1727 editor.set_wrap_width(Some(140.0.into()), cx);
1728
1729 // We expect the following lines after wrapping
1730 // ```
1731 // thequickbrownfox
1732 // jumpedoverthelazydo
1733 // gs
1734 // ```
1735 // The final `gs` was soft-wrapped onto a new line.
1736 assert_eq!(
1737 "thequickbrownfox\njumpedoverthelaz\nydogs",
1738 editor.display_text(cx),
1739 );
1740
1741 // First, let's assert behavior on the first line, that was not soft-wrapped.
1742 // Start the cursor at the `k` on the first line
1743 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1744 s.select_display_ranges([
1745 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1746 ]);
1747 });
1748
1749 // Moving to the beginning of the line should put us at the beginning of the line.
1750 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1751 assert_eq!(
1752 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1753 editor.selections.display_ranges(cx)
1754 );
1755
1756 // Moving to the end of the line should put us at the end of the line.
1757 editor.move_to_end_of_line(&move_to_end, window, cx);
1758 assert_eq!(
1759 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1760 editor.selections.display_ranges(cx)
1761 );
1762
1763 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1764 // Start the cursor at the last line (`y` that was wrapped to a new line)
1765 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1766 s.select_display_ranges([
1767 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1768 ]);
1769 });
1770
1771 // Moving to the beginning of the line should put us at the start of the second line of
1772 // display text, i.e., the `j`.
1773 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1774 assert_eq!(
1775 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1776 editor.selections.display_ranges(cx)
1777 );
1778
1779 // Moving to the beginning of the line again should be a no-op.
1780 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1781 assert_eq!(
1782 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1783 editor.selections.display_ranges(cx)
1784 );
1785
1786 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1787 // next display line.
1788 editor.move_to_end_of_line(&move_to_end, window, cx);
1789 assert_eq!(
1790 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1791 editor.selections.display_ranges(cx)
1792 );
1793
1794 // Moving to the end of the line again should be a no-op.
1795 editor.move_to_end_of_line(&move_to_end, window, cx);
1796 assert_eq!(
1797 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1798 editor.selections.display_ranges(cx)
1799 );
1800 });
1801}
1802
1803#[gpui::test]
1804fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1805 init_test(cx, |_| {});
1806
1807 let move_to_beg = MoveToBeginningOfLine {
1808 stop_at_soft_wraps: true,
1809 stop_at_indent: true,
1810 };
1811
1812 let select_to_beg = SelectToBeginningOfLine {
1813 stop_at_soft_wraps: true,
1814 stop_at_indent: true,
1815 };
1816
1817 let delete_to_beg = DeleteToBeginningOfLine {
1818 stop_at_indent: true,
1819 };
1820
1821 let move_to_end = MoveToEndOfLine {
1822 stop_at_soft_wraps: false,
1823 };
1824
1825 let editor = cx.add_window(|window, cx| {
1826 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1827 build_editor(buffer, window, cx)
1828 });
1829
1830 _ = editor.update(cx, |editor, window, cx| {
1831 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1832 s.select_display_ranges([
1833 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1834 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1835 ]);
1836 });
1837
1838 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1839 // and the second cursor at the first non-whitespace character in the line.
1840 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1841 assert_eq!(
1842 editor.selections.display_ranges(cx),
1843 &[
1844 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1845 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1846 ]
1847 );
1848
1849 // Moving to the beginning of the line again should be a no-op for the first cursor,
1850 // and should move the second cursor to the beginning of the line.
1851 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1852 assert_eq!(
1853 editor.selections.display_ranges(cx),
1854 &[
1855 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1856 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1857 ]
1858 );
1859
1860 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1861 // and should move the second cursor back to the first non-whitespace character in the line.
1862 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1863 assert_eq!(
1864 editor.selections.display_ranges(cx),
1865 &[
1866 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1867 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1868 ]
1869 );
1870
1871 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1872 // and to the first non-whitespace character in the line for the second cursor.
1873 editor.move_to_end_of_line(&move_to_end, window, cx);
1874 editor.move_left(&MoveLeft, window, cx);
1875 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1876 assert_eq!(
1877 editor.selections.display_ranges(cx),
1878 &[
1879 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1880 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1881 ]
1882 );
1883
1884 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1885 // and should select to the beginning of the line for the second cursor.
1886 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1887 assert_eq!(
1888 editor.selections.display_ranges(cx),
1889 &[
1890 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1891 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1892 ]
1893 );
1894
1895 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1896 // and should delete to the first non-whitespace character in the line for the second cursor.
1897 editor.move_to_end_of_line(&move_to_end, window, cx);
1898 editor.move_left(&MoveLeft, window, cx);
1899 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1900 assert_eq!(editor.text(cx), "c\n f");
1901 });
1902}
1903
1904#[gpui::test]
1905fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1906 init_test(cx, |_| {});
1907
1908 let editor = cx.add_window(|window, cx| {
1909 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1910 build_editor(buffer, window, cx)
1911 });
1912 _ = editor.update(cx, |editor, window, cx| {
1913 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1914 s.select_display_ranges([
1915 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1916 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1917 ])
1918 });
1919 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1920 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1921
1922 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1923 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1924
1925 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1926 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1927
1928 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1929 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1930
1931 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1932 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1933
1934 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1935 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1936
1937 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1938 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1939
1940 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1941 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1942
1943 editor.move_right(&MoveRight, window, cx);
1944 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1945 assert_selection_ranges(
1946 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1947 editor,
1948 cx,
1949 );
1950
1951 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1952 assert_selection_ranges(
1953 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
1954 editor,
1955 cx,
1956 );
1957
1958 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1959 assert_selection_ranges(
1960 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
1961 editor,
1962 cx,
1963 );
1964 });
1965}
1966
1967#[gpui::test]
1968fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1969 init_test(cx, |_| {});
1970
1971 let editor = cx.add_window(|window, cx| {
1972 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1973 build_editor(buffer, window, cx)
1974 });
1975
1976 _ = editor.update(cx, |editor, window, cx| {
1977 editor.set_wrap_width(Some(140.0.into()), cx);
1978 assert_eq!(
1979 editor.display_text(cx),
1980 "use one::{\n two::three::\n four::five\n};"
1981 );
1982
1983 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1984 s.select_display_ranges([
1985 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1986 ]);
1987 });
1988
1989 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1990 assert_eq!(
1991 editor.selections.display_ranges(cx),
1992 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1993 );
1994
1995 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1996 assert_eq!(
1997 editor.selections.display_ranges(cx),
1998 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1999 );
2000
2001 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2002 assert_eq!(
2003 editor.selections.display_ranges(cx),
2004 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2005 );
2006
2007 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2008 assert_eq!(
2009 editor.selections.display_ranges(cx),
2010 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2011 );
2012
2013 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2014 assert_eq!(
2015 editor.selections.display_ranges(cx),
2016 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2017 );
2018
2019 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2020 assert_eq!(
2021 editor.selections.display_ranges(cx),
2022 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2023 );
2024 });
2025}
2026
2027#[gpui::test]
2028async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2029 init_test(cx, |_| {});
2030 let mut cx = EditorTestContext::new(cx).await;
2031
2032 let line_height = cx.editor(|editor, window, _| {
2033 editor
2034 .style()
2035 .unwrap()
2036 .text
2037 .line_height_in_pixels(window.rem_size())
2038 });
2039 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2040
2041 cx.set_state(
2042 &r#"ˇone
2043 two
2044
2045 three
2046 fourˇ
2047 five
2048
2049 six"#
2050 .unindent(),
2051 );
2052
2053 cx.update_editor(|editor, window, cx| {
2054 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2055 });
2056 cx.assert_editor_state(
2057 &r#"one
2058 two
2059 ˇ
2060 three
2061 four
2062 five
2063 ˇ
2064 six"#
2065 .unindent(),
2066 );
2067
2068 cx.update_editor(|editor, window, cx| {
2069 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2070 });
2071 cx.assert_editor_state(
2072 &r#"one
2073 two
2074
2075 three
2076 four
2077 five
2078 ˇ
2079 sixˇ"#
2080 .unindent(),
2081 );
2082
2083 cx.update_editor(|editor, window, cx| {
2084 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2085 });
2086 cx.assert_editor_state(
2087 &r#"one
2088 two
2089
2090 three
2091 four
2092 five
2093
2094 sixˇ"#
2095 .unindent(),
2096 );
2097
2098 cx.update_editor(|editor, window, cx| {
2099 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2100 });
2101 cx.assert_editor_state(
2102 &r#"one
2103 two
2104
2105 three
2106 four
2107 five
2108 ˇ
2109 six"#
2110 .unindent(),
2111 );
2112
2113 cx.update_editor(|editor, window, cx| {
2114 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2115 });
2116 cx.assert_editor_state(
2117 &r#"one
2118 two
2119 ˇ
2120 three
2121 four
2122 five
2123
2124 six"#
2125 .unindent(),
2126 );
2127
2128 cx.update_editor(|editor, window, cx| {
2129 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2130 });
2131 cx.assert_editor_state(
2132 &r#"ˇone
2133 two
2134
2135 three
2136 four
2137 five
2138
2139 six"#
2140 .unindent(),
2141 );
2142}
2143
2144#[gpui::test]
2145async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2146 init_test(cx, |_| {});
2147 let mut cx = EditorTestContext::new(cx).await;
2148 let line_height = cx.editor(|editor, window, _| {
2149 editor
2150 .style()
2151 .unwrap()
2152 .text
2153 .line_height_in_pixels(window.rem_size())
2154 });
2155 let window = cx.window;
2156 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2157
2158 cx.set_state(
2159 r#"ˇone
2160 two
2161 three
2162 four
2163 five
2164 six
2165 seven
2166 eight
2167 nine
2168 ten
2169 "#,
2170 );
2171
2172 cx.update_editor(|editor, window, cx| {
2173 assert_eq!(
2174 editor.snapshot(window, cx).scroll_position(),
2175 gpui::Point::new(0., 0.)
2176 );
2177 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2178 assert_eq!(
2179 editor.snapshot(window, cx).scroll_position(),
2180 gpui::Point::new(0., 3.)
2181 );
2182 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2183 assert_eq!(
2184 editor.snapshot(window, cx).scroll_position(),
2185 gpui::Point::new(0., 6.)
2186 );
2187 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2188 assert_eq!(
2189 editor.snapshot(window, cx).scroll_position(),
2190 gpui::Point::new(0., 3.)
2191 );
2192
2193 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2194 assert_eq!(
2195 editor.snapshot(window, cx).scroll_position(),
2196 gpui::Point::new(0., 1.)
2197 );
2198 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2199 assert_eq!(
2200 editor.snapshot(window, cx).scroll_position(),
2201 gpui::Point::new(0., 3.)
2202 );
2203 });
2204}
2205
2206#[gpui::test]
2207async fn test_autoscroll(cx: &mut TestAppContext) {
2208 init_test(cx, |_| {});
2209 let mut cx = EditorTestContext::new(cx).await;
2210
2211 let line_height = cx.update_editor(|editor, window, cx| {
2212 editor.set_vertical_scroll_margin(2, cx);
2213 editor
2214 .style()
2215 .unwrap()
2216 .text
2217 .line_height_in_pixels(window.rem_size())
2218 });
2219 let window = cx.window;
2220 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2221
2222 cx.set_state(
2223 r#"ˇone
2224 two
2225 three
2226 four
2227 five
2228 six
2229 seven
2230 eight
2231 nine
2232 ten
2233 "#,
2234 );
2235 cx.update_editor(|editor, window, cx| {
2236 assert_eq!(
2237 editor.snapshot(window, cx).scroll_position(),
2238 gpui::Point::new(0., 0.0)
2239 );
2240 });
2241
2242 // Add a cursor below the visible area. Since both cursors cannot fit
2243 // on screen, the editor autoscrolls to reveal the newest cursor, and
2244 // allows the vertical scroll margin below that cursor.
2245 cx.update_editor(|editor, window, cx| {
2246 editor.change_selections(Default::default(), window, cx, |selections| {
2247 selections.select_ranges([
2248 Point::new(0, 0)..Point::new(0, 0),
2249 Point::new(6, 0)..Point::new(6, 0),
2250 ]);
2251 })
2252 });
2253 cx.update_editor(|editor, window, cx| {
2254 assert_eq!(
2255 editor.snapshot(window, cx).scroll_position(),
2256 gpui::Point::new(0., 3.0)
2257 );
2258 });
2259
2260 // Move down. The editor cursor scrolls down to track the newest cursor.
2261 cx.update_editor(|editor, window, cx| {
2262 editor.move_down(&Default::default(), window, cx);
2263 });
2264 cx.update_editor(|editor, window, cx| {
2265 assert_eq!(
2266 editor.snapshot(window, cx).scroll_position(),
2267 gpui::Point::new(0., 4.0)
2268 );
2269 });
2270
2271 // Add a cursor above the visible area. Since both cursors fit on screen,
2272 // the editor scrolls to show both.
2273 cx.update_editor(|editor, window, cx| {
2274 editor.change_selections(Default::default(), window, cx, |selections| {
2275 selections.select_ranges([
2276 Point::new(1, 0)..Point::new(1, 0),
2277 Point::new(6, 0)..Point::new(6, 0),
2278 ]);
2279 })
2280 });
2281 cx.update_editor(|editor, window, cx| {
2282 assert_eq!(
2283 editor.snapshot(window, cx).scroll_position(),
2284 gpui::Point::new(0., 1.0)
2285 );
2286 });
2287}
2288
2289#[gpui::test]
2290async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2291 init_test(cx, |_| {});
2292 let mut cx = EditorTestContext::new(cx).await;
2293
2294 let line_height = cx.editor(|editor, window, _cx| {
2295 editor
2296 .style()
2297 .unwrap()
2298 .text
2299 .line_height_in_pixels(window.rem_size())
2300 });
2301 let window = cx.window;
2302 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2303 cx.set_state(
2304 &r#"
2305 ˇone
2306 two
2307 threeˇ
2308 four
2309 five
2310 six
2311 seven
2312 eight
2313 nine
2314 ten
2315 "#
2316 .unindent(),
2317 );
2318
2319 cx.update_editor(|editor, window, cx| {
2320 editor.move_page_down(&MovePageDown::default(), window, cx)
2321 });
2322 cx.assert_editor_state(
2323 &r#"
2324 one
2325 two
2326 three
2327 ˇfour
2328 five
2329 sixˇ
2330 seven
2331 eight
2332 nine
2333 ten
2334 "#
2335 .unindent(),
2336 );
2337
2338 cx.update_editor(|editor, window, cx| {
2339 editor.move_page_down(&MovePageDown::default(), window, cx)
2340 });
2341 cx.assert_editor_state(
2342 &r#"
2343 one
2344 two
2345 three
2346 four
2347 five
2348 six
2349 ˇseven
2350 eight
2351 nineˇ
2352 ten
2353 "#
2354 .unindent(),
2355 );
2356
2357 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2358 cx.assert_editor_state(
2359 &r#"
2360 one
2361 two
2362 three
2363 ˇfour
2364 five
2365 sixˇ
2366 seven
2367 eight
2368 nine
2369 ten
2370 "#
2371 .unindent(),
2372 );
2373
2374 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2375 cx.assert_editor_state(
2376 &r#"
2377 ˇone
2378 two
2379 threeˇ
2380 four
2381 five
2382 six
2383 seven
2384 eight
2385 nine
2386 ten
2387 "#
2388 .unindent(),
2389 );
2390
2391 // Test select collapsing
2392 cx.update_editor(|editor, window, cx| {
2393 editor.move_page_down(&MovePageDown::default(), window, cx);
2394 editor.move_page_down(&MovePageDown::default(), window, cx);
2395 editor.move_page_down(&MovePageDown::default(), window, cx);
2396 });
2397 cx.assert_editor_state(
2398 &r#"
2399 one
2400 two
2401 three
2402 four
2403 five
2404 six
2405 seven
2406 eight
2407 nine
2408 ˇten
2409 ˇ"#
2410 .unindent(),
2411 );
2412}
2413
2414#[gpui::test]
2415async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2416 init_test(cx, |_| {});
2417 let mut cx = EditorTestContext::new(cx).await;
2418 cx.set_state("one «two threeˇ» four");
2419 cx.update_editor(|editor, window, cx| {
2420 editor.delete_to_beginning_of_line(
2421 &DeleteToBeginningOfLine {
2422 stop_at_indent: false,
2423 },
2424 window,
2425 cx,
2426 );
2427 assert_eq!(editor.text(cx), " four");
2428 });
2429}
2430
2431#[gpui::test]
2432fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2433 init_test(cx, |_| {});
2434
2435 let editor = cx.add_window(|window, cx| {
2436 let buffer = MultiBuffer::build_simple("one two three four", cx);
2437 build_editor(buffer.clone(), window, cx)
2438 });
2439
2440 _ = editor.update(cx, |editor, window, cx| {
2441 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2442 s.select_display_ranges([
2443 // an empty selection - the preceding word fragment is deleted
2444 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2445 // characters selected - they are deleted
2446 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2447 ])
2448 });
2449 editor.delete_to_previous_word_start(
2450 &DeleteToPreviousWordStart {
2451 ignore_newlines: false,
2452 },
2453 window,
2454 cx,
2455 );
2456 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2457 });
2458
2459 _ = editor.update(cx, |editor, window, cx| {
2460 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2461 s.select_display_ranges([
2462 // an empty selection - the following word fragment is deleted
2463 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2464 // characters selected - they are deleted
2465 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2466 ])
2467 });
2468 editor.delete_to_next_word_end(
2469 &DeleteToNextWordEnd {
2470 ignore_newlines: false,
2471 },
2472 window,
2473 cx,
2474 );
2475 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2476 });
2477}
2478
2479#[gpui::test]
2480fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2481 init_test(cx, |_| {});
2482
2483 let editor = cx.add_window(|window, cx| {
2484 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2485 build_editor(buffer.clone(), window, cx)
2486 });
2487 let del_to_prev_word_start = DeleteToPreviousWordStart {
2488 ignore_newlines: false,
2489 };
2490 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2491 ignore_newlines: true,
2492 };
2493
2494 _ = editor.update(cx, |editor, window, cx| {
2495 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2496 s.select_display_ranges([
2497 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2498 ])
2499 });
2500 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2502 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2503 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2504 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2505 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2506 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2507 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2508 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2509 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2510 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2511 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2512 });
2513}
2514
2515#[gpui::test]
2516fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2517 init_test(cx, |_| {});
2518
2519 let editor = cx.add_window(|window, cx| {
2520 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2521 build_editor(buffer.clone(), window, cx)
2522 });
2523 let del_to_next_word_end = DeleteToNextWordEnd {
2524 ignore_newlines: false,
2525 };
2526 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2527 ignore_newlines: true,
2528 };
2529
2530 _ = editor.update(cx, |editor, window, cx| {
2531 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2532 s.select_display_ranges([
2533 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2534 ])
2535 });
2536 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2537 assert_eq!(
2538 editor.buffer.read(cx).read(cx).text(),
2539 "one\n two\nthree\n four"
2540 );
2541 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2542 assert_eq!(
2543 editor.buffer.read(cx).read(cx).text(),
2544 "\n two\nthree\n four"
2545 );
2546 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2547 assert_eq!(
2548 editor.buffer.read(cx).read(cx).text(),
2549 "two\nthree\n four"
2550 );
2551 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2552 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2553 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2554 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2555 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2556 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2557 });
2558}
2559
2560#[gpui::test]
2561fn test_newline(cx: &mut TestAppContext) {
2562 init_test(cx, |_| {});
2563
2564 let editor = cx.add_window(|window, cx| {
2565 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2566 build_editor(buffer.clone(), window, cx)
2567 });
2568
2569 _ = editor.update(cx, |editor, window, cx| {
2570 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2571 s.select_display_ranges([
2572 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2573 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2574 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2575 ])
2576 });
2577
2578 editor.newline(&Newline, window, cx);
2579 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2580 });
2581}
2582
2583#[gpui::test]
2584fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2585 init_test(cx, |_| {});
2586
2587 let editor = cx.add_window(|window, cx| {
2588 let buffer = MultiBuffer::build_simple(
2589 "
2590 a
2591 b(
2592 X
2593 )
2594 c(
2595 X
2596 )
2597 "
2598 .unindent()
2599 .as_str(),
2600 cx,
2601 );
2602 let mut editor = build_editor(buffer.clone(), window, cx);
2603 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2604 s.select_ranges([
2605 Point::new(2, 4)..Point::new(2, 5),
2606 Point::new(5, 4)..Point::new(5, 5),
2607 ])
2608 });
2609 editor
2610 });
2611
2612 _ = editor.update(cx, |editor, window, cx| {
2613 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2614 editor.buffer.update(cx, |buffer, cx| {
2615 buffer.edit(
2616 [
2617 (Point::new(1, 2)..Point::new(3, 0), ""),
2618 (Point::new(4, 2)..Point::new(6, 0), ""),
2619 ],
2620 None,
2621 cx,
2622 );
2623 assert_eq!(
2624 buffer.read(cx).text(),
2625 "
2626 a
2627 b()
2628 c()
2629 "
2630 .unindent()
2631 );
2632 });
2633 assert_eq!(
2634 editor.selections.ranges(cx),
2635 &[
2636 Point::new(1, 2)..Point::new(1, 2),
2637 Point::new(2, 2)..Point::new(2, 2),
2638 ],
2639 );
2640
2641 editor.newline(&Newline, window, cx);
2642 assert_eq!(
2643 editor.text(cx),
2644 "
2645 a
2646 b(
2647 )
2648 c(
2649 )
2650 "
2651 .unindent()
2652 );
2653
2654 // The selections are moved after the inserted newlines
2655 assert_eq!(
2656 editor.selections.ranges(cx),
2657 &[
2658 Point::new(2, 0)..Point::new(2, 0),
2659 Point::new(4, 0)..Point::new(4, 0),
2660 ],
2661 );
2662 });
2663}
2664
2665#[gpui::test]
2666async fn test_newline_above(cx: &mut TestAppContext) {
2667 init_test(cx, |settings| {
2668 settings.defaults.tab_size = NonZeroU32::new(4)
2669 });
2670
2671 let language = Arc::new(
2672 Language::new(
2673 LanguageConfig::default(),
2674 Some(tree_sitter_rust::LANGUAGE.into()),
2675 )
2676 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2677 .unwrap(),
2678 );
2679
2680 let mut cx = EditorTestContext::new(cx).await;
2681 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2682 cx.set_state(indoc! {"
2683 const a: ˇA = (
2684 (ˇ
2685 «const_functionˇ»(ˇ),
2686 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2687 )ˇ
2688 ˇ);ˇ
2689 "});
2690
2691 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2692 cx.assert_editor_state(indoc! {"
2693 ˇ
2694 const a: A = (
2695 ˇ
2696 (
2697 ˇ
2698 ˇ
2699 const_function(),
2700 ˇ
2701 ˇ
2702 ˇ
2703 ˇ
2704 something_else,
2705 ˇ
2706 )
2707 ˇ
2708 ˇ
2709 );
2710 "});
2711}
2712
2713#[gpui::test]
2714async fn test_newline_below(cx: &mut TestAppContext) {
2715 init_test(cx, |settings| {
2716 settings.defaults.tab_size = NonZeroU32::new(4)
2717 });
2718
2719 let language = Arc::new(
2720 Language::new(
2721 LanguageConfig::default(),
2722 Some(tree_sitter_rust::LANGUAGE.into()),
2723 )
2724 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2725 .unwrap(),
2726 );
2727
2728 let mut cx = EditorTestContext::new(cx).await;
2729 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2730 cx.set_state(indoc! {"
2731 const a: ˇA = (
2732 (ˇ
2733 «const_functionˇ»(ˇ),
2734 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2735 )ˇ
2736 ˇ);ˇ
2737 "});
2738
2739 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2740 cx.assert_editor_state(indoc! {"
2741 const a: A = (
2742 ˇ
2743 (
2744 ˇ
2745 const_function(),
2746 ˇ
2747 ˇ
2748 something_else,
2749 ˇ
2750 ˇ
2751 ˇ
2752 ˇ
2753 )
2754 ˇ
2755 );
2756 ˇ
2757 ˇ
2758 "});
2759}
2760
2761#[gpui::test]
2762async fn test_newline_comments(cx: &mut TestAppContext) {
2763 init_test(cx, |settings| {
2764 settings.defaults.tab_size = NonZeroU32::new(4)
2765 });
2766
2767 let language = Arc::new(Language::new(
2768 LanguageConfig {
2769 line_comments: vec!["// ".into()],
2770 ..LanguageConfig::default()
2771 },
2772 None,
2773 ));
2774 {
2775 let mut cx = EditorTestContext::new(cx).await;
2776 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2777 cx.set_state(indoc! {"
2778 // Fooˇ
2779 "});
2780
2781 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2782 cx.assert_editor_state(indoc! {"
2783 // Foo
2784 // ˇ
2785 "});
2786 // Ensure that we add comment prefix when existing line contains space
2787 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2788 cx.assert_editor_state(
2789 indoc! {"
2790 // Foo
2791 //s
2792 // ˇ
2793 "}
2794 .replace("s", " ") // s is used as space placeholder to prevent format on save
2795 .as_str(),
2796 );
2797 // Ensure that we add comment prefix when existing line does not contain space
2798 cx.set_state(indoc! {"
2799 // Foo
2800 //ˇ
2801 "});
2802 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2803 cx.assert_editor_state(indoc! {"
2804 // Foo
2805 //
2806 // ˇ
2807 "});
2808 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2809 cx.set_state(indoc! {"
2810 ˇ// Foo
2811 "});
2812 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2813 cx.assert_editor_state(indoc! {"
2814
2815 ˇ// Foo
2816 "});
2817 }
2818 // Ensure that comment continuations can be disabled.
2819 update_test_language_settings(cx, |settings| {
2820 settings.defaults.extend_comment_on_newline = Some(false);
2821 });
2822 let mut cx = EditorTestContext::new(cx).await;
2823 cx.set_state(indoc! {"
2824 // Fooˇ
2825 "});
2826 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2827 cx.assert_editor_state(indoc! {"
2828 // Foo
2829 ˇ
2830 "});
2831}
2832
2833#[gpui::test]
2834async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2835 init_test(cx, |settings| {
2836 settings.defaults.tab_size = NonZeroU32::new(4)
2837 });
2838
2839 let language = Arc::new(Language::new(
2840 LanguageConfig {
2841 line_comments: vec!["// ".into(), "/// ".into()],
2842 ..LanguageConfig::default()
2843 },
2844 None,
2845 ));
2846 {
2847 let mut cx = EditorTestContext::new(cx).await;
2848 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2849 cx.set_state(indoc! {"
2850 //ˇ
2851 "});
2852 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2853 cx.assert_editor_state(indoc! {"
2854 //
2855 // ˇ
2856 "});
2857
2858 cx.set_state(indoc! {"
2859 ///ˇ
2860 "});
2861 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2862 cx.assert_editor_state(indoc! {"
2863 ///
2864 /// ˇ
2865 "});
2866 }
2867}
2868
2869#[gpui::test]
2870async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2871 init_test(cx, |settings| {
2872 settings.defaults.tab_size = NonZeroU32::new(4)
2873 });
2874
2875 let language = Arc::new(
2876 Language::new(
2877 LanguageConfig {
2878 documentation: Some(language::DocumentationConfig {
2879 start: "/**".into(),
2880 end: "*/".into(),
2881 prefix: "* ".into(),
2882 tab_size: NonZeroU32::new(1).unwrap(),
2883 }),
2884
2885 ..LanguageConfig::default()
2886 },
2887 Some(tree_sitter_rust::LANGUAGE.into()),
2888 )
2889 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2890 .unwrap(),
2891 );
2892
2893 {
2894 let mut cx = EditorTestContext::new(cx).await;
2895 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2896 cx.set_state(indoc! {"
2897 /**ˇ
2898 "});
2899
2900 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2901 cx.assert_editor_state(indoc! {"
2902 /**
2903 * ˇ
2904 "});
2905 // Ensure that if cursor is before the comment start,
2906 // we do not actually insert a comment prefix.
2907 cx.set_state(indoc! {"
2908 ˇ/**
2909 "});
2910 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912
2913 ˇ/**
2914 "});
2915 // Ensure that if cursor is between it doesn't add comment prefix.
2916 cx.set_state(indoc! {"
2917 /*ˇ*
2918 "});
2919 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2920 cx.assert_editor_state(indoc! {"
2921 /*
2922 ˇ*
2923 "});
2924 // Ensure that if suffix exists on same line after cursor it adds new line.
2925 cx.set_state(indoc! {"
2926 /**ˇ*/
2927 "});
2928 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2929 cx.assert_editor_state(indoc! {"
2930 /**
2931 * ˇ
2932 */
2933 "});
2934 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2935 cx.set_state(indoc! {"
2936 /**ˇ */
2937 "});
2938 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2939 cx.assert_editor_state(indoc! {"
2940 /**
2941 * ˇ
2942 */
2943 "});
2944 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2945 cx.set_state(indoc! {"
2946 /** ˇ*/
2947 "});
2948 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2949 cx.assert_editor_state(
2950 indoc! {"
2951 /**s
2952 * ˇ
2953 */
2954 "}
2955 .replace("s", " ") // s is used as space placeholder to prevent format on save
2956 .as_str(),
2957 );
2958 // Ensure that delimiter space is preserved when newline on already
2959 // spaced delimiter.
2960 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2961 cx.assert_editor_state(
2962 indoc! {"
2963 /**s
2964 *s
2965 * ˇ
2966 */
2967 "}
2968 .replace("s", " ") // s is used as space placeholder to prevent format on save
2969 .as_str(),
2970 );
2971 // Ensure that delimiter space is preserved when space is not
2972 // on existing delimiter.
2973 cx.set_state(indoc! {"
2974 /**
2975 *ˇ
2976 */
2977 "});
2978 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2979 cx.assert_editor_state(indoc! {"
2980 /**
2981 *
2982 * ˇ
2983 */
2984 "});
2985 // Ensure that if suffix exists on same line after cursor it
2986 // doesn't add extra new line if prefix is not on same line.
2987 cx.set_state(indoc! {"
2988 /**
2989 ˇ*/
2990 "});
2991 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2992 cx.assert_editor_state(indoc! {"
2993 /**
2994
2995 ˇ*/
2996 "});
2997 // Ensure that it detects suffix after existing prefix.
2998 cx.set_state(indoc! {"
2999 /**ˇ/
3000 "});
3001 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3002 cx.assert_editor_state(indoc! {"
3003 /**
3004 ˇ/
3005 "});
3006 // Ensure that if suffix exists on same line before
3007 // cursor it does not add comment prefix.
3008 cx.set_state(indoc! {"
3009 /** */ˇ
3010 "});
3011 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3012 cx.assert_editor_state(indoc! {"
3013 /** */
3014 ˇ
3015 "});
3016 // Ensure that if suffix exists on same line before
3017 // cursor it does not add comment prefix.
3018 cx.set_state(indoc! {"
3019 /**
3020 *
3021 */ˇ
3022 "});
3023 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3024 cx.assert_editor_state(indoc! {"
3025 /**
3026 *
3027 */
3028 ˇ
3029 "});
3030
3031 // Ensure that inline comment followed by code
3032 // doesn't add comment prefix on newline
3033 cx.set_state(indoc! {"
3034 /** */ textˇ
3035 "});
3036 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3037 cx.assert_editor_state(indoc! {"
3038 /** */ text
3039 ˇ
3040 "});
3041
3042 // Ensure that text after comment end tag
3043 // doesn't add comment prefix on newline
3044 cx.set_state(indoc! {"
3045 /**
3046 *
3047 */ˇtext
3048 "});
3049 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3050 cx.assert_editor_state(indoc! {"
3051 /**
3052 *
3053 */
3054 ˇtext
3055 "});
3056
3057 // Ensure if not comment block it doesn't
3058 // add comment prefix on newline
3059 cx.set_state(indoc! {"
3060 * textˇ
3061 "});
3062 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3063 cx.assert_editor_state(indoc! {"
3064 * text
3065 ˇ
3066 "});
3067 }
3068 // Ensure that comment continuations can be disabled.
3069 update_test_language_settings(cx, |settings| {
3070 settings.defaults.extend_comment_on_newline = Some(false);
3071 });
3072 let mut cx = EditorTestContext::new(cx).await;
3073 cx.set_state(indoc! {"
3074 /**ˇ
3075 "});
3076 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3077 cx.assert_editor_state(indoc! {"
3078 /**
3079 ˇ
3080 "});
3081}
3082
3083#[gpui::test]
3084fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3085 init_test(cx, |_| {});
3086
3087 let editor = cx.add_window(|window, cx| {
3088 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3089 let mut editor = build_editor(buffer.clone(), window, cx);
3090 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3091 s.select_ranges([3..4, 11..12, 19..20])
3092 });
3093 editor
3094 });
3095
3096 _ = editor.update(cx, |editor, window, cx| {
3097 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3098 editor.buffer.update(cx, |buffer, cx| {
3099 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3100 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3101 });
3102 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3103
3104 editor.insert("Z", window, cx);
3105 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3106
3107 // The selections are moved after the inserted characters
3108 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3109 });
3110}
3111
3112#[gpui::test]
3113async fn test_tab(cx: &mut TestAppContext) {
3114 init_test(cx, |settings| {
3115 settings.defaults.tab_size = NonZeroU32::new(3)
3116 });
3117
3118 let mut cx = EditorTestContext::new(cx).await;
3119 cx.set_state(indoc! {"
3120 ˇabˇc
3121 ˇ🏀ˇ🏀ˇefg
3122 dˇ
3123 "});
3124 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3125 cx.assert_editor_state(indoc! {"
3126 ˇab ˇc
3127 ˇ🏀 ˇ🏀 ˇefg
3128 d ˇ
3129 "});
3130
3131 cx.set_state(indoc! {"
3132 a
3133 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3134 "});
3135 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3136 cx.assert_editor_state(indoc! {"
3137 a
3138 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3139 "});
3140}
3141
3142#[gpui::test]
3143async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3144 init_test(cx, |_| {});
3145
3146 let mut cx = EditorTestContext::new(cx).await;
3147 let language = Arc::new(
3148 Language::new(
3149 LanguageConfig::default(),
3150 Some(tree_sitter_rust::LANGUAGE.into()),
3151 )
3152 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3153 .unwrap(),
3154 );
3155 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3156
3157 // test when all cursors are not at suggested indent
3158 // then simply move to their suggested indent location
3159 cx.set_state(indoc! {"
3160 const a: B = (
3161 c(
3162 ˇ
3163 ˇ )
3164 );
3165 "});
3166 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3167 cx.assert_editor_state(indoc! {"
3168 const a: B = (
3169 c(
3170 ˇ
3171 ˇ)
3172 );
3173 "});
3174
3175 // test cursor already at suggested indent not moving when
3176 // other cursors are yet to reach their suggested indents
3177 cx.set_state(indoc! {"
3178 ˇ
3179 const a: B = (
3180 c(
3181 d(
3182 ˇ
3183 )
3184 ˇ
3185 ˇ )
3186 );
3187 "});
3188 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3189 cx.assert_editor_state(indoc! {"
3190 ˇ
3191 const a: B = (
3192 c(
3193 d(
3194 ˇ
3195 )
3196 ˇ
3197 ˇ)
3198 );
3199 "});
3200 // test when all cursors are at suggested indent then tab is inserted
3201 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3202 cx.assert_editor_state(indoc! {"
3203 ˇ
3204 const a: B = (
3205 c(
3206 d(
3207 ˇ
3208 )
3209 ˇ
3210 ˇ)
3211 );
3212 "});
3213
3214 // test when current indent is less than suggested indent,
3215 // we adjust line to match suggested indent and move cursor to it
3216 //
3217 // when no other cursor is at word boundary, all of them should move
3218 cx.set_state(indoc! {"
3219 const a: B = (
3220 c(
3221 d(
3222 ˇ
3223 ˇ )
3224 ˇ )
3225 );
3226 "});
3227 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3228 cx.assert_editor_state(indoc! {"
3229 const a: B = (
3230 c(
3231 d(
3232 ˇ
3233 ˇ)
3234 ˇ)
3235 );
3236 "});
3237
3238 // test when current indent is less than suggested indent,
3239 // we adjust line to match suggested indent and move cursor to it
3240 //
3241 // when some other cursor is at word boundary, it should not move
3242 cx.set_state(indoc! {"
3243 const a: B = (
3244 c(
3245 d(
3246 ˇ
3247 ˇ )
3248 ˇ)
3249 );
3250 "});
3251 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3252 cx.assert_editor_state(indoc! {"
3253 const a: B = (
3254 c(
3255 d(
3256 ˇ
3257 ˇ)
3258 ˇ)
3259 );
3260 "});
3261
3262 // test when current indent is more than suggested indent,
3263 // we just move cursor to current indent instead of suggested indent
3264 //
3265 // when no other cursor is at word boundary, all of them should move
3266 cx.set_state(indoc! {"
3267 const a: B = (
3268 c(
3269 d(
3270 ˇ
3271 ˇ )
3272 ˇ )
3273 );
3274 "});
3275 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3276 cx.assert_editor_state(indoc! {"
3277 const a: B = (
3278 c(
3279 d(
3280 ˇ
3281 ˇ)
3282 ˇ)
3283 );
3284 "});
3285 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3286 cx.assert_editor_state(indoc! {"
3287 const a: B = (
3288 c(
3289 d(
3290 ˇ
3291 ˇ)
3292 ˇ)
3293 );
3294 "});
3295
3296 // test when current indent is more than suggested indent,
3297 // we just move cursor to current indent instead of suggested indent
3298 //
3299 // when some other cursor is at word boundary, it doesn't move
3300 cx.set_state(indoc! {"
3301 const a: B = (
3302 c(
3303 d(
3304 ˇ
3305 ˇ )
3306 ˇ)
3307 );
3308 "});
3309 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3310 cx.assert_editor_state(indoc! {"
3311 const a: B = (
3312 c(
3313 d(
3314 ˇ
3315 ˇ)
3316 ˇ)
3317 );
3318 "});
3319
3320 // handle auto-indent when there are multiple cursors on the same line
3321 cx.set_state(indoc! {"
3322 const a: B = (
3323 c(
3324 ˇ ˇ
3325 ˇ )
3326 );
3327 "});
3328 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3329 cx.assert_editor_state(indoc! {"
3330 const a: B = (
3331 c(
3332 ˇ
3333 ˇ)
3334 );
3335 "});
3336}
3337
3338#[gpui::test]
3339async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3340 init_test(cx, |settings| {
3341 settings.defaults.tab_size = NonZeroU32::new(3)
3342 });
3343
3344 let mut cx = EditorTestContext::new(cx).await;
3345 cx.set_state(indoc! {"
3346 ˇ
3347 \t ˇ
3348 \t ˇ
3349 \t ˇ
3350 \t \t\t \t \t\t \t\t \t \t ˇ
3351 "});
3352
3353 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3354 cx.assert_editor_state(indoc! {"
3355 ˇ
3356 \t ˇ
3357 \t ˇ
3358 \t ˇ
3359 \t \t\t \t \t\t \t\t \t \t ˇ
3360 "});
3361}
3362
3363#[gpui::test]
3364async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3365 init_test(cx, |settings| {
3366 settings.defaults.tab_size = NonZeroU32::new(4)
3367 });
3368
3369 let language = Arc::new(
3370 Language::new(
3371 LanguageConfig::default(),
3372 Some(tree_sitter_rust::LANGUAGE.into()),
3373 )
3374 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3375 .unwrap(),
3376 );
3377
3378 let mut cx = EditorTestContext::new(cx).await;
3379 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3380 cx.set_state(indoc! {"
3381 fn a() {
3382 if b {
3383 \t ˇc
3384 }
3385 }
3386 "});
3387
3388 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3389 cx.assert_editor_state(indoc! {"
3390 fn a() {
3391 if b {
3392 ˇc
3393 }
3394 }
3395 "});
3396}
3397
3398#[gpui::test]
3399async fn test_indent_outdent(cx: &mut TestAppContext) {
3400 init_test(cx, |settings| {
3401 settings.defaults.tab_size = NonZeroU32::new(4);
3402 });
3403
3404 let mut cx = EditorTestContext::new(cx).await;
3405
3406 cx.set_state(indoc! {"
3407 «oneˇ» «twoˇ»
3408 three
3409 four
3410 "});
3411 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3412 cx.assert_editor_state(indoc! {"
3413 «oneˇ» «twoˇ»
3414 three
3415 four
3416 "});
3417
3418 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3419 cx.assert_editor_state(indoc! {"
3420 «oneˇ» «twoˇ»
3421 three
3422 four
3423 "});
3424
3425 // select across line ending
3426 cx.set_state(indoc! {"
3427 one two
3428 t«hree
3429 ˇ» four
3430 "});
3431 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3432 cx.assert_editor_state(indoc! {"
3433 one two
3434 t«hree
3435 ˇ» four
3436 "});
3437
3438 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3439 cx.assert_editor_state(indoc! {"
3440 one two
3441 t«hree
3442 ˇ» four
3443 "});
3444
3445 // Ensure that indenting/outdenting works when the cursor is at column 0.
3446 cx.set_state(indoc! {"
3447 one two
3448 ˇthree
3449 four
3450 "});
3451 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3452 cx.assert_editor_state(indoc! {"
3453 one two
3454 ˇthree
3455 four
3456 "});
3457
3458 cx.set_state(indoc! {"
3459 one two
3460 ˇ three
3461 four
3462 "});
3463 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3464 cx.assert_editor_state(indoc! {"
3465 one two
3466 ˇthree
3467 four
3468 "});
3469}
3470
3471#[gpui::test]
3472async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3473 init_test(cx, |settings| {
3474 settings.defaults.hard_tabs = Some(true);
3475 });
3476
3477 let mut cx = EditorTestContext::new(cx).await;
3478
3479 // select two ranges on one line
3480 cx.set_state(indoc! {"
3481 «oneˇ» «twoˇ»
3482 three
3483 four
3484 "});
3485 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3486 cx.assert_editor_state(indoc! {"
3487 \t«oneˇ» «twoˇ»
3488 three
3489 four
3490 "});
3491 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3492 cx.assert_editor_state(indoc! {"
3493 \t\t«oneˇ» «twoˇ»
3494 three
3495 four
3496 "});
3497 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3498 cx.assert_editor_state(indoc! {"
3499 \t«oneˇ» «twoˇ»
3500 three
3501 four
3502 "});
3503 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3504 cx.assert_editor_state(indoc! {"
3505 «oneˇ» «twoˇ»
3506 three
3507 four
3508 "});
3509
3510 // select across a line ending
3511 cx.set_state(indoc! {"
3512 one two
3513 t«hree
3514 ˇ»four
3515 "});
3516 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3517 cx.assert_editor_state(indoc! {"
3518 one two
3519 \tt«hree
3520 ˇ»four
3521 "});
3522 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3523 cx.assert_editor_state(indoc! {"
3524 one two
3525 \t\tt«hree
3526 ˇ»four
3527 "});
3528 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3529 cx.assert_editor_state(indoc! {"
3530 one two
3531 \tt«hree
3532 ˇ»four
3533 "});
3534 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3535 cx.assert_editor_state(indoc! {"
3536 one two
3537 t«hree
3538 ˇ»four
3539 "});
3540
3541 // Ensure that indenting/outdenting works when the cursor is at column 0.
3542 cx.set_state(indoc! {"
3543 one two
3544 ˇthree
3545 four
3546 "});
3547 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3548 cx.assert_editor_state(indoc! {"
3549 one two
3550 ˇthree
3551 four
3552 "});
3553 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3554 cx.assert_editor_state(indoc! {"
3555 one two
3556 \tˇthree
3557 four
3558 "});
3559 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3560 cx.assert_editor_state(indoc! {"
3561 one two
3562 ˇthree
3563 four
3564 "});
3565}
3566
3567#[gpui::test]
3568fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3569 init_test(cx, |settings| {
3570 settings.languages.0.extend([
3571 (
3572 "TOML".into(),
3573 LanguageSettingsContent {
3574 tab_size: NonZeroU32::new(2),
3575 ..Default::default()
3576 },
3577 ),
3578 (
3579 "Rust".into(),
3580 LanguageSettingsContent {
3581 tab_size: NonZeroU32::new(4),
3582 ..Default::default()
3583 },
3584 ),
3585 ]);
3586 });
3587
3588 let toml_language = Arc::new(Language::new(
3589 LanguageConfig {
3590 name: "TOML".into(),
3591 ..Default::default()
3592 },
3593 None,
3594 ));
3595 let rust_language = Arc::new(Language::new(
3596 LanguageConfig {
3597 name: "Rust".into(),
3598 ..Default::default()
3599 },
3600 None,
3601 ));
3602
3603 let toml_buffer =
3604 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3605 let rust_buffer =
3606 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3607 let multibuffer = cx.new(|cx| {
3608 let mut multibuffer = MultiBuffer::new(ReadWrite);
3609 multibuffer.push_excerpts(
3610 toml_buffer.clone(),
3611 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3612 cx,
3613 );
3614 multibuffer.push_excerpts(
3615 rust_buffer.clone(),
3616 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3617 cx,
3618 );
3619 multibuffer
3620 });
3621
3622 cx.add_window(|window, cx| {
3623 let mut editor = build_editor(multibuffer, window, cx);
3624
3625 assert_eq!(
3626 editor.text(cx),
3627 indoc! {"
3628 a = 1
3629 b = 2
3630
3631 const c: usize = 3;
3632 "}
3633 );
3634
3635 select_ranges(
3636 &mut editor,
3637 indoc! {"
3638 «aˇ» = 1
3639 b = 2
3640
3641 «const c:ˇ» usize = 3;
3642 "},
3643 window,
3644 cx,
3645 );
3646
3647 editor.tab(&Tab, window, cx);
3648 assert_text_with_selections(
3649 &mut editor,
3650 indoc! {"
3651 «aˇ» = 1
3652 b = 2
3653
3654 «const c:ˇ» usize = 3;
3655 "},
3656 cx,
3657 );
3658 editor.backtab(&Backtab, window, cx);
3659 assert_text_with_selections(
3660 &mut editor,
3661 indoc! {"
3662 «aˇ» = 1
3663 b = 2
3664
3665 «const c:ˇ» usize = 3;
3666 "},
3667 cx,
3668 );
3669
3670 editor
3671 });
3672}
3673
3674#[gpui::test]
3675async fn test_backspace(cx: &mut TestAppContext) {
3676 init_test(cx, |_| {});
3677
3678 let mut cx = EditorTestContext::new(cx).await;
3679
3680 // Basic backspace
3681 cx.set_state(indoc! {"
3682 onˇe two three
3683 fou«rˇ» five six
3684 seven «ˇeight nine
3685 »ten
3686 "});
3687 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3688 cx.assert_editor_state(indoc! {"
3689 oˇe two three
3690 fouˇ five six
3691 seven ˇten
3692 "});
3693
3694 // Test backspace inside and around indents
3695 cx.set_state(indoc! {"
3696 zero
3697 ˇone
3698 ˇtwo
3699 ˇ ˇ ˇ three
3700 ˇ ˇ four
3701 "});
3702 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3703 cx.assert_editor_state(indoc! {"
3704 zero
3705 ˇone
3706 ˇtwo
3707 ˇ threeˇ four
3708 "});
3709}
3710
3711#[gpui::test]
3712async fn test_delete(cx: &mut TestAppContext) {
3713 init_test(cx, |_| {});
3714
3715 let mut cx = EditorTestContext::new(cx).await;
3716 cx.set_state(indoc! {"
3717 onˇe two three
3718 fou«rˇ» five six
3719 seven «ˇeight nine
3720 »ten
3721 "});
3722 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3723 cx.assert_editor_state(indoc! {"
3724 onˇ two three
3725 fouˇ five six
3726 seven ˇten
3727 "});
3728}
3729
3730#[gpui::test]
3731fn test_delete_line(cx: &mut TestAppContext) {
3732 init_test(cx, |_| {});
3733
3734 let editor = cx.add_window(|window, cx| {
3735 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3736 build_editor(buffer, window, cx)
3737 });
3738 _ = editor.update(cx, |editor, window, cx| {
3739 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3740 s.select_display_ranges([
3741 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3742 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3743 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3744 ])
3745 });
3746 editor.delete_line(&DeleteLine, window, cx);
3747 assert_eq!(editor.display_text(cx), "ghi");
3748 assert_eq!(
3749 editor.selections.display_ranges(cx),
3750 vec![
3751 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3752 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3753 ]
3754 );
3755 });
3756
3757 let editor = cx.add_window(|window, cx| {
3758 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3759 build_editor(buffer, window, cx)
3760 });
3761 _ = editor.update(cx, |editor, window, cx| {
3762 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3763 s.select_display_ranges([
3764 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3765 ])
3766 });
3767 editor.delete_line(&DeleteLine, window, cx);
3768 assert_eq!(editor.display_text(cx), "ghi\n");
3769 assert_eq!(
3770 editor.selections.display_ranges(cx),
3771 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3772 );
3773 });
3774}
3775
3776#[gpui::test]
3777fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3778 init_test(cx, |_| {});
3779
3780 cx.add_window(|window, cx| {
3781 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3782 let mut editor = build_editor(buffer.clone(), window, cx);
3783 let buffer = buffer.read(cx).as_singleton().unwrap();
3784
3785 assert_eq!(
3786 editor.selections.ranges::<Point>(cx),
3787 &[Point::new(0, 0)..Point::new(0, 0)]
3788 );
3789
3790 // When on single line, replace newline at end by space
3791 editor.join_lines(&JoinLines, window, cx);
3792 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3793 assert_eq!(
3794 editor.selections.ranges::<Point>(cx),
3795 &[Point::new(0, 3)..Point::new(0, 3)]
3796 );
3797
3798 // When multiple lines are selected, remove newlines that are spanned by the selection
3799 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3800 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3801 });
3802 editor.join_lines(&JoinLines, window, cx);
3803 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3804 assert_eq!(
3805 editor.selections.ranges::<Point>(cx),
3806 &[Point::new(0, 11)..Point::new(0, 11)]
3807 );
3808
3809 // Undo should be transactional
3810 editor.undo(&Undo, window, cx);
3811 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3812 assert_eq!(
3813 editor.selections.ranges::<Point>(cx),
3814 &[Point::new(0, 5)..Point::new(2, 2)]
3815 );
3816
3817 // When joining an empty line don't insert a space
3818 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3819 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3820 });
3821 editor.join_lines(&JoinLines, window, cx);
3822 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3823 assert_eq!(
3824 editor.selections.ranges::<Point>(cx),
3825 [Point::new(2, 3)..Point::new(2, 3)]
3826 );
3827
3828 // We can remove trailing newlines
3829 editor.join_lines(&JoinLines, window, cx);
3830 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3831 assert_eq!(
3832 editor.selections.ranges::<Point>(cx),
3833 [Point::new(2, 3)..Point::new(2, 3)]
3834 );
3835
3836 // We don't blow up on the last line
3837 editor.join_lines(&JoinLines, window, cx);
3838 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3839 assert_eq!(
3840 editor.selections.ranges::<Point>(cx),
3841 [Point::new(2, 3)..Point::new(2, 3)]
3842 );
3843
3844 // reset to test indentation
3845 editor.buffer.update(cx, |buffer, cx| {
3846 buffer.edit(
3847 [
3848 (Point::new(1, 0)..Point::new(1, 2), " "),
3849 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3850 ],
3851 None,
3852 cx,
3853 )
3854 });
3855
3856 // We remove any leading spaces
3857 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3858 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3859 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3860 });
3861 editor.join_lines(&JoinLines, window, cx);
3862 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3863
3864 // We don't insert a space for a line containing only spaces
3865 editor.join_lines(&JoinLines, window, cx);
3866 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3867
3868 // We ignore any leading tabs
3869 editor.join_lines(&JoinLines, window, cx);
3870 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3871
3872 editor
3873 });
3874}
3875
3876#[gpui::test]
3877fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3878 init_test(cx, |_| {});
3879
3880 cx.add_window(|window, cx| {
3881 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3882 let mut editor = build_editor(buffer.clone(), window, cx);
3883 let buffer = buffer.read(cx).as_singleton().unwrap();
3884
3885 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3886 s.select_ranges([
3887 Point::new(0, 2)..Point::new(1, 1),
3888 Point::new(1, 2)..Point::new(1, 2),
3889 Point::new(3, 1)..Point::new(3, 2),
3890 ])
3891 });
3892
3893 editor.join_lines(&JoinLines, window, cx);
3894 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3895
3896 assert_eq!(
3897 editor.selections.ranges::<Point>(cx),
3898 [
3899 Point::new(0, 7)..Point::new(0, 7),
3900 Point::new(1, 3)..Point::new(1, 3)
3901 ]
3902 );
3903 editor
3904 });
3905}
3906
3907#[gpui::test]
3908async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3909 init_test(cx, |_| {});
3910
3911 let mut cx = EditorTestContext::new(cx).await;
3912
3913 let diff_base = r#"
3914 Line 0
3915 Line 1
3916 Line 2
3917 Line 3
3918 "#
3919 .unindent();
3920
3921 cx.set_state(
3922 &r#"
3923 ˇLine 0
3924 Line 1
3925 Line 2
3926 Line 3
3927 "#
3928 .unindent(),
3929 );
3930
3931 cx.set_head_text(&diff_base);
3932 executor.run_until_parked();
3933
3934 // Join lines
3935 cx.update_editor(|editor, window, cx| {
3936 editor.join_lines(&JoinLines, window, cx);
3937 });
3938 executor.run_until_parked();
3939
3940 cx.assert_editor_state(
3941 &r#"
3942 Line 0ˇ Line 1
3943 Line 2
3944 Line 3
3945 "#
3946 .unindent(),
3947 );
3948 // Join again
3949 cx.update_editor(|editor, window, cx| {
3950 editor.join_lines(&JoinLines, window, cx);
3951 });
3952 executor.run_until_parked();
3953
3954 cx.assert_editor_state(
3955 &r#"
3956 Line 0 Line 1ˇ Line 2
3957 Line 3
3958 "#
3959 .unindent(),
3960 );
3961}
3962
3963#[gpui::test]
3964async fn test_custom_newlines_cause_no_false_positive_diffs(
3965 executor: BackgroundExecutor,
3966 cx: &mut TestAppContext,
3967) {
3968 init_test(cx, |_| {});
3969 let mut cx = EditorTestContext::new(cx).await;
3970 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3971 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3972 executor.run_until_parked();
3973
3974 cx.update_editor(|editor, window, cx| {
3975 let snapshot = editor.snapshot(window, cx);
3976 assert_eq!(
3977 snapshot
3978 .buffer_snapshot
3979 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3980 .collect::<Vec<_>>(),
3981 Vec::new(),
3982 "Should not have any diffs for files with custom newlines"
3983 );
3984 });
3985}
3986
3987#[gpui::test]
3988async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
3989 init_test(cx, |_| {});
3990
3991 let mut cx = EditorTestContext::new(cx).await;
3992
3993 // Test sort_lines_case_insensitive()
3994 cx.set_state(indoc! {"
3995 «z
3996 y
3997 x
3998 Z
3999 Y
4000 Xˇ»
4001 "});
4002 cx.update_editor(|e, window, cx| {
4003 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4004 });
4005 cx.assert_editor_state(indoc! {"
4006 «x
4007 X
4008 y
4009 Y
4010 z
4011 Zˇ»
4012 "});
4013
4014 // Test reverse_lines()
4015 cx.set_state(indoc! {"
4016 «5
4017 4
4018 3
4019 2
4020 1ˇ»
4021 "});
4022 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4023 cx.assert_editor_state(indoc! {"
4024 «1
4025 2
4026 3
4027 4
4028 5ˇ»
4029 "});
4030
4031 // Skip testing shuffle_line()
4032
4033 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4034 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4035
4036 // Don't manipulate when cursor is on single line, but expand the selection
4037 cx.set_state(indoc! {"
4038 ddˇdd
4039 ccc
4040 bb
4041 a
4042 "});
4043 cx.update_editor(|e, window, cx| {
4044 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4045 });
4046 cx.assert_editor_state(indoc! {"
4047 «ddddˇ»
4048 ccc
4049 bb
4050 a
4051 "});
4052
4053 // Basic manipulate case
4054 // Start selection moves to column 0
4055 // End of selection shrinks to fit shorter line
4056 cx.set_state(indoc! {"
4057 dd«d
4058 ccc
4059 bb
4060 aaaaaˇ»
4061 "});
4062 cx.update_editor(|e, window, cx| {
4063 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4064 });
4065 cx.assert_editor_state(indoc! {"
4066 «aaaaa
4067 bb
4068 ccc
4069 dddˇ»
4070 "});
4071
4072 // Manipulate case with newlines
4073 cx.set_state(indoc! {"
4074 dd«d
4075 ccc
4076
4077 bb
4078 aaaaa
4079
4080 ˇ»
4081 "});
4082 cx.update_editor(|e, window, cx| {
4083 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4084 });
4085 cx.assert_editor_state(indoc! {"
4086 «
4087
4088 aaaaa
4089 bb
4090 ccc
4091 dddˇ»
4092
4093 "});
4094
4095 // Adding new line
4096 cx.set_state(indoc! {"
4097 aa«a
4098 bbˇ»b
4099 "});
4100 cx.update_editor(|e, window, cx| {
4101 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4102 });
4103 cx.assert_editor_state(indoc! {"
4104 «aaa
4105 bbb
4106 added_lineˇ»
4107 "});
4108
4109 // Removing line
4110 cx.set_state(indoc! {"
4111 aa«a
4112 bbbˇ»
4113 "});
4114 cx.update_editor(|e, window, cx| {
4115 e.manipulate_immutable_lines(window, cx, |lines| {
4116 lines.pop();
4117 })
4118 });
4119 cx.assert_editor_state(indoc! {"
4120 «aaaˇ»
4121 "});
4122
4123 // Removing all lines
4124 cx.set_state(indoc! {"
4125 aa«a
4126 bbbˇ»
4127 "});
4128 cx.update_editor(|e, window, cx| {
4129 e.manipulate_immutable_lines(window, cx, |lines| {
4130 lines.drain(..);
4131 })
4132 });
4133 cx.assert_editor_state(indoc! {"
4134 ˇ
4135 "});
4136}
4137
4138#[gpui::test]
4139async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4140 init_test(cx, |_| {});
4141
4142 let mut cx = EditorTestContext::new(cx).await;
4143
4144 // Consider continuous selection as single selection
4145 cx.set_state(indoc! {"
4146 Aaa«aa
4147 cˇ»c«c
4148 bb
4149 aaaˇ»aa
4150 "});
4151 cx.update_editor(|e, window, cx| {
4152 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4153 });
4154 cx.assert_editor_state(indoc! {"
4155 «Aaaaa
4156 ccc
4157 bb
4158 aaaaaˇ»
4159 "});
4160
4161 cx.set_state(indoc! {"
4162 Aaa«aa
4163 cˇ»c«c
4164 bb
4165 aaaˇ»aa
4166 "});
4167 cx.update_editor(|e, window, cx| {
4168 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4169 });
4170 cx.assert_editor_state(indoc! {"
4171 «Aaaaa
4172 ccc
4173 bbˇ»
4174 "});
4175
4176 // Consider non continuous selection as distinct dedup operations
4177 cx.set_state(indoc! {"
4178 «aaaaa
4179 bb
4180 aaaaa
4181 aaaaaˇ»
4182
4183 aaa«aaˇ»
4184 "});
4185 cx.update_editor(|e, window, cx| {
4186 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4187 });
4188 cx.assert_editor_state(indoc! {"
4189 «aaaaa
4190 bbˇ»
4191
4192 «aaaaaˇ»
4193 "});
4194}
4195
4196#[gpui::test]
4197async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4198 init_test(cx, |_| {});
4199
4200 let mut cx = EditorTestContext::new(cx).await;
4201
4202 cx.set_state(indoc! {"
4203 «Aaa
4204 aAa
4205 Aaaˇ»
4206 "});
4207 cx.update_editor(|e, window, cx| {
4208 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4209 });
4210 cx.assert_editor_state(indoc! {"
4211 «Aaa
4212 aAaˇ»
4213 "});
4214
4215 cx.set_state(indoc! {"
4216 «Aaa
4217 aAa
4218 aaAˇ»
4219 "});
4220 cx.update_editor(|e, window, cx| {
4221 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4222 });
4223 cx.assert_editor_state(indoc! {"
4224 «Aaaˇ»
4225 "});
4226}
4227
4228#[gpui::test]
4229async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4230 init_test(cx, |_| {});
4231
4232 let mut cx = EditorTestContext::new(cx).await;
4233
4234 // Manipulate with multiple selections on a single line
4235 cx.set_state(indoc! {"
4236 dd«dd
4237 cˇ»c«c
4238 bb
4239 aaaˇ»aa
4240 "});
4241 cx.update_editor(|e, window, cx| {
4242 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4243 });
4244 cx.assert_editor_state(indoc! {"
4245 «aaaaa
4246 bb
4247 ccc
4248 ddddˇ»
4249 "});
4250
4251 // Manipulate with multiple disjoin selections
4252 cx.set_state(indoc! {"
4253 5«
4254 4
4255 3
4256 2
4257 1ˇ»
4258
4259 dd«dd
4260 ccc
4261 bb
4262 aaaˇ»aa
4263 "});
4264 cx.update_editor(|e, window, cx| {
4265 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4266 });
4267 cx.assert_editor_state(indoc! {"
4268 «1
4269 2
4270 3
4271 4
4272 5ˇ»
4273
4274 «aaaaa
4275 bb
4276 ccc
4277 ddddˇ»
4278 "});
4279
4280 // Adding lines on each selection
4281 cx.set_state(indoc! {"
4282 2«
4283 1ˇ»
4284
4285 bb«bb
4286 aaaˇ»aa
4287 "});
4288 cx.update_editor(|e, window, cx| {
4289 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4290 });
4291 cx.assert_editor_state(indoc! {"
4292 «2
4293 1
4294 added lineˇ»
4295
4296 «bbbb
4297 aaaaa
4298 added lineˇ»
4299 "});
4300
4301 // Removing lines on each selection
4302 cx.set_state(indoc! {"
4303 2«
4304 1ˇ»
4305
4306 bb«bb
4307 aaaˇ»aa
4308 "});
4309 cx.update_editor(|e, window, cx| {
4310 e.manipulate_immutable_lines(window, cx, |lines| {
4311 lines.pop();
4312 })
4313 });
4314 cx.assert_editor_state(indoc! {"
4315 «2ˇ»
4316
4317 «bbbbˇ»
4318 "});
4319}
4320
4321#[gpui::test]
4322async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4323 init_test(cx, |settings| {
4324 settings.defaults.tab_size = NonZeroU32::new(3)
4325 });
4326
4327 let mut cx = EditorTestContext::new(cx).await;
4328
4329 // MULTI SELECTION
4330 // Ln.1 "«" tests empty lines
4331 // Ln.9 tests just leading whitespace
4332 cx.set_state(indoc! {"
4333 «
4334 abc // No indentationˇ»
4335 «\tabc // 1 tabˇ»
4336 \t\tabc « ˇ» // 2 tabs
4337 \t ab«c // Tab followed by space
4338 \tabc // Space followed by tab (3 spaces should be the result)
4339 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4340 abˇ»ˇc ˇ ˇ // Already space indented«
4341 \t
4342 \tabc\tdef // Only the leading tab is manipulatedˇ»
4343 "});
4344 cx.update_editor(|e, window, cx| {
4345 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4346 });
4347 cx.assert_editor_state(
4348 indoc! {"
4349 «
4350 abc // No indentation
4351 abc // 1 tab
4352 abc // 2 tabs
4353 abc // Tab followed by space
4354 abc // Space followed by tab (3 spaces should be the result)
4355 abc // Mixed indentation (tab conversion depends on the column)
4356 abc // Already space indented
4357 ·
4358 abc\tdef // Only the leading tab is manipulatedˇ»
4359 "}
4360 .replace("·", "")
4361 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4362 );
4363
4364 // Test on just a few lines, the others should remain unchanged
4365 // Only lines (3, 5, 10, 11) should change
4366 cx.set_state(
4367 indoc! {"
4368 ·
4369 abc // No indentation
4370 \tabcˇ // 1 tab
4371 \t\tabc // 2 tabs
4372 \t abcˇ // Tab followed by space
4373 \tabc // Space followed by tab (3 spaces should be the result)
4374 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4375 abc // Already space indented
4376 «\t
4377 \tabc\tdef // Only the leading tab is manipulatedˇ»
4378 "}
4379 .replace("·", "")
4380 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4381 );
4382 cx.update_editor(|e, window, cx| {
4383 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4384 });
4385 cx.assert_editor_state(
4386 indoc! {"
4387 ·
4388 abc // No indentation
4389 « abc // 1 tabˇ»
4390 \t\tabc // 2 tabs
4391 « abc // Tab followed by spaceˇ»
4392 \tabc // Space followed by tab (3 spaces should be the result)
4393 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4394 abc // Already space indented
4395 « ·
4396 abc\tdef // Only the leading tab is manipulatedˇ»
4397 "}
4398 .replace("·", "")
4399 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4400 );
4401
4402 // SINGLE SELECTION
4403 // Ln.1 "«" tests empty lines
4404 // Ln.9 tests just leading whitespace
4405 cx.set_state(indoc! {"
4406 «
4407 abc // No indentation
4408 \tabc // 1 tab
4409 \t\tabc // 2 tabs
4410 \t abc // Tab followed by space
4411 \tabc // Space followed by tab (3 spaces should be the result)
4412 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4413 abc // Already space indented
4414 \t
4415 \tabc\tdef // Only the leading tab is manipulatedˇ»
4416 "});
4417 cx.update_editor(|e, window, cx| {
4418 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4419 });
4420 cx.assert_editor_state(
4421 indoc! {"
4422 «
4423 abc // No indentation
4424 abc // 1 tab
4425 abc // 2 tabs
4426 abc // Tab followed by space
4427 abc // Space followed by tab (3 spaces should be the result)
4428 abc // Mixed indentation (tab conversion depends on the column)
4429 abc // Already space indented
4430 ·
4431 abc\tdef // Only the leading tab is manipulatedˇ»
4432 "}
4433 .replace("·", "")
4434 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4435 );
4436}
4437
4438#[gpui::test]
4439async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
4440 init_test(cx, |settings| {
4441 settings.defaults.tab_size = NonZeroU32::new(3)
4442 });
4443
4444 let mut cx = EditorTestContext::new(cx).await;
4445
4446 // MULTI SELECTION
4447 // Ln.1 "«" tests empty lines
4448 // Ln.11 tests just leading whitespace
4449 cx.set_state(indoc! {"
4450 «
4451 abˇ»ˇc // No indentation
4452 abc ˇ ˇ // 1 space (< 3 so dont convert)
4453 abc « // 2 spaces (< 3 so dont convert)
4454 abc // 3 spaces (convert)
4455 abc ˇ» // 5 spaces (1 tab + 2 spaces)
4456 «\tˇ»\t«\tˇ»abc // Already tab indented
4457 «\t abc // Tab followed by space
4458 \tabc // Space followed by tab (should be consumed due to tab)
4459 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4460 \tˇ» «\t
4461 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
4462 "});
4463 cx.update_editor(|e, window, cx| {
4464 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4465 });
4466 cx.assert_editor_state(indoc! {"
4467 «
4468 abc // No indentation
4469 abc // 1 space (< 3 so dont convert)
4470 abc // 2 spaces (< 3 so dont convert)
4471 \tabc // 3 spaces (convert)
4472 \t abc // 5 spaces (1 tab + 2 spaces)
4473 \t\t\tabc // Already tab indented
4474 \t abc // Tab followed by space
4475 \tabc // Space followed by tab (should be consumed due to tab)
4476 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4477 \t\t\t
4478 \tabc \t // Only the leading spaces should be convertedˇ»
4479 "});
4480
4481 // Test on just a few lines, the other should remain unchanged
4482 // Only lines (4, 8, 11, 12) should change
4483 cx.set_state(
4484 indoc! {"
4485 ·
4486 abc // No indentation
4487 abc // 1 space (< 3 so dont convert)
4488 abc // 2 spaces (< 3 so dont convert)
4489 « abc // 3 spaces (convert)ˇ»
4490 abc // 5 spaces (1 tab + 2 spaces)
4491 \t\t\tabc // Already tab indented
4492 \t abc // Tab followed by space
4493 \tabc ˇ // Space followed by tab (should be consumed due to tab)
4494 \t\t \tabc // Mixed indentation
4495 \t \t \t \tabc // Mixed indentation
4496 \t \tˇ
4497 « abc \t // Only the leading spaces should be convertedˇ»
4498 "}
4499 .replace("·", "")
4500 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4501 );
4502 cx.update_editor(|e, window, cx| {
4503 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4504 });
4505 cx.assert_editor_state(
4506 indoc! {"
4507 ·
4508 abc // No indentation
4509 abc // 1 space (< 3 so dont convert)
4510 abc // 2 spaces (< 3 so dont convert)
4511 «\tabc // 3 spaces (convert)ˇ»
4512 abc // 5 spaces (1 tab + 2 spaces)
4513 \t\t\tabc // Already tab indented
4514 \t abc // Tab followed by space
4515 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
4516 \t\t \tabc // Mixed indentation
4517 \t \t \t \tabc // Mixed indentation
4518 «\t\t\t
4519 \tabc \t // Only the leading spaces should be convertedˇ»
4520 "}
4521 .replace("·", "")
4522 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4523 );
4524
4525 // SINGLE SELECTION
4526 // Ln.1 "«" tests empty lines
4527 // Ln.11 tests just leading whitespace
4528 cx.set_state(indoc! {"
4529 «
4530 abc // No indentation
4531 abc // 1 space (< 3 so dont convert)
4532 abc // 2 spaces (< 3 so dont convert)
4533 abc // 3 spaces (convert)
4534 abc // 5 spaces (1 tab + 2 spaces)
4535 \t\t\tabc // Already tab indented
4536 \t abc // Tab followed by space
4537 \tabc // Space followed by tab (should be consumed due to tab)
4538 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4539 \t \t
4540 abc \t // Only the leading spaces should be convertedˇ»
4541 "});
4542 cx.update_editor(|e, window, cx| {
4543 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4544 });
4545 cx.assert_editor_state(indoc! {"
4546 «
4547 abc // No indentation
4548 abc // 1 space (< 3 so dont convert)
4549 abc // 2 spaces (< 3 so dont convert)
4550 \tabc // 3 spaces (convert)
4551 \t abc // 5 spaces (1 tab + 2 spaces)
4552 \t\t\tabc // Already tab indented
4553 \t abc // Tab followed by space
4554 \tabc // Space followed by tab (should be consumed due to tab)
4555 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4556 \t\t\t
4557 \tabc \t // Only the leading spaces should be convertedˇ»
4558 "});
4559}
4560
4561#[gpui::test]
4562async fn test_toggle_case(cx: &mut TestAppContext) {
4563 init_test(cx, |_| {});
4564
4565 let mut cx = EditorTestContext::new(cx).await;
4566
4567 // If all lower case -> upper case
4568 cx.set_state(indoc! {"
4569 «hello worldˇ»
4570 "});
4571 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4572 cx.assert_editor_state(indoc! {"
4573 «HELLO WORLDˇ»
4574 "});
4575
4576 // If all upper case -> lower case
4577 cx.set_state(indoc! {"
4578 «HELLO WORLDˇ»
4579 "});
4580 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4581 cx.assert_editor_state(indoc! {"
4582 «hello worldˇ»
4583 "});
4584
4585 // If any upper case characters are identified -> lower case
4586 // This matches JetBrains IDEs
4587 cx.set_state(indoc! {"
4588 «hEllo worldˇ»
4589 "});
4590 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4591 cx.assert_editor_state(indoc! {"
4592 «hello worldˇ»
4593 "});
4594}
4595
4596#[gpui::test]
4597async fn test_manipulate_text(cx: &mut TestAppContext) {
4598 init_test(cx, |_| {});
4599
4600 let mut cx = EditorTestContext::new(cx).await;
4601
4602 // Test convert_to_upper_case()
4603 cx.set_state(indoc! {"
4604 «hello worldˇ»
4605 "});
4606 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4607 cx.assert_editor_state(indoc! {"
4608 «HELLO WORLDˇ»
4609 "});
4610
4611 // Test convert_to_lower_case()
4612 cx.set_state(indoc! {"
4613 «HELLO WORLDˇ»
4614 "});
4615 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4616 cx.assert_editor_state(indoc! {"
4617 «hello worldˇ»
4618 "});
4619
4620 // Test multiple line, single selection case
4621 cx.set_state(indoc! {"
4622 «The quick brown
4623 fox jumps over
4624 the lazy dogˇ»
4625 "});
4626 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4627 cx.assert_editor_state(indoc! {"
4628 «The Quick Brown
4629 Fox Jumps Over
4630 The Lazy Dogˇ»
4631 "});
4632
4633 // Test multiple line, single selection case
4634 cx.set_state(indoc! {"
4635 «The quick brown
4636 fox jumps over
4637 the lazy dogˇ»
4638 "});
4639 cx.update_editor(|e, window, cx| {
4640 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4641 });
4642 cx.assert_editor_state(indoc! {"
4643 «TheQuickBrown
4644 FoxJumpsOver
4645 TheLazyDogˇ»
4646 "});
4647
4648 // From here on out, test more complex cases of manipulate_text()
4649
4650 // Test no selection case - should affect words cursors are in
4651 // Cursor at beginning, middle, and end of word
4652 cx.set_state(indoc! {"
4653 ˇhello big beauˇtiful worldˇ
4654 "});
4655 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4656 cx.assert_editor_state(indoc! {"
4657 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4658 "});
4659
4660 // Test multiple selections on a single line and across multiple lines
4661 cx.set_state(indoc! {"
4662 «Theˇ» quick «brown
4663 foxˇ» jumps «overˇ»
4664 the «lazyˇ» dog
4665 "});
4666 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4667 cx.assert_editor_state(indoc! {"
4668 «THEˇ» quick «BROWN
4669 FOXˇ» jumps «OVERˇ»
4670 the «LAZYˇ» dog
4671 "});
4672
4673 // Test case where text length grows
4674 cx.set_state(indoc! {"
4675 «tschüߡ»
4676 "});
4677 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4678 cx.assert_editor_state(indoc! {"
4679 «TSCHÜSSˇ»
4680 "});
4681
4682 // Test to make sure we don't crash when text shrinks
4683 cx.set_state(indoc! {"
4684 aaa_bbbˇ
4685 "});
4686 cx.update_editor(|e, window, cx| {
4687 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4688 });
4689 cx.assert_editor_state(indoc! {"
4690 «aaaBbbˇ»
4691 "});
4692
4693 // Test to make sure we all aware of the fact that each word can grow and shrink
4694 // Final selections should be aware of this fact
4695 cx.set_state(indoc! {"
4696 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4697 "});
4698 cx.update_editor(|e, window, cx| {
4699 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4700 });
4701 cx.assert_editor_state(indoc! {"
4702 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4703 "});
4704
4705 cx.set_state(indoc! {"
4706 «hElLo, WoRld!ˇ»
4707 "});
4708 cx.update_editor(|e, window, cx| {
4709 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4710 });
4711 cx.assert_editor_state(indoc! {"
4712 «HeLlO, wOrLD!ˇ»
4713 "});
4714}
4715
4716#[gpui::test]
4717fn test_duplicate_line(cx: &mut TestAppContext) {
4718 init_test(cx, |_| {});
4719
4720 let editor = cx.add_window(|window, cx| {
4721 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4722 build_editor(buffer, window, cx)
4723 });
4724 _ = editor.update(cx, |editor, window, cx| {
4725 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4726 s.select_display_ranges([
4727 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4728 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4729 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4730 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4731 ])
4732 });
4733 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4734 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4735 assert_eq!(
4736 editor.selections.display_ranges(cx),
4737 vec![
4738 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4739 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4740 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4741 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4742 ]
4743 );
4744 });
4745
4746 let editor = cx.add_window(|window, cx| {
4747 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4748 build_editor(buffer, window, cx)
4749 });
4750 _ = editor.update(cx, |editor, window, cx| {
4751 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4752 s.select_display_ranges([
4753 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4754 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4755 ])
4756 });
4757 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4758 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4759 assert_eq!(
4760 editor.selections.display_ranges(cx),
4761 vec![
4762 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4763 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4764 ]
4765 );
4766 });
4767
4768 // With `move_upwards` the selections stay in place, except for
4769 // the lines inserted above them
4770 let editor = cx.add_window(|window, cx| {
4771 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4772 build_editor(buffer, window, cx)
4773 });
4774 _ = editor.update(cx, |editor, window, cx| {
4775 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4776 s.select_display_ranges([
4777 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4778 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4779 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4780 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4781 ])
4782 });
4783 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4784 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4785 assert_eq!(
4786 editor.selections.display_ranges(cx),
4787 vec![
4788 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4789 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4790 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4791 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4792 ]
4793 );
4794 });
4795
4796 let editor = cx.add_window(|window, cx| {
4797 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4798 build_editor(buffer, window, cx)
4799 });
4800 _ = editor.update(cx, |editor, window, cx| {
4801 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4802 s.select_display_ranges([
4803 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4804 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4805 ])
4806 });
4807 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4808 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4809 assert_eq!(
4810 editor.selections.display_ranges(cx),
4811 vec![
4812 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4813 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4814 ]
4815 );
4816 });
4817
4818 let editor = cx.add_window(|window, cx| {
4819 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4820 build_editor(buffer, window, cx)
4821 });
4822 _ = editor.update(cx, |editor, window, cx| {
4823 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4824 s.select_display_ranges([
4825 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4826 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4827 ])
4828 });
4829 editor.duplicate_selection(&DuplicateSelection, window, cx);
4830 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4831 assert_eq!(
4832 editor.selections.display_ranges(cx),
4833 vec![
4834 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4835 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4836 ]
4837 );
4838 });
4839}
4840
4841#[gpui::test]
4842fn test_move_line_up_down(cx: &mut TestAppContext) {
4843 init_test(cx, |_| {});
4844
4845 let editor = cx.add_window(|window, cx| {
4846 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4847 build_editor(buffer, window, cx)
4848 });
4849 _ = editor.update(cx, |editor, window, cx| {
4850 editor.fold_creases(
4851 vec![
4852 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4853 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4854 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4855 ],
4856 true,
4857 window,
4858 cx,
4859 );
4860 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4861 s.select_display_ranges([
4862 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4863 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4864 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4865 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4866 ])
4867 });
4868 assert_eq!(
4869 editor.display_text(cx),
4870 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4871 );
4872
4873 editor.move_line_up(&MoveLineUp, window, cx);
4874 assert_eq!(
4875 editor.display_text(cx),
4876 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4877 );
4878 assert_eq!(
4879 editor.selections.display_ranges(cx),
4880 vec![
4881 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4882 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4883 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4884 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4885 ]
4886 );
4887 });
4888
4889 _ = editor.update(cx, |editor, window, cx| {
4890 editor.move_line_down(&MoveLineDown, window, cx);
4891 assert_eq!(
4892 editor.display_text(cx),
4893 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4894 );
4895 assert_eq!(
4896 editor.selections.display_ranges(cx),
4897 vec![
4898 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4899 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4900 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4901 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4902 ]
4903 );
4904 });
4905
4906 _ = editor.update(cx, |editor, window, cx| {
4907 editor.move_line_down(&MoveLineDown, window, cx);
4908 assert_eq!(
4909 editor.display_text(cx),
4910 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4911 );
4912 assert_eq!(
4913 editor.selections.display_ranges(cx),
4914 vec![
4915 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4916 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4917 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4918 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4919 ]
4920 );
4921 });
4922
4923 _ = editor.update(cx, |editor, window, cx| {
4924 editor.move_line_up(&MoveLineUp, window, cx);
4925 assert_eq!(
4926 editor.display_text(cx),
4927 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4928 );
4929 assert_eq!(
4930 editor.selections.display_ranges(cx),
4931 vec![
4932 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4933 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4934 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4935 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4936 ]
4937 );
4938 });
4939}
4940
4941#[gpui::test]
4942fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4943 init_test(cx, |_| {});
4944
4945 let editor = cx.add_window(|window, cx| {
4946 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4947 build_editor(buffer, window, cx)
4948 });
4949 _ = editor.update(cx, |editor, window, cx| {
4950 let snapshot = editor.buffer.read(cx).snapshot(cx);
4951 editor.insert_blocks(
4952 [BlockProperties {
4953 style: BlockStyle::Fixed,
4954 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4955 height: Some(1),
4956 render: Arc::new(|_| div().into_any()),
4957 priority: 0,
4958 render_in_minimap: true,
4959 }],
4960 Some(Autoscroll::fit()),
4961 cx,
4962 );
4963 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4964 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4965 });
4966 editor.move_line_down(&MoveLineDown, window, cx);
4967 });
4968}
4969
4970#[gpui::test]
4971async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4972 init_test(cx, |_| {});
4973
4974 let mut cx = EditorTestContext::new(cx).await;
4975 cx.set_state(
4976 &"
4977 ˇzero
4978 one
4979 two
4980 three
4981 four
4982 five
4983 "
4984 .unindent(),
4985 );
4986
4987 // Create a four-line block that replaces three lines of text.
4988 cx.update_editor(|editor, window, cx| {
4989 let snapshot = editor.snapshot(window, cx);
4990 let snapshot = &snapshot.buffer_snapshot;
4991 let placement = BlockPlacement::Replace(
4992 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4993 );
4994 editor.insert_blocks(
4995 [BlockProperties {
4996 placement,
4997 height: Some(4),
4998 style: BlockStyle::Sticky,
4999 render: Arc::new(|_| gpui::div().into_any_element()),
5000 priority: 0,
5001 render_in_minimap: true,
5002 }],
5003 None,
5004 cx,
5005 );
5006 });
5007
5008 // Move down so that the cursor touches the block.
5009 cx.update_editor(|editor, window, cx| {
5010 editor.move_down(&Default::default(), window, cx);
5011 });
5012 cx.assert_editor_state(
5013 &"
5014 zero
5015 «one
5016 two
5017 threeˇ»
5018 four
5019 five
5020 "
5021 .unindent(),
5022 );
5023
5024 // Move down past the block.
5025 cx.update_editor(|editor, window, cx| {
5026 editor.move_down(&Default::default(), window, cx);
5027 });
5028 cx.assert_editor_state(
5029 &"
5030 zero
5031 one
5032 two
5033 three
5034 ˇfour
5035 five
5036 "
5037 .unindent(),
5038 );
5039}
5040
5041#[gpui::test]
5042fn test_transpose(cx: &mut TestAppContext) {
5043 init_test(cx, |_| {});
5044
5045 _ = cx.add_window(|window, cx| {
5046 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5047 editor.set_style(EditorStyle::default(), window, cx);
5048 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5049 s.select_ranges([1..1])
5050 });
5051 editor.transpose(&Default::default(), window, cx);
5052 assert_eq!(editor.text(cx), "bac");
5053 assert_eq!(editor.selections.ranges(cx), [2..2]);
5054
5055 editor.transpose(&Default::default(), window, cx);
5056 assert_eq!(editor.text(cx), "bca");
5057 assert_eq!(editor.selections.ranges(cx), [3..3]);
5058
5059 editor.transpose(&Default::default(), window, cx);
5060 assert_eq!(editor.text(cx), "bac");
5061 assert_eq!(editor.selections.ranges(cx), [3..3]);
5062
5063 editor
5064 });
5065
5066 _ = cx.add_window(|window, cx| {
5067 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5068 editor.set_style(EditorStyle::default(), window, cx);
5069 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5070 s.select_ranges([3..3])
5071 });
5072 editor.transpose(&Default::default(), window, cx);
5073 assert_eq!(editor.text(cx), "acb\nde");
5074 assert_eq!(editor.selections.ranges(cx), [3..3]);
5075
5076 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5077 s.select_ranges([4..4])
5078 });
5079 editor.transpose(&Default::default(), window, cx);
5080 assert_eq!(editor.text(cx), "acbd\ne");
5081 assert_eq!(editor.selections.ranges(cx), [5..5]);
5082
5083 editor.transpose(&Default::default(), window, cx);
5084 assert_eq!(editor.text(cx), "acbde\n");
5085 assert_eq!(editor.selections.ranges(cx), [6..6]);
5086
5087 editor.transpose(&Default::default(), window, cx);
5088 assert_eq!(editor.text(cx), "acbd\ne");
5089 assert_eq!(editor.selections.ranges(cx), [6..6]);
5090
5091 editor
5092 });
5093
5094 _ = cx.add_window(|window, cx| {
5095 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5096 editor.set_style(EditorStyle::default(), window, cx);
5097 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5098 s.select_ranges([1..1, 2..2, 4..4])
5099 });
5100 editor.transpose(&Default::default(), window, cx);
5101 assert_eq!(editor.text(cx), "bacd\ne");
5102 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5103
5104 editor.transpose(&Default::default(), window, cx);
5105 assert_eq!(editor.text(cx), "bcade\n");
5106 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5107
5108 editor.transpose(&Default::default(), window, cx);
5109 assert_eq!(editor.text(cx), "bcda\ne");
5110 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5111
5112 editor.transpose(&Default::default(), window, cx);
5113 assert_eq!(editor.text(cx), "bcade\n");
5114 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5115
5116 editor.transpose(&Default::default(), window, cx);
5117 assert_eq!(editor.text(cx), "bcaed\n");
5118 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5119
5120 editor
5121 });
5122
5123 _ = cx.add_window(|window, cx| {
5124 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5125 editor.set_style(EditorStyle::default(), window, cx);
5126 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5127 s.select_ranges([4..4])
5128 });
5129 editor.transpose(&Default::default(), window, cx);
5130 assert_eq!(editor.text(cx), "🏀🍐✋");
5131 assert_eq!(editor.selections.ranges(cx), [8..8]);
5132
5133 editor.transpose(&Default::default(), window, cx);
5134 assert_eq!(editor.text(cx), "🏀✋🍐");
5135 assert_eq!(editor.selections.ranges(cx), [11..11]);
5136
5137 editor.transpose(&Default::default(), window, cx);
5138 assert_eq!(editor.text(cx), "🏀🍐✋");
5139 assert_eq!(editor.selections.ranges(cx), [11..11]);
5140
5141 editor
5142 });
5143}
5144
5145#[gpui::test]
5146async fn test_rewrap(cx: &mut TestAppContext) {
5147 init_test(cx, |settings| {
5148 settings.languages.0.extend([
5149 (
5150 "Markdown".into(),
5151 LanguageSettingsContent {
5152 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5153 preferred_line_length: Some(40),
5154 ..Default::default()
5155 },
5156 ),
5157 (
5158 "Plain Text".into(),
5159 LanguageSettingsContent {
5160 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5161 preferred_line_length: Some(40),
5162 ..Default::default()
5163 },
5164 ),
5165 (
5166 "C++".into(),
5167 LanguageSettingsContent {
5168 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5169 preferred_line_length: Some(40),
5170 ..Default::default()
5171 },
5172 ),
5173 (
5174 "Python".into(),
5175 LanguageSettingsContent {
5176 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5177 preferred_line_length: Some(40),
5178 ..Default::default()
5179 },
5180 ),
5181 (
5182 "Rust".into(),
5183 LanguageSettingsContent {
5184 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5185 preferred_line_length: Some(40),
5186 ..Default::default()
5187 },
5188 ),
5189 ])
5190 });
5191
5192 let mut cx = EditorTestContext::new(cx).await;
5193
5194 let cpp_language = Arc::new(Language::new(
5195 LanguageConfig {
5196 name: "C++".into(),
5197 line_comments: vec!["// ".into()],
5198 ..LanguageConfig::default()
5199 },
5200 None,
5201 ));
5202 let python_language = Arc::new(Language::new(
5203 LanguageConfig {
5204 name: "Python".into(),
5205 line_comments: vec!["# ".into()],
5206 ..LanguageConfig::default()
5207 },
5208 None,
5209 ));
5210 let markdown_language = Arc::new(Language::new(
5211 LanguageConfig {
5212 name: "Markdown".into(),
5213 rewrap_prefixes: vec![
5214 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5215 regex::Regex::new("[-*+]\\s+").unwrap(),
5216 ],
5217 ..LanguageConfig::default()
5218 },
5219 None,
5220 ));
5221 let rust_language = Arc::new(Language::new(
5222 LanguageConfig {
5223 name: "Rust".into(),
5224 line_comments: vec!["// ".into(), "/// ".into()],
5225 ..LanguageConfig::default()
5226 },
5227 Some(tree_sitter_rust::LANGUAGE.into()),
5228 ));
5229
5230 let plaintext_language = Arc::new(Language::new(
5231 LanguageConfig {
5232 name: "Plain Text".into(),
5233 ..LanguageConfig::default()
5234 },
5235 None,
5236 ));
5237
5238 // Test basic rewrapping of a long line with a cursor
5239 assert_rewrap(
5240 indoc! {"
5241 // ˇThis is a long comment that needs to be wrapped.
5242 "},
5243 indoc! {"
5244 // ˇThis is a long comment that needs to
5245 // be wrapped.
5246 "},
5247 cpp_language.clone(),
5248 &mut cx,
5249 );
5250
5251 // Test rewrapping a full selection
5252 assert_rewrap(
5253 indoc! {"
5254 «// This selected long comment needs to be wrapped.ˇ»"
5255 },
5256 indoc! {"
5257 «// This selected long comment needs to
5258 // be wrapped.ˇ»"
5259 },
5260 cpp_language.clone(),
5261 &mut cx,
5262 );
5263
5264 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5265 assert_rewrap(
5266 indoc! {"
5267 // ˇThis is the first line.
5268 // Thisˇ is the second line.
5269 // This is the thirdˇ line, all part of one paragraph.
5270 "},
5271 indoc! {"
5272 // ˇThis is the first line. Thisˇ is the
5273 // second line. This is the thirdˇ line,
5274 // all part of one paragraph.
5275 "},
5276 cpp_language.clone(),
5277 &mut cx,
5278 );
5279
5280 // Test multiple cursors in different paragraphs trigger separate rewraps
5281 assert_rewrap(
5282 indoc! {"
5283 // ˇThis is the first paragraph, first line.
5284 // ˇThis is the first paragraph, second line.
5285
5286 // ˇThis is the second paragraph, first line.
5287 // ˇThis is the second paragraph, second line.
5288 "},
5289 indoc! {"
5290 // ˇThis is the first paragraph, first
5291 // line. ˇThis is the first paragraph,
5292 // second line.
5293
5294 // ˇThis is the second paragraph, first
5295 // line. ˇThis is the second paragraph,
5296 // second line.
5297 "},
5298 cpp_language.clone(),
5299 &mut cx,
5300 );
5301
5302 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5303 assert_rewrap(
5304 indoc! {"
5305 «// A regular long long comment to be wrapped.
5306 /// A documentation long comment to be wrapped.ˇ»
5307 "},
5308 indoc! {"
5309 «// A regular long long comment to be
5310 // wrapped.
5311 /// A documentation long comment to be
5312 /// wrapped.ˇ»
5313 "},
5314 rust_language.clone(),
5315 &mut cx,
5316 );
5317
5318 // Test that change in indentation level trigger seperate rewraps
5319 assert_rewrap(
5320 indoc! {"
5321 fn foo() {
5322 «// This is a long comment at the base indent.
5323 // This is a long comment at the next indent.ˇ»
5324 }
5325 "},
5326 indoc! {"
5327 fn foo() {
5328 «// This is a long comment at the
5329 // base indent.
5330 // This is a long comment at the
5331 // next indent.ˇ»
5332 }
5333 "},
5334 rust_language.clone(),
5335 &mut cx,
5336 );
5337
5338 // Test that different comment prefix characters (e.g., '#') are handled correctly
5339 assert_rewrap(
5340 indoc! {"
5341 # ˇThis is a long comment using a pound sign.
5342 "},
5343 indoc! {"
5344 # ˇThis is a long comment using a pound
5345 # sign.
5346 "},
5347 python_language.clone(),
5348 &mut cx,
5349 );
5350
5351 // Test rewrapping only affects comments, not code even when selected
5352 assert_rewrap(
5353 indoc! {"
5354 «/// This doc comment is long and should be wrapped.
5355 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5356 "},
5357 indoc! {"
5358 «/// This doc comment is long and should
5359 /// be wrapped.
5360 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5361 "},
5362 rust_language.clone(),
5363 &mut cx,
5364 );
5365
5366 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
5367 assert_rewrap(
5368 indoc! {"
5369 # Header
5370
5371 A long long long line of markdown text to wrap.ˇ
5372 "},
5373 indoc! {"
5374 # Header
5375
5376 A long long long line of markdown text
5377 to wrap.ˇ
5378 "},
5379 markdown_language.clone(),
5380 &mut cx,
5381 );
5382
5383 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
5384 assert_rewrap(
5385 indoc! {"
5386 «1. This is a numbered list item that is very long and needs to be wrapped properly.
5387 2. This is a numbered list item that is very long and needs to be wrapped properly.
5388 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
5389 "},
5390 indoc! {"
5391 «1. This is a numbered list item that is
5392 very long and needs to be wrapped
5393 properly.
5394 2. This is a numbered list item that is
5395 very long and needs to be wrapped
5396 properly.
5397 - This is an unordered list item that is
5398 also very long and should not merge
5399 with the numbered item.ˇ»
5400 "},
5401 markdown_language.clone(),
5402 &mut cx,
5403 );
5404
5405 // Test that rewrapping add indents for rewrapping boundary if not exists already.
5406 assert_rewrap(
5407 indoc! {"
5408 «1. This is a numbered list item that is
5409 very long and needs to be wrapped
5410 properly.
5411 2. This is a numbered list item that is
5412 very long and needs to be wrapped
5413 properly.
5414 - This is an unordered list item that is
5415 also very long and should not merge with
5416 the numbered item.ˇ»
5417 "},
5418 indoc! {"
5419 «1. This is a numbered list item that is
5420 very long and needs to be wrapped
5421 properly.
5422 2. This is a numbered list item that is
5423 very long and needs to be wrapped
5424 properly.
5425 - This is an unordered list item that is
5426 also very long and should not merge
5427 with the numbered item.ˇ»
5428 "},
5429 markdown_language.clone(),
5430 &mut cx,
5431 );
5432
5433 // Test that rewrapping maintain indents even when they already exists.
5434 assert_rewrap(
5435 indoc! {"
5436 «1. This is a numbered list
5437 item that is very long and needs to be wrapped properly.
5438 2. This is a numbered list
5439 item that is very long and needs to be wrapped properly.
5440 - This is an unordered list item that is also very long and
5441 should not merge with the numbered item.ˇ»
5442 "},
5443 indoc! {"
5444 «1. This is a numbered list item that is
5445 very long and needs to be wrapped
5446 properly.
5447 2. This is a numbered list item that is
5448 very long and needs to be wrapped
5449 properly.
5450 - This is an unordered list item that is
5451 also very long and should not merge
5452 with the numbered item.ˇ»
5453 "},
5454 markdown_language.clone(),
5455 &mut cx,
5456 );
5457
5458 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
5459 assert_rewrap(
5460 indoc! {"
5461 ˇThis is a very long line of plain text that will be wrapped.
5462 "},
5463 indoc! {"
5464 ˇThis is a very long line of plain text
5465 that will be wrapped.
5466 "},
5467 plaintext_language.clone(),
5468 &mut cx,
5469 );
5470
5471 // Test that non-commented code acts as a paragraph boundary within a selection
5472 assert_rewrap(
5473 indoc! {"
5474 «// This is the first long comment block to be wrapped.
5475 fn my_func(a: u32);
5476 // This is the second long comment block to be wrapped.ˇ»
5477 "},
5478 indoc! {"
5479 «// This is the first long comment block
5480 // to be wrapped.
5481 fn my_func(a: u32);
5482 // This is the second long comment block
5483 // to be wrapped.ˇ»
5484 "},
5485 rust_language.clone(),
5486 &mut cx,
5487 );
5488
5489 // Test rewrapping multiple selections, including ones with blank lines or tabs
5490 assert_rewrap(
5491 indoc! {"
5492 «ˇThis is a very long line that will be wrapped.
5493
5494 This is another paragraph in the same selection.»
5495
5496 «\tThis is a very long indented line that will be wrapped.ˇ»
5497 "},
5498 indoc! {"
5499 «ˇThis is a very long line that will be
5500 wrapped.
5501
5502 This is another paragraph in the same
5503 selection.»
5504
5505 «\tThis is a very long indented line
5506 \tthat will be wrapped.ˇ»
5507 "},
5508 plaintext_language.clone(),
5509 &mut cx,
5510 );
5511
5512 // Test that an empty comment line acts as a paragraph boundary
5513 assert_rewrap(
5514 indoc! {"
5515 // ˇThis is a long comment that will be wrapped.
5516 //
5517 // And this is another long comment that will also be wrapped.ˇ
5518 "},
5519 indoc! {"
5520 // ˇThis is a long comment that will be
5521 // wrapped.
5522 //
5523 // And this is another long comment that
5524 // will also be wrapped.ˇ
5525 "},
5526 cpp_language,
5527 &mut cx,
5528 );
5529
5530 #[track_caller]
5531 fn assert_rewrap(
5532 unwrapped_text: &str,
5533 wrapped_text: &str,
5534 language: Arc<Language>,
5535 cx: &mut EditorTestContext,
5536 ) {
5537 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5538 cx.set_state(unwrapped_text);
5539 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5540 cx.assert_editor_state(wrapped_text);
5541 }
5542}
5543
5544#[gpui::test]
5545async fn test_hard_wrap(cx: &mut TestAppContext) {
5546 init_test(cx, |_| {});
5547 let mut cx = EditorTestContext::new(cx).await;
5548
5549 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5550 cx.update_editor(|editor, _, cx| {
5551 editor.set_hard_wrap(Some(14), cx);
5552 });
5553
5554 cx.set_state(indoc!(
5555 "
5556 one two three ˇ
5557 "
5558 ));
5559 cx.simulate_input("four");
5560 cx.run_until_parked();
5561
5562 cx.assert_editor_state(indoc!(
5563 "
5564 one two three
5565 fourˇ
5566 "
5567 ));
5568
5569 cx.update_editor(|editor, window, cx| {
5570 editor.newline(&Default::default(), window, cx);
5571 });
5572 cx.run_until_parked();
5573 cx.assert_editor_state(indoc!(
5574 "
5575 one two three
5576 four
5577 ˇ
5578 "
5579 ));
5580
5581 cx.simulate_input("five");
5582 cx.run_until_parked();
5583 cx.assert_editor_state(indoc!(
5584 "
5585 one two three
5586 four
5587 fiveˇ
5588 "
5589 ));
5590
5591 cx.update_editor(|editor, window, cx| {
5592 editor.newline(&Default::default(), window, cx);
5593 });
5594 cx.run_until_parked();
5595 cx.simulate_input("# ");
5596 cx.run_until_parked();
5597 cx.assert_editor_state(indoc!(
5598 "
5599 one two three
5600 four
5601 five
5602 # ˇ
5603 "
5604 ));
5605
5606 cx.update_editor(|editor, window, cx| {
5607 editor.newline(&Default::default(), window, cx);
5608 });
5609 cx.run_until_parked();
5610 cx.assert_editor_state(indoc!(
5611 "
5612 one two three
5613 four
5614 five
5615 #\x20
5616 #ˇ
5617 "
5618 ));
5619
5620 cx.simulate_input(" 6");
5621 cx.run_until_parked();
5622 cx.assert_editor_state(indoc!(
5623 "
5624 one two three
5625 four
5626 five
5627 #
5628 # 6ˇ
5629 "
5630 ));
5631}
5632
5633#[gpui::test]
5634async fn test_clipboard(cx: &mut TestAppContext) {
5635 init_test(cx, |_| {});
5636
5637 let mut cx = EditorTestContext::new(cx).await;
5638
5639 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5640 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5641 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5642
5643 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5644 cx.set_state("two ˇfour ˇsix ˇ");
5645 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5646 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5647
5648 // Paste again but with only two cursors. Since the number of cursors doesn't
5649 // match the number of slices in the clipboard, the entire clipboard text
5650 // is pasted at each cursor.
5651 cx.set_state("ˇtwo one✅ four three six five ˇ");
5652 cx.update_editor(|e, window, cx| {
5653 e.handle_input("( ", window, cx);
5654 e.paste(&Paste, window, cx);
5655 e.handle_input(") ", window, cx);
5656 });
5657 cx.assert_editor_state(
5658 &([
5659 "( one✅ ",
5660 "three ",
5661 "five ) ˇtwo one✅ four three six five ( one✅ ",
5662 "three ",
5663 "five ) ˇ",
5664 ]
5665 .join("\n")),
5666 );
5667
5668 // Cut with three selections, one of which is full-line.
5669 cx.set_state(indoc! {"
5670 1«2ˇ»3
5671 4ˇ567
5672 «8ˇ»9"});
5673 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5674 cx.assert_editor_state(indoc! {"
5675 1ˇ3
5676 ˇ9"});
5677
5678 // Paste with three selections, noticing how the copied selection that was full-line
5679 // gets inserted before the second cursor.
5680 cx.set_state(indoc! {"
5681 1ˇ3
5682 9ˇ
5683 «oˇ»ne"});
5684 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5685 cx.assert_editor_state(indoc! {"
5686 12ˇ3
5687 4567
5688 9ˇ
5689 8ˇne"});
5690
5691 // Copy with a single cursor only, which writes the whole line into the clipboard.
5692 cx.set_state(indoc! {"
5693 The quick brown
5694 fox juˇmps over
5695 the lazy dog"});
5696 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5697 assert_eq!(
5698 cx.read_from_clipboard()
5699 .and_then(|item| item.text().as_deref().map(str::to_string)),
5700 Some("fox jumps over\n".to_string())
5701 );
5702
5703 // Paste with three selections, noticing how the copied full-line selection is inserted
5704 // before the empty selections but replaces the selection that is non-empty.
5705 cx.set_state(indoc! {"
5706 Tˇhe quick brown
5707 «foˇ»x jumps over
5708 tˇhe lazy dog"});
5709 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5710 cx.assert_editor_state(indoc! {"
5711 fox jumps over
5712 Tˇhe quick brown
5713 fox jumps over
5714 ˇx jumps over
5715 fox jumps over
5716 tˇhe lazy dog"});
5717}
5718
5719#[gpui::test]
5720async fn test_copy_trim(cx: &mut TestAppContext) {
5721 init_test(cx, |_| {});
5722
5723 let mut cx = EditorTestContext::new(cx).await;
5724 cx.set_state(
5725 r#" «for selection in selections.iter() {
5726 let mut start = selection.start;
5727 let mut end = selection.end;
5728 let is_entire_line = selection.is_empty();
5729 if is_entire_line {
5730 start = Point::new(start.row, 0);ˇ»
5731 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5732 }
5733 "#,
5734 );
5735 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5736 assert_eq!(
5737 cx.read_from_clipboard()
5738 .and_then(|item| item.text().as_deref().map(str::to_string)),
5739 Some(
5740 "for selection in selections.iter() {
5741 let mut start = selection.start;
5742 let mut end = selection.end;
5743 let is_entire_line = selection.is_empty();
5744 if is_entire_line {
5745 start = Point::new(start.row, 0);"
5746 .to_string()
5747 ),
5748 "Regular copying preserves all indentation selected",
5749 );
5750 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5751 assert_eq!(
5752 cx.read_from_clipboard()
5753 .and_then(|item| item.text().as_deref().map(str::to_string)),
5754 Some(
5755 "for selection in selections.iter() {
5756let mut start = selection.start;
5757let mut end = selection.end;
5758let is_entire_line = selection.is_empty();
5759if is_entire_line {
5760 start = Point::new(start.row, 0);"
5761 .to_string()
5762 ),
5763 "Copying with stripping should strip all leading whitespaces"
5764 );
5765
5766 cx.set_state(
5767 r#" « for selection in selections.iter() {
5768 let mut start = selection.start;
5769 let mut end = selection.end;
5770 let is_entire_line = selection.is_empty();
5771 if is_entire_line {
5772 start = Point::new(start.row, 0);ˇ»
5773 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5774 }
5775 "#,
5776 );
5777 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5778 assert_eq!(
5779 cx.read_from_clipboard()
5780 .and_then(|item| item.text().as_deref().map(str::to_string)),
5781 Some(
5782 " for selection in selections.iter() {
5783 let mut start = selection.start;
5784 let mut end = selection.end;
5785 let is_entire_line = selection.is_empty();
5786 if is_entire_line {
5787 start = Point::new(start.row, 0);"
5788 .to_string()
5789 ),
5790 "Regular copying preserves all indentation selected",
5791 );
5792 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5793 assert_eq!(
5794 cx.read_from_clipboard()
5795 .and_then(|item| item.text().as_deref().map(str::to_string)),
5796 Some(
5797 "for selection in selections.iter() {
5798let mut start = selection.start;
5799let mut end = selection.end;
5800let is_entire_line = selection.is_empty();
5801if is_entire_line {
5802 start = Point::new(start.row, 0);"
5803 .to_string()
5804 ),
5805 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5806 );
5807
5808 cx.set_state(
5809 r#" «ˇ for selection in selections.iter() {
5810 let mut start = selection.start;
5811 let mut end = selection.end;
5812 let is_entire_line = selection.is_empty();
5813 if is_entire_line {
5814 start = Point::new(start.row, 0);»
5815 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5816 }
5817 "#,
5818 );
5819 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5820 assert_eq!(
5821 cx.read_from_clipboard()
5822 .and_then(|item| item.text().as_deref().map(str::to_string)),
5823 Some(
5824 " for selection in selections.iter() {
5825 let mut start = selection.start;
5826 let mut end = selection.end;
5827 let is_entire_line = selection.is_empty();
5828 if is_entire_line {
5829 start = Point::new(start.row, 0);"
5830 .to_string()
5831 ),
5832 "Regular copying for reverse selection works the same",
5833 );
5834 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5835 assert_eq!(
5836 cx.read_from_clipboard()
5837 .and_then(|item| item.text().as_deref().map(str::to_string)),
5838 Some(
5839 "for selection in selections.iter() {
5840let mut start = selection.start;
5841let mut end = selection.end;
5842let is_entire_line = selection.is_empty();
5843if is_entire_line {
5844 start = Point::new(start.row, 0);"
5845 .to_string()
5846 ),
5847 "Copying with stripping for reverse selection works the same"
5848 );
5849
5850 cx.set_state(
5851 r#" for selection «in selections.iter() {
5852 let mut start = selection.start;
5853 let mut end = selection.end;
5854 let is_entire_line = selection.is_empty();
5855 if is_entire_line {
5856 start = Point::new(start.row, 0);ˇ»
5857 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5858 }
5859 "#,
5860 );
5861 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5862 assert_eq!(
5863 cx.read_from_clipboard()
5864 .and_then(|item| item.text().as_deref().map(str::to_string)),
5865 Some(
5866 "in selections.iter() {
5867 let mut start = selection.start;
5868 let mut end = selection.end;
5869 let is_entire_line = selection.is_empty();
5870 if is_entire_line {
5871 start = Point::new(start.row, 0);"
5872 .to_string()
5873 ),
5874 "When selecting past the indent, the copying works as usual",
5875 );
5876 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5877 assert_eq!(
5878 cx.read_from_clipboard()
5879 .and_then(|item| item.text().as_deref().map(str::to_string)),
5880 Some(
5881 "in selections.iter() {
5882 let mut start = selection.start;
5883 let mut end = selection.end;
5884 let is_entire_line = selection.is_empty();
5885 if is_entire_line {
5886 start = Point::new(start.row, 0);"
5887 .to_string()
5888 ),
5889 "When selecting past the indent, nothing is trimmed"
5890 );
5891
5892 cx.set_state(
5893 r#" «for selection in selections.iter() {
5894 let mut start = selection.start;
5895
5896 let mut end = selection.end;
5897 let is_entire_line = selection.is_empty();
5898 if is_entire_line {
5899 start = Point::new(start.row, 0);
5900ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5901 }
5902 "#,
5903 );
5904 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5905 assert_eq!(
5906 cx.read_from_clipboard()
5907 .and_then(|item| item.text().as_deref().map(str::to_string)),
5908 Some(
5909 "for selection in selections.iter() {
5910let mut start = selection.start;
5911
5912let mut end = selection.end;
5913let is_entire_line = selection.is_empty();
5914if is_entire_line {
5915 start = Point::new(start.row, 0);
5916"
5917 .to_string()
5918 ),
5919 "Copying with stripping should ignore empty lines"
5920 );
5921}
5922
5923#[gpui::test]
5924async fn test_paste_multiline(cx: &mut TestAppContext) {
5925 init_test(cx, |_| {});
5926
5927 let mut cx = EditorTestContext::new(cx).await;
5928 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5929
5930 // Cut an indented block, without the leading whitespace.
5931 cx.set_state(indoc! {"
5932 const a: B = (
5933 c(),
5934 «d(
5935 e,
5936 f
5937 )ˇ»
5938 );
5939 "});
5940 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5941 cx.assert_editor_state(indoc! {"
5942 const a: B = (
5943 c(),
5944 ˇ
5945 );
5946 "});
5947
5948 // Paste it at the same position.
5949 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5950 cx.assert_editor_state(indoc! {"
5951 const a: B = (
5952 c(),
5953 d(
5954 e,
5955 f
5956 )ˇ
5957 );
5958 "});
5959
5960 // Paste it at a line with a lower indent level.
5961 cx.set_state(indoc! {"
5962 ˇ
5963 const a: B = (
5964 c(),
5965 );
5966 "});
5967 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5968 cx.assert_editor_state(indoc! {"
5969 d(
5970 e,
5971 f
5972 )ˇ
5973 const a: B = (
5974 c(),
5975 );
5976 "});
5977
5978 // Cut an indented block, with the leading whitespace.
5979 cx.set_state(indoc! {"
5980 const a: B = (
5981 c(),
5982 « d(
5983 e,
5984 f
5985 )
5986 ˇ»);
5987 "});
5988 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5989 cx.assert_editor_state(indoc! {"
5990 const a: B = (
5991 c(),
5992 ˇ);
5993 "});
5994
5995 // Paste it at the same position.
5996 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5997 cx.assert_editor_state(indoc! {"
5998 const a: B = (
5999 c(),
6000 d(
6001 e,
6002 f
6003 )
6004 ˇ);
6005 "});
6006
6007 // Paste it at a line with a higher indent level.
6008 cx.set_state(indoc! {"
6009 const a: B = (
6010 c(),
6011 d(
6012 e,
6013 fˇ
6014 )
6015 );
6016 "});
6017 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6018 cx.assert_editor_state(indoc! {"
6019 const a: B = (
6020 c(),
6021 d(
6022 e,
6023 f d(
6024 e,
6025 f
6026 )
6027 ˇ
6028 )
6029 );
6030 "});
6031
6032 // Copy an indented block, starting mid-line
6033 cx.set_state(indoc! {"
6034 const a: B = (
6035 c(),
6036 somethin«g(
6037 e,
6038 f
6039 )ˇ»
6040 );
6041 "});
6042 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6043
6044 // Paste it on a line with a lower indent level
6045 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
6046 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6047 cx.assert_editor_state(indoc! {"
6048 const a: B = (
6049 c(),
6050 something(
6051 e,
6052 f
6053 )
6054 );
6055 g(
6056 e,
6057 f
6058 )ˇ"});
6059}
6060
6061#[gpui::test]
6062async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
6063 init_test(cx, |_| {});
6064
6065 cx.write_to_clipboard(ClipboardItem::new_string(
6066 " d(\n e\n );\n".into(),
6067 ));
6068
6069 let mut cx = EditorTestContext::new(cx).await;
6070 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6071
6072 cx.set_state(indoc! {"
6073 fn a() {
6074 b();
6075 if c() {
6076 ˇ
6077 }
6078 }
6079 "});
6080
6081 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6082 cx.assert_editor_state(indoc! {"
6083 fn a() {
6084 b();
6085 if c() {
6086 d(
6087 e
6088 );
6089 ˇ
6090 }
6091 }
6092 "});
6093
6094 cx.set_state(indoc! {"
6095 fn a() {
6096 b();
6097 ˇ
6098 }
6099 "});
6100
6101 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6102 cx.assert_editor_state(indoc! {"
6103 fn a() {
6104 b();
6105 d(
6106 e
6107 );
6108 ˇ
6109 }
6110 "});
6111}
6112
6113#[gpui::test]
6114fn test_select_all(cx: &mut TestAppContext) {
6115 init_test(cx, |_| {});
6116
6117 let editor = cx.add_window(|window, cx| {
6118 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6119 build_editor(buffer, window, cx)
6120 });
6121 _ = editor.update(cx, |editor, window, cx| {
6122 editor.select_all(&SelectAll, window, cx);
6123 assert_eq!(
6124 editor.selections.display_ranges(cx),
6125 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6126 );
6127 });
6128}
6129
6130#[gpui::test]
6131fn test_select_line(cx: &mut TestAppContext) {
6132 init_test(cx, |_| {});
6133
6134 let editor = cx.add_window(|window, cx| {
6135 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6136 build_editor(buffer, window, cx)
6137 });
6138 _ = editor.update(cx, |editor, window, cx| {
6139 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6140 s.select_display_ranges([
6141 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6142 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6143 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6144 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6145 ])
6146 });
6147 editor.select_line(&SelectLine, window, cx);
6148 assert_eq!(
6149 editor.selections.display_ranges(cx),
6150 vec![
6151 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6152 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6153 ]
6154 );
6155 });
6156
6157 _ = editor.update(cx, |editor, window, cx| {
6158 editor.select_line(&SelectLine, window, cx);
6159 assert_eq!(
6160 editor.selections.display_ranges(cx),
6161 vec![
6162 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6163 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6164 ]
6165 );
6166 });
6167
6168 _ = editor.update(cx, |editor, window, cx| {
6169 editor.select_line(&SelectLine, window, cx);
6170 assert_eq!(
6171 editor.selections.display_ranges(cx),
6172 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6173 );
6174 });
6175}
6176
6177#[gpui::test]
6178async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6179 init_test(cx, |_| {});
6180 let mut cx = EditorTestContext::new(cx).await;
6181
6182 #[track_caller]
6183 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6184 cx.set_state(initial_state);
6185 cx.update_editor(|e, window, cx| {
6186 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
6187 });
6188 cx.assert_editor_state(expected_state);
6189 }
6190
6191 // Selection starts and ends at the middle of lines, left-to-right
6192 test(
6193 &mut cx,
6194 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6195 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6196 );
6197 // Same thing, right-to-left
6198 test(
6199 &mut cx,
6200 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6201 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6202 );
6203
6204 // Whole buffer, left-to-right, last line *doesn't* end with newline
6205 test(
6206 &mut cx,
6207 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6208 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6209 );
6210 // Same thing, right-to-left
6211 test(
6212 &mut cx,
6213 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6214 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6215 );
6216
6217 // Whole buffer, left-to-right, last line ends with newline
6218 test(
6219 &mut cx,
6220 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6221 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6222 );
6223 // Same thing, right-to-left
6224 test(
6225 &mut cx,
6226 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6227 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6228 );
6229
6230 // Starts at the end of a line, ends at the start of another
6231 test(
6232 &mut cx,
6233 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6234 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6235 );
6236}
6237
6238#[gpui::test]
6239async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6240 init_test(cx, |_| {});
6241
6242 let editor = cx.add_window(|window, cx| {
6243 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6244 build_editor(buffer, window, cx)
6245 });
6246
6247 // setup
6248 _ = editor.update(cx, |editor, window, cx| {
6249 editor.fold_creases(
6250 vec![
6251 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6252 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6253 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6254 ],
6255 true,
6256 window,
6257 cx,
6258 );
6259 assert_eq!(
6260 editor.display_text(cx),
6261 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6262 );
6263 });
6264
6265 _ = editor.update(cx, |editor, window, cx| {
6266 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6267 s.select_display_ranges([
6268 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6269 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6270 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6271 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6272 ])
6273 });
6274 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6275 assert_eq!(
6276 editor.display_text(cx),
6277 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6278 );
6279 });
6280 EditorTestContext::for_editor(editor, cx)
6281 .await
6282 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6283
6284 _ = editor.update(cx, |editor, window, cx| {
6285 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6286 s.select_display_ranges([
6287 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6288 ])
6289 });
6290 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6291 assert_eq!(
6292 editor.display_text(cx),
6293 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6294 );
6295 assert_eq!(
6296 editor.selections.display_ranges(cx),
6297 [
6298 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6299 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6300 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6301 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6302 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6303 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6304 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6305 ]
6306 );
6307 });
6308 EditorTestContext::for_editor(editor, cx)
6309 .await
6310 .assert_editor_state(
6311 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6312 );
6313}
6314
6315#[gpui::test]
6316async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6317 init_test(cx, |_| {});
6318
6319 let mut cx = EditorTestContext::new(cx).await;
6320
6321 cx.set_state(indoc!(
6322 r#"abc
6323 defˇghi
6324
6325 jk
6326 nlmo
6327 "#
6328 ));
6329
6330 cx.update_editor(|editor, window, cx| {
6331 editor.add_selection_above(&Default::default(), window, cx);
6332 });
6333
6334 cx.assert_editor_state(indoc!(
6335 r#"abcˇ
6336 defˇghi
6337
6338 jk
6339 nlmo
6340 "#
6341 ));
6342
6343 cx.update_editor(|editor, window, cx| {
6344 editor.add_selection_above(&Default::default(), window, cx);
6345 });
6346
6347 cx.assert_editor_state(indoc!(
6348 r#"abcˇ
6349 defˇghi
6350
6351 jk
6352 nlmo
6353 "#
6354 ));
6355
6356 cx.update_editor(|editor, window, cx| {
6357 editor.add_selection_below(&Default::default(), window, cx);
6358 });
6359
6360 cx.assert_editor_state(indoc!(
6361 r#"abc
6362 defˇghi
6363
6364 jk
6365 nlmo
6366 "#
6367 ));
6368
6369 cx.update_editor(|editor, window, cx| {
6370 editor.undo_selection(&Default::default(), window, cx);
6371 });
6372
6373 cx.assert_editor_state(indoc!(
6374 r#"abcˇ
6375 defˇghi
6376
6377 jk
6378 nlmo
6379 "#
6380 ));
6381
6382 cx.update_editor(|editor, window, cx| {
6383 editor.redo_selection(&Default::default(), window, cx);
6384 });
6385
6386 cx.assert_editor_state(indoc!(
6387 r#"abc
6388 defˇghi
6389
6390 jk
6391 nlmo
6392 "#
6393 ));
6394
6395 cx.update_editor(|editor, window, cx| {
6396 editor.add_selection_below(&Default::default(), window, cx);
6397 });
6398
6399 cx.assert_editor_state(indoc!(
6400 r#"abc
6401 defˇghi
6402 ˇ
6403 jk
6404 nlmo
6405 "#
6406 ));
6407
6408 cx.update_editor(|editor, window, cx| {
6409 editor.add_selection_below(&Default::default(), window, cx);
6410 });
6411
6412 cx.assert_editor_state(indoc!(
6413 r#"abc
6414 defˇghi
6415 ˇ
6416 jkˇ
6417 nlmo
6418 "#
6419 ));
6420
6421 cx.update_editor(|editor, window, cx| {
6422 editor.add_selection_below(&Default::default(), window, cx);
6423 });
6424
6425 cx.assert_editor_state(indoc!(
6426 r#"abc
6427 defˇghi
6428 ˇ
6429 jkˇ
6430 nlmˇo
6431 "#
6432 ));
6433
6434 cx.update_editor(|editor, window, cx| {
6435 editor.add_selection_below(&Default::default(), window, cx);
6436 });
6437
6438 cx.assert_editor_state(indoc!(
6439 r#"abc
6440 defˇghi
6441 ˇ
6442 jkˇ
6443 nlmˇo
6444 ˇ"#
6445 ));
6446
6447 // change selections
6448 cx.set_state(indoc!(
6449 r#"abc
6450 def«ˇg»hi
6451
6452 jk
6453 nlmo
6454 "#
6455 ));
6456
6457 cx.update_editor(|editor, window, cx| {
6458 editor.add_selection_below(&Default::default(), window, cx);
6459 });
6460
6461 cx.assert_editor_state(indoc!(
6462 r#"abc
6463 def«ˇg»hi
6464
6465 jk
6466 nlm«ˇo»
6467 "#
6468 ));
6469
6470 cx.update_editor(|editor, window, cx| {
6471 editor.add_selection_below(&Default::default(), window, cx);
6472 });
6473
6474 cx.assert_editor_state(indoc!(
6475 r#"abc
6476 def«ˇg»hi
6477
6478 jk
6479 nlm«ˇo»
6480 "#
6481 ));
6482
6483 cx.update_editor(|editor, window, cx| {
6484 editor.add_selection_above(&Default::default(), window, cx);
6485 });
6486
6487 cx.assert_editor_state(indoc!(
6488 r#"abc
6489 def«ˇg»hi
6490
6491 jk
6492 nlmo
6493 "#
6494 ));
6495
6496 cx.update_editor(|editor, window, cx| {
6497 editor.add_selection_above(&Default::default(), window, cx);
6498 });
6499
6500 cx.assert_editor_state(indoc!(
6501 r#"abc
6502 def«ˇg»hi
6503
6504 jk
6505 nlmo
6506 "#
6507 ));
6508
6509 // Change selections again
6510 cx.set_state(indoc!(
6511 r#"a«bc
6512 defgˇ»hi
6513
6514 jk
6515 nlmo
6516 "#
6517 ));
6518
6519 cx.update_editor(|editor, window, cx| {
6520 editor.add_selection_below(&Default::default(), window, cx);
6521 });
6522
6523 cx.assert_editor_state(indoc!(
6524 r#"a«bcˇ»
6525 d«efgˇ»hi
6526
6527 j«kˇ»
6528 nlmo
6529 "#
6530 ));
6531
6532 cx.update_editor(|editor, window, cx| {
6533 editor.add_selection_below(&Default::default(), window, cx);
6534 });
6535 cx.assert_editor_state(indoc!(
6536 r#"a«bcˇ»
6537 d«efgˇ»hi
6538
6539 j«kˇ»
6540 n«lmoˇ»
6541 "#
6542 ));
6543 cx.update_editor(|editor, window, cx| {
6544 editor.add_selection_above(&Default::default(), window, cx);
6545 });
6546
6547 cx.assert_editor_state(indoc!(
6548 r#"a«bcˇ»
6549 d«efgˇ»hi
6550
6551 j«kˇ»
6552 nlmo
6553 "#
6554 ));
6555
6556 // Change selections again
6557 cx.set_state(indoc!(
6558 r#"abc
6559 d«ˇefghi
6560
6561 jk
6562 nlm»o
6563 "#
6564 ));
6565
6566 cx.update_editor(|editor, window, cx| {
6567 editor.add_selection_above(&Default::default(), window, cx);
6568 });
6569
6570 cx.assert_editor_state(indoc!(
6571 r#"a«ˇbc»
6572 d«ˇef»ghi
6573
6574 j«ˇk»
6575 n«ˇlm»o
6576 "#
6577 ));
6578
6579 cx.update_editor(|editor, window, cx| {
6580 editor.add_selection_below(&Default::default(), window, cx);
6581 });
6582
6583 cx.assert_editor_state(indoc!(
6584 r#"abc
6585 d«ˇef»ghi
6586
6587 j«ˇk»
6588 n«ˇlm»o
6589 "#
6590 ));
6591}
6592
6593#[gpui::test]
6594async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6595 init_test(cx, |_| {});
6596 let mut cx = EditorTestContext::new(cx).await;
6597
6598 cx.set_state(indoc!(
6599 r#"line onˇe
6600 liˇne two
6601 line three
6602 line four"#
6603 ));
6604
6605 cx.update_editor(|editor, window, cx| {
6606 editor.add_selection_below(&Default::default(), window, cx);
6607 });
6608
6609 // test multiple cursors expand in the same direction
6610 cx.assert_editor_state(indoc!(
6611 r#"line onˇe
6612 liˇne twˇo
6613 liˇne three
6614 line four"#
6615 ));
6616
6617 cx.update_editor(|editor, window, cx| {
6618 editor.add_selection_below(&Default::default(), window, cx);
6619 });
6620
6621 cx.update_editor(|editor, window, cx| {
6622 editor.add_selection_below(&Default::default(), window, cx);
6623 });
6624
6625 // test multiple cursors expand below overflow
6626 cx.assert_editor_state(indoc!(
6627 r#"line onˇe
6628 liˇne twˇo
6629 liˇne thˇree
6630 liˇne foˇur"#
6631 ));
6632
6633 cx.update_editor(|editor, window, cx| {
6634 editor.add_selection_above(&Default::default(), window, cx);
6635 });
6636
6637 // test multiple cursors retrieves back correctly
6638 cx.assert_editor_state(indoc!(
6639 r#"line onˇe
6640 liˇne twˇo
6641 liˇne thˇree
6642 line four"#
6643 ));
6644
6645 cx.update_editor(|editor, window, cx| {
6646 editor.add_selection_above(&Default::default(), window, cx);
6647 });
6648
6649 cx.update_editor(|editor, window, cx| {
6650 editor.add_selection_above(&Default::default(), window, cx);
6651 });
6652
6653 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6654 cx.assert_editor_state(indoc!(
6655 r#"liˇne onˇe
6656 liˇne two
6657 line three
6658 line four"#
6659 ));
6660
6661 cx.update_editor(|editor, window, cx| {
6662 editor.undo_selection(&Default::default(), window, cx);
6663 });
6664
6665 // test undo
6666 cx.assert_editor_state(indoc!(
6667 r#"line onˇe
6668 liˇne twˇo
6669 line three
6670 line four"#
6671 ));
6672
6673 cx.update_editor(|editor, window, cx| {
6674 editor.redo_selection(&Default::default(), window, cx);
6675 });
6676
6677 // test redo
6678 cx.assert_editor_state(indoc!(
6679 r#"liˇne onˇe
6680 liˇne two
6681 line three
6682 line four"#
6683 ));
6684
6685 cx.set_state(indoc!(
6686 r#"abcd
6687 ef«ghˇ»
6688 ijkl
6689 «mˇ»nop"#
6690 ));
6691
6692 cx.update_editor(|editor, window, cx| {
6693 editor.add_selection_above(&Default::default(), window, cx);
6694 });
6695
6696 // test multiple selections expand in the same direction
6697 cx.assert_editor_state(indoc!(
6698 r#"ab«cdˇ»
6699 ef«ghˇ»
6700 «iˇ»jkl
6701 «mˇ»nop"#
6702 ));
6703
6704 cx.update_editor(|editor, window, cx| {
6705 editor.add_selection_above(&Default::default(), window, cx);
6706 });
6707
6708 // test multiple selection upward overflow
6709 cx.assert_editor_state(indoc!(
6710 r#"ab«cdˇ»
6711 «eˇ»f«ghˇ»
6712 «iˇ»jkl
6713 «mˇ»nop"#
6714 ));
6715
6716 cx.update_editor(|editor, window, cx| {
6717 editor.add_selection_below(&Default::default(), window, cx);
6718 });
6719
6720 // test multiple selection retrieves back correctly
6721 cx.assert_editor_state(indoc!(
6722 r#"abcd
6723 ef«ghˇ»
6724 «iˇ»jkl
6725 «mˇ»nop"#
6726 ));
6727
6728 cx.update_editor(|editor, window, cx| {
6729 editor.add_selection_below(&Default::default(), window, cx);
6730 });
6731
6732 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6733 cx.assert_editor_state(indoc!(
6734 r#"abcd
6735 ef«ghˇ»
6736 ij«klˇ»
6737 «mˇ»nop"#
6738 ));
6739
6740 cx.update_editor(|editor, window, cx| {
6741 editor.undo_selection(&Default::default(), window, cx);
6742 });
6743
6744 // test undo
6745 cx.assert_editor_state(indoc!(
6746 r#"abcd
6747 ef«ghˇ»
6748 «iˇ»jkl
6749 «mˇ»nop"#
6750 ));
6751
6752 cx.update_editor(|editor, window, cx| {
6753 editor.redo_selection(&Default::default(), window, cx);
6754 });
6755
6756 // test redo
6757 cx.assert_editor_state(indoc!(
6758 r#"abcd
6759 ef«ghˇ»
6760 ij«klˇ»
6761 «mˇ»nop"#
6762 ));
6763}
6764
6765#[gpui::test]
6766async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6767 init_test(cx, |_| {});
6768 let mut cx = EditorTestContext::new(cx).await;
6769
6770 cx.set_state(indoc!(
6771 r#"line onˇe
6772 liˇne two
6773 line three
6774 line four"#
6775 ));
6776
6777 cx.update_editor(|editor, window, cx| {
6778 editor.add_selection_below(&Default::default(), window, cx);
6779 editor.add_selection_below(&Default::default(), window, cx);
6780 editor.add_selection_below(&Default::default(), window, cx);
6781 });
6782
6783 // initial state with two multi cursor groups
6784 cx.assert_editor_state(indoc!(
6785 r#"line onˇe
6786 liˇne twˇo
6787 liˇne thˇree
6788 liˇne foˇur"#
6789 ));
6790
6791 // add single cursor in middle - simulate opt click
6792 cx.update_editor(|editor, window, cx| {
6793 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6794 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6795 editor.end_selection(window, cx);
6796 });
6797
6798 cx.assert_editor_state(indoc!(
6799 r#"line onˇe
6800 liˇne twˇo
6801 liˇneˇ thˇree
6802 liˇne foˇur"#
6803 ));
6804
6805 cx.update_editor(|editor, window, cx| {
6806 editor.add_selection_above(&Default::default(), window, cx);
6807 });
6808
6809 // test new added selection expands above and existing selection shrinks
6810 cx.assert_editor_state(indoc!(
6811 r#"line onˇe
6812 liˇneˇ twˇo
6813 liˇneˇ thˇree
6814 line four"#
6815 ));
6816
6817 cx.update_editor(|editor, window, cx| {
6818 editor.add_selection_above(&Default::default(), window, cx);
6819 });
6820
6821 // test new added selection expands above and existing selection shrinks
6822 cx.assert_editor_state(indoc!(
6823 r#"lineˇ onˇe
6824 liˇneˇ twˇo
6825 lineˇ three
6826 line four"#
6827 ));
6828
6829 // intial state with two selection groups
6830 cx.set_state(indoc!(
6831 r#"abcd
6832 ef«ghˇ»
6833 ijkl
6834 «mˇ»nop"#
6835 ));
6836
6837 cx.update_editor(|editor, window, cx| {
6838 editor.add_selection_above(&Default::default(), window, cx);
6839 editor.add_selection_above(&Default::default(), window, cx);
6840 });
6841
6842 cx.assert_editor_state(indoc!(
6843 r#"ab«cdˇ»
6844 «eˇ»f«ghˇ»
6845 «iˇ»jkl
6846 «mˇ»nop"#
6847 ));
6848
6849 // add single selection in middle - simulate opt drag
6850 cx.update_editor(|editor, window, cx| {
6851 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
6852 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6853 editor.update_selection(
6854 DisplayPoint::new(DisplayRow(2), 4),
6855 0,
6856 gpui::Point::<f32>::default(),
6857 window,
6858 cx,
6859 );
6860 editor.end_selection(window, cx);
6861 });
6862
6863 cx.assert_editor_state(indoc!(
6864 r#"ab«cdˇ»
6865 «eˇ»f«ghˇ»
6866 «iˇ»jk«lˇ»
6867 «mˇ»nop"#
6868 ));
6869
6870 cx.update_editor(|editor, window, cx| {
6871 editor.add_selection_below(&Default::default(), window, cx);
6872 });
6873
6874 // test new added selection expands below, others shrinks from above
6875 cx.assert_editor_state(indoc!(
6876 r#"abcd
6877 ef«ghˇ»
6878 «iˇ»jk«lˇ»
6879 «mˇ»no«pˇ»"#
6880 ));
6881}
6882
6883#[gpui::test]
6884async fn test_select_next(cx: &mut TestAppContext) {
6885 init_test(cx, |_| {});
6886
6887 let mut cx = EditorTestContext::new(cx).await;
6888 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6889
6890 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6891 .unwrap();
6892 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6893
6894 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6895 .unwrap();
6896 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6897
6898 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6899 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6900
6901 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6902 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6903
6904 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6905 .unwrap();
6906 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6907
6908 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6909 .unwrap();
6910 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6911
6912 // Test selection direction should be preserved
6913 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6914
6915 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6916 .unwrap();
6917 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6918}
6919
6920#[gpui::test]
6921async fn test_select_all_matches(cx: &mut TestAppContext) {
6922 init_test(cx, |_| {});
6923
6924 let mut cx = EditorTestContext::new(cx).await;
6925
6926 // Test caret-only selections
6927 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6928 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6929 .unwrap();
6930 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6931
6932 // Test left-to-right selections
6933 cx.set_state("abc\n«abcˇ»\nabc");
6934 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6935 .unwrap();
6936 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6937
6938 // Test right-to-left selections
6939 cx.set_state("abc\n«ˇabc»\nabc");
6940 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6941 .unwrap();
6942 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6943
6944 // Test selecting whitespace with caret selection
6945 cx.set_state("abc\nˇ abc\nabc");
6946 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6947 .unwrap();
6948 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6949
6950 // Test selecting whitespace with left-to-right selection
6951 cx.set_state("abc\n«ˇ »abc\nabc");
6952 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6953 .unwrap();
6954 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6955
6956 // Test no matches with right-to-left selection
6957 cx.set_state("abc\n« ˇ»abc\nabc");
6958 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6959 .unwrap();
6960 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6961
6962 // Test with a single word and clip_at_line_ends=true (#29823)
6963 cx.set_state("aˇbc");
6964 cx.update_editor(|e, window, cx| {
6965 e.set_clip_at_line_ends(true, cx);
6966 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
6967 e.set_clip_at_line_ends(false, cx);
6968 });
6969 cx.assert_editor_state("«abcˇ»");
6970}
6971
6972#[gpui::test]
6973async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6974 init_test(cx, |_| {});
6975
6976 let mut cx = EditorTestContext::new(cx).await;
6977
6978 let large_body_1 = "\nd".repeat(200);
6979 let large_body_2 = "\ne".repeat(200);
6980
6981 cx.set_state(&format!(
6982 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6983 ));
6984 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6985 let scroll_position = editor.scroll_position(cx);
6986 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6987 scroll_position
6988 });
6989
6990 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6991 .unwrap();
6992 cx.assert_editor_state(&format!(
6993 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6994 ));
6995 let scroll_position_after_selection =
6996 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6997 assert_eq!(
6998 initial_scroll_position, scroll_position_after_selection,
6999 "Scroll position should not change after selecting all matches"
7000 );
7001}
7002
7003#[gpui::test]
7004async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
7005 init_test(cx, |_| {});
7006
7007 let mut cx = EditorLspTestContext::new_rust(
7008 lsp::ServerCapabilities {
7009 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7010 ..Default::default()
7011 },
7012 cx,
7013 )
7014 .await;
7015
7016 cx.set_state(indoc! {"
7017 line 1
7018 line 2
7019 linˇe 3
7020 line 4
7021 line 5
7022 "});
7023
7024 // Make an edit
7025 cx.update_editor(|editor, window, cx| {
7026 editor.handle_input("X", window, cx);
7027 });
7028
7029 // Move cursor to a different position
7030 cx.update_editor(|editor, window, cx| {
7031 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7032 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
7033 });
7034 });
7035
7036 cx.assert_editor_state(indoc! {"
7037 line 1
7038 line 2
7039 linXe 3
7040 line 4
7041 liˇne 5
7042 "});
7043
7044 cx.lsp
7045 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7046 Ok(Some(vec![lsp::TextEdit::new(
7047 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
7048 "PREFIX ".to_string(),
7049 )]))
7050 });
7051
7052 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
7053 .unwrap()
7054 .await
7055 .unwrap();
7056
7057 cx.assert_editor_state(indoc! {"
7058 PREFIX line 1
7059 line 2
7060 linXe 3
7061 line 4
7062 liˇne 5
7063 "});
7064
7065 // Undo formatting
7066 cx.update_editor(|editor, window, cx| {
7067 editor.undo(&Default::default(), window, cx);
7068 });
7069
7070 // Verify cursor moved back to position after edit
7071 cx.assert_editor_state(indoc! {"
7072 line 1
7073 line 2
7074 linXˇe 3
7075 line 4
7076 line 5
7077 "});
7078}
7079
7080#[gpui::test]
7081async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7082 init_test(cx, |_| {});
7083
7084 let mut cx = EditorTestContext::new(cx).await;
7085
7086 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
7087 cx.update_editor(|editor, window, cx| {
7088 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7089 });
7090
7091 cx.set_state(indoc! {"
7092 line 1
7093 line 2
7094 linˇe 3
7095 line 4
7096 line 5
7097 line 6
7098 line 7
7099 line 8
7100 line 9
7101 line 10
7102 "});
7103
7104 let snapshot = cx.buffer_snapshot();
7105 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7106
7107 cx.update(|_, cx| {
7108 provider.update(cx, |provider, _| {
7109 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
7110 id: None,
7111 edits: vec![(edit_position..edit_position, "X".into())],
7112 edit_preview: None,
7113 }))
7114 })
7115 });
7116
7117 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
7118 cx.update_editor(|editor, window, cx| {
7119 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7120 });
7121
7122 cx.assert_editor_state(indoc! {"
7123 line 1
7124 line 2
7125 lineXˇ 3
7126 line 4
7127 line 5
7128 line 6
7129 line 7
7130 line 8
7131 line 9
7132 line 10
7133 "});
7134
7135 cx.update_editor(|editor, window, cx| {
7136 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7137 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7138 });
7139 });
7140
7141 cx.assert_editor_state(indoc! {"
7142 line 1
7143 line 2
7144 lineX 3
7145 line 4
7146 line 5
7147 line 6
7148 line 7
7149 line 8
7150 line 9
7151 liˇne 10
7152 "});
7153
7154 cx.update_editor(|editor, window, cx| {
7155 editor.undo(&Default::default(), window, cx);
7156 });
7157
7158 cx.assert_editor_state(indoc! {"
7159 line 1
7160 line 2
7161 lineˇ 3
7162 line 4
7163 line 5
7164 line 6
7165 line 7
7166 line 8
7167 line 9
7168 line 10
7169 "});
7170}
7171
7172#[gpui::test]
7173async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7174 init_test(cx, |_| {});
7175
7176 let mut cx = EditorTestContext::new(cx).await;
7177 cx.set_state(
7178 r#"let foo = 2;
7179lˇet foo = 2;
7180let fooˇ = 2;
7181let foo = 2;
7182let foo = ˇ2;"#,
7183 );
7184
7185 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7186 .unwrap();
7187 cx.assert_editor_state(
7188 r#"let foo = 2;
7189«letˇ» foo = 2;
7190let «fooˇ» = 2;
7191let foo = 2;
7192let foo = «2ˇ»;"#,
7193 );
7194
7195 // noop for multiple selections with different contents
7196 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7197 .unwrap();
7198 cx.assert_editor_state(
7199 r#"let foo = 2;
7200«letˇ» foo = 2;
7201let «fooˇ» = 2;
7202let foo = 2;
7203let foo = «2ˇ»;"#,
7204 );
7205
7206 // Test last selection direction should be preserved
7207 cx.set_state(
7208 r#"let foo = 2;
7209let foo = 2;
7210let «fooˇ» = 2;
7211let «ˇfoo» = 2;
7212let foo = 2;"#,
7213 );
7214
7215 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7216 .unwrap();
7217 cx.assert_editor_state(
7218 r#"let foo = 2;
7219let foo = 2;
7220let «fooˇ» = 2;
7221let «ˇfoo» = 2;
7222let «ˇfoo» = 2;"#,
7223 );
7224}
7225
7226#[gpui::test]
7227async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7228 init_test(cx, |_| {});
7229
7230 let mut cx =
7231 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7232
7233 cx.assert_editor_state(indoc! {"
7234 ˇbbb
7235 ccc
7236
7237 bbb
7238 ccc
7239 "});
7240 cx.dispatch_action(SelectPrevious::default());
7241 cx.assert_editor_state(indoc! {"
7242 «bbbˇ»
7243 ccc
7244
7245 bbb
7246 ccc
7247 "});
7248 cx.dispatch_action(SelectPrevious::default());
7249 cx.assert_editor_state(indoc! {"
7250 «bbbˇ»
7251 ccc
7252
7253 «bbbˇ»
7254 ccc
7255 "});
7256}
7257
7258#[gpui::test]
7259async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7260 init_test(cx, |_| {});
7261
7262 let mut cx = EditorTestContext::new(cx).await;
7263 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7264
7265 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7266 .unwrap();
7267 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7268
7269 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7270 .unwrap();
7271 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7272
7273 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7274 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7275
7276 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7277 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7278
7279 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7280 .unwrap();
7281 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7282
7283 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7284 .unwrap();
7285 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7286}
7287
7288#[gpui::test]
7289async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7290 init_test(cx, |_| {});
7291
7292 let mut cx = EditorTestContext::new(cx).await;
7293 cx.set_state("aˇ");
7294
7295 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7296 .unwrap();
7297 cx.assert_editor_state("«aˇ»");
7298 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7299 .unwrap();
7300 cx.assert_editor_state("«aˇ»");
7301}
7302
7303#[gpui::test]
7304async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7305 init_test(cx, |_| {});
7306
7307 let mut cx = EditorTestContext::new(cx).await;
7308 cx.set_state(
7309 r#"let foo = 2;
7310lˇet foo = 2;
7311let fooˇ = 2;
7312let foo = 2;
7313let foo = ˇ2;"#,
7314 );
7315
7316 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7317 .unwrap();
7318 cx.assert_editor_state(
7319 r#"let foo = 2;
7320«letˇ» foo = 2;
7321let «fooˇ» = 2;
7322let foo = 2;
7323let foo = «2ˇ»;"#,
7324 );
7325
7326 // noop for multiple selections with different contents
7327 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7328 .unwrap();
7329 cx.assert_editor_state(
7330 r#"let foo = 2;
7331«letˇ» foo = 2;
7332let «fooˇ» = 2;
7333let foo = 2;
7334let foo = «2ˇ»;"#,
7335 );
7336}
7337
7338#[gpui::test]
7339async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7340 init_test(cx, |_| {});
7341
7342 let mut cx = EditorTestContext::new(cx).await;
7343 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7344
7345 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7346 .unwrap();
7347 // selection direction is preserved
7348 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7349
7350 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7351 .unwrap();
7352 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7353
7354 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7355 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7356
7357 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7358 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7359
7360 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7361 .unwrap();
7362 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7363
7364 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7365 .unwrap();
7366 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7367}
7368
7369#[gpui::test]
7370async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7371 init_test(cx, |_| {});
7372
7373 let language = Arc::new(Language::new(
7374 LanguageConfig::default(),
7375 Some(tree_sitter_rust::LANGUAGE.into()),
7376 ));
7377
7378 let text = r#"
7379 use mod1::mod2::{mod3, mod4};
7380
7381 fn fn_1(param1: bool, param2: &str) {
7382 let var1 = "text";
7383 }
7384 "#
7385 .unindent();
7386
7387 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7388 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7389 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7390
7391 editor
7392 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7393 .await;
7394
7395 editor.update_in(cx, |editor, window, cx| {
7396 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7397 s.select_display_ranges([
7398 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7399 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7400 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7401 ]);
7402 });
7403 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7404 });
7405 editor.update(cx, |editor, cx| {
7406 assert_text_with_selections(
7407 editor,
7408 indoc! {r#"
7409 use mod1::mod2::{mod3, «mod4ˇ»};
7410
7411 fn fn_1«ˇ(param1: bool, param2: &str)» {
7412 let var1 = "«ˇtext»";
7413 }
7414 "#},
7415 cx,
7416 );
7417 });
7418
7419 editor.update_in(cx, |editor, window, cx| {
7420 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7421 });
7422 editor.update(cx, |editor, cx| {
7423 assert_text_with_selections(
7424 editor,
7425 indoc! {r#"
7426 use mod1::mod2::«{mod3, mod4}ˇ»;
7427
7428 «ˇfn fn_1(param1: bool, param2: &str) {
7429 let var1 = "text";
7430 }»
7431 "#},
7432 cx,
7433 );
7434 });
7435
7436 editor.update_in(cx, |editor, window, cx| {
7437 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7438 });
7439 assert_eq!(
7440 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7441 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7442 );
7443
7444 // Trying to expand the selected syntax node one more time has no effect.
7445 editor.update_in(cx, |editor, window, cx| {
7446 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7447 });
7448 assert_eq!(
7449 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7450 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7451 );
7452
7453 editor.update_in(cx, |editor, window, cx| {
7454 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7455 });
7456 editor.update(cx, |editor, cx| {
7457 assert_text_with_selections(
7458 editor,
7459 indoc! {r#"
7460 use mod1::mod2::«{mod3, mod4}ˇ»;
7461
7462 «ˇfn fn_1(param1: bool, param2: &str) {
7463 let var1 = "text";
7464 }»
7465 "#},
7466 cx,
7467 );
7468 });
7469
7470 editor.update_in(cx, |editor, window, cx| {
7471 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7472 });
7473 editor.update(cx, |editor, cx| {
7474 assert_text_with_selections(
7475 editor,
7476 indoc! {r#"
7477 use mod1::mod2::{mod3, «mod4ˇ»};
7478
7479 fn fn_1«ˇ(param1: bool, param2: &str)» {
7480 let var1 = "«ˇtext»";
7481 }
7482 "#},
7483 cx,
7484 );
7485 });
7486
7487 editor.update_in(cx, |editor, window, cx| {
7488 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7489 });
7490 editor.update(cx, |editor, cx| {
7491 assert_text_with_selections(
7492 editor,
7493 indoc! {r#"
7494 use mod1::mod2::{mod3, mo«ˇ»d4};
7495
7496 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7497 let var1 = "te«ˇ»xt";
7498 }
7499 "#},
7500 cx,
7501 );
7502 });
7503
7504 // Trying to shrink the selected syntax node one more time has no effect.
7505 editor.update_in(cx, |editor, window, cx| {
7506 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7507 });
7508 editor.update_in(cx, |editor, _, cx| {
7509 assert_text_with_selections(
7510 editor,
7511 indoc! {r#"
7512 use mod1::mod2::{mod3, mo«ˇ»d4};
7513
7514 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7515 let var1 = "te«ˇ»xt";
7516 }
7517 "#},
7518 cx,
7519 );
7520 });
7521
7522 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7523 // a fold.
7524 editor.update_in(cx, |editor, window, cx| {
7525 editor.fold_creases(
7526 vec![
7527 Crease::simple(
7528 Point::new(0, 21)..Point::new(0, 24),
7529 FoldPlaceholder::test(),
7530 ),
7531 Crease::simple(
7532 Point::new(3, 20)..Point::new(3, 22),
7533 FoldPlaceholder::test(),
7534 ),
7535 ],
7536 true,
7537 window,
7538 cx,
7539 );
7540 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7541 });
7542 editor.update(cx, |editor, cx| {
7543 assert_text_with_selections(
7544 editor,
7545 indoc! {r#"
7546 use mod1::mod2::«{mod3, mod4}ˇ»;
7547
7548 fn fn_1«ˇ(param1: bool, param2: &str)» {
7549 let var1 = "«ˇtext»";
7550 }
7551 "#},
7552 cx,
7553 );
7554 });
7555}
7556
7557#[gpui::test]
7558async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7559 init_test(cx, |_| {});
7560
7561 let language = Arc::new(Language::new(
7562 LanguageConfig::default(),
7563 Some(tree_sitter_rust::LANGUAGE.into()),
7564 ));
7565
7566 let text = "let a = 2;";
7567
7568 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7569 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7570 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7571
7572 editor
7573 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7574 .await;
7575
7576 // Test case 1: Cursor at end of word
7577 editor.update_in(cx, |editor, window, cx| {
7578 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7579 s.select_display_ranges([
7580 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7581 ]);
7582 });
7583 });
7584 editor.update(cx, |editor, cx| {
7585 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7586 });
7587 editor.update_in(cx, |editor, window, cx| {
7588 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7589 });
7590 editor.update(cx, |editor, cx| {
7591 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7592 });
7593 editor.update_in(cx, |editor, window, cx| {
7594 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7595 });
7596 editor.update(cx, |editor, cx| {
7597 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7598 });
7599
7600 // Test case 2: Cursor at end of statement
7601 editor.update_in(cx, |editor, window, cx| {
7602 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7603 s.select_display_ranges([
7604 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7605 ]);
7606 });
7607 });
7608 editor.update(cx, |editor, cx| {
7609 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7610 });
7611 editor.update_in(cx, |editor, window, cx| {
7612 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7613 });
7614 editor.update(cx, |editor, cx| {
7615 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7616 });
7617}
7618
7619#[gpui::test]
7620async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7621 init_test(cx, |_| {});
7622
7623 let language = Arc::new(Language::new(
7624 LanguageConfig::default(),
7625 Some(tree_sitter_rust::LANGUAGE.into()),
7626 ));
7627
7628 let text = r#"
7629 use mod1::mod2::{mod3, mod4};
7630
7631 fn fn_1(param1: bool, param2: &str) {
7632 let var1 = "hello world";
7633 }
7634 "#
7635 .unindent();
7636
7637 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7638 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7639 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7640
7641 editor
7642 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7643 .await;
7644
7645 // Test 1: Cursor on a letter of a string word
7646 editor.update_in(cx, |editor, window, cx| {
7647 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7648 s.select_display_ranges([
7649 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7650 ]);
7651 });
7652 });
7653 editor.update_in(cx, |editor, window, cx| {
7654 assert_text_with_selections(
7655 editor,
7656 indoc! {r#"
7657 use mod1::mod2::{mod3, mod4};
7658
7659 fn fn_1(param1: bool, param2: &str) {
7660 let var1 = "hˇello world";
7661 }
7662 "#},
7663 cx,
7664 );
7665 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7666 assert_text_with_selections(
7667 editor,
7668 indoc! {r#"
7669 use mod1::mod2::{mod3, mod4};
7670
7671 fn fn_1(param1: bool, param2: &str) {
7672 let var1 = "«ˇhello» world";
7673 }
7674 "#},
7675 cx,
7676 );
7677 });
7678
7679 // Test 2: Partial selection within a word
7680 editor.update_in(cx, |editor, window, cx| {
7681 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7682 s.select_display_ranges([
7683 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7684 ]);
7685 });
7686 });
7687 editor.update_in(cx, |editor, window, cx| {
7688 assert_text_with_selections(
7689 editor,
7690 indoc! {r#"
7691 use mod1::mod2::{mod3, mod4};
7692
7693 fn fn_1(param1: bool, param2: &str) {
7694 let var1 = "h«elˇ»lo world";
7695 }
7696 "#},
7697 cx,
7698 );
7699 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7700 assert_text_with_selections(
7701 editor,
7702 indoc! {r#"
7703 use mod1::mod2::{mod3, mod4};
7704
7705 fn fn_1(param1: bool, param2: &str) {
7706 let var1 = "«ˇhello» world";
7707 }
7708 "#},
7709 cx,
7710 );
7711 });
7712
7713 // Test 3: Complete word already selected
7714 editor.update_in(cx, |editor, window, cx| {
7715 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7716 s.select_display_ranges([
7717 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7718 ]);
7719 });
7720 });
7721 editor.update_in(cx, |editor, window, cx| {
7722 assert_text_with_selections(
7723 editor,
7724 indoc! {r#"
7725 use mod1::mod2::{mod3, mod4};
7726
7727 fn fn_1(param1: bool, param2: &str) {
7728 let var1 = "«helloˇ» world";
7729 }
7730 "#},
7731 cx,
7732 );
7733 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7734 assert_text_with_selections(
7735 editor,
7736 indoc! {r#"
7737 use mod1::mod2::{mod3, mod4};
7738
7739 fn fn_1(param1: bool, param2: &str) {
7740 let var1 = "«hello worldˇ»";
7741 }
7742 "#},
7743 cx,
7744 );
7745 });
7746
7747 // Test 4: Selection spanning across words
7748 editor.update_in(cx, |editor, window, cx| {
7749 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7750 s.select_display_ranges([
7751 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7752 ]);
7753 });
7754 });
7755 editor.update_in(cx, |editor, window, cx| {
7756 assert_text_with_selections(
7757 editor,
7758 indoc! {r#"
7759 use mod1::mod2::{mod3, mod4};
7760
7761 fn fn_1(param1: bool, param2: &str) {
7762 let var1 = "hel«lo woˇ»rld";
7763 }
7764 "#},
7765 cx,
7766 );
7767 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7768 assert_text_with_selections(
7769 editor,
7770 indoc! {r#"
7771 use mod1::mod2::{mod3, mod4};
7772
7773 fn fn_1(param1: bool, param2: &str) {
7774 let var1 = "«ˇhello world»";
7775 }
7776 "#},
7777 cx,
7778 );
7779 });
7780
7781 // Test 5: Expansion beyond string
7782 editor.update_in(cx, |editor, window, cx| {
7783 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7784 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7785 assert_text_with_selections(
7786 editor,
7787 indoc! {r#"
7788 use mod1::mod2::{mod3, mod4};
7789
7790 fn fn_1(param1: bool, param2: &str) {
7791 «ˇlet var1 = "hello world";»
7792 }
7793 "#},
7794 cx,
7795 );
7796 });
7797}
7798
7799#[gpui::test]
7800async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7801 init_test(cx, |_| {});
7802
7803 let base_text = r#"
7804 impl A {
7805 // this is an uncommitted comment
7806
7807 fn b() {
7808 c();
7809 }
7810
7811 // this is another uncommitted comment
7812
7813 fn d() {
7814 // e
7815 // f
7816 }
7817 }
7818
7819 fn g() {
7820 // h
7821 }
7822 "#
7823 .unindent();
7824
7825 let text = r#"
7826 ˇimpl A {
7827
7828 fn b() {
7829 c();
7830 }
7831
7832 fn d() {
7833 // e
7834 // f
7835 }
7836 }
7837
7838 fn g() {
7839 // h
7840 }
7841 "#
7842 .unindent();
7843
7844 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7845 cx.set_state(&text);
7846 cx.set_head_text(&base_text);
7847 cx.update_editor(|editor, window, cx| {
7848 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7849 });
7850
7851 cx.assert_state_with_diff(
7852 "
7853 ˇimpl A {
7854 - // this is an uncommitted comment
7855
7856 fn b() {
7857 c();
7858 }
7859
7860 - // this is another uncommitted comment
7861 -
7862 fn d() {
7863 // e
7864 // f
7865 }
7866 }
7867
7868 fn g() {
7869 // h
7870 }
7871 "
7872 .unindent(),
7873 );
7874
7875 let expected_display_text = "
7876 impl A {
7877 // this is an uncommitted comment
7878
7879 fn b() {
7880 ⋯
7881 }
7882
7883 // this is another uncommitted comment
7884
7885 fn d() {
7886 ⋯
7887 }
7888 }
7889
7890 fn g() {
7891 ⋯
7892 }
7893 "
7894 .unindent();
7895
7896 cx.update_editor(|editor, window, cx| {
7897 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7898 assert_eq!(editor.display_text(cx), expected_display_text);
7899 });
7900}
7901
7902#[gpui::test]
7903async fn test_autoindent(cx: &mut TestAppContext) {
7904 init_test(cx, |_| {});
7905
7906 let language = Arc::new(
7907 Language::new(
7908 LanguageConfig {
7909 brackets: BracketPairConfig {
7910 pairs: vec![
7911 BracketPair {
7912 start: "{".to_string(),
7913 end: "}".to_string(),
7914 close: false,
7915 surround: false,
7916 newline: true,
7917 },
7918 BracketPair {
7919 start: "(".to_string(),
7920 end: ")".to_string(),
7921 close: false,
7922 surround: false,
7923 newline: true,
7924 },
7925 ],
7926 ..Default::default()
7927 },
7928 ..Default::default()
7929 },
7930 Some(tree_sitter_rust::LANGUAGE.into()),
7931 )
7932 .with_indents_query(
7933 r#"
7934 (_ "(" ")" @end) @indent
7935 (_ "{" "}" @end) @indent
7936 "#,
7937 )
7938 .unwrap(),
7939 );
7940
7941 let text = "fn a() {}";
7942
7943 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7944 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7945 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7946 editor
7947 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7948 .await;
7949
7950 editor.update_in(cx, |editor, window, cx| {
7951 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7952 s.select_ranges([5..5, 8..8, 9..9])
7953 });
7954 editor.newline(&Newline, window, cx);
7955 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7956 assert_eq!(
7957 editor.selections.ranges(cx),
7958 &[
7959 Point::new(1, 4)..Point::new(1, 4),
7960 Point::new(3, 4)..Point::new(3, 4),
7961 Point::new(5, 0)..Point::new(5, 0)
7962 ]
7963 );
7964 });
7965}
7966
7967#[gpui::test]
7968async fn test_autoindent_selections(cx: &mut TestAppContext) {
7969 init_test(cx, |_| {});
7970
7971 {
7972 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7973 cx.set_state(indoc! {"
7974 impl A {
7975
7976 fn b() {}
7977
7978 «fn c() {
7979
7980 }ˇ»
7981 }
7982 "});
7983
7984 cx.update_editor(|editor, window, cx| {
7985 editor.autoindent(&Default::default(), window, cx);
7986 });
7987
7988 cx.assert_editor_state(indoc! {"
7989 impl A {
7990
7991 fn b() {}
7992
7993 «fn c() {
7994
7995 }ˇ»
7996 }
7997 "});
7998 }
7999
8000 {
8001 let mut cx = EditorTestContext::new_multibuffer(
8002 cx,
8003 [indoc! { "
8004 impl A {
8005 «
8006 // a
8007 fn b(){}
8008 »
8009 «
8010 }
8011 fn c(){}
8012 »
8013 "}],
8014 );
8015
8016 let buffer = cx.update_editor(|editor, _, cx| {
8017 let buffer = editor.buffer().update(cx, |buffer, _| {
8018 buffer.all_buffers().iter().next().unwrap().clone()
8019 });
8020 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8021 buffer
8022 });
8023
8024 cx.run_until_parked();
8025 cx.update_editor(|editor, window, cx| {
8026 editor.select_all(&Default::default(), window, cx);
8027 editor.autoindent(&Default::default(), window, cx)
8028 });
8029 cx.run_until_parked();
8030
8031 cx.update(|_, cx| {
8032 assert_eq!(
8033 buffer.read(cx).text(),
8034 indoc! { "
8035 impl A {
8036
8037 // a
8038 fn b(){}
8039
8040
8041 }
8042 fn c(){}
8043
8044 " }
8045 )
8046 });
8047 }
8048}
8049
8050#[gpui::test]
8051async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8052 init_test(cx, |_| {});
8053
8054 let mut cx = EditorTestContext::new(cx).await;
8055
8056 let language = Arc::new(Language::new(
8057 LanguageConfig {
8058 brackets: BracketPairConfig {
8059 pairs: vec![
8060 BracketPair {
8061 start: "{".to_string(),
8062 end: "}".to_string(),
8063 close: true,
8064 surround: true,
8065 newline: true,
8066 },
8067 BracketPair {
8068 start: "(".to_string(),
8069 end: ")".to_string(),
8070 close: true,
8071 surround: true,
8072 newline: true,
8073 },
8074 BracketPair {
8075 start: "/*".to_string(),
8076 end: " */".to_string(),
8077 close: true,
8078 surround: true,
8079 newline: true,
8080 },
8081 BracketPair {
8082 start: "[".to_string(),
8083 end: "]".to_string(),
8084 close: false,
8085 surround: false,
8086 newline: true,
8087 },
8088 BracketPair {
8089 start: "\"".to_string(),
8090 end: "\"".to_string(),
8091 close: true,
8092 surround: true,
8093 newline: false,
8094 },
8095 BracketPair {
8096 start: "<".to_string(),
8097 end: ">".to_string(),
8098 close: false,
8099 surround: true,
8100 newline: true,
8101 },
8102 ],
8103 ..Default::default()
8104 },
8105 autoclose_before: "})]".to_string(),
8106 ..Default::default()
8107 },
8108 Some(tree_sitter_rust::LANGUAGE.into()),
8109 ));
8110
8111 cx.language_registry().add(language.clone());
8112 cx.update_buffer(|buffer, cx| {
8113 buffer.set_language(Some(language), cx);
8114 });
8115
8116 cx.set_state(
8117 &r#"
8118 🏀ˇ
8119 εˇ
8120 ❤️ˇ
8121 "#
8122 .unindent(),
8123 );
8124
8125 // autoclose multiple nested brackets at multiple cursors
8126 cx.update_editor(|editor, window, cx| {
8127 editor.handle_input("{", window, cx);
8128 editor.handle_input("{", window, cx);
8129 editor.handle_input("{", window, cx);
8130 });
8131 cx.assert_editor_state(
8132 &"
8133 🏀{{{ˇ}}}
8134 ε{{{ˇ}}}
8135 ❤️{{{ˇ}}}
8136 "
8137 .unindent(),
8138 );
8139
8140 // insert a different closing bracket
8141 cx.update_editor(|editor, window, cx| {
8142 editor.handle_input(")", window, cx);
8143 });
8144 cx.assert_editor_state(
8145 &"
8146 🏀{{{)ˇ}}}
8147 ε{{{)ˇ}}}
8148 ❤️{{{)ˇ}}}
8149 "
8150 .unindent(),
8151 );
8152
8153 // skip over the auto-closed brackets when typing a closing bracket
8154 cx.update_editor(|editor, window, cx| {
8155 editor.move_right(&MoveRight, window, cx);
8156 editor.handle_input("}", window, cx);
8157 editor.handle_input("}", window, cx);
8158 editor.handle_input("}", window, cx);
8159 });
8160 cx.assert_editor_state(
8161 &"
8162 🏀{{{)}}}}ˇ
8163 ε{{{)}}}}ˇ
8164 ❤️{{{)}}}}ˇ
8165 "
8166 .unindent(),
8167 );
8168
8169 // autoclose multi-character pairs
8170 cx.set_state(
8171 &"
8172 ˇ
8173 ˇ
8174 "
8175 .unindent(),
8176 );
8177 cx.update_editor(|editor, window, cx| {
8178 editor.handle_input("/", window, cx);
8179 editor.handle_input("*", window, cx);
8180 });
8181 cx.assert_editor_state(
8182 &"
8183 /*ˇ */
8184 /*ˇ */
8185 "
8186 .unindent(),
8187 );
8188
8189 // one cursor autocloses a multi-character pair, one cursor
8190 // does not autoclose.
8191 cx.set_state(
8192 &"
8193 /ˇ
8194 ˇ
8195 "
8196 .unindent(),
8197 );
8198 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8199 cx.assert_editor_state(
8200 &"
8201 /*ˇ */
8202 *ˇ
8203 "
8204 .unindent(),
8205 );
8206
8207 // Don't autoclose if the next character isn't whitespace and isn't
8208 // listed in the language's "autoclose_before" section.
8209 cx.set_state("ˇa b");
8210 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8211 cx.assert_editor_state("{ˇa b");
8212
8213 // Don't autoclose if `close` is false for the bracket pair
8214 cx.set_state("ˇ");
8215 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8216 cx.assert_editor_state("[ˇ");
8217
8218 // Surround with brackets if text is selected
8219 cx.set_state("«aˇ» b");
8220 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8221 cx.assert_editor_state("{«aˇ»} b");
8222
8223 // Autoclose when not immediately after a word character
8224 cx.set_state("a ˇ");
8225 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8226 cx.assert_editor_state("a \"ˇ\"");
8227
8228 // Autoclose pair where the start and end characters are the same
8229 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8230 cx.assert_editor_state("a \"\"ˇ");
8231
8232 // Don't autoclose when immediately after a word character
8233 cx.set_state("aˇ");
8234 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8235 cx.assert_editor_state("a\"ˇ");
8236
8237 // Do autoclose when after a non-word character
8238 cx.set_state("{ˇ");
8239 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8240 cx.assert_editor_state("{\"ˇ\"");
8241
8242 // Non identical pairs autoclose regardless of preceding character
8243 cx.set_state("aˇ");
8244 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8245 cx.assert_editor_state("a{ˇ}");
8246
8247 // Don't autoclose pair if autoclose is disabled
8248 cx.set_state("ˇ");
8249 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8250 cx.assert_editor_state("<ˇ");
8251
8252 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8253 cx.set_state("«aˇ» b");
8254 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8255 cx.assert_editor_state("<«aˇ»> b");
8256}
8257
8258#[gpui::test]
8259async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8260 init_test(cx, |settings| {
8261 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8262 });
8263
8264 let mut cx = EditorTestContext::new(cx).await;
8265
8266 let language = Arc::new(Language::new(
8267 LanguageConfig {
8268 brackets: BracketPairConfig {
8269 pairs: vec![
8270 BracketPair {
8271 start: "{".to_string(),
8272 end: "}".to_string(),
8273 close: true,
8274 surround: true,
8275 newline: true,
8276 },
8277 BracketPair {
8278 start: "(".to_string(),
8279 end: ")".to_string(),
8280 close: true,
8281 surround: true,
8282 newline: true,
8283 },
8284 BracketPair {
8285 start: "[".to_string(),
8286 end: "]".to_string(),
8287 close: false,
8288 surround: false,
8289 newline: true,
8290 },
8291 ],
8292 ..Default::default()
8293 },
8294 autoclose_before: "})]".to_string(),
8295 ..Default::default()
8296 },
8297 Some(tree_sitter_rust::LANGUAGE.into()),
8298 ));
8299
8300 cx.language_registry().add(language.clone());
8301 cx.update_buffer(|buffer, cx| {
8302 buffer.set_language(Some(language), cx);
8303 });
8304
8305 cx.set_state(
8306 &"
8307 ˇ
8308 ˇ
8309 ˇ
8310 "
8311 .unindent(),
8312 );
8313
8314 // ensure only matching closing brackets are skipped over
8315 cx.update_editor(|editor, window, cx| {
8316 editor.handle_input("}", window, cx);
8317 editor.move_left(&MoveLeft, window, cx);
8318 editor.handle_input(")", window, cx);
8319 editor.move_left(&MoveLeft, window, cx);
8320 });
8321 cx.assert_editor_state(
8322 &"
8323 ˇ)}
8324 ˇ)}
8325 ˇ)}
8326 "
8327 .unindent(),
8328 );
8329
8330 // skip-over closing brackets at multiple cursors
8331 cx.update_editor(|editor, window, cx| {
8332 editor.handle_input(")", window, cx);
8333 editor.handle_input("}", window, cx);
8334 });
8335 cx.assert_editor_state(
8336 &"
8337 )}ˇ
8338 )}ˇ
8339 )}ˇ
8340 "
8341 .unindent(),
8342 );
8343
8344 // ignore non-close brackets
8345 cx.update_editor(|editor, window, cx| {
8346 editor.handle_input("]", window, cx);
8347 editor.move_left(&MoveLeft, window, cx);
8348 editor.handle_input("]", window, cx);
8349 });
8350 cx.assert_editor_state(
8351 &"
8352 )}]ˇ]
8353 )}]ˇ]
8354 )}]ˇ]
8355 "
8356 .unindent(),
8357 );
8358}
8359
8360#[gpui::test]
8361async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8362 init_test(cx, |_| {});
8363
8364 let mut cx = EditorTestContext::new(cx).await;
8365
8366 let html_language = Arc::new(
8367 Language::new(
8368 LanguageConfig {
8369 name: "HTML".into(),
8370 brackets: BracketPairConfig {
8371 pairs: vec![
8372 BracketPair {
8373 start: "<".into(),
8374 end: ">".into(),
8375 close: true,
8376 ..Default::default()
8377 },
8378 BracketPair {
8379 start: "{".into(),
8380 end: "}".into(),
8381 close: true,
8382 ..Default::default()
8383 },
8384 BracketPair {
8385 start: "(".into(),
8386 end: ")".into(),
8387 close: true,
8388 ..Default::default()
8389 },
8390 ],
8391 ..Default::default()
8392 },
8393 autoclose_before: "})]>".into(),
8394 ..Default::default()
8395 },
8396 Some(tree_sitter_html::LANGUAGE.into()),
8397 )
8398 .with_injection_query(
8399 r#"
8400 (script_element
8401 (raw_text) @injection.content
8402 (#set! injection.language "javascript"))
8403 "#,
8404 )
8405 .unwrap(),
8406 );
8407
8408 let javascript_language = Arc::new(Language::new(
8409 LanguageConfig {
8410 name: "JavaScript".into(),
8411 brackets: BracketPairConfig {
8412 pairs: vec![
8413 BracketPair {
8414 start: "/*".into(),
8415 end: " */".into(),
8416 close: true,
8417 ..Default::default()
8418 },
8419 BracketPair {
8420 start: "{".into(),
8421 end: "}".into(),
8422 close: true,
8423 ..Default::default()
8424 },
8425 BracketPair {
8426 start: "(".into(),
8427 end: ")".into(),
8428 close: true,
8429 ..Default::default()
8430 },
8431 ],
8432 ..Default::default()
8433 },
8434 autoclose_before: "})]>".into(),
8435 ..Default::default()
8436 },
8437 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8438 ));
8439
8440 cx.language_registry().add(html_language.clone());
8441 cx.language_registry().add(javascript_language.clone());
8442
8443 cx.update_buffer(|buffer, cx| {
8444 buffer.set_language(Some(html_language), cx);
8445 });
8446
8447 cx.set_state(
8448 &r#"
8449 <body>ˇ
8450 <script>
8451 var x = 1;ˇ
8452 </script>
8453 </body>ˇ
8454 "#
8455 .unindent(),
8456 );
8457
8458 // Precondition: different languages are active at different locations.
8459 cx.update_editor(|editor, window, cx| {
8460 let snapshot = editor.snapshot(window, cx);
8461 let cursors = editor.selections.ranges::<usize>(cx);
8462 let languages = cursors
8463 .iter()
8464 .map(|c| snapshot.language_at(c.start).unwrap().name())
8465 .collect::<Vec<_>>();
8466 assert_eq!(
8467 languages,
8468 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8469 );
8470 });
8471
8472 // Angle brackets autoclose in HTML, but not JavaScript.
8473 cx.update_editor(|editor, window, cx| {
8474 editor.handle_input("<", window, cx);
8475 editor.handle_input("a", window, cx);
8476 });
8477 cx.assert_editor_state(
8478 &r#"
8479 <body><aˇ>
8480 <script>
8481 var x = 1;<aˇ
8482 </script>
8483 </body><aˇ>
8484 "#
8485 .unindent(),
8486 );
8487
8488 // Curly braces and parens autoclose in both HTML and JavaScript.
8489 cx.update_editor(|editor, window, cx| {
8490 editor.handle_input(" b=", window, cx);
8491 editor.handle_input("{", window, cx);
8492 editor.handle_input("c", window, cx);
8493 editor.handle_input("(", window, cx);
8494 });
8495 cx.assert_editor_state(
8496 &r#"
8497 <body><a b={c(ˇ)}>
8498 <script>
8499 var x = 1;<a b={c(ˇ)}
8500 </script>
8501 </body><a b={c(ˇ)}>
8502 "#
8503 .unindent(),
8504 );
8505
8506 // Brackets that were already autoclosed are skipped.
8507 cx.update_editor(|editor, window, cx| {
8508 editor.handle_input(")", window, cx);
8509 editor.handle_input("d", window, cx);
8510 editor.handle_input("}", window, cx);
8511 });
8512 cx.assert_editor_state(
8513 &r#"
8514 <body><a b={c()d}ˇ>
8515 <script>
8516 var x = 1;<a b={c()d}ˇ
8517 </script>
8518 </body><a b={c()d}ˇ>
8519 "#
8520 .unindent(),
8521 );
8522 cx.update_editor(|editor, window, cx| {
8523 editor.handle_input(">", window, cx);
8524 });
8525 cx.assert_editor_state(
8526 &r#"
8527 <body><a b={c()d}>ˇ
8528 <script>
8529 var x = 1;<a b={c()d}>ˇ
8530 </script>
8531 </body><a b={c()d}>ˇ
8532 "#
8533 .unindent(),
8534 );
8535
8536 // Reset
8537 cx.set_state(
8538 &r#"
8539 <body>ˇ
8540 <script>
8541 var x = 1;ˇ
8542 </script>
8543 </body>ˇ
8544 "#
8545 .unindent(),
8546 );
8547
8548 cx.update_editor(|editor, window, cx| {
8549 editor.handle_input("<", window, cx);
8550 });
8551 cx.assert_editor_state(
8552 &r#"
8553 <body><ˇ>
8554 <script>
8555 var x = 1;<ˇ
8556 </script>
8557 </body><ˇ>
8558 "#
8559 .unindent(),
8560 );
8561
8562 // When backspacing, the closing angle brackets are removed.
8563 cx.update_editor(|editor, window, cx| {
8564 editor.backspace(&Backspace, window, cx);
8565 });
8566 cx.assert_editor_state(
8567 &r#"
8568 <body>ˇ
8569 <script>
8570 var x = 1;ˇ
8571 </script>
8572 </body>ˇ
8573 "#
8574 .unindent(),
8575 );
8576
8577 // Block comments autoclose in JavaScript, but not HTML.
8578 cx.update_editor(|editor, window, cx| {
8579 editor.handle_input("/", window, cx);
8580 editor.handle_input("*", window, cx);
8581 });
8582 cx.assert_editor_state(
8583 &r#"
8584 <body>/*ˇ
8585 <script>
8586 var x = 1;/*ˇ */
8587 </script>
8588 </body>/*ˇ
8589 "#
8590 .unindent(),
8591 );
8592}
8593
8594#[gpui::test]
8595async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8596 init_test(cx, |_| {});
8597
8598 let mut cx = EditorTestContext::new(cx).await;
8599
8600 let rust_language = Arc::new(
8601 Language::new(
8602 LanguageConfig {
8603 name: "Rust".into(),
8604 brackets: serde_json::from_value(json!([
8605 { "start": "{", "end": "}", "close": true, "newline": true },
8606 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8607 ]))
8608 .unwrap(),
8609 autoclose_before: "})]>".into(),
8610 ..Default::default()
8611 },
8612 Some(tree_sitter_rust::LANGUAGE.into()),
8613 )
8614 .with_override_query("(string_literal) @string")
8615 .unwrap(),
8616 );
8617
8618 cx.language_registry().add(rust_language.clone());
8619 cx.update_buffer(|buffer, cx| {
8620 buffer.set_language(Some(rust_language), cx);
8621 });
8622
8623 cx.set_state(
8624 &r#"
8625 let x = ˇ
8626 "#
8627 .unindent(),
8628 );
8629
8630 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8631 cx.update_editor(|editor, window, cx| {
8632 editor.handle_input("\"", window, cx);
8633 });
8634 cx.assert_editor_state(
8635 &r#"
8636 let x = "ˇ"
8637 "#
8638 .unindent(),
8639 );
8640
8641 // Inserting another quotation mark. The cursor moves across the existing
8642 // automatically-inserted quotation mark.
8643 cx.update_editor(|editor, window, cx| {
8644 editor.handle_input("\"", window, cx);
8645 });
8646 cx.assert_editor_state(
8647 &r#"
8648 let x = ""ˇ
8649 "#
8650 .unindent(),
8651 );
8652
8653 // Reset
8654 cx.set_state(
8655 &r#"
8656 let x = ˇ
8657 "#
8658 .unindent(),
8659 );
8660
8661 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8662 cx.update_editor(|editor, window, cx| {
8663 editor.handle_input("\"", window, cx);
8664 editor.handle_input(" ", window, cx);
8665 editor.move_left(&Default::default(), window, cx);
8666 editor.handle_input("\\", window, cx);
8667 editor.handle_input("\"", window, cx);
8668 });
8669 cx.assert_editor_state(
8670 &r#"
8671 let x = "\"ˇ "
8672 "#
8673 .unindent(),
8674 );
8675
8676 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8677 // mark. Nothing is inserted.
8678 cx.update_editor(|editor, window, cx| {
8679 editor.move_right(&Default::default(), window, cx);
8680 editor.handle_input("\"", window, cx);
8681 });
8682 cx.assert_editor_state(
8683 &r#"
8684 let x = "\" "ˇ
8685 "#
8686 .unindent(),
8687 );
8688}
8689
8690#[gpui::test]
8691async fn test_surround_with_pair(cx: &mut TestAppContext) {
8692 init_test(cx, |_| {});
8693
8694 let language = Arc::new(Language::new(
8695 LanguageConfig {
8696 brackets: BracketPairConfig {
8697 pairs: vec![
8698 BracketPair {
8699 start: "{".to_string(),
8700 end: "}".to_string(),
8701 close: true,
8702 surround: true,
8703 newline: true,
8704 },
8705 BracketPair {
8706 start: "/* ".to_string(),
8707 end: "*/".to_string(),
8708 close: true,
8709 surround: true,
8710 ..Default::default()
8711 },
8712 ],
8713 ..Default::default()
8714 },
8715 ..Default::default()
8716 },
8717 Some(tree_sitter_rust::LANGUAGE.into()),
8718 ));
8719
8720 let text = r#"
8721 a
8722 b
8723 c
8724 "#
8725 .unindent();
8726
8727 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8728 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8729 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8730 editor
8731 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8732 .await;
8733
8734 editor.update_in(cx, |editor, window, cx| {
8735 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8736 s.select_display_ranges([
8737 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8738 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8739 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8740 ])
8741 });
8742
8743 editor.handle_input("{", window, cx);
8744 editor.handle_input("{", window, cx);
8745 editor.handle_input("{", window, cx);
8746 assert_eq!(
8747 editor.text(cx),
8748 "
8749 {{{a}}}
8750 {{{b}}}
8751 {{{c}}}
8752 "
8753 .unindent()
8754 );
8755 assert_eq!(
8756 editor.selections.display_ranges(cx),
8757 [
8758 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8759 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8760 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8761 ]
8762 );
8763
8764 editor.undo(&Undo, window, cx);
8765 editor.undo(&Undo, window, cx);
8766 editor.undo(&Undo, window, cx);
8767 assert_eq!(
8768 editor.text(cx),
8769 "
8770 a
8771 b
8772 c
8773 "
8774 .unindent()
8775 );
8776 assert_eq!(
8777 editor.selections.display_ranges(cx),
8778 [
8779 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8780 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8781 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8782 ]
8783 );
8784
8785 // Ensure inserting the first character of a multi-byte bracket pair
8786 // doesn't surround the selections with the bracket.
8787 editor.handle_input("/", window, cx);
8788 assert_eq!(
8789 editor.text(cx),
8790 "
8791 /
8792 /
8793 /
8794 "
8795 .unindent()
8796 );
8797 assert_eq!(
8798 editor.selections.display_ranges(cx),
8799 [
8800 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8801 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8802 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8803 ]
8804 );
8805
8806 editor.undo(&Undo, window, cx);
8807 assert_eq!(
8808 editor.text(cx),
8809 "
8810 a
8811 b
8812 c
8813 "
8814 .unindent()
8815 );
8816 assert_eq!(
8817 editor.selections.display_ranges(cx),
8818 [
8819 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8820 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8821 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8822 ]
8823 );
8824
8825 // Ensure inserting the last character of a multi-byte bracket pair
8826 // doesn't surround the selections with the bracket.
8827 editor.handle_input("*", window, cx);
8828 assert_eq!(
8829 editor.text(cx),
8830 "
8831 *
8832 *
8833 *
8834 "
8835 .unindent()
8836 );
8837 assert_eq!(
8838 editor.selections.display_ranges(cx),
8839 [
8840 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8841 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8842 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8843 ]
8844 );
8845 });
8846}
8847
8848#[gpui::test]
8849async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8850 init_test(cx, |_| {});
8851
8852 let language = Arc::new(Language::new(
8853 LanguageConfig {
8854 brackets: BracketPairConfig {
8855 pairs: vec![BracketPair {
8856 start: "{".to_string(),
8857 end: "}".to_string(),
8858 close: true,
8859 surround: true,
8860 newline: true,
8861 }],
8862 ..Default::default()
8863 },
8864 autoclose_before: "}".to_string(),
8865 ..Default::default()
8866 },
8867 Some(tree_sitter_rust::LANGUAGE.into()),
8868 ));
8869
8870 let text = r#"
8871 a
8872 b
8873 c
8874 "#
8875 .unindent();
8876
8877 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8878 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8879 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8880 editor
8881 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8882 .await;
8883
8884 editor.update_in(cx, |editor, window, cx| {
8885 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8886 s.select_ranges([
8887 Point::new(0, 1)..Point::new(0, 1),
8888 Point::new(1, 1)..Point::new(1, 1),
8889 Point::new(2, 1)..Point::new(2, 1),
8890 ])
8891 });
8892
8893 editor.handle_input("{", window, cx);
8894 editor.handle_input("{", window, cx);
8895 editor.handle_input("_", window, cx);
8896 assert_eq!(
8897 editor.text(cx),
8898 "
8899 a{{_}}
8900 b{{_}}
8901 c{{_}}
8902 "
8903 .unindent()
8904 );
8905 assert_eq!(
8906 editor.selections.ranges::<Point>(cx),
8907 [
8908 Point::new(0, 4)..Point::new(0, 4),
8909 Point::new(1, 4)..Point::new(1, 4),
8910 Point::new(2, 4)..Point::new(2, 4)
8911 ]
8912 );
8913
8914 editor.backspace(&Default::default(), window, cx);
8915 editor.backspace(&Default::default(), window, cx);
8916 assert_eq!(
8917 editor.text(cx),
8918 "
8919 a{}
8920 b{}
8921 c{}
8922 "
8923 .unindent()
8924 );
8925 assert_eq!(
8926 editor.selections.ranges::<Point>(cx),
8927 [
8928 Point::new(0, 2)..Point::new(0, 2),
8929 Point::new(1, 2)..Point::new(1, 2),
8930 Point::new(2, 2)..Point::new(2, 2)
8931 ]
8932 );
8933
8934 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8935 assert_eq!(
8936 editor.text(cx),
8937 "
8938 a
8939 b
8940 c
8941 "
8942 .unindent()
8943 );
8944 assert_eq!(
8945 editor.selections.ranges::<Point>(cx),
8946 [
8947 Point::new(0, 1)..Point::new(0, 1),
8948 Point::new(1, 1)..Point::new(1, 1),
8949 Point::new(2, 1)..Point::new(2, 1)
8950 ]
8951 );
8952 });
8953}
8954
8955#[gpui::test]
8956async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8957 init_test(cx, |settings| {
8958 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8959 });
8960
8961 let mut cx = EditorTestContext::new(cx).await;
8962
8963 let language = Arc::new(Language::new(
8964 LanguageConfig {
8965 brackets: BracketPairConfig {
8966 pairs: vec![
8967 BracketPair {
8968 start: "{".to_string(),
8969 end: "}".to_string(),
8970 close: true,
8971 surround: true,
8972 newline: true,
8973 },
8974 BracketPair {
8975 start: "(".to_string(),
8976 end: ")".to_string(),
8977 close: true,
8978 surround: true,
8979 newline: true,
8980 },
8981 BracketPair {
8982 start: "[".to_string(),
8983 end: "]".to_string(),
8984 close: false,
8985 surround: true,
8986 newline: true,
8987 },
8988 ],
8989 ..Default::default()
8990 },
8991 autoclose_before: "})]".to_string(),
8992 ..Default::default()
8993 },
8994 Some(tree_sitter_rust::LANGUAGE.into()),
8995 ));
8996
8997 cx.language_registry().add(language.clone());
8998 cx.update_buffer(|buffer, cx| {
8999 buffer.set_language(Some(language), cx);
9000 });
9001
9002 cx.set_state(
9003 &"
9004 {(ˇ)}
9005 [[ˇ]]
9006 {(ˇ)}
9007 "
9008 .unindent(),
9009 );
9010
9011 cx.update_editor(|editor, window, cx| {
9012 editor.backspace(&Default::default(), window, cx);
9013 editor.backspace(&Default::default(), window, cx);
9014 });
9015
9016 cx.assert_editor_state(
9017 &"
9018 ˇ
9019 ˇ]]
9020 ˇ
9021 "
9022 .unindent(),
9023 );
9024
9025 cx.update_editor(|editor, window, cx| {
9026 editor.handle_input("{", window, cx);
9027 editor.handle_input("{", window, cx);
9028 editor.move_right(&MoveRight, window, cx);
9029 editor.move_right(&MoveRight, window, cx);
9030 editor.move_left(&MoveLeft, window, cx);
9031 editor.move_left(&MoveLeft, window, cx);
9032 editor.backspace(&Default::default(), window, cx);
9033 });
9034
9035 cx.assert_editor_state(
9036 &"
9037 {ˇ}
9038 {ˇ}]]
9039 {ˇ}
9040 "
9041 .unindent(),
9042 );
9043
9044 cx.update_editor(|editor, window, cx| {
9045 editor.backspace(&Default::default(), window, cx);
9046 });
9047
9048 cx.assert_editor_state(
9049 &"
9050 ˇ
9051 ˇ]]
9052 ˇ
9053 "
9054 .unindent(),
9055 );
9056}
9057
9058#[gpui::test]
9059async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9060 init_test(cx, |_| {});
9061
9062 let language = Arc::new(Language::new(
9063 LanguageConfig::default(),
9064 Some(tree_sitter_rust::LANGUAGE.into()),
9065 ));
9066
9067 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9068 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9069 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9070 editor
9071 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9072 .await;
9073
9074 editor.update_in(cx, |editor, window, cx| {
9075 editor.set_auto_replace_emoji_shortcode(true);
9076
9077 editor.handle_input("Hello ", window, cx);
9078 editor.handle_input(":wave", window, cx);
9079 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9080
9081 editor.handle_input(":", window, cx);
9082 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9083
9084 editor.handle_input(" :smile", window, cx);
9085 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9086
9087 editor.handle_input(":", window, cx);
9088 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9089
9090 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9091 editor.handle_input(":wave", window, cx);
9092 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9093
9094 editor.handle_input(":", window, cx);
9095 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9096
9097 editor.handle_input(":1", window, cx);
9098 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9099
9100 editor.handle_input(":", window, cx);
9101 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9102
9103 // Ensure shortcode does not get replaced when it is part of a word
9104 editor.handle_input(" Test:wave", window, cx);
9105 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9106
9107 editor.handle_input(":", window, cx);
9108 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9109
9110 editor.set_auto_replace_emoji_shortcode(false);
9111
9112 // Ensure shortcode does not get replaced when auto replace is off
9113 editor.handle_input(" :wave", window, cx);
9114 assert_eq!(
9115 editor.text(cx),
9116 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9117 );
9118
9119 editor.handle_input(":", window, cx);
9120 assert_eq!(
9121 editor.text(cx),
9122 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9123 );
9124 });
9125}
9126
9127#[gpui::test]
9128async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9129 init_test(cx, |_| {});
9130
9131 let (text, insertion_ranges) = marked_text_ranges(
9132 indoc! {"
9133 ˇ
9134 "},
9135 false,
9136 );
9137
9138 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9139 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9140
9141 _ = editor.update_in(cx, |editor, window, cx| {
9142 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9143
9144 editor
9145 .insert_snippet(&insertion_ranges, snippet, window, cx)
9146 .unwrap();
9147
9148 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9149 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9150 assert_eq!(editor.text(cx), expected_text);
9151 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9152 }
9153
9154 assert(
9155 editor,
9156 cx,
9157 indoc! {"
9158 type «» =•
9159 "},
9160 );
9161
9162 assert!(editor.context_menu_visible(), "There should be a matches");
9163 });
9164}
9165
9166#[gpui::test]
9167async fn test_snippets(cx: &mut TestAppContext) {
9168 init_test(cx, |_| {});
9169
9170 let mut cx = EditorTestContext::new(cx).await;
9171
9172 cx.set_state(indoc! {"
9173 a.ˇ b
9174 a.ˇ b
9175 a.ˇ b
9176 "});
9177
9178 cx.update_editor(|editor, window, cx| {
9179 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9180 let insertion_ranges = editor
9181 .selections
9182 .all(cx)
9183 .iter()
9184 .map(|s| s.range().clone())
9185 .collect::<Vec<_>>();
9186 editor
9187 .insert_snippet(&insertion_ranges, snippet, window, cx)
9188 .unwrap();
9189 });
9190
9191 cx.assert_editor_state(indoc! {"
9192 a.f(«oneˇ», two, «threeˇ») b
9193 a.f(«oneˇ», two, «threeˇ») b
9194 a.f(«oneˇ», two, «threeˇ») b
9195 "});
9196
9197 // Can't move earlier than the first tab stop
9198 cx.update_editor(|editor, window, cx| {
9199 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9200 });
9201 cx.assert_editor_state(indoc! {"
9202 a.f(«oneˇ», two, «threeˇ») b
9203 a.f(«oneˇ», two, «threeˇ») b
9204 a.f(«oneˇ», two, «threeˇ») b
9205 "});
9206
9207 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9208 cx.assert_editor_state(indoc! {"
9209 a.f(one, «twoˇ», three) b
9210 a.f(one, «twoˇ», three) b
9211 a.f(one, «twoˇ», three) b
9212 "});
9213
9214 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9215 cx.assert_editor_state(indoc! {"
9216 a.f(«oneˇ», two, «threeˇ») b
9217 a.f(«oneˇ», two, «threeˇ») b
9218 a.f(«oneˇ», two, «threeˇ») b
9219 "});
9220
9221 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9222 cx.assert_editor_state(indoc! {"
9223 a.f(one, «twoˇ», three) b
9224 a.f(one, «twoˇ», three) b
9225 a.f(one, «twoˇ», three) b
9226 "});
9227 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9228 cx.assert_editor_state(indoc! {"
9229 a.f(one, two, three)ˇ b
9230 a.f(one, two, three)ˇ b
9231 a.f(one, two, three)ˇ b
9232 "});
9233
9234 // As soon as the last tab stop is reached, snippet state is gone
9235 cx.update_editor(|editor, window, cx| {
9236 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9237 });
9238 cx.assert_editor_state(indoc! {"
9239 a.f(one, two, three)ˇ b
9240 a.f(one, two, three)ˇ b
9241 a.f(one, two, three)ˇ b
9242 "});
9243}
9244
9245#[gpui::test]
9246async fn test_snippet_indentation(cx: &mut TestAppContext) {
9247 init_test(cx, |_| {});
9248
9249 let mut cx = EditorTestContext::new(cx).await;
9250
9251 cx.update_editor(|editor, window, cx| {
9252 let snippet = Snippet::parse(indoc! {"
9253 /*
9254 * Multiline comment with leading indentation
9255 *
9256 * $1
9257 */
9258 $0"})
9259 .unwrap();
9260 let insertion_ranges = editor
9261 .selections
9262 .all(cx)
9263 .iter()
9264 .map(|s| s.range().clone())
9265 .collect::<Vec<_>>();
9266 editor
9267 .insert_snippet(&insertion_ranges, snippet, window, cx)
9268 .unwrap();
9269 });
9270
9271 cx.assert_editor_state(indoc! {"
9272 /*
9273 * Multiline comment with leading indentation
9274 *
9275 * ˇ
9276 */
9277 "});
9278
9279 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9280 cx.assert_editor_state(indoc! {"
9281 /*
9282 * Multiline comment with leading indentation
9283 *
9284 *•
9285 */
9286 ˇ"});
9287}
9288
9289#[gpui::test]
9290async fn test_document_format_during_save(cx: &mut TestAppContext) {
9291 init_test(cx, |_| {});
9292
9293 let fs = FakeFs::new(cx.executor());
9294 fs.insert_file(path!("/file.rs"), Default::default()).await;
9295
9296 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9297
9298 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9299 language_registry.add(rust_lang());
9300 let mut fake_servers = language_registry.register_fake_lsp(
9301 "Rust",
9302 FakeLspAdapter {
9303 capabilities: lsp::ServerCapabilities {
9304 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9305 ..Default::default()
9306 },
9307 ..Default::default()
9308 },
9309 );
9310
9311 let buffer = project
9312 .update(cx, |project, cx| {
9313 project.open_local_buffer(path!("/file.rs"), cx)
9314 })
9315 .await
9316 .unwrap();
9317
9318 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9319 let (editor, cx) = cx.add_window_view(|window, cx| {
9320 build_editor_with_project(project.clone(), buffer, window, cx)
9321 });
9322 editor.update_in(cx, |editor, window, cx| {
9323 editor.set_text("one\ntwo\nthree\n", window, cx)
9324 });
9325 assert!(cx.read(|cx| editor.is_dirty(cx)));
9326
9327 cx.executor().start_waiting();
9328 let fake_server = fake_servers.next().await.unwrap();
9329
9330 {
9331 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9332 move |params, _| async move {
9333 assert_eq!(
9334 params.text_document.uri,
9335 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9336 );
9337 assert_eq!(params.options.tab_size, 4);
9338 Ok(Some(vec![lsp::TextEdit::new(
9339 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9340 ", ".to_string(),
9341 )]))
9342 },
9343 );
9344 let save = editor
9345 .update_in(cx, |editor, window, cx| {
9346 editor.save(
9347 SaveOptions {
9348 format: true,
9349 autosave: false,
9350 },
9351 project.clone(),
9352 window,
9353 cx,
9354 )
9355 })
9356 .unwrap();
9357 cx.executor().start_waiting();
9358 save.await;
9359
9360 assert_eq!(
9361 editor.update(cx, |editor, cx| editor.text(cx)),
9362 "one, two\nthree\n"
9363 );
9364 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9365 }
9366
9367 {
9368 editor.update_in(cx, |editor, window, cx| {
9369 editor.set_text("one\ntwo\nthree\n", window, cx)
9370 });
9371 assert!(cx.read(|cx| editor.is_dirty(cx)));
9372
9373 // Ensure we can still save even if formatting hangs.
9374 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9375 move |params, _| async move {
9376 assert_eq!(
9377 params.text_document.uri,
9378 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9379 );
9380 futures::future::pending::<()>().await;
9381 unreachable!()
9382 },
9383 );
9384 let save = editor
9385 .update_in(cx, |editor, window, cx| {
9386 editor.save(
9387 SaveOptions {
9388 format: true,
9389 autosave: false,
9390 },
9391 project.clone(),
9392 window,
9393 cx,
9394 )
9395 })
9396 .unwrap();
9397 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9398 cx.executor().start_waiting();
9399 save.await;
9400 assert_eq!(
9401 editor.update(cx, |editor, cx| editor.text(cx)),
9402 "one\ntwo\nthree\n"
9403 );
9404 }
9405
9406 // Set rust language override and assert overridden tabsize is sent to language server
9407 update_test_language_settings(cx, |settings| {
9408 settings.languages.0.insert(
9409 "Rust".into(),
9410 LanguageSettingsContent {
9411 tab_size: NonZeroU32::new(8),
9412 ..Default::default()
9413 },
9414 );
9415 });
9416
9417 {
9418 editor.update_in(cx, |editor, window, cx| {
9419 editor.set_text("somehting_new\n", window, cx)
9420 });
9421 assert!(cx.read(|cx| editor.is_dirty(cx)));
9422 let _formatting_request_signal = fake_server
9423 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9424 assert_eq!(
9425 params.text_document.uri,
9426 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9427 );
9428 assert_eq!(params.options.tab_size, 8);
9429 Ok(Some(vec![]))
9430 });
9431 let save = editor
9432 .update_in(cx, |editor, window, cx| {
9433 editor.save(
9434 SaveOptions {
9435 format: true,
9436 autosave: false,
9437 },
9438 project.clone(),
9439 window,
9440 cx,
9441 )
9442 })
9443 .unwrap();
9444 cx.executor().start_waiting();
9445 save.await;
9446 }
9447}
9448
9449#[gpui::test]
9450async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9451 init_test(cx, |_| {});
9452
9453 let cols = 4;
9454 let rows = 10;
9455 let sample_text_1 = sample_text(rows, cols, 'a');
9456 assert_eq!(
9457 sample_text_1,
9458 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9459 );
9460 let sample_text_2 = sample_text(rows, cols, 'l');
9461 assert_eq!(
9462 sample_text_2,
9463 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9464 );
9465 let sample_text_3 = sample_text(rows, cols, 'v');
9466 assert_eq!(
9467 sample_text_3,
9468 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9469 );
9470
9471 let fs = FakeFs::new(cx.executor());
9472 fs.insert_tree(
9473 path!("/a"),
9474 json!({
9475 "main.rs": sample_text_1,
9476 "other.rs": sample_text_2,
9477 "lib.rs": sample_text_3,
9478 }),
9479 )
9480 .await;
9481
9482 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9483 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9484 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9485
9486 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9487 language_registry.add(rust_lang());
9488 let mut fake_servers = language_registry.register_fake_lsp(
9489 "Rust",
9490 FakeLspAdapter {
9491 capabilities: lsp::ServerCapabilities {
9492 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9493 ..Default::default()
9494 },
9495 ..Default::default()
9496 },
9497 );
9498
9499 let worktree = project.update(cx, |project, cx| {
9500 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9501 assert_eq!(worktrees.len(), 1);
9502 worktrees.pop().unwrap()
9503 });
9504 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9505
9506 let buffer_1 = project
9507 .update(cx, |project, cx| {
9508 project.open_buffer((worktree_id, "main.rs"), cx)
9509 })
9510 .await
9511 .unwrap();
9512 let buffer_2 = project
9513 .update(cx, |project, cx| {
9514 project.open_buffer((worktree_id, "other.rs"), cx)
9515 })
9516 .await
9517 .unwrap();
9518 let buffer_3 = project
9519 .update(cx, |project, cx| {
9520 project.open_buffer((worktree_id, "lib.rs"), cx)
9521 })
9522 .await
9523 .unwrap();
9524
9525 let multi_buffer = cx.new(|cx| {
9526 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9527 multi_buffer.push_excerpts(
9528 buffer_1.clone(),
9529 [
9530 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9531 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9532 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9533 ],
9534 cx,
9535 );
9536 multi_buffer.push_excerpts(
9537 buffer_2.clone(),
9538 [
9539 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9540 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9541 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9542 ],
9543 cx,
9544 );
9545 multi_buffer.push_excerpts(
9546 buffer_3.clone(),
9547 [
9548 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9549 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9550 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9551 ],
9552 cx,
9553 );
9554 multi_buffer
9555 });
9556 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9557 Editor::new(
9558 EditorMode::full(),
9559 multi_buffer,
9560 Some(project.clone()),
9561 window,
9562 cx,
9563 )
9564 });
9565
9566 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9567 editor.change_selections(
9568 SelectionEffects::scroll(Autoscroll::Next),
9569 window,
9570 cx,
9571 |s| s.select_ranges(Some(1..2)),
9572 );
9573 editor.insert("|one|two|three|", window, cx);
9574 });
9575 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9576 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9577 editor.change_selections(
9578 SelectionEffects::scroll(Autoscroll::Next),
9579 window,
9580 cx,
9581 |s| s.select_ranges(Some(60..70)),
9582 );
9583 editor.insert("|four|five|six|", window, cx);
9584 });
9585 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9586
9587 // First two buffers should be edited, but not the third one.
9588 assert_eq!(
9589 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9590 "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}",
9591 );
9592 buffer_1.update(cx, |buffer, _| {
9593 assert!(buffer.is_dirty());
9594 assert_eq!(
9595 buffer.text(),
9596 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9597 )
9598 });
9599 buffer_2.update(cx, |buffer, _| {
9600 assert!(buffer.is_dirty());
9601 assert_eq!(
9602 buffer.text(),
9603 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9604 )
9605 });
9606 buffer_3.update(cx, |buffer, _| {
9607 assert!(!buffer.is_dirty());
9608 assert_eq!(buffer.text(), sample_text_3,)
9609 });
9610 cx.executor().run_until_parked();
9611
9612 cx.executor().start_waiting();
9613 let save = multi_buffer_editor
9614 .update_in(cx, |editor, window, cx| {
9615 editor.save(
9616 SaveOptions {
9617 format: true,
9618 autosave: false,
9619 },
9620 project.clone(),
9621 window,
9622 cx,
9623 )
9624 })
9625 .unwrap();
9626
9627 let fake_server = fake_servers.next().await.unwrap();
9628 fake_server
9629 .server
9630 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9631 Ok(Some(vec![lsp::TextEdit::new(
9632 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9633 format!("[{} formatted]", params.text_document.uri),
9634 )]))
9635 })
9636 .detach();
9637 save.await;
9638
9639 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9640 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9641 assert_eq!(
9642 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9643 uri!(
9644 "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}"
9645 ),
9646 );
9647 buffer_1.update(cx, |buffer, _| {
9648 assert!(!buffer.is_dirty());
9649 assert_eq!(
9650 buffer.text(),
9651 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9652 )
9653 });
9654 buffer_2.update(cx, |buffer, _| {
9655 assert!(!buffer.is_dirty());
9656 assert_eq!(
9657 buffer.text(),
9658 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9659 )
9660 });
9661 buffer_3.update(cx, |buffer, _| {
9662 assert!(!buffer.is_dirty());
9663 assert_eq!(buffer.text(), sample_text_3,)
9664 });
9665}
9666
9667#[gpui::test]
9668async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9669 init_test(cx, |_| {});
9670
9671 let fs = FakeFs::new(cx.executor());
9672 fs.insert_tree(
9673 path!("/dir"),
9674 json!({
9675 "file1.rs": "fn main() { println!(\"hello\"); }",
9676 "file2.rs": "fn test() { println!(\"test\"); }",
9677 "file3.rs": "fn other() { println!(\"other\"); }\n",
9678 }),
9679 )
9680 .await;
9681
9682 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9683 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9684 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9685
9686 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9687 language_registry.add(rust_lang());
9688
9689 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9690 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9691
9692 // Open three buffers
9693 let buffer_1 = project
9694 .update(cx, |project, cx| {
9695 project.open_buffer((worktree_id, "file1.rs"), cx)
9696 })
9697 .await
9698 .unwrap();
9699 let buffer_2 = project
9700 .update(cx, |project, cx| {
9701 project.open_buffer((worktree_id, "file2.rs"), cx)
9702 })
9703 .await
9704 .unwrap();
9705 let buffer_3 = project
9706 .update(cx, |project, cx| {
9707 project.open_buffer((worktree_id, "file3.rs"), cx)
9708 })
9709 .await
9710 .unwrap();
9711
9712 // Create a multi-buffer with all three buffers
9713 let multi_buffer = cx.new(|cx| {
9714 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9715 multi_buffer.push_excerpts(
9716 buffer_1.clone(),
9717 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9718 cx,
9719 );
9720 multi_buffer.push_excerpts(
9721 buffer_2.clone(),
9722 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9723 cx,
9724 );
9725 multi_buffer.push_excerpts(
9726 buffer_3.clone(),
9727 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9728 cx,
9729 );
9730 multi_buffer
9731 });
9732
9733 let editor = cx.new_window_entity(|window, cx| {
9734 Editor::new(
9735 EditorMode::full(),
9736 multi_buffer,
9737 Some(project.clone()),
9738 window,
9739 cx,
9740 )
9741 });
9742
9743 // Edit only the first buffer
9744 editor.update_in(cx, |editor, window, cx| {
9745 editor.change_selections(
9746 SelectionEffects::scroll(Autoscroll::Next),
9747 window,
9748 cx,
9749 |s| s.select_ranges(Some(10..10)),
9750 );
9751 editor.insert("// edited", window, cx);
9752 });
9753
9754 // Verify that only buffer 1 is dirty
9755 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
9756 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9757 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9758
9759 // Get write counts after file creation (files were created with initial content)
9760 // We expect each file to have been written once during creation
9761 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
9762 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
9763 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
9764
9765 // Perform autosave
9766 let save_task = editor.update_in(cx, |editor, window, cx| {
9767 editor.save(
9768 SaveOptions {
9769 format: true,
9770 autosave: true,
9771 },
9772 project.clone(),
9773 window,
9774 cx,
9775 )
9776 });
9777 save_task.await.unwrap();
9778
9779 // Only the dirty buffer should have been saved
9780 assert_eq!(
9781 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9782 1,
9783 "Buffer 1 was dirty, so it should have been written once during autosave"
9784 );
9785 assert_eq!(
9786 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9787 0,
9788 "Buffer 2 was clean, so it should not have been written during autosave"
9789 );
9790 assert_eq!(
9791 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9792 0,
9793 "Buffer 3 was clean, so it should not have been written during autosave"
9794 );
9795
9796 // Verify buffer states after autosave
9797 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9798 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9799 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9800
9801 // Now perform a manual save (format = true)
9802 let save_task = editor.update_in(cx, |editor, window, cx| {
9803 editor.save(
9804 SaveOptions {
9805 format: true,
9806 autosave: false,
9807 },
9808 project.clone(),
9809 window,
9810 cx,
9811 )
9812 });
9813 save_task.await.unwrap();
9814
9815 // During manual save, clean buffers don't get written to disk
9816 // They just get did_save called for language server notifications
9817 assert_eq!(
9818 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9819 1,
9820 "Buffer 1 should only have been written once total (during autosave, not manual save)"
9821 );
9822 assert_eq!(
9823 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9824 0,
9825 "Buffer 2 should not have been written at all"
9826 );
9827 assert_eq!(
9828 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9829 0,
9830 "Buffer 3 should not have been written at all"
9831 );
9832}
9833
9834#[gpui::test]
9835async fn test_range_format_during_save(cx: &mut TestAppContext) {
9836 init_test(cx, |_| {});
9837
9838 let fs = FakeFs::new(cx.executor());
9839 fs.insert_file(path!("/file.rs"), Default::default()).await;
9840
9841 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9842
9843 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9844 language_registry.add(rust_lang());
9845 let mut fake_servers = language_registry.register_fake_lsp(
9846 "Rust",
9847 FakeLspAdapter {
9848 capabilities: lsp::ServerCapabilities {
9849 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
9850 ..Default::default()
9851 },
9852 ..Default::default()
9853 },
9854 );
9855
9856 let buffer = project
9857 .update(cx, |project, cx| {
9858 project.open_local_buffer(path!("/file.rs"), cx)
9859 })
9860 .await
9861 .unwrap();
9862
9863 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9864 let (editor, cx) = cx.add_window_view(|window, cx| {
9865 build_editor_with_project(project.clone(), buffer, window, cx)
9866 });
9867 editor.update_in(cx, |editor, window, cx| {
9868 editor.set_text("one\ntwo\nthree\n", window, cx)
9869 });
9870 assert!(cx.read(|cx| editor.is_dirty(cx)));
9871
9872 cx.executor().start_waiting();
9873 let fake_server = fake_servers.next().await.unwrap();
9874
9875 let save = editor
9876 .update_in(cx, |editor, window, cx| {
9877 editor.save(
9878 SaveOptions {
9879 format: true,
9880 autosave: false,
9881 },
9882 project.clone(),
9883 window,
9884 cx,
9885 )
9886 })
9887 .unwrap();
9888 fake_server
9889 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9890 assert_eq!(
9891 params.text_document.uri,
9892 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9893 );
9894 assert_eq!(params.options.tab_size, 4);
9895 Ok(Some(vec![lsp::TextEdit::new(
9896 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9897 ", ".to_string(),
9898 )]))
9899 })
9900 .next()
9901 .await;
9902 cx.executor().start_waiting();
9903 save.await;
9904 assert_eq!(
9905 editor.update(cx, |editor, cx| editor.text(cx)),
9906 "one, two\nthree\n"
9907 );
9908 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9909
9910 editor.update_in(cx, |editor, window, cx| {
9911 editor.set_text("one\ntwo\nthree\n", window, cx)
9912 });
9913 assert!(cx.read(|cx| editor.is_dirty(cx)));
9914
9915 // Ensure we can still save even if formatting hangs.
9916 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
9917 move |params, _| async move {
9918 assert_eq!(
9919 params.text_document.uri,
9920 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9921 );
9922 futures::future::pending::<()>().await;
9923 unreachable!()
9924 },
9925 );
9926 let save = editor
9927 .update_in(cx, |editor, window, cx| {
9928 editor.save(
9929 SaveOptions {
9930 format: true,
9931 autosave: false,
9932 },
9933 project.clone(),
9934 window,
9935 cx,
9936 )
9937 })
9938 .unwrap();
9939 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9940 cx.executor().start_waiting();
9941 save.await;
9942 assert_eq!(
9943 editor.update(cx, |editor, cx| editor.text(cx)),
9944 "one\ntwo\nthree\n"
9945 );
9946 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9947
9948 // For non-dirty buffer, no formatting request should be sent
9949 let save = editor
9950 .update_in(cx, |editor, window, cx| {
9951 editor.save(
9952 SaveOptions {
9953 format: false,
9954 autosave: false,
9955 },
9956 project.clone(),
9957 window,
9958 cx,
9959 )
9960 })
9961 .unwrap();
9962 let _pending_format_request = fake_server
9963 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
9964 panic!("Should not be invoked");
9965 })
9966 .next();
9967 cx.executor().start_waiting();
9968 save.await;
9969
9970 // Set Rust language override and assert overridden tabsize is sent to language server
9971 update_test_language_settings(cx, |settings| {
9972 settings.languages.0.insert(
9973 "Rust".into(),
9974 LanguageSettingsContent {
9975 tab_size: NonZeroU32::new(8),
9976 ..Default::default()
9977 },
9978 );
9979 });
9980
9981 editor.update_in(cx, |editor, window, cx| {
9982 editor.set_text("somehting_new\n", window, cx)
9983 });
9984 assert!(cx.read(|cx| editor.is_dirty(cx)));
9985 let save = editor
9986 .update_in(cx, |editor, window, cx| {
9987 editor.save(
9988 SaveOptions {
9989 format: true,
9990 autosave: false,
9991 },
9992 project.clone(),
9993 window,
9994 cx,
9995 )
9996 })
9997 .unwrap();
9998 fake_server
9999 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10000 assert_eq!(
10001 params.text_document.uri,
10002 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10003 );
10004 assert_eq!(params.options.tab_size, 8);
10005 Ok(Some(Vec::new()))
10006 })
10007 .next()
10008 .await;
10009 save.await;
10010}
10011
10012#[gpui::test]
10013async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
10014 init_test(cx, |settings| {
10015 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![
10016 Formatter::LanguageServer { name: None },
10017 ]))
10018 });
10019
10020 let fs = FakeFs::new(cx.executor());
10021 fs.insert_file(path!("/file.rs"), Default::default()).await;
10022
10023 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10024
10025 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10026 language_registry.add(Arc::new(Language::new(
10027 LanguageConfig {
10028 name: "Rust".into(),
10029 matcher: LanguageMatcher {
10030 path_suffixes: vec!["rs".to_string()],
10031 ..Default::default()
10032 },
10033 ..LanguageConfig::default()
10034 },
10035 Some(tree_sitter_rust::LANGUAGE.into()),
10036 )));
10037 update_test_language_settings(cx, |settings| {
10038 // Enable Prettier formatting for the same buffer, and ensure
10039 // LSP is called instead of Prettier.
10040 settings.defaults.prettier = Some(PrettierSettings {
10041 allowed: true,
10042 ..PrettierSettings::default()
10043 });
10044 });
10045 let mut fake_servers = language_registry.register_fake_lsp(
10046 "Rust",
10047 FakeLspAdapter {
10048 capabilities: lsp::ServerCapabilities {
10049 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10050 ..Default::default()
10051 },
10052 ..Default::default()
10053 },
10054 );
10055
10056 let buffer = project
10057 .update(cx, |project, cx| {
10058 project.open_local_buffer(path!("/file.rs"), cx)
10059 })
10060 .await
10061 .unwrap();
10062
10063 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10064 let (editor, cx) = cx.add_window_view(|window, cx| {
10065 build_editor_with_project(project.clone(), buffer, window, cx)
10066 });
10067 editor.update_in(cx, |editor, window, cx| {
10068 editor.set_text("one\ntwo\nthree\n", window, cx)
10069 });
10070
10071 cx.executor().start_waiting();
10072 let fake_server = fake_servers.next().await.unwrap();
10073
10074 let format = editor
10075 .update_in(cx, |editor, window, cx| {
10076 editor.perform_format(
10077 project.clone(),
10078 FormatTrigger::Manual,
10079 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10080 window,
10081 cx,
10082 )
10083 })
10084 .unwrap();
10085 fake_server
10086 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10087 assert_eq!(
10088 params.text_document.uri,
10089 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10090 );
10091 assert_eq!(params.options.tab_size, 4);
10092 Ok(Some(vec![lsp::TextEdit::new(
10093 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10094 ", ".to_string(),
10095 )]))
10096 })
10097 .next()
10098 .await;
10099 cx.executor().start_waiting();
10100 format.await;
10101 assert_eq!(
10102 editor.update(cx, |editor, cx| editor.text(cx)),
10103 "one, two\nthree\n"
10104 );
10105
10106 editor.update_in(cx, |editor, window, cx| {
10107 editor.set_text("one\ntwo\nthree\n", window, cx)
10108 });
10109 // Ensure we don't lock if formatting hangs.
10110 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10111 move |params, _| async move {
10112 assert_eq!(
10113 params.text_document.uri,
10114 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10115 );
10116 futures::future::pending::<()>().await;
10117 unreachable!()
10118 },
10119 );
10120 let format = editor
10121 .update_in(cx, |editor, window, cx| {
10122 editor.perform_format(
10123 project,
10124 FormatTrigger::Manual,
10125 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10126 window,
10127 cx,
10128 )
10129 })
10130 .unwrap();
10131 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10132 cx.executor().start_waiting();
10133 format.await;
10134 assert_eq!(
10135 editor.update(cx, |editor, cx| editor.text(cx)),
10136 "one\ntwo\nthree\n"
10137 );
10138}
10139
10140#[gpui::test]
10141async fn test_multiple_formatters(cx: &mut TestAppContext) {
10142 init_test(cx, |settings| {
10143 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10144 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![
10145 Formatter::LanguageServer { name: None },
10146 Formatter::CodeActions(
10147 [
10148 ("code-action-1".into(), true),
10149 ("code-action-2".into(), true),
10150 ]
10151 .into_iter()
10152 .collect(),
10153 ),
10154 ]))
10155 });
10156
10157 let fs = FakeFs::new(cx.executor());
10158 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10159 .await;
10160
10161 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10162 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10163 language_registry.add(rust_lang());
10164
10165 let mut fake_servers = language_registry.register_fake_lsp(
10166 "Rust",
10167 FakeLspAdapter {
10168 capabilities: lsp::ServerCapabilities {
10169 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10170 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10171 commands: vec!["the-command-for-code-action-1".into()],
10172 ..Default::default()
10173 }),
10174 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10175 ..Default::default()
10176 },
10177 ..Default::default()
10178 },
10179 );
10180
10181 let buffer = project
10182 .update(cx, |project, cx| {
10183 project.open_local_buffer(path!("/file.rs"), cx)
10184 })
10185 .await
10186 .unwrap();
10187
10188 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10189 let (editor, cx) = cx.add_window_view(|window, cx| {
10190 build_editor_with_project(project.clone(), buffer, window, cx)
10191 });
10192
10193 cx.executor().start_waiting();
10194
10195 let fake_server = fake_servers.next().await.unwrap();
10196 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10197 move |_params, _| async move {
10198 Ok(Some(vec![lsp::TextEdit::new(
10199 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10200 "applied-formatting\n".to_string(),
10201 )]))
10202 },
10203 );
10204 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10205 move |params, _| async move {
10206 assert_eq!(
10207 params.context.only,
10208 Some(vec!["code-action-1".into(), "code-action-2".into()])
10209 );
10210 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10211 Ok(Some(vec![
10212 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10213 kind: Some("code-action-1".into()),
10214 edit: Some(lsp::WorkspaceEdit::new(
10215 [(
10216 uri.clone(),
10217 vec![lsp::TextEdit::new(
10218 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10219 "applied-code-action-1-edit\n".to_string(),
10220 )],
10221 )]
10222 .into_iter()
10223 .collect(),
10224 )),
10225 command: Some(lsp::Command {
10226 command: "the-command-for-code-action-1".into(),
10227 ..Default::default()
10228 }),
10229 ..Default::default()
10230 }),
10231 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10232 kind: Some("code-action-2".into()),
10233 edit: Some(lsp::WorkspaceEdit::new(
10234 [(
10235 uri.clone(),
10236 vec![lsp::TextEdit::new(
10237 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10238 "applied-code-action-2-edit\n".to_string(),
10239 )],
10240 )]
10241 .into_iter()
10242 .collect(),
10243 )),
10244 ..Default::default()
10245 }),
10246 ]))
10247 },
10248 );
10249
10250 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10251 move |params, _| async move { Ok(params) }
10252 });
10253
10254 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10255 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10256 let fake = fake_server.clone();
10257 let lock = command_lock.clone();
10258 move |params, _| {
10259 assert_eq!(params.command, "the-command-for-code-action-1");
10260 let fake = fake.clone();
10261 let lock = lock.clone();
10262 async move {
10263 lock.lock().await;
10264 fake.server
10265 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10266 label: None,
10267 edit: lsp::WorkspaceEdit {
10268 changes: Some(
10269 [(
10270 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10271 vec![lsp::TextEdit {
10272 range: lsp::Range::new(
10273 lsp::Position::new(0, 0),
10274 lsp::Position::new(0, 0),
10275 ),
10276 new_text: "applied-code-action-1-command\n".into(),
10277 }],
10278 )]
10279 .into_iter()
10280 .collect(),
10281 ),
10282 ..Default::default()
10283 },
10284 })
10285 .await
10286 .into_response()
10287 .unwrap();
10288 Ok(Some(json!(null)))
10289 }
10290 }
10291 });
10292
10293 cx.executor().start_waiting();
10294 editor
10295 .update_in(cx, |editor, window, cx| {
10296 editor.perform_format(
10297 project.clone(),
10298 FormatTrigger::Manual,
10299 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10300 window,
10301 cx,
10302 )
10303 })
10304 .unwrap()
10305 .await;
10306 editor.update(cx, |editor, cx| {
10307 assert_eq!(
10308 editor.text(cx),
10309 r#"
10310 applied-code-action-2-edit
10311 applied-code-action-1-command
10312 applied-code-action-1-edit
10313 applied-formatting
10314 one
10315 two
10316 three
10317 "#
10318 .unindent()
10319 );
10320 });
10321
10322 editor.update_in(cx, |editor, window, cx| {
10323 editor.undo(&Default::default(), window, cx);
10324 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10325 });
10326
10327 // Perform a manual edit while waiting for an LSP command
10328 // that's being run as part of a formatting code action.
10329 let lock_guard = command_lock.lock().await;
10330 let format = editor
10331 .update_in(cx, |editor, window, cx| {
10332 editor.perform_format(
10333 project.clone(),
10334 FormatTrigger::Manual,
10335 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10336 window,
10337 cx,
10338 )
10339 })
10340 .unwrap();
10341 cx.run_until_parked();
10342 editor.update(cx, |editor, cx| {
10343 assert_eq!(
10344 editor.text(cx),
10345 r#"
10346 applied-code-action-1-edit
10347 applied-formatting
10348 one
10349 two
10350 three
10351 "#
10352 .unindent()
10353 );
10354
10355 editor.buffer.update(cx, |buffer, cx| {
10356 let ix = buffer.len(cx);
10357 buffer.edit([(ix..ix, "edited\n")], None, cx);
10358 });
10359 });
10360
10361 // Allow the LSP command to proceed. Because the buffer was edited,
10362 // the second code action will not be run.
10363 drop(lock_guard);
10364 format.await;
10365 editor.update_in(cx, |editor, window, cx| {
10366 assert_eq!(
10367 editor.text(cx),
10368 r#"
10369 applied-code-action-1-command
10370 applied-code-action-1-edit
10371 applied-formatting
10372 one
10373 two
10374 three
10375 edited
10376 "#
10377 .unindent()
10378 );
10379
10380 // The manual edit is undone first, because it is the last thing the user did
10381 // (even though the command completed afterwards).
10382 editor.undo(&Default::default(), window, cx);
10383 assert_eq!(
10384 editor.text(cx),
10385 r#"
10386 applied-code-action-1-command
10387 applied-code-action-1-edit
10388 applied-formatting
10389 one
10390 two
10391 three
10392 "#
10393 .unindent()
10394 );
10395
10396 // All the formatting (including the command, which completed after the manual edit)
10397 // is undone together.
10398 editor.undo(&Default::default(), window, cx);
10399 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10400 });
10401}
10402
10403#[gpui::test]
10404async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10405 init_test(cx, |settings| {
10406 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![
10407 Formatter::LanguageServer { name: None },
10408 ]))
10409 });
10410
10411 let fs = FakeFs::new(cx.executor());
10412 fs.insert_file(path!("/file.ts"), Default::default()).await;
10413
10414 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10415
10416 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10417 language_registry.add(Arc::new(Language::new(
10418 LanguageConfig {
10419 name: "TypeScript".into(),
10420 matcher: LanguageMatcher {
10421 path_suffixes: vec!["ts".to_string()],
10422 ..Default::default()
10423 },
10424 ..LanguageConfig::default()
10425 },
10426 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10427 )));
10428 update_test_language_settings(cx, |settings| {
10429 settings.defaults.prettier = Some(PrettierSettings {
10430 allowed: true,
10431 ..PrettierSettings::default()
10432 });
10433 });
10434 let mut fake_servers = language_registry.register_fake_lsp(
10435 "TypeScript",
10436 FakeLspAdapter {
10437 capabilities: lsp::ServerCapabilities {
10438 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10439 ..Default::default()
10440 },
10441 ..Default::default()
10442 },
10443 );
10444
10445 let buffer = project
10446 .update(cx, |project, cx| {
10447 project.open_local_buffer(path!("/file.ts"), cx)
10448 })
10449 .await
10450 .unwrap();
10451
10452 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10453 let (editor, cx) = cx.add_window_view(|window, cx| {
10454 build_editor_with_project(project.clone(), buffer, window, cx)
10455 });
10456 editor.update_in(cx, |editor, window, cx| {
10457 editor.set_text(
10458 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10459 window,
10460 cx,
10461 )
10462 });
10463
10464 cx.executor().start_waiting();
10465 let fake_server = fake_servers.next().await.unwrap();
10466
10467 let format = editor
10468 .update_in(cx, |editor, window, cx| {
10469 editor.perform_code_action_kind(
10470 project.clone(),
10471 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10472 window,
10473 cx,
10474 )
10475 })
10476 .unwrap();
10477 fake_server
10478 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10479 assert_eq!(
10480 params.text_document.uri,
10481 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10482 );
10483 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10484 lsp::CodeAction {
10485 title: "Organize Imports".to_string(),
10486 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10487 edit: Some(lsp::WorkspaceEdit {
10488 changes: Some(
10489 [(
10490 params.text_document.uri.clone(),
10491 vec![lsp::TextEdit::new(
10492 lsp::Range::new(
10493 lsp::Position::new(1, 0),
10494 lsp::Position::new(2, 0),
10495 ),
10496 "".to_string(),
10497 )],
10498 )]
10499 .into_iter()
10500 .collect(),
10501 ),
10502 ..Default::default()
10503 }),
10504 ..Default::default()
10505 },
10506 )]))
10507 })
10508 .next()
10509 .await;
10510 cx.executor().start_waiting();
10511 format.await;
10512 assert_eq!(
10513 editor.update(cx, |editor, cx| editor.text(cx)),
10514 "import { a } from 'module';\n\nconst x = a;\n"
10515 );
10516
10517 editor.update_in(cx, |editor, window, cx| {
10518 editor.set_text(
10519 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10520 window,
10521 cx,
10522 )
10523 });
10524 // Ensure we don't lock if code action hangs.
10525 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10526 move |params, _| async move {
10527 assert_eq!(
10528 params.text_document.uri,
10529 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10530 );
10531 futures::future::pending::<()>().await;
10532 unreachable!()
10533 },
10534 );
10535 let format = editor
10536 .update_in(cx, |editor, window, cx| {
10537 editor.perform_code_action_kind(
10538 project,
10539 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10540 window,
10541 cx,
10542 )
10543 })
10544 .unwrap();
10545 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10546 cx.executor().start_waiting();
10547 format.await;
10548 assert_eq!(
10549 editor.update(cx, |editor, cx| editor.text(cx)),
10550 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10551 );
10552}
10553
10554#[gpui::test]
10555async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10556 init_test(cx, |_| {});
10557
10558 let mut cx = EditorLspTestContext::new_rust(
10559 lsp::ServerCapabilities {
10560 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10561 ..Default::default()
10562 },
10563 cx,
10564 )
10565 .await;
10566
10567 cx.set_state(indoc! {"
10568 one.twoˇ
10569 "});
10570
10571 // The format request takes a long time. When it completes, it inserts
10572 // a newline and an indent before the `.`
10573 cx.lsp
10574 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10575 let executor = cx.background_executor().clone();
10576 async move {
10577 executor.timer(Duration::from_millis(100)).await;
10578 Ok(Some(vec![lsp::TextEdit {
10579 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10580 new_text: "\n ".into(),
10581 }]))
10582 }
10583 });
10584
10585 // Submit a format request.
10586 let format_1 = cx
10587 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10588 .unwrap();
10589 cx.executor().run_until_parked();
10590
10591 // Submit a second format request.
10592 let format_2 = cx
10593 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10594 .unwrap();
10595 cx.executor().run_until_parked();
10596
10597 // Wait for both format requests to complete
10598 cx.executor().advance_clock(Duration::from_millis(200));
10599 cx.executor().start_waiting();
10600 format_1.await.unwrap();
10601 cx.executor().start_waiting();
10602 format_2.await.unwrap();
10603
10604 // The formatting edits only happens once.
10605 cx.assert_editor_state(indoc! {"
10606 one
10607 .twoˇ
10608 "});
10609}
10610
10611#[gpui::test]
10612async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10613 init_test(cx, |settings| {
10614 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
10615 });
10616
10617 let mut cx = EditorLspTestContext::new_rust(
10618 lsp::ServerCapabilities {
10619 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10620 ..Default::default()
10621 },
10622 cx,
10623 )
10624 .await;
10625
10626 // Set up a buffer white some trailing whitespace and no trailing newline.
10627 cx.set_state(
10628 &[
10629 "one ", //
10630 "twoˇ", //
10631 "three ", //
10632 "four", //
10633 ]
10634 .join("\n"),
10635 );
10636
10637 // Submit a format request.
10638 let format = cx
10639 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10640 .unwrap();
10641
10642 // Record which buffer changes have been sent to the language server
10643 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10644 cx.lsp
10645 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10646 let buffer_changes = buffer_changes.clone();
10647 move |params, _| {
10648 buffer_changes.lock().extend(
10649 params
10650 .content_changes
10651 .into_iter()
10652 .map(|e| (e.range.unwrap(), e.text)),
10653 );
10654 }
10655 });
10656
10657 // Handle formatting requests to the language server.
10658 cx.lsp
10659 .set_request_handler::<lsp::request::Formatting, _, _>({
10660 let buffer_changes = buffer_changes.clone();
10661 move |_, _| {
10662 // When formatting is requested, trailing whitespace has already been stripped,
10663 // and the trailing newline has already been added.
10664 assert_eq!(
10665 &buffer_changes.lock()[1..],
10666 &[
10667 (
10668 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10669 "".into()
10670 ),
10671 (
10672 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10673 "".into()
10674 ),
10675 (
10676 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10677 "\n".into()
10678 ),
10679 ]
10680 );
10681
10682 // Insert blank lines between each line of the buffer.
10683 async move {
10684 Ok(Some(vec![
10685 lsp::TextEdit {
10686 range: lsp::Range::new(
10687 lsp::Position::new(1, 0),
10688 lsp::Position::new(1, 0),
10689 ),
10690 new_text: "\n".into(),
10691 },
10692 lsp::TextEdit {
10693 range: lsp::Range::new(
10694 lsp::Position::new(2, 0),
10695 lsp::Position::new(2, 0),
10696 ),
10697 new_text: "\n".into(),
10698 },
10699 ]))
10700 }
10701 }
10702 });
10703
10704 // After formatting the buffer, the trailing whitespace is stripped,
10705 // a newline is appended, and the edits provided by the language server
10706 // have been applied.
10707 format.await.unwrap();
10708 cx.assert_editor_state(
10709 &[
10710 "one", //
10711 "", //
10712 "twoˇ", //
10713 "", //
10714 "three", //
10715 "four", //
10716 "", //
10717 ]
10718 .join("\n"),
10719 );
10720
10721 // Undoing the formatting undoes the trailing whitespace removal, the
10722 // trailing newline, and the LSP edits.
10723 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10724 cx.assert_editor_state(
10725 &[
10726 "one ", //
10727 "twoˇ", //
10728 "three ", //
10729 "four", //
10730 ]
10731 .join("\n"),
10732 );
10733}
10734
10735#[gpui::test]
10736async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10737 cx: &mut TestAppContext,
10738) {
10739 init_test(cx, |_| {});
10740
10741 cx.update(|cx| {
10742 cx.update_global::<SettingsStore, _>(|settings, cx| {
10743 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10744 settings.auto_signature_help = Some(true);
10745 });
10746 });
10747 });
10748
10749 let mut cx = EditorLspTestContext::new_rust(
10750 lsp::ServerCapabilities {
10751 signature_help_provider: Some(lsp::SignatureHelpOptions {
10752 ..Default::default()
10753 }),
10754 ..Default::default()
10755 },
10756 cx,
10757 )
10758 .await;
10759
10760 let language = Language::new(
10761 LanguageConfig {
10762 name: "Rust".into(),
10763 brackets: BracketPairConfig {
10764 pairs: vec![
10765 BracketPair {
10766 start: "{".to_string(),
10767 end: "}".to_string(),
10768 close: true,
10769 surround: true,
10770 newline: true,
10771 },
10772 BracketPair {
10773 start: "(".to_string(),
10774 end: ")".to_string(),
10775 close: true,
10776 surround: true,
10777 newline: true,
10778 },
10779 BracketPair {
10780 start: "/*".to_string(),
10781 end: " */".to_string(),
10782 close: true,
10783 surround: true,
10784 newline: true,
10785 },
10786 BracketPair {
10787 start: "[".to_string(),
10788 end: "]".to_string(),
10789 close: false,
10790 surround: false,
10791 newline: true,
10792 },
10793 BracketPair {
10794 start: "\"".to_string(),
10795 end: "\"".to_string(),
10796 close: true,
10797 surround: true,
10798 newline: false,
10799 },
10800 BracketPair {
10801 start: "<".to_string(),
10802 end: ">".to_string(),
10803 close: false,
10804 surround: true,
10805 newline: true,
10806 },
10807 ],
10808 ..Default::default()
10809 },
10810 autoclose_before: "})]".to_string(),
10811 ..Default::default()
10812 },
10813 Some(tree_sitter_rust::LANGUAGE.into()),
10814 );
10815 let language = Arc::new(language);
10816
10817 cx.language_registry().add(language.clone());
10818 cx.update_buffer(|buffer, cx| {
10819 buffer.set_language(Some(language), cx);
10820 });
10821
10822 cx.set_state(
10823 &r#"
10824 fn main() {
10825 sampleˇ
10826 }
10827 "#
10828 .unindent(),
10829 );
10830
10831 cx.update_editor(|editor, window, cx| {
10832 editor.handle_input("(", window, cx);
10833 });
10834 cx.assert_editor_state(
10835 &"
10836 fn main() {
10837 sample(ˇ)
10838 }
10839 "
10840 .unindent(),
10841 );
10842
10843 let mocked_response = lsp::SignatureHelp {
10844 signatures: vec![lsp::SignatureInformation {
10845 label: "fn sample(param1: u8, param2: u8)".to_string(),
10846 documentation: None,
10847 parameters: Some(vec![
10848 lsp::ParameterInformation {
10849 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10850 documentation: None,
10851 },
10852 lsp::ParameterInformation {
10853 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10854 documentation: None,
10855 },
10856 ]),
10857 active_parameter: None,
10858 }],
10859 active_signature: Some(0),
10860 active_parameter: Some(0),
10861 };
10862 handle_signature_help_request(&mut cx, mocked_response).await;
10863
10864 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10865 .await;
10866
10867 cx.editor(|editor, _, _| {
10868 let signature_help_state = editor.signature_help_state.popover().cloned();
10869 assert_eq!(
10870 signature_help_state.unwrap().label,
10871 "param1: u8, param2: u8"
10872 );
10873 });
10874}
10875
10876#[gpui::test]
10877async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
10878 init_test(cx, |_| {});
10879
10880 cx.update(|cx| {
10881 cx.update_global::<SettingsStore, _>(|settings, cx| {
10882 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10883 settings.auto_signature_help = Some(false);
10884 settings.show_signature_help_after_edits = Some(false);
10885 });
10886 });
10887 });
10888
10889 let mut cx = EditorLspTestContext::new_rust(
10890 lsp::ServerCapabilities {
10891 signature_help_provider: Some(lsp::SignatureHelpOptions {
10892 ..Default::default()
10893 }),
10894 ..Default::default()
10895 },
10896 cx,
10897 )
10898 .await;
10899
10900 let language = Language::new(
10901 LanguageConfig {
10902 name: "Rust".into(),
10903 brackets: BracketPairConfig {
10904 pairs: vec![
10905 BracketPair {
10906 start: "{".to_string(),
10907 end: "}".to_string(),
10908 close: true,
10909 surround: true,
10910 newline: true,
10911 },
10912 BracketPair {
10913 start: "(".to_string(),
10914 end: ")".to_string(),
10915 close: true,
10916 surround: true,
10917 newline: true,
10918 },
10919 BracketPair {
10920 start: "/*".to_string(),
10921 end: " */".to_string(),
10922 close: true,
10923 surround: true,
10924 newline: true,
10925 },
10926 BracketPair {
10927 start: "[".to_string(),
10928 end: "]".to_string(),
10929 close: false,
10930 surround: false,
10931 newline: true,
10932 },
10933 BracketPair {
10934 start: "\"".to_string(),
10935 end: "\"".to_string(),
10936 close: true,
10937 surround: true,
10938 newline: false,
10939 },
10940 BracketPair {
10941 start: "<".to_string(),
10942 end: ">".to_string(),
10943 close: false,
10944 surround: true,
10945 newline: true,
10946 },
10947 ],
10948 ..Default::default()
10949 },
10950 autoclose_before: "})]".to_string(),
10951 ..Default::default()
10952 },
10953 Some(tree_sitter_rust::LANGUAGE.into()),
10954 );
10955 let language = Arc::new(language);
10956
10957 cx.language_registry().add(language.clone());
10958 cx.update_buffer(|buffer, cx| {
10959 buffer.set_language(Some(language), cx);
10960 });
10961
10962 // Ensure that signature_help is not called when no signature help is enabled.
10963 cx.set_state(
10964 &r#"
10965 fn main() {
10966 sampleˇ
10967 }
10968 "#
10969 .unindent(),
10970 );
10971 cx.update_editor(|editor, window, cx| {
10972 editor.handle_input("(", window, cx);
10973 });
10974 cx.assert_editor_state(
10975 &"
10976 fn main() {
10977 sample(ˇ)
10978 }
10979 "
10980 .unindent(),
10981 );
10982 cx.editor(|editor, _, _| {
10983 assert!(editor.signature_help_state.task().is_none());
10984 });
10985
10986 let mocked_response = lsp::SignatureHelp {
10987 signatures: vec![lsp::SignatureInformation {
10988 label: "fn sample(param1: u8, param2: u8)".to_string(),
10989 documentation: None,
10990 parameters: Some(vec![
10991 lsp::ParameterInformation {
10992 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10993 documentation: None,
10994 },
10995 lsp::ParameterInformation {
10996 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10997 documentation: None,
10998 },
10999 ]),
11000 active_parameter: None,
11001 }],
11002 active_signature: Some(0),
11003 active_parameter: Some(0),
11004 };
11005
11006 // Ensure that signature_help is called when enabled afte edits
11007 cx.update(|_, cx| {
11008 cx.update_global::<SettingsStore, _>(|settings, cx| {
11009 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11010 settings.auto_signature_help = Some(false);
11011 settings.show_signature_help_after_edits = Some(true);
11012 });
11013 });
11014 });
11015 cx.set_state(
11016 &r#"
11017 fn main() {
11018 sampleˇ
11019 }
11020 "#
11021 .unindent(),
11022 );
11023 cx.update_editor(|editor, window, cx| {
11024 editor.handle_input("(", window, cx);
11025 });
11026 cx.assert_editor_state(
11027 &"
11028 fn main() {
11029 sample(ˇ)
11030 }
11031 "
11032 .unindent(),
11033 );
11034 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11035 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11036 .await;
11037 cx.update_editor(|editor, _, _| {
11038 let signature_help_state = editor.signature_help_state.popover().cloned();
11039 assert!(signature_help_state.is_some());
11040 assert_eq!(
11041 signature_help_state.unwrap().label,
11042 "param1: u8, param2: u8"
11043 );
11044 editor.signature_help_state = SignatureHelpState::default();
11045 });
11046
11047 // Ensure that signature_help is called when auto signature help override is enabled
11048 cx.update(|_, cx| {
11049 cx.update_global::<SettingsStore, _>(|settings, cx| {
11050 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11051 settings.auto_signature_help = Some(true);
11052 settings.show_signature_help_after_edits = Some(false);
11053 });
11054 });
11055 });
11056 cx.set_state(
11057 &r#"
11058 fn main() {
11059 sampleˇ
11060 }
11061 "#
11062 .unindent(),
11063 );
11064 cx.update_editor(|editor, window, cx| {
11065 editor.handle_input("(", window, cx);
11066 });
11067 cx.assert_editor_state(
11068 &"
11069 fn main() {
11070 sample(ˇ)
11071 }
11072 "
11073 .unindent(),
11074 );
11075 handle_signature_help_request(&mut cx, mocked_response).await;
11076 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11077 .await;
11078 cx.editor(|editor, _, _| {
11079 let signature_help_state = editor.signature_help_state.popover().cloned();
11080 assert!(signature_help_state.is_some());
11081 assert_eq!(
11082 signature_help_state.unwrap().label,
11083 "param1: u8, param2: u8"
11084 );
11085 });
11086}
11087
11088#[gpui::test]
11089async fn test_signature_help(cx: &mut TestAppContext) {
11090 init_test(cx, |_| {});
11091 cx.update(|cx| {
11092 cx.update_global::<SettingsStore, _>(|settings, cx| {
11093 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11094 settings.auto_signature_help = Some(true);
11095 });
11096 });
11097 });
11098
11099 let mut cx = EditorLspTestContext::new_rust(
11100 lsp::ServerCapabilities {
11101 signature_help_provider: Some(lsp::SignatureHelpOptions {
11102 ..Default::default()
11103 }),
11104 ..Default::default()
11105 },
11106 cx,
11107 )
11108 .await;
11109
11110 // A test that directly calls `show_signature_help`
11111 cx.update_editor(|editor, window, cx| {
11112 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11113 });
11114
11115 let mocked_response = lsp::SignatureHelp {
11116 signatures: vec![lsp::SignatureInformation {
11117 label: "fn sample(param1: u8, param2: u8)".to_string(),
11118 documentation: None,
11119 parameters: Some(vec![
11120 lsp::ParameterInformation {
11121 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11122 documentation: None,
11123 },
11124 lsp::ParameterInformation {
11125 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11126 documentation: None,
11127 },
11128 ]),
11129 active_parameter: None,
11130 }],
11131 active_signature: Some(0),
11132 active_parameter: Some(0),
11133 };
11134 handle_signature_help_request(&mut cx, mocked_response).await;
11135
11136 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11137 .await;
11138
11139 cx.editor(|editor, _, _| {
11140 let signature_help_state = editor.signature_help_state.popover().cloned();
11141 assert!(signature_help_state.is_some());
11142 assert_eq!(
11143 signature_help_state.unwrap().label,
11144 "param1: u8, param2: u8"
11145 );
11146 });
11147
11148 // When exiting outside from inside the brackets, `signature_help` is closed.
11149 cx.set_state(indoc! {"
11150 fn main() {
11151 sample(ˇ);
11152 }
11153
11154 fn sample(param1: u8, param2: u8) {}
11155 "});
11156
11157 cx.update_editor(|editor, window, cx| {
11158 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11159 s.select_ranges([0..0])
11160 });
11161 });
11162
11163 let mocked_response = lsp::SignatureHelp {
11164 signatures: Vec::new(),
11165 active_signature: None,
11166 active_parameter: None,
11167 };
11168 handle_signature_help_request(&mut cx, mocked_response).await;
11169
11170 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11171 .await;
11172
11173 cx.editor(|editor, _, _| {
11174 assert!(!editor.signature_help_state.is_shown());
11175 });
11176
11177 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11178 cx.set_state(indoc! {"
11179 fn main() {
11180 sample(ˇ);
11181 }
11182
11183 fn sample(param1: u8, param2: u8) {}
11184 "});
11185
11186 let mocked_response = lsp::SignatureHelp {
11187 signatures: vec![lsp::SignatureInformation {
11188 label: "fn sample(param1: u8, param2: u8)".to_string(),
11189 documentation: None,
11190 parameters: Some(vec![
11191 lsp::ParameterInformation {
11192 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11193 documentation: None,
11194 },
11195 lsp::ParameterInformation {
11196 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11197 documentation: None,
11198 },
11199 ]),
11200 active_parameter: None,
11201 }],
11202 active_signature: Some(0),
11203 active_parameter: Some(0),
11204 };
11205 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11206 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11207 .await;
11208 cx.editor(|editor, _, _| {
11209 assert!(editor.signature_help_state.is_shown());
11210 });
11211
11212 // Restore the popover with more parameter input
11213 cx.set_state(indoc! {"
11214 fn main() {
11215 sample(param1, param2ˇ);
11216 }
11217
11218 fn sample(param1: u8, param2: u8) {}
11219 "});
11220
11221 let mocked_response = lsp::SignatureHelp {
11222 signatures: vec![lsp::SignatureInformation {
11223 label: "fn sample(param1: u8, param2: u8)".to_string(),
11224 documentation: None,
11225 parameters: Some(vec![
11226 lsp::ParameterInformation {
11227 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11228 documentation: None,
11229 },
11230 lsp::ParameterInformation {
11231 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11232 documentation: None,
11233 },
11234 ]),
11235 active_parameter: None,
11236 }],
11237 active_signature: Some(0),
11238 active_parameter: Some(1),
11239 };
11240 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11241 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11242 .await;
11243
11244 // When selecting a range, the popover is gone.
11245 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11246 cx.update_editor(|editor, window, cx| {
11247 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11248 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11249 })
11250 });
11251 cx.assert_editor_state(indoc! {"
11252 fn main() {
11253 sample(param1, «ˇparam2»);
11254 }
11255
11256 fn sample(param1: u8, param2: u8) {}
11257 "});
11258 cx.editor(|editor, _, _| {
11259 assert!(!editor.signature_help_state.is_shown());
11260 });
11261
11262 // When unselecting again, the popover is back if within the brackets.
11263 cx.update_editor(|editor, window, cx| {
11264 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11265 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11266 })
11267 });
11268 cx.assert_editor_state(indoc! {"
11269 fn main() {
11270 sample(param1, ˇparam2);
11271 }
11272
11273 fn sample(param1: u8, param2: u8) {}
11274 "});
11275 handle_signature_help_request(&mut cx, mocked_response).await;
11276 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11277 .await;
11278 cx.editor(|editor, _, _| {
11279 assert!(editor.signature_help_state.is_shown());
11280 });
11281
11282 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11283 cx.update_editor(|editor, window, cx| {
11284 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11285 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11286 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11287 })
11288 });
11289 cx.assert_editor_state(indoc! {"
11290 fn main() {
11291 sample(param1, ˇparam2);
11292 }
11293
11294 fn sample(param1: u8, param2: u8) {}
11295 "});
11296
11297 let mocked_response = lsp::SignatureHelp {
11298 signatures: vec![lsp::SignatureInformation {
11299 label: "fn sample(param1: u8, param2: u8)".to_string(),
11300 documentation: None,
11301 parameters: Some(vec![
11302 lsp::ParameterInformation {
11303 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11304 documentation: None,
11305 },
11306 lsp::ParameterInformation {
11307 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11308 documentation: None,
11309 },
11310 ]),
11311 active_parameter: None,
11312 }],
11313 active_signature: Some(0),
11314 active_parameter: Some(1),
11315 };
11316 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11317 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11318 .await;
11319 cx.update_editor(|editor, _, cx| {
11320 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11321 });
11322 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11323 .await;
11324 cx.update_editor(|editor, window, cx| {
11325 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11326 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11327 })
11328 });
11329 cx.assert_editor_state(indoc! {"
11330 fn main() {
11331 sample(param1, «ˇparam2»);
11332 }
11333
11334 fn sample(param1: u8, param2: u8) {}
11335 "});
11336 cx.update_editor(|editor, window, cx| {
11337 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11338 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11339 })
11340 });
11341 cx.assert_editor_state(indoc! {"
11342 fn main() {
11343 sample(param1, ˇparam2);
11344 }
11345
11346 fn sample(param1: u8, param2: u8) {}
11347 "});
11348 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11349 .await;
11350}
11351
11352#[gpui::test]
11353async fn test_completion_mode(cx: &mut TestAppContext) {
11354 init_test(cx, |_| {});
11355 let mut cx = EditorLspTestContext::new_rust(
11356 lsp::ServerCapabilities {
11357 completion_provider: Some(lsp::CompletionOptions {
11358 resolve_provider: Some(true),
11359 ..Default::default()
11360 }),
11361 ..Default::default()
11362 },
11363 cx,
11364 )
11365 .await;
11366
11367 struct Run {
11368 run_description: &'static str,
11369 initial_state: String,
11370 buffer_marked_text: String,
11371 completion_label: &'static str,
11372 completion_text: &'static str,
11373 expected_with_insert_mode: String,
11374 expected_with_replace_mode: String,
11375 expected_with_replace_subsequence_mode: String,
11376 expected_with_replace_suffix_mode: String,
11377 }
11378
11379 let runs = [
11380 Run {
11381 run_description: "Start of word matches completion text",
11382 initial_state: "before ediˇ after".into(),
11383 buffer_marked_text: "before <edi|> after".into(),
11384 completion_label: "editor",
11385 completion_text: "editor",
11386 expected_with_insert_mode: "before editorˇ after".into(),
11387 expected_with_replace_mode: "before editorˇ after".into(),
11388 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11389 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11390 },
11391 Run {
11392 run_description: "Accept same text at the middle of the word",
11393 initial_state: "before ediˇtor after".into(),
11394 buffer_marked_text: "before <edi|tor> after".into(),
11395 completion_label: "editor",
11396 completion_text: "editor",
11397 expected_with_insert_mode: "before editorˇtor after".into(),
11398 expected_with_replace_mode: "before editorˇ after".into(),
11399 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11400 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11401 },
11402 Run {
11403 run_description: "End of word matches completion text -- cursor at end",
11404 initial_state: "before torˇ after".into(),
11405 buffer_marked_text: "before <tor|> after".into(),
11406 completion_label: "editor",
11407 completion_text: "editor",
11408 expected_with_insert_mode: "before editorˇ after".into(),
11409 expected_with_replace_mode: "before editorˇ after".into(),
11410 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11411 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11412 },
11413 Run {
11414 run_description: "End of word matches completion text -- cursor at start",
11415 initial_state: "before ˇtor after".into(),
11416 buffer_marked_text: "before <|tor> after".into(),
11417 completion_label: "editor",
11418 completion_text: "editor",
11419 expected_with_insert_mode: "before editorˇtor after".into(),
11420 expected_with_replace_mode: "before editorˇ after".into(),
11421 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11422 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11423 },
11424 Run {
11425 run_description: "Prepend text containing whitespace",
11426 initial_state: "pˇfield: bool".into(),
11427 buffer_marked_text: "<p|field>: bool".into(),
11428 completion_label: "pub ",
11429 completion_text: "pub ",
11430 expected_with_insert_mode: "pub ˇfield: bool".into(),
11431 expected_with_replace_mode: "pub ˇ: bool".into(),
11432 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11433 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11434 },
11435 Run {
11436 run_description: "Add element to start of list",
11437 initial_state: "[element_ˇelement_2]".into(),
11438 buffer_marked_text: "[<element_|element_2>]".into(),
11439 completion_label: "element_1",
11440 completion_text: "element_1",
11441 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11442 expected_with_replace_mode: "[element_1ˇ]".into(),
11443 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11444 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11445 },
11446 Run {
11447 run_description: "Add element to start of list -- first and second elements are equal",
11448 initial_state: "[elˇelement]".into(),
11449 buffer_marked_text: "[<el|element>]".into(),
11450 completion_label: "element",
11451 completion_text: "element",
11452 expected_with_insert_mode: "[elementˇelement]".into(),
11453 expected_with_replace_mode: "[elementˇ]".into(),
11454 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11455 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11456 },
11457 Run {
11458 run_description: "Ends with matching suffix",
11459 initial_state: "SubˇError".into(),
11460 buffer_marked_text: "<Sub|Error>".into(),
11461 completion_label: "SubscriptionError",
11462 completion_text: "SubscriptionError",
11463 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11464 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11465 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11466 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11467 },
11468 Run {
11469 run_description: "Suffix is a subsequence -- contiguous",
11470 initial_state: "SubˇErr".into(),
11471 buffer_marked_text: "<Sub|Err>".into(),
11472 completion_label: "SubscriptionError",
11473 completion_text: "SubscriptionError",
11474 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11475 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11476 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11477 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11478 },
11479 Run {
11480 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11481 initial_state: "Suˇscrirr".into(),
11482 buffer_marked_text: "<Su|scrirr>".into(),
11483 completion_label: "SubscriptionError",
11484 completion_text: "SubscriptionError",
11485 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11486 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11487 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11488 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11489 },
11490 Run {
11491 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11492 initial_state: "foo(indˇix)".into(),
11493 buffer_marked_text: "foo(<ind|ix>)".into(),
11494 completion_label: "node_index",
11495 completion_text: "node_index",
11496 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11497 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11498 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11499 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11500 },
11501 Run {
11502 run_description: "Replace range ends before cursor - should extend to cursor",
11503 initial_state: "before editˇo after".into(),
11504 buffer_marked_text: "before <{ed}>it|o after".into(),
11505 completion_label: "editor",
11506 completion_text: "editor",
11507 expected_with_insert_mode: "before editorˇo after".into(),
11508 expected_with_replace_mode: "before editorˇo after".into(),
11509 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11510 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11511 },
11512 Run {
11513 run_description: "Uses label for suffix matching",
11514 initial_state: "before ediˇtor after".into(),
11515 buffer_marked_text: "before <edi|tor> after".into(),
11516 completion_label: "editor",
11517 completion_text: "editor()",
11518 expected_with_insert_mode: "before editor()ˇtor after".into(),
11519 expected_with_replace_mode: "before editor()ˇ after".into(),
11520 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11521 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11522 },
11523 Run {
11524 run_description: "Case insensitive subsequence and suffix matching",
11525 initial_state: "before EDiˇtoR after".into(),
11526 buffer_marked_text: "before <EDi|toR> after".into(),
11527 completion_label: "editor",
11528 completion_text: "editor",
11529 expected_with_insert_mode: "before editorˇtoR after".into(),
11530 expected_with_replace_mode: "before editorˇ after".into(),
11531 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11532 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11533 },
11534 ];
11535
11536 for run in runs {
11537 let run_variations = [
11538 (LspInsertMode::Insert, run.expected_with_insert_mode),
11539 (LspInsertMode::Replace, run.expected_with_replace_mode),
11540 (
11541 LspInsertMode::ReplaceSubsequence,
11542 run.expected_with_replace_subsequence_mode,
11543 ),
11544 (
11545 LspInsertMode::ReplaceSuffix,
11546 run.expected_with_replace_suffix_mode,
11547 ),
11548 ];
11549
11550 for (lsp_insert_mode, expected_text) in run_variations {
11551 eprintln!(
11552 "run = {:?}, mode = {lsp_insert_mode:.?}",
11553 run.run_description,
11554 );
11555
11556 update_test_language_settings(&mut cx, |settings| {
11557 settings.defaults.completions = Some(CompletionSettings {
11558 lsp_insert_mode,
11559 words: WordsCompletionMode::Disabled,
11560 lsp: true,
11561 lsp_fetch_timeout_ms: 0,
11562 });
11563 });
11564
11565 cx.set_state(&run.initial_state);
11566 cx.update_editor(|editor, window, cx| {
11567 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11568 });
11569
11570 let counter = Arc::new(AtomicUsize::new(0));
11571 handle_completion_request_with_insert_and_replace(
11572 &mut cx,
11573 &run.buffer_marked_text,
11574 vec![(run.completion_label, run.completion_text)],
11575 counter.clone(),
11576 )
11577 .await;
11578 cx.condition(|editor, _| editor.context_menu_visible())
11579 .await;
11580 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11581
11582 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11583 editor
11584 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11585 .unwrap()
11586 });
11587 cx.assert_editor_state(&expected_text);
11588 handle_resolve_completion_request(&mut cx, None).await;
11589 apply_additional_edits.await.unwrap();
11590 }
11591 }
11592}
11593
11594#[gpui::test]
11595async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11596 init_test(cx, |_| {});
11597 let mut cx = EditorLspTestContext::new_rust(
11598 lsp::ServerCapabilities {
11599 completion_provider: Some(lsp::CompletionOptions {
11600 resolve_provider: Some(true),
11601 ..Default::default()
11602 }),
11603 ..Default::default()
11604 },
11605 cx,
11606 )
11607 .await;
11608
11609 let initial_state = "SubˇError";
11610 let buffer_marked_text = "<Sub|Error>";
11611 let completion_text = "SubscriptionError";
11612 let expected_with_insert_mode = "SubscriptionErrorˇError";
11613 let expected_with_replace_mode = "SubscriptionErrorˇ";
11614
11615 update_test_language_settings(&mut cx, |settings| {
11616 settings.defaults.completions = Some(CompletionSettings {
11617 words: WordsCompletionMode::Disabled,
11618 // set the opposite here to ensure that the action is overriding the default behavior
11619 lsp_insert_mode: LspInsertMode::Insert,
11620 lsp: true,
11621 lsp_fetch_timeout_ms: 0,
11622 });
11623 });
11624
11625 cx.set_state(initial_state);
11626 cx.update_editor(|editor, window, cx| {
11627 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11628 });
11629
11630 let counter = Arc::new(AtomicUsize::new(0));
11631 handle_completion_request_with_insert_and_replace(
11632 &mut cx,
11633 &buffer_marked_text,
11634 vec![(completion_text, completion_text)],
11635 counter.clone(),
11636 )
11637 .await;
11638 cx.condition(|editor, _| editor.context_menu_visible())
11639 .await;
11640 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11641
11642 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11643 editor
11644 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11645 .unwrap()
11646 });
11647 cx.assert_editor_state(&expected_with_replace_mode);
11648 handle_resolve_completion_request(&mut cx, None).await;
11649 apply_additional_edits.await.unwrap();
11650
11651 update_test_language_settings(&mut cx, |settings| {
11652 settings.defaults.completions = Some(CompletionSettings {
11653 words: WordsCompletionMode::Disabled,
11654 // set the opposite here to ensure that the action is overriding the default behavior
11655 lsp_insert_mode: LspInsertMode::Replace,
11656 lsp: true,
11657 lsp_fetch_timeout_ms: 0,
11658 });
11659 });
11660
11661 cx.set_state(initial_state);
11662 cx.update_editor(|editor, window, cx| {
11663 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11664 });
11665 handle_completion_request_with_insert_and_replace(
11666 &mut cx,
11667 &buffer_marked_text,
11668 vec![(completion_text, completion_text)],
11669 counter.clone(),
11670 )
11671 .await;
11672 cx.condition(|editor, _| editor.context_menu_visible())
11673 .await;
11674 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11675
11676 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11677 editor
11678 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
11679 .unwrap()
11680 });
11681 cx.assert_editor_state(&expected_with_insert_mode);
11682 handle_resolve_completion_request(&mut cx, None).await;
11683 apply_additional_edits.await.unwrap();
11684}
11685
11686#[gpui::test]
11687async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
11688 init_test(cx, |_| {});
11689 let mut cx = EditorLspTestContext::new_rust(
11690 lsp::ServerCapabilities {
11691 completion_provider: Some(lsp::CompletionOptions {
11692 resolve_provider: Some(true),
11693 ..Default::default()
11694 }),
11695 ..Default::default()
11696 },
11697 cx,
11698 )
11699 .await;
11700
11701 // scenario: surrounding text matches completion text
11702 let completion_text = "to_offset";
11703 let initial_state = indoc! {"
11704 1. buf.to_offˇsuffix
11705 2. buf.to_offˇsuf
11706 3. buf.to_offˇfix
11707 4. buf.to_offˇ
11708 5. into_offˇensive
11709 6. ˇsuffix
11710 7. let ˇ //
11711 8. aaˇzz
11712 9. buf.to_off«zzzzzˇ»suffix
11713 10. buf.«ˇzzzzz»suffix
11714 11. to_off«ˇzzzzz»
11715
11716 buf.to_offˇsuffix // newest cursor
11717 "};
11718 let completion_marked_buffer = indoc! {"
11719 1. buf.to_offsuffix
11720 2. buf.to_offsuf
11721 3. buf.to_offfix
11722 4. buf.to_off
11723 5. into_offensive
11724 6. suffix
11725 7. let //
11726 8. aazz
11727 9. buf.to_offzzzzzsuffix
11728 10. buf.zzzzzsuffix
11729 11. to_offzzzzz
11730
11731 buf.<to_off|suffix> // newest cursor
11732 "};
11733 let expected = indoc! {"
11734 1. buf.to_offsetˇ
11735 2. buf.to_offsetˇsuf
11736 3. buf.to_offsetˇfix
11737 4. buf.to_offsetˇ
11738 5. into_offsetˇensive
11739 6. to_offsetˇsuffix
11740 7. let to_offsetˇ //
11741 8. aato_offsetˇzz
11742 9. buf.to_offsetˇ
11743 10. buf.to_offsetˇsuffix
11744 11. to_offsetˇ
11745
11746 buf.to_offsetˇ // newest cursor
11747 "};
11748 cx.set_state(initial_state);
11749 cx.update_editor(|editor, window, cx| {
11750 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11751 });
11752 handle_completion_request_with_insert_and_replace(
11753 &mut cx,
11754 completion_marked_buffer,
11755 vec![(completion_text, completion_text)],
11756 Arc::new(AtomicUsize::new(0)),
11757 )
11758 .await;
11759 cx.condition(|editor, _| editor.context_menu_visible())
11760 .await;
11761 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11762 editor
11763 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11764 .unwrap()
11765 });
11766 cx.assert_editor_state(expected);
11767 handle_resolve_completion_request(&mut cx, None).await;
11768 apply_additional_edits.await.unwrap();
11769
11770 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
11771 let completion_text = "foo_and_bar";
11772 let initial_state = indoc! {"
11773 1. ooanbˇ
11774 2. zooanbˇ
11775 3. ooanbˇz
11776 4. zooanbˇz
11777 5. ooanˇ
11778 6. oanbˇ
11779
11780 ooanbˇ
11781 "};
11782 let completion_marked_buffer = indoc! {"
11783 1. ooanb
11784 2. zooanb
11785 3. ooanbz
11786 4. zooanbz
11787 5. ooan
11788 6. oanb
11789
11790 <ooanb|>
11791 "};
11792 let expected = indoc! {"
11793 1. foo_and_barˇ
11794 2. zfoo_and_barˇ
11795 3. foo_and_barˇz
11796 4. zfoo_and_barˇz
11797 5. ooanfoo_and_barˇ
11798 6. oanbfoo_and_barˇ
11799
11800 foo_and_barˇ
11801 "};
11802 cx.set_state(initial_state);
11803 cx.update_editor(|editor, window, cx| {
11804 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11805 });
11806 handle_completion_request_with_insert_and_replace(
11807 &mut cx,
11808 completion_marked_buffer,
11809 vec![(completion_text, completion_text)],
11810 Arc::new(AtomicUsize::new(0)),
11811 )
11812 .await;
11813 cx.condition(|editor, _| editor.context_menu_visible())
11814 .await;
11815 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11816 editor
11817 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11818 .unwrap()
11819 });
11820 cx.assert_editor_state(expected);
11821 handle_resolve_completion_request(&mut cx, None).await;
11822 apply_additional_edits.await.unwrap();
11823
11824 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
11825 // (expects the same as if it was inserted at the end)
11826 let completion_text = "foo_and_bar";
11827 let initial_state = indoc! {"
11828 1. ooˇanb
11829 2. zooˇanb
11830 3. ooˇanbz
11831 4. zooˇanbz
11832
11833 ooˇanb
11834 "};
11835 let completion_marked_buffer = indoc! {"
11836 1. ooanb
11837 2. zooanb
11838 3. ooanbz
11839 4. zooanbz
11840
11841 <oo|anb>
11842 "};
11843 let expected = indoc! {"
11844 1. foo_and_barˇ
11845 2. zfoo_and_barˇ
11846 3. foo_and_barˇz
11847 4. zfoo_and_barˇz
11848
11849 foo_and_barˇ
11850 "};
11851 cx.set_state(initial_state);
11852 cx.update_editor(|editor, window, cx| {
11853 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11854 });
11855 handle_completion_request_with_insert_and_replace(
11856 &mut cx,
11857 completion_marked_buffer,
11858 vec![(completion_text, completion_text)],
11859 Arc::new(AtomicUsize::new(0)),
11860 )
11861 .await;
11862 cx.condition(|editor, _| editor.context_menu_visible())
11863 .await;
11864 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11865 editor
11866 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11867 .unwrap()
11868 });
11869 cx.assert_editor_state(expected);
11870 handle_resolve_completion_request(&mut cx, None).await;
11871 apply_additional_edits.await.unwrap();
11872}
11873
11874// This used to crash
11875#[gpui::test]
11876async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
11877 init_test(cx, |_| {});
11878
11879 let buffer_text = indoc! {"
11880 fn main() {
11881 10.satu;
11882
11883 //
11884 // separate cursors so they open in different excerpts (manually reproducible)
11885 //
11886
11887 10.satu20;
11888 }
11889 "};
11890 let multibuffer_text_with_selections = indoc! {"
11891 fn main() {
11892 10.satuˇ;
11893
11894 //
11895
11896 //
11897
11898 10.satuˇ20;
11899 }
11900 "};
11901 let expected_multibuffer = indoc! {"
11902 fn main() {
11903 10.saturating_sub()ˇ;
11904
11905 //
11906
11907 //
11908
11909 10.saturating_sub()ˇ;
11910 }
11911 "};
11912
11913 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
11914 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
11915
11916 let fs = FakeFs::new(cx.executor());
11917 fs.insert_tree(
11918 path!("/a"),
11919 json!({
11920 "main.rs": buffer_text,
11921 }),
11922 )
11923 .await;
11924
11925 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11926 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11927 language_registry.add(rust_lang());
11928 let mut fake_servers = language_registry.register_fake_lsp(
11929 "Rust",
11930 FakeLspAdapter {
11931 capabilities: lsp::ServerCapabilities {
11932 completion_provider: Some(lsp::CompletionOptions {
11933 resolve_provider: None,
11934 ..lsp::CompletionOptions::default()
11935 }),
11936 ..lsp::ServerCapabilities::default()
11937 },
11938 ..FakeLspAdapter::default()
11939 },
11940 );
11941 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11942 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11943 let buffer = project
11944 .update(cx, |project, cx| {
11945 project.open_local_buffer(path!("/a/main.rs"), cx)
11946 })
11947 .await
11948 .unwrap();
11949
11950 let multi_buffer = cx.new(|cx| {
11951 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
11952 multi_buffer.push_excerpts(
11953 buffer.clone(),
11954 [ExcerptRange::new(0..first_excerpt_end)],
11955 cx,
11956 );
11957 multi_buffer.push_excerpts(
11958 buffer.clone(),
11959 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
11960 cx,
11961 );
11962 multi_buffer
11963 });
11964
11965 let editor = workspace
11966 .update(cx, |_, window, cx| {
11967 cx.new(|cx| {
11968 Editor::new(
11969 EditorMode::Full {
11970 scale_ui_elements_with_buffer_font_size: false,
11971 show_active_line_background: false,
11972 sized_by_content: false,
11973 },
11974 multi_buffer.clone(),
11975 Some(project.clone()),
11976 window,
11977 cx,
11978 )
11979 })
11980 })
11981 .unwrap();
11982
11983 let pane = workspace
11984 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11985 .unwrap();
11986 pane.update_in(cx, |pane, window, cx| {
11987 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
11988 });
11989
11990 let fake_server = fake_servers.next().await.unwrap();
11991
11992 editor.update_in(cx, |editor, window, cx| {
11993 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11994 s.select_ranges([
11995 Point::new(1, 11)..Point::new(1, 11),
11996 Point::new(7, 11)..Point::new(7, 11),
11997 ])
11998 });
11999
12000 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
12001 });
12002
12003 editor.update_in(cx, |editor, window, cx| {
12004 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12005 });
12006
12007 fake_server
12008 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12009 let completion_item = lsp::CompletionItem {
12010 label: "saturating_sub()".into(),
12011 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12012 lsp::InsertReplaceEdit {
12013 new_text: "saturating_sub()".to_owned(),
12014 insert: lsp::Range::new(
12015 lsp::Position::new(7, 7),
12016 lsp::Position::new(7, 11),
12017 ),
12018 replace: lsp::Range::new(
12019 lsp::Position::new(7, 7),
12020 lsp::Position::new(7, 13),
12021 ),
12022 },
12023 )),
12024 ..lsp::CompletionItem::default()
12025 };
12026
12027 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
12028 })
12029 .next()
12030 .await
12031 .unwrap();
12032
12033 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12034 .await;
12035
12036 editor
12037 .update_in(cx, |editor, window, cx| {
12038 editor
12039 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12040 .unwrap()
12041 })
12042 .await
12043 .unwrap();
12044
12045 editor.update(cx, |editor, cx| {
12046 assert_text_with_selections(editor, expected_multibuffer, cx);
12047 })
12048}
12049
12050#[gpui::test]
12051async fn test_completion(cx: &mut TestAppContext) {
12052 init_test(cx, |_| {});
12053
12054 let mut cx = EditorLspTestContext::new_rust(
12055 lsp::ServerCapabilities {
12056 completion_provider: Some(lsp::CompletionOptions {
12057 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12058 resolve_provider: Some(true),
12059 ..Default::default()
12060 }),
12061 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12062 ..Default::default()
12063 },
12064 cx,
12065 )
12066 .await;
12067 let counter = Arc::new(AtomicUsize::new(0));
12068
12069 cx.set_state(indoc! {"
12070 oneˇ
12071 two
12072 three
12073 "});
12074 cx.simulate_keystroke(".");
12075 handle_completion_request(
12076 indoc! {"
12077 one.|<>
12078 two
12079 three
12080 "},
12081 vec!["first_completion", "second_completion"],
12082 true,
12083 counter.clone(),
12084 &mut cx,
12085 )
12086 .await;
12087 cx.condition(|editor, _| editor.context_menu_visible())
12088 .await;
12089 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12090
12091 let _handler = handle_signature_help_request(
12092 &mut cx,
12093 lsp::SignatureHelp {
12094 signatures: vec![lsp::SignatureInformation {
12095 label: "test signature".to_string(),
12096 documentation: None,
12097 parameters: Some(vec![lsp::ParameterInformation {
12098 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12099 documentation: None,
12100 }]),
12101 active_parameter: None,
12102 }],
12103 active_signature: None,
12104 active_parameter: None,
12105 },
12106 );
12107 cx.update_editor(|editor, window, cx| {
12108 assert!(
12109 !editor.signature_help_state.is_shown(),
12110 "No signature help was called for"
12111 );
12112 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12113 });
12114 cx.run_until_parked();
12115 cx.update_editor(|editor, _, _| {
12116 assert!(
12117 !editor.signature_help_state.is_shown(),
12118 "No signature help should be shown when completions menu is open"
12119 );
12120 });
12121
12122 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12123 editor.context_menu_next(&Default::default(), window, cx);
12124 editor
12125 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12126 .unwrap()
12127 });
12128 cx.assert_editor_state(indoc! {"
12129 one.second_completionˇ
12130 two
12131 three
12132 "});
12133
12134 handle_resolve_completion_request(
12135 &mut cx,
12136 Some(vec![
12137 (
12138 //This overlaps with the primary completion edit which is
12139 //misbehavior from the LSP spec, test that we filter it out
12140 indoc! {"
12141 one.second_ˇcompletion
12142 two
12143 threeˇ
12144 "},
12145 "overlapping additional edit",
12146 ),
12147 (
12148 indoc! {"
12149 one.second_completion
12150 two
12151 threeˇ
12152 "},
12153 "\nadditional edit",
12154 ),
12155 ]),
12156 )
12157 .await;
12158 apply_additional_edits.await.unwrap();
12159 cx.assert_editor_state(indoc! {"
12160 one.second_completionˇ
12161 two
12162 three
12163 additional edit
12164 "});
12165
12166 cx.set_state(indoc! {"
12167 one.second_completion
12168 twoˇ
12169 threeˇ
12170 additional edit
12171 "});
12172 cx.simulate_keystroke(" ");
12173 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12174 cx.simulate_keystroke("s");
12175 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12176
12177 cx.assert_editor_state(indoc! {"
12178 one.second_completion
12179 two sˇ
12180 three sˇ
12181 additional edit
12182 "});
12183 handle_completion_request(
12184 indoc! {"
12185 one.second_completion
12186 two s
12187 three <s|>
12188 additional edit
12189 "},
12190 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12191 true,
12192 counter.clone(),
12193 &mut cx,
12194 )
12195 .await;
12196 cx.condition(|editor, _| editor.context_menu_visible())
12197 .await;
12198 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12199
12200 cx.simulate_keystroke("i");
12201
12202 handle_completion_request(
12203 indoc! {"
12204 one.second_completion
12205 two si
12206 three <si|>
12207 additional edit
12208 "},
12209 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12210 true,
12211 counter.clone(),
12212 &mut cx,
12213 )
12214 .await;
12215 cx.condition(|editor, _| editor.context_menu_visible())
12216 .await;
12217 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12218
12219 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12220 editor
12221 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12222 .unwrap()
12223 });
12224 cx.assert_editor_state(indoc! {"
12225 one.second_completion
12226 two sixth_completionˇ
12227 three sixth_completionˇ
12228 additional edit
12229 "});
12230
12231 apply_additional_edits.await.unwrap();
12232
12233 update_test_language_settings(&mut cx, |settings| {
12234 settings.defaults.show_completions_on_input = Some(false);
12235 });
12236 cx.set_state("editorˇ");
12237 cx.simulate_keystroke(".");
12238 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12239 cx.simulate_keystrokes("c l o");
12240 cx.assert_editor_state("editor.cloˇ");
12241 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12242 cx.update_editor(|editor, window, cx| {
12243 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12244 });
12245 handle_completion_request(
12246 "editor.<clo|>",
12247 vec!["close", "clobber"],
12248 true,
12249 counter.clone(),
12250 &mut cx,
12251 )
12252 .await;
12253 cx.condition(|editor, _| editor.context_menu_visible())
12254 .await;
12255 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12256
12257 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12258 editor
12259 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12260 .unwrap()
12261 });
12262 cx.assert_editor_state("editor.clobberˇ");
12263 handle_resolve_completion_request(&mut cx, None).await;
12264 apply_additional_edits.await.unwrap();
12265}
12266
12267#[gpui::test]
12268async fn test_completion_reuse(cx: &mut TestAppContext) {
12269 init_test(cx, |_| {});
12270
12271 let mut cx = EditorLspTestContext::new_rust(
12272 lsp::ServerCapabilities {
12273 completion_provider: Some(lsp::CompletionOptions {
12274 trigger_characters: Some(vec![".".to_string()]),
12275 ..Default::default()
12276 }),
12277 ..Default::default()
12278 },
12279 cx,
12280 )
12281 .await;
12282
12283 let counter = Arc::new(AtomicUsize::new(0));
12284 cx.set_state("objˇ");
12285 cx.simulate_keystroke(".");
12286
12287 // Initial completion request returns complete results
12288 let is_incomplete = false;
12289 handle_completion_request(
12290 "obj.|<>",
12291 vec!["a", "ab", "abc"],
12292 is_incomplete,
12293 counter.clone(),
12294 &mut cx,
12295 )
12296 .await;
12297 cx.run_until_parked();
12298 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12299 cx.assert_editor_state("obj.ˇ");
12300 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12301
12302 // Type "a" - filters existing completions
12303 cx.simulate_keystroke("a");
12304 cx.run_until_parked();
12305 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12306 cx.assert_editor_state("obj.aˇ");
12307 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12308
12309 // Type "b" - filters existing completions
12310 cx.simulate_keystroke("b");
12311 cx.run_until_parked();
12312 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12313 cx.assert_editor_state("obj.abˇ");
12314 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12315
12316 // Type "c" - filters existing completions
12317 cx.simulate_keystroke("c");
12318 cx.run_until_parked();
12319 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12320 cx.assert_editor_state("obj.abcˇ");
12321 check_displayed_completions(vec!["abc"], &mut cx);
12322
12323 // Backspace to delete "c" - filters existing completions
12324 cx.update_editor(|editor, window, cx| {
12325 editor.backspace(&Backspace, window, cx);
12326 });
12327 cx.run_until_parked();
12328 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12329 cx.assert_editor_state("obj.abˇ");
12330 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12331
12332 // Moving cursor to the left dismisses menu.
12333 cx.update_editor(|editor, window, cx| {
12334 editor.move_left(&MoveLeft, window, cx);
12335 });
12336 cx.run_until_parked();
12337 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12338 cx.assert_editor_state("obj.aˇb");
12339 cx.update_editor(|editor, _, _| {
12340 assert_eq!(editor.context_menu_visible(), false);
12341 });
12342
12343 // Type "b" - new request
12344 cx.simulate_keystroke("b");
12345 let is_incomplete = false;
12346 handle_completion_request(
12347 "obj.<ab|>a",
12348 vec!["ab", "abc"],
12349 is_incomplete,
12350 counter.clone(),
12351 &mut cx,
12352 )
12353 .await;
12354 cx.run_until_parked();
12355 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12356 cx.assert_editor_state("obj.abˇb");
12357 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12358
12359 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12360 cx.update_editor(|editor, window, cx| {
12361 editor.backspace(&Backspace, window, cx);
12362 });
12363 let is_incomplete = false;
12364 handle_completion_request(
12365 "obj.<a|>b",
12366 vec!["a", "ab", "abc"],
12367 is_incomplete,
12368 counter.clone(),
12369 &mut cx,
12370 )
12371 .await;
12372 cx.run_until_parked();
12373 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12374 cx.assert_editor_state("obj.aˇb");
12375 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12376
12377 // Backspace to delete "a" - dismisses menu.
12378 cx.update_editor(|editor, window, cx| {
12379 editor.backspace(&Backspace, window, cx);
12380 });
12381 cx.run_until_parked();
12382 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12383 cx.assert_editor_state("obj.ˇb");
12384 cx.update_editor(|editor, _, _| {
12385 assert_eq!(editor.context_menu_visible(), false);
12386 });
12387}
12388
12389#[gpui::test]
12390async fn test_word_completion(cx: &mut TestAppContext) {
12391 let lsp_fetch_timeout_ms = 10;
12392 init_test(cx, |language_settings| {
12393 language_settings.defaults.completions = Some(CompletionSettings {
12394 words: WordsCompletionMode::Fallback,
12395 lsp: true,
12396 lsp_fetch_timeout_ms: 10,
12397 lsp_insert_mode: LspInsertMode::Insert,
12398 });
12399 });
12400
12401 let mut cx = EditorLspTestContext::new_rust(
12402 lsp::ServerCapabilities {
12403 completion_provider: Some(lsp::CompletionOptions {
12404 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12405 ..lsp::CompletionOptions::default()
12406 }),
12407 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12408 ..lsp::ServerCapabilities::default()
12409 },
12410 cx,
12411 )
12412 .await;
12413
12414 let throttle_completions = Arc::new(AtomicBool::new(false));
12415
12416 let lsp_throttle_completions = throttle_completions.clone();
12417 let _completion_requests_handler =
12418 cx.lsp
12419 .server
12420 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12421 let lsp_throttle_completions = lsp_throttle_completions.clone();
12422 let cx = cx.clone();
12423 async move {
12424 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12425 cx.background_executor()
12426 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12427 .await;
12428 }
12429 Ok(Some(lsp::CompletionResponse::Array(vec![
12430 lsp::CompletionItem {
12431 label: "first".into(),
12432 ..lsp::CompletionItem::default()
12433 },
12434 lsp::CompletionItem {
12435 label: "last".into(),
12436 ..lsp::CompletionItem::default()
12437 },
12438 ])))
12439 }
12440 });
12441
12442 cx.set_state(indoc! {"
12443 oneˇ
12444 two
12445 three
12446 "});
12447 cx.simulate_keystroke(".");
12448 cx.executor().run_until_parked();
12449 cx.condition(|editor, _| editor.context_menu_visible())
12450 .await;
12451 cx.update_editor(|editor, window, cx| {
12452 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12453 {
12454 assert_eq!(
12455 completion_menu_entries(&menu),
12456 &["first", "last"],
12457 "When LSP server is fast to reply, no fallback word completions are used"
12458 );
12459 } else {
12460 panic!("expected completion menu to be open");
12461 }
12462 editor.cancel(&Cancel, window, cx);
12463 });
12464 cx.executor().run_until_parked();
12465 cx.condition(|editor, _| !editor.context_menu_visible())
12466 .await;
12467
12468 throttle_completions.store(true, atomic::Ordering::Release);
12469 cx.simulate_keystroke(".");
12470 cx.executor()
12471 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12472 cx.executor().run_until_parked();
12473 cx.condition(|editor, _| editor.context_menu_visible())
12474 .await;
12475 cx.update_editor(|editor, _, _| {
12476 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12477 {
12478 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12479 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12480 } else {
12481 panic!("expected completion menu to be open");
12482 }
12483 });
12484}
12485
12486#[gpui::test]
12487async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12488 init_test(cx, |language_settings| {
12489 language_settings.defaults.completions = Some(CompletionSettings {
12490 words: WordsCompletionMode::Enabled,
12491 lsp: true,
12492 lsp_fetch_timeout_ms: 0,
12493 lsp_insert_mode: LspInsertMode::Insert,
12494 });
12495 });
12496
12497 let mut cx = EditorLspTestContext::new_rust(
12498 lsp::ServerCapabilities {
12499 completion_provider: Some(lsp::CompletionOptions {
12500 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12501 ..lsp::CompletionOptions::default()
12502 }),
12503 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12504 ..lsp::ServerCapabilities::default()
12505 },
12506 cx,
12507 )
12508 .await;
12509
12510 let _completion_requests_handler =
12511 cx.lsp
12512 .server
12513 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12514 Ok(Some(lsp::CompletionResponse::Array(vec![
12515 lsp::CompletionItem {
12516 label: "first".into(),
12517 ..lsp::CompletionItem::default()
12518 },
12519 lsp::CompletionItem {
12520 label: "last".into(),
12521 ..lsp::CompletionItem::default()
12522 },
12523 ])))
12524 });
12525
12526 cx.set_state(indoc! {"ˇ
12527 first
12528 last
12529 second
12530 "});
12531 cx.simulate_keystroke(".");
12532 cx.executor().run_until_parked();
12533 cx.condition(|editor, _| editor.context_menu_visible())
12534 .await;
12535 cx.update_editor(|editor, _, _| {
12536 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12537 {
12538 assert_eq!(
12539 completion_menu_entries(&menu),
12540 &["first", "last", "second"],
12541 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12542 );
12543 } else {
12544 panic!("expected completion menu to be open");
12545 }
12546 });
12547}
12548
12549#[gpui::test]
12550async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12551 init_test(cx, |language_settings| {
12552 language_settings.defaults.completions = Some(CompletionSettings {
12553 words: WordsCompletionMode::Disabled,
12554 lsp: true,
12555 lsp_fetch_timeout_ms: 0,
12556 lsp_insert_mode: LspInsertMode::Insert,
12557 });
12558 });
12559
12560 let mut cx = EditorLspTestContext::new_rust(
12561 lsp::ServerCapabilities {
12562 completion_provider: Some(lsp::CompletionOptions {
12563 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12564 ..lsp::CompletionOptions::default()
12565 }),
12566 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12567 ..lsp::ServerCapabilities::default()
12568 },
12569 cx,
12570 )
12571 .await;
12572
12573 let _completion_requests_handler =
12574 cx.lsp
12575 .server
12576 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12577 panic!("LSP completions should not be queried when dealing with word completions")
12578 });
12579
12580 cx.set_state(indoc! {"ˇ
12581 first
12582 last
12583 second
12584 "});
12585 cx.update_editor(|editor, window, cx| {
12586 editor.show_word_completions(&ShowWordCompletions, window, cx);
12587 });
12588 cx.executor().run_until_parked();
12589 cx.condition(|editor, _| editor.context_menu_visible())
12590 .await;
12591 cx.update_editor(|editor, _, _| {
12592 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12593 {
12594 assert_eq!(
12595 completion_menu_entries(&menu),
12596 &["first", "last", "second"],
12597 "`ShowWordCompletions` action should show word completions"
12598 );
12599 } else {
12600 panic!("expected completion menu to be open");
12601 }
12602 });
12603
12604 cx.simulate_keystroke("l");
12605 cx.executor().run_until_parked();
12606 cx.condition(|editor, _| editor.context_menu_visible())
12607 .await;
12608 cx.update_editor(|editor, _, _| {
12609 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12610 {
12611 assert_eq!(
12612 completion_menu_entries(&menu),
12613 &["last"],
12614 "After showing word completions, further editing should filter them and not query the LSP"
12615 );
12616 } else {
12617 panic!("expected completion menu to be open");
12618 }
12619 });
12620}
12621
12622#[gpui::test]
12623async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12624 init_test(cx, |language_settings| {
12625 language_settings.defaults.completions = Some(CompletionSettings {
12626 words: WordsCompletionMode::Fallback,
12627 lsp: false,
12628 lsp_fetch_timeout_ms: 0,
12629 lsp_insert_mode: LspInsertMode::Insert,
12630 });
12631 });
12632
12633 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12634
12635 cx.set_state(indoc! {"ˇ
12636 0_usize
12637 let
12638 33
12639 4.5f32
12640 "});
12641 cx.update_editor(|editor, window, cx| {
12642 editor.show_completions(&ShowCompletions::default(), window, cx);
12643 });
12644 cx.executor().run_until_parked();
12645 cx.condition(|editor, _| editor.context_menu_visible())
12646 .await;
12647 cx.update_editor(|editor, window, cx| {
12648 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12649 {
12650 assert_eq!(
12651 completion_menu_entries(&menu),
12652 &["let"],
12653 "With no digits in the completion query, no digits should be in the word completions"
12654 );
12655 } else {
12656 panic!("expected completion menu to be open");
12657 }
12658 editor.cancel(&Cancel, window, cx);
12659 });
12660
12661 cx.set_state(indoc! {"3ˇ
12662 0_usize
12663 let
12664 3
12665 33.35f32
12666 "});
12667 cx.update_editor(|editor, window, cx| {
12668 editor.show_completions(&ShowCompletions::default(), window, cx);
12669 });
12670 cx.executor().run_until_parked();
12671 cx.condition(|editor, _| editor.context_menu_visible())
12672 .await;
12673 cx.update_editor(|editor, _, _| {
12674 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12675 {
12676 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
12677 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
12678 } else {
12679 panic!("expected completion menu to be open");
12680 }
12681 });
12682}
12683
12684fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
12685 let position = || lsp::Position {
12686 line: params.text_document_position.position.line,
12687 character: params.text_document_position.position.character,
12688 };
12689 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12690 range: lsp::Range {
12691 start: position(),
12692 end: position(),
12693 },
12694 new_text: text.to_string(),
12695 }))
12696}
12697
12698#[gpui::test]
12699async fn test_multiline_completion(cx: &mut TestAppContext) {
12700 init_test(cx, |_| {});
12701
12702 let fs = FakeFs::new(cx.executor());
12703 fs.insert_tree(
12704 path!("/a"),
12705 json!({
12706 "main.ts": "a",
12707 }),
12708 )
12709 .await;
12710
12711 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12712 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12713 let typescript_language = Arc::new(Language::new(
12714 LanguageConfig {
12715 name: "TypeScript".into(),
12716 matcher: LanguageMatcher {
12717 path_suffixes: vec!["ts".to_string()],
12718 ..LanguageMatcher::default()
12719 },
12720 line_comments: vec!["// ".into()],
12721 ..LanguageConfig::default()
12722 },
12723 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12724 ));
12725 language_registry.add(typescript_language.clone());
12726 let mut fake_servers = language_registry.register_fake_lsp(
12727 "TypeScript",
12728 FakeLspAdapter {
12729 capabilities: lsp::ServerCapabilities {
12730 completion_provider: Some(lsp::CompletionOptions {
12731 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12732 ..lsp::CompletionOptions::default()
12733 }),
12734 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12735 ..lsp::ServerCapabilities::default()
12736 },
12737 // Emulate vtsls label generation
12738 label_for_completion: Some(Box::new(|item, _| {
12739 let text = if let Some(description) = item
12740 .label_details
12741 .as_ref()
12742 .and_then(|label_details| label_details.description.as_ref())
12743 {
12744 format!("{} {}", item.label, description)
12745 } else if let Some(detail) = &item.detail {
12746 format!("{} {}", item.label, detail)
12747 } else {
12748 item.label.clone()
12749 };
12750 let len = text.len();
12751 Some(language::CodeLabel {
12752 text,
12753 runs: Vec::new(),
12754 filter_range: 0..len,
12755 })
12756 })),
12757 ..FakeLspAdapter::default()
12758 },
12759 );
12760 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12761 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12762 let worktree_id = workspace
12763 .update(cx, |workspace, _window, cx| {
12764 workspace.project().update(cx, |project, cx| {
12765 project.worktrees(cx).next().unwrap().read(cx).id()
12766 })
12767 })
12768 .unwrap();
12769 let _buffer = project
12770 .update(cx, |project, cx| {
12771 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
12772 })
12773 .await
12774 .unwrap();
12775 let editor = workspace
12776 .update(cx, |workspace, window, cx| {
12777 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
12778 })
12779 .unwrap()
12780 .await
12781 .unwrap()
12782 .downcast::<Editor>()
12783 .unwrap();
12784 let fake_server = fake_servers.next().await.unwrap();
12785
12786 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
12787 let multiline_label_2 = "a\nb\nc\n";
12788 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
12789 let multiline_description = "d\ne\nf\n";
12790 let multiline_detail_2 = "g\nh\ni\n";
12791
12792 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
12793 move |params, _| async move {
12794 Ok(Some(lsp::CompletionResponse::Array(vec![
12795 lsp::CompletionItem {
12796 label: multiline_label.to_string(),
12797 text_edit: gen_text_edit(¶ms, "new_text_1"),
12798 ..lsp::CompletionItem::default()
12799 },
12800 lsp::CompletionItem {
12801 label: "single line label 1".to_string(),
12802 detail: Some(multiline_detail.to_string()),
12803 text_edit: gen_text_edit(¶ms, "new_text_2"),
12804 ..lsp::CompletionItem::default()
12805 },
12806 lsp::CompletionItem {
12807 label: "single line label 2".to_string(),
12808 label_details: Some(lsp::CompletionItemLabelDetails {
12809 description: Some(multiline_description.to_string()),
12810 detail: None,
12811 }),
12812 text_edit: gen_text_edit(¶ms, "new_text_2"),
12813 ..lsp::CompletionItem::default()
12814 },
12815 lsp::CompletionItem {
12816 label: multiline_label_2.to_string(),
12817 detail: Some(multiline_detail_2.to_string()),
12818 text_edit: gen_text_edit(¶ms, "new_text_3"),
12819 ..lsp::CompletionItem::default()
12820 },
12821 lsp::CompletionItem {
12822 label: "Label with many spaces and \t but without newlines".to_string(),
12823 detail: Some(
12824 "Details with many spaces and \t but without newlines".to_string(),
12825 ),
12826 text_edit: gen_text_edit(¶ms, "new_text_4"),
12827 ..lsp::CompletionItem::default()
12828 },
12829 ])))
12830 },
12831 );
12832
12833 editor.update_in(cx, |editor, window, cx| {
12834 cx.focus_self(window);
12835 editor.move_to_end(&MoveToEnd, window, cx);
12836 editor.handle_input(".", window, cx);
12837 });
12838 cx.run_until_parked();
12839 completion_handle.next().await.unwrap();
12840
12841 editor.update(cx, |editor, _| {
12842 assert!(editor.context_menu_visible());
12843 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12844 {
12845 let completion_labels = menu
12846 .completions
12847 .borrow()
12848 .iter()
12849 .map(|c| c.label.text.clone())
12850 .collect::<Vec<_>>();
12851 assert_eq!(
12852 completion_labels,
12853 &[
12854 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
12855 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
12856 "single line label 2 d e f ",
12857 "a b c g h i ",
12858 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
12859 ],
12860 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
12861 );
12862
12863 for completion in menu
12864 .completions
12865 .borrow()
12866 .iter() {
12867 assert_eq!(
12868 completion.label.filter_range,
12869 0..completion.label.text.len(),
12870 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
12871 );
12872 }
12873 } else {
12874 panic!("expected completion menu to be open");
12875 }
12876 });
12877}
12878
12879#[gpui::test]
12880async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
12881 init_test(cx, |_| {});
12882 let mut cx = EditorLspTestContext::new_rust(
12883 lsp::ServerCapabilities {
12884 completion_provider: Some(lsp::CompletionOptions {
12885 trigger_characters: Some(vec![".".to_string()]),
12886 ..Default::default()
12887 }),
12888 ..Default::default()
12889 },
12890 cx,
12891 )
12892 .await;
12893 cx.lsp
12894 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12895 Ok(Some(lsp::CompletionResponse::Array(vec![
12896 lsp::CompletionItem {
12897 label: "first".into(),
12898 ..Default::default()
12899 },
12900 lsp::CompletionItem {
12901 label: "last".into(),
12902 ..Default::default()
12903 },
12904 ])))
12905 });
12906 cx.set_state("variableˇ");
12907 cx.simulate_keystroke(".");
12908 cx.executor().run_until_parked();
12909
12910 cx.update_editor(|editor, _, _| {
12911 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12912 {
12913 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
12914 } else {
12915 panic!("expected completion menu to be open");
12916 }
12917 });
12918
12919 cx.update_editor(|editor, window, cx| {
12920 editor.move_page_down(&MovePageDown::default(), window, cx);
12921 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12922 {
12923 assert!(
12924 menu.selected_item == 1,
12925 "expected PageDown to select the last item from the context menu"
12926 );
12927 } else {
12928 panic!("expected completion menu to stay open after PageDown");
12929 }
12930 });
12931
12932 cx.update_editor(|editor, window, cx| {
12933 editor.move_page_up(&MovePageUp::default(), window, cx);
12934 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12935 {
12936 assert!(
12937 menu.selected_item == 0,
12938 "expected PageUp to select the first item from the context menu"
12939 );
12940 } else {
12941 panic!("expected completion menu to stay open after PageUp");
12942 }
12943 });
12944}
12945
12946#[gpui::test]
12947async fn test_as_is_completions(cx: &mut TestAppContext) {
12948 init_test(cx, |_| {});
12949 let mut cx = EditorLspTestContext::new_rust(
12950 lsp::ServerCapabilities {
12951 completion_provider: Some(lsp::CompletionOptions {
12952 ..Default::default()
12953 }),
12954 ..Default::default()
12955 },
12956 cx,
12957 )
12958 .await;
12959 cx.lsp
12960 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12961 Ok(Some(lsp::CompletionResponse::Array(vec![
12962 lsp::CompletionItem {
12963 label: "unsafe".into(),
12964 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12965 range: lsp::Range {
12966 start: lsp::Position {
12967 line: 1,
12968 character: 2,
12969 },
12970 end: lsp::Position {
12971 line: 1,
12972 character: 3,
12973 },
12974 },
12975 new_text: "unsafe".to_string(),
12976 })),
12977 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
12978 ..Default::default()
12979 },
12980 ])))
12981 });
12982 cx.set_state("fn a() {}\n nˇ");
12983 cx.executor().run_until_parked();
12984 cx.update_editor(|editor, window, cx| {
12985 editor.show_completions(
12986 &ShowCompletions {
12987 trigger: Some("\n".into()),
12988 },
12989 window,
12990 cx,
12991 );
12992 });
12993 cx.executor().run_until_parked();
12994
12995 cx.update_editor(|editor, window, cx| {
12996 editor.confirm_completion(&Default::default(), window, cx)
12997 });
12998 cx.executor().run_until_parked();
12999 cx.assert_editor_state("fn a() {}\n unsafeˇ");
13000}
13001
13002#[gpui::test]
13003async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
13004 init_test(cx, |_| {});
13005
13006 let mut cx = EditorLspTestContext::new_rust(
13007 lsp::ServerCapabilities {
13008 completion_provider: Some(lsp::CompletionOptions {
13009 trigger_characters: Some(vec![".".to_string()]),
13010 resolve_provider: Some(true),
13011 ..Default::default()
13012 }),
13013 ..Default::default()
13014 },
13015 cx,
13016 )
13017 .await;
13018
13019 cx.set_state("fn main() { let a = 2ˇ; }");
13020 cx.simulate_keystroke(".");
13021 let completion_item = lsp::CompletionItem {
13022 label: "Some".into(),
13023 kind: Some(lsp::CompletionItemKind::SNIPPET),
13024 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13025 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13026 kind: lsp::MarkupKind::Markdown,
13027 value: "```rust\nSome(2)\n```".to_string(),
13028 })),
13029 deprecated: Some(false),
13030 sort_text: Some("Some".to_string()),
13031 filter_text: Some("Some".to_string()),
13032 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13033 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13034 range: lsp::Range {
13035 start: lsp::Position {
13036 line: 0,
13037 character: 22,
13038 },
13039 end: lsp::Position {
13040 line: 0,
13041 character: 22,
13042 },
13043 },
13044 new_text: "Some(2)".to_string(),
13045 })),
13046 additional_text_edits: Some(vec![lsp::TextEdit {
13047 range: lsp::Range {
13048 start: lsp::Position {
13049 line: 0,
13050 character: 20,
13051 },
13052 end: lsp::Position {
13053 line: 0,
13054 character: 22,
13055 },
13056 },
13057 new_text: "".to_string(),
13058 }]),
13059 ..Default::default()
13060 };
13061
13062 let closure_completion_item = completion_item.clone();
13063 let counter = Arc::new(AtomicUsize::new(0));
13064 let counter_clone = counter.clone();
13065 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13066 let task_completion_item = closure_completion_item.clone();
13067 counter_clone.fetch_add(1, atomic::Ordering::Release);
13068 async move {
13069 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13070 is_incomplete: true,
13071 item_defaults: None,
13072 items: vec![task_completion_item],
13073 })))
13074 }
13075 });
13076
13077 cx.condition(|editor, _| editor.context_menu_visible())
13078 .await;
13079 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13080 assert!(request.next().await.is_some());
13081 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13082
13083 cx.simulate_keystrokes("S o m");
13084 cx.condition(|editor, _| editor.context_menu_visible())
13085 .await;
13086 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13087 assert!(request.next().await.is_some());
13088 assert!(request.next().await.is_some());
13089 assert!(request.next().await.is_some());
13090 request.close();
13091 assert!(request.next().await.is_none());
13092 assert_eq!(
13093 counter.load(atomic::Ordering::Acquire),
13094 4,
13095 "With the completions menu open, only one LSP request should happen per input"
13096 );
13097}
13098
13099#[gpui::test]
13100async fn test_toggle_comment(cx: &mut TestAppContext) {
13101 init_test(cx, |_| {});
13102 let mut cx = EditorTestContext::new(cx).await;
13103 let language = Arc::new(Language::new(
13104 LanguageConfig {
13105 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13106 ..Default::default()
13107 },
13108 Some(tree_sitter_rust::LANGUAGE.into()),
13109 ));
13110 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13111
13112 // If multiple selections intersect a line, the line is only toggled once.
13113 cx.set_state(indoc! {"
13114 fn a() {
13115 «//b();
13116 ˇ»// «c();
13117 //ˇ» d();
13118 }
13119 "});
13120
13121 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13122
13123 cx.assert_editor_state(indoc! {"
13124 fn a() {
13125 «b();
13126 c();
13127 ˇ» d();
13128 }
13129 "});
13130
13131 // The comment prefix is inserted at the same column for every line in a
13132 // selection.
13133 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13134
13135 cx.assert_editor_state(indoc! {"
13136 fn a() {
13137 // «b();
13138 // c();
13139 ˇ»// d();
13140 }
13141 "});
13142
13143 // If a selection ends at the beginning of a line, that line is not toggled.
13144 cx.set_selections_state(indoc! {"
13145 fn a() {
13146 // b();
13147 «// c();
13148 ˇ» // d();
13149 }
13150 "});
13151
13152 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13153
13154 cx.assert_editor_state(indoc! {"
13155 fn a() {
13156 // b();
13157 «c();
13158 ˇ» // d();
13159 }
13160 "});
13161
13162 // If a selection span a single line and is empty, the line is toggled.
13163 cx.set_state(indoc! {"
13164 fn a() {
13165 a();
13166 b();
13167 ˇ
13168 }
13169 "});
13170
13171 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13172
13173 cx.assert_editor_state(indoc! {"
13174 fn a() {
13175 a();
13176 b();
13177 //•ˇ
13178 }
13179 "});
13180
13181 // If a selection span multiple lines, empty lines are not toggled.
13182 cx.set_state(indoc! {"
13183 fn a() {
13184 «a();
13185
13186 c();ˇ»
13187 }
13188 "});
13189
13190 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13191
13192 cx.assert_editor_state(indoc! {"
13193 fn a() {
13194 // «a();
13195
13196 // c();ˇ»
13197 }
13198 "});
13199
13200 // If a selection includes multiple comment prefixes, all lines are uncommented.
13201 cx.set_state(indoc! {"
13202 fn a() {
13203 «// a();
13204 /// b();
13205 //! c();ˇ»
13206 }
13207 "});
13208
13209 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13210
13211 cx.assert_editor_state(indoc! {"
13212 fn a() {
13213 «a();
13214 b();
13215 c();ˇ»
13216 }
13217 "});
13218}
13219
13220#[gpui::test]
13221async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
13222 init_test(cx, |_| {});
13223 let mut cx = EditorTestContext::new(cx).await;
13224 let language = Arc::new(Language::new(
13225 LanguageConfig {
13226 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13227 ..Default::default()
13228 },
13229 Some(tree_sitter_rust::LANGUAGE.into()),
13230 ));
13231 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13232
13233 let toggle_comments = &ToggleComments {
13234 advance_downwards: false,
13235 ignore_indent: true,
13236 };
13237
13238 // If multiple selections intersect a line, the line is only toggled once.
13239 cx.set_state(indoc! {"
13240 fn a() {
13241 // «b();
13242 // c();
13243 // ˇ» d();
13244 }
13245 "});
13246
13247 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13248
13249 cx.assert_editor_state(indoc! {"
13250 fn a() {
13251 «b();
13252 c();
13253 ˇ» d();
13254 }
13255 "});
13256
13257 // The comment prefix is inserted at the beginning of each line
13258 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13259
13260 cx.assert_editor_state(indoc! {"
13261 fn a() {
13262 // «b();
13263 // c();
13264 // ˇ» d();
13265 }
13266 "});
13267
13268 // If a selection ends at the beginning of a line, that line is not toggled.
13269 cx.set_selections_state(indoc! {"
13270 fn a() {
13271 // b();
13272 // «c();
13273 ˇ»// d();
13274 }
13275 "});
13276
13277 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13278
13279 cx.assert_editor_state(indoc! {"
13280 fn a() {
13281 // b();
13282 «c();
13283 ˇ»// d();
13284 }
13285 "});
13286
13287 // If a selection span a single line and is empty, the line is toggled.
13288 cx.set_state(indoc! {"
13289 fn a() {
13290 a();
13291 b();
13292 ˇ
13293 }
13294 "});
13295
13296 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13297
13298 cx.assert_editor_state(indoc! {"
13299 fn a() {
13300 a();
13301 b();
13302 //ˇ
13303 }
13304 "});
13305
13306 // If a selection span multiple lines, empty lines are not toggled.
13307 cx.set_state(indoc! {"
13308 fn a() {
13309 «a();
13310
13311 c();ˇ»
13312 }
13313 "});
13314
13315 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13316
13317 cx.assert_editor_state(indoc! {"
13318 fn a() {
13319 // «a();
13320
13321 // c();ˇ»
13322 }
13323 "});
13324
13325 // If a selection includes multiple comment prefixes, all lines are uncommented.
13326 cx.set_state(indoc! {"
13327 fn a() {
13328 // «a();
13329 /// b();
13330 //! c();ˇ»
13331 }
13332 "});
13333
13334 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13335
13336 cx.assert_editor_state(indoc! {"
13337 fn a() {
13338 «a();
13339 b();
13340 c();ˇ»
13341 }
13342 "});
13343}
13344
13345#[gpui::test]
13346async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13347 init_test(cx, |_| {});
13348
13349 let language = Arc::new(Language::new(
13350 LanguageConfig {
13351 line_comments: vec!["// ".into()],
13352 ..Default::default()
13353 },
13354 Some(tree_sitter_rust::LANGUAGE.into()),
13355 ));
13356
13357 let mut cx = EditorTestContext::new(cx).await;
13358
13359 cx.language_registry().add(language.clone());
13360 cx.update_buffer(|buffer, cx| {
13361 buffer.set_language(Some(language), cx);
13362 });
13363
13364 let toggle_comments = &ToggleComments {
13365 advance_downwards: true,
13366 ignore_indent: false,
13367 };
13368
13369 // Single cursor on one line -> advance
13370 // Cursor moves horizontally 3 characters as well on non-blank line
13371 cx.set_state(indoc!(
13372 "fn a() {
13373 ˇdog();
13374 cat();
13375 }"
13376 ));
13377 cx.update_editor(|editor, window, cx| {
13378 editor.toggle_comments(toggle_comments, window, cx);
13379 });
13380 cx.assert_editor_state(indoc!(
13381 "fn a() {
13382 // dog();
13383 catˇ();
13384 }"
13385 ));
13386
13387 // Single selection on one line -> don't advance
13388 cx.set_state(indoc!(
13389 "fn a() {
13390 «dog()ˇ»;
13391 cat();
13392 }"
13393 ));
13394 cx.update_editor(|editor, window, cx| {
13395 editor.toggle_comments(toggle_comments, window, cx);
13396 });
13397 cx.assert_editor_state(indoc!(
13398 "fn a() {
13399 // «dog()ˇ»;
13400 cat();
13401 }"
13402 ));
13403
13404 // Multiple cursors on one line -> advance
13405 cx.set_state(indoc!(
13406 "fn a() {
13407 ˇdˇog();
13408 cat();
13409 }"
13410 ));
13411 cx.update_editor(|editor, window, cx| {
13412 editor.toggle_comments(toggle_comments, window, cx);
13413 });
13414 cx.assert_editor_state(indoc!(
13415 "fn a() {
13416 // dog();
13417 catˇ(ˇ);
13418 }"
13419 ));
13420
13421 // Multiple cursors on one line, with selection -> don't advance
13422 cx.set_state(indoc!(
13423 "fn a() {
13424 ˇdˇog«()ˇ»;
13425 cat();
13426 }"
13427 ));
13428 cx.update_editor(|editor, window, cx| {
13429 editor.toggle_comments(toggle_comments, window, cx);
13430 });
13431 cx.assert_editor_state(indoc!(
13432 "fn a() {
13433 // ˇdˇog«()ˇ»;
13434 cat();
13435 }"
13436 ));
13437
13438 // Single cursor on one line -> advance
13439 // Cursor moves to column 0 on blank line
13440 cx.set_state(indoc!(
13441 "fn a() {
13442 ˇdog();
13443
13444 cat();
13445 }"
13446 ));
13447 cx.update_editor(|editor, window, cx| {
13448 editor.toggle_comments(toggle_comments, window, cx);
13449 });
13450 cx.assert_editor_state(indoc!(
13451 "fn a() {
13452 // dog();
13453 ˇ
13454 cat();
13455 }"
13456 ));
13457
13458 // Single cursor on one line -> advance
13459 // Cursor starts and ends at column 0
13460 cx.set_state(indoc!(
13461 "fn a() {
13462 ˇ dog();
13463 cat();
13464 }"
13465 ));
13466 cx.update_editor(|editor, window, cx| {
13467 editor.toggle_comments(toggle_comments, window, cx);
13468 });
13469 cx.assert_editor_state(indoc!(
13470 "fn a() {
13471 // dog();
13472 ˇ cat();
13473 }"
13474 ));
13475}
13476
13477#[gpui::test]
13478async fn test_toggle_block_comment(cx: &mut TestAppContext) {
13479 init_test(cx, |_| {});
13480
13481 let mut cx = EditorTestContext::new(cx).await;
13482
13483 let html_language = Arc::new(
13484 Language::new(
13485 LanguageConfig {
13486 name: "HTML".into(),
13487 block_comment: Some(("<!-- ".into(), " -->".into())),
13488 ..Default::default()
13489 },
13490 Some(tree_sitter_html::LANGUAGE.into()),
13491 )
13492 .with_injection_query(
13493 r#"
13494 (script_element
13495 (raw_text) @injection.content
13496 (#set! injection.language "javascript"))
13497 "#,
13498 )
13499 .unwrap(),
13500 );
13501
13502 let javascript_language = Arc::new(Language::new(
13503 LanguageConfig {
13504 name: "JavaScript".into(),
13505 line_comments: vec!["// ".into()],
13506 ..Default::default()
13507 },
13508 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13509 ));
13510
13511 cx.language_registry().add(html_language.clone());
13512 cx.language_registry().add(javascript_language.clone());
13513 cx.update_buffer(|buffer, cx| {
13514 buffer.set_language(Some(html_language), cx);
13515 });
13516
13517 // Toggle comments for empty selections
13518 cx.set_state(
13519 &r#"
13520 <p>A</p>ˇ
13521 <p>B</p>ˇ
13522 <p>C</p>ˇ
13523 "#
13524 .unindent(),
13525 );
13526 cx.update_editor(|editor, window, cx| {
13527 editor.toggle_comments(&ToggleComments::default(), window, cx)
13528 });
13529 cx.assert_editor_state(
13530 &r#"
13531 <!-- <p>A</p>ˇ -->
13532 <!-- <p>B</p>ˇ -->
13533 <!-- <p>C</p>ˇ -->
13534 "#
13535 .unindent(),
13536 );
13537 cx.update_editor(|editor, window, cx| {
13538 editor.toggle_comments(&ToggleComments::default(), window, cx)
13539 });
13540 cx.assert_editor_state(
13541 &r#"
13542 <p>A</p>ˇ
13543 <p>B</p>ˇ
13544 <p>C</p>ˇ
13545 "#
13546 .unindent(),
13547 );
13548
13549 // Toggle comments for mixture of empty and non-empty selections, where
13550 // multiple selections occupy a given line.
13551 cx.set_state(
13552 &r#"
13553 <p>A«</p>
13554 <p>ˇ»B</p>ˇ
13555 <p>C«</p>
13556 <p>ˇ»D</p>ˇ
13557 "#
13558 .unindent(),
13559 );
13560
13561 cx.update_editor(|editor, window, cx| {
13562 editor.toggle_comments(&ToggleComments::default(), window, cx)
13563 });
13564 cx.assert_editor_state(
13565 &r#"
13566 <!-- <p>A«</p>
13567 <p>ˇ»B</p>ˇ -->
13568 <!-- <p>C«</p>
13569 <p>ˇ»D</p>ˇ -->
13570 "#
13571 .unindent(),
13572 );
13573 cx.update_editor(|editor, window, cx| {
13574 editor.toggle_comments(&ToggleComments::default(), window, cx)
13575 });
13576 cx.assert_editor_state(
13577 &r#"
13578 <p>A«</p>
13579 <p>ˇ»B</p>ˇ
13580 <p>C«</p>
13581 <p>ˇ»D</p>ˇ
13582 "#
13583 .unindent(),
13584 );
13585
13586 // Toggle comments when different languages are active for different
13587 // selections.
13588 cx.set_state(
13589 &r#"
13590 ˇ<script>
13591 ˇvar x = new Y();
13592 ˇ</script>
13593 "#
13594 .unindent(),
13595 );
13596 cx.executor().run_until_parked();
13597 cx.update_editor(|editor, window, cx| {
13598 editor.toggle_comments(&ToggleComments::default(), window, cx)
13599 });
13600 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13601 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13602 cx.assert_editor_state(
13603 &r#"
13604 <!-- ˇ<script> -->
13605 // ˇvar x = new Y();
13606 <!-- ˇ</script> -->
13607 "#
13608 .unindent(),
13609 );
13610}
13611
13612#[gpui::test]
13613fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13614 init_test(cx, |_| {});
13615
13616 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13617 let multibuffer = cx.new(|cx| {
13618 let mut multibuffer = MultiBuffer::new(ReadWrite);
13619 multibuffer.push_excerpts(
13620 buffer.clone(),
13621 [
13622 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13623 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
13624 ],
13625 cx,
13626 );
13627 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
13628 multibuffer
13629 });
13630
13631 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13632 editor.update_in(cx, |editor, window, cx| {
13633 assert_eq!(editor.text(cx), "aaaa\nbbbb");
13634 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13635 s.select_ranges([
13636 Point::new(0, 0)..Point::new(0, 0),
13637 Point::new(1, 0)..Point::new(1, 0),
13638 ])
13639 });
13640
13641 editor.handle_input("X", window, cx);
13642 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
13643 assert_eq!(
13644 editor.selections.ranges(cx),
13645 [
13646 Point::new(0, 1)..Point::new(0, 1),
13647 Point::new(1, 1)..Point::new(1, 1),
13648 ]
13649 );
13650
13651 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
13652 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13653 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
13654 });
13655 editor.backspace(&Default::default(), window, cx);
13656 assert_eq!(editor.text(cx), "Xa\nbbb");
13657 assert_eq!(
13658 editor.selections.ranges(cx),
13659 [Point::new(1, 0)..Point::new(1, 0)]
13660 );
13661
13662 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13663 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
13664 });
13665 editor.backspace(&Default::default(), window, cx);
13666 assert_eq!(editor.text(cx), "X\nbb");
13667 assert_eq!(
13668 editor.selections.ranges(cx),
13669 [Point::new(0, 1)..Point::new(0, 1)]
13670 );
13671 });
13672}
13673
13674#[gpui::test]
13675fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
13676 init_test(cx, |_| {});
13677
13678 let markers = vec![('[', ']').into(), ('(', ')').into()];
13679 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
13680 indoc! {"
13681 [aaaa
13682 (bbbb]
13683 cccc)",
13684 },
13685 markers.clone(),
13686 );
13687 let excerpt_ranges = markers.into_iter().map(|marker| {
13688 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
13689 ExcerptRange::new(context.clone())
13690 });
13691 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
13692 let multibuffer = cx.new(|cx| {
13693 let mut multibuffer = MultiBuffer::new(ReadWrite);
13694 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
13695 multibuffer
13696 });
13697
13698 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13699 editor.update_in(cx, |editor, window, cx| {
13700 let (expected_text, selection_ranges) = marked_text_ranges(
13701 indoc! {"
13702 aaaa
13703 bˇbbb
13704 bˇbbˇb
13705 cccc"
13706 },
13707 true,
13708 );
13709 assert_eq!(editor.text(cx), expected_text);
13710 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13711 s.select_ranges(selection_ranges)
13712 });
13713
13714 editor.handle_input("X", window, cx);
13715
13716 let (expected_text, expected_selections) = marked_text_ranges(
13717 indoc! {"
13718 aaaa
13719 bXˇbbXb
13720 bXˇbbXˇb
13721 cccc"
13722 },
13723 false,
13724 );
13725 assert_eq!(editor.text(cx), expected_text);
13726 assert_eq!(editor.selections.ranges(cx), expected_selections);
13727
13728 editor.newline(&Newline, window, cx);
13729 let (expected_text, expected_selections) = marked_text_ranges(
13730 indoc! {"
13731 aaaa
13732 bX
13733 ˇbbX
13734 b
13735 bX
13736 ˇbbX
13737 ˇb
13738 cccc"
13739 },
13740 false,
13741 );
13742 assert_eq!(editor.text(cx), expected_text);
13743 assert_eq!(editor.selections.ranges(cx), expected_selections);
13744 });
13745}
13746
13747#[gpui::test]
13748fn test_refresh_selections(cx: &mut TestAppContext) {
13749 init_test(cx, |_| {});
13750
13751 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13752 let mut excerpt1_id = None;
13753 let multibuffer = cx.new(|cx| {
13754 let mut multibuffer = MultiBuffer::new(ReadWrite);
13755 excerpt1_id = multibuffer
13756 .push_excerpts(
13757 buffer.clone(),
13758 [
13759 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13760 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13761 ],
13762 cx,
13763 )
13764 .into_iter()
13765 .next();
13766 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13767 multibuffer
13768 });
13769
13770 let editor = cx.add_window(|window, cx| {
13771 let mut editor = build_editor(multibuffer.clone(), window, cx);
13772 let snapshot = editor.snapshot(window, cx);
13773 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13774 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
13775 });
13776 editor.begin_selection(
13777 Point::new(2, 1).to_display_point(&snapshot),
13778 true,
13779 1,
13780 window,
13781 cx,
13782 );
13783 assert_eq!(
13784 editor.selections.ranges(cx),
13785 [
13786 Point::new(1, 3)..Point::new(1, 3),
13787 Point::new(2, 1)..Point::new(2, 1),
13788 ]
13789 );
13790 editor
13791 });
13792
13793 // Refreshing selections is a no-op when excerpts haven't changed.
13794 _ = editor.update(cx, |editor, window, cx| {
13795 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
13796 assert_eq!(
13797 editor.selections.ranges(cx),
13798 [
13799 Point::new(1, 3)..Point::new(1, 3),
13800 Point::new(2, 1)..Point::new(2, 1),
13801 ]
13802 );
13803 });
13804
13805 multibuffer.update(cx, |multibuffer, cx| {
13806 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13807 });
13808 _ = editor.update(cx, |editor, window, cx| {
13809 // Removing an excerpt causes the first selection to become degenerate.
13810 assert_eq!(
13811 editor.selections.ranges(cx),
13812 [
13813 Point::new(0, 0)..Point::new(0, 0),
13814 Point::new(0, 1)..Point::new(0, 1)
13815 ]
13816 );
13817
13818 // Refreshing selections will relocate the first selection to the original buffer
13819 // location.
13820 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
13821 assert_eq!(
13822 editor.selections.ranges(cx),
13823 [
13824 Point::new(0, 1)..Point::new(0, 1),
13825 Point::new(0, 3)..Point::new(0, 3)
13826 ]
13827 );
13828 assert!(editor.selections.pending_anchor().is_some());
13829 });
13830}
13831
13832#[gpui::test]
13833fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
13834 init_test(cx, |_| {});
13835
13836 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13837 let mut excerpt1_id = None;
13838 let multibuffer = cx.new(|cx| {
13839 let mut multibuffer = MultiBuffer::new(ReadWrite);
13840 excerpt1_id = multibuffer
13841 .push_excerpts(
13842 buffer.clone(),
13843 [
13844 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13845 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13846 ],
13847 cx,
13848 )
13849 .into_iter()
13850 .next();
13851 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13852 multibuffer
13853 });
13854
13855 let editor = cx.add_window(|window, cx| {
13856 let mut editor = build_editor(multibuffer.clone(), window, cx);
13857 let snapshot = editor.snapshot(window, cx);
13858 editor.begin_selection(
13859 Point::new(1, 3).to_display_point(&snapshot),
13860 false,
13861 1,
13862 window,
13863 cx,
13864 );
13865 assert_eq!(
13866 editor.selections.ranges(cx),
13867 [Point::new(1, 3)..Point::new(1, 3)]
13868 );
13869 editor
13870 });
13871
13872 multibuffer.update(cx, |multibuffer, cx| {
13873 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13874 });
13875 _ = editor.update(cx, |editor, window, cx| {
13876 assert_eq!(
13877 editor.selections.ranges(cx),
13878 [Point::new(0, 0)..Point::new(0, 0)]
13879 );
13880
13881 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
13882 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
13883 assert_eq!(
13884 editor.selections.ranges(cx),
13885 [Point::new(0, 3)..Point::new(0, 3)]
13886 );
13887 assert!(editor.selections.pending_anchor().is_some());
13888 });
13889}
13890
13891#[gpui::test]
13892async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
13893 init_test(cx, |_| {});
13894
13895 let language = Arc::new(
13896 Language::new(
13897 LanguageConfig {
13898 brackets: BracketPairConfig {
13899 pairs: vec![
13900 BracketPair {
13901 start: "{".to_string(),
13902 end: "}".to_string(),
13903 close: true,
13904 surround: true,
13905 newline: true,
13906 },
13907 BracketPair {
13908 start: "/* ".to_string(),
13909 end: " */".to_string(),
13910 close: true,
13911 surround: true,
13912 newline: true,
13913 },
13914 ],
13915 ..Default::default()
13916 },
13917 ..Default::default()
13918 },
13919 Some(tree_sitter_rust::LANGUAGE.into()),
13920 )
13921 .with_indents_query("")
13922 .unwrap(),
13923 );
13924
13925 let text = concat!(
13926 "{ }\n", //
13927 " x\n", //
13928 " /* */\n", //
13929 "x\n", //
13930 "{{} }\n", //
13931 );
13932
13933 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
13934 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13935 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13936 editor
13937 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
13938 .await;
13939
13940 editor.update_in(cx, |editor, window, cx| {
13941 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13942 s.select_display_ranges([
13943 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
13944 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
13945 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
13946 ])
13947 });
13948 editor.newline(&Newline, window, cx);
13949
13950 assert_eq!(
13951 editor.buffer().read(cx).read(cx).text(),
13952 concat!(
13953 "{ \n", // Suppress rustfmt
13954 "\n", //
13955 "}\n", //
13956 " x\n", //
13957 " /* \n", //
13958 " \n", //
13959 " */\n", //
13960 "x\n", //
13961 "{{} \n", //
13962 "}\n", //
13963 )
13964 );
13965 });
13966}
13967
13968#[gpui::test]
13969fn test_highlighted_ranges(cx: &mut TestAppContext) {
13970 init_test(cx, |_| {});
13971
13972 let editor = cx.add_window(|window, cx| {
13973 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
13974 build_editor(buffer.clone(), window, cx)
13975 });
13976
13977 _ = editor.update(cx, |editor, window, cx| {
13978 struct Type1;
13979 struct Type2;
13980
13981 let buffer = editor.buffer.read(cx).snapshot(cx);
13982
13983 let anchor_range =
13984 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
13985
13986 editor.highlight_background::<Type1>(
13987 &[
13988 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
13989 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
13990 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
13991 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
13992 ],
13993 |_| Hsla::red(),
13994 cx,
13995 );
13996 editor.highlight_background::<Type2>(
13997 &[
13998 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
13999 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
14000 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
14001 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
14002 ],
14003 |_| Hsla::green(),
14004 cx,
14005 );
14006
14007 let snapshot = editor.snapshot(window, cx);
14008 let mut highlighted_ranges = editor.background_highlights_in_range(
14009 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
14010 &snapshot,
14011 cx.theme(),
14012 );
14013 // Enforce a consistent ordering based on color without relying on the ordering of the
14014 // highlight's `TypeId` which is non-executor.
14015 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
14016 assert_eq!(
14017 highlighted_ranges,
14018 &[
14019 (
14020 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
14021 Hsla::red(),
14022 ),
14023 (
14024 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14025 Hsla::red(),
14026 ),
14027 (
14028 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
14029 Hsla::green(),
14030 ),
14031 (
14032 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
14033 Hsla::green(),
14034 ),
14035 ]
14036 );
14037 assert_eq!(
14038 editor.background_highlights_in_range(
14039 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
14040 &snapshot,
14041 cx.theme(),
14042 ),
14043 &[(
14044 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14045 Hsla::red(),
14046 )]
14047 );
14048 });
14049}
14050
14051#[gpui::test]
14052async fn test_following(cx: &mut TestAppContext) {
14053 init_test(cx, |_| {});
14054
14055 let fs = FakeFs::new(cx.executor());
14056 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14057
14058 let buffer = project.update(cx, |project, cx| {
14059 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
14060 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
14061 });
14062 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
14063 let follower = cx.update(|cx| {
14064 cx.open_window(
14065 WindowOptions {
14066 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
14067 gpui::Point::new(px(0.), px(0.)),
14068 gpui::Point::new(px(10.), px(80.)),
14069 ))),
14070 ..Default::default()
14071 },
14072 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
14073 )
14074 .unwrap()
14075 });
14076
14077 let is_still_following = Rc::new(RefCell::new(true));
14078 let follower_edit_event_count = Rc::new(RefCell::new(0));
14079 let pending_update = Rc::new(RefCell::new(None));
14080 let leader_entity = leader.root(cx).unwrap();
14081 let follower_entity = follower.root(cx).unwrap();
14082 _ = follower.update(cx, {
14083 let update = pending_update.clone();
14084 let is_still_following = is_still_following.clone();
14085 let follower_edit_event_count = follower_edit_event_count.clone();
14086 |_, window, cx| {
14087 cx.subscribe_in(
14088 &leader_entity,
14089 window,
14090 move |_, leader, event, window, cx| {
14091 leader.read(cx).add_event_to_update_proto(
14092 event,
14093 &mut update.borrow_mut(),
14094 window,
14095 cx,
14096 );
14097 },
14098 )
14099 .detach();
14100
14101 cx.subscribe_in(
14102 &follower_entity,
14103 window,
14104 move |_, _, event: &EditorEvent, _window, _cx| {
14105 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14106 *is_still_following.borrow_mut() = false;
14107 }
14108
14109 if let EditorEvent::BufferEdited = event {
14110 *follower_edit_event_count.borrow_mut() += 1;
14111 }
14112 },
14113 )
14114 .detach();
14115 }
14116 });
14117
14118 // Update the selections only
14119 _ = leader.update(cx, |leader, window, cx| {
14120 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14121 s.select_ranges([1..1])
14122 });
14123 });
14124 follower
14125 .update(cx, |follower, window, cx| {
14126 follower.apply_update_proto(
14127 &project,
14128 pending_update.borrow_mut().take().unwrap(),
14129 window,
14130 cx,
14131 )
14132 })
14133 .unwrap()
14134 .await
14135 .unwrap();
14136 _ = follower.update(cx, |follower, _, cx| {
14137 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14138 });
14139 assert!(*is_still_following.borrow());
14140 assert_eq!(*follower_edit_event_count.borrow(), 0);
14141
14142 // Update the scroll position only
14143 _ = leader.update(cx, |leader, window, cx| {
14144 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14145 });
14146 follower
14147 .update(cx, |follower, window, cx| {
14148 follower.apply_update_proto(
14149 &project,
14150 pending_update.borrow_mut().take().unwrap(),
14151 window,
14152 cx,
14153 )
14154 })
14155 .unwrap()
14156 .await
14157 .unwrap();
14158 assert_eq!(
14159 follower
14160 .update(cx, |follower, _, cx| follower.scroll_position(cx))
14161 .unwrap(),
14162 gpui::Point::new(1.5, 3.5)
14163 );
14164 assert!(*is_still_following.borrow());
14165 assert_eq!(*follower_edit_event_count.borrow(), 0);
14166
14167 // Update the selections and scroll position. The follower's scroll position is updated
14168 // via autoscroll, not via the leader's exact scroll position.
14169 _ = leader.update(cx, |leader, window, cx| {
14170 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14171 s.select_ranges([0..0])
14172 });
14173 leader.request_autoscroll(Autoscroll::newest(), cx);
14174 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14175 });
14176 follower
14177 .update(cx, |follower, window, cx| {
14178 follower.apply_update_proto(
14179 &project,
14180 pending_update.borrow_mut().take().unwrap(),
14181 window,
14182 cx,
14183 )
14184 })
14185 .unwrap()
14186 .await
14187 .unwrap();
14188 _ = follower.update(cx, |follower, _, cx| {
14189 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
14190 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
14191 });
14192 assert!(*is_still_following.borrow());
14193
14194 // Creating a pending selection that precedes another selection
14195 _ = leader.update(cx, |leader, window, cx| {
14196 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14197 s.select_ranges([1..1])
14198 });
14199 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
14200 });
14201 follower
14202 .update(cx, |follower, window, cx| {
14203 follower.apply_update_proto(
14204 &project,
14205 pending_update.borrow_mut().take().unwrap(),
14206 window,
14207 cx,
14208 )
14209 })
14210 .unwrap()
14211 .await
14212 .unwrap();
14213 _ = follower.update(cx, |follower, _, cx| {
14214 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
14215 });
14216 assert!(*is_still_following.borrow());
14217
14218 // Extend the pending selection so that it surrounds another selection
14219 _ = leader.update(cx, |leader, window, cx| {
14220 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
14221 });
14222 follower
14223 .update(cx, |follower, window, cx| {
14224 follower.apply_update_proto(
14225 &project,
14226 pending_update.borrow_mut().take().unwrap(),
14227 window,
14228 cx,
14229 )
14230 })
14231 .unwrap()
14232 .await
14233 .unwrap();
14234 _ = follower.update(cx, |follower, _, cx| {
14235 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
14236 });
14237
14238 // Scrolling locally breaks the follow
14239 _ = follower.update(cx, |follower, window, cx| {
14240 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
14241 follower.set_scroll_anchor(
14242 ScrollAnchor {
14243 anchor: top_anchor,
14244 offset: gpui::Point::new(0.0, 0.5),
14245 },
14246 window,
14247 cx,
14248 );
14249 });
14250 assert!(!(*is_still_following.borrow()));
14251}
14252
14253#[gpui::test]
14254async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
14255 init_test(cx, |_| {});
14256
14257 let fs = FakeFs::new(cx.executor());
14258 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14259 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14260 let pane = workspace
14261 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14262 .unwrap();
14263
14264 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14265
14266 let leader = pane.update_in(cx, |_, window, cx| {
14267 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
14268 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
14269 });
14270
14271 // Start following the editor when it has no excerpts.
14272 let mut state_message =
14273 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14274 let workspace_entity = workspace.root(cx).unwrap();
14275 let follower_1 = cx
14276 .update_window(*workspace.deref(), |_, window, cx| {
14277 Editor::from_state_proto(
14278 workspace_entity,
14279 ViewId {
14280 creator: CollaboratorId::PeerId(PeerId::default()),
14281 id: 0,
14282 },
14283 &mut state_message,
14284 window,
14285 cx,
14286 )
14287 })
14288 .unwrap()
14289 .unwrap()
14290 .await
14291 .unwrap();
14292
14293 let update_message = Rc::new(RefCell::new(None));
14294 follower_1.update_in(cx, {
14295 let update = update_message.clone();
14296 |_, window, cx| {
14297 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
14298 leader.read(cx).add_event_to_update_proto(
14299 event,
14300 &mut update.borrow_mut(),
14301 window,
14302 cx,
14303 );
14304 })
14305 .detach();
14306 }
14307 });
14308
14309 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
14310 (
14311 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
14312 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
14313 )
14314 });
14315
14316 // Insert some excerpts.
14317 leader.update(cx, |leader, cx| {
14318 leader.buffer.update(cx, |multibuffer, cx| {
14319 multibuffer.set_excerpts_for_path(
14320 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14321 buffer_1.clone(),
14322 vec![
14323 Point::row_range(0..3),
14324 Point::row_range(1..6),
14325 Point::row_range(12..15),
14326 ],
14327 0,
14328 cx,
14329 );
14330 multibuffer.set_excerpts_for_path(
14331 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14332 buffer_2.clone(),
14333 vec![Point::row_range(0..6), Point::row_range(8..12)],
14334 0,
14335 cx,
14336 );
14337 });
14338 });
14339
14340 // Apply the update of adding the excerpts.
14341 follower_1
14342 .update_in(cx, |follower, window, cx| {
14343 follower.apply_update_proto(
14344 &project,
14345 update_message.borrow().clone().unwrap(),
14346 window,
14347 cx,
14348 )
14349 })
14350 .await
14351 .unwrap();
14352 assert_eq!(
14353 follower_1.update(cx, |editor, cx| editor.text(cx)),
14354 leader.update(cx, |editor, cx| editor.text(cx))
14355 );
14356 update_message.borrow_mut().take();
14357
14358 // Start following separately after it already has excerpts.
14359 let mut state_message =
14360 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14361 let workspace_entity = workspace.root(cx).unwrap();
14362 let follower_2 = cx
14363 .update_window(*workspace.deref(), |_, window, cx| {
14364 Editor::from_state_proto(
14365 workspace_entity,
14366 ViewId {
14367 creator: CollaboratorId::PeerId(PeerId::default()),
14368 id: 0,
14369 },
14370 &mut state_message,
14371 window,
14372 cx,
14373 )
14374 })
14375 .unwrap()
14376 .unwrap()
14377 .await
14378 .unwrap();
14379 assert_eq!(
14380 follower_2.update(cx, |editor, cx| editor.text(cx)),
14381 leader.update(cx, |editor, cx| editor.text(cx))
14382 );
14383
14384 // Remove some excerpts.
14385 leader.update(cx, |leader, cx| {
14386 leader.buffer.update(cx, |multibuffer, cx| {
14387 let excerpt_ids = multibuffer.excerpt_ids();
14388 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14389 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14390 });
14391 });
14392
14393 // Apply the update of removing the excerpts.
14394 follower_1
14395 .update_in(cx, |follower, window, cx| {
14396 follower.apply_update_proto(
14397 &project,
14398 update_message.borrow().clone().unwrap(),
14399 window,
14400 cx,
14401 )
14402 })
14403 .await
14404 .unwrap();
14405 follower_2
14406 .update_in(cx, |follower, window, cx| {
14407 follower.apply_update_proto(
14408 &project,
14409 update_message.borrow().clone().unwrap(),
14410 window,
14411 cx,
14412 )
14413 })
14414 .await
14415 .unwrap();
14416 update_message.borrow_mut().take();
14417 assert_eq!(
14418 follower_1.update(cx, |editor, cx| editor.text(cx)),
14419 leader.update(cx, |editor, cx| editor.text(cx))
14420 );
14421}
14422
14423#[gpui::test]
14424async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14425 init_test(cx, |_| {});
14426
14427 let mut cx = EditorTestContext::new(cx).await;
14428 let lsp_store =
14429 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
14430
14431 cx.set_state(indoc! {"
14432 ˇfn func(abc def: i32) -> u32 {
14433 }
14434 "});
14435
14436 cx.update(|_, cx| {
14437 lsp_store.update(cx, |lsp_store, cx| {
14438 lsp_store
14439 .update_diagnostics(
14440 LanguageServerId(0),
14441 lsp::PublishDiagnosticsParams {
14442 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
14443 version: None,
14444 diagnostics: vec![
14445 lsp::Diagnostic {
14446 range: lsp::Range::new(
14447 lsp::Position::new(0, 11),
14448 lsp::Position::new(0, 12),
14449 ),
14450 severity: Some(lsp::DiagnosticSeverity::ERROR),
14451 ..Default::default()
14452 },
14453 lsp::Diagnostic {
14454 range: lsp::Range::new(
14455 lsp::Position::new(0, 12),
14456 lsp::Position::new(0, 15),
14457 ),
14458 severity: Some(lsp::DiagnosticSeverity::ERROR),
14459 ..Default::default()
14460 },
14461 lsp::Diagnostic {
14462 range: lsp::Range::new(
14463 lsp::Position::new(0, 25),
14464 lsp::Position::new(0, 28),
14465 ),
14466 severity: Some(lsp::DiagnosticSeverity::ERROR),
14467 ..Default::default()
14468 },
14469 ],
14470 },
14471 None,
14472 DiagnosticSourceKind::Pushed,
14473 &[],
14474 cx,
14475 )
14476 .unwrap()
14477 });
14478 });
14479
14480 executor.run_until_parked();
14481
14482 cx.update_editor(|editor, window, cx| {
14483 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14484 });
14485
14486 cx.assert_editor_state(indoc! {"
14487 fn func(abc def: i32) -> ˇu32 {
14488 }
14489 "});
14490
14491 cx.update_editor(|editor, window, cx| {
14492 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14493 });
14494
14495 cx.assert_editor_state(indoc! {"
14496 fn func(abc ˇdef: i32) -> u32 {
14497 }
14498 "});
14499
14500 cx.update_editor(|editor, window, cx| {
14501 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14502 });
14503
14504 cx.assert_editor_state(indoc! {"
14505 fn func(abcˇ def: i32) -> u32 {
14506 }
14507 "});
14508
14509 cx.update_editor(|editor, window, cx| {
14510 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14511 });
14512
14513 cx.assert_editor_state(indoc! {"
14514 fn func(abc def: i32) -> ˇu32 {
14515 }
14516 "});
14517}
14518
14519#[gpui::test]
14520async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14521 init_test(cx, |_| {});
14522
14523 let mut cx = EditorTestContext::new(cx).await;
14524
14525 let diff_base = r#"
14526 use some::mod;
14527
14528 const A: u32 = 42;
14529
14530 fn main() {
14531 println!("hello");
14532
14533 println!("world");
14534 }
14535 "#
14536 .unindent();
14537
14538 // Edits are modified, removed, modified, added
14539 cx.set_state(
14540 &r#"
14541 use some::modified;
14542
14543 ˇ
14544 fn main() {
14545 println!("hello there");
14546
14547 println!("around the");
14548 println!("world");
14549 }
14550 "#
14551 .unindent(),
14552 );
14553
14554 cx.set_head_text(&diff_base);
14555 executor.run_until_parked();
14556
14557 cx.update_editor(|editor, window, cx| {
14558 //Wrap around the bottom of the buffer
14559 for _ in 0..3 {
14560 editor.go_to_next_hunk(&GoToHunk, window, cx);
14561 }
14562 });
14563
14564 cx.assert_editor_state(
14565 &r#"
14566 ˇuse some::modified;
14567
14568
14569 fn main() {
14570 println!("hello there");
14571
14572 println!("around the");
14573 println!("world");
14574 }
14575 "#
14576 .unindent(),
14577 );
14578
14579 cx.update_editor(|editor, window, cx| {
14580 //Wrap around the top of the buffer
14581 for _ in 0..2 {
14582 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14583 }
14584 });
14585
14586 cx.assert_editor_state(
14587 &r#"
14588 use some::modified;
14589
14590
14591 fn main() {
14592 ˇ println!("hello there");
14593
14594 println!("around the");
14595 println!("world");
14596 }
14597 "#
14598 .unindent(),
14599 );
14600
14601 cx.update_editor(|editor, window, cx| {
14602 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14603 });
14604
14605 cx.assert_editor_state(
14606 &r#"
14607 use some::modified;
14608
14609 ˇ
14610 fn main() {
14611 println!("hello there");
14612
14613 println!("around the");
14614 println!("world");
14615 }
14616 "#
14617 .unindent(),
14618 );
14619
14620 cx.update_editor(|editor, window, cx| {
14621 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14622 });
14623
14624 cx.assert_editor_state(
14625 &r#"
14626 ˇuse some::modified;
14627
14628
14629 fn main() {
14630 println!("hello there");
14631
14632 println!("around the");
14633 println!("world");
14634 }
14635 "#
14636 .unindent(),
14637 );
14638
14639 cx.update_editor(|editor, window, cx| {
14640 for _ in 0..2 {
14641 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14642 }
14643 });
14644
14645 cx.assert_editor_state(
14646 &r#"
14647 use some::modified;
14648
14649
14650 fn main() {
14651 ˇ println!("hello there");
14652
14653 println!("around the");
14654 println!("world");
14655 }
14656 "#
14657 .unindent(),
14658 );
14659
14660 cx.update_editor(|editor, window, cx| {
14661 editor.fold(&Fold, window, cx);
14662 });
14663
14664 cx.update_editor(|editor, window, cx| {
14665 editor.go_to_next_hunk(&GoToHunk, window, cx);
14666 });
14667
14668 cx.assert_editor_state(
14669 &r#"
14670 ˇuse some::modified;
14671
14672
14673 fn main() {
14674 println!("hello there");
14675
14676 println!("around the");
14677 println!("world");
14678 }
14679 "#
14680 .unindent(),
14681 );
14682}
14683
14684#[test]
14685fn test_split_words() {
14686 fn split(text: &str) -> Vec<&str> {
14687 split_words(text).collect()
14688 }
14689
14690 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
14691 assert_eq!(split("hello_world"), &["hello_", "world"]);
14692 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
14693 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
14694 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
14695 assert_eq!(split("helloworld"), &["helloworld"]);
14696
14697 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
14698}
14699
14700#[gpui::test]
14701async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
14702 init_test(cx, |_| {});
14703
14704 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
14705 let mut assert = |before, after| {
14706 let _state_context = cx.set_state(before);
14707 cx.run_until_parked();
14708 cx.update_editor(|editor, window, cx| {
14709 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
14710 });
14711 cx.run_until_parked();
14712 cx.assert_editor_state(after);
14713 };
14714
14715 // Outside bracket jumps to outside of matching bracket
14716 assert("console.logˇ(var);", "console.log(var)ˇ;");
14717 assert("console.log(var)ˇ;", "console.logˇ(var);");
14718
14719 // Inside bracket jumps to inside of matching bracket
14720 assert("console.log(ˇvar);", "console.log(varˇ);");
14721 assert("console.log(varˇ);", "console.log(ˇvar);");
14722
14723 // When outside a bracket and inside, favor jumping to the inside bracket
14724 assert(
14725 "console.log('foo', [1, 2, 3]ˇ);",
14726 "console.log(ˇ'foo', [1, 2, 3]);",
14727 );
14728 assert(
14729 "console.log(ˇ'foo', [1, 2, 3]);",
14730 "console.log('foo', [1, 2, 3]ˇ);",
14731 );
14732
14733 // Bias forward if two options are equally likely
14734 assert(
14735 "let result = curried_fun()ˇ();",
14736 "let result = curried_fun()()ˇ;",
14737 );
14738
14739 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
14740 assert(
14741 indoc! {"
14742 function test() {
14743 console.log('test')ˇ
14744 }"},
14745 indoc! {"
14746 function test() {
14747 console.logˇ('test')
14748 }"},
14749 );
14750}
14751
14752#[gpui::test]
14753async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
14754 init_test(cx, |_| {});
14755
14756 let fs = FakeFs::new(cx.executor());
14757 fs.insert_tree(
14758 path!("/a"),
14759 json!({
14760 "main.rs": "fn main() { let a = 5; }",
14761 "other.rs": "// Test file",
14762 }),
14763 )
14764 .await;
14765 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14766
14767 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14768 language_registry.add(Arc::new(Language::new(
14769 LanguageConfig {
14770 name: "Rust".into(),
14771 matcher: LanguageMatcher {
14772 path_suffixes: vec!["rs".to_string()],
14773 ..Default::default()
14774 },
14775 brackets: BracketPairConfig {
14776 pairs: vec![BracketPair {
14777 start: "{".to_string(),
14778 end: "}".to_string(),
14779 close: true,
14780 surround: true,
14781 newline: true,
14782 }],
14783 disabled_scopes_by_bracket_ix: Vec::new(),
14784 },
14785 ..Default::default()
14786 },
14787 Some(tree_sitter_rust::LANGUAGE.into()),
14788 )));
14789 let mut fake_servers = language_registry.register_fake_lsp(
14790 "Rust",
14791 FakeLspAdapter {
14792 capabilities: lsp::ServerCapabilities {
14793 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
14794 first_trigger_character: "{".to_string(),
14795 more_trigger_character: None,
14796 }),
14797 ..Default::default()
14798 },
14799 ..Default::default()
14800 },
14801 );
14802
14803 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14804
14805 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14806
14807 let worktree_id = workspace
14808 .update(cx, |workspace, _, cx| {
14809 workspace.project().update(cx, |project, cx| {
14810 project.worktrees(cx).next().unwrap().read(cx).id()
14811 })
14812 })
14813 .unwrap();
14814
14815 let buffer = project
14816 .update(cx, |project, cx| {
14817 project.open_local_buffer(path!("/a/main.rs"), cx)
14818 })
14819 .await
14820 .unwrap();
14821 let editor_handle = workspace
14822 .update(cx, |workspace, window, cx| {
14823 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
14824 })
14825 .unwrap()
14826 .await
14827 .unwrap()
14828 .downcast::<Editor>()
14829 .unwrap();
14830
14831 cx.executor().start_waiting();
14832 let fake_server = fake_servers.next().await.unwrap();
14833
14834 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
14835 |params, _| async move {
14836 assert_eq!(
14837 params.text_document_position.text_document.uri,
14838 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
14839 );
14840 assert_eq!(
14841 params.text_document_position.position,
14842 lsp::Position::new(0, 21),
14843 );
14844
14845 Ok(Some(vec![lsp::TextEdit {
14846 new_text: "]".to_string(),
14847 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14848 }]))
14849 },
14850 );
14851
14852 editor_handle.update_in(cx, |editor, window, cx| {
14853 window.focus(&editor.focus_handle(cx));
14854 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14855 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
14856 });
14857 editor.handle_input("{", window, cx);
14858 });
14859
14860 cx.executor().run_until_parked();
14861
14862 buffer.update(cx, |buffer, _| {
14863 assert_eq!(
14864 buffer.text(),
14865 "fn main() { let a = {5}; }",
14866 "No extra braces from on type formatting should appear in the buffer"
14867 )
14868 });
14869}
14870
14871#[gpui::test(iterations = 20, seeds(31))]
14872async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
14873 init_test(cx, |_| {});
14874
14875 let mut cx = EditorLspTestContext::new_rust(
14876 lsp::ServerCapabilities {
14877 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
14878 first_trigger_character: ".".to_string(),
14879 more_trigger_character: None,
14880 }),
14881 ..Default::default()
14882 },
14883 cx,
14884 )
14885 .await;
14886
14887 cx.update_buffer(|buffer, _| {
14888 // This causes autoindent to be async.
14889 buffer.set_sync_parse_timeout(Duration::ZERO)
14890 });
14891
14892 cx.set_state("fn c() {\n d()ˇ\n}\n");
14893 cx.simulate_keystroke("\n");
14894 cx.run_until_parked();
14895
14896 let buffer_cloned =
14897 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
14898 let mut request =
14899 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
14900 let buffer_cloned = buffer_cloned.clone();
14901 async move {
14902 buffer_cloned.update(&mut cx, |buffer, _| {
14903 assert_eq!(
14904 buffer.text(),
14905 "fn c() {\n d()\n .\n}\n",
14906 "OnTypeFormatting should triggered after autoindent applied"
14907 )
14908 })?;
14909
14910 Ok(Some(vec![]))
14911 }
14912 });
14913
14914 cx.simulate_keystroke(".");
14915 cx.run_until_parked();
14916
14917 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
14918 assert!(request.next().await.is_some());
14919 request.close();
14920 assert!(request.next().await.is_none());
14921}
14922
14923#[gpui::test]
14924async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
14925 init_test(cx, |_| {});
14926
14927 let fs = FakeFs::new(cx.executor());
14928 fs.insert_tree(
14929 path!("/a"),
14930 json!({
14931 "main.rs": "fn main() { let a = 5; }",
14932 "other.rs": "// Test file",
14933 }),
14934 )
14935 .await;
14936
14937 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14938
14939 let server_restarts = Arc::new(AtomicUsize::new(0));
14940 let closure_restarts = Arc::clone(&server_restarts);
14941 let language_server_name = "test language server";
14942 let language_name: LanguageName = "Rust".into();
14943
14944 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14945 language_registry.add(Arc::new(Language::new(
14946 LanguageConfig {
14947 name: language_name.clone(),
14948 matcher: LanguageMatcher {
14949 path_suffixes: vec!["rs".to_string()],
14950 ..Default::default()
14951 },
14952 ..Default::default()
14953 },
14954 Some(tree_sitter_rust::LANGUAGE.into()),
14955 )));
14956 let mut fake_servers = language_registry.register_fake_lsp(
14957 "Rust",
14958 FakeLspAdapter {
14959 name: language_server_name,
14960 initialization_options: Some(json!({
14961 "testOptionValue": true
14962 })),
14963 initializer: Some(Box::new(move |fake_server| {
14964 let task_restarts = Arc::clone(&closure_restarts);
14965 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
14966 task_restarts.fetch_add(1, atomic::Ordering::Release);
14967 futures::future::ready(Ok(()))
14968 });
14969 })),
14970 ..Default::default()
14971 },
14972 );
14973
14974 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14975 let _buffer = project
14976 .update(cx, |project, cx| {
14977 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
14978 })
14979 .await
14980 .unwrap();
14981 let _fake_server = fake_servers.next().await.unwrap();
14982 update_test_language_settings(cx, |language_settings| {
14983 language_settings.languages.0.insert(
14984 language_name.clone(),
14985 LanguageSettingsContent {
14986 tab_size: NonZeroU32::new(8),
14987 ..Default::default()
14988 },
14989 );
14990 });
14991 cx.executor().run_until_parked();
14992 assert_eq!(
14993 server_restarts.load(atomic::Ordering::Acquire),
14994 0,
14995 "Should not restart LSP server on an unrelated change"
14996 );
14997
14998 update_test_project_settings(cx, |project_settings| {
14999 project_settings.lsp.insert(
15000 "Some other server name".into(),
15001 LspSettings {
15002 binary: None,
15003 settings: None,
15004 initialization_options: Some(json!({
15005 "some other init value": false
15006 })),
15007 enable_lsp_tasks: false,
15008 },
15009 );
15010 });
15011 cx.executor().run_until_parked();
15012 assert_eq!(
15013 server_restarts.load(atomic::Ordering::Acquire),
15014 0,
15015 "Should not restart LSP server on an unrelated LSP settings change"
15016 );
15017
15018 update_test_project_settings(cx, |project_settings| {
15019 project_settings.lsp.insert(
15020 language_server_name.into(),
15021 LspSettings {
15022 binary: None,
15023 settings: None,
15024 initialization_options: Some(json!({
15025 "anotherInitValue": false
15026 })),
15027 enable_lsp_tasks: false,
15028 },
15029 );
15030 });
15031 cx.executor().run_until_parked();
15032 assert_eq!(
15033 server_restarts.load(atomic::Ordering::Acquire),
15034 1,
15035 "Should restart LSP server on a related LSP settings change"
15036 );
15037
15038 update_test_project_settings(cx, |project_settings| {
15039 project_settings.lsp.insert(
15040 language_server_name.into(),
15041 LspSettings {
15042 binary: None,
15043 settings: None,
15044 initialization_options: Some(json!({
15045 "anotherInitValue": false
15046 })),
15047 enable_lsp_tasks: false,
15048 },
15049 );
15050 });
15051 cx.executor().run_until_parked();
15052 assert_eq!(
15053 server_restarts.load(atomic::Ordering::Acquire),
15054 1,
15055 "Should not restart LSP server on a related LSP settings change that is the same"
15056 );
15057
15058 update_test_project_settings(cx, |project_settings| {
15059 project_settings.lsp.insert(
15060 language_server_name.into(),
15061 LspSettings {
15062 binary: None,
15063 settings: None,
15064 initialization_options: None,
15065 enable_lsp_tasks: false,
15066 },
15067 );
15068 });
15069 cx.executor().run_until_parked();
15070 assert_eq!(
15071 server_restarts.load(atomic::Ordering::Acquire),
15072 2,
15073 "Should restart LSP server on another related LSP settings change"
15074 );
15075}
15076
15077#[gpui::test]
15078async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15079 init_test(cx, |_| {});
15080
15081 let mut cx = EditorLspTestContext::new_rust(
15082 lsp::ServerCapabilities {
15083 completion_provider: Some(lsp::CompletionOptions {
15084 trigger_characters: Some(vec![".".to_string()]),
15085 resolve_provider: Some(true),
15086 ..Default::default()
15087 }),
15088 ..Default::default()
15089 },
15090 cx,
15091 )
15092 .await;
15093
15094 cx.set_state("fn main() { let a = 2ˇ; }");
15095 cx.simulate_keystroke(".");
15096 let completion_item = lsp::CompletionItem {
15097 label: "some".into(),
15098 kind: Some(lsp::CompletionItemKind::SNIPPET),
15099 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15100 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15101 kind: lsp::MarkupKind::Markdown,
15102 value: "```rust\nSome(2)\n```".to_string(),
15103 })),
15104 deprecated: Some(false),
15105 sort_text: Some("fffffff2".to_string()),
15106 filter_text: Some("some".to_string()),
15107 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15108 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15109 range: lsp::Range {
15110 start: lsp::Position {
15111 line: 0,
15112 character: 22,
15113 },
15114 end: lsp::Position {
15115 line: 0,
15116 character: 22,
15117 },
15118 },
15119 new_text: "Some(2)".to_string(),
15120 })),
15121 additional_text_edits: Some(vec![lsp::TextEdit {
15122 range: lsp::Range {
15123 start: lsp::Position {
15124 line: 0,
15125 character: 20,
15126 },
15127 end: lsp::Position {
15128 line: 0,
15129 character: 22,
15130 },
15131 },
15132 new_text: "".to_string(),
15133 }]),
15134 ..Default::default()
15135 };
15136
15137 let closure_completion_item = completion_item.clone();
15138 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15139 let task_completion_item = closure_completion_item.clone();
15140 async move {
15141 Ok(Some(lsp::CompletionResponse::Array(vec![
15142 task_completion_item,
15143 ])))
15144 }
15145 });
15146
15147 request.next().await;
15148
15149 cx.condition(|editor, _| editor.context_menu_visible())
15150 .await;
15151 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15152 editor
15153 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15154 .unwrap()
15155 });
15156 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
15157
15158 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15159 let task_completion_item = completion_item.clone();
15160 async move { Ok(task_completion_item) }
15161 })
15162 .next()
15163 .await
15164 .unwrap();
15165 apply_additional_edits.await.unwrap();
15166 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
15167}
15168
15169#[gpui::test]
15170async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
15171 init_test(cx, |_| {});
15172
15173 let mut cx = EditorLspTestContext::new_rust(
15174 lsp::ServerCapabilities {
15175 completion_provider: Some(lsp::CompletionOptions {
15176 trigger_characters: Some(vec![".".to_string()]),
15177 resolve_provider: Some(true),
15178 ..Default::default()
15179 }),
15180 ..Default::default()
15181 },
15182 cx,
15183 )
15184 .await;
15185
15186 cx.set_state("fn main() { let a = 2ˇ; }");
15187 cx.simulate_keystroke(".");
15188
15189 let item1 = lsp::CompletionItem {
15190 label: "method id()".to_string(),
15191 filter_text: Some("id".to_string()),
15192 detail: None,
15193 documentation: None,
15194 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15195 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15196 new_text: ".id".to_string(),
15197 })),
15198 ..lsp::CompletionItem::default()
15199 };
15200
15201 let item2 = lsp::CompletionItem {
15202 label: "other".to_string(),
15203 filter_text: Some("other".to_string()),
15204 detail: None,
15205 documentation: None,
15206 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15207 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15208 new_text: ".other".to_string(),
15209 })),
15210 ..lsp::CompletionItem::default()
15211 };
15212
15213 let item1 = item1.clone();
15214 cx.set_request_handler::<lsp::request::Completion, _, _>({
15215 let item1 = item1.clone();
15216 move |_, _, _| {
15217 let item1 = item1.clone();
15218 let item2 = item2.clone();
15219 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
15220 }
15221 })
15222 .next()
15223 .await;
15224
15225 cx.condition(|editor, _| editor.context_menu_visible())
15226 .await;
15227 cx.update_editor(|editor, _, _| {
15228 let context_menu = editor.context_menu.borrow_mut();
15229 let context_menu = context_menu
15230 .as_ref()
15231 .expect("Should have the context menu deployed");
15232 match context_menu {
15233 CodeContextMenu::Completions(completions_menu) => {
15234 let completions = completions_menu.completions.borrow_mut();
15235 assert_eq!(
15236 completions
15237 .iter()
15238 .map(|completion| &completion.label.text)
15239 .collect::<Vec<_>>(),
15240 vec!["method id()", "other"]
15241 )
15242 }
15243 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15244 }
15245 });
15246
15247 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
15248 let item1 = item1.clone();
15249 move |_, item_to_resolve, _| {
15250 let item1 = item1.clone();
15251 async move {
15252 if item1 == item_to_resolve {
15253 Ok(lsp::CompletionItem {
15254 label: "method id()".to_string(),
15255 filter_text: Some("id".to_string()),
15256 detail: Some("Now resolved!".to_string()),
15257 documentation: Some(lsp::Documentation::String("Docs".to_string())),
15258 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15259 range: lsp::Range::new(
15260 lsp::Position::new(0, 22),
15261 lsp::Position::new(0, 22),
15262 ),
15263 new_text: ".id".to_string(),
15264 })),
15265 ..lsp::CompletionItem::default()
15266 })
15267 } else {
15268 Ok(item_to_resolve)
15269 }
15270 }
15271 }
15272 })
15273 .next()
15274 .await
15275 .unwrap();
15276 cx.run_until_parked();
15277
15278 cx.update_editor(|editor, window, cx| {
15279 editor.context_menu_next(&Default::default(), window, cx);
15280 });
15281
15282 cx.update_editor(|editor, _, _| {
15283 let context_menu = editor.context_menu.borrow_mut();
15284 let context_menu = context_menu
15285 .as_ref()
15286 .expect("Should have the context menu deployed");
15287 match context_menu {
15288 CodeContextMenu::Completions(completions_menu) => {
15289 let completions = completions_menu.completions.borrow_mut();
15290 assert_eq!(
15291 completions
15292 .iter()
15293 .map(|completion| &completion.label.text)
15294 .collect::<Vec<_>>(),
15295 vec!["method id() Now resolved!", "other"],
15296 "Should update first completion label, but not second as the filter text did not match."
15297 );
15298 }
15299 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15300 }
15301 });
15302}
15303
15304#[gpui::test]
15305async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
15306 init_test(cx, |_| {});
15307 let mut cx = EditorLspTestContext::new_rust(
15308 lsp::ServerCapabilities {
15309 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
15310 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
15311 completion_provider: Some(lsp::CompletionOptions {
15312 resolve_provider: Some(true),
15313 ..Default::default()
15314 }),
15315 ..Default::default()
15316 },
15317 cx,
15318 )
15319 .await;
15320 cx.set_state(indoc! {"
15321 struct TestStruct {
15322 field: i32
15323 }
15324
15325 fn mainˇ() {
15326 let unused_var = 42;
15327 let test_struct = TestStruct { field: 42 };
15328 }
15329 "});
15330 let symbol_range = cx.lsp_range(indoc! {"
15331 struct TestStruct {
15332 field: i32
15333 }
15334
15335 «fn main»() {
15336 let unused_var = 42;
15337 let test_struct = TestStruct { field: 42 };
15338 }
15339 "});
15340 let mut hover_requests =
15341 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15342 Ok(Some(lsp::Hover {
15343 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
15344 kind: lsp::MarkupKind::Markdown,
15345 value: "Function documentation".to_string(),
15346 }),
15347 range: Some(symbol_range),
15348 }))
15349 });
15350
15351 // Case 1: Test that code action menu hide hover popover
15352 cx.dispatch_action(Hover);
15353 hover_requests.next().await;
15354 cx.condition(|editor, _| editor.hover_state.visible()).await;
15355 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
15356 move |_, _, _| async move {
15357 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
15358 lsp::CodeAction {
15359 title: "Remove unused variable".to_string(),
15360 kind: Some(CodeActionKind::QUICKFIX),
15361 edit: Some(lsp::WorkspaceEdit {
15362 changes: Some(
15363 [(
15364 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
15365 vec![lsp::TextEdit {
15366 range: lsp::Range::new(
15367 lsp::Position::new(5, 4),
15368 lsp::Position::new(5, 27),
15369 ),
15370 new_text: "".to_string(),
15371 }],
15372 )]
15373 .into_iter()
15374 .collect(),
15375 ),
15376 ..Default::default()
15377 }),
15378 ..Default::default()
15379 },
15380 )]))
15381 },
15382 );
15383 cx.update_editor(|editor, window, cx| {
15384 editor.toggle_code_actions(
15385 &ToggleCodeActions {
15386 deployed_from: None,
15387 quick_launch: false,
15388 },
15389 window,
15390 cx,
15391 );
15392 });
15393 code_action_requests.next().await;
15394 cx.run_until_parked();
15395 cx.condition(|editor, _| editor.context_menu_visible())
15396 .await;
15397 cx.update_editor(|editor, _, _| {
15398 assert!(
15399 !editor.hover_state.visible(),
15400 "Hover popover should be hidden when code action menu is shown"
15401 );
15402 // Hide code actions
15403 editor.context_menu.take();
15404 });
15405
15406 // Case 2: Test that code completions hide hover popover
15407 cx.dispatch_action(Hover);
15408 hover_requests.next().await;
15409 cx.condition(|editor, _| editor.hover_state.visible()).await;
15410 let counter = Arc::new(AtomicUsize::new(0));
15411 let mut completion_requests =
15412 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15413 let counter = counter.clone();
15414 async move {
15415 counter.fetch_add(1, atomic::Ordering::Release);
15416 Ok(Some(lsp::CompletionResponse::Array(vec![
15417 lsp::CompletionItem {
15418 label: "main".into(),
15419 kind: Some(lsp::CompletionItemKind::FUNCTION),
15420 detail: Some("() -> ()".to_string()),
15421 ..Default::default()
15422 },
15423 lsp::CompletionItem {
15424 label: "TestStruct".into(),
15425 kind: Some(lsp::CompletionItemKind::STRUCT),
15426 detail: Some("struct TestStruct".to_string()),
15427 ..Default::default()
15428 },
15429 ])))
15430 }
15431 });
15432 cx.update_editor(|editor, window, cx| {
15433 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15434 });
15435 completion_requests.next().await;
15436 cx.condition(|editor, _| editor.context_menu_visible())
15437 .await;
15438 cx.update_editor(|editor, _, _| {
15439 assert!(
15440 !editor.hover_state.visible(),
15441 "Hover popover should be hidden when completion menu is shown"
15442 );
15443 });
15444}
15445
15446#[gpui::test]
15447async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
15448 init_test(cx, |_| {});
15449
15450 let mut cx = EditorLspTestContext::new_rust(
15451 lsp::ServerCapabilities {
15452 completion_provider: Some(lsp::CompletionOptions {
15453 trigger_characters: Some(vec![".".to_string()]),
15454 resolve_provider: Some(true),
15455 ..Default::default()
15456 }),
15457 ..Default::default()
15458 },
15459 cx,
15460 )
15461 .await;
15462
15463 cx.set_state("fn main() { let a = 2ˇ; }");
15464 cx.simulate_keystroke(".");
15465
15466 let unresolved_item_1 = lsp::CompletionItem {
15467 label: "id".to_string(),
15468 filter_text: Some("id".to_string()),
15469 detail: None,
15470 documentation: None,
15471 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15472 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15473 new_text: ".id".to_string(),
15474 })),
15475 ..lsp::CompletionItem::default()
15476 };
15477 let resolved_item_1 = lsp::CompletionItem {
15478 additional_text_edits: Some(vec![lsp::TextEdit {
15479 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15480 new_text: "!!".to_string(),
15481 }]),
15482 ..unresolved_item_1.clone()
15483 };
15484 let unresolved_item_2 = lsp::CompletionItem {
15485 label: "other".to_string(),
15486 filter_text: Some("other".to_string()),
15487 detail: None,
15488 documentation: None,
15489 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15490 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15491 new_text: ".other".to_string(),
15492 })),
15493 ..lsp::CompletionItem::default()
15494 };
15495 let resolved_item_2 = lsp::CompletionItem {
15496 additional_text_edits: Some(vec![lsp::TextEdit {
15497 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15498 new_text: "??".to_string(),
15499 }]),
15500 ..unresolved_item_2.clone()
15501 };
15502
15503 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
15504 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
15505 cx.lsp
15506 .server
15507 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15508 let unresolved_item_1 = unresolved_item_1.clone();
15509 let resolved_item_1 = resolved_item_1.clone();
15510 let unresolved_item_2 = unresolved_item_2.clone();
15511 let resolved_item_2 = resolved_item_2.clone();
15512 let resolve_requests_1 = resolve_requests_1.clone();
15513 let resolve_requests_2 = resolve_requests_2.clone();
15514 move |unresolved_request, _| {
15515 let unresolved_item_1 = unresolved_item_1.clone();
15516 let resolved_item_1 = resolved_item_1.clone();
15517 let unresolved_item_2 = unresolved_item_2.clone();
15518 let resolved_item_2 = resolved_item_2.clone();
15519 let resolve_requests_1 = resolve_requests_1.clone();
15520 let resolve_requests_2 = resolve_requests_2.clone();
15521 async move {
15522 if unresolved_request == unresolved_item_1 {
15523 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
15524 Ok(resolved_item_1.clone())
15525 } else if unresolved_request == unresolved_item_2 {
15526 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
15527 Ok(resolved_item_2.clone())
15528 } else {
15529 panic!("Unexpected completion item {unresolved_request:?}")
15530 }
15531 }
15532 }
15533 })
15534 .detach();
15535
15536 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15537 let unresolved_item_1 = unresolved_item_1.clone();
15538 let unresolved_item_2 = unresolved_item_2.clone();
15539 async move {
15540 Ok(Some(lsp::CompletionResponse::Array(vec![
15541 unresolved_item_1,
15542 unresolved_item_2,
15543 ])))
15544 }
15545 })
15546 .next()
15547 .await;
15548
15549 cx.condition(|editor, _| editor.context_menu_visible())
15550 .await;
15551 cx.update_editor(|editor, _, _| {
15552 let context_menu = editor.context_menu.borrow_mut();
15553 let context_menu = context_menu
15554 .as_ref()
15555 .expect("Should have the context menu deployed");
15556 match context_menu {
15557 CodeContextMenu::Completions(completions_menu) => {
15558 let completions = completions_menu.completions.borrow_mut();
15559 assert_eq!(
15560 completions
15561 .iter()
15562 .map(|completion| &completion.label.text)
15563 .collect::<Vec<_>>(),
15564 vec!["id", "other"]
15565 )
15566 }
15567 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15568 }
15569 });
15570 cx.run_until_parked();
15571
15572 cx.update_editor(|editor, window, cx| {
15573 editor.context_menu_next(&ContextMenuNext, window, cx);
15574 });
15575 cx.run_until_parked();
15576 cx.update_editor(|editor, window, cx| {
15577 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15578 });
15579 cx.run_until_parked();
15580 cx.update_editor(|editor, window, cx| {
15581 editor.context_menu_next(&ContextMenuNext, window, cx);
15582 });
15583 cx.run_until_parked();
15584 cx.update_editor(|editor, window, cx| {
15585 editor
15586 .compose_completion(&ComposeCompletion::default(), window, cx)
15587 .expect("No task returned")
15588 })
15589 .await
15590 .expect("Completion failed");
15591 cx.run_until_parked();
15592
15593 cx.update_editor(|editor, _, cx| {
15594 assert_eq!(
15595 resolve_requests_1.load(atomic::Ordering::Acquire),
15596 1,
15597 "Should always resolve once despite multiple selections"
15598 );
15599 assert_eq!(
15600 resolve_requests_2.load(atomic::Ordering::Acquire),
15601 1,
15602 "Should always resolve once after multiple selections and applying the completion"
15603 );
15604 assert_eq!(
15605 editor.text(cx),
15606 "fn main() { let a = ??.other; }",
15607 "Should use resolved data when applying the completion"
15608 );
15609 });
15610}
15611
15612#[gpui::test]
15613async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
15614 init_test(cx, |_| {});
15615
15616 let item_0 = lsp::CompletionItem {
15617 label: "abs".into(),
15618 insert_text: Some("abs".into()),
15619 data: Some(json!({ "very": "special"})),
15620 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
15621 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15622 lsp::InsertReplaceEdit {
15623 new_text: "abs".to_string(),
15624 insert: lsp::Range::default(),
15625 replace: lsp::Range::default(),
15626 },
15627 )),
15628 ..lsp::CompletionItem::default()
15629 };
15630 let items = iter::once(item_0.clone())
15631 .chain((11..51).map(|i| lsp::CompletionItem {
15632 label: format!("item_{}", i),
15633 insert_text: Some(format!("item_{}", i)),
15634 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15635 ..lsp::CompletionItem::default()
15636 }))
15637 .collect::<Vec<_>>();
15638
15639 let default_commit_characters = vec!["?".to_string()];
15640 let default_data = json!({ "default": "data"});
15641 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
15642 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
15643 let default_edit_range = lsp::Range {
15644 start: lsp::Position {
15645 line: 0,
15646 character: 5,
15647 },
15648 end: lsp::Position {
15649 line: 0,
15650 character: 5,
15651 },
15652 };
15653
15654 let mut cx = EditorLspTestContext::new_rust(
15655 lsp::ServerCapabilities {
15656 completion_provider: Some(lsp::CompletionOptions {
15657 trigger_characters: Some(vec![".".to_string()]),
15658 resolve_provider: Some(true),
15659 ..Default::default()
15660 }),
15661 ..Default::default()
15662 },
15663 cx,
15664 )
15665 .await;
15666
15667 cx.set_state("fn main() { let a = 2ˇ; }");
15668 cx.simulate_keystroke(".");
15669
15670 let completion_data = default_data.clone();
15671 let completion_characters = default_commit_characters.clone();
15672 let completion_items = items.clone();
15673 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15674 let default_data = completion_data.clone();
15675 let default_commit_characters = completion_characters.clone();
15676 let items = completion_items.clone();
15677 async move {
15678 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15679 items,
15680 item_defaults: Some(lsp::CompletionListItemDefaults {
15681 data: Some(default_data.clone()),
15682 commit_characters: Some(default_commit_characters.clone()),
15683 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
15684 default_edit_range,
15685 )),
15686 insert_text_format: Some(default_insert_text_format),
15687 insert_text_mode: Some(default_insert_text_mode),
15688 }),
15689 ..lsp::CompletionList::default()
15690 })))
15691 }
15692 })
15693 .next()
15694 .await;
15695
15696 let resolved_items = Arc::new(Mutex::new(Vec::new()));
15697 cx.lsp
15698 .server
15699 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15700 let closure_resolved_items = resolved_items.clone();
15701 move |item_to_resolve, _| {
15702 let closure_resolved_items = closure_resolved_items.clone();
15703 async move {
15704 closure_resolved_items.lock().push(item_to_resolve.clone());
15705 Ok(item_to_resolve)
15706 }
15707 }
15708 })
15709 .detach();
15710
15711 cx.condition(|editor, _| editor.context_menu_visible())
15712 .await;
15713 cx.run_until_parked();
15714 cx.update_editor(|editor, _, _| {
15715 let menu = editor.context_menu.borrow_mut();
15716 match menu.as_ref().expect("should have the completions menu") {
15717 CodeContextMenu::Completions(completions_menu) => {
15718 assert_eq!(
15719 completions_menu
15720 .entries
15721 .borrow()
15722 .iter()
15723 .map(|mat| mat.string.clone())
15724 .collect::<Vec<String>>(),
15725 items
15726 .iter()
15727 .map(|completion| completion.label.clone())
15728 .collect::<Vec<String>>()
15729 );
15730 }
15731 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
15732 }
15733 });
15734 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
15735 // with 4 from the end.
15736 assert_eq!(
15737 *resolved_items.lock(),
15738 [&items[0..16], &items[items.len() - 4..items.len()]]
15739 .concat()
15740 .iter()
15741 .cloned()
15742 .map(|mut item| {
15743 if item.data.is_none() {
15744 item.data = Some(default_data.clone());
15745 }
15746 item
15747 })
15748 .collect::<Vec<lsp::CompletionItem>>(),
15749 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
15750 );
15751 resolved_items.lock().clear();
15752
15753 cx.update_editor(|editor, window, cx| {
15754 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15755 });
15756 cx.run_until_parked();
15757 // Completions that have already been resolved are skipped.
15758 assert_eq!(
15759 *resolved_items.lock(),
15760 items[items.len() - 17..items.len() - 4]
15761 .iter()
15762 .cloned()
15763 .map(|mut item| {
15764 if item.data.is_none() {
15765 item.data = Some(default_data.clone());
15766 }
15767 item
15768 })
15769 .collect::<Vec<lsp::CompletionItem>>()
15770 );
15771 resolved_items.lock().clear();
15772}
15773
15774#[gpui::test]
15775async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
15776 init_test(cx, |_| {});
15777
15778 let mut cx = EditorLspTestContext::new(
15779 Language::new(
15780 LanguageConfig {
15781 matcher: LanguageMatcher {
15782 path_suffixes: vec!["jsx".into()],
15783 ..Default::default()
15784 },
15785 overrides: [(
15786 "element".into(),
15787 LanguageConfigOverride {
15788 completion_query_characters: Override::Set(['-'].into_iter().collect()),
15789 ..Default::default()
15790 },
15791 )]
15792 .into_iter()
15793 .collect(),
15794 ..Default::default()
15795 },
15796 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15797 )
15798 .with_override_query("(jsx_self_closing_element) @element")
15799 .unwrap(),
15800 lsp::ServerCapabilities {
15801 completion_provider: Some(lsp::CompletionOptions {
15802 trigger_characters: Some(vec![":".to_string()]),
15803 ..Default::default()
15804 }),
15805 ..Default::default()
15806 },
15807 cx,
15808 )
15809 .await;
15810
15811 cx.lsp
15812 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15813 Ok(Some(lsp::CompletionResponse::Array(vec![
15814 lsp::CompletionItem {
15815 label: "bg-blue".into(),
15816 ..Default::default()
15817 },
15818 lsp::CompletionItem {
15819 label: "bg-red".into(),
15820 ..Default::default()
15821 },
15822 lsp::CompletionItem {
15823 label: "bg-yellow".into(),
15824 ..Default::default()
15825 },
15826 ])))
15827 });
15828
15829 cx.set_state(r#"<p class="bgˇ" />"#);
15830
15831 // Trigger completion when typing a dash, because the dash is an extra
15832 // word character in the 'element' scope, which contains the cursor.
15833 cx.simulate_keystroke("-");
15834 cx.executor().run_until_parked();
15835 cx.update_editor(|editor, _, _| {
15836 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15837 {
15838 assert_eq!(
15839 completion_menu_entries(&menu),
15840 &["bg-blue", "bg-red", "bg-yellow"]
15841 );
15842 } else {
15843 panic!("expected completion menu to be open");
15844 }
15845 });
15846
15847 cx.simulate_keystroke("l");
15848 cx.executor().run_until_parked();
15849 cx.update_editor(|editor, _, _| {
15850 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15851 {
15852 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
15853 } else {
15854 panic!("expected completion menu to be open");
15855 }
15856 });
15857
15858 // When filtering completions, consider the character after the '-' to
15859 // be the start of a subword.
15860 cx.set_state(r#"<p class="yelˇ" />"#);
15861 cx.simulate_keystroke("l");
15862 cx.executor().run_until_parked();
15863 cx.update_editor(|editor, _, _| {
15864 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15865 {
15866 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
15867 } else {
15868 panic!("expected completion menu to be open");
15869 }
15870 });
15871}
15872
15873fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
15874 let entries = menu.entries.borrow();
15875 entries.iter().map(|mat| mat.string.clone()).collect()
15876}
15877
15878#[gpui::test]
15879async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
15880 init_test(cx, |settings| {
15881 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![
15882 Formatter::Prettier,
15883 ]))
15884 });
15885
15886 let fs = FakeFs::new(cx.executor());
15887 fs.insert_file(path!("/file.ts"), Default::default()).await;
15888
15889 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
15890 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15891
15892 language_registry.add(Arc::new(Language::new(
15893 LanguageConfig {
15894 name: "TypeScript".into(),
15895 matcher: LanguageMatcher {
15896 path_suffixes: vec!["ts".to_string()],
15897 ..Default::default()
15898 },
15899 ..Default::default()
15900 },
15901 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15902 )));
15903 update_test_language_settings(cx, |settings| {
15904 settings.defaults.prettier = Some(PrettierSettings {
15905 allowed: true,
15906 ..PrettierSettings::default()
15907 });
15908 });
15909
15910 let test_plugin = "test_plugin";
15911 let _ = language_registry.register_fake_lsp(
15912 "TypeScript",
15913 FakeLspAdapter {
15914 prettier_plugins: vec![test_plugin],
15915 ..Default::default()
15916 },
15917 );
15918
15919 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
15920 let buffer = project
15921 .update(cx, |project, cx| {
15922 project.open_local_buffer(path!("/file.ts"), cx)
15923 })
15924 .await
15925 .unwrap();
15926
15927 let buffer_text = "one\ntwo\nthree\n";
15928 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
15929 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
15930 editor.update_in(cx, |editor, window, cx| {
15931 editor.set_text(buffer_text, window, cx)
15932 });
15933
15934 editor
15935 .update_in(cx, |editor, window, cx| {
15936 editor.perform_format(
15937 project.clone(),
15938 FormatTrigger::Manual,
15939 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
15940 window,
15941 cx,
15942 )
15943 })
15944 .unwrap()
15945 .await;
15946 assert_eq!(
15947 editor.update(cx, |editor, cx| editor.text(cx)),
15948 buffer_text.to_string() + prettier_format_suffix,
15949 "Test prettier formatting was not applied to the original buffer text",
15950 );
15951
15952 update_test_language_settings(cx, |settings| {
15953 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
15954 });
15955 let format = editor.update_in(cx, |editor, window, cx| {
15956 editor.perform_format(
15957 project.clone(),
15958 FormatTrigger::Manual,
15959 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
15960 window,
15961 cx,
15962 )
15963 });
15964 format.await.unwrap();
15965 assert_eq!(
15966 editor.update(cx, |editor, cx| editor.text(cx)),
15967 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
15968 "Autoformatting (via test prettier) was not applied to the original buffer text",
15969 );
15970}
15971
15972#[gpui::test]
15973async fn test_addition_reverts(cx: &mut TestAppContext) {
15974 init_test(cx, |_| {});
15975 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15976 let base_text = indoc! {r#"
15977 struct Row;
15978 struct Row1;
15979 struct Row2;
15980
15981 struct Row4;
15982 struct Row5;
15983 struct Row6;
15984
15985 struct Row8;
15986 struct Row9;
15987 struct Row10;"#};
15988
15989 // When addition hunks are not adjacent to carets, no hunk revert is performed
15990 assert_hunk_revert(
15991 indoc! {r#"struct Row;
15992 struct Row1;
15993 struct Row1.1;
15994 struct Row1.2;
15995 struct Row2;ˇ
15996
15997 struct Row4;
15998 struct Row5;
15999 struct Row6;
16000
16001 struct Row8;
16002 ˇstruct Row9;
16003 struct Row9.1;
16004 struct Row9.2;
16005 struct Row9.3;
16006 struct Row10;"#},
16007 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16008 indoc! {r#"struct Row;
16009 struct Row1;
16010 struct Row1.1;
16011 struct Row1.2;
16012 struct Row2;ˇ
16013
16014 struct Row4;
16015 struct Row5;
16016 struct Row6;
16017
16018 struct Row8;
16019 ˇstruct Row9;
16020 struct Row9.1;
16021 struct Row9.2;
16022 struct Row9.3;
16023 struct Row10;"#},
16024 base_text,
16025 &mut cx,
16026 );
16027 // Same for selections
16028 assert_hunk_revert(
16029 indoc! {r#"struct Row;
16030 struct Row1;
16031 struct Row2;
16032 struct Row2.1;
16033 struct Row2.2;
16034 «ˇ
16035 struct Row4;
16036 struct» Row5;
16037 «struct Row6;
16038 ˇ»
16039 struct Row9.1;
16040 struct Row9.2;
16041 struct Row9.3;
16042 struct Row8;
16043 struct Row9;
16044 struct Row10;"#},
16045 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16046 indoc! {r#"struct Row;
16047 struct Row1;
16048 struct Row2;
16049 struct Row2.1;
16050 struct Row2.2;
16051 «ˇ
16052 struct Row4;
16053 struct» Row5;
16054 «struct Row6;
16055 ˇ»
16056 struct Row9.1;
16057 struct Row9.2;
16058 struct Row9.3;
16059 struct Row8;
16060 struct Row9;
16061 struct Row10;"#},
16062 base_text,
16063 &mut cx,
16064 );
16065
16066 // When carets and selections intersect the addition hunks, those are reverted.
16067 // Adjacent carets got merged.
16068 assert_hunk_revert(
16069 indoc! {r#"struct Row;
16070 ˇ// something on the top
16071 struct Row1;
16072 struct Row2;
16073 struct Roˇw3.1;
16074 struct Row2.2;
16075 struct Row2.3;ˇ
16076
16077 struct Row4;
16078 struct ˇRow5.1;
16079 struct Row5.2;
16080 struct «Rowˇ»5.3;
16081 struct Row5;
16082 struct Row6;
16083 ˇ
16084 struct Row9.1;
16085 struct «Rowˇ»9.2;
16086 struct «ˇRow»9.3;
16087 struct Row8;
16088 struct Row9;
16089 «ˇ// something on bottom»
16090 struct Row10;"#},
16091 vec![
16092 DiffHunkStatusKind::Added,
16093 DiffHunkStatusKind::Added,
16094 DiffHunkStatusKind::Added,
16095 DiffHunkStatusKind::Added,
16096 DiffHunkStatusKind::Added,
16097 ],
16098 indoc! {r#"struct Row;
16099 ˇstruct Row1;
16100 struct Row2;
16101 ˇ
16102 struct Row4;
16103 ˇstruct Row5;
16104 struct Row6;
16105 ˇ
16106 ˇstruct Row8;
16107 struct Row9;
16108 ˇstruct Row10;"#},
16109 base_text,
16110 &mut cx,
16111 );
16112}
16113
16114#[gpui::test]
16115async fn test_modification_reverts(cx: &mut TestAppContext) {
16116 init_test(cx, |_| {});
16117 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16118 let base_text = indoc! {r#"
16119 struct Row;
16120 struct Row1;
16121 struct Row2;
16122
16123 struct Row4;
16124 struct Row5;
16125 struct Row6;
16126
16127 struct Row8;
16128 struct Row9;
16129 struct Row10;"#};
16130
16131 // Modification hunks behave the same as the addition ones.
16132 assert_hunk_revert(
16133 indoc! {r#"struct Row;
16134 struct Row1;
16135 struct Row33;
16136 ˇ
16137 struct Row4;
16138 struct Row5;
16139 struct Row6;
16140 ˇ
16141 struct Row99;
16142 struct Row9;
16143 struct Row10;"#},
16144 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16145 indoc! {r#"struct Row;
16146 struct Row1;
16147 struct Row33;
16148 ˇ
16149 struct Row4;
16150 struct Row5;
16151 struct Row6;
16152 ˇ
16153 struct Row99;
16154 struct Row9;
16155 struct Row10;"#},
16156 base_text,
16157 &mut cx,
16158 );
16159 assert_hunk_revert(
16160 indoc! {r#"struct Row;
16161 struct Row1;
16162 struct Row33;
16163 «ˇ
16164 struct Row4;
16165 struct» Row5;
16166 «struct Row6;
16167 ˇ»
16168 struct Row99;
16169 struct Row9;
16170 struct Row10;"#},
16171 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16172 indoc! {r#"struct Row;
16173 struct Row1;
16174 struct Row33;
16175 «ˇ
16176 struct Row4;
16177 struct» Row5;
16178 «struct Row6;
16179 ˇ»
16180 struct Row99;
16181 struct Row9;
16182 struct Row10;"#},
16183 base_text,
16184 &mut cx,
16185 );
16186
16187 assert_hunk_revert(
16188 indoc! {r#"ˇstruct Row1.1;
16189 struct Row1;
16190 «ˇstr»uct Row22;
16191
16192 struct ˇRow44;
16193 struct Row5;
16194 struct «Rˇ»ow66;ˇ
16195
16196 «struˇ»ct Row88;
16197 struct Row9;
16198 struct Row1011;ˇ"#},
16199 vec![
16200 DiffHunkStatusKind::Modified,
16201 DiffHunkStatusKind::Modified,
16202 DiffHunkStatusKind::Modified,
16203 DiffHunkStatusKind::Modified,
16204 DiffHunkStatusKind::Modified,
16205 DiffHunkStatusKind::Modified,
16206 ],
16207 indoc! {r#"struct Row;
16208 ˇstruct Row1;
16209 struct Row2;
16210 ˇ
16211 struct Row4;
16212 ˇstruct Row5;
16213 struct Row6;
16214 ˇ
16215 struct Row8;
16216 ˇstruct Row9;
16217 struct Row10;ˇ"#},
16218 base_text,
16219 &mut cx,
16220 );
16221}
16222
16223#[gpui::test]
16224async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
16225 init_test(cx, |_| {});
16226 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16227 let base_text = indoc! {r#"
16228 one
16229
16230 two
16231 three
16232 "#};
16233
16234 cx.set_head_text(base_text);
16235 cx.set_state("\nˇ\n");
16236 cx.executor().run_until_parked();
16237 cx.update_editor(|editor, _window, cx| {
16238 editor.expand_selected_diff_hunks(cx);
16239 });
16240 cx.executor().run_until_parked();
16241 cx.update_editor(|editor, window, cx| {
16242 editor.backspace(&Default::default(), window, cx);
16243 });
16244 cx.run_until_parked();
16245 cx.assert_state_with_diff(
16246 indoc! {r#"
16247
16248 - two
16249 - threeˇ
16250 +
16251 "#}
16252 .to_string(),
16253 );
16254}
16255
16256#[gpui::test]
16257async fn test_deletion_reverts(cx: &mut TestAppContext) {
16258 init_test(cx, |_| {});
16259 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16260 let base_text = indoc! {r#"struct Row;
16261struct Row1;
16262struct Row2;
16263
16264struct Row4;
16265struct Row5;
16266struct Row6;
16267
16268struct Row8;
16269struct Row9;
16270struct Row10;"#};
16271
16272 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
16273 assert_hunk_revert(
16274 indoc! {r#"struct Row;
16275 struct Row2;
16276
16277 ˇstruct Row4;
16278 struct Row5;
16279 struct Row6;
16280 ˇ
16281 struct Row8;
16282 struct Row10;"#},
16283 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16284 indoc! {r#"struct Row;
16285 struct Row2;
16286
16287 ˇstruct Row4;
16288 struct Row5;
16289 struct Row6;
16290 ˇ
16291 struct Row8;
16292 struct Row10;"#},
16293 base_text,
16294 &mut cx,
16295 );
16296 assert_hunk_revert(
16297 indoc! {r#"struct Row;
16298 struct Row2;
16299
16300 «ˇstruct Row4;
16301 struct» Row5;
16302 «struct Row6;
16303 ˇ»
16304 struct Row8;
16305 struct Row10;"#},
16306 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16307 indoc! {r#"struct Row;
16308 struct Row2;
16309
16310 «ˇstruct Row4;
16311 struct» Row5;
16312 «struct Row6;
16313 ˇ»
16314 struct Row8;
16315 struct Row10;"#},
16316 base_text,
16317 &mut cx,
16318 );
16319
16320 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16321 assert_hunk_revert(
16322 indoc! {r#"struct Row;
16323 ˇstruct Row2;
16324
16325 struct Row4;
16326 struct Row5;
16327 struct Row6;
16328
16329 struct Row8;ˇ
16330 struct Row10;"#},
16331 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16332 indoc! {r#"struct Row;
16333 struct Row1;
16334 ˇstruct Row2;
16335
16336 struct Row4;
16337 struct Row5;
16338 struct Row6;
16339
16340 struct Row8;ˇ
16341 struct Row9;
16342 struct Row10;"#},
16343 base_text,
16344 &mut cx,
16345 );
16346 assert_hunk_revert(
16347 indoc! {r#"struct Row;
16348 struct Row2«ˇ;
16349 struct Row4;
16350 struct» Row5;
16351 «struct Row6;
16352
16353 struct Row8;ˇ»
16354 struct Row10;"#},
16355 vec![
16356 DiffHunkStatusKind::Deleted,
16357 DiffHunkStatusKind::Deleted,
16358 DiffHunkStatusKind::Deleted,
16359 ],
16360 indoc! {r#"struct Row;
16361 struct Row1;
16362 struct Row2«ˇ;
16363
16364 struct Row4;
16365 struct» Row5;
16366 «struct Row6;
16367
16368 struct Row8;ˇ»
16369 struct Row9;
16370 struct Row10;"#},
16371 base_text,
16372 &mut cx,
16373 );
16374}
16375
16376#[gpui::test]
16377async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16378 init_test(cx, |_| {});
16379
16380 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16381 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16382 let base_text_3 =
16383 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16384
16385 let text_1 = edit_first_char_of_every_line(base_text_1);
16386 let text_2 = edit_first_char_of_every_line(base_text_2);
16387 let text_3 = edit_first_char_of_every_line(base_text_3);
16388
16389 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16390 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16391 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16392
16393 let multibuffer = cx.new(|cx| {
16394 let mut multibuffer = MultiBuffer::new(ReadWrite);
16395 multibuffer.push_excerpts(
16396 buffer_1.clone(),
16397 [
16398 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16399 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16400 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16401 ],
16402 cx,
16403 );
16404 multibuffer.push_excerpts(
16405 buffer_2.clone(),
16406 [
16407 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16408 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16409 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16410 ],
16411 cx,
16412 );
16413 multibuffer.push_excerpts(
16414 buffer_3.clone(),
16415 [
16416 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16417 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16418 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16419 ],
16420 cx,
16421 );
16422 multibuffer
16423 });
16424
16425 let fs = FakeFs::new(cx.executor());
16426 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
16427 let (editor, cx) = cx
16428 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
16429 editor.update_in(cx, |editor, _window, cx| {
16430 for (buffer, diff_base) in [
16431 (buffer_1.clone(), base_text_1),
16432 (buffer_2.clone(), base_text_2),
16433 (buffer_3.clone(), base_text_3),
16434 ] {
16435 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16436 editor
16437 .buffer
16438 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16439 }
16440 });
16441 cx.executor().run_until_parked();
16442
16443 editor.update_in(cx, |editor, window, cx| {
16444 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}");
16445 editor.select_all(&SelectAll, window, cx);
16446 editor.git_restore(&Default::default(), window, cx);
16447 });
16448 cx.executor().run_until_parked();
16449
16450 // When all ranges are selected, all buffer hunks are reverted.
16451 editor.update(cx, |editor, cx| {
16452 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");
16453 });
16454 buffer_1.update(cx, |buffer, _| {
16455 assert_eq!(buffer.text(), base_text_1);
16456 });
16457 buffer_2.update(cx, |buffer, _| {
16458 assert_eq!(buffer.text(), base_text_2);
16459 });
16460 buffer_3.update(cx, |buffer, _| {
16461 assert_eq!(buffer.text(), base_text_3);
16462 });
16463
16464 editor.update_in(cx, |editor, window, cx| {
16465 editor.undo(&Default::default(), window, cx);
16466 });
16467
16468 editor.update_in(cx, |editor, window, cx| {
16469 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16470 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
16471 });
16472 editor.git_restore(&Default::default(), window, cx);
16473 });
16474
16475 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
16476 // but not affect buffer_2 and its related excerpts.
16477 editor.update(cx, |editor, cx| {
16478 assert_eq!(
16479 editor.text(cx),
16480 "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}"
16481 );
16482 });
16483 buffer_1.update(cx, |buffer, _| {
16484 assert_eq!(buffer.text(), base_text_1);
16485 });
16486 buffer_2.update(cx, |buffer, _| {
16487 assert_eq!(
16488 buffer.text(),
16489 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
16490 );
16491 });
16492 buffer_3.update(cx, |buffer, _| {
16493 assert_eq!(
16494 buffer.text(),
16495 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
16496 );
16497 });
16498
16499 fn edit_first_char_of_every_line(text: &str) -> String {
16500 text.split('\n')
16501 .map(|line| format!("X{}", &line[1..]))
16502 .collect::<Vec<_>>()
16503 .join("\n")
16504 }
16505}
16506
16507#[gpui::test]
16508async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
16509 init_test(cx, |_| {});
16510
16511 let cols = 4;
16512 let rows = 10;
16513 let sample_text_1 = sample_text(rows, cols, 'a');
16514 assert_eq!(
16515 sample_text_1,
16516 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
16517 );
16518 let sample_text_2 = sample_text(rows, cols, 'l');
16519 assert_eq!(
16520 sample_text_2,
16521 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
16522 );
16523 let sample_text_3 = sample_text(rows, cols, 'v');
16524 assert_eq!(
16525 sample_text_3,
16526 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
16527 );
16528
16529 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
16530 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
16531 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
16532
16533 let multi_buffer = cx.new(|cx| {
16534 let mut multibuffer = MultiBuffer::new(ReadWrite);
16535 multibuffer.push_excerpts(
16536 buffer_1.clone(),
16537 [
16538 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16539 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16540 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16541 ],
16542 cx,
16543 );
16544 multibuffer.push_excerpts(
16545 buffer_2.clone(),
16546 [
16547 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16548 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16549 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16550 ],
16551 cx,
16552 );
16553 multibuffer.push_excerpts(
16554 buffer_3.clone(),
16555 [
16556 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16557 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16558 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16559 ],
16560 cx,
16561 );
16562 multibuffer
16563 });
16564
16565 let fs = FakeFs::new(cx.executor());
16566 fs.insert_tree(
16567 "/a",
16568 json!({
16569 "main.rs": sample_text_1,
16570 "other.rs": sample_text_2,
16571 "lib.rs": sample_text_3,
16572 }),
16573 )
16574 .await;
16575 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16576 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16577 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16578 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16579 Editor::new(
16580 EditorMode::full(),
16581 multi_buffer,
16582 Some(project.clone()),
16583 window,
16584 cx,
16585 )
16586 });
16587 let multibuffer_item_id = workspace
16588 .update(cx, |workspace, window, cx| {
16589 assert!(
16590 workspace.active_item(cx).is_none(),
16591 "active item should be None before the first item is added"
16592 );
16593 workspace.add_item_to_active_pane(
16594 Box::new(multi_buffer_editor.clone()),
16595 None,
16596 true,
16597 window,
16598 cx,
16599 );
16600 let active_item = workspace
16601 .active_item(cx)
16602 .expect("should have an active item after adding the multi buffer");
16603 assert!(
16604 !active_item.is_singleton(cx),
16605 "A multi buffer was expected to active after adding"
16606 );
16607 active_item.item_id()
16608 })
16609 .unwrap();
16610 cx.executor().run_until_parked();
16611
16612 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16613 editor.change_selections(
16614 SelectionEffects::scroll(Autoscroll::Next),
16615 window,
16616 cx,
16617 |s| s.select_ranges(Some(1..2)),
16618 );
16619 editor.open_excerpts(&OpenExcerpts, window, cx);
16620 });
16621 cx.executor().run_until_parked();
16622 let first_item_id = workspace
16623 .update(cx, |workspace, window, cx| {
16624 let active_item = workspace
16625 .active_item(cx)
16626 .expect("should have an active item after navigating into the 1st buffer");
16627 let first_item_id = active_item.item_id();
16628 assert_ne!(
16629 first_item_id, multibuffer_item_id,
16630 "Should navigate into the 1st buffer and activate it"
16631 );
16632 assert!(
16633 active_item.is_singleton(cx),
16634 "New active item should be a singleton buffer"
16635 );
16636 assert_eq!(
16637 active_item
16638 .act_as::<Editor>(cx)
16639 .expect("should have navigated into an editor for the 1st buffer")
16640 .read(cx)
16641 .text(cx),
16642 sample_text_1
16643 );
16644
16645 workspace
16646 .go_back(workspace.active_pane().downgrade(), window, cx)
16647 .detach_and_log_err(cx);
16648
16649 first_item_id
16650 })
16651 .unwrap();
16652 cx.executor().run_until_parked();
16653 workspace
16654 .update(cx, |workspace, _, cx| {
16655 let active_item = workspace
16656 .active_item(cx)
16657 .expect("should have an active item after navigating back");
16658 assert_eq!(
16659 active_item.item_id(),
16660 multibuffer_item_id,
16661 "Should navigate back to the multi buffer"
16662 );
16663 assert!(!active_item.is_singleton(cx));
16664 })
16665 .unwrap();
16666
16667 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16668 editor.change_selections(
16669 SelectionEffects::scroll(Autoscroll::Next),
16670 window,
16671 cx,
16672 |s| s.select_ranges(Some(39..40)),
16673 );
16674 editor.open_excerpts(&OpenExcerpts, window, cx);
16675 });
16676 cx.executor().run_until_parked();
16677 let second_item_id = workspace
16678 .update(cx, |workspace, window, cx| {
16679 let active_item = workspace
16680 .active_item(cx)
16681 .expect("should have an active item after navigating into the 2nd buffer");
16682 let second_item_id = active_item.item_id();
16683 assert_ne!(
16684 second_item_id, multibuffer_item_id,
16685 "Should navigate away from the multibuffer"
16686 );
16687 assert_ne!(
16688 second_item_id, first_item_id,
16689 "Should navigate into the 2nd buffer and activate it"
16690 );
16691 assert!(
16692 active_item.is_singleton(cx),
16693 "New active item should be a singleton buffer"
16694 );
16695 assert_eq!(
16696 active_item
16697 .act_as::<Editor>(cx)
16698 .expect("should have navigated into an editor")
16699 .read(cx)
16700 .text(cx),
16701 sample_text_2
16702 );
16703
16704 workspace
16705 .go_back(workspace.active_pane().downgrade(), window, cx)
16706 .detach_and_log_err(cx);
16707
16708 second_item_id
16709 })
16710 .unwrap();
16711 cx.executor().run_until_parked();
16712 workspace
16713 .update(cx, |workspace, _, cx| {
16714 let active_item = workspace
16715 .active_item(cx)
16716 .expect("should have an active item after navigating back from the 2nd buffer");
16717 assert_eq!(
16718 active_item.item_id(),
16719 multibuffer_item_id,
16720 "Should navigate back from the 2nd buffer to the multi buffer"
16721 );
16722 assert!(!active_item.is_singleton(cx));
16723 })
16724 .unwrap();
16725
16726 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16727 editor.change_selections(
16728 SelectionEffects::scroll(Autoscroll::Next),
16729 window,
16730 cx,
16731 |s| s.select_ranges(Some(70..70)),
16732 );
16733 editor.open_excerpts(&OpenExcerpts, window, cx);
16734 });
16735 cx.executor().run_until_parked();
16736 workspace
16737 .update(cx, |workspace, window, cx| {
16738 let active_item = workspace
16739 .active_item(cx)
16740 .expect("should have an active item after navigating into the 3rd buffer");
16741 let third_item_id = active_item.item_id();
16742 assert_ne!(
16743 third_item_id, multibuffer_item_id,
16744 "Should navigate into the 3rd buffer and activate it"
16745 );
16746 assert_ne!(third_item_id, first_item_id);
16747 assert_ne!(third_item_id, second_item_id);
16748 assert!(
16749 active_item.is_singleton(cx),
16750 "New active item should be a singleton buffer"
16751 );
16752 assert_eq!(
16753 active_item
16754 .act_as::<Editor>(cx)
16755 .expect("should have navigated into an editor")
16756 .read(cx)
16757 .text(cx),
16758 sample_text_3
16759 );
16760
16761 workspace
16762 .go_back(workspace.active_pane().downgrade(), window, cx)
16763 .detach_and_log_err(cx);
16764 })
16765 .unwrap();
16766 cx.executor().run_until_parked();
16767 workspace
16768 .update(cx, |workspace, _, cx| {
16769 let active_item = workspace
16770 .active_item(cx)
16771 .expect("should have an active item after navigating back from the 3rd buffer");
16772 assert_eq!(
16773 active_item.item_id(),
16774 multibuffer_item_id,
16775 "Should navigate back from the 3rd buffer to the multi buffer"
16776 );
16777 assert!(!active_item.is_singleton(cx));
16778 })
16779 .unwrap();
16780}
16781
16782#[gpui::test]
16783async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16784 init_test(cx, |_| {});
16785
16786 let mut cx = EditorTestContext::new(cx).await;
16787
16788 let diff_base = r#"
16789 use some::mod;
16790
16791 const A: u32 = 42;
16792
16793 fn main() {
16794 println!("hello");
16795
16796 println!("world");
16797 }
16798 "#
16799 .unindent();
16800
16801 cx.set_state(
16802 &r#"
16803 use some::modified;
16804
16805 ˇ
16806 fn main() {
16807 println!("hello there");
16808
16809 println!("around the");
16810 println!("world");
16811 }
16812 "#
16813 .unindent(),
16814 );
16815
16816 cx.set_head_text(&diff_base);
16817 executor.run_until_parked();
16818
16819 cx.update_editor(|editor, window, cx| {
16820 editor.go_to_next_hunk(&GoToHunk, window, cx);
16821 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16822 });
16823 executor.run_until_parked();
16824 cx.assert_state_with_diff(
16825 r#"
16826 use some::modified;
16827
16828
16829 fn main() {
16830 - println!("hello");
16831 + ˇ println!("hello there");
16832
16833 println!("around the");
16834 println!("world");
16835 }
16836 "#
16837 .unindent(),
16838 );
16839
16840 cx.update_editor(|editor, window, cx| {
16841 for _ in 0..2 {
16842 editor.go_to_next_hunk(&GoToHunk, window, cx);
16843 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16844 }
16845 });
16846 executor.run_until_parked();
16847 cx.assert_state_with_diff(
16848 r#"
16849 - use some::mod;
16850 + ˇuse some::modified;
16851
16852
16853 fn main() {
16854 - println!("hello");
16855 + println!("hello there");
16856
16857 + println!("around the");
16858 println!("world");
16859 }
16860 "#
16861 .unindent(),
16862 );
16863
16864 cx.update_editor(|editor, window, cx| {
16865 editor.go_to_next_hunk(&GoToHunk, window, cx);
16866 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16867 });
16868 executor.run_until_parked();
16869 cx.assert_state_with_diff(
16870 r#"
16871 - use some::mod;
16872 + use some::modified;
16873
16874 - const A: u32 = 42;
16875 ˇ
16876 fn main() {
16877 - println!("hello");
16878 + println!("hello there");
16879
16880 + println!("around the");
16881 println!("world");
16882 }
16883 "#
16884 .unindent(),
16885 );
16886
16887 cx.update_editor(|editor, window, cx| {
16888 editor.cancel(&Cancel, window, cx);
16889 });
16890
16891 cx.assert_state_with_diff(
16892 r#"
16893 use some::modified;
16894
16895 ˇ
16896 fn main() {
16897 println!("hello there");
16898
16899 println!("around the");
16900 println!("world");
16901 }
16902 "#
16903 .unindent(),
16904 );
16905}
16906
16907#[gpui::test]
16908async fn test_diff_base_change_with_expanded_diff_hunks(
16909 executor: BackgroundExecutor,
16910 cx: &mut TestAppContext,
16911) {
16912 init_test(cx, |_| {});
16913
16914 let mut cx = EditorTestContext::new(cx).await;
16915
16916 let diff_base = r#"
16917 use some::mod1;
16918 use some::mod2;
16919
16920 const A: u32 = 42;
16921 const B: u32 = 42;
16922 const C: u32 = 42;
16923
16924 fn main() {
16925 println!("hello");
16926
16927 println!("world");
16928 }
16929 "#
16930 .unindent();
16931
16932 cx.set_state(
16933 &r#"
16934 use some::mod2;
16935
16936 const A: u32 = 42;
16937 const C: u32 = 42;
16938
16939 fn main(ˇ) {
16940 //println!("hello");
16941
16942 println!("world");
16943 //
16944 //
16945 }
16946 "#
16947 .unindent(),
16948 );
16949
16950 cx.set_head_text(&diff_base);
16951 executor.run_until_parked();
16952
16953 cx.update_editor(|editor, window, cx| {
16954 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16955 });
16956 executor.run_until_parked();
16957 cx.assert_state_with_diff(
16958 r#"
16959 - use some::mod1;
16960 use some::mod2;
16961
16962 const A: u32 = 42;
16963 - const B: u32 = 42;
16964 const C: u32 = 42;
16965
16966 fn main(ˇ) {
16967 - println!("hello");
16968 + //println!("hello");
16969
16970 println!("world");
16971 + //
16972 + //
16973 }
16974 "#
16975 .unindent(),
16976 );
16977
16978 cx.set_head_text("new diff base!");
16979 executor.run_until_parked();
16980 cx.assert_state_with_diff(
16981 r#"
16982 - new diff base!
16983 + use some::mod2;
16984 +
16985 + const A: u32 = 42;
16986 + const C: u32 = 42;
16987 +
16988 + fn main(ˇ) {
16989 + //println!("hello");
16990 +
16991 + println!("world");
16992 + //
16993 + //
16994 + }
16995 "#
16996 .unindent(),
16997 );
16998}
16999
17000#[gpui::test]
17001async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
17002 init_test(cx, |_| {});
17003
17004 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17005 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17006 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17007 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17008 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
17009 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
17010
17011 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
17012 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
17013 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
17014
17015 let multi_buffer = cx.new(|cx| {
17016 let mut multibuffer = MultiBuffer::new(ReadWrite);
17017 multibuffer.push_excerpts(
17018 buffer_1.clone(),
17019 [
17020 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17021 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17022 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17023 ],
17024 cx,
17025 );
17026 multibuffer.push_excerpts(
17027 buffer_2.clone(),
17028 [
17029 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17030 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17031 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17032 ],
17033 cx,
17034 );
17035 multibuffer.push_excerpts(
17036 buffer_3.clone(),
17037 [
17038 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17039 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17040 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17041 ],
17042 cx,
17043 );
17044 multibuffer
17045 });
17046
17047 let editor =
17048 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17049 editor
17050 .update(cx, |editor, _window, cx| {
17051 for (buffer, diff_base) in [
17052 (buffer_1.clone(), file_1_old),
17053 (buffer_2.clone(), file_2_old),
17054 (buffer_3.clone(), file_3_old),
17055 ] {
17056 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17057 editor
17058 .buffer
17059 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17060 }
17061 })
17062 .unwrap();
17063
17064 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17065 cx.run_until_parked();
17066
17067 cx.assert_editor_state(
17068 &"
17069 ˇaaa
17070 ccc
17071 ddd
17072
17073 ggg
17074 hhh
17075
17076
17077 lll
17078 mmm
17079 NNN
17080
17081 qqq
17082 rrr
17083
17084 uuu
17085 111
17086 222
17087 333
17088
17089 666
17090 777
17091
17092 000
17093 !!!"
17094 .unindent(),
17095 );
17096
17097 cx.update_editor(|editor, window, cx| {
17098 editor.select_all(&SelectAll, window, cx);
17099 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17100 });
17101 cx.executor().run_until_parked();
17102
17103 cx.assert_state_with_diff(
17104 "
17105 «aaa
17106 - bbb
17107 ccc
17108 ddd
17109
17110 ggg
17111 hhh
17112
17113
17114 lll
17115 mmm
17116 - nnn
17117 + NNN
17118
17119 qqq
17120 rrr
17121
17122 uuu
17123 111
17124 222
17125 333
17126
17127 + 666
17128 777
17129
17130 000
17131 !!!ˇ»"
17132 .unindent(),
17133 );
17134}
17135
17136#[gpui::test]
17137async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17138 init_test(cx, |_| {});
17139
17140 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17141 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17142
17143 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
17144 let multi_buffer = cx.new(|cx| {
17145 let mut multibuffer = MultiBuffer::new(ReadWrite);
17146 multibuffer.push_excerpts(
17147 buffer.clone(),
17148 [
17149 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
17150 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
17151 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
17152 ],
17153 cx,
17154 );
17155 multibuffer
17156 });
17157
17158 let editor =
17159 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17160 editor
17161 .update(cx, |editor, _window, cx| {
17162 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
17163 editor
17164 .buffer
17165 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
17166 })
17167 .unwrap();
17168
17169 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17170 cx.run_until_parked();
17171
17172 cx.update_editor(|editor, window, cx| {
17173 editor.expand_all_diff_hunks(&Default::default(), window, cx)
17174 });
17175 cx.executor().run_until_parked();
17176
17177 // When the start of a hunk coincides with the start of its excerpt,
17178 // the hunk is expanded. When the start of a a hunk is earlier than
17179 // the start of its excerpt, the hunk is not expanded.
17180 cx.assert_state_with_diff(
17181 "
17182 ˇaaa
17183 - bbb
17184 + BBB
17185
17186 - ddd
17187 - eee
17188 + DDD
17189 + EEE
17190 fff
17191
17192 iii
17193 "
17194 .unindent(),
17195 );
17196}
17197
17198#[gpui::test]
17199async fn test_edits_around_expanded_insertion_hunks(
17200 executor: BackgroundExecutor,
17201 cx: &mut TestAppContext,
17202) {
17203 init_test(cx, |_| {});
17204
17205 let mut cx = EditorTestContext::new(cx).await;
17206
17207 let diff_base = r#"
17208 use some::mod1;
17209 use some::mod2;
17210
17211 const A: u32 = 42;
17212
17213 fn main() {
17214 println!("hello");
17215
17216 println!("world");
17217 }
17218 "#
17219 .unindent();
17220 executor.run_until_parked();
17221 cx.set_state(
17222 &r#"
17223 use some::mod1;
17224 use some::mod2;
17225
17226 const A: u32 = 42;
17227 const B: u32 = 42;
17228 const C: u32 = 42;
17229 ˇ
17230
17231 fn main() {
17232 println!("hello");
17233
17234 println!("world");
17235 }
17236 "#
17237 .unindent(),
17238 );
17239
17240 cx.set_head_text(&diff_base);
17241 executor.run_until_parked();
17242
17243 cx.update_editor(|editor, window, cx| {
17244 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17245 });
17246 executor.run_until_parked();
17247
17248 cx.assert_state_with_diff(
17249 r#"
17250 use some::mod1;
17251 use some::mod2;
17252
17253 const A: u32 = 42;
17254 + const B: u32 = 42;
17255 + const C: u32 = 42;
17256 + ˇ
17257
17258 fn main() {
17259 println!("hello");
17260
17261 println!("world");
17262 }
17263 "#
17264 .unindent(),
17265 );
17266
17267 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
17268 executor.run_until_parked();
17269
17270 cx.assert_state_with_diff(
17271 r#"
17272 use some::mod1;
17273 use some::mod2;
17274
17275 const A: u32 = 42;
17276 + const B: u32 = 42;
17277 + const C: u32 = 42;
17278 + const D: u32 = 42;
17279 + ˇ
17280
17281 fn main() {
17282 println!("hello");
17283
17284 println!("world");
17285 }
17286 "#
17287 .unindent(),
17288 );
17289
17290 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
17291 executor.run_until_parked();
17292
17293 cx.assert_state_with_diff(
17294 r#"
17295 use some::mod1;
17296 use some::mod2;
17297
17298 const A: u32 = 42;
17299 + const B: u32 = 42;
17300 + const C: u32 = 42;
17301 + const D: u32 = 42;
17302 + const E: u32 = 42;
17303 + ˇ
17304
17305 fn main() {
17306 println!("hello");
17307
17308 println!("world");
17309 }
17310 "#
17311 .unindent(),
17312 );
17313
17314 cx.update_editor(|editor, window, cx| {
17315 editor.delete_line(&DeleteLine, window, cx);
17316 });
17317 executor.run_until_parked();
17318
17319 cx.assert_state_with_diff(
17320 r#"
17321 use some::mod1;
17322 use some::mod2;
17323
17324 const A: u32 = 42;
17325 + const B: u32 = 42;
17326 + const C: u32 = 42;
17327 + const D: u32 = 42;
17328 + const E: u32 = 42;
17329 ˇ
17330 fn main() {
17331 println!("hello");
17332
17333 println!("world");
17334 }
17335 "#
17336 .unindent(),
17337 );
17338
17339 cx.update_editor(|editor, window, cx| {
17340 editor.move_up(&MoveUp, window, cx);
17341 editor.delete_line(&DeleteLine, window, cx);
17342 editor.move_up(&MoveUp, window, cx);
17343 editor.delete_line(&DeleteLine, window, cx);
17344 editor.move_up(&MoveUp, window, cx);
17345 editor.delete_line(&DeleteLine, window, cx);
17346 });
17347 executor.run_until_parked();
17348 cx.assert_state_with_diff(
17349 r#"
17350 use some::mod1;
17351 use some::mod2;
17352
17353 const A: u32 = 42;
17354 + const B: u32 = 42;
17355 ˇ
17356 fn main() {
17357 println!("hello");
17358
17359 println!("world");
17360 }
17361 "#
17362 .unindent(),
17363 );
17364
17365 cx.update_editor(|editor, window, cx| {
17366 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
17367 editor.delete_line(&DeleteLine, window, cx);
17368 });
17369 executor.run_until_parked();
17370 cx.assert_state_with_diff(
17371 r#"
17372 ˇ
17373 fn main() {
17374 println!("hello");
17375
17376 println!("world");
17377 }
17378 "#
17379 .unindent(),
17380 );
17381}
17382
17383#[gpui::test]
17384async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17385 init_test(cx, |_| {});
17386
17387 let mut cx = EditorTestContext::new(cx).await;
17388 cx.set_head_text(indoc! { "
17389 one
17390 two
17391 three
17392 four
17393 five
17394 "
17395 });
17396 cx.set_state(indoc! { "
17397 one
17398 ˇthree
17399 five
17400 "});
17401 cx.run_until_parked();
17402 cx.update_editor(|editor, window, cx| {
17403 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17404 });
17405 cx.assert_state_with_diff(
17406 indoc! { "
17407 one
17408 - two
17409 ˇthree
17410 - four
17411 five
17412 "}
17413 .to_string(),
17414 );
17415 cx.update_editor(|editor, window, cx| {
17416 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17417 });
17418
17419 cx.assert_state_with_diff(
17420 indoc! { "
17421 one
17422 ˇthree
17423 five
17424 "}
17425 .to_string(),
17426 );
17427
17428 cx.set_state(indoc! { "
17429 one
17430 ˇTWO
17431 three
17432 four
17433 five
17434 "});
17435 cx.run_until_parked();
17436 cx.update_editor(|editor, window, cx| {
17437 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17438 });
17439
17440 cx.assert_state_with_diff(
17441 indoc! { "
17442 one
17443 - two
17444 + ˇTWO
17445 three
17446 four
17447 five
17448 "}
17449 .to_string(),
17450 );
17451 cx.update_editor(|editor, window, cx| {
17452 editor.move_up(&Default::default(), window, cx);
17453 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17454 });
17455 cx.assert_state_with_diff(
17456 indoc! { "
17457 one
17458 ˇTWO
17459 three
17460 four
17461 five
17462 "}
17463 .to_string(),
17464 );
17465}
17466
17467#[gpui::test]
17468async fn test_edits_around_expanded_deletion_hunks(
17469 executor: BackgroundExecutor,
17470 cx: &mut TestAppContext,
17471) {
17472 init_test(cx, |_| {});
17473
17474 let mut cx = EditorTestContext::new(cx).await;
17475
17476 let diff_base = r#"
17477 use some::mod1;
17478 use some::mod2;
17479
17480 const A: u32 = 42;
17481 const B: u32 = 42;
17482 const C: u32 = 42;
17483
17484
17485 fn main() {
17486 println!("hello");
17487
17488 println!("world");
17489 }
17490 "#
17491 .unindent();
17492 executor.run_until_parked();
17493 cx.set_state(
17494 &r#"
17495 use some::mod1;
17496 use some::mod2;
17497
17498 ˇconst B: u32 = 42;
17499 const C: u32 = 42;
17500
17501
17502 fn main() {
17503 println!("hello");
17504
17505 println!("world");
17506 }
17507 "#
17508 .unindent(),
17509 );
17510
17511 cx.set_head_text(&diff_base);
17512 executor.run_until_parked();
17513
17514 cx.update_editor(|editor, window, cx| {
17515 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17516 });
17517 executor.run_until_parked();
17518
17519 cx.assert_state_with_diff(
17520 r#"
17521 use some::mod1;
17522 use some::mod2;
17523
17524 - const A: u32 = 42;
17525 ˇconst B: u32 = 42;
17526 const C: u32 = 42;
17527
17528
17529 fn main() {
17530 println!("hello");
17531
17532 println!("world");
17533 }
17534 "#
17535 .unindent(),
17536 );
17537
17538 cx.update_editor(|editor, window, cx| {
17539 editor.delete_line(&DeleteLine, window, cx);
17540 });
17541 executor.run_until_parked();
17542 cx.assert_state_with_diff(
17543 r#"
17544 use some::mod1;
17545 use some::mod2;
17546
17547 - const A: u32 = 42;
17548 - const B: u32 = 42;
17549 ˇconst C: u32 = 42;
17550
17551
17552 fn main() {
17553 println!("hello");
17554
17555 println!("world");
17556 }
17557 "#
17558 .unindent(),
17559 );
17560
17561 cx.update_editor(|editor, window, cx| {
17562 editor.delete_line(&DeleteLine, window, cx);
17563 });
17564 executor.run_until_parked();
17565 cx.assert_state_with_diff(
17566 r#"
17567 use some::mod1;
17568 use some::mod2;
17569
17570 - const A: u32 = 42;
17571 - const B: u32 = 42;
17572 - const C: u32 = 42;
17573 ˇ
17574
17575 fn main() {
17576 println!("hello");
17577
17578 println!("world");
17579 }
17580 "#
17581 .unindent(),
17582 );
17583
17584 cx.update_editor(|editor, window, cx| {
17585 editor.handle_input("replacement", window, cx);
17586 });
17587 executor.run_until_parked();
17588 cx.assert_state_with_diff(
17589 r#"
17590 use some::mod1;
17591 use some::mod2;
17592
17593 - const A: u32 = 42;
17594 - const B: u32 = 42;
17595 - const C: u32 = 42;
17596 -
17597 + replacementˇ
17598
17599 fn main() {
17600 println!("hello");
17601
17602 println!("world");
17603 }
17604 "#
17605 .unindent(),
17606 );
17607}
17608
17609#[gpui::test]
17610async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17611 init_test(cx, |_| {});
17612
17613 let mut cx = EditorTestContext::new(cx).await;
17614
17615 let base_text = r#"
17616 one
17617 two
17618 three
17619 four
17620 five
17621 "#
17622 .unindent();
17623 executor.run_until_parked();
17624 cx.set_state(
17625 &r#"
17626 one
17627 two
17628 fˇour
17629 five
17630 "#
17631 .unindent(),
17632 );
17633
17634 cx.set_head_text(&base_text);
17635 executor.run_until_parked();
17636
17637 cx.update_editor(|editor, window, cx| {
17638 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17639 });
17640 executor.run_until_parked();
17641
17642 cx.assert_state_with_diff(
17643 r#"
17644 one
17645 two
17646 - three
17647 fˇour
17648 five
17649 "#
17650 .unindent(),
17651 );
17652
17653 cx.update_editor(|editor, window, cx| {
17654 editor.backspace(&Backspace, window, cx);
17655 editor.backspace(&Backspace, window, cx);
17656 });
17657 executor.run_until_parked();
17658 cx.assert_state_with_diff(
17659 r#"
17660 one
17661 two
17662 - threeˇ
17663 - four
17664 + our
17665 five
17666 "#
17667 .unindent(),
17668 );
17669}
17670
17671#[gpui::test]
17672async fn test_edit_after_expanded_modification_hunk(
17673 executor: BackgroundExecutor,
17674 cx: &mut TestAppContext,
17675) {
17676 init_test(cx, |_| {});
17677
17678 let mut cx = EditorTestContext::new(cx).await;
17679
17680 let diff_base = r#"
17681 use some::mod1;
17682 use some::mod2;
17683
17684 const A: u32 = 42;
17685 const B: u32 = 42;
17686 const C: u32 = 42;
17687 const D: u32 = 42;
17688
17689
17690 fn main() {
17691 println!("hello");
17692
17693 println!("world");
17694 }"#
17695 .unindent();
17696
17697 cx.set_state(
17698 &r#"
17699 use some::mod1;
17700 use some::mod2;
17701
17702 const A: u32 = 42;
17703 const B: u32 = 42;
17704 const C: u32 = 43ˇ
17705 const D: u32 = 42;
17706
17707
17708 fn main() {
17709 println!("hello");
17710
17711 println!("world");
17712 }"#
17713 .unindent(),
17714 );
17715
17716 cx.set_head_text(&diff_base);
17717 executor.run_until_parked();
17718 cx.update_editor(|editor, window, cx| {
17719 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17720 });
17721 executor.run_until_parked();
17722
17723 cx.assert_state_with_diff(
17724 r#"
17725 use some::mod1;
17726 use some::mod2;
17727
17728 const A: u32 = 42;
17729 const B: u32 = 42;
17730 - const C: u32 = 42;
17731 + const C: u32 = 43ˇ
17732 const D: u32 = 42;
17733
17734
17735 fn main() {
17736 println!("hello");
17737
17738 println!("world");
17739 }"#
17740 .unindent(),
17741 );
17742
17743 cx.update_editor(|editor, window, cx| {
17744 editor.handle_input("\nnew_line\n", window, cx);
17745 });
17746 executor.run_until_parked();
17747
17748 cx.assert_state_with_diff(
17749 r#"
17750 use some::mod1;
17751 use some::mod2;
17752
17753 const A: u32 = 42;
17754 const B: u32 = 42;
17755 - const C: u32 = 42;
17756 + const C: u32 = 43
17757 + new_line
17758 + ˇ
17759 const D: u32 = 42;
17760
17761
17762 fn main() {
17763 println!("hello");
17764
17765 println!("world");
17766 }"#
17767 .unindent(),
17768 );
17769}
17770
17771#[gpui::test]
17772async fn test_stage_and_unstage_added_file_hunk(
17773 executor: BackgroundExecutor,
17774 cx: &mut TestAppContext,
17775) {
17776 init_test(cx, |_| {});
17777
17778 let mut cx = EditorTestContext::new(cx).await;
17779 cx.update_editor(|editor, _, cx| {
17780 editor.set_expand_all_diff_hunks(cx);
17781 });
17782
17783 let working_copy = r#"
17784 ˇfn main() {
17785 println!("hello, world!");
17786 }
17787 "#
17788 .unindent();
17789
17790 cx.set_state(&working_copy);
17791 executor.run_until_parked();
17792
17793 cx.assert_state_with_diff(
17794 r#"
17795 + ˇfn main() {
17796 + println!("hello, world!");
17797 + }
17798 "#
17799 .unindent(),
17800 );
17801 cx.assert_index_text(None);
17802
17803 cx.update_editor(|editor, window, cx| {
17804 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17805 });
17806 executor.run_until_parked();
17807 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
17808 cx.assert_state_with_diff(
17809 r#"
17810 + ˇfn main() {
17811 + println!("hello, world!");
17812 + }
17813 "#
17814 .unindent(),
17815 );
17816
17817 cx.update_editor(|editor, window, cx| {
17818 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17819 });
17820 executor.run_until_parked();
17821 cx.assert_index_text(None);
17822}
17823
17824async fn setup_indent_guides_editor(
17825 text: &str,
17826 cx: &mut TestAppContext,
17827) -> (BufferId, EditorTestContext) {
17828 init_test(cx, |_| {});
17829
17830 let mut cx = EditorTestContext::new(cx).await;
17831
17832 let buffer_id = cx.update_editor(|editor, window, cx| {
17833 editor.set_text(text, window, cx);
17834 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
17835
17836 buffer_ids[0]
17837 });
17838
17839 (buffer_id, cx)
17840}
17841
17842fn assert_indent_guides(
17843 range: Range<u32>,
17844 expected: Vec<IndentGuide>,
17845 active_indices: Option<Vec<usize>>,
17846 cx: &mut EditorTestContext,
17847) {
17848 let indent_guides = cx.update_editor(|editor, window, cx| {
17849 let snapshot = editor.snapshot(window, cx).display_snapshot;
17850 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
17851 editor,
17852 MultiBufferRow(range.start)..MultiBufferRow(range.end),
17853 true,
17854 &snapshot,
17855 cx,
17856 );
17857
17858 indent_guides.sort_by(|a, b| {
17859 a.depth.cmp(&b.depth).then(
17860 a.start_row
17861 .cmp(&b.start_row)
17862 .then(a.end_row.cmp(&b.end_row)),
17863 )
17864 });
17865 indent_guides
17866 });
17867
17868 if let Some(expected) = active_indices {
17869 let active_indices = cx.update_editor(|editor, window, cx| {
17870 let snapshot = editor.snapshot(window, cx).display_snapshot;
17871 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
17872 });
17873
17874 assert_eq!(
17875 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
17876 expected,
17877 "Active indent guide indices do not match"
17878 );
17879 }
17880
17881 assert_eq!(indent_guides, expected, "Indent guides do not match");
17882}
17883
17884fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
17885 IndentGuide {
17886 buffer_id,
17887 start_row: MultiBufferRow(start_row),
17888 end_row: MultiBufferRow(end_row),
17889 depth,
17890 tab_size: 4,
17891 settings: IndentGuideSettings {
17892 enabled: true,
17893 line_width: 1,
17894 active_line_width: 1,
17895 ..Default::default()
17896 },
17897 }
17898}
17899
17900#[gpui::test]
17901async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
17902 let (buffer_id, mut cx) = setup_indent_guides_editor(
17903 &"
17904 fn main() {
17905 let a = 1;
17906 }"
17907 .unindent(),
17908 cx,
17909 )
17910 .await;
17911
17912 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17913}
17914
17915#[gpui::test]
17916async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
17917 let (buffer_id, mut cx) = setup_indent_guides_editor(
17918 &"
17919 fn main() {
17920 let a = 1;
17921 let b = 2;
17922 }"
17923 .unindent(),
17924 cx,
17925 )
17926 .await;
17927
17928 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
17929}
17930
17931#[gpui::test]
17932async fn test_indent_guide_nested(cx: &mut TestAppContext) {
17933 let (buffer_id, mut cx) = setup_indent_guides_editor(
17934 &"
17935 fn main() {
17936 let a = 1;
17937 if a == 3 {
17938 let b = 2;
17939 } else {
17940 let c = 3;
17941 }
17942 }"
17943 .unindent(),
17944 cx,
17945 )
17946 .await;
17947
17948 assert_indent_guides(
17949 0..8,
17950 vec![
17951 indent_guide(buffer_id, 1, 6, 0),
17952 indent_guide(buffer_id, 3, 3, 1),
17953 indent_guide(buffer_id, 5, 5, 1),
17954 ],
17955 None,
17956 &mut cx,
17957 );
17958}
17959
17960#[gpui::test]
17961async fn test_indent_guide_tab(cx: &mut TestAppContext) {
17962 let (buffer_id, mut cx) = setup_indent_guides_editor(
17963 &"
17964 fn main() {
17965 let a = 1;
17966 let b = 2;
17967 let c = 3;
17968 }"
17969 .unindent(),
17970 cx,
17971 )
17972 .await;
17973
17974 assert_indent_guides(
17975 0..5,
17976 vec![
17977 indent_guide(buffer_id, 1, 3, 0),
17978 indent_guide(buffer_id, 2, 2, 1),
17979 ],
17980 None,
17981 &mut cx,
17982 );
17983}
17984
17985#[gpui::test]
17986async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
17987 let (buffer_id, mut cx) = setup_indent_guides_editor(
17988 &"
17989 fn main() {
17990 let a = 1;
17991
17992 let c = 3;
17993 }"
17994 .unindent(),
17995 cx,
17996 )
17997 .await;
17998
17999 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
18000}
18001
18002#[gpui::test]
18003async fn test_indent_guide_complex(cx: &mut TestAppContext) {
18004 let (buffer_id, mut cx) = setup_indent_guides_editor(
18005 &"
18006 fn main() {
18007 let a = 1;
18008
18009 let c = 3;
18010
18011 if a == 3 {
18012 let b = 2;
18013 } else {
18014 let c = 3;
18015 }
18016 }"
18017 .unindent(),
18018 cx,
18019 )
18020 .await;
18021
18022 assert_indent_guides(
18023 0..11,
18024 vec![
18025 indent_guide(buffer_id, 1, 9, 0),
18026 indent_guide(buffer_id, 6, 6, 1),
18027 indent_guide(buffer_id, 8, 8, 1),
18028 ],
18029 None,
18030 &mut cx,
18031 );
18032}
18033
18034#[gpui::test]
18035async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
18036 let (buffer_id, mut cx) = setup_indent_guides_editor(
18037 &"
18038 fn main() {
18039 let a = 1;
18040
18041 let c = 3;
18042
18043 if a == 3 {
18044 let b = 2;
18045 } else {
18046 let c = 3;
18047 }
18048 }"
18049 .unindent(),
18050 cx,
18051 )
18052 .await;
18053
18054 assert_indent_guides(
18055 1..11,
18056 vec![
18057 indent_guide(buffer_id, 1, 9, 0),
18058 indent_guide(buffer_id, 6, 6, 1),
18059 indent_guide(buffer_id, 8, 8, 1),
18060 ],
18061 None,
18062 &mut cx,
18063 );
18064}
18065
18066#[gpui::test]
18067async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
18068 let (buffer_id, mut cx) = setup_indent_guides_editor(
18069 &"
18070 fn main() {
18071 let a = 1;
18072
18073 let c = 3;
18074
18075 if a == 3 {
18076 let b = 2;
18077 } else {
18078 let c = 3;
18079 }
18080 }"
18081 .unindent(),
18082 cx,
18083 )
18084 .await;
18085
18086 assert_indent_guides(
18087 1..10,
18088 vec![
18089 indent_guide(buffer_id, 1, 9, 0),
18090 indent_guide(buffer_id, 6, 6, 1),
18091 indent_guide(buffer_id, 8, 8, 1),
18092 ],
18093 None,
18094 &mut cx,
18095 );
18096}
18097
18098#[gpui::test]
18099async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18100 let (buffer_id, mut cx) = setup_indent_guides_editor(
18101 &"
18102 fn main() {
18103 if a {
18104 b(
18105 c,
18106 d,
18107 )
18108 } else {
18109 e(
18110 f
18111 )
18112 }
18113 }"
18114 .unindent(),
18115 cx,
18116 )
18117 .await;
18118
18119 assert_indent_guides(
18120 0..11,
18121 vec![
18122 indent_guide(buffer_id, 1, 10, 0),
18123 indent_guide(buffer_id, 2, 5, 1),
18124 indent_guide(buffer_id, 7, 9, 1),
18125 indent_guide(buffer_id, 3, 4, 2),
18126 indent_guide(buffer_id, 8, 8, 2),
18127 ],
18128 None,
18129 &mut cx,
18130 );
18131
18132 cx.update_editor(|editor, window, cx| {
18133 editor.fold_at(MultiBufferRow(2), window, cx);
18134 assert_eq!(
18135 editor.display_text(cx),
18136 "
18137 fn main() {
18138 if a {
18139 b(⋯
18140 )
18141 } else {
18142 e(
18143 f
18144 )
18145 }
18146 }"
18147 .unindent()
18148 );
18149 });
18150
18151 assert_indent_guides(
18152 0..11,
18153 vec![
18154 indent_guide(buffer_id, 1, 10, 0),
18155 indent_guide(buffer_id, 2, 5, 1),
18156 indent_guide(buffer_id, 7, 9, 1),
18157 indent_guide(buffer_id, 8, 8, 2),
18158 ],
18159 None,
18160 &mut cx,
18161 );
18162}
18163
18164#[gpui::test]
18165async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
18166 let (buffer_id, mut cx) = setup_indent_guides_editor(
18167 &"
18168 block1
18169 block2
18170 block3
18171 block4
18172 block2
18173 block1
18174 block1"
18175 .unindent(),
18176 cx,
18177 )
18178 .await;
18179
18180 assert_indent_guides(
18181 1..10,
18182 vec![
18183 indent_guide(buffer_id, 1, 4, 0),
18184 indent_guide(buffer_id, 2, 3, 1),
18185 indent_guide(buffer_id, 3, 3, 2),
18186 ],
18187 None,
18188 &mut cx,
18189 );
18190}
18191
18192#[gpui::test]
18193async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
18194 let (buffer_id, mut cx) = setup_indent_guides_editor(
18195 &"
18196 block1
18197 block2
18198 block3
18199
18200 block1
18201 block1"
18202 .unindent(),
18203 cx,
18204 )
18205 .await;
18206
18207 assert_indent_guides(
18208 0..6,
18209 vec![
18210 indent_guide(buffer_id, 1, 2, 0),
18211 indent_guide(buffer_id, 2, 2, 1),
18212 ],
18213 None,
18214 &mut cx,
18215 );
18216}
18217
18218#[gpui::test]
18219async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
18220 let (buffer_id, mut cx) = setup_indent_guides_editor(
18221 &"
18222 function component() {
18223 \treturn (
18224 \t\t\t
18225 \t\t<div>
18226 \t\t\t<abc></abc>
18227 \t\t</div>
18228 \t)
18229 }"
18230 .unindent(),
18231 cx,
18232 )
18233 .await;
18234
18235 assert_indent_guides(
18236 0..8,
18237 vec![
18238 indent_guide(buffer_id, 1, 6, 0),
18239 indent_guide(buffer_id, 2, 5, 1),
18240 indent_guide(buffer_id, 4, 4, 2),
18241 ],
18242 None,
18243 &mut cx,
18244 );
18245}
18246
18247#[gpui::test]
18248async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
18249 let (buffer_id, mut cx) = setup_indent_guides_editor(
18250 &"
18251 function component() {
18252 \treturn (
18253 \t
18254 \t\t<div>
18255 \t\t\t<abc></abc>
18256 \t\t</div>
18257 \t)
18258 }"
18259 .unindent(),
18260 cx,
18261 )
18262 .await;
18263
18264 assert_indent_guides(
18265 0..8,
18266 vec![
18267 indent_guide(buffer_id, 1, 6, 0),
18268 indent_guide(buffer_id, 2, 5, 1),
18269 indent_guide(buffer_id, 4, 4, 2),
18270 ],
18271 None,
18272 &mut cx,
18273 );
18274}
18275
18276#[gpui::test]
18277async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
18278 let (buffer_id, mut cx) = setup_indent_guides_editor(
18279 &"
18280 block1
18281
18282
18283
18284 block2
18285 "
18286 .unindent(),
18287 cx,
18288 )
18289 .await;
18290
18291 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18292}
18293
18294#[gpui::test]
18295async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
18296 let (buffer_id, mut cx) = setup_indent_guides_editor(
18297 &"
18298 def a:
18299 \tb = 3
18300 \tif True:
18301 \t\tc = 4
18302 \t\td = 5
18303 \tprint(b)
18304 "
18305 .unindent(),
18306 cx,
18307 )
18308 .await;
18309
18310 assert_indent_guides(
18311 0..6,
18312 vec![
18313 indent_guide(buffer_id, 1, 5, 0),
18314 indent_guide(buffer_id, 3, 4, 1),
18315 ],
18316 None,
18317 &mut cx,
18318 );
18319}
18320
18321#[gpui::test]
18322async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
18323 let (buffer_id, mut cx) = setup_indent_guides_editor(
18324 &"
18325 fn main() {
18326 let a = 1;
18327 }"
18328 .unindent(),
18329 cx,
18330 )
18331 .await;
18332
18333 cx.update_editor(|editor, window, cx| {
18334 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18335 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18336 });
18337 });
18338
18339 assert_indent_guides(
18340 0..3,
18341 vec![indent_guide(buffer_id, 1, 1, 0)],
18342 Some(vec![0]),
18343 &mut cx,
18344 );
18345}
18346
18347#[gpui::test]
18348async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
18349 let (buffer_id, mut cx) = setup_indent_guides_editor(
18350 &"
18351 fn main() {
18352 if 1 == 2 {
18353 let a = 1;
18354 }
18355 }"
18356 .unindent(),
18357 cx,
18358 )
18359 .await;
18360
18361 cx.update_editor(|editor, window, cx| {
18362 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18363 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18364 });
18365 });
18366
18367 assert_indent_guides(
18368 0..4,
18369 vec![
18370 indent_guide(buffer_id, 1, 3, 0),
18371 indent_guide(buffer_id, 2, 2, 1),
18372 ],
18373 Some(vec![1]),
18374 &mut cx,
18375 );
18376
18377 cx.update_editor(|editor, window, cx| {
18378 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18379 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18380 });
18381 });
18382
18383 assert_indent_guides(
18384 0..4,
18385 vec![
18386 indent_guide(buffer_id, 1, 3, 0),
18387 indent_guide(buffer_id, 2, 2, 1),
18388 ],
18389 Some(vec![1]),
18390 &mut cx,
18391 );
18392
18393 cx.update_editor(|editor, window, cx| {
18394 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18395 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18396 });
18397 });
18398
18399 assert_indent_guides(
18400 0..4,
18401 vec![
18402 indent_guide(buffer_id, 1, 3, 0),
18403 indent_guide(buffer_id, 2, 2, 1),
18404 ],
18405 Some(vec![0]),
18406 &mut cx,
18407 );
18408}
18409
18410#[gpui::test]
18411async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18412 let (buffer_id, mut cx) = setup_indent_guides_editor(
18413 &"
18414 fn main() {
18415 let a = 1;
18416
18417 let b = 2;
18418 }"
18419 .unindent(),
18420 cx,
18421 )
18422 .await;
18423
18424 cx.update_editor(|editor, window, cx| {
18425 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18426 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18427 });
18428 });
18429
18430 assert_indent_guides(
18431 0..5,
18432 vec![indent_guide(buffer_id, 1, 3, 0)],
18433 Some(vec![0]),
18434 &mut cx,
18435 );
18436}
18437
18438#[gpui::test]
18439async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
18440 let (buffer_id, mut cx) = setup_indent_guides_editor(
18441 &"
18442 def m:
18443 a = 1
18444 pass"
18445 .unindent(),
18446 cx,
18447 )
18448 .await;
18449
18450 cx.update_editor(|editor, window, cx| {
18451 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18452 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18453 });
18454 });
18455
18456 assert_indent_guides(
18457 0..3,
18458 vec![indent_guide(buffer_id, 1, 2, 0)],
18459 Some(vec![0]),
18460 &mut cx,
18461 );
18462}
18463
18464#[gpui::test]
18465async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
18466 init_test(cx, |_| {});
18467 let mut cx = EditorTestContext::new(cx).await;
18468 let text = indoc! {
18469 "
18470 impl A {
18471 fn b() {
18472 0;
18473 3;
18474 5;
18475 6;
18476 7;
18477 }
18478 }
18479 "
18480 };
18481 let base_text = indoc! {
18482 "
18483 impl A {
18484 fn b() {
18485 0;
18486 1;
18487 2;
18488 3;
18489 4;
18490 }
18491 fn c() {
18492 5;
18493 6;
18494 7;
18495 }
18496 }
18497 "
18498 };
18499
18500 cx.update_editor(|editor, window, cx| {
18501 editor.set_text(text, window, cx);
18502
18503 editor.buffer().update(cx, |multibuffer, cx| {
18504 let buffer = multibuffer.as_singleton().unwrap();
18505 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
18506
18507 multibuffer.set_all_diff_hunks_expanded(cx);
18508 multibuffer.add_diff(diff, cx);
18509
18510 buffer.read(cx).remote_id()
18511 })
18512 });
18513 cx.run_until_parked();
18514
18515 cx.assert_state_with_diff(
18516 indoc! { "
18517 impl A {
18518 fn b() {
18519 0;
18520 - 1;
18521 - 2;
18522 3;
18523 - 4;
18524 - }
18525 - fn c() {
18526 5;
18527 6;
18528 7;
18529 }
18530 }
18531 ˇ"
18532 }
18533 .to_string(),
18534 );
18535
18536 let mut actual_guides = cx.update_editor(|editor, window, cx| {
18537 editor
18538 .snapshot(window, cx)
18539 .buffer_snapshot
18540 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
18541 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
18542 .collect::<Vec<_>>()
18543 });
18544 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
18545 assert_eq!(
18546 actual_guides,
18547 vec![
18548 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
18549 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
18550 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
18551 ]
18552 );
18553}
18554
18555#[gpui::test]
18556async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18557 init_test(cx, |_| {});
18558 let mut cx = EditorTestContext::new(cx).await;
18559
18560 let diff_base = r#"
18561 a
18562 b
18563 c
18564 "#
18565 .unindent();
18566
18567 cx.set_state(
18568 &r#"
18569 ˇA
18570 b
18571 C
18572 "#
18573 .unindent(),
18574 );
18575 cx.set_head_text(&diff_base);
18576 cx.update_editor(|editor, window, cx| {
18577 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18578 });
18579 executor.run_until_parked();
18580
18581 let both_hunks_expanded = r#"
18582 - a
18583 + ˇA
18584 b
18585 - c
18586 + C
18587 "#
18588 .unindent();
18589
18590 cx.assert_state_with_diff(both_hunks_expanded.clone());
18591
18592 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18593 let snapshot = editor.snapshot(window, cx);
18594 let hunks = editor
18595 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18596 .collect::<Vec<_>>();
18597 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18598 let buffer_id = hunks[0].buffer_id;
18599 hunks
18600 .into_iter()
18601 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18602 .collect::<Vec<_>>()
18603 });
18604 assert_eq!(hunk_ranges.len(), 2);
18605
18606 cx.update_editor(|editor, _, cx| {
18607 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18608 });
18609 executor.run_until_parked();
18610
18611 let second_hunk_expanded = r#"
18612 ˇA
18613 b
18614 - c
18615 + C
18616 "#
18617 .unindent();
18618
18619 cx.assert_state_with_diff(second_hunk_expanded);
18620
18621 cx.update_editor(|editor, _, cx| {
18622 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18623 });
18624 executor.run_until_parked();
18625
18626 cx.assert_state_with_diff(both_hunks_expanded.clone());
18627
18628 cx.update_editor(|editor, _, cx| {
18629 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18630 });
18631 executor.run_until_parked();
18632
18633 let first_hunk_expanded = r#"
18634 - a
18635 + ˇA
18636 b
18637 C
18638 "#
18639 .unindent();
18640
18641 cx.assert_state_with_diff(first_hunk_expanded);
18642
18643 cx.update_editor(|editor, _, cx| {
18644 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18645 });
18646 executor.run_until_parked();
18647
18648 cx.assert_state_with_diff(both_hunks_expanded);
18649
18650 cx.set_state(
18651 &r#"
18652 ˇA
18653 b
18654 "#
18655 .unindent(),
18656 );
18657 cx.run_until_parked();
18658
18659 // TODO this cursor position seems bad
18660 cx.assert_state_with_diff(
18661 r#"
18662 - ˇa
18663 + A
18664 b
18665 "#
18666 .unindent(),
18667 );
18668
18669 cx.update_editor(|editor, window, cx| {
18670 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18671 });
18672
18673 cx.assert_state_with_diff(
18674 r#"
18675 - ˇa
18676 + A
18677 b
18678 - c
18679 "#
18680 .unindent(),
18681 );
18682
18683 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18684 let snapshot = editor.snapshot(window, cx);
18685 let hunks = editor
18686 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18687 .collect::<Vec<_>>();
18688 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18689 let buffer_id = hunks[0].buffer_id;
18690 hunks
18691 .into_iter()
18692 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18693 .collect::<Vec<_>>()
18694 });
18695 assert_eq!(hunk_ranges.len(), 2);
18696
18697 cx.update_editor(|editor, _, cx| {
18698 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18699 });
18700 executor.run_until_parked();
18701
18702 cx.assert_state_with_diff(
18703 r#"
18704 - ˇa
18705 + A
18706 b
18707 "#
18708 .unindent(),
18709 );
18710}
18711
18712#[gpui::test]
18713async fn test_toggle_deletion_hunk_at_start_of_file(
18714 executor: BackgroundExecutor,
18715 cx: &mut TestAppContext,
18716) {
18717 init_test(cx, |_| {});
18718 let mut cx = EditorTestContext::new(cx).await;
18719
18720 let diff_base = r#"
18721 a
18722 b
18723 c
18724 "#
18725 .unindent();
18726
18727 cx.set_state(
18728 &r#"
18729 ˇb
18730 c
18731 "#
18732 .unindent(),
18733 );
18734 cx.set_head_text(&diff_base);
18735 cx.update_editor(|editor, window, cx| {
18736 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18737 });
18738 executor.run_until_parked();
18739
18740 let hunk_expanded = r#"
18741 - a
18742 ˇb
18743 c
18744 "#
18745 .unindent();
18746
18747 cx.assert_state_with_diff(hunk_expanded.clone());
18748
18749 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18750 let snapshot = editor.snapshot(window, cx);
18751 let hunks = editor
18752 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18753 .collect::<Vec<_>>();
18754 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18755 let buffer_id = hunks[0].buffer_id;
18756 hunks
18757 .into_iter()
18758 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18759 .collect::<Vec<_>>()
18760 });
18761 assert_eq!(hunk_ranges.len(), 1);
18762
18763 cx.update_editor(|editor, _, cx| {
18764 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18765 });
18766 executor.run_until_parked();
18767
18768 let hunk_collapsed = r#"
18769 ˇb
18770 c
18771 "#
18772 .unindent();
18773
18774 cx.assert_state_with_diff(hunk_collapsed);
18775
18776 cx.update_editor(|editor, _, cx| {
18777 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18778 });
18779 executor.run_until_parked();
18780
18781 cx.assert_state_with_diff(hunk_expanded.clone());
18782}
18783
18784#[gpui::test]
18785async fn test_display_diff_hunks(cx: &mut TestAppContext) {
18786 init_test(cx, |_| {});
18787
18788 let fs = FakeFs::new(cx.executor());
18789 fs.insert_tree(
18790 path!("/test"),
18791 json!({
18792 ".git": {},
18793 "file-1": "ONE\n",
18794 "file-2": "TWO\n",
18795 "file-3": "THREE\n",
18796 }),
18797 )
18798 .await;
18799
18800 fs.set_head_for_repo(
18801 path!("/test/.git").as_ref(),
18802 &[
18803 ("file-1".into(), "one\n".into()),
18804 ("file-2".into(), "two\n".into()),
18805 ("file-3".into(), "three\n".into()),
18806 ],
18807 "deadbeef",
18808 );
18809
18810 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
18811 let mut buffers = vec![];
18812 for i in 1..=3 {
18813 let buffer = project
18814 .update(cx, |project, cx| {
18815 let path = format!(path!("/test/file-{}"), i);
18816 project.open_local_buffer(path, cx)
18817 })
18818 .await
18819 .unwrap();
18820 buffers.push(buffer);
18821 }
18822
18823 let multibuffer = cx.new(|cx| {
18824 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
18825 multibuffer.set_all_diff_hunks_expanded(cx);
18826 for buffer in &buffers {
18827 let snapshot = buffer.read(cx).snapshot();
18828 multibuffer.set_excerpts_for_path(
18829 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
18830 buffer.clone(),
18831 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
18832 DEFAULT_MULTIBUFFER_CONTEXT,
18833 cx,
18834 );
18835 }
18836 multibuffer
18837 });
18838
18839 let editor = cx.add_window(|window, cx| {
18840 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
18841 });
18842 cx.run_until_parked();
18843
18844 let snapshot = editor
18845 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18846 .unwrap();
18847 let hunks = snapshot
18848 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
18849 .map(|hunk| match hunk {
18850 DisplayDiffHunk::Unfolded {
18851 display_row_range, ..
18852 } => display_row_range,
18853 DisplayDiffHunk::Folded { .. } => unreachable!(),
18854 })
18855 .collect::<Vec<_>>();
18856 assert_eq!(
18857 hunks,
18858 [
18859 DisplayRow(2)..DisplayRow(4),
18860 DisplayRow(7)..DisplayRow(9),
18861 DisplayRow(12)..DisplayRow(14),
18862 ]
18863 );
18864}
18865
18866#[gpui::test]
18867async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
18868 init_test(cx, |_| {});
18869
18870 let mut cx = EditorTestContext::new(cx).await;
18871 cx.set_head_text(indoc! { "
18872 one
18873 two
18874 three
18875 four
18876 five
18877 "
18878 });
18879 cx.set_index_text(indoc! { "
18880 one
18881 two
18882 three
18883 four
18884 five
18885 "
18886 });
18887 cx.set_state(indoc! {"
18888 one
18889 TWO
18890 ˇTHREE
18891 FOUR
18892 five
18893 "});
18894 cx.run_until_parked();
18895 cx.update_editor(|editor, window, cx| {
18896 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18897 });
18898 cx.run_until_parked();
18899 cx.assert_index_text(Some(indoc! {"
18900 one
18901 TWO
18902 THREE
18903 FOUR
18904 five
18905 "}));
18906 cx.set_state(indoc! { "
18907 one
18908 TWO
18909 ˇTHREE-HUNDRED
18910 FOUR
18911 five
18912 "});
18913 cx.run_until_parked();
18914 cx.update_editor(|editor, window, cx| {
18915 let snapshot = editor.snapshot(window, cx);
18916 let hunks = editor
18917 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18918 .collect::<Vec<_>>();
18919 assert_eq!(hunks.len(), 1);
18920 assert_eq!(
18921 hunks[0].status(),
18922 DiffHunkStatus {
18923 kind: DiffHunkStatusKind::Modified,
18924 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
18925 }
18926 );
18927
18928 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18929 });
18930 cx.run_until_parked();
18931 cx.assert_index_text(Some(indoc! {"
18932 one
18933 TWO
18934 THREE-HUNDRED
18935 FOUR
18936 five
18937 "}));
18938}
18939
18940#[gpui::test]
18941fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
18942 init_test(cx, |_| {});
18943
18944 let editor = cx.add_window(|window, cx| {
18945 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
18946 build_editor(buffer, window, cx)
18947 });
18948
18949 let render_args = Arc::new(Mutex::new(None));
18950 let snapshot = editor
18951 .update(cx, |editor, window, cx| {
18952 let snapshot = editor.buffer().read(cx).snapshot(cx);
18953 let range =
18954 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
18955
18956 struct RenderArgs {
18957 row: MultiBufferRow,
18958 folded: bool,
18959 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
18960 }
18961
18962 let crease = Crease::inline(
18963 range,
18964 FoldPlaceholder::test(),
18965 {
18966 let toggle_callback = render_args.clone();
18967 move |row, folded, callback, _window, _cx| {
18968 *toggle_callback.lock() = Some(RenderArgs {
18969 row,
18970 folded,
18971 callback,
18972 });
18973 div()
18974 }
18975 },
18976 |_row, _folded, _window, _cx| div(),
18977 );
18978
18979 editor.insert_creases(Some(crease), cx);
18980 let snapshot = editor.snapshot(window, cx);
18981 let _div = snapshot.render_crease_toggle(
18982 MultiBufferRow(1),
18983 false,
18984 cx.entity().clone(),
18985 window,
18986 cx,
18987 );
18988 snapshot
18989 })
18990 .unwrap();
18991
18992 let render_args = render_args.lock().take().unwrap();
18993 assert_eq!(render_args.row, MultiBufferRow(1));
18994 assert!(!render_args.folded);
18995 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18996
18997 cx.update_window(*editor, |_, window, cx| {
18998 (render_args.callback)(true, window, cx)
18999 })
19000 .unwrap();
19001 let snapshot = editor
19002 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19003 .unwrap();
19004 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
19005
19006 cx.update_window(*editor, |_, window, cx| {
19007 (render_args.callback)(false, window, cx)
19008 })
19009 .unwrap();
19010 let snapshot = editor
19011 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19012 .unwrap();
19013 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19014}
19015
19016#[gpui::test]
19017async fn test_input_text(cx: &mut TestAppContext) {
19018 init_test(cx, |_| {});
19019 let mut cx = EditorTestContext::new(cx).await;
19020
19021 cx.set_state(
19022 &r#"ˇone
19023 two
19024
19025 three
19026 fourˇ
19027 five
19028
19029 siˇx"#
19030 .unindent(),
19031 );
19032
19033 cx.dispatch_action(HandleInput(String::new()));
19034 cx.assert_editor_state(
19035 &r#"ˇone
19036 two
19037
19038 three
19039 fourˇ
19040 five
19041
19042 siˇx"#
19043 .unindent(),
19044 );
19045
19046 cx.dispatch_action(HandleInput("AAAA".to_string()));
19047 cx.assert_editor_state(
19048 &r#"AAAAˇone
19049 two
19050
19051 three
19052 fourAAAAˇ
19053 five
19054
19055 siAAAAˇx"#
19056 .unindent(),
19057 );
19058}
19059
19060#[gpui::test]
19061async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
19062 init_test(cx, |_| {});
19063
19064 let mut cx = EditorTestContext::new(cx).await;
19065 cx.set_state(
19066 r#"let foo = 1;
19067let foo = 2;
19068let foo = 3;
19069let fooˇ = 4;
19070let foo = 5;
19071let foo = 6;
19072let foo = 7;
19073let foo = 8;
19074let foo = 9;
19075let foo = 10;
19076let foo = 11;
19077let foo = 12;
19078let foo = 13;
19079let foo = 14;
19080let foo = 15;"#,
19081 );
19082
19083 cx.update_editor(|e, window, cx| {
19084 assert_eq!(
19085 e.next_scroll_position,
19086 NextScrollCursorCenterTopBottom::Center,
19087 "Default next scroll direction is center",
19088 );
19089
19090 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19091 assert_eq!(
19092 e.next_scroll_position,
19093 NextScrollCursorCenterTopBottom::Top,
19094 "After center, next scroll direction should be top",
19095 );
19096
19097 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19098 assert_eq!(
19099 e.next_scroll_position,
19100 NextScrollCursorCenterTopBottom::Bottom,
19101 "After top, next scroll direction should be bottom",
19102 );
19103
19104 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19105 assert_eq!(
19106 e.next_scroll_position,
19107 NextScrollCursorCenterTopBottom::Center,
19108 "After bottom, scrolling should start over",
19109 );
19110
19111 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19112 assert_eq!(
19113 e.next_scroll_position,
19114 NextScrollCursorCenterTopBottom::Top,
19115 "Scrolling continues if retriggered fast enough"
19116 );
19117 });
19118
19119 cx.executor()
19120 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19121 cx.executor().run_until_parked();
19122 cx.update_editor(|e, _, _| {
19123 assert_eq!(
19124 e.next_scroll_position,
19125 NextScrollCursorCenterTopBottom::Center,
19126 "If scrolling is not triggered fast enough, it should reset"
19127 );
19128 });
19129}
19130
19131#[gpui::test]
19132async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19133 init_test(cx, |_| {});
19134 let mut cx = EditorLspTestContext::new_rust(
19135 lsp::ServerCapabilities {
19136 definition_provider: Some(lsp::OneOf::Left(true)),
19137 references_provider: Some(lsp::OneOf::Left(true)),
19138 ..lsp::ServerCapabilities::default()
19139 },
19140 cx,
19141 )
19142 .await;
19143
19144 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19145 let go_to_definition = cx
19146 .lsp
19147 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19148 move |params, _| async move {
19149 if empty_go_to_definition {
19150 Ok(None)
19151 } else {
19152 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
19153 uri: params.text_document_position_params.text_document.uri,
19154 range: lsp::Range::new(
19155 lsp::Position::new(4, 3),
19156 lsp::Position::new(4, 6),
19157 ),
19158 })))
19159 }
19160 },
19161 );
19162 let references = cx
19163 .lsp
19164 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
19165 Ok(Some(vec![lsp::Location {
19166 uri: params.text_document_position.text_document.uri,
19167 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
19168 }]))
19169 });
19170 (go_to_definition, references)
19171 };
19172
19173 cx.set_state(
19174 &r#"fn one() {
19175 let mut a = ˇtwo();
19176 }
19177
19178 fn two() {}"#
19179 .unindent(),
19180 );
19181 set_up_lsp_handlers(false, &mut cx);
19182 let navigated = cx
19183 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19184 .await
19185 .expect("Failed to navigate to definition");
19186 assert_eq!(
19187 navigated,
19188 Navigated::Yes,
19189 "Should have navigated to definition from the GetDefinition response"
19190 );
19191 cx.assert_editor_state(
19192 &r#"fn one() {
19193 let mut a = two();
19194 }
19195
19196 fn «twoˇ»() {}"#
19197 .unindent(),
19198 );
19199
19200 let editors = cx.update_workspace(|workspace, _, cx| {
19201 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19202 });
19203 cx.update_editor(|_, _, test_editor_cx| {
19204 assert_eq!(
19205 editors.len(),
19206 1,
19207 "Initially, only one, test, editor should be open in the workspace"
19208 );
19209 assert_eq!(
19210 test_editor_cx.entity(),
19211 editors.last().expect("Asserted len is 1").clone()
19212 );
19213 });
19214
19215 set_up_lsp_handlers(true, &mut cx);
19216 let navigated = cx
19217 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19218 .await
19219 .expect("Failed to navigate to lookup references");
19220 assert_eq!(
19221 navigated,
19222 Navigated::Yes,
19223 "Should have navigated to references as a fallback after empty GoToDefinition response"
19224 );
19225 // We should not change the selections in the existing file,
19226 // if opening another milti buffer with the references
19227 cx.assert_editor_state(
19228 &r#"fn one() {
19229 let mut a = two();
19230 }
19231
19232 fn «twoˇ»() {}"#
19233 .unindent(),
19234 );
19235 let editors = cx.update_workspace(|workspace, _, cx| {
19236 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19237 });
19238 cx.update_editor(|_, _, test_editor_cx| {
19239 assert_eq!(
19240 editors.len(),
19241 2,
19242 "After falling back to references search, we open a new editor with the results"
19243 );
19244 let references_fallback_text = editors
19245 .into_iter()
19246 .find(|new_editor| *new_editor != test_editor_cx.entity())
19247 .expect("Should have one non-test editor now")
19248 .read(test_editor_cx)
19249 .text(test_editor_cx);
19250 assert_eq!(
19251 references_fallback_text, "fn one() {\n let mut a = two();\n}",
19252 "Should use the range from the references response and not the GoToDefinition one"
19253 );
19254 });
19255}
19256
19257#[gpui::test]
19258async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
19259 init_test(cx, |_| {});
19260 cx.update(|cx| {
19261 let mut editor_settings = EditorSettings::get_global(cx).clone();
19262 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
19263 EditorSettings::override_global(editor_settings, cx);
19264 });
19265 let mut cx = EditorLspTestContext::new_rust(
19266 lsp::ServerCapabilities {
19267 definition_provider: Some(lsp::OneOf::Left(true)),
19268 references_provider: Some(lsp::OneOf::Left(true)),
19269 ..lsp::ServerCapabilities::default()
19270 },
19271 cx,
19272 )
19273 .await;
19274 let original_state = r#"fn one() {
19275 let mut a = ˇtwo();
19276 }
19277
19278 fn two() {}"#
19279 .unindent();
19280 cx.set_state(&original_state);
19281
19282 let mut go_to_definition = cx
19283 .lsp
19284 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19285 move |_, _| async move { Ok(None) },
19286 );
19287 let _references = cx
19288 .lsp
19289 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
19290 panic!("Should not call for references with no go to definition fallback")
19291 });
19292
19293 let navigated = cx
19294 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19295 .await
19296 .expect("Failed to navigate to lookup references");
19297 go_to_definition
19298 .next()
19299 .await
19300 .expect("Should have called the go_to_definition handler");
19301
19302 assert_eq!(
19303 navigated,
19304 Navigated::No,
19305 "Should have navigated to references as a fallback after empty GoToDefinition response"
19306 );
19307 cx.assert_editor_state(&original_state);
19308 let editors = cx.update_workspace(|workspace, _, cx| {
19309 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19310 });
19311 cx.update_editor(|_, _, _| {
19312 assert_eq!(
19313 editors.len(),
19314 1,
19315 "After unsuccessful fallback, no other editor should have been opened"
19316 );
19317 });
19318}
19319
19320#[gpui::test]
19321async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
19322 init_test(cx, |_| {});
19323
19324 let language = Arc::new(Language::new(
19325 LanguageConfig::default(),
19326 Some(tree_sitter_rust::LANGUAGE.into()),
19327 ));
19328
19329 let text = r#"
19330 #[cfg(test)]
19331 mod tests() {
19332 #[test]
19333 fn runnable_1() {
19334 let a = 1;
19335 }
19336
19337 #[test]
19338 fn runnable_2() {
19339 let a = 1;
19340 let b = 2;
19341 }
19342 }
19343 "#
19344 .unindent();
19345
19346 let fs = FakeFs::new(cx.executor());
19347 fs.insert_file("/file.rs", Default::default()).await;
19348
19349 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19350 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19351 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19352 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
19353 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
19354
19355 let editor = cx.new_window_entity(|window, cx| {
19356 Editor::new(
19357 EditorMode::full(),
19358 multi_buffer,
19359 Some(project.clone()),
19360 window,
19361 cx,
19362 )
19363 });
19364
19365 editor.update_in(cx, |editor, window, cx| {
19366 let snapshot = editor.buffer().read(cx).snapshot(cx);
19367 editor.tasks.insert(
19368 (buffer.read(cx).remote_id(), 3),
19369 RunnableTasks {
19370 templates: vec![],
19371 offset: snapshot.anchor_before(43),
19372 column: 0,
19373 extra_variables: HashMap::default(),
19374 context_range: BufferOffset(43)..BufferOffset(85),
19375 },
19376 );
19377 editor.tasks.insert(
19378 (buffer.read(cx).remote_id(), 8),
19379 RunnableTasks {
19380 templates: vec![],
19381 offset: snapshot.anchor_before(86),
19382 column: 0,
19383 extra_variables: HashMap::default(),
19384 context_range: BufferOffset(86)..BufferOffset(191),
19385 },
19386 );
19387
19388 // Test finding task when cursor is inside function body
19389 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19390 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19391 });
19392 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19393 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19394
19395 // Test finding task when cursor is on function name
19396 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19397 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19398 });
19399 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19400 assert_eq!(row, 8, "Should find task when cursor is on function name");
19401 });
19402}
19403
19404#[gpui::test]
19405async fn test_folding_buffers(cx: &mut TestAppContext) {
19406 init_test(cx, |_| {});
19407
19408 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19409 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19410 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19411
19412 let fs = FakeFs::new(cx.executor());
19413 fs.insert_tree(
19414 path!("/a"),
19415 json!({
19416 "first.rs": sample_text_1,
19417 "second.rs": sample_text_2,
19418 "third.rs": sample_text_3,
19419 }),
19420 )
19421 .await;
19422 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19423 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19424 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19425 let worktree = project.update(cx, |project, cx| {
19426 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19427 assert_eq!(worktrees.len(), 1);
19428 worktrees.pop().unwrap()
19429 });
19430 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19431
19432 let buffer_1 = project
19433 .update(cx, |project, cx| {
19434 project.open_buffer((worktree_id, "first.rs"), cx)
19435 })
19436 .await
19437 .unwrap();
19438 let buffer_2 = project
19439 .update(cx, |project, cx| {
19440 project.open_buffer((worktree_id, "second.rs"), cx)
19441 })
19442 .await
19443 .unwrap();
19444 let buffer_3 = project
19445 .update(cx, |project, cx| {
19446 project.open_buffer((worktree_id, "third.rs"), cx)
19447 })
19448 .await
19449 .unwrap();
19450
19451 let multi_buffer = cx.new(|cx| {
19452 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19453 multi_buffer.push_excerpts(
19454 buffer_1.clone(),
19455 [
19456 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19457 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19458 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19459 ],
19460 cx,
19461 );
19462 multi_buffer.push_excerpts(
19463 buffer_2.clone(),
19464 [
19465 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19466 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19467 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19468 ],
19469 cx,
19470 );
19471 multi_buffer.push_excerpts(
19472 buffer_3.clone(),
19473 [
19474 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19475 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19476 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19477 ],
19478 cx,
19479 );
19480 multi_buffer
19481 });
19482 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19483 Editor::new(
19484 EditorMode::full(),
19485 multi_buffer.clone(),
19486 Some(project.clone()),
19487 window,
19488 cx,
19489 )
19490 });
19491
19492 assert_eq!(
19493 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19494 "\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",
19495 );
19496
19497 multi_buffer_editor.update(cx, |editor, cx| {
19498 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19499 });
19500 assert_eq!(
19501 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19502 "\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",
19503 "After folding the first buffer, its text should not be displayed"
19504 );
19505
19506 multi_buffer_editor.update(cx, |editor, cx| {
19507 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19508 });
19509 assert_eq!(
19510 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19511 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
19512 "After folding the second buffer, its text should not be displayed"
19513 );
19514
19515 multi_buffer_editor.update(cx, |editor, cx| {
19516 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19517 });
19518 assert_eq!(
19519 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19520 "\n\n\n\n\n",
19521 "After folding the third buffer, its text should not be displayed"
19522 );
19523
19524 // Emulate selection inside the fold logic, that should work
19525 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19526 editor
19527 .snapshot(window, cx)
19528 .next_line_boundary(Point::new(0, 4));
19529 });
19530
19531 multi_buffer_editor.update(cx, |editor, cx| {
19532 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19533 });
19534 assert_eq!(
19535 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19536 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19537 "After unfolding the second buffer, its text should be displayed"
19538 );
19539
19540 // Typing inside of buffer 1 causes that buffer to be unfolded.
19541 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19542 assert_eq!(
19543 multi_buffer
19544 .read(cx)
19545 .snapshot(cx)
19546 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
19547 .collect::<String>(),
19548 "bbbb"
19549 );
19550 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
19551 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
19552 });
19553 editor.handle_input("B", window, cx);
19554 });
19555
19556 assert_eq!(
19557 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19558 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19559 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
19560 );
19561
19562 multi_buffer_editor.update(cx, |editor, cx| {
19563 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19564 });
19565 assert_eq!(
19566 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19567 "\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",
19568 "After unfolding the all buffers, all original text should be displayed"
19569 );
19570}
19571
19572#[gpui::test]
19573async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
19574 init_test(cx, |_| {});
19575
19576 let sample_text_1 = "1111\n2222\n3333".to_string();
19577 let sample_text_2 = "4444\n5555\n6666".to_string();
19578 let sample_text_3 = "7777\n8888\n9999".to_string();
19579
19580 let fs = FakeFs::new(cx.executor());
19581 fs.insert_tree(
19582 path!("/a"),
19583 json!({
19584 "first.rs": sample_text_1,
19585 "second.rs": sample_text_2,
19586 "third.rs": sample_text_3,
19587 }),
19588 )
19589 .await;
19590 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19591 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19592 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19593 let worktree = project.update(cx, |project, cx| {
19594 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19595 assert_eq!(worktrees.len(), 1);
19596 worktrees.pop().unwrap()
19597 });
19598 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19599
19600 let buffer_1 = project
19601 .update(cx, |project, cx| {
19602 project.open_buffer((worktree_id, "first.rs"), cx)
19603 })
19604 .await
19605 .unwrap();
19606 let buffer_2 = project
19607 .update(cx, |project, cx| {
19608 project.open_buffer((worktree_id, "second.rs"), cx)
19609 })
19610 .await
19611 .unwrap();
19612 let buffer_3 = project
19613 .update(cx, |project, cx| {
19614 project.open_buffer((worktree_id, "third.rs"), cx)
19615 })
19616 .await
19617 .unwrap();
19618
19619 let multi_buffer = cx.new(|cx| {
19620 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19621 multi_buffer.push_excerpts(
19622 buffer_1.clone(),
19623 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19624 cx,
19625 );
19626 multi_buffer.push_excerpts(
19627 buffer_2.clone(),
19628 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19629 cx,
19630 );
19631 multi_buffer.push_excerpts(
19632 buffer_3.clone(),
19633 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19634 cx,
19635 );
19636 multi_buffer
19637 });
19638
19639 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19640 Editor::new(
19641 EditorMode::full(),
19642 multi_buffer,
19643 Some(project.clone()),
19644 window,
19645 cx,
19646 )
19647 });
19648
19649 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
19650 assert_eq!(
19651 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19652 full_text,
19653 );
19654
19655 multi_buffer_editor.update(cx, |editor, cx| {
19656 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19657 });
19658 assert_eq!(
19659 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19660 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
19661 "After folding the first buffer, its text should not be displayed"
19662 );
19663
19664 multi_buffer_editor.update(cx, |editor, cx| {
19665 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19666 });
19667
19668 assert_eq!(
19669 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19670 "\n\n\n\n\n\n7777\n8888\n9999",
19671 "After folding the second buffer, its text should not be displayed"
19672 );
19673
19674 multi_buffer_editor.update(cx, |editor, cx| {
19675 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19676 });
19677 assert_eq!(
19678 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19679 "\n\n\n\n\n",
19680 "After folding the third buffer, its text should not be displayed"
19681 );
19682
19683 multi_buffer_editor.update(cx, |editor, cx| {
19684 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19685 });
19686 assert_eq!(
19687 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19688 "\n\n\n\n4444\n5555\n6666\n\n",
19689 "After unfolding the second buffer, its text should be displayed"
19690 );
19691
19692 multi_buffer_editor.update(cx, |editor, cx| {
19693 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
19694 });
19695 assert_eq!(
19696 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19697 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
19698 "After unfolding the first buffer, its text should be displayed"
19699 );
19700
19701 multi_buffer_editor.update(cx, |editor, cx| {
19702 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19703 });
19704 assert_eq!(
19705 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19706 full_text,
19707 "After unfolding all buffers, all original text should be displayed"
19708 );
19709}
19710
19711#[gpui::test]
19712async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
19713 init_test(cx, |_| {});
19714
19715 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19716
19717 let fs = FakeFs::new(cx.executor());
19718 fs.insert_tree(
19719 path!("/a"),
19720 json!({
19721 "main.rs": sample_text,
19722 }),
19723 )
19724 .await;
19725 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19726 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19727 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19728 let worktree = project.update(cx, |project, cx| {
19729 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19730 assert_eq!(worktrees.len(), 1);
19731 worktrees.pop().unwrap()
19732 });
19733 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19734
19735 let buffer_1 = project
19736 .update(cx, |project, cx| {
19737 project.open_buffer((worktree_id, "main.rs"), cx)
19738 })
19739 .await
19740 .unwrap();
19741
19742 let multi_buffer = cx.new(|cx| {
19743 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19744 multi_buffer.push_excerpts(
19745 buffer_1.clone(),
19746 [ExcerptRange::new(
19747 Point::new(0, 0)
19748 ..Point::new(
19749 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
19750 0,
19751 ),
19752 )],
19753 cx,
19754 );
19755 multi_buffer
19756 });
19757 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19758 Editor::new(
19759 EditorMode::full(),
19760 multi_buffer,
19761 Some(project.clone()),
19762 window,
19763 cx,
19764 )
19765 });
19766
19767 let selection_range = Point::new(1, 0)..Point::new(2, 0);
19768 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19769 enum TestHighlight {}
19770 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
19771 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
19772 editor.highlight_text::<TestHighlight>(
19773 vec![highlight_range.clone()],
19774 HighlightStyle::color(Hsla::green()),
19775 cx,
19776 );
19777 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19778 s.select_ranges(Some(highlight_range))
19779 });
19780 });
19781
19782 let full_text = format!("\n\n{sample_text}");
19783 assert_eq!(
19784 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19785 full_text,
19786 );
19787}
19788
19789#[gpui::test]
19790async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
19791 init_test(cx, |_| {});
19792 cx.update(|cx| {
19793 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
19794 "keymaps/default-linux.json",
19795 cx,
19796 )
19797 .unwrap();
19798 cx.bind_keys(default_key_bindings);
19799 });
19800
19801 let (editor, cx) = cx.add_window_view(|window, cx| {
19802 let multi_buffer = MultiBuffer::build_multi(
19803 [
19804 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
19805 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
19806 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
19807 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
19808 ],
19809 cx,
19810 );
19811 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
19812
19813 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
19814 // fold all but the second buffer, so that we test navigating between two
19815 // adjacent folded buffers, as well as folded buffers at the start and
19816 // end the multibuffer
19817 editor.fold_buffer(buffer_ids[0], cx);
19818 editor.fold_buffer(buffer_ids[2], cx);
19819 editor.fold_buffer(buffer_ids[3], cx);
19820
19821 editor
19822 });
19823 cx.simulate_resize(size(px(1000.), px(1000.)));
19824
19825 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
19826 cx.assert_excerpts_with_selections(indoc! {"
19827 [EXCERPT]
19828 ˇ[FOLDED]
19829 [EXCERPT]
19830 a1
19831 b1
19832 [EXCERPT]
19833 [FOLDED]
19834 [EXCERPT]
19835 [FOLDED]
19836 "
19837 });
19838 cx.simulate_keystroke("down");
19839 cx.assert_excerpts_with_selections(indoc! {"
19840 [EXCERPT]
19841 [FOLDED]
19842 [EXCERPT]
19843 ˇa1
19844 b1
19845 [EXCERPT]
19846 [FOLDED]
19847 [EXCERPT]
19848 [FOLDED]
19849 "
19850 });
19851 cx.simulate_keystroke("down");
19852 cx.assert_excerpts_with_selections(indoc! {"
19853 [EXCERPT]
19854 [FOLDED]
19855 [EXCERPT]
19856 a1
19857 ˇb1
19858 [EXCERPT]
19859 [FOLDED]
19860 [EXCERPT]
19861 [FOLDED]
19862 "
19863 });
19864 cx.simulate_keystroke("down");
19865 cx.assert_excerpts_with_selections(indoc! {"
19866 [EXCERPT]
19867 [FOLDED]
19868 [EXCERPT]
19869 a1
19870 b1
19871 ˇ[EXCERPT]
19872 [FOLDED]
19873 [EXCERPT]
19874 [FOLDED]
19875 "
19876 });
19877 cx.simulate_keystroke("down");
19878 cx.assert_excerpts_with_selections(indoc! {"
19879 [EXCERPT]
19880 [FOLDED]
19881 [EXCERPT]
19882 a1
19883 b1
19884 [EXCERPT]
19885 ˇ[FOLDED]
19886 [EXCERPT]
19887 [FOLDED]
19888 "
19889 });
19890 for _ in 0..5 {
19891 cx.simulate_keystroke("down");
19892 cx.assert_excerpts_with_selections(indoc! {"
19893 [EXCERPT]
19894 [FOLDED]
19895 [EXCERPT]
19896 a1
19897 b1
19898 [EXCERPT]
19899 [FOLDED]
19900 [EXCERPT]
19901 ˇ[FOLDED]
19902 "
19903 });
19904 }
19905
19906 cx.simulate_keystroke("up");
19907 cx.assert_excerpts_with_selections(indoc! {"
19908 [EXCERPT]
19909 [FOLDED]
19910 [EXCERPT]
19911 a1
19912 b1
19913 [EXCERPT]
19914 ˇ[FOLDED]
19915 [EXCERPT]
19916 [FOLDED]
19917 "
19918 });
19919 cx.simulate_keystroke("up");
19920 cx.assert_excerpts_with_selections(indoc! {"
19921 [EXCERPT]
19922 [FOLDED]
19923 [EXCERPT]
19924 a1
19925 b1
19926 ˇ[EXCERPT]
19927 [FOLDED]
19928 [EXCERPT]
19929 [FOLDED]
19930 "
19931 });
19932 cx.simulate_keystroke("up");
19933 cx.assert_excerpts_with_selections(indoc! {"
19934 [EXCERPT]
19935 [FOLDED]
19936 [EXCERPT]
19937 a1
19938 ˇb1
19939 [EXCERPT]
19940 [FOLDED]
19941 [EXCERPT]
19942 [FOLDED]
19943 "
19944 });
19945 cx.simulate_keystroke("up");
19946 cx.assert_excerpts_with_selections(indoc! {"
19947 [EXCERPT]
19948 [FOLDED]
19949 [EXCERPT]
19950 ˇa1
19951 b1
19952 [EXCERPT]
19953 [FOLDED]
19954 [EXCERPT]
19955 [FOLDED]
19956 "
19957 });
19958 for _ in 0..5 {
19959 cx.simulate_keystroke("up");
19960 cx.assert_excerpts_with_selections(indoc! {"
19961 [EXCERPT]
19962 ˇ[FOLDED]
19963 [EXCERPT]
19964 a1
19965 b1
19966 [EXCERPT]
19967 [FOLDED]
19968 [EXCERPT]
19969 [FOLDED]
19970 "
19971 });
19972 }
19973}
19974
19975#[gpui::test]
19976async fn test_inline_completion_text(cx: &mut TestAppContext) {
19977 init_test(cx, |_| {});
19978
19979 // Simple insertion
19980 assert_highlighted_edits(
19981 "Hello, world!",
19982 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
19983 true,
19984 cx,
19985 |highlighted_edits, cx| {
19986 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
19987 assert_eq!(highlighted_edits.highlights.len(), 1);
19988 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
19989 assert_eq!(
19990 highlighted_edits.highlights[0].1.background_color,
19991 Some(cx.theme().status().created_background)
19992 );
19993 },
19994 )
19995 .await;
19996
19997 // Replacement
19998 assert_highlighted_edits(
19999 "This is a test.",
20000 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
20001 false,
20002 cx,
20003 |highlighted_edits, cx| {
20004 assert_eq!(highlighted_edits.text, "That is a test.");
20005 assert_eq!(highlighted_edits.highlights.len(), 1);
20006 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
20007 assert_eq!(
20008 highlighted_edits.highlights[0].1.background_color,
20009 Some(cx.theme().status().created_background)
20010 );
20011 },
20012 )
20013 .await;
20014
20015 // Multiple edits
20016 assert_highlighted_edits(
20017 "Hello, world!",
20018 vec![
20019 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
20020 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
20021 ],
20022 false,
20023 cx,
20024 |highlighted_edits, cx| {
20025 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
20026 assert_eq!(highlighted_edits.highlights.len(), 2);
20027 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
20028 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
20029 assert_eq!(
20030 highlighted_edits.highlights[0].1.background_color,
20031 Some(cx.theme().status().created_background)
20032 );
20033 assert_eq!(
20034 highlighted_edits.highlights[1].1.background_color,
20035 Some(cx.theme().status().created_background)
20036 );
20037 },
20038 )
20039 .await;
20040
20041 // Multiple lines with edits
20042 assert_highlighted_edits(
20043 "First line\nSecond line\nThird line\nFourth line",
20044 vec![
20045 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
20046 (
20047 Point::new(2, 0)..Point::new(2, 10),
20048 "New third line".to_string(),
20049 ),
20050 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
20051 ],
20052 false,
20053 cx,
20054 |highlighted_edits, cx| {
20055 assert_eq!(
20056 highlighted_edits.text,
20057 "Second modified\nNew third line\nFourth updated line"
20058 );
20059 assert_eq!(highlighted_edits.highlights.len(), 3);
20060 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
20061 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
20062 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
20063 for highlight in &highlighted_edits.highlights {
20064 assert_eq!(
20065 highlight.1.background_color,
20066 Some(cx.theme().status().created_background)
20067 );
20068 }
20069 },
20070 )
20071 .await;
20072}
20073
20074#[gpui::test]
20075async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
20076 init_test(cx, |_| {});
20077
20078 // Deletion
20079 assert_highlighted_edits(
20080 "Hello, world!",
20081 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20082 true,
20083 cx,
20084 |highlighted_edits, cx| {
20085 assert_eq!(highlighted_edits.text, "Hello, world!");
20086 assert_eq!(highlighted_edits.highlights.len(), 1);
20087 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20088 assert_eq!(
20089 highlighted_edits.highlights[0].1.background_color,
20090 Some(cx.theme().status().deleted_background)
20091 );
20092 },
20093 )
20094 .await;
20095
20096 // Insertion
20097 assert_highlighted_edits(
20098 "Hello, world!",
20099 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
20100 true,
20101 cx,
20102 |highlighted_edits, cx| {
20103 assert_eq!(highlighted_edits.highlights.len(), 1);
20104 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20105 assert_eq!(
20106 highlighted_edits.highlights[0].1.background_color,
20107 Some(cx.theme().status().created_background)
20108 );
20109 },
20110 )
20111 .await;
20112}
20113
20114async fn assert_highlighted_edits(
20115 text: &str,
20116 edits: Vec<(Range<Point>, String)>,
20117 include_deletions: bool,
20118 cx: &mut TestAppContext,
20119 assertion_fn: impl Fn(HighlightedText, &App),
20120) {
20121 let window = cx.add_window(|window, cx| {
20122 let buffer = MultiBuffer::build_simple(text, cx);
20123 Editor::new(EditorMode::full(), buffer, None, window, cx)
20124 });
20125 let cx = &mut VisualTestContext::from_window(*window, cx);
20126
20127 let (buffer, snapshot) = window
20128 .update(cx, |editor, _window, cx| {
20129 (
20130 editor.buffer().clone(),
20131 editor.buffer().read(cx).snapshot(cx),
20132 )
20133 })
20134 .unwrap();
20135
20136 let edits = edits
20137 .into_iter()
20138 .map(|(range, edit)| {
20139 (
20140 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20141 edit,
20142 )
20143 })
20144 .collect::<Vec<_>>();
20145
20146 let text_anchor_edits = edits
20147 .clone()
20148 .into_iter()
20149 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
20150 .collect::<Vec<_>>();
20151
20152 let edit_preview = window
20153 .update(cx, |_, _window, cx| {
20154 buffer
20155 .read(cx)
20156 .as_singleton()
20157 .unwrap()
20158 .read(cx)
20159 .preview_edits(text_anchor_edits.into(), cx)
20160 })
20161 .unwrap()
20162 .await;
20163
20164 cx.update(|_window, cx| {
20165 let highlighted_edits = inline_completion_edit_text(
20166 &snapshot.as_singleton().unwrap().2,
20167 &edits,
20168 &edit_preview,
20169 include_deletions,
20170 cx,
20171 );
20172 assertion_fn(highlighted_edits, cx)
20173 });
20174}
20175
20176#[track_caller]
20177fn assert_breakpoint(
20178 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
20179 path: &Arc<Path>,
20180 expected: Vec<(u32, Breakpoint)>,
20181) {
20182 if expected.len() == 0usize {
20183 assert!(!breakpoints.contains_key(path), "{}", path.display());
20184 } else {
20185 let mut breakpoint = breakpoints
20186 .get(path)
20187 .unwrap()
20188 .into_iter()
20189 .map(|breakpoint| {
20190 (
20191 breakpoint.row,
20192 Breakpoint {
20193 message: breakpoint.message.clone(),
20194 state: breakpoint.state,
20195 condition: breakpoint.condition.clone(),
20196 hit_condition: breakpoint.hit_condition.clone(),
20197 },
20198 )
20199 })
20200 .collect::<Vec<_>>();
20201
20202 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
20203
20204 assert_eq!(expected, breakpoint);
20205 }
20206}
20207
20208fn add_log_breakpoint_at_cursor(
20209 editor: &mut Editor,
20210 log_message: &str,
20211 window: &mut Window,
20212 cx: &mut Context<Editor>,
20213) {
20214 let (anchor, bp) = editor
20215 .breakpoints_at_cursors(window, cx)
20216 .first()
20217 .and_then(|(anchor, bp)| {
20218 if let Some(bp) = bp {
20219 Some((*anchor, bp.clone()))
20220 } else {
20221 None
20222 }
20223 })
20224 .unwrap_or_else(|| {
20225 let cursor_position: Point = editor.selections.newest(cx).head();
20226
20227 let breakpoint_position = editor
20228 .snapshot(window, cx)
20229 .display_snapshot
20230 .buffer_snapshot
20231 .anchor_before(Point::new(cursor_position.row, 0));
20232
20233 (breakpoint_position, Breakpoint::new_log(&log_message))
20234 });
20235
20236 editor.edit_breakpoint_at_anchor(
20237 anchor,
20238 bp,
20239 BreakpointEditAction::EditLogMessage(log_message.into()),
20240 cx,
20241 );
20242}
20243
20244#[gpui::test]
20245async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
20246 init_test(cx, |_| {});
20247
20248 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20249 let fs = FakeFs::new(cx.executor());
20250 fs.insert_tree(
20251 path!("/a"),
20252 json!({
20253 "main.rs": sample_text,
20254 }),
20255 )
20256 .await;
20257 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20258 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20259 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20260
20261 let fs = FakeFs::new(cx.executor());
20262 fs.insert_tree(
20263 path!("/a"),
20264 json!({
20265 "main.rs": sample_text,
20266 }),
20267 )
20268 .await;
20269 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20270 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20271 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20272 let worktree_id = workspace
20273 .update(cx, |workspace, _window, cx| {
20274 workspace.project().update(cx, |project, cx| {
20275 project.worktrees(cx).next().unwrap().read(cx).id()
20276 })
20277 })
20278 .unwrap();
20279
20280 let buffer = project
20281 .update(cx, |project, cx| {
20282 project.open_buffer((worktree_id, "main.rs"), cx)
20283 })
20284 .await
20285 .unwrap();
20286
20287 let (editor, cx) = cx.add_window_view(|window, cx| {
20288 Editor::new(
20289 EditorMode::full(),
20290 MultiBuffer::build_from_buffer(buffer, cx),
20291 Some(project.clone()),
20292 window,
20293 cx,
20294 )
20295 });
20296
20297 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20298 let abs_path = project.read_with(cx, |project, cx| {
20299 project
20300 .absolute_path(&project_path, cx)
20301 .map(|path_buf| Arc::from(path_buf.to_owned()))
20302 .unwrap()
20303 });
20304
20305 // assert we can add breakpoint on the first line
20306 editor.update_in(cx, |editor, window, cx| {
20307 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20308 editor.move_to_end(&MoveToEnd, window, cx);
20309 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20310 });
20311
20312 let breakpoints = editor.update(cx, |editor, cx| {
20313 editor
20314 .breakpoint_store()
20315 .as_ref()
20316 .unwrap()
20317 .read(cx)
20318 .all_source_breakpoints(cx)
20319 .clone()
20320 });
20321
20322 assert_eq!(1, breakpoints.len());
20323 assert_breakpoint(
20324 &breakpoints,
20325 &abs_path,
20326 vec![
20327 (0, Breakpoint::new_standard()),
20328 (3, Breakpoint::new_standard()),
20329 ],
20330 );
20331
20332 editor.update_in(cx, |editor, window, cx| {
20333 editor.move_to_beginning(&MoveToBeginning, window, cx);
20334 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20335 });
20336
20337 let breakpoints = editor.update(cx, |editor, cx| {
20338 editor
20339 .breakpoint_store()
20340 .as_ref()
20341 .unwrap()
20342 .read(cx)
20343 .all_source_breakpoints(cx)
20344 .clone()
20345 });
20346
20347 assert_eq!(1, breakpoints.len());
20348 assert_breakpoint(
20349 &breakpoints,
20350 &abs_path,
20351 vec![(3, Breakpoint::new_standard())],
20352 );
20353
20354 editor.update_in(cx, |editor, window, cx| {
20355 editor.move_to_end(&MoveToEnd, window, cx);
20356 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20357 });
20358
20359 let breakpoints = editor.update(cx, |editor, cx| {
20360 editor
20361 .breakpoint_store()
20362 .as_ref()
20363 .unwrap()
20364 .read(cx)
20365 .all_source_breakpoints(cx)
20366 .clone()
20367 });
20368
20369 assert_eq!(0, breakpoints.len());
20370 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20371}
20372
20373#[gpui::test]
20374async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
20375 init_test(cx, |_| {});
20376
20377 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20378
20379 let fs = FakeFs::new(cx.executor());
20380 fs.insert_tree(
20381 path!("/a"),
20382 json!({
20383 "main.rs": sample_text,
20384 }),
20385 )
20386 .await;
20387 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20388 let (workspace, cx) =
20389 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20390
20391 let worktree_id = workspace.update(cx, |workspace, cx| {
20392 workspace.project().update(cx, |project, cx| {
20393 project.worktrees(cx).next().unwrap().read(cx).id()
20394 })
20395 });
20396
20397 let buffer = project
20398 .update(cx, |project, cx| {
20399 project.open_buffer((worktree_id, "main.rs"), cx)
20400 })
20401 .await
20402 .unwrap();
20403
20404 let (editor, cx) = cx.add_window_view(|window, cx| {
20405 Editor::new(
20406 EditorMode::full(),
20407 MultiBuffer::build_from_buffer(buffer, cx),
20408 Some(project.clone()),
20409 window,
20410 cx,
20411 )
20412 });
20413
20414 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20415 let abs_path = project.read_with(cx, |project, cx| {
20416 project
20417 .absolute_path(&project_path, cx)
20418 .map(|path_buf| Arc::from(path_buf.to_owned()))
20419 .unwrap()
20420 });
20421
20422 editor.update_in(cx, |editor, window, cx| {
20423 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20424 });
20425
20426 let breakpoints = editor.update(cx, |editor, cx| {
20427 editor
20428 .breakpoint_store()
20429 .as_ref()
20430 .unwrap()
20431 .read(cx)
20432 .all_source_breakpoints(cx)
20433 .clone()
20434 });
20435
20436 assert_breakpoint(
20437 &breakpoints,
20438 &abs_path,
20439 vec![(0, Breakpoint::new_log("hello world"))],
20440 );
20441
20442 // Removing a log message from a log breakpoint should remove it
20443 editor.update_in(cx, |editor, window, cx| {
20444 add_log_breakpoint_at_cursor(editor, "", window, cx);
20445 });
20446
20447 let breakpoints = editor.update(cx, |editor, cx| {
20448 editor
20449 .breakpoint_store()
20450 .as_ref()
20451 .unwrap()
20452 .read(cx)
20453 .all_source_breakpoints(cx)
20454 .clone()
20455 });
20456
20457 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20458
20459 editor.update_in(cx, |editor, window, cx| {
20460 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20461 editor.move_to_end(&MoveToEnd, window, cx);
20462 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20463 // Not adding a log message to a standard breakpoint shouldn't remove it
20464 add_log_breakpoint_at_cursor(editor, "", window, cx);
20465 });
20466
20467 let breakpoints = editor.update(cx, |editor, cx| {
20468 editor
20469 .breakpoint_store()
20470 .as_ref()
20471 .unwrap()
20472 .read(cx)
20473 .all_source_breakpoints(cx)
20474 .clone()
20475 });
20476
20477 assert_breakpoint(
20478 &breakpoints,
20479 &abs_path,
20480 vec![
20481 (0, Breakpoint::new_standard()),
20482 (3, Breakpoint::new_standard()),
20483 ],
20484 );
20485
20486 editor.update_in(cx, |editor, window, cx| {
20487 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20488 });
20489
20490 let breakpoints = editor.update(cx, |editor, cx| {
20491 editor
20492 .breakpoint_store()
20493 .as_ref()
20494 .unwrap()
20495 .read(cx)
20496 .all_source_breakpoints(cx)
20497 .clone()
20498 });
20499
20500 assert_breakpoint(
20501 &breakpoints,
20502 &abs_path,
20503 vec![
20504 (0, Breakpoint::new_standard()),
20505 (3, Breakpoint::new_log("hello world")),
20506 ],
20507 );
20508
20509 editor.update_in(cx, |editor, window, cx| {
20510 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
20511 });
20512
20513 let breakpoints = editor.update(cx, |editor, cx| {
20514 editor
20515 .breakpoint_store()
20516 .as_ref()
20517 .unwrap()
20518 .read(cx)
20519 .all_source_breakpoints(cx)
20520 .clone()
20521 });
20522
20523 assert_breakpoint(
20524 &breakpoints,
20525 &abs_path,
20526 vec![
20527 (0, Breakpoint::new_standard()),
20528 (3, Breakpoint::new_log("hello Earth!!")),
20529 ],
20530 );
20531}
20532
20533/// This also tests that Editor::breakpoint_at_cursor_head is working properly
20534/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
20535/// or when breakpoints were placed out of order. This tests for a regression too
20536#[gpui::test]
20537async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
20538 init_test(cx, |_| {});
20539
20540 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20541 let fs = FakeFs::new(cx.executor());
20542 fs.insert_tree(
20543 path!("/a"),
20544 json!({
20545 "main.rs": sample_text,
20546 }),
20547 )
20548 .await;
20549 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20550 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20551 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20552
20553 let fs = FakeFs::new(cx.executor());
20554 fs.insert_tree(
20555 path!("/a"),
20556 json!({
20557 "main.rs": sample_text,
20558 }),
20559 )
20560 .await;
20561 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20562 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20563 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20564 let worktree_id = workspace
20565 .update(cx, |workspace, _window, cx| {
20566 workspace.project().update(cx, |project, cx| {
20567 project.worktrees(cx).next().unwrap().read(cx).id()
20568 })
20569 })
20570 .unwrap();
20571
20572 let buffer = project
20573 .update(cx, |project, cx| {
20574 project.open_buffer((worktree_id, "main.rs"), cx)
20575 })
20576 .await
20577 .unwrap();
20578
20579 let (editor, cx) = cx.add_window_view(|window, cx| {
20580 Editor::new(
20581 EditorMode::full(),
20582 MultiBuffer::build_from_buffer(buffer, cx),
20583 Some(project.clone()),
20584 window,
20585 cx,
20586 )
20587 });
20588
20589 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20590 let abs_path = project.read_with(cx, |project, cx| {
20591 project
20592 .absolute_path(&project_path, cx)
20593 .map(|path_buf| Arc::from(path_buf.to_owned()))
20594 .unwrap()
20595 });
20596
20597 // assert we can add breakpoint on the first line
20598 editor.update_in(cx, |editor, window, cx| {
20599 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20600 editor.move_to_end(&MoveToEnd, window, cx);
20601 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20602 editor.move_up(&MoveUp, window, cx);
20603 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20604 });
20605
20606 let breakpoints = editor.update(cx, |editor, cx| {
20607 editor
20608 .breakpoint_store()
20609 .as_ref()
20610 .unwrap()
20611 .read(cx)
20612 .all_source_breakpoints(cx)
20613 .clone()
20614 });
20615
20616 assert_eq!(1, breakpoints.len());
20617 assert_breakpoint(
20618 &breakpoints,
20619 &abs_path,
20620 vec![
20621 (0, Breakpoint::new_standard()),
20622 (2, Breakpoint::new_standard()),
20623 (3, Breakpoint::new_standard()),
20624 ],
20625 );
20626
20627 editor.update_in(cx, |editor, window, cx| {
20628 editor.move_to_beginning(&MoveToBeginning, window, cx);
20629 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20630 editor.move_to_end(&MoveToEnd, window, cx);
20631 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20632 // Disabling a breakpoint that doesn't exist should do nothing
20633 editor.move_up(&MoveUp, window, cx);
20634 editor.move_up(&MoveUp, window, cx);
20635 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20636 });
20637
20638 let breakpoints = editor.update(cx, |editor, cx| {
20639 editor
20640 .breakpoint_store()
20641 .as_ref()
20642 .unwrap()
20643 .read(cx)
20644 .all_source_breakpoints(cx)
20645 .clone()
20646 });
20647
20648 let disable_breakpoint = {
20649 let mut bp = Breakpoint::new_standard();
20650 bp.state = BreakpointState::Disabled;
20651 bp
20652 };
20653
20654 assert_eq!(1, breakpoints.len());
20655 assert_breakpoint(
20656 &breakpoints,
20657 &abs_path,
20658 vec![
20659 (0, disable_breakpoint.clone()),
20660 (2, Breakpoint::new_standard()),
20661 (3, disable_breakpoint.clone()),
20662 ],
20663 );
20664
20665 editor.update_in(cx, |editor, window, cx| {
20666 editor.move_to_beginning(&MoveToBeginning, window, cx);
20667 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20668 editor.move_to_end(&MoveToEnd, window, cx);
20669 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20670 editor.move_up(&MoveUp, window, cx);
20671 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20672 });
20673
20674 let breakpoints = editor.update(cx, |editor, cx| {
20675 editor
20676 .breakpoint_store()
20677 .as_ref()
20678 .unwrap()
20679 .read(cx)
20680 .all_source_breakpoints(cx)
20681 .clone()
20682 });
20683
20684 assert_eq!(1, breakpoints.len());
20685 assert_breakpoint(
20686 &breakpoints,
20687 &abs_path,
20688 vec![
20689 (0, Breakpoint::new_standard()),
20690 (2, disable_breakpoint),
20691 (3, Breakpoint::new_standard()),
20692 ],
20693 );
20694}
20695
20696#[gpui::test]
20697async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
20698 init_test(cx, |_| {});
20699 let capabilities = lsp::ServerCapabilities {
20700 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
20701 prepare_provider: Some(true),
20702 work_done_progress_options: Default::default(),
20703 })),
20704 ..Default::default()
20705 };
20706 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20707
20708 cx.set_state(indoc! {"
20709 struct Fˇoo {}
20710 "});
20711
20712 cx.update_editor(|editor, _, cx| {
20713 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20714 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20715 editor.highlight_background::<DocumentHighlightRead>(
20716 &[highlight_range],
20717 |theme| theme.colors().editor_document_highlight_read_background,
20718 cx,
20719 );
20720 });
20721
20722 let mut prepare_rename_handler = cx
20723 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
20724 move |_, _, _| async move {
20725 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
20726 start: lsp::Position {
20727 line: 0,
20728 character: 7,
20729 },
20730 end: lsp::Position {
20731 line: 0,
20732 character: 10,
20733 },
20734 })))
20735 },
20736 );
20737 let prepare_rename_task = cx
20738 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20739 .expect("Prepare rename was not started");
20740 prepare_rename_handler.next().await.unwrap();
20741 prepare_rename_task.await.expect("Prepare rename failed");
20742
20743 let mut rename_handler =
20744 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20745 let edit = lsp::TextEdit {
20746 range: lsp::Range {
20747 start: lsp::Position {
20748 line: 0,
20749 character: 7,
20750 },
20751 end: lsp::Position {
20752 line: 0,
20753 character: 10,
20754 },
20755 },
20756 new_text: "FooRenamed".to_string(),
20757 };
20758 Ok(Some(lsp::WorkspaceEdit::new(
20759 // Specify the same edit twice
20760 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
20761 )))
20762 });
20763 let rename_task = cx
20764 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20765 .expect("Confirm rename was not started");
20766 rename_handler.next().await.unwrap();
20767 rename_task.await.expect("Confirm rename failed");
20768 cx.run_until_parked();
20769
20770 // Despite two edits, only one is actually applied as those are identical
20771 cx.assert_editor_state(indoc! {"
20772 struct FooRenamedˇ {}
20773 "});
20774}
20775
20776#[gpui::test]
20777async fn test_rename_without_prepare(cx: &mut TestAppContext) {
20778 init_test(cx, |_| {});
20779 // These capabilities indicate that the server does not support prepare rename.
20780 let capabilities = lsp::ServerCapabilities {
20781 rename_provider: Some(lsp::OneOf::Left(true)),
20782 ..Default::default()
20783 };
20784 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20785
20786 cx.set_state(indoc! {"
20787 struct Fˇoo {}
20788 "});
20789
20790 cx.update_editor(|editor, _window, cx| {
20791 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20792 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20793 editor.highlight_background::<DocumentHighlightRead>(
20794 &[highlight_range],
20795 |theme| theme.colors().editor_document_highlight_read_background,
20796 cx,
20797 );
20798 });
20799
20800 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20801 .expect("Prepare rename was not started")
20802 .await
20803 .expect("Prepare rename failed");
20804
20805 let mut rename_handler =
20806 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20807 let edit = lsp::TextEdit {
20808 range: lsp::Range {
20809 start: lsp::Position {
20810 line: 0,
20811 character: 7,
20812 },
20813 end: lsp::Position {
20814 line: 0,
20815 character: 10,
20816 },
20817 },
20818 new_text: "FooRenamed".to_string(),
20819 };
20820 Ok(Some(lsp::WorkspaceEdit::new(
20821 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
20822 )))
20823 });
20824 let rename_task = cx
20825 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20826 .expect("Confirm rename was not started");
20827 rename_handler.next().await.unwrap();
20828 rename_task.await.expect("Confirm rename failed");
20829 cx.run_until_parked();
20830
20831 // Correct range is renamed, as `surrounding_word` is used to find it.
20832 cx.assert_editor_state(indoc! {"
20833 struct FooRenamedˇ {}
20834 "});
20835}
20836
20837#[gpui::test]
20838async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
20839 init_test(cx, |_| {});
20840 let mut cx = EditorTestContext::new(cx).await;
20841
20842 let language = Arc::new(
20843 Language::new(
20844 LanguageConfig::default(),
20845 Some(tree_sitter_html::LANGUAGE.into()),
20846 )
20847 .with_brackets_query(
20848 r#"
20849 ("<" @open "/>" @close)
20850 ("</" @open ">" @close)
20851 ("<" @open ">" @close)
20852 ("\"" @open "\"" @close)
20853 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
20854 "#,
20855 )
20856 .unwrap(),
20857 );
20858 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20859
20860 cx.set_state(indoc! {"
20861 <span>ˇ</span>
20862 "});
20863 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20864 cx.assert_editor_state(indoc! {"
20865 <span>
20866 ˇ
20867 </span>
20868 "});
20869
20870 cx.set_state(indoc! {"
20871 <span><span></span>ˇ</span>
20872 "});
20873 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20874 cx.assert_editor_state(indoc! {"
20875 <span><span></span>
20876 ˇ</span>
20877 "});
20878
20879 cx.set_state(indoc! {"
20880 <span>ˇ
20881 </span>
20882 "});
20883 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20884 cx.assert_editor_state(indoc! {"
20885 <span>
20886 ˇ
20887 </span>
20888 "});
20889}
20890
20891#[gpui::test(iterations = 10)]
20892async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
20893 init_test(cx, |_| {});
20894
20895 let fs = FakeFs::new(cx.executor());
20896 fs.insert_tree(
20897 path!("/dir"),
20898 json!({
20899 "a.ts": "a",
20900 }),
20901 )
20902 .await;
20903
20904 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
20905 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20906 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20907
20908 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20909 language_registry.add(Arc::new(Language::new(
20910 LanguageConfig {
20911 name: "TypeScript".into(),
20912 matcher: LanguageMatcher {
20913 path_suffixes: vec!["ts".to_string()],
20914 ..Default::default()
20915 },
20916 ..Default::default()
20917 },
20918 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
20919 )));
20920 let mut fake_language_servers = language_registry.register_fake_lsp(
20921 "TypeScript",
20922 FakeLspAdapter {
20923 capabilities: lsp::ServerCapabilities {
20924 code_lens_provider: Some(lsp::CodeLensOptions {
20925 resolve_provider: Some(true),
20926 }),
20927 execute_command_provider: Some(lsp::ExecuteCommandOptions {
20928 commands: vec!["_the/command".to_string()],
20929 ..lsp::ExecuteCommandOptions::default()
20930 }),
20931 ..lsp::ServerCapabilities::default()
20932 },
20933 ..FakeLspAdapter::default()
20934 },
20935 );
20936
20937 let (buffer, _handle) = project
20938 .update(cx, |p, cx| {
20939 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
20940 })
20941 .await
20942 .unwrap();
20943 cx.executor().run_until_parked();
20944
20945 let fake_server = fake_language_servers.next().await.unwrap();
20946
20947 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
20948 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
20949 drop(buffer_snapshot);
20950 let actions = cx
20951 .update_window(*workspace, |_, window, cx| {
20952 project.code_actions(&buffer, anchor..anchor, window, cx)
20953 })
20954 .unwrap();
20955
20956 fake_server
20957 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
20958 Ok(Some(vec![
20959 lsp::CodeLens {
20960 range: lsp::Range::default(),
20961 command: Some(lsp::Command {
20962 title: "Code lens command".to_owned(),
20963 command: "_the/command".to_owned(),
20964 arguments: None,
20965 }),
20966 data: None,
20967 },
20968 lsp::CodeLens {
20969 range: lsp::Range::default(),
20970 command: Some(lsp::Command {
20971 title: "Command not in capabilities".to_owned(),
20972 command: "not in capabilities".to_owned(),
20973 arguments: None,
20974 }),
20975 data: None,
20976 },
20977 lsp::CodeLens {
20978 range: lsp::Range {
20979 start: lsp::Position {
20980 line: 1,
20981 character: 1,
20982 },
20983 end: lsp::Position {
20984 line: 1,
20985 character: 1,
20986 },
20987 },
20988 command: Some(lsp::Command {
20989 title: "Command not in range".to_owned(),
20990 command: "_the/command".to_owned(),
20991 arguments: None,
20992 }),
20993 data: None,
20994 },
20995 ]))
20996 })
20997 .next()
20998 .await;
20999
21000 let actions = actions.await.unwrap();
21001 assert_eq!(
21002 actions.len(),
21003 1,
21004 "Should have only one valid action for the 0..0 range"
21005 );
21006 let action = actions[0].clone();
21007 let apply = project.update(cx, |project, cx| {
21008 project.apply_code_action(buffer.clone(), action, true, cx)
21009 });
21010
21011 // Resolving the code action does not populate its edits. In absence of
21012 // edits, we must execute the given command.
21013 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
21014 |mut lens, _| async move {
21015 let lens_command = lens.command.as_mut().expect("should have a command");
21016 assert_eq!(lens_command.title, "Code lens command");
21017 lens_command.arguments = Some(vec![json!("the-argument")]);
21018 Ok(lens)
21019 },
21020 );
21021
21022 // While executing the command, the language server sends the editor
21023 // a `workspaceEdit` request.
21024 fake_server
21025 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
21026 let fake = fake_server.clone();
21027 move |params, _| {
21028 assert_eq!(params.command, "_the/command");
21029 let fake = fake.clone();
21030 async move {
21031 fake.server
21032 .request::<lsp::request::ApplyWorkspaceEdit>(
21033 lsp::ApplyWorkspaceEditParams {
21034 label: None,
21035 edit: lsp::WorkspaceEdit {
21036 changes: Some(
21037 [(
21038 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
21039 vec![lsp::TextEdit {
21040 range: lsp::Range::new(
21041 lsp::Position::new(0, 0),
21042 lsp::Position::new(0, 0),
21043 ),
21044 new_text: "X".into(),
21045 }],
21046 )]
21047 .into_iter()
21048 .collect(),
21049 ),
21050 ..Default::default()
21051 },
21052 },
21053 )
21054 .await
21055 .into_response()
21056 .unwrap();
21057 Ok(Some(json!(null)))
21058 }
21059 }
21060 })
21061 .next()
21062 .await;
21063
21064 // Applying the code lens command returns a project transaction containing the edits
21065 // sent by the language server in its `workspaceEdit` request.
21066 let transaction = apply.await.unwrap();
21067 assert!(transaction.0.contains_key(&buffer));
21068 buffer.update(cx, |buffer, cx| {
21069 assert_eq!(buffer.text(), "Xa");
21070 buffer.undo(cx);
21071 assert_eq!(buffer.text(), "a");
21072 });
21073}
21074
21075#[gpui::test]
21076async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
21077 init_test(cx, |_| {});
21078
21079 let fs = FakeFs::new(cx.executor());
21080 let main_text = r#"fn main() {
21081println!("1");
21082println!("2");
21083println!("3");
21084println!("4");
21085println!("5");
21086}"#;
21087 let lib_text = "mod foo {}";
21088 fs.insert_tree(
21089 path!("/a"),
21090 json!({
21091 "lib.rs": lib_text,
21092 "main.rs": main_text,
21093 }),
21094 )
21095 .await;
21096
21097 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21098 let (workspace, cx) =
21099 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21100 let worktree_id = workspace.update(cx, |workspace, cx| {
21101 workspace.project().update(cx, |project, cx| {
21102 project.worktrees(cx).next().unwrap().read(cx).id()
21103 })
21104 });
21105
21106 let expected_ranges = vec![
21107 Point::new(0, 0)..Point::new(0, 0),
21108 Point::new(1, 0)..Point::new(1, 1),
21109 Point::new(2, 0)..Point::new(2, 2),
21110 Point::new(3, 0)..Point::new(3, 3),
21111 ];
21112
21113 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21114 let editor_1 = workspace
21115 .update_in(cx, |workspace, window, cx| {
21116 workspace.open_path(
21117 (worktree_id, "main.rs"),
21118 Some(pane_1.downgrade()),
21119 true,
21120 window,
21121 cx,
21122 )
21123 })
21124 .unwrap()
21125 .await
21126 .downcast::<Editor>()
21127 .unwrap();
21128 pane_1.update(cx, |pane, cx| {
21129 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21130 open_editor.update(cx, |editor, cx| {
21131 assert_eq!(
21132 editor.display_text(cx),
21133 main_text,
21134 "Original main.rs text on initial open",
21135 );
21136 assert_eq!(
21137 editor
21138 .selections
21139 .all::<Point>(cx)
21140 .into_iter()
21141 .map(|s| s.range())
21142 .collect::<Vec<_>>(),
21143 vec![Point::zero()..Point::zero()],
21144 "Default selections on initial open",
21145 );
21146 })
21147 });
21148 editor_1.update_in(cx, |editor, window, cx| {
21149 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21150 s.select_ranges(expected_ranges.clone());
21151 });
21152 });
21153
21154 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
21155 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
21156 });
21157 let editor_2 = workspace
21158 .update_in(cx, |workspace, window, cx| {
21159 workspace.open_path(
21160 (worktree_id, "main.rs"),
21161 Some(pane_2.downgrade()),
21162 true,
21163 window,
21164 cx,
21165 )
21166 })
21167 .unwrap()
21168 .await
21169 .downcast::<Editor>()
21170 .unwrap();
21171 pane_2.update(cx, |pane, cx| {
21172 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21173 open_editor.update(cx, |editor, cx| {
21174 assert_eq!(
21175 editor.display_text(cx),
21176 main_text,
21177 "Original main.rs text on initial open in another panel",
21178 );
21179 assert_eq!(
21180 editor
21181 .selections
21182 .all::<Point>(cx)
21183 .into_iter()
21184 .map(|s| s.range())
21185 .collect::<Vec<_>>(),
21186 vec![Point::zero()..Point::zero()],
21187 "Default selections on initial open in another panel",
21188 );
21189 })
21190 });
21191
21192 editor_2.update_in(cx, |editor, window, cx| {
21193 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
21194 });
21195
21196 let _other_editor_1 = workspace
21197 .update_in(cx, |workspace, window, cx| {
21198 workspace.open_path(
21199 (worktree_id, "lib.rs"),
21200 Some(pane_1.downgrade()),
21201 true,
21202 window,
21203 cx,
21204 )
21205 })
21206 .unwrap()
21207 .await
21208 .downcast::<Editor>()
21209 .unwrap();
21210 pane_1
21211 .update_in(cx, |pane, window, cx| {
21212 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
21213 })
21214 .await
21215 .unwrap();
21216 drop(editor_1);
21217 pane_1.update(cx, |pane, cx| {
21218 pane.active_item()
21219 .unwrap()
21220 .downcast::<Editor>()
21221 .unwrap()
21222 .update(cx, |editor, cx| {
21223 assert_eq!(
21224 editor.display_text(cx),
21225 lib_text,
21226 "Other file should be open and active",
21227 );
21228 });
21229 assert_eq!(pane.items().count(), 1, "No other editors should be open");
21230 });
21231
21232 let _other_editor_2 = workspace
21233 .update_in(cx, |workspace, window, cx| {
21234 workspace.open_path(
21235 (worktree_id, "lib.rs"),
21236 Some(pane_2.downgrade()),
21237 true,
21238 window,
21239 cx,
21240 )
21241 })
21242 .unwrap()
21243 .await
21244 .downcast::<Editor>()
21245 .unwrap();
21246 pane_2
21247 .update_in(cx, |pane, window, cx| {
21248 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
21249 })
21250 .await
21251 .unwrap();
21252 drop(editor_2);
21253 pane_2.update(cx, |pane, cx| {
21254 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21255 open_editor.update(cx, |editor, cx| {
21256 assert_eq!(
21257 editor.display_text(cx),
21258 lib_text,
21259 "Other file should be open and active in another panel too",
21260 );
21261 });
21262 assert_eq!(
21263 pane.items().count(),
21264 1,
21265 "No other editors should be open in another pane",
21266 );
21267 });
21268
21269 let _editor_1_reopened = workspace
21270 .update_in(cx, |workspace, window, cx| {
21271 workspace.open_path(
21272 (worktree_id, "main.rs"),
21273 Some(pane_1.downgrade()),
21274 true,
21275 window,
21276 cx,
21277 )
21278 })
21279 .unwrap()
21280 .await
21281 .downcast::<Editor>()
21282 .unwrap();
21283 let _editor_2_reopened = workspace
21284 .update_in(cx, |workspace, window, cx| {
21285 workspace.open_path(
21286 (worktree_id, "main.rs"),
21287 Some(pane_2.downgrade()),
21288 true,
21289 window,
21290 cx,
21291 )
21292 })
21293 .unwrap()
21294 .await
21295 .downcast::<Editor>()
21296 .unwrap();
21297 pane_1.update(cx, |pane, cx| {
21298 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21299 open_editor.update(cx, |editor, cx| {
21300 assert_eq!(
21301 editor.display_text(cx),
21302 main_text,
21303 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
21304 );
21305 assert_eq!(
21306 editor
21307 .selections
21308 .all::<Point>(cx)
21309 .into_iter()
21310 .map(|s| s.range())
21311 .collect::<Vec<_>>(),
21312 expected_ranges,
21313 "Previous editor in the 1st panel had selections and should get them restored on reopen",
21314 );
21315 })
21316 });
21317 pane_2.update(cx, |pane, cx| {
21318 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21319 open_editor.update(cx, |editor, cx| {
21320 assert_eq!(
21321 editor.display_text(cx),
21322 r#"fn main() {
21323⋯rintln!("1");
21324⋯intln!("2");
21325⋯ntln!("3");
21326println!("4");
21327println!("5");
21328}"#,
21329 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
21330 );
21331 assert_eq!(
21332 editor
21333 .selections
21334 .all::<Point>(cx)
21335 .into_iter()
21336 .map(|s| s.range())
21337 .collect::<Vec<_>>(),
21338 vec![Point::zero()..Point::zero()],
21339 "Previous editor in the 2nd pane had no selections changed hence should restore none",
21340 );
21341 })
21342 });
21343}
21344
21345#[gpui::test]
21346async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
21347 init_test(cx, |_| {});
21348
21349 let fs = FakeFs::new(cx.executor());
21350 let main_text = r#"fn main() {
21351println!("1");
21352println!("2");
21353println!("3");
21354println!("4");
21355println!("5");
21356}"#;
21357 let lib_text = "mod foo {}";
21358 fs.insert_tree(
21359 path!("/a"),
21360 json!({
21361 "lib.rs": lib_text,
21362 "main.rs": main_text,
21363 }),
21364 )
21365 .await;
21366
21367 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21368 let (workspace, cx) =
21369 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21370 let worktree_id = workspace.update(cx, |workspace, cx| {
21371 workspace.project().update(cx, |project, cx| {
21372 project.worktrees(cx).next().unwrap().read(cx).id()
21373 })
21374 });
21375
21376 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21377 let editor = workspace
21378 .update_in(cx, |workspace, window, cx| {
21379 workspace.open_path(
21380 (worktree_id, "main.rs"),
21381 Some(pane.downgrade()),
21382 true,
21383 window,
21384 cx,
21385 )
21386 })
21387 .unwrap()
21388 .await
21389 .downcast::<Editor>()
21390 .unwrap();
21391 pane.update(cx, |pane, cx| {
21392 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21393 open_editor.update(cx, |editor, cx| {
21394 assert_eq!(
21395 editor.display_text(cx),
21396 main_text,
21397 "Original main.rs text on initial open",
21398 );
21399 })
21400 });
21401 editor.update_in(cx, |editor, window, cx| {
21402 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
21403 });
21404
21405 cx.update_global(|store: &mut SettingsStore, cx| {
21406 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21407 s.restore_on_file_reopen = Some(false);
21408 });
21409 });
21410 editor.update_in(cx, |editor, window, cx| {
21411 editor.fold_ranges(
21412 vec![
21413 Point::new(1, 0)..Point::new(1, 1),
21414 Point::new(2, 0)..Point::new(2, 2),
21415 Point::new(3, 0)..Point::new(3, 3),
21416 ],
21417 false,
21418 window,
21419 cx,
21420 );
21421 });
21422 pane.update_in(cx, |pane, window, cx| {
21423 pane.close_all_items(&CloseAllItems::default(), window, cx)
21424 })
21425 .await
21426 .unwrap();
21427 pane.update(cx, |pane, _| {
21428 assert!(pane.active_item().is_none());
21429 });
21430 cx.update_global(|store: &mut SettingsStore, cx| {
21431 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21432 s.restore_on_file_reopen = Some(true);
21433 });
21434 });
21435
21436 let _editor_reopened = workspace
21437 .update_in(cx, |workspace, window, cx| {
21438 workspace.open_path(
21439 (worktree_id, "main.rs"),
21440 Some(pane.downgrade()),
21441 true,
21442 window,
21443 cx,
21444 )
21445 })
21446 .unwrap()
21447 .await
21448 .downcast::<Editor>()
21449 .unwrap();
21450 pane.update(cx, |pane, cx| {
21451 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21452 open_editor.update(cx, |editor, cx| {
21453 assert_eq!(
21454 editor.display_text(cx),
21455 main_text,
21456 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
21457 );
21458 })
21459 });
21460}
21461
21462#[gpui::test]
21463async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
21464 struct EmptyModalView {
21465 focus_handle: gpui::FocusHandle,
21466 }
21467 impl EventEmitter<DismissEvent> for EmptyModalView {}
21468 impl Render for EmptyModalView {
21469 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
21470 div()
21471 }
21472 }
21473 impl Focusable for EmptyModalView {
21474 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
21475 self.focus_handle.clone()
21476 }
21477 }
21478 impl workspace::ModalView for EmptyModalView {}
21479 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
21480 EmptyModalView {
21481 focus_handle: cx.focus_handle(),
21482 }
21483 }
21484
21485 init_test(cx, |_| {});
21486
21487 let fs = FakeFs::new(cx.executor());
21488 let project = Project::test(fs, [], cx).await;
21489 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21490 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
21491 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21492 let editor = cx.new_window_entity(|window, cx| {
21493 Editor::new(
21494 EditorMode::full(),
21495 buffer,
21496 Some(project.clone()),
21497 window,
21498 cx,
21499 )
21500 });
21501 workspace
21502 .update(cx, |workspace, window, cx| {
21503 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
21504 })
21505 .unwrap();
21506 editor.update_in(cx, |editor, window, cx| {
21507 editor.open_context_menu(&OpenContextMenu, window, cx);
21508 assert!(editor.mouse_context_menu.is_some());
21509 });
21510 workspace
21511 .update(cx, |workspace, window, cx| {
21512 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
21513 })
21514 .unwrap();
21515 cx.read(|cx| {
21516 assert!(editor.read(cx).mouse_context_menu.is_none());
21517 });
21518}
21519
21520#[gpui::test]
21521async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
21522 init_test(cx, |_| {});
21523
21524 let fs = FakeFs::new(cx.executor());
21525 fs.insert_file(path!("/file.html"), Default::default())
21526 .await;
21527
21528 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21529
21530 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21531 let html_language = Arc::new(Language::new(
21532 LanguageConfig {
21533 name: "HTML".into(),
21534 matcher: LanguageMatcher {
21535 path_suffixes: vec!["html".to_string()],
21536 ..LanguageMatcher::default()
21537 },
21538 brackets: BracketPairConfig {
21539 pairs: vec![BracketPair {
21540 start: "<".into(),
21541 end: ">".into(),
21542 close: true,
21543 ..Default::default()
21544 }],
21545 ..Default::default()
21546 },
21547 ..Default::default()
21548 },
21549 Some(tree_sitter_html::LANGUAGE.into()),
21550 ));
21551 language_registry.add(html_language);
21552 let mut fake_servers = language_registry.register_fake_lsp(
21553 "HTML",
21554 FakeLspAdapter {
21555 capabilities: lsp::ServerCapabilities {
21556 completion_provider: Some(lsp::CompletionOptions {
21557 resolve_provider: Some(true),
21558 ..Default::default()
21559 }),
21560 ..Default::default()
21561 },
21562 ..Default::default()
21563 },
21564 );
21565
21566 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21567 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21568
21569 let worktree_id = workspace
21570 .update(cx, |workspace, _window, cx| {
21571 workspace.project().update(cx, |project, cx| {
21572 project.worktrees(cx).next().unwrap().read(cx).id()
21573 })
21574 })
21575 .unwrap();
21576 project
21577 .update(cx, |project, cx| {
21578 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
21579 })
21580 .await
21581 .unwrap();
21582 let editor = workspace
21583 .update(cx, |workspace, window, cx| {
21584 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
21585 })
21586 .unwrap()
21587 .await
21588 .unwrap()
21589 .downcast::<Editor>()
21590 .unwrap();
21591
21592 let fake_server = fake_servers.next().await.unwrap();
21593 editor.update_in(cx, |editor, window, cx| {
21594 editor.set_text("<ad></ad>", window, cx);
21595 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21596 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
21597 });
21598 let Some((buffer, _)) = editor
21599 .buffer
21600 .read(cx)
21601 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
21602 else {
21603 panic!("Failed to get buffer for selection position");
21604 };
21605 let buffer = buffer.read(cx);
21606 let buffer_id = buffer.remote_id();
21607 let opening_range =
21608 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
21609 let closing_range =
21610 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
21611 let mut linked_ranges = HashMap::default();
21612 linked_ranges.insert(
21613 buffer_id,
21614 vec![(opening_range.clone(), vec![closing_range.clone()])],
21615 );
21616 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
21617 });
21618 let mut completion_handle =
21619 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
21620 Ok(Some(lsp::CompletionResponse::Array(vec![
21621 lsp::CompletionItem {
21622 label: "head".to_string(),
21623 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21624 lsp::InsertReplaceEdit {
21625 new_text: "head".to_string(),
21626 insert: lsp::Range::new(
21627 lsp::Position::new(0, 1),
21628 lsp::Position::new(0, 3),
21629 ),
21630 replace: lsp::Range::new(
21631 lsp::Position::new(0, 1),
21632 lsp::Position::new(0, 3),
21633 ),
21634 },
21635 )),
21636 ..Default::default()
21637 },
21638 ])))
21639 });
21640 editor.update_in(cx, |editor, window, cx| {
21641 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
21642 });
21643 cx.run_until_parked();
21644 completion_handle.next().await.unwrap();
21645 editor.update(cx, |editor, _| {
21646 assert!(
21647 editor.context_menu_visible(),
21648 "Completion menu should be visible"
21649 );
21650 });
21651 editor.update_in(cx, |editor, window, cx| {
21652 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
21653 });
21654 cx.executor().run_until_parked();
21655 editor.update(cx, |editor, cx| {
21656 assert_eq!(editor.text(cx), "<head></head>");
21657 });
21658}
21659
21660#[gpui::test]
21661async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
21662 init_test(cx, |_| {});
21663
21664 let fs = FakeFs::new(cx.executor());
21665 fs.insert_tree(
21666 path!("/root"),
21667 json!({
21668 "a": {
21669 "main.rs": "fn main() {}",
21670 },
21671 "foo": {
21672 "bar": {
21673 "external_file.rs": "pub mod external {}",
21674 }
21675 }
21676 }),
21677 )
21678 .await;
21679
21680 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
21681 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21682 language_registry.add(rust_lang());
21683 let _fake_servers = language_registry.register_fake_lsp(
21684 "Rust",
21685 FakeLspAdapter {
21686 ..FakeLspAdapter::default()
21687 },
21688 );
21689 let (workspace, cx) =
21690 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21691 let worktree_id = workspace.update(cx, |workspace, cx| {
21692 workspace.project().update(cx, |project, cx| {
21693 project.worktrees(cx).next().unwrap().read(cx).id()
21694 })
21695 });
21696
21697 let assert_language_servers_count =
21698 |expected: usize, context: &str, cx: &mut VisualTestContext| {
21699 project.update(cx, |project, cx| {
21700 let current = project
21701 .lsp_store()
21702 .read(cx)
21703 .as_local()
21704 .unwrap()
21705 .language_servers
21706 .len();
21707 assert_eq!(expected, current, "{context}");
21708 });
21709 };
21710
21711 assert_language_servers_count(
21712 0,
21713 "No servers should be running before any file is open",
21714 cx,
21715 );
21716 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21717 let main_editor = workspace
21718 .update_in(cx, |workspace, window, cx| {
21719 workspace.open_path(
21720 (worktree_id, "main.rs"),
21721 Some(pane.downgrade()),
21722 true,
21723 window,
21724 cx,
21725 )
21726 })
21727 .unwrap()
21728 .await
21729 .downcast::<Editor>()
21730 .unwrap();
21731 pane.update(cx, |pane, cx| {
21732 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21733 open_editor.update(cx, |editor, cx| {
21734 assert_eq!(
21735 editor.display_text(cx),
21736 "fn main() {}",
21737 "Original main.rs text on initial open",
21738 );
21739 });
21740 assert_eq!(open_editor, main_editor);
21741 });
21742 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
21743
21744 let external_editor = workspace
21745 .update_in(cx, |workspace, window, cx| {
21746 workspace.open_abs_path(
21747 PathBuf::from("/root/foo/bar/external_file.rs"),
21748 OpenOptions::default(),
21749 window,
21750 cx,
21751 )
21752 })
21753 .await
21754 .expect("opening external file")
21755 .downcast::<Editor>()
21756 .expect("downcasted external file's open element to editor");
21757 pane.update(cx, |pane, cx| {
21758 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21759 open_editor.update(cx, |editor, cx| {
21760 assert_eq!(
21761 editor.display_text(cx),
21762 "pub mod external {}",
21763 "External file is open now",
21764 );
21765 });
21766 assert_eq!(open_editor, external_editor);
21767 });
21768 assert_language_servers_count(
21769 1,
21770 "Second, external, *.rs file should join the existing server",
21771 cx,
21772 );
21773
21774 pane.update_in(cx, |pane, window, cx| {
21775 pane.close_active_item(&CloseActiveItem::default(), window, cx)
21776 })
21777 .await
21778 .unwrap();
21779 pane.update_in(cx, |pane, window, cx| {
21780 pane.navigate_backward(window, cx);
21781 });
21782 cx.run_until_parked();
21783 pane.update(cx, |pane, cx| {
21784 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21785 open_editor.update(cx, |editor, cx| {
21786 assert_eq!(
21787 editor.display_text(cx),
21788 "pub mod external {}",
21789 "External file is open now",
21790 );
21791 });
21792 });
21793 assert_language_servers_count(
21794 1,
21795 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
21796 cx,
21797 );
21798
21799 cx.update(|_, cx| {
21800 workspace::reload(&workspace::Reload::default(), cx);
21801 });
21802 assert_language_servers_count(
21803 1,
21804 "After reloading the worktree with local and external files opened, only one project should be started",
21805 cx,
21806 );
21807}
21808
21809#[gpui::test]
21810async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
21811 init_test(cx, |_| {});
21812
21813 let mut cx = EditorTestContext::new(cx).await;
21814 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21815 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21816
21817 // test cursor move to start of each line on tab
21818 // for `if`, `elif`, `else`, `while`, `with` and `for`
21819 cx.set_state(indoc! {"
21820 def main():
21821 ˇ for item in items:
21822 ˇ while item.active:
21823 ˇ if item.value > 10:
21824 ˇ continue
21825 ˇ elif item.value < 0:
21826 ˇ break
21827 ˇ else:
21828 ˇ with item.context() as ctx:
21829 ˇ yield count
21830 ˇ else:
21831 ˇ log('while else')
21832 ˇ else:
21833 ˇ log('for else')
21834 "});
21835 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21836 cx.assert_editor_state(indoc! {"
21837 def main():
21838 ˇfor item in items:
21839 ˇwhile item.active:
21840 ˇif item.value > 10:
21841 ˇcontinue
21842 ˇelif item.value < 0:
21843 ˇbreak
21844 ˇelse:
21845 ˇwith item.context() as ctx:
21846 ˇyield count
21847 ˇelse:
21848 ˇlog('while else')
21849 ˇelse:
21850 ˇlog('for else')
21851 "});
21852 // test relative indent is preserved when tab
21853 // for `if`, `elif`, `else`, `while`, `with` and `for`
21854 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21855 cx.assert_editor_state(indoc! {"
21856 def main():
21857 ˇfor item in items:
21858 ˇwhile item.active:
21859 ˇif item.value > 10:
21860 ˇcontinue
21861 ˇelif item.value < 0:
21862 ˇbreak
21863 ˇelse:
21864 ˇwith item.context() as ctx:
21865 ˇyield count
21866 ˇelse:
21867 ˇlog('while else')
21868 ˇelse:
21869 ˇlog('for else')
21870 "});
21871
21872 // test cursor move to start of each line on tab
21873 // for `try`, `except`, `else`, `finally`, `match` and `def`
21874 cx.set_state(indoc! {"
21875 def main():
21876 ˇ try:
21877 ˇ fetch()
21878 ˇ except ValueError:
21879 ˇ handle_error()
21880 ˇ else:
21881 ˇ match value:
21882 ˇ case _:
21883 ˇ finally:
21884 ˇ def status():
21885 ˇ return 0
21886 "});
21887 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21888 cx.assert_editor_state(indoc! {"
21889 def main():
21890 ˇtry:
21891 ˇfetch()
21892 ˇexcept ValueError:
21893 ˇhandle_error()
21894 ˇelse:
21895 ˇmatch value:
21896 ˇcase _:
21897 ˇfinally:
21898 ˇdef status():
21899 ˇreturn 0
21900 "});
21901 // test relative indent is preserved when tab
21902 // for `try`, `except`, `else`, `finally`, `match` and `def`
21903 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21904 cx.assert_editor_state(indoc! {"
21905 def main():
21906 ˇtry:
21907 ˇfetch()
21908 ˇexcept ValueError:
21909 ˇhandle_error()
21910 ˇelse:
21911 ˇmatch value:
21912 ˇcase _:
21913 ˇfinally:
21914 ˇdef status():
21915 ˇreturn 0
21916 "});
21917}
21918
21919#[gpui::test]
21920async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
21921 init_test(cx, |_| {});
21922
21923 let mut cx = EditorTestContext::new(cx).await;
21924 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21925 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21926
21927 // test `else` auto outdents when typed inside `if` block
21928 cx.set_state(indoc! {"
21929 def main():
21930 if i == 2:
21931 return
21932 ˇ
21933 "});
21934 cx.update_editor(|editor, window, cx| {
21935 editor.handle_input("else:", window, cx);
21936 });
21937 cx.assert_editor_state(indoc! {"
21938 def main():
21939 if i == 2:
21940 return
21941 else:ˇ
21942 "});
21943
21944 // test `except` auto outdents when typed inside `try` block
21945 cx.set_state(indoc! {"
21946 def main():
21947 try:
21948 i = 2
21949 ˇ
21950 "});
21951 cx.update_editor(|editor, window, cx| {
21952 editor.handle_input("except:", window, cx);
21953 });
21954 cx.assert_editor_state(indoc! {"
21955 def main():
21956 try:
21957 i = 2
21958 except:ˇ
21959 "});
21960
21961 // test `else` auto outdents when typed inside `except` block
21962 cx.set_state(indoc! {"
21963 def main():
21964 try:
21965 i = 2
21966 except:
21967 j = 2
21968 ˇ
21969 "});
21970 cx.update_editor(|editor, window, cx| {
21971 editor.handle_input("else:", window, cx);
21972 });
21973 cx.assert_editor_state(indoc! {"
21974 def main():
21975 try:
21976 i = 2
21977 except:
21978 j = 2
21979 else:ˇ
21980 "});
21981
21982 // test `finally` auto outdents when typed inside `else` block
21983 cx.set_state(indoc! {"
21984 def main():
21985 try:
21986 i = 2
21987 except:
21988 j = 2
21989 else:
21990 k = 2
21991 ˇ
21992 "});
21993 cx.update_editor(|editor, window, cx| {
21994 editor.handle_input("finally:", window, cx);
21995 });
21996 cx.assert_editor_state(indoc! {"
21997 def main():
21998 try:
21999 i = 2
22000 except:
22001 j = 2
22002 else:
22003 k = 2
22004 finally:ˇ
22005 "});
22006
22007 // test `else` does not outdents when typed inside `except` block right after for block
22008 cx.set_state(indoc! {"
22009 def main():
22010 try:
22011 i = 2
22012 except:
22013 for i in range(n):
22014 pass
22015 ˇ
22016 "});
22017 cx.update_editor(|editor, window, cx| {
22018 editor.handle_input("else:", window, cx);
22019 });
22020 cx.assert_editor_state(indoc! {"
22021 def main():
22022 try:
22023 i = 2
22024 except:
22025 for i in range(n):
22026 pass
22027 else:ˇ
22028 "});
22029
22030 // test `finally` auto outdents when typed inside `else` block right after for block
22031 cx.set_state(indoc! {"
22032 def main():
22033 try:
22034 i = 2
22035 except:
22036 j = 2
22037 else:
22038 for i in range(n):
22039 pass
22040 ˇ
22041 "});
22042 cx.update_editor(|editor, window, cx| {
22043 editor.handle_input("finally:", window, cx);
22044 });
22045 cx.assert_editor_state(indoc! {"
22046 def main():
22047 try:
22048 i = 2
22049 except:
22050 j = 2
22051 else:
22052 for i in range(n):
22053 pass
22054 finally:ˇ
22055 "});
22056
22057 // test `except` outdents to inner "try" block
22058 cx.set_state(indoc! {"
22059 def main():
22060 try:
22061 i = 2
22062 if i == 2:
22063 try:
22064 i = 3
22065 ˇ
22066 "});
22067 cx.update_editor(|editor, window, cx| {
22068 editor.handle_input("except:", window, cx);
22069 });
22070 cx.assert_editor_state(indoc! {"
22071 def main():
22072 try:
22073 i = 2
22074 if i == 2:
22075 try:
22076 i = 3
22077 except:ˇ
22078 "});
22079
22080 // test `except` outdents to outer "try" block
22081 cx.set_state(indoc! {"
22082 def main():
22083 try:
22084 i = 2
22085 if i == 2:
22086 try:
22087 i = 3
22088 ˇ
22089 "});
22090 cx.update_editor(|editor, window, cx| {
22091 editor.handle_input("except:", window, cx);
22092 });
22093 cx.assert_editor_state(indoc! {"
22094 def main():
22095 try:
22096 i = 2
22097 if i == 2:
22098 try:
22099 i = 3
22100 except:ˇ
22101 "});
22102
22103 // test `else` stays at correct indent when typed after `for` block
22104 cx.set_state(indoc! {"
22105 def main():
22106 for i in range(10):
22107 if i == 3:
22108 break
22109 ˇ
22110 "});
22111 cx.update_editor(|editor, window, cx| {
22112 editor.handle_input("else:", window, cx);
22113 });
22114 cx.assert_editor_state(indoc! {"
22115 def main():
22116 for i in range(10):
22117 if i == 3:
22118 break
22119 else:ˇ
22120 "});
22121
22122 // test does not outdent on typing after line with square brackets
22123 cx.set_state(indoc! {"
22124 def f() -> list[str]:
22125 ˇ
22126 "});
22127 cx.update_editor(|editor, window, cx| {
22128 editor.handle_input("a", window, cx);
22129 });
22130 cx.assert_editor_state(indoc! {"
22131 def f() -> list[str]:
22132 aˇ
22133 "});
22134}
22135
22136#[gpui::test]
22137async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
22138 init_test(cx, |_| {});
22139 update_test_language_settings(cx, |settings| {
22140 settings.defaults.extend_comment_on_newline = Some(false);
22141 });
22142 let mut cx = EditorTestContext::new(cx).await;
22143 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22144 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22145
22146 // test correct indent after newline on comment
22147 cx.set_state(indoc! {"
22148 # COMMENT:ˇ
22149 "});
22150 cx.update_editor(|editor, window, cx| {
22151 editor.newline(&Newline, window, cx);
22152 });
22153 cx.assert_editor_state(indoc! {"
22154 # COMMENT:
22155 ˇ
22156 "});
22157
22158 // test correct indent after newline in brackets
22159 cx.set_state(indoc! {"
22160 {ˇ}
22161 "});
22162 cx.update_editor(|editor, window, cx| {
22163 editor.newline(&Newline, window, cx);
22164 });
22165 cx.run_until_parked();
22166 cx.assert_editor_state(indoc! {"
22167 {
22168 ˇ
22169 }
22170 "});
22171
22172 cx.set_state(indoc! {"
22173 (ˇ)
22174 "});
22175 cx.update_editor(|editor, window, cx| {
22176 editor.newline(&Newline, window, cx);
22177 });
22178 cx.run_until_parked();
22179 cx.assert_editor_state(indoc! {"
22180 (
22181 ˇ
22182 )
22183 "});
22184
22185 // do not indent after empty lists or dictionaries
22186 cx.set_state(indoc! {"
22187 a = []ˇ
22188 "});
22189 cx.update_editor(|editor, window, cx| {
22190 editor.newline(&Newline, window, cx);
22191 });
22192 cx.run_until_parked();
22193 cx.assert_editor_state(indoc! {"
22194 a = []
22195 ˇ
22196 "});
22197}
22198
22199fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
22200 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
22201 point..point
22202}
22203
22204#[track_caller]
22205fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
22206 let (text, ranges) = marked_text_ranges(marked_text, true);
22207 assert_eq!(editor.text(cx), text);
22208 assert_eq!(
22209 editor.selections.ranges(cx),
22210 ranges,
22211 "Assert selections are {}",
22212 marked_text
22213 );
22214}
22215
22216pub fn handle_signature_help_request(
22217 cx: &mut EditorLspTestContext,
22218 mocked_response: lsp::SignatureHelp,
22219) -> impl Future<Output = ()> + use<> {
22220 let mut request =
22221 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
22222 let mocked_response = mocked_response.clone();
22223 async move { Ok(Some(mocked_response)) }
22224 });
22225
22226 async move {
22227 request.next().await;
22228 }
22229}
22230
22231#[track_caller]
22232pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
22233 cx.update_editor(|editor, _, _| {
22234 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
22235 let entries = menu.entries.borrow();
22236 let entries = entries
22237 .iter()
22238 .map(|entry| entry.string.as_str())
22239 .collect::<Vec<_>>();
22240 assert_eq!(entries, expected);
22241 } else {
22242 panic!("Expected completions menu");
22243 }
22244 });
22245}
22246
22247/// Handle completion request passing a marked string specifying where the completion
22248/// should be triggered from using '|' character, what range should be replaced, and what completions
22249/// should be returned using '<' and '>' to delimit the range.
22250///
22251/// Also see `handle_completion_request_with_insert_and_replace`.
22252#[track_caller]
22253pub fn handle_completion_request(
22254 marked_string: &str,
22255 completions: Vec<&'static str>,
22256 is_incomplete: bool,
22257 counter: Arc<AtomicUsize>,
22258 cx: &mut EditorLspTestContext,
22259) -> impl Future<Output = ()> {
22260 let complete_from_marker: TextRangeMarker = '|'.into();
22261 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22262 let (_, mut marked_ranges) = marked_text_ranges_by(
22263 marked_string,
22264 vec![complete_from_marker.clone(), replace_range_marker.clone()],
22265 );
22266
22267 let complete_from_position =
22268 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22269 let replace_range =
22270 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22271
22272 let mut request =
22273 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22274 let completions = completions.clone();
22275 counter.fetch_add(1, atomic::Ordering::Release);
22276 async move {
22277 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22278 assert_eq!(
22279 params.text_document_position.position,
22280 complete_from_position
22281 );
22282 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
22283 is_incomplete: is_incomplete,
22284 item_defaults: None,
22285 items: completions
22286 .iter()
22287 .map(|completion_text| lsp::CompletionItem {
22288 label: completion_text.to_string(),
22289 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
22290 range: replace_range,
22291 new_text: completion_text.to_string(),
22292 })),
22293 ..Default::default()
22294 })
22295 .collect(),
22296 })))
22297 }
22298 });
22299
22300 async move {
22301 request.next().await;
22302 }
22303}
22304
22305/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
22306/// given instead, which also contains an `insert` range.
22307///
22308/// This function uses markers to define ranges:
22309/// - `|` marks the cursor position
22310/// - `<>` marks the replace range
22311/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
22312pub fn handle_completion_request_with_insert_and_replace(
22313 cx: &mut EditorLspTestContext,
22314 marked_string: &str,
22315 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
22316 counter: Arc<AtomicUsize>,
22317) -> impl Future<Output = ()> {
22318 let complete_from_marker: TextRangeMarker = '|'.into();
22319 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22320 let insert_range_marker: TextRangeMarker = ('{', '}').into();
22321
22322 let (_, mut marked_ranges) = marked_text_ranges_by(
22323 marked_string,
22324 vec![
22325 complete_from_marker.clone(),
22326 replace_range_marker.clone(),
22327 insert_range_marker.clone(),
22328 ],
22329 );
22330
22331 let complete_from_position =
22332 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22333 let replace_range =
22334 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22335
22336 let insert_range = match marked_ranges.remove(&insert_range_marker) {
22337 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
22338 _ => lsp::Range {
22339 start: replace_range.start,
22340 end: complete_from_position,
22341 },
22342 };
22343
22344 let mut request =
22345 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22346 let completions = completions.clone();
22347 counter.fetch_add(1, atomic::Ordering::Release);
22348 async move {
22349 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22350 assert_eq!(
22351 params.text_document_position.position, complete_from_position,
22352 "marker `|` position doesn't match",
22353 );
22354 Ok(Some(lsp::CompletionResponse::Array(
22355 completions
22356 .iter()
22357 .map(|(label, new_text)| lsp::CompletionItem {
22358 label: label.to_string(),
22359 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22360 lsp::InsertReplaceEdit {
22361 insert: insert_range,
22362 replace: replace_range,
22363 new_text: new_text.to_string(),
22364 },
22365 )),
22366 ..Default::default()
22367 })
22368 .collect(),
22369 )))
22370 }
22371 });
22372
22373 async move {
22374 request.next().await;
22375 }
22376}
22377
22378fn handle_resolve_completion_request(
22379 cx: &mut EditorLspTestContext,
22380 edits: Option<Vec<(&'static str, &'static str)>>,
22381) -> impl Future<Output = ()> {
22382 let edits = edits.map(|edits| {
22383 edits
22384 .iter()
22385 .map(|(marked_string, new_text)| {
22386 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
22387 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
22388 lsp::TextEdit::new(replace_range, new_text.to_string())
22389 })
22390 .collect::<Vec<_>>()
22391 });
22392
22393 let mut request =
22394 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
22395 let edits = edits.clone();
22396 async move {
22397 Ok(lsp::CompletionItem {
22398 additional_text_edits: edits,
22399 ..Default::default()
22400 })
22401 }
22402 });
22403
22404 async move {
22405 request.next().await;
22406 }
22407}
22408
22409pub(crate) fn update_test_language_settings(
22410 cx: &mut TestAppContext,
22411 f: impl Fn(&mut AllLanguageSettingsContent),
22412) {
22413 cx.update(|cx| {
22414 SettingsStore::update_global(cx, |store, cx| {
22415 store.update_user_settings::<AllLanguageSettings>(cx, f);
22416 });
22417 });
22418}
22419
22420pub(crate) fn update_test_project_settings(
22421 cx: &mut TestAppContext,
22422 f: impl Fn(&mut ProjectSettings),
22423) {
22424 cx.update(|cx| {
22425 SettingsStore::update_global(cx, |store, cx| {
22426 store.update_user_settings::<ProjectSettings>(cx, f);
22427 });
22428 });
22429}
22430
22431pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
22432 cx.update(|cx| {
22433 assets::Assets.load_test_fonts(cx);
22434 let store = SettingsStore::test(cx);
22435 cx.set_global(store);
22436 theme::init(theme::LoadThemes::JustBase, cx);
22437 release_channel::init(SemanticVersion::default(), cx);
22438 client::init_settings(cx);
22439 language::init(cx);
22440 Project::init_settings(cx);
22441 workspace::init_settings(cx);
22442 crate::init(cx);
22443 });
22444
22445 update_test_language_settings(cx, f);
22446}
22447
22448#[track_caller]
22449fn assert_hunk_revert(
22450 not_reverted_text_with_selections: &str,
22451 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
22452 expected_reverted_text_with_selections: &str,
22453 base_text: &str,
22454 cx: &mut EditorLspTestContext,
22455) {
22456 cx.set_state(not_reverted_text_with_selections);
22457 cx.set_head_text(base_text);
22458 cx.executor().run_until_parked();
22459
22460 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
22461 let snapshot = editor.snapshot(window, cx);
22462 let reverted_hunk_statuses = snapshot
22463 .buffer_snapshot
22464 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
22465 .map(|hunk| hunk.status().kind)
22466 .collect::<Vec<_>>();
22467
22468 editor.git_restore(&Default::default(), window, cx);
22469 reverted_hunk_statuses
22470 });
22471 cx.executor().run_until_parked();
22472 cx.assert_editor_state(expected_reverted_text_with_selections);
22473 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
22474}
22475
22476#[gpui::test(iterations = 10)]
22477async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
22478 init_test(cx, |_| {});
22479
22480 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
22481 let counter = diagnostic_requests.clone();
22482
22483 let fs = FakeFs::new(cx.executor());
22484 fs.insert_tree(
22485 path!("/a"),
22486 json!({
22487 "first.rs": "fn main() { let a = 5; }",
22488 "second.rs": "// Test file",
22489 }),
22490 )
22491 .await;
22492
22493 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22494 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22495 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22496
22497 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22498 language_registry.add(rust_lang());
22499 let mut fake_servers = language_registry.register_fake_lsp(
22500 "Rust",
22501 FakeLspAdapter {
22502 capabilities: lsp::ServerCapabilities {
22503 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
22504 lsp::DiagnosticOptions {
22505 identifier: None,
22506 inter_file_dependencies: true,
22507 workspace_diagnostics: true,
22508 work_done_progress_options: Default::default(),
22509 },
22510 )),
22511 ..Default::default()
22512 },
22513 ..Default::default()
22514 },
22515 );
22516
22517 let editor = workspace
22518 .update(cx, |workspace, window, cx| {
22519 workspace.open_abs_path(
22520 PathBuf::from(path!("/a/first.rs")),
22521 OpenOptions::default(),
22522 window,
22523 cx,
22524 )
22525 })
22526 .unwrap()
22527 .await
22528 .unwrap()
22529 .downcast::<Editor>()
22530 .unwrap();
22531 let fake_server = fake_servers.next().await.unwrap();
22532 let server_id = fake_server.server.server_id();
22533 let mut first_request = fake_server
22534 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
22535 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
22536 let result_id = Some(new_result_id.to_string());
22537 assert_eq!(
22538 params.text_document.uri,
22539 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22540 );
22541 async move {
22542 Ok(lsp::DocumentDiagnosticReportResult::Report(
22543 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
22544 related_documents: None,
22545 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
22546 items: Vec::new(),
22547 result_id,
22548 },
22549 }),
22550 ))
22551 }
22552 });
22553
22554 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
22555 project.update(cx, |project, cx| {
22556 let buffer_id = editor
22557 .read(cx)
22558 .buffer()
22559 .read(cx)
22560 .as_singleton()
22561 .expect("created a singleton buffer")
22562 .read(cx)
22563 .remote_id();
22564 let buffer_result_id = project
22565 .lsp_store()
22566 .read(cx)
22567 .result_id(server_id, buffer_id, cx);
22568 assert_eq!(expected, buffer_result_id);
22569 });
22570 };
22571
22572 ensure_result_id(None, cx);
22573 cx.executor().advance_clock(Duration::from_millis(60));
22574 cx.executor().run_until_parked();
22575 assert_eq!(
22576 diagnostic_requests.load(atomic::Ordering::Acquire),
22577 1,
22578 "Opening file should trigger diagnostic request"
22579 );
22580 first_request
22581 .next()
22582 .await
22583 .expect("should have sent the first diagnostics pull request");
22584 ensure_result_id(Some("1".to_string()), cx);
22585
22586 // Editing should trigger diagnostics
22587 editor.update_in(cx, |editor, window, cx| {
22588 editor.handle_input("2", window, cx)
22589 });
22590 cx.executor().advance_clock(Duration::from_millis(60));
22591 cx.executor().run_until_parked();
22592 assert_eq!(
22593 diagnostic_requests.load(atomic::Ordering::Acquire),
22594 2,
22595 "Editing should trigger diagnostic request"
22596 );
22597 ensure_result_id(Some("2".to_string()), cx);
22598
22599 // Moving cursor should not trigger diagnostic request
22600 editor.update_in(cx, |editor, window, cx| {
22601 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22602 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
22603 });
22604 });
22605 cx.executor().advance_clock(Duration::from_millis(60));
22606 cx.executor().run_until_parked();
22607 assert_eq!(
22608 diagnostic_requests.load(atomic::Ordering::Acquire),
22609 2,
22610 "Cursor movement should not trigger diagnostic request"
22611 );
22612 ensure_result_id(Some("2".to_string()), cx);
22613 // Multiple rapid edits should be debounced
22614 for _ in 0..5 {
22615 editor.update_in(cx, |editor, window, cx| {
22616 editor.handle_input("x", window, cx)
22617 });
22618 }
22619 cx.executor().advance_clock(Duration::from_millis(60));
22620 cx.executor().run_until_parked();
22621
22622 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
22623 assert!(
22624 final_requests <= 4,
22625 "Multiple rapid edits should be debounced (got {final_requests} requests)",
22626 );
22627 ensure_result_id(Some(final_requests.to_string()), cx);
22628}
22629
22630#[gpui::test]
22631async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
22632 // Regression test for issue #11671
22633 // Previously, adding a cursor after moving multiple cursors would reset
22634 // the cursor count instead of adding to the existing cursors.
22635 init_test(cx, |_| {});
22636 let mut cx = EditorTestContext::new(cx).await;
22637
22638 // Create a simple buffer with cursor at start
22639 cx.set_state(indoc! {"
22640 ˇaaaa
22641 bbbb
22642 cccc
22643 dddd
22644 eeee
22645 ffff
22646 gggg
22647 hhhh"});
22648
22649 // Add 2 cursors below (so we have 3 total)
22650 cx.update_editor(|editor, window, cx| {
22651 editor.add_selection_below(&Default::default(), window, cx);
22652 editor.add_selection_below(&Default::default(), window, cx);
22653 });
22654
22655 // Verify we have 3 cursors
22656 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
22657 assert_eq!(
22658 initial_count, 3,
22659 "Should have 3 cursors after adding 2 below"
22660 );
22661
22662 // Move down one line
22663 cx.update_editor(|editor, window, cx| {
22664 editor.move_down(&MoveDown, window, cx);
22665 });
22666
22667 // Add another cursor below
22668 cx.update_editor(|editor, window, cx| {
22669 editor.add_selection_below(&Default::default(), window, cx);
22670 });
22671
22672 // Should now have 4 cursors (3 original + 1 new)
22673 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
22674 assert_eq!(
22675 final_count, 4,
22676 "Should have 4 cursors after moving and adding another"
22677 );
22678}
22679
22680#[gpui::test(iterations = 10)]
22681async fn test_document_colors(cx: &mut TestAppContext) {
22682 let expected_color = Rgba {
22683 r: 0.33,
22684 g: 0.33,
22685 b: 0.33,
22686 a: 0.33,
22687 };
22688
22689 init_test(cx, |_| {});
22690
22691 let fs = FakeFs::new(cx.executor());
22692 fs.insert_tree(
22693 path!("/a"),
22694 json!({
22695 "first.rs": "fn main() { let a = 5; }",
22696 }),
22697 )
22698 .await;
22699
22700 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22701 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22702 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22703
22704 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22705 language_registry.add(rust_lang());
22706 let mut fake_servers = language_registry.register_fake_lsp(
22707 "Rust",
22708 FakeLspAdapter {
22709 capabilities: lsp::ServerCapabilities {
22710 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
22711 ..lsp::ServerCapabilities::default()
22712 },
22713 name: "rust-analyzer",
22714 ..FakeLspAdapter::default()
22715 },
22716 );
22717 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
22718 "Rust",
22719 FakeLspAdapter {
22720 capabilities: lsp::ServerCapabilities {
22721 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
22722 ..lsp::ServerCapabilities::default()
22723 },
22724 name: "not-rust-analyzer",
22725 ..FakeLspAdapter::default()
22726 },
22727 );
22728
22729 let editor = workspace
22730 .update(cx, |workspace, window, cx| {
22731 workspace.open_abs_path(
22732 PathBuf::from(path!("/a/first.rs")),
22733 OpenOptions::default(),
22734 window,
22735 cx,
22736 )
22737 })
22738 .unwrap()
22739 .await
22740 .unwrap()
22741 .downcast::<Editor>()
22742 .unwrap();
22743 let fake_language_server = fake_servers.next().await.unwrap();
22744 let fake_language_server_without_capabilities =
22745 fake_servers_without_capabilities.next().await.unwrap();
22746 let requests_made = Arc::new(AtomicUsize::new(0));
22747 let closure_requests_made = Arc::clone(&requests_made);
22748 let mut color_request_handle = fake_language_server
22749 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
22750 let requests_made = Arc::clone(&closure_requests_made);
22751 async move {
22752 assert_eq!(
22753 params.text_document.uri,
22754 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22755 );
22756 requests_made.fetch_add(1, atomic::Ordering::Release);
22757 Ok(vec![
22758 lsp::ColorInformation {
22759 range: lsp::Range {
22760 start: lsp::Position {
22761 line: 0,
22762 character: 0,
22763 },
22764 end: lsp::Position {
22765 line: 0,
22766 character: 1,
22767 },
22768 },
22769 color: lsp::Color {
22770 red: 0.33,
22771 green: 0.33,
22772 blue: 0.33,
22773 alpha: 0.33,
22774 },
22775 },
22776 lsp::ColorInformation {
22777 range: lsp::Range {
22778 start: lsp::Position {
22779 line: 0,
22780 character: 0,
22781 },
22782 end: lsp::Position {
22783 line: 0,
22784 character: 1,
22785 },
22786 },
22787 color: lsp::Color {
22788 red: 0.33,
22789 green: 0.33,
22790 blue: 0.33,
22791 alpha: 0.33,
22792 },
22793 },
22794 ])
22795 }
22796 });
22797
22798 let _handle = fake_language_server_without_capabilities
22799 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
22800 panic!("Should not be called");
22801 });
22802 cx.executor().advance_clock(Duration::from_millis(100));
22803 color_request_handle.next().await.unwrap();
22804 cx.run_until_parked();
22805 assert_eq!(
22806 1,
22807 requests_made.load(atomic::Ordering::Acquire),
22808 "Should query for colors once per editor open"
22809 );
22810 editor.update_in(cx, |editor, _, cx| {
22811 assert_eq!(
22812 vec![expected_color],
22813 extract_color_inlays(editor, cx),
22814 "Should have an initial inlay"
22815 );
22816 });
22817
22818 // opening another file in a split should not influence the LSP query counter
22819 workspace
22820 .update(cx, |workspace, window, cx| {
22821 assert_eq!(
22822 workspace.panes().len(),
22823 1,
22824 "Should have one pane with one editor"
22825 );
22826 workspace.move_item_to_pane_in_direction(
22827 &MoveItemToPaneInDirection {
22828 direction: SplitDirection::Right,
22829 focus: false,
22830 clone: true,
22831 },
22832 window,
22833 cx,
22834 );
22835 })
22836 .unwrap();
22837 cx.run_until_parked();
22838 workspace
22839 .update(cx, |workspace, _, cx| {
22840 let panes = workspace.panes();
22841 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
22842 for pane in panes {
22843 let editor = pane
22844 .read(cx)
22845 .active_item()
22846 .and_then(|item| item.downcast::<Editor>())
22847 .expect("Should have opened an editor in each split");
22848 let editor_file = editor
22849 .read(cx)
22850 .buffer()
22851 .read(cx)
22852 .as_singleton()
22853 .expect("test deals with singleton buffers")
22854 .read(cx)
22855 .file()
22856 .expect("test buffese should have a file")
22857 .path();
22858 assert_eq!(
22859 editor_file.as_ref(),
22860 Path::new("first.rs"),
22861 "Both editors should be opened for the same file"
22862 )
22863 }
22864 })
22865 .unwrap();
22866
22867 cx.executor().advance_clock(Duration::from_millis(500));
22868 let save = editor.update_in(cx, |editor, window, cx| {
22869 editor.move_to_end(&MoveToEnd, window, cx);
22870 editor.handle_input("dirty", window, cx);
22871 editor.save(
22872 SaveOptions {
22873 format: true,
22874 autosave: true,
22875 },
22876 project.clone(),
22877 window,
22878 cx,
22879 )
22880 });
22881 save.await.unwrap();
22882
22883 color_request_handle.next().await.unwrap();
22884 cx.run_until_parked();
22885 assert_eq!(
22886 3,
22887 requests_made.load(atomic::Ordering::Acquire),
22888 "Should query for colors once per save and once per formatting after save"
22889 );
22890
22891 drop(editor);
22892 let close = workspace
22893 .update(cx, |workspace, window, cx| {
22894 workspace.active_pane().update(cx, |pane, cx| {
22895 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22896 })
22897 })
22898 .unwrap();
22899 close.await.unwrap();
22900 let close = workspace
22901 .update(cx, |workspace, window, cx| {
22902 workspace.active_pane().update(cx, |pane, cx| {
22903 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22904 })
22905 })
22906 .unwrap();
22907 close.await.unwrap();
22908 assert_eq!(
22909 3,
22910 requests_made.load(atomic::Ordering::Acquire),
22911 "After saving and closing all editors, no extra requests should be made"
22912 );
22913 workspace
22914 .update(cx, |workspace, _, cx| {
22915 assert!(
22916 workspace.active_item(cx).is_none(),
22917 "Should close all editors"
22918 )
22919 })
22920 .unwrap();
22921
22922 workspace
22923 .update(cx, |workspace, window, cx| {
22924 workspace.active_pane().update(cx, |pane, cx| {
22925 pane.navigate_backward(window, cx);
22926 })
22927 })
22928 .unwrap();
22929 cx.executor().advance_clock(Duration::from_millis(100));
22930 cx.run_until_parked();
22931 let editor = workspace
22932 .update(cx, |workspace, _, cx| {
22933 workspace
22934 .active_item(cx)
22935 .expect("Should have reopened the editor again after navigating back")
22936 .downcast::<Editor>()
22937 .expect("Should be an editor")
22938 })
22939 .unwrap();
22940 color_request_handle.next().await.unwrap();
22941 assert_eq!(
22942 3,
22943 requests_made.load(atomic::Ordering::Acquire),
22944 "Cache should be reused on buffer close and reopen"
22945 );
22946 editor.update(cx, |editor, cx| {
22947 assert_eq!(
22948 vec![expected_color],
22949 extract_color_inlays(editor, cx),
22950 "Should have an initial inlay"
22951 );
22952 });
22953}
22954
22955#[gpui::test]
22956async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
22957 init_test(cx, |_| {});
22958 let (editor, cx) = cx.add_window_view(Editor::single_line);
22959 editor.update_in(cx, |editor, window, cx| {
22960 editor.set_text("oops\n\nwow\n", window, cx)
22961 });
22962 cx.run_until_parked();
22963 editor.update(cx, |editor, cx| {
22964 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
22965 });
22966 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
22967 cx.run_until_parked();
22968 editor.update(cx, |editor, cx| {
22969 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
22970 });
22971}
22972
22973#[track_caller]
22974fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
22975 editor
22976 .all_inlays(cx)
22977 .into_iter()
22978 .filter_map(|inlay| inlay.get_color())
22979 .map(Rgba::from)
22980 .collect()
22981}