1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
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, FormatterList,
29 LanguageSettingsContent, LspInsertMode, PrettierSettings, SelectedFormatter,
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, CloseOtherItems, 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();
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(),
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_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
1906 init_test(cx, |_| {});
1907
1908 let move_to_beg = MoveToBeginningOfLine {
1909 stop_at_soft_wraps: true,
1910 stop_at_indent: true,
1911 };
1912
1913 let editor = cx.add_window(|window, cx| {
1914 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
1915 build_editor(buffer, window, cx)
1916 });
1917
1918 _ = editor.update(cx, |editor, window, cx| {
1919 // test cursor between line_start and indent_start
1920 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1921 s.select_display_ranges([
1922 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
1923 ]);
1924 });
1925
1926 // cursor should move to line_start
1927 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1928 assert_eq!(
1929 editor.selections.display_ranges(cx),
1930 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1931 );
1932
1933 // cursor should move to indent_start
1934 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1935 assert_eq!(
1936 editor.selections.display_ranges(cx),
1937 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
1938 );
1939
1940 // cursor should move to back to line_start
1941 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1942 assert_eq!(
1943 editor.selections.display_ranges(cx),
1944 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1945 );
1946 });
1947}
1948
1949#[gpui::test]
1950fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1951 init_test(cx, |_| {});
1952
1953 let editor = cx.add_window(|window, cx| {
1954 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1955 build_editor(buffer, window, cx)
1956 });
1957 _ = editor.update(cx, |editor, window, cx| {
1958 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1959 s.select_display_ranges([
1960 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1961 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1962 ])
1963 });
1964 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1965 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1966
1967 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1968 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1969
1970 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1971 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1972
1973 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1974 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1975
1976 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1977 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1978
1979 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1980 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1981
1982 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1983 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1984
1985 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1986 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1987
1988 editor.move_right(&MoveRight, window, cx);
1989 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1990 assert_selection_ranges(
1991 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1992 editor,
1993 cx,
1994 );
1995
1996 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1997 assert_selection_ranges(
1998 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
1999 editor,
2000 cx,
2001 );
2002
2003 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2004 assert_selection_ranges(
2005 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2006 editor,
2007 cx,
2008 );
2009 });
2010}
2011
2012#[gpui::test]
2013fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2014 init_test(cx, |_| {});
2015
2016 let editor = cx.add_window(|window, cx| {
2017 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2018 build_editor(buffer, window, cx)
2019 });
2020
2021 _ = editor.update(cx, |editor, window, cx| {
2022 editor.set_wrap_width(Some(140.0.into()), cx);
2023 assert_eq!(
2024 editor.display_text(cx),
2025 "use one::{\n two::three::\n four::five\n};"
2026 );
2027
2028 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2029 s.select_display_ranges([
2030 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2031 ]);
2032 });
2033
2034 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2035 assert_eq!(
2036 editor.selections.display_ranges(cx),
2037 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2038 );
2039
2040 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2041 assert_eq!(
2042 editor.selections.display_ranges(cx),
2043 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2044 );
2045
2046 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2047 assert_eq!(
2048 editor.selections.display_ranges(cx),
2049 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2050 );
2051
2052 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2053 assert_eq!(
2054 editor.selections.display_ranges(cx),
2055 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2056 );
2057
2058 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2059 assert_eq!(
2060 editor.selections.display_ranges(cx),
2061 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2062 );
2063
2064 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2065 assert_eq!(
2066 editor.selections.display_ranges(cx),
2067 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2068 );
2069 });
2070}
2071
2072#[gpui::test]
2073async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2074 init_test(cx, |_| {});
2075 let mut cx = EditorTestContext::new(cx).await;
2076
2077 let line_height = cx.editor(|editor, window, _| {
2078 editor
2079 .style()
2080 .unwrap()
2081 .text
2082 .line_height_in_pixels(window.rem_size())
2083 });
2084 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2085
2086 cx.set_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_end_of_paragraph(&MoveToEndOfParagraph, 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_end_of_paragraph(&MoveToEndOfParagraph, 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_end_of_paragraph(&MoveToEndOfParagraph, 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 cx.update_editor(|editor, window, cx| {
2144 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2145 });
2146 cx.assert_editor_state(
2147 &r#"one
2148 two
2149
2150 three
2151 four
2152 five
2153 ˇ
2154 six"#
2155 .unindent(),
2156 );
2157
2158 cx.update_editor(|editor, window, cx| {
2159 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2160 });
2161 cx.assert_editor_state(
2162 &r#"one
2163 two
2164 ˇ
2165 three
2166 four
2167 five
2168
2169 six"#
2170 .unindent(),
2171 );
2172
2173 cx.update_editor(|editor, window, cx| {
2174 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2175 });
2176 cx.assert_editor_state(
2177 &r#"ˇone
2178 two
2179
2180 three
2181 four
2182 five
2183
2184 six"#
2185 .unindent(),
2186 );
2187}
2188
2189#[gpui::test]
2190async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2191 init_test(cx, |_| {});
2192 let mut cx = EditorTestContext::new(cx).await;
2193 let line_height = cx.editor(|editor, window, _| {
2194 editor
2195 .style()
2196 .unwrap()
2197 .text
2198 .line_height_in_pixels(window.rem_size())
2199 });
2200 let window = cx.window;
2201 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2202
2203 cx.set_state(
2204 r#"ˇone
2205 two
2206 three
2207 four
2208 five
2209 six
2210 seven
2211 eight
2212 nine
2213 ten
2214 "#,
2215 );
2216
2217 cx.update_editor(|editor, window, cx| {
2218 assert_eq!(
2219 editor.snapshot(window, cx).scroll_position(),
2220 gpui::Point::new(0., 0.)
2221 );
2222 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2223 assert_eq!(
2224 editor.snapshot(window, cx).scroll_position(),
2225 gpui::Point::new(0., 3.)
2226 );
2227 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2228 assert_eq!(
2229 editor.snapshot(window, cx).scroll_position(),
2230 gpui::Point::new(0., 6.)
2231 );
2232 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2233 assert_eq!(
2234 editor.snapshot(window, cx).scroll_position(),
2235 gpui::Point::new(0., 3.)
2236 );
2237
2238 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2239 assert_eq!(
2240 editor.snapshot(window, cx).scroll_position(),
2241 gpui::Point::new(0., 1.)
2242 );
2243 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2244 assert_eq!(
2245 editor.snapshot(window, cx).scroll_position(),
2246 gpui::Point::new(0., 3.)
2247 );
2248 });
2249}
2250
2251#[gpui::test]
2252async fn test_autoscroll(cx: &mut TestAppContext) {
2253 init_test(cx, |_| {});
2254 let mut cx = EditorTestContext::new(cx).await;
2255
2256 let line_height = cx.update_editor(|editor, window, cx| {
2257 editor.set_vertical_scroll_margin(2, cx);
2258 editor
2259 .style()
2260 .unwrap()
2261 .text
2262 .line_height_in_pixels(window.rem_size())
2263 });
2264 let window = cx.window;
2265 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2266
2267 cx.set_state(
2268 r#"ˇone
2269 two
2270 three
2271 four
2272 five
2273 six
2274 seven
2275 eight
2276 nine
2277 ten
2278 "#,
2279 );
2280 cx.update_editor(|editor, window, cx| {
2281 assert_eq!(
2282 editor.snapshot(window, cx).scroll_position(),
2283 gpui::Point::new(0., 0.0)
2284 );
2285 });
2286
2287 // Add a cursor below the visible area. Since both cursors cannot fit
2288 // on screen, the editor autoscrolls to reveal the newest cursor, and
2289 // allows the vertical scroll margin below that cursor.
2290 cx.update_editor(|editor, window, cx| {
2291 editor.change_selections(Default::default(), window, cx, |selections| {
2292 selections.select_ranges([
2293 Point::new(0, 0)..Point::new(0, 0),
2294 Point::new(6, 0)..Point::new(6, 0),
2295 ]);
2296 })
2297 });
2298 cx.update_editor(|editor, window, cx| {
2299 assert_eq!(
2300 editor.snapshot(window, cx).scroll_position(),
2301 gpui::Point::new(0., 3.0)
2302 );
2303 });
2304
2305 // Move down. The editor cursor scrolls down to track the newest cursor.
2306 cx.update_editor(|editor, window, cx| {
2307 editor.move_down(&Default::default(), window, cx);
2308 });
2309 cx.update_editor(|editor, window, cx| {
2310 assert_eq!(
2311 editor.snapshot(window, cx).scroll_position(),
2312 gpui::Point::new(0., 4.0)
2313 );
2314 });
2315
2316 // Add a cursor above the visible area. Since both cursors fit on screen,
2317 // the editor scrolls to show both.
2318 cx.update_editor(|editor, window, cx| {
2319 editor.change_selections(Default::default(), window, cx, |selections| {
2320 selections.select_ranges([
2321 Point::new(1, 0)..Point::new(1, 0),
2322 Point::new(6, 0)..Point::new(6, 0),
2323 ]);
2324 })
2325 });
2326 cx.update_editor(|editor, window, cx| {
2327 assert_eq!(
2328 editor.snapshot(window, cx).scroll_position(),
2329 gpui::Point::new(0., 1.0)
2330 );
2331 });
2332}
2333
2334#[gpui::test]
2335async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2336 init_test(cx, |_| {});
2337 let mut cx = EditorTestContext::new(cx).await;
2338
2339 let line_height = cx.editor(|editor, window, _cx| {
2340 editor
2341 .style()
2342 .unwrap()
2343 .text
2344 .line_height_in_pixels(window.rem_size())
2345 });
2346 let window = cx.window;
2347 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2348 cx.set_state(
2349 &r#"
2350 ˇone
2351 two
2352 threeˇ
2353 four
2354 five
2355 six
2356 seven
2357 eight
2358 nine
2359 ten
2360 "#
2361 .unindent(),
2362 );
2363
2364 cx.update_editor(|editor, window, cx| {
2365 editor.move_page_down(&MovePageDown::default(), window, cx)
2366 });
2367 cx.assert_editor_state(
2368 &r#"
2369 one
2370 two
2371 three
2372 ˇfour
2373 five
2374 sixˇ
2375 seven
2376 eight
2377 nine
2378 ten
2379 "#
2380 .unindent(),
2381 );
2382
2383 cx.update_editor(|editor, window, cx| {
2384 editor.move_page_down(&MovePageDown::default(), window, cx)
2385 });
2386 cx.assert_editor_state(
2387 &r#"
2388 one
2389 two
2390 three
2391 four
2392 five
2393 six
2394 ˇseven
2395 eight
2396 nineˇ
2397 ten
2398 "#
2399 .unindent(),
2400 );
2401
2402 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2403 cx.assert_editor_state(
2404 &r#"
2405 one
2406 two
2407 three
2408 ˇfour
2409 five
2410 sixˇ
2411 seven
2412 eight
2413 nine
2414 ten
2415 "#
2416 .unindent(),
2417 );
2418
2419 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2420 cx.assert_editor_state(
2421 &r#"
2422 ˇone
2423 two
2424 threeˇ
2425 four
2426 five
2427 six
2428 seven
2429 eight
2430 nine
2431 ten
2432 "#
2433 .unindent(),
2434 );
2435
2436 // Test select collapsing
2437 cx.update_editor(|editor, window, cx| {
2438 editor.move_page_down(&MovePageDown::default(), window, cx);
2439 editor.move_page_down(&MovePageDown::default(), window, cx);
2440 editor.move_page_down(&MovePageDown::default(), window, cx);
2441 });
2442 cx.assert_editor_state(
2443 &r#"
2444 one
2445 two
2446 three
2447 four
2448 five
2449 six
2450 seven
2451 eight
2452 nine
2453 ˇten
2454 ˇ"#
2455 .unindent(),
2456 );
2457}
2458
2459#[gpui::test]
2460async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2461 init_test(cx, |_| {});
2462 let mut cx = EditorTestContext::new(cx).await;
2463 cx.set_state("one «two threeˇ» four");
2464 cx.update_editor(|editor, window, cx| {
2465 editor.delete_to_beginning_of_line(
2466 &DeleteToBeginningOfLine {
2467 stop_at_indent: false,
2468 },
2469 window,
2470 cx,
2471 );
2472 assert_eq!(editor.text(cx), " four");
2473 });
2474}
2475
2476#[gpui::test]
2477fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2478 init_test(cx, |_| {});
2479
2480 let editor = cx.add_window(|window, cx| {
2481 let buffer = MultiBuffer::build_simple("one two three four", cx);
2482 build_editor(buffer.clone(), window, cx)
2483 });
2484
2485 _ = editor.update(cx, |editor, window, cx| {
2486 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2487 s.select_display_ranges([
2488 // an empty selection - the preceding word fragment is deleted
2489 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2490 // characters selected - they are deleted
2491 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2492 ])
2493 });
2494 editor.delete_to_previous_word_start(
2495 &DeleteToPreviousWordStart {
2496 ignore_newlines: false,
2497 },
2498 window,
2499 cx,
2500 );
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2502 });
2503
2504 _ = editor.update(cx, |editor, window, cx| {
2505 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2506 s.select_display_ranges([
2507 // an empty selection - the following word fragment is deleted
2508 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2509 // characters selected - they are deleted
2510 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2511 ])
2512 });
2513 editor.delete_to_next_word_end(
2514 &DeleteToNextWordEnd {
2515 ignore_newlines: false,
2516 },
2517 window,
2518 cx,
2519 );
2520 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2521 });
2522}
2523
2524#[gpui::test]
2525fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2526 init_test(cx, |_| {});
2527
2528 let editor = cx.add_window(|window, cx| {
2529 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2530 build_editor(buffer.clone(), window, cx)
2531 });
2532 let del_to_prev_word_start = DeleteToPreviousWordStart {
2533 ignore_newlines: false,
2534 };
2535 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2536 ignore_newlines: true,
2537 };
2538
2539 _ = editor.update(cx, |editor, window, cx| {
2540 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2541 s.select_display_ranges([
2542 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2543 ])
2544 });
2545 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2546 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2547 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2548 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2549 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2550 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2551 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2552 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2553 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2554 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2555 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2556 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2557 });
2558}
2559
2560#[gpui::test]
2561fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2562 init_test(cx, |_| {});
2563
2564 let editor = cx.add_window(|window, cx| {
2565 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2566 build_editor(buffer.clone(), window, cx)
2567 });
2568 let del_to_next_word_end = DeleteToNextWordEnd {
2569 ignore_newlines: false,
2570 };
2571 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2572 ignore_newlines: true,
2573 };
2574
2575 _ = editor.update(cx, |editor, window, cx| {
2576 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2577 s.select_display_ranges([
2578 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2579 ])
2580 });
2581 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2582 assert_eq!(
2583 editor.buffer.read(cx).read(cx).text(),
2584 "one\n two\nthree\n four"
2585 );
2586 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2587 assert_eq!(
2588 editor.buffer.read(cx).read(cx).text(),
2589 "\n two\nthree\n four"
2590 );
2591 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2592 assert_eq!(
2593 editor.buffer.read(cx).read(cx).text(),
2594 "two\nthree\n four"
2595 );
2596 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2597 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2598 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2599 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2600 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2601 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2602 });
2603}
2604
2605#[gpui::test]
2606fn test_newline(cx: &mut TestAppContext) {
2607 init_test(cx, |_| {});
2608
2609 let editor = cx.add_window(|window, cx| {
2610 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2611 build_editor(buffer.clone(), window, cx)
2612 });
2613
2614 _ = editor.update(cx, |editor, window, cx| {
2615 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2616 s.select_display_ranges([
2617 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2618 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2619 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2620 ])
2621 });
2622
2623 editor.newline(&Newline, window, cx);
2624 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2625 });
2626}
2627
2628#[gpui::test]
2629fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2630 init_test(cx, |_| {});
2631
2632 let editor = cx.add_window(|window, cx| {
2633 let buffer = MultiBuffer::build_simple(
2634 "
2635 a
2636 b(
2637 X
2638 )
2639 c(
2640 X
2641 )
2642 "
2643 .unindent()
2644 .as_str(),
2645 cx,
2646 );
2647 let mut editor = build_editor(buffer.clone(), window, cx);
2648 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2649 s.select_ranges([
2650 Point::new(2, 4)..Point::new(2, 5),
2651 Point::new(5, 4)..Point::new(5, 5),
2652 ])
2653 });
2654 editor
2655 });
2656
2657 _ = editor.update(cx, |editor, window, cx| {
2658 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2659 editor.buffer.update(cx, |buffer, cx| {
2660 buffer.edit(
2661 [
2662 (Point::new(1, 2)..Point::new(3, 0), ""),
2663 (Point::new(4, 2)..Point::new(6, 0), ""),
2664 ],
2665 None,
2666 cx,
2667 );
2668 assert_eq!(
2669 buffer.read(cx).text(),
2670 "
2671 a
2672 b()
2673 c()
2674 "
2675 .unindent()
2676 );
2677 });
2678 assert_eq!(
2679 editor.selections.ranges(cx),
2680 &[
2681 Point::new(1, 2)..Point::new(1, 2),
2682 Point::new(2, 2)..Point::new(2, 2),
2683 ],
2684 );
2685
2686 editor.newline(&Newline, window, cx);
2687 assert_eq!(
2688 editor.text(cx),
2689 "
2690 a
2691 b(
2692 )
2693 c(
2694 )
2695 "
2696 .unindent()
2697 );
2698
2699 // The selections are moved after the inserted newlines
2700 assert_eq!(
2701 editor.selections.ranges(cx),
2702 &[
2703 Point::new(2, 0)..Point::new(2, 0),
2704 Point::new(4, 0)..Point::new(4, 0),
2705 ],
2706 );
2707 });
2708}
2709
2710#[gpui::test]
2711async fn test_newline_above(cx: &mut TestAppContext) {
2712 init_test(cx, |settings| {
2713 settings.defaults.tab_size = NonZeroU32::new(4)
2714 });
2715
2716 let language = Arc::new(
2717 Language::new(
2718 LanguageConfig::default(),
2719 Some(tree_sitter_rust::LANGUAGE.into()),
2720 )
2721 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2722 .unwrap(),
2723 );
2724
2725 let mut cx = EditorTestContext::new(cx).await;
2726 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2727 cx.set_state(indoc! {"
2728 const a: ˇA = (
2729 (ˇ
2730 «const_functionˇ»(ˇ),
2731 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2732 )ˇ
2733 ˇ);ˇ
2734 "});
2735
2736 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2737 cx.assert_editor_state(indoc! {"
2738 ˇ
2739 const a: A = (
2740 ˇ
2741 (
2742 ˇ
2743 ˇ
2744 const_function(),
2745 ˇ
2746 ˇ
2747 ˇ
2748 ˇ
2749 something_else,
2750 ˇ
2751 )
2752 ˇ
2753 ˇ
2754 );
2755 "});
2756}
2757
2758#[gpui::test]
2759async fn test_newline_below(cx: &mut TestAppContext) {
2760 init_test(cx, |settings| {
2761 settings.defaults.tab_size = NonZeroU32::new(4)
2762 });
2763
2764 let language = Arc::new(
2765 Language::new(
2766 LanguageConfig::default(),
2767 Some(tree_sitter_rust::LANGUAGE.into()),
2768 )
2769 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2770 .unwrap(),
2771 );
2772
2773 let mut cx = EditorTestContext::new(cx).await;
2774 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2775 cx.set_state(indoc! {"
2776 const a: ˇA = (
2777 (ˇ
2778 «const_functionˇ»(ˇ),
2779 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2780 )ˇ
2781 ˇ);ˇ
2782 "});
2783
2784 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2785 cx.assert_editor_state(indoc! {"
2786 const a: A = (
2787 ˇ
2788 (
2789 ˇ
2790 const_function(),
2791 ˇ
2792 ˇ
2793 something_else,
2794 ˇ
2795 ˇ
2796 ˇ
2797 ˇ
2798 )
2799 ˇ
2800 );
2801 ˇ
2802 ˇ
2803 "});
2804}
2805
2806#[gpui::test]
2807async fn test_newline_comments(cx: &mut TestAppContext) {
2808 init_test(cx, |settings| {
2809 settings.defaults.tab_size = NonZeroU32::new(4)
2810 });
2811
2812 let language = Arc::new(Language::new(
2813 LanguageConfig {
2814 line_comments: vec!["// ".into()],
2815 ..LanguageConfig::default()
2816 },
2817 None,
2818 ));
2819 {
2820 let mut cx = EditorTestContext::new(cx).await;
2821 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2822 cx.set_state(indoc! {"
2823 // Fooˇ
2824 "});
2825
2826 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2827 cx.assert_editor_state(indoc! {"
2828 // Foo
2829 // ˇ
2830 "});
2831 // Ensure that we add comment prefix when existing line contains space
2832 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2833 cx.assert_editor_state(
2834 indoc! {"
2835 // Foo
2836 //s
2837 // ˇ
2838 "}
2839 .replace("s", " ") // s is used as space placeholder to prevent format on save
2840 .as_str(),
2841 );
2842 // Ensure that we add comment prefix when existing line does not contain space
2843 cx.set_state(indoc! {"
2844 // Foo
2845 //ˇ
2846 "});
2847 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2848 cx.assert_editor_state(indoc! {"
2849 // Foo
2850 //
2851 // ˇ
2852 "});
2853 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2854 cx.set_state(indoc! {"
2855 ˇ// Foo
2856 "});
2857 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2858 cx.assert_editor_state(indoc! {"
2859
2860 ˇ// Foo
2861 "});
2862 }
2863 // Ensure that comment continuations can be disabled.
2864 update_test_language_settings(cx, |settings| {
2865 settings.defaults.extend_comment_on_newline = Some(false);
2866 });
2867 let mut cx = EditorTestContext::new(cx).await;
2868 cx.set_state(indoc! {"
2869 // Fooˇ
2870 "});
2871 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2872 cx.assert_editor_state(indoc! {"
2873 // Foo
2874 ˇ
2875 "});
2876}
2877
2878#[gpui::test]
2879async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2880 init_test(cx, |settings| {
2881 settings.defaults.tab_size = NonZeroU32::new(4)
2882 });
2883
2884 let language = Arc::new(Language::new(
2885 LanguageConfig {
2886 line_comments: vec!["// ".into(), "/// ".into()],
2887 ..LanguageConfig::default()
2888 },
2889 None,
2890 ));
2891 {
2892 let mut cx = EditorTestContext::new(cx).await;
2893 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2894 cx.set_state(indoc! {"
2895 //ˇ
2896 "});
2897 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2898 cx.assert_editor_state(indoc! {"
2899 //
2900 // ˇ
2901 "});
2902
2903 cx.set_state(indoc! {"
2904 ///ˇ
2905 "});
2906 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2907 cx.assert_editor_state(indoc! {"
2908 ///
2909 /// ˇ
2910 "});
2911 }
2912}
2913
2914#[gpui::test]
2915async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2916 init_test(cx, |settings| {
2917 settings.defaults.tab_size = NonZeroU32::new(4)
2918 });
2919
2920 let language = Arc::new(
2921 Language::new(
2922 LanguageConfig {
2923 documentation_comment: Some(language::BlockCommentConfig {
2924 start: "/**".into(),
2925 end: "*/".into(),
2926 prefix: "* ".into(),
2927 tab_size: 1,
2928 }),
2929
2930 ..LanguageConfig::default()
2931 },
2932 Some(tree_sitter_rust::LANGUAGE.into()),
2933 )
2934 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2935 .unwrap(),
2936 );
2937
2938 {
2939 let mut cx = EditorTestContext::new(cx).await;
2940 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2941 cx.set_state(indoc! {"
2942 /**ˇ
2943 "});
2944
2945 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2946 cx.assert_editor_state(indoc! {"
2947 /**
2948 * ˇ
2949 "});
2950 // Ensure that if cursor is before the comment start,
2951 // we do not actually insert a comment prefix.
2952 cx.set_state(indoc! {"
2953 ˇ/**
2954 "});
2955 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2956 cx.assert_editor_state(indoc! {"
2957
2958 ˇ/**
2959 "});
2960 // Ensure that if cursor is between it doesn't add comment prefix.
2961 cx.set_state(indoc! {"
2962 /*ˇ*
2963 "});
2964 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2965 cx.assert_editor_state(indoc! {"
2966 /*
2967 ˇ*
2968 "});
2969 // Ensure that if suffix exists on same line after cursor it adds new line.
2970 cx.set_state(indoc! {"
2971 /**ˇ*/
2972 "});
2973 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2974 cx.assert_editor_state(indoc! {"
2975 /**
2976 * ˇ
2977 */
2978 "});
2979 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2980 cx.set_state(indoc! {"
2981 /**ˇ */
2982 "});
2983 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2984 cx.assert_editor_state(indoc! {"
2985 /**
2986 * ˇ
2987 */
2988 "});
2989 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2990 cx.set_state(indoc! {"
2991 /** ˇ*/
2992 "});
2993 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2994 cx.assert_editor_state(
2995 indoc! {"
2996 /**s
2997 * ˇ
2998 */
2999 "}
3000 .replace("s", " ") // s is used as space placeholder to prevent format on save
3001 .as_str(),
3002 );
3003 // Ensure that delimiter space is preserved when newline on already
3004 // spaced delimiter.
3005 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3006 cx.assert_editor_state(
3007 indoc! {"
3008 /**s
3009 *s
3010 * ˇ
3011 */
3012 "}
3013 .replace("s", " ") // s is used as space placeholder to prevent format on save
3014 .as_str(),
3015 );
3016 // Ensure that delimiter space is preserved when space is not
3017 // on existing delimiter.
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 // Ensure that if suffix exists on same line after cursor it
3031 // doesn't add extra new line if prefix is not on same line.
3032 cx.set_state(indoc! {"
3033 /**
3034 ˇ*/
3035 "});
3036 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3037 cx.assert_editor_state(indoc! {"
3038 /**
3039
3040 ˇ*/
3041 "});
3042 // Ensure that it detects suffix after existing prefix.
3043 cx.set_state(indoc! {"
3044 /**ˇ/
3045 "});
3046 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3047 cx.assert_editor_state(indoc! {"
3048 /**
3049 ˇ/
3050 "});
3051 // Ensure that if suffix exists on same line before
3052 // cursor it does not add comment prefix.
3053 cx.set_state(indoc! {"
3054 /** */ˇ
3055 "});
3056 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3057 cx.assert_editor_state(indoc! {"
3058 /** */
3059 ˇ
3060 "});
3061 // Ensure that if suffix exists on same line before
3062 // cursor it does not add comment prefix.
3063 cx.set_state(indoc! {"
3064 /**
3065 *
3066 */ˇ
3067 "});
3068 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3069 cx.assert_editor_state(indoc! {"
3070 /**
3071 *
3072 */
3073 ˇ
3074 "});
3075
3076 // Ensure that inline comment followed by code
3077 // doesn't add comment prefix on newline
3078 cx.set_state(indoc! {"
3079 /** */ textˇ
3080 "});
3081 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3082 cx.assert_editor_state(indoc! {"
3083 /** */ text
3084 ˇ
3085 "});
3086
3087 // Ensure that text after comment end tag
3088 // doesn't add comment prefix on newline
3089 cx.set_state(indoc! {"
3090 /**
3091 *
3092 */ˇtext
3093 "});
3094 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3095 cx.assert_editor_state(indoc! {"
3096 /**
3097 *
3098 */
3099 ˇtext
3100 "});
3101
3102 // Ensure if not comment block it doesn't
3103 // add comment prefix on newline
3104 cx.set_state(indoc! {"
3105 * textˇ
3106 "});
3107 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3108 cx.assert_editor_state(indoc! {"
3109 * text
3110 ˇ
3111 "});
3112 }
3113 // Ensure that comment continuations can be disabled.
3114 update_test_language_settings(cx, |settings| {
3115 settings.defaults.extend_comment_on_newline = Some(false);
3116 });
3117 let mut cx = EditorTestContext::new(cx).await;
3118 cx.set_state(indoc! {"
3119 /**ˇ
3120 "});
3121 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3122 cx.assert_editor_state(indoc! {"
3123 /**
3124 ˇ
3125 "});
3126}
3127
3128#[gpui::test]
3129async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3130 init_test(cx, |settings| {
3131 settings.defaults.tab_size = NonZeroU32::new(4)
3132 });
3133
3134 let lua_language = Arc::new(Language::new(
3135 LanguageConfig {
3136 line_comments: vec!["--".into()],
3137 block_comment: Some(language::BlockCommentConfig {
3138 start: "--[[".into(),
3139 prefix: "".into(),
3140 end: "]]".into(),
3141 tab_size: 0,
3142 }),
3143 ..LanguageConfig::default()
3144 },
3145 None,
3146 ));
3147
3148 let mut cx = EditorTestContext::new(cx).await;
3149 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3150
3151 // Line with line comment should extend
3152 cx.set_state(indoc! {"
3153 --ˇ
3154 "});
3155 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3156 cx.assert_editor_state(indoc! {"
3157 --
3158 --ˇ
3159 "});
3160
3161 // Line with block comment that matches line comment should not extend
3162 cx.set_state(indoc! {"
3163 --[[ˇ
3164 "});
3165 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3166 cx.assert_editor_state(indoc! {"
3167 --[[
3168 ˇ
3169 "});
3170}
3171
3172#[gpui::test]
3173fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3174 init_test(cx, |_| {});
3175
3176 let editor = cx.add_window(|window, cx| {
3177 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3178 let mut editor = build_editor(buffer.clone(), window, cx);
3179 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3180 s.select_ranges([3..4, 11..12, 19..20])
3181 });
3182 editor
3183 });
3184
3185 _ = editor.update(cx, |editor, window, cx| {
3186 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3187 editor.buffer.update(cx, |buffer, cx| {
3188 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3189 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3190 });
3191 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3192
3193 editor.insert("Z", window, cx);
3194 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3195
3196 // The selections are moved after the inserted characters
3197 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3198 });
3199}
3200
3201#[gpui::test]
3202async fn test_tab(cx: &mut TestAppContext) {
3203 init_test(cx, |settings| {
3204 settings.defaults.tab_size = NonZeroU32::new(3)
3205 });
3206
3207 let mut cx = EditorTestContext::new(cx).await;
3208 cx.set_state(indoc! {"
3209 ˇabˇc
3210 ˇ🏀ˇ🏀ˇefg
3211 dˇ
3212 "});
3213 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3214 cx.assert_editor_state(indoc! {"
3215 ˇab ˇc
3216 ˇ🏀 ˇ🏀 ˇefg
3217 d ˇ
3218 "});
3219
3220 cx.set_state(indoc! {"
3221 a
3222 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3223 "});
3224 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3225 cx.assert_editor_state(indoc! {"
3226 a
3227 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3228 "});
3229}
3230
3231#[gpui::test]
3232async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3233 init_test(cx, |_| {});
3234
3235 let mut cx = EditorTestContext::new(cx).await;
3236 let language = Arc::new(
3237 Language::new(
3238 LanguageConfig::default(),
3239 Some(tree_sitter_rust::LANGUAGE.into()),
3240 )
3241 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3242 .unwrap(),
3243 );
3244 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3245
3246 // test when all cursors are not at suggested indent
3247 // then simply move to their suggested indent location
3248 cx.set_state(indoc! {"
3249 const a: B = (
3250 c(
3251 ˇ
3252 ˇ )
3253 );
3254 "});
3255 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3256 cx.assert_editor_state(indoc! {"
3257 const a: B = (
3258 c(
3259 ˇ
3260 ˇ)
3261 );
3262 "});
3263
3264 // test cursor already at suggested indent not moving when
3265 // other cursors are yet to reach their suggested indents
3266 cx.set_state(indoc! {"
3267 ˇ
3268 const a: B = (
3269 c(
3270 d(
3271 ˇ
3272 )
3273 ˇ
3274 ˇ )
3275 );
3276 "});
3277 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3278 cx.assert_editor_state(indoc! {"
3279 ˇ
3280 const a: B = (
3281 c(
3282 d(
3283 ˇ
3284 )
3285 ˇ
3286 ˇ)
3287 );
3288 "});
3289 // test when all cursors are at suggested indent then tab is inserted
3290 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3291 cx.assert_editor_state(indoc! {"
3292 ˇ
3293 const a: B = (
3294 c(
3295 d(
3296 ˇ
3297 )
3298 ˇ
3299 ˇ)
3300 );
3301 "});
3302
3303 // test when current indent is less than suggested indent,
3304 // we adjust line to match suggested indent and move cursor to it
3305 //
3306 // when no other cursor is at word boundary, all of them should move
3307 cx.set_state(indoc! {"
3308 const a: B = (
3309 c(
3310 d(
3311 ˇ
3312 ˇ )
3313 ˇ )
3314 );
3315 "});
3316 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3317 cx.assert_editor_state(indoc! {"
3318 const a: B = (
3319 c(
3320 d(
3321 ˇ
3322 ˇ)
3323 ˇ)
3324 );
3325 "});
3326
3327 // test when current indent is less than suggested indent,
3328 // we adjust line to match suggested indent and move cursor to it
3329 //
3330 // when some other cursor is at word boundary, it should not move
3331 cx.set_state(indoc! {"
3332 const a: B = (
3333 c(
3334 d(
3335 ˇ
3336 ˇ )
3337 ˇ)
3338 );
3339 "});
3340 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3341 cx.assert_editor_state(indoc! {"
3342 const a: B = (
3343 c(
3344 d(
3345 ˇ
3346 ˇ)
3347 ˇ)
3348 );
3349 "});
3350
3351 // test when current indent is more than suggested indent,
3352 // we just move cursor to current indent instead of suggested indent
3353 //
3354 // when no other cursor is at word boundary, all of them should move
3355 cx.set_state(indoc! {"
3356 const a: B = (
3357 c(
3358 d(
3359 ˇ
3360 ˇ )
3361 ˇ )
3362 );
3363 "});
3364 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3365 cx.assert_editor_state(indoc! {"
3366 const a: B = (
3367 c(
3368 d(
3369 ˇ
3370 ˇ)
3371 ˇ)
3372 );
3373 "});
3374 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3375 cx.assert_editor_state(indoc! {"
3376 const a: B = (
3377 c(
3378 d(
3379 ˇ
3380 ˇ)
3381 ˇ)
3382 );
3383 "});
3384
3385 // test when current indent is more than suggested indent,
3386 // we just move cursor to current indent instead of suggested indent
3387 //
3388 // when some other cursor is at word boundary, it doesn't move
3389 cx.set_state(indoc! {"
3390 const a: B = (
3391 c(
3392 d(
3393 ˇ
3394 ˇ )
3395 ˇ)
3396 );
3397 "});
3398 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3399 cx.assert_editor_state(indoc! {"
3400 const a: B = (
3401 c(
3402 d(
3403 ˇ
3404 ˇ)
3405 ˇ)
3406 );
3407 "});
3408
3409 // handle auto-indent when there are multiple cursors on the same line
3410 cx.set_state(indoc! {"
3411 const a: B = (
3412 c(
3413 ˇ ˇ
3414 ˇ )
3415 );
3416 "});
3417 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3418 cx.assert_editor_state(indoc! {"
3419 const a: B = (
3420 c(
3421 ˇ
3422 ˇ)
3423 );
3424 "});
3425}
3426
3427#[gpui::test]
3428async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3429 init_test(cx, |settings| {
3430 settings.defaults.tab_size = NonZeroU32::new(3)
3431 });
3432
3433 let mut cx = EditorTestContext::new(cx).await;
3434 cx.set_state(indoc! {"
3435 ˇ
3436 \t ˇ
3437 \t ˇ
3438 \t ˇ
3439 \t \t\t \t \t\t \t\t \t \t ˇ
3440 "});
3441
3442 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3443 cx.assert_editor_state(indoc! {"
3444 ˇ
3445 \t ˇ
3446 \t ˇ
3447 \t ˇ
3448 \t \t\t \t \t\t \t\t \t \t ˇ
3449 "});
3450}
3451
3452#[gpui::test]
3453async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3454 init_test(cx, |settings| {
3455 settings.defaults.tab_size = NonZeroU32::new(4)
3456 });
3457
3458 let language = Arc::new(
3459 Language::new(
3460 LanguageConfig::default(),
3461 Some(tree_sitter_rust::LANGUAGE.into()),
3462 )
3463 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3464 .unwrap(),
3465 );
3466
3467 let mut cx = EditorTestContext::new(cx).await;
3468 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3469 cx.set_state(indoc! {"
3470 fn a() {
3471 if b {
3472 \t ˇc
3473 }
3474 }
3475 "});
3476
3477 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3478 cx.assert_editor_state(indoc! {"
3479 fn a() {
3480 if b {
3481 ˇc
3482 }
3483 }
3484 "});
3485}
3486
3487#[gpui::test]
3488async fn test_indent_outdent(cx: &mut TestAppContext) {
3489 init_test(cx, |settings| {
3490 settings.defaults.tab_size = NonZeroU32::new(4);
3491 });
3492
3493 let mut cx = EditorTestContext::new(cx).await;
3494
3495 cx.set_state(indoc! {"
3496 «oneˇ» «twoˇ»
3497 three
3498 four
3499 "});
3500 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3501 cx.assert_editor_state(indoc! {"
3502 «oneˇ» «twoˇ»
3503 three
3504 four
3505 "});
3506
3507 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3508 cx.assert_editor_state(indoc! {"
3509 «oneˇ» «twoˇ»
3510 three
3511 four
3512 "});
3513
3514 // select across line ending
3515 cx.set_state(indoc! {"
3516 one two
3517 t«hree
3518 ˇ» four
3519 "});
3520 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3521 cx.assert_editor_state(indoc! {"
3522 one two
3523 t«hree
3524 ˇ» four
3525 "});
3526
3527 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3528 cx.assert_editor_state(indoc! {"
3529 one two
3530 t«hree
3531 ˇ» four
3532 "});
3533
3534 // Ensure that indenting/outdenting works when the cursor is at column 0.
3535 cx.set_state(indoc! {"
3536 one two
3537 ˇthree
3538 four
3539 "});
3540 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3541 cx.assert_editor_state(indoc! {"
3542 one two
3543 ˇthree
3544 four
3545 "});
3546
3547 cx.set_state(indoc! {"
3548 one two
3549 ˇ three
3550 four
3551 "});
3552 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3553 cx.assert_editor_state(indoc! {"
3554 one two
3555 ˇthree
3556 four
3557 "});
3558}
3559
3560#[gpui::test]
3561async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3562 // This is a regression test for issue #33761
3563 init_test(cx, |_| {});
3564
3565 let mut cx = EditorTestContext::new(cx).await;
3566 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3567 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3568
3569 cx.set_state(
3570 r#"ˇ# ingress:
3571ˇ# api:
3572ˇ# enabled: false
3573ˇ# pathType: Prefix
3574ˇ# console:
3575ˇ# enabled: false
3576ˇ# pathType: Prefix
3577"#,
3578 );
3579
3580 // Press tab to indent all lines
3581 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3582
3583 cx.assert_editor_state(
3584 r#" ˇ# ingress:
3585 ˇ# api:
3586 ˇ# enabled: false
3587 ˇ# pathType: Prefix
3588 ˇ# console:
3589 ˇ# enabled: false
3590 ˇ# pathType: Prefix
3591"#,
3592 );
3593}
3594
3595#[gpui::test]
3596async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3597 // This is a test to make sure our fix for issue #33761 didn't break anything
3598 init_test(cx, |_| {});
3599
3600 let mut cx = EditorTestContext::new(cx).await;
3601 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3602 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3603
3604 cx.set_state(
3605 r#"ˇingress:
3606ˇ api:
3607ˇ enabled: false
3608ˇ pathType: Prefix
3609"#,
3610 );
3611
3612 // Press tab to indent all lines
3613 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3614
3615 cx.assert_editor_state(
3616 r#"ˇingress:
3617 ˇapi:
3618 ˇenabled: false
3619 ˇpathType: Prefix
3620"#,
3621 );
3622}
3623
3624#[gpui::test]
3625async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3626 init_test(cx, |settings| {
3627 settings.defaults.hard_tabs = Some(true);
3628 });
3629
3630 let mut cx = EditorTestContext::new(cx).await;
3631
3632 // select two ranges on one line
3633 cx.set_state(indoc! {"
3634 «oneˇ» «twoˇ»
3635 three
3636 four
3637 "});
3638 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3639 cx.assert_editor_state(indoc! {"
3640 \t«oneˇ» «twoˇ»
3641 three
3642 four
3643 "});
3644 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3645 cx.assert_editor_state(indoc! {"
3646 \t\t«oneˇ» «twoˇ»
3647 three
3648 four
3649 "});
3650 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3651 cx.assert_editor_state(indoc! {"
3652 \t«oneˇ» «twoˇ»
3653 three
3654 four
3655 "});
3656 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3657 cx.assert_editor_state(indoc! {"
3658 «oneˇ» «twoˇ»
3659 three
3660 four
3661 "});
3662
3663 // select across a line ending
3664 cx.set_state(indoc! {"
3665 one two
3666 t«hree
3667 ˇ»four
3668 "});
3669 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3670 cx.assert_editor_state(indoc! {"
3671 one two
3672 \tt«hree
3673 ˇ»four
3674 "});
3675 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3676 cx.assert_editor_state(indoc! {"
3677 one two
3678 \t\tt«hree
3679 ˇ»four
3680 "});
3681 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3682 cx.assert_editor_state(indoc! {"
3683 one two
3684 \tt«hree
3685 ˇ»four
3686 "});
3687 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3688 cx.assert_editor_state(indoc! {"
3689 one two
3690 t«hree
3691 ˇ»four
3692 "});
3693
3694 // Ensure that indenting/outdenting works when the cursor is at column 0.
3695 cx.set_state(indoc! {"
3696 one two
3697 ˇthree
3698 four
3699 "});
3700 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3701 cx.assert_editor_state(indoc! {"
3702 one two
3703 ˇthree
3704 four
3705 "});
3706 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3707 cx.assert_editor_state(indoc! {"
3708 one two
3709 \tˇthree
3710 four
3711 "});
3712 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3713 cx.assert_editor_state(indoc! {"
3714 one two
3715 ˇthree
3716 four
3717 "});
3718}
3719
3720#[gpui::test]
3721fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3722 init_test(cx, |settings| {
3723 settings.languages.0.extend([
3724 (
3725 "TOML".into(),
3726 LanguageSettingsContent {
3727 tab_size: NonZeroU32::new(2),
3728 ..Default::default()
3729 },
3730 ),
3731 (
3732 "Rust".into(),
3733 LanguageSettingsContent {
3734 tab_size: NonZeroU32::new(4),
3735 ..Default::default()
3736 },
3737 ),
3738 ]);
3739 });
3740
3741 let toml_language = Arc::new(Language::new(
3742 LanguageConfig {
3743 name: "TOML".into(),
3744 ..Default::default()
3745 },
3746 None,
3747 ));
3748 let rust_language = Arc::new(Language::new(
3749 LanguageConfig {
3750 name: "Rust".into(),
3751 ..Default::default()
3752 },
3753 None,
3754 ));
3755
3756 let toml_buffer =
3757 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3758 let rust_buffer =
3759 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3760 let multibuffer = cx.new(|cx| {
3761 let mut multibuffer = MultiBuffer::new(ReadWrite);
3762 multibuffer.push_excerpts(
3763 toml_buffer.clone(),
3764 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3765 cx,
3766 );
3767 multibuffer.push_excerpts(
3768 rust_buffer.clone(),
3769 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3770 cx,
3771 );
3772 multibuffer
3773 });
3774
3775 cx.add_window(|window, cx| {
3776 let mut editor = build_editor(multibuffer, window, cx);
3777
3778 assert_eq!(
3779 editor.text(cx),
3780 indoc! {"
3781 a = 1
3782 b = 2
3783
3784 const c: usize = 3;
3785 "}
3786 );
3787
3788 select_ranges(
3789 &mut editor,
3790 indoc! {"
3791 «aˇ» = 1
3792 b = 2
3793
3794 «const c:ˇ» usize = 3;
3795 "},
3796 window,
3797 cx,
3798 );
3799
3800 editor.tab(&Tab, window, cx);
3801 assert_text_with_selections(
3802 &mut editor,
3803 indoc! {"
3804 «aˇ» = 1
3805 b = 2
3806
3807 «const c:ˇ» usize = 3;
3808 "},
3809 cx,
3810 );
3811 editor.backtab(&Backtab, window, cx);
3812 assert_text_with_selections(
3813 &mut editor,
3814 indoc! {"
3815 «aˇ» = 1
3816 b = 2
3817
3818 «const c:ˇ» usize = 3;
3819 "},
3820 cx,
3821 );
3822
3823 editor
3824 });
3825}
3826
3827#[gpui::test]
3828async fn test_backspace(cx: &mut TestAppContext) {
3829 init_test(cx, |_| {});
3830
3831 let mut cx = EditorTestContext::new(cx).await;
3832
3833 // Basic backspace
3834 cx.set_state(indoc! {"
3835 onˇe two three
3836 fou«rˇ» five six
3837 seven «ˇeight nine
3838 »ten
3839 "});
3840 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3841 cx.assert_editor_state(indoc! {"
3842 oˇe two three
3843 fouˇ five six
3844 seven ˇten
3845 "});
3846
3847 // Test backspace inside and around indents
3848 cx.set_state(indoc! {"
3849 zero
3850 ˇone
3851 ˇtwo
3852 ˇ ˇ ˇ three
3853 ˇ ˇ four
3854 "});
3855 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3856 cx.assert_editor_state(indoc! {"
3857 zero
3858 ˇone
3859 ˇtwo
3860 ˇ threeˇ four
3861 "});
3862}
3863
3864#[gpui::test]
3865async fn test_delete(cx: &mut TestAppContext) {
3866 init_test(cx, |_| {});
3867
3868 let mut cx = EditorTestContext::new(cx).await;
3869 cx.set_state(indoc! {"
3870 onˇe two three
3871 fou«rˇ» five six
3872 seven «ˇeight nine
3873 »ten
3874 "});
3875 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3876 cx.assert_editor_state(indoc! {"
3877 onˇ two three
3878 fouˇ five six
3879 seven ˇten
3880 "});
3881}
3882
3883#[gpui::test]
3884fn test_delete_line(cx: &mut TestAppContext) {
3885 init_test(cx, |_| {});
3886
3887 let editor = cx.add_window(|window, cx| {
3888 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3889 build_editor(buffer, window, cx)
3890 });
3891 _ = editor.update(cx, |editor, window, cx| {
3892 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3893 s.select_display_ranges([
3894 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3895 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3896 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3897 ])
3898 });
3899 editor.delete_line(&DeleteLine, window, cx);
3900 assert_eq!(editor.display_text(cx), "ghi");
3901 assert_eq!(
3902 editor.selections.display_ranges(cx),
3903 vec![
3904 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3905 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3906 ]
3907 );
3908 });
3909
3910 let editor = cx.add_window(|window, cx| {
3911 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3912 build_editor(buffer, window, cx)
3913 });
3914 _ = editor.update(cx, |editor, window, cx| {
3915 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3916 s.select_display_ranges([
3917 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3918 ])
3919 });
3920 editor.delete_line(&DeleteLine, window, cx);
3921 assert_eq!(editor.display_text(cx), "ghi\n");
3922 assert_eq!(
3923 editor.selections.display_ranges(cx),
3924 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3925 );
3926 });
3927}
3928
3929#[gpui::test]
3930fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3931 init_test(cx, |_| {});
3932
3933 cx.add_window(|window, cx| {
3934 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3935 let mut editor = build_editor(buffer.clone(), window, cx);
3936 let buffer = buffer.read(cx).as_singleton().unwrap();
3937
3938 assert_eq!(
3939 editor.selections.ranges::<Point>(cx),
3940 &[Point::new(0, 0)..Point::new(0, 0)]
3941 );
3942
3943 // When on single line, replace newline at end by space
3944 editor.join_lines(&JoinLines, window, cx);
3945 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3946 assert_eq!(
3947 editor.selections.ranges::<Point>(cx),
3948 &[Point::new(0, 3)..Point::new(0, 3)]
3949 );
3950
3951 // When multiple lines are selected, remove newlines that are spanned by the selection
3952 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3953 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3954 });
3955 editor.join_lines(&JoinLines, window, cx);
3956 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3957 assert_eq!(
3958 editor.selections.ranges::<Point>(cx),
3959 &[Point::new(0, 11)..Point::new(0, 11)]
3960 );
3961
3962 // Undo should be transactional
3963 editor.undo(&Undo, window, cx);
3964 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3965 assert_eq!(
3966 editor.selections.ranges::<Point>(cx),
3967 &[Point::new(0, 5)..Point::new(2, 2)]
3968 );
3969
3970 // When joining an empty line don't insert a space
3971 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3972 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3973 });
3974 editor.join_lines(&JoinLines, window, cx);
3975 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3976 assert_eq!(
3977 editor.selections.ranges::<Point>(cx),
3978 [Point::new(2, 3)..Point::new(2, 3)]
3979 );
3980
3981 // We can remove trailing newlines
3982 editor.join_lines(&JoinLines, window, cx);
3983 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3984 assert_eq!(
3985 editor.selections.ranges::<Point>(cx),
3986 [Point::new(2, 3)..Point::new(2, 3)]
3987 );
3988
3989 // We don't blow up on the last line
3990 editor.join_lines(&JoinLines, window, cx);
3991 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3992 assert_eq!(
3993 editor.selections.ranges::<Point>(cx),
3994 [Point::new(2, 3)..Point::new(2, 3)]
3995 );
3996
3997 // reset to test indentation
3998 editor.buffer.update(cx, |buffer, cx| {
3999 buffer.edit(
4000 [
4001 (Point::new(1, 0)..Point::new(1, 2), " "),
4002 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4003 ],
4004 None,
4005 cx,
4006 )
4007 });
4008
4009 // We remove any leading spaces
4010 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4011 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4012 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4013 });
4014 editor.join_lines(&JoinLines, window, cx);
4015 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4016
4017 // We don't insert a space for a line containing only spaces
4018 editor.join_lines(&JoinLines, window, cx);
4019 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4020
4021 // We ignore any leading tabs
4022 editor.join_lines(&JoinLines, window, cx);
4023 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4024
4025 editor
4026 });
4027}
4028
4029#[gpui::test]
4030fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4031 init_test(cx, |_| {});
4032
4033 cx.add_window(|window, cx| {
4034 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4035 let mut editor = build_editor(buffer.clone(), window, cx);
4036 let buffer = buffer.read(cx).as_singleton().unwrap();
4037
4038 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4039 s.select_ranges([
4040 Point::new(0, 2)..Point::new(1, 1),
4041 Point::new(1, 2)..Point::new(1, 2),
4042 Point::new(3, 1)..Point::new(3, 2),
4043 ])
4044 });
4045
4046 editor.join_lines(&JoinLines, window, cx);
4047 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4048
4049 assert_eq!(
4050 editor.selections.ranges::<Point>(cx),
4051 [
4052 Point::new(0, 7)..Point::new(0, 7),
4053 Point::new(1, 3)..Point::new(1, 3)
4054 ]
4055 );
4056 editor
4057 });
4058}
4059
4060#[gpui::test]
4061async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4062 init_test(cx, |_| {});
4063
4064 let mut cx = EditorTestContext::new(cx).await;
4065
4066 let diff_base = r#"
4067 Line 0
4068 Line 1
4069 Line 2
4070 Line 3
4071 "#
4072 .unindent();
4073
4074 cx.set_state(
4075 &r#"
4076 ˇLine 0
4077 Line 1
4078 Line 2
4079 Line 3
4080 "#
4081 .unindent(),
4082 );
4083
4084 cx.set_head_text(&diff_base);
4085 executor.run_until_parked();
4086
4087 // Join lines
4088 cx.update_editor(|editor, window, cx| {
4089 editor.join_lines(&JoinLines, window, cx);
4090 });
4091 executor.run_until_parked();
4092
4093 cx.assert_editor_state(
4094 &r#"
4095 Line 0ˇ Line 1
4096 Line 2
4097 Line 3
4098 "#
4099 .unindent(),
4100 );
4101 // Join again
4102 cx.update_editor(|editor, window, cx| {
4103 editor.join_lines(&JoinLines, window, cx);
4104 });
4105 executor.run_until_parked();
4106
4107 cx.assert_editor_state(
4108 &r#"
4109 Line 0 Line 1ˇ Line 2
4110 Line 3
4111 "#
4112 .unindent(),
4113 );
4114}
4115
4116#[gpui::test]
4117async fn test_custom_newlines_cause_no_false_positive_diffs(
4118 executor: BackgroundExecutor,
4119 cx: &mut TestAppContext,
4120) {
4121 init_test(cx, |_| {});
4122 let mut cx = EditorTestContext::new(cx).await;
4123 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4124 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4125 executor.run_until_parked();
4126
4127 cx.update_editor(|editor, window, cx| {
4128 let snapshot = editor.snapshot(window, cx);
4129 assert_eq!(
4130 snapshot
4131 .buffer_snapshot
4132 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4133 .collect::<Vec<_>>(),
4134 Vec::new(),
4135 "Should not have any diffs for files with custom newlines"
4136 );
4137 });
4138}
4139
4140#[gpui::test]
4141async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4142 init_test(cx, |_| {});
4143
4144 let mut cx = EditorTestContext::new(cx).await;
4145
4146 // Test sort_lines_case_insensitive()
4147 cx.set_state(indoc! {"
4148 «z
4149 y
4150 x
4151 Z
4152 Y
4153 Xˇ»
4154 "});
4155 cx.update_editor(|e, window, cx| {
4156 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4157 });
4158 cx.assert_editor_state(indoc! {"
4159 «x
4160 X
4161 y
4162 Y
4163 z
4164 Zˇ»
4165 "});
4166
4167 // Test sort_lines_by_length()
4168 //
4169 // Demonstrates:
4170 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4171 // - sort is stable
4172 cx.set_state(indoc! {"
4173 «123
4174 æ
4175 12
4176 ∞
4177 1
4178 æˇ»
4179 "});
4180 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4181 cx.assert_editor_state(indoc! {"
4182 «æ
4183 ∞
4184 1
4185 æ
4186 12
4187 123ˇ»
4188 "});
4189
4190 // Test reverse_lines()
4191 cx.set_state(indoc! {"
4192 «5
4193 4
4194 3
4195 2
4196 1ˇ»
4197 "});
4198 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4199 cx.assert_editor_state(indoc! {"
4200 «1
4201 2
4202 3
4203 4
4204 5ˇ»
4205 "});
4206
4207 // Skip testing shuffle_line()
4208
4209 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4210 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4211
4212 // Don't manipulate when cursor is on single line, but expand the selection
4213 cx.set_state(indoc! {"
4214 ddˇdd
4215 ccc
4216 bb
4217 a
4218 "});
4219 cx.update_editor(|e, window, cx| {
4220 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4221 });
4222 cx.assert_editor_state(indoc! {"
4223 «ddddˇ»
4224 ccc
4225 bb
4226 a
4227 "});
4228
4229 // Basic manipulate case
4230 // Start selection moves to column 0
4231 // End of selection shrinks to fit shorter line
4232 cx.set_state(indoc! {"
4233 dd«d
4234 ccc
4235 bb
4236 aaaaaˇ»
4237 "});
4238 cx.update_editor(|e, window, cx| {
4239 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4240 });
4241 cx.assert_editor_state(indoc! {"
4242 «aaaaa
4243 bb
4244 ccc
4245 dddˇ»
4246 "});
4247
4248 // Manipulate case with newlines
4249 cx.set_state(indoc! {"
4250 dd«d
4251 ccc
4252
4253 bb
4254 aaaaa
4255
4256 ˇ»
4257 "});
4258 cx.update_editor(|e, window, cx| {
4259 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4260 });
4261 cx.assert_editor_state(indoc! {"
4262 «
4263
4264 aaaaa
4265 bb
4266 ccc
4267 dddˇ»
4268
4269 "});
4270
4271 // Adding new line
4272 cx.set_state(indoc! {"
4273 aa«a
4274 bbˇ»b
4275 "});
4276 cx.update_editor(|e, window, cx| {
4277 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4278 });
4279 cx.assert_editor_state(indoc! {"
4280 «aaa
4281 bbb
4282 added_lineˇ»
4283 "});
4284
4285 // Removing line
4286 cx.set_state(indoc! {"
4287 aa«a
4288 bbbˇ»
4289 "});
4290 cx.update_editor(|e, window, cx| {
4291 e.manipulate_immutable_lines(window, cx, |lines| {
4292 lines.pop();
4293 })
4294 });
4295 cx.assert_editor_state(indoc! {"
4296 «aaaˇ»
4297 "});
4298
4299 // Removing all lines
4300 cx.set_state(indoc! {"
4301 aa«a
4302 bbbˇ»
4303 "});
4304 cx.update_editor(|e, window, cx| {
4305 e.manipulate_immutable_lines(window, cx, |lines| {
4306 lines.drain(..);
4307 })
4308 });
4309 cx.assert_editor_state(indoc! {"
4310 ˇ
4311 "});
4312}
4313
4314#[gpui::test]
4315async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4316 init_test(cx, |_| {});
4317
4318 let mut cx = EditorTestContext::new(cx).await;
4319
4320 // Consider continuous selection as single selection
4321 cx.set_state(indoc! {"
4322 Aaa«aa
4323 cˇ»c«c
4324 bb
4325 aaaˇ»aa
4326 "});
4327 cx.update_editor(|e, window, cx| {
4328 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4329 });
4330 cx.assert_editor_state(indoc! {"
4331 «Aaaaa
4332 ccc
4333 bb
4334 aaaaaˇ»
4335 "});
4336
4337 cx.set_state(indoc! {"
4338 Aaa«aa
4339 cˇ»c«c
4340 bb
4341 aaaˇ»aa
4342 "});
4343 cx.update_editor(|e, window, cx| {
4344 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4345 });
4346 cx.assert_editor_state(indoc! {"
4347 «Aaaaa
4348 ccc
4349 bbˇ»
4350 "});
4351
4352 // Consider non continuous selection as distinct dedup operations
4353 cx.set_state(indoc! {"
4354 «aaaaa
4355 bb
4356 aaaaa
4357 aaaaaˇ»
4358
4359 aaa«aaˇ»
4360 "});
4361 cx.update_editor(|e, window, cx| {
4362 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4363 });
4364 cx.assert_editor_state(indoc! {"
4365 «aaaaa
4366 bbˇ»
4367
4368 «aaaaaˇ»
4369 "});
4370}
4371
4372#[gpui::test]
4373async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4374 init_test(cx, |_| {});
4375
4376 let mut cx = EditorTestContext::new(cx).await;
4377
4378 cx.set_state(indoc! {"
4379 «Aaa
4380 aAa
4381 Aaaˇ»
4382 "});
4383 cx.update_editor(|e, window, cx| {
4384 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4385 });
4386 cx.assert_editor_state(indoc! {"
4387 «Aaa
4388 aAaˇ»
4389 "});
4390
4391 cx.set_state(indoc! {"
4392 «Aaa
4393 aAa
4394 aaAˇ»
4395 "});
4396 cx.update_editor(|e, window, cx| {
4397 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4398 });
4399 cx.assert_editor_state(indoc! {"
4400 «Aaaˇ»
4401 "});
4402}
4403
4404#[gpui::test]
4405async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4406 init_test(cx, |_| {});
4407
4408 let mut cx = EditorTestContext::new(cx).await;
4409
4410 // Manipulate with multiple selections on a single line
4411 cx.set_state(indoc! {"
4412 dd«dd
4413 cˇ»c«c
4414 bb
4415 aaaˇ»aa
4416 "});
4417 cx.update_editor(|e, window, cx| {
4418 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4419 });
4420 cx.assert_editor_state(indoc! {"
4421 «aaaaa
4422 bb
4423 ccc
4424 ddddˇ»
4425 "});
4426
4427 // Manipulate with multiple disjoin selections
4428 cx.set_state(indoc! {"
4429 5«
4430 4
4431 3
4432 2
4433 1ˇ»
4434
4435 dd«dd
4436 ccc
4437 bb
4438 aaaˇ»aa
4439 "});
4440 cx.update_editor(|e, window, cx| {
4441 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4442 });
4443 cx.assert_editor_state(indoc! {"
4444 «1
4445 2
4446 3
4447 4
4448 5ˇ»
4449
4450 «aaaaa
4451 bb
4452 ccc
4453 ddddˇ»
4454 "});
4455
4456 // Adding lines on each selection
4457 cx.set_state(indoc! {"
4458 2«
4459 1ˇ»
4460
4461 bb«bb
4462 aaaˇ»aa
4463 "});
4464 cx.update_editor(|e, window, cx| {
4465 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4466 });
4467 cx.assert_editor_state(indoc! {"
4468 «2
4469 1
4470 added lineˇ»
4471
4472 «bbbb
4473 aaaaa
4474 added lineˇ»
4475 "});
4476
4477 // Removing lines on each selection
4478 cx.set_state(indoc! {"
4479 2«
4480 1ˇ»
4481
4482 bb«bb
4483 aaaˇ»aa
4484 "});
4485 cx.update_editor(|e, window, cx| {
4486 e.manipulate_immutable_lines(window, cx, |lines| {
4487 lines.pop();
4488 })
4489 });
4490 cx.assert_editor_state(indoc! {"
4491 «2ˇ»
4492
4493 «bbbbˇ»
4494 "});
4495}
4496
4497#[gpui::test]
4498async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4499 init_test(cx, |settings| {
4500 settings.defaults.tab_size = NonZeroU32::new(3)
4501 });
4502
4503 let mut cx = EditorTestContext::new(cx).await;
4504
4505 // MULTI SELECTION
4506 // Ln.1 "«" tests empty lines
4507 // Ln.9 tests just leading whitespace
4508 cx.set_state(indoc! {"
4509 «
4510 abc // No indentationˇ»
4511 «\tabc // 1 tabˇ»
4512 \t\tabc « ˇ» // 2 tabs
4513 \t ab«c // Tab followed by space
4514 \tabc // Space followed by tab (3 spaces should be the result)
4515 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4516 abˇ»ˇc ˇ ˇ // Already space indented«
4517 \t
4518 \tabc\tdef // Only the leading tab is manipulatedˇ»
4519 "});
4520 cx.update_editor(|e, window, cx| {
4521 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4522 });
4523 cx.assert_editor_state(
4524 indoc! {"
4525 «
4526 abc // No indentation
4527 abc // 1 tab
4528 abc // 2 tabs
4529 abc // Tab followed by space
4530 abc // Space followed by tab (3 spaces should be the result)
4531 abc // Mixed indentation (tab conversion depends on the column)
4532 abc // Already space indented
4533 ·
4534 abc\tdef // Only the leading tab is manipulatedˇ»
4535 "}
4536 .replace("·", "")
4537 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4538 );
4539
4540 // Test on just a few lines, the others should remain unchanged
4541 // Only lines (3, 5, 10, 11) should change
4542 cx.set_state(
4543 indoc! {"
4544 ·
4545 abc // No indentation
4546 \tabcˇ // 1 tab
4547 \t\tabc // 2 tabs
4548 \t abcˇ // Tab followed by space
4549 \tabc // Space followed by tab (3 spaces should be the result)
4550 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4551 abc // Already space indented
4552 «\t
4553 \tabc\tdef // Only the leading tab is manipulatedˇ»
4554 "}
4555 .replace("·", "")
4556 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4557 );
4558 cx.update_editor(|e, window, cx| {
4559 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4560 });
4561 cx.assert_editor_state(
4562 indoc! {"
4563 ·
4564 abc // No indentation
4565 « abc // 1 tabˇ»
4566 \t\tabc // 2 tabs
4567 « abc // Tab followed by spaceˇ»
4568 \tabc // Space followed by tab (3 spaces should be the result)
4569 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4570 abc // Already space indented
4571 « ·
4572 abc\tdef // Only the leading tab is manipulatedˇ»
4573 "}
4574 .replace("·", "")
4575 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4576 );
4577
4578 // SINGLE SELECTION
4579 // Ln.1 "«" tests empty lines
4580 // Ln.9 tests just leading whitespace
4581 cx.set_state(indoc! {"
4582 «
4583 abc // No indentation
4584 \tabc // 1 tab
4585 \t\tabc // 2 tabs
4586 \t abc // Tab followed by space
4587 \tabc // Space followed by tab (3 spaces should be the result)
4588 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4589 abc // Already space indented
4590 \t
4591 \tabc\tdef // Only the leading tab is manipulatedˇ»
4592 "});
4593 cx.update_editor(|e, window, cx| {
4594 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4595 });
4596 cx.assert_editor_state(
4597 indoc! {"
4598 «
4599 abc // No indentation
4600 abc // 1 tab
4601 abc // 2 tabs
4602 abc // Tab followed by space
4603 abc // Space followed by tab (3 spaces should be the result)
4604 abc // Mixed indentation (tab conversion depends on the column)
4605 abc // Already space indented
4606 ·
4607 abc\tdef // Only the leading tab is manipulatedˇ»
4608 "}
4609 .replace("·", "")
4610 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4611 );
4612}
4613
4614#[gpui::test]
4615async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
4616 init_test(cx, |settings| {
4617 settings.defaults.tab_size = NonZeroU32::new(3)
4618 });
4619
4620 let mut cx = EditorTestContext::new(cx).await;
4621
4622 // MULTI SELECTION
4623 // Ln.1 "«" tests empty lines
4624 // Ln.11 tests just leading whitespace
4625 cx.set_state(indoc! {"
4626 «
4627 abˇ»ˇc // No indentation
4628 abc ˇ ˇ // 1 space (< 3 so dont convert)
4629 abc « // 2 spaces (< 3 so dont convert)
4630 abc // 3 spaces (convert)
4631 abc ˇ» // 5 spaces (1 tab + 2 spaces)
4632 «\tˇ»\t«\tˇ»abc // Already tab indented
4633 «\t abc // Tab followed by space
4634 \tabc // Space followed by tab (should be consumed due to tab)
4635 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4636 \tˇ» «\t
4637 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
4638 "});
4639 cx.update_editor(|e, window, cx| {
4640 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4641 });
4642 cx.assert_editor_state(indoc! {"
4643 «
4644 abc // No indentation
4645 abc // 1 space (< 3 so dont convert)
4646 abc // 2 spaces (< 3 so dont convert)
4647 \tabc // 3 spaces (convert)
4648 \t abc // 5 spaces (1 tab + 2 spaces)
4649 \t\t\tabc // Already tab indented
4650 \t abc // Tab followed by space
4651 \tabc // Space followed by tab (should be consumed due to tab)
4652 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4653 \t\t\t
4654 \tabc \t // Only the leading spaces should be convertedˇ»
4655 "});
4656
4657 // Test on just a few lines, the other should remain unchanged
4658 // Only lines (4, 8, 11, 12) should change
4659 cx.set_state(
4660 indoc! {"
4661 ·
4662 abc // No indentation
4663 abc // 1 space (< 3 so dont convert)
4664 abc // 2 spaces (< 3 so dont convert)
4665 « abc // 3 spaces (convert)ˇ»
4666 abc // 5 spaces (1 tab + 2 spaces)
4667 \t\t\tabc // Already tab indented
4668 \t abc // Tab followed by space
4669 \tabc ˇ // Space followed by tab (should be consumed due to tab)
4670 \t\t \tabc // Mixed indentation
4671 \t \t \t \tabc // Mixed indentation
4672 \t \tˇ
4673 « abc \t // Only the leading spaces should be convertedˇ»
4674 "}
4675 .replace("·", "")
4676 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4677 );
4678 cx.update_editor(|e, window, cx| {
4679 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4680 });
4681 cx.assert_editor_state(
4682 indoc! {"
4683 ·
4684 abc // No indentation
4685 abc // 1 space (< 3 so dont convert)
4686 abc // 2 spaces (< 3 so dont convert)
4687 «\tabc // 3 spaces (convert)ˇ»
4688 abc // 5 spaces (1 tab + 2 spaces)
4689 \t\t\tabc // Already tab indented
4690 \t abc // Tab followed by space
4691 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
4692 \t\t \tabc // Mixed indentation
4693 \t \t \t \tabc // Mixed indentation
4694 «\t\t\t
4695 \tabc \t // Only the leading spaces should be convertedˇ»
4696 "}
4697 .replace("·", "")
4698 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4699 );
4700
4701 // SINGLE SELECTION
4702 // Ln.1 "«" tests empty lines
4703 // Ln.11 tests just leading whitespace
4704 cx.set_state(indoc! {"
4705 «
4706 abc // No indentation
4707 abc // 1 space (< 3 so dont convert)
4708 abc // 2 spaces (< 3 so dont convert)
4709 abc // 3 spaces (convert)
4710 abc // 5 spaces (1 tab + 2 spaces)
4711 \t\t\tabc // Already tab indented
4712 \t abc // Tab followed by space
4713 \tabc // Space followed by tab (should be consumed due to tab)
4714 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4715 \t \t
4716 abc \t // Only the leading spaces should be convertedˇ»
4717 "});
4718 cx.update_editor(|e, window, cx| {
4719 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4720 });
4721 cx.assert_editor_state(indoc! {"
4722 «
4723 abc // No indentation
4724 abc // 1 space (< 3 so dont convert)
4725 abc // 2 spaces (< 3 so dont convert)
4726 \tabc // 3 spaces (convert)
4727 \t abc // 5 spaces (1 tab + 2 spaces)
4728 \t\t\tabc // Already tab indented
4729 \t abc // Tab followed by space
4730 \tabc // Space followed by tab (should be consumed due to tab)
4731 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4732 \t\t\t
4733 \tabc \t // Only the leading spaces should be convertedˇ»
4734 "});
4735}
4736
4737#[gpui::test]
4738async fn test_toggle_case(cx: &mut TestAppContext) {
4739 init_test(cx, |_| {});
4740
4741 let mut cx = EditorTestContext::new(cx).await;
4742
4743 // If all lower case -> upper case
4744 cx.set_state(indoc! {"
4745 «hello worldˇ»
4746 "});
4747 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4748 cx.assert_editor_state(indoc! {"
4749 «HELLO WORLDˇ»
4750 "});
4751
4752 // If all upper case -> lower case
4753 cx.set_state(indoc! {"
4754 «HELLO WORLDˇ»
4755 "});
4756 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4757 cx.assert_editor_state(indoc! {"
4758 «hello worldˇ»
4759 "});
4760
4761 // If any upper case characters are identified -> lower case
4762 // This matches JetBrains IDEs
4763 cx.set_state(indoc! {"
4764 «hEllo worldˇ»
4765 "});
4766 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4767 cx.assert_editor_state(indoc! {"
4768 «hello worldˇ»
4769 "});
4770}
4771
4772#[gpui::test]
4773async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
4774 init_test(cx, |_| {});
4775
4776 let mut cx = EditorTestContext::new(cx).await;
4777
4778 cx.set_state(indoc! {"
4779 «implement-windows-supportˇ»
4780 "});
4781 cx.update_editor(|e, window, cx| {
4782 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
4783 });
4784 cx.assert_editor_state(indoc! {"
4785 «Implement windows supportˇ»
4786 "});
4787}
4788
4789#[gpui::test]
4790async fn test_manipulate_text(cx: &mut TestAppContext) {
4791 init_test(cx, |_| {});
4792
4793 let mut cx = EditorTestContext::new(cx).await;
4794
4795 // Test convert_to_upper_case()
4796 cx.set_state(indoc! {"
4797 «hello worldˇ»
4798 "});
4799 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4800 cx.assert_editor_state(indoc! {"
4801 «HELLO WORLDˇ»
4802 "});
4803
4804 // Test convert_to_lower_case()
4805 cx.set_state(indoc! {"
4806 «HELLO WORLDˇ»
4807 "});
4808 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4809 cx.assert_editor_state(indoc! {"
4810 «hello worldˇ»
4811 "});
4812
4813 // Test multiple line, single selection case
4814 cx.set_state(indoc! {"
4815 «The quick brown
4816 fox jumps over
4817 the lazy dogˇ»
4818 "});
4819 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4820 cx.assert_editor_state(indoc! {"
4821 «The Quick Brown
4822 Fox Jumps Over
4823 The Lazy Dogˇ»
4824 "});
4825
4826 // Test multiple line, single selection case
4827 cx.set_state(indoc! {"
4828 «The quick brown
4829 fox jumps over
4830 the lazy dogˇ»
4831 "});
4832 cx.update_editor(|e, window, cx| {
4833 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4834 });
4835 cx.assert_editor_state(indoc! {"
4836 «TheQuickBrown
4837 FoxJumpsOver
4838 TheLazyDogˇ»
4839 "});
4840
4841 // From here on out, test more complex cases of manipulate_text()
4842
4843 // Test no selection case - should affect words cursors are in
4844 // Cursor at beginning, middle, and end of word
4845 cx.set_state(indoc! {"
4846 ˇhello big beauˇtiful worldˇ
4847 "});
4848 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4849 cx.assert_editor_state(indoc! {"
4850 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4851 "});
4852
4853 // Test multiple selections on a single line and across multiple lines
4854 cx.set_state(indoc! {"
4855 «Theˇ» quick «brown
4856 foxˇ» jumps «overˇ»
4857 the «lazyˇ» dog
4858 "});
4859 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4860 cx.assert_editor_state(indoc! {"
4861 «THEˇ» quick «BROWN
4862 FOXˇ» jumps «OVERˇ»
4863 the «LAZYˇ» dog
4864 "});
4865
4866 // Test case where text length grows
4867 cx.set_state(indoc! {"
4868 «tschüߡ»
4869 "});
4870 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4871 cx.assert_editor_state(indoc! {"
4872 «TSCHÜSSˇ»
4873 "});
4874
4875 // Test to make sure we don't crash when text shrinks
4876 cx.set_state(indoc! {"
4877 aaa_bbbˇ
4878 "});
4879 cx.update_editor(|e, window, cx| {
4880 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4881 });
4882 cx.assert_editor_state(indoc! {"
4883 «aaaBbbˇ»
4884 "});
4885
4886 // Test to make sure we all aware of the fact that each word can grow and shrink
4887 // Final selections should be aware of this fact
4888 cx.set_state(indoc! {"
4889 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4890 "});
4891 cx.update_editor(|e, window, cx| {
4892 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4893 });
4894 cx.assert_editor_state(indoc! {"
4895 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4896 "});
4897
4898 cx.set_state(indoc! {"
4899 «hElLo, WoRld!ˇ»
4900 "});
4901 cx.update_editor(|e, window, cx| {
4902 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4903 });
4904 cx.assert_editor_state(indoc! {"
4905 «HeLlO, wOrLD!ˇ»
4906 "});
4907}
4908
4909#[gpui::test]
4910fn test_duplicate_line(cx: &mut TestAppContext) {
4911 init_test(cx, |_| {});
4912
4913 let editor = cx.add_window(|window, cx| {
4914 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4915 build_editor(buffer, window, cx)
4916 });
4917 _ = editor.update(cx, |editor, window, cx| {
4918 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4919 s.select_display_ranges([
4920 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4921 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4922 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4923 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4924 ])
4925 });
4926 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4927 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4928 assert_eq!(
4929 editor.selections.display_ranges(cx),
4930 vec![
4931 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4932 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4933 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4934 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4935 ]
4936 );
4937 });
4938
4939 let editor = cx.add_window(|window, cx| {
4940 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4941 build_editor(buffer, window, cx)
4942 });
4943 _ = editor.update(cx, |editor, window, cx| {
4944 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4945 s.select_display_ranges([
4946 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4947 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4948 ])
4949 });
4950 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4951 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4952 assert_eq!(
4953 editor.selections.display_ranges(cx),
4954 vec![
4955 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4956 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4957 ]
4958 );
4959 });
4960
4961 // With `move_upwards` the selections stay in place, except for
4962 // the lines inserted above them
4963 let editor = cx.add_window(|window, cx| {
4964 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4965 build_editor(buffer, window, cx)
4966 });
4967 _ = editor.update(cx, |editor, window, cx| {
4968 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4969 s.select_display_ranges([
4970 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4971 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4972 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4973 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4974 ])
4975 });
4976 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4977 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4978 assert_eq!(
4979 editor.selections.display_ranges(cx),
4980 vec![
4981 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4982 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4983 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4984 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4985 ]
4986 );
4987 });
4988
4989 let editor = cx.add_window(|window, cx| {
4990 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4991 build_editor(buffer, window, cx)
4992 });
4993 _ = editor.update(cx, |editor, window, cx| {
4994 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4995 s.select_display_ranges([
4996 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4997 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4998 ])
4999 });
5000 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5001 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5002 assert_eq!(
5003 editor.selections.display_ranges(cx),
5004 vec![
5005 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5006 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5007 ]
5008 );
5009 });
5010
5011 let editor = cx.add_window(|window, cx| {
5012 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5013 build_editor(buffer, window, cx)
5014 });
5015 _ = editor.update(cx, |editor, window, cx| {
5016 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5017 s.select_display_ranges([
5018 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5019 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5020 ])
5021 });
5022 editor.duplicate_selection(&DuplicateSelection, window, cx);
5023 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5024 assert_eq!(
5025 editor.selections.display_ranges(cx),
5026 vec![
5027 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5028 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5029 ]
5030 );
5031 });
5032}
5033
5034#[gpui::test]
5035fn test_move_line_up_down(cx: &mut TestAppContext) {
5036 init_test(cx, |_| {});
5037
5038 let editor = cx.add_window(|window, cx| {
5039 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5040 build_editor(buffer, window, cx)
5041 });
5042 _ = editor.update(cx, |editor, window, cx| {
5043 editor.fold_creases(
5044 vec![
5045 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5046 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5047 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5048 ],
5049 true,
5050 window,
5051 cx,
5052 );
5053 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5054 s.select_display_ranges([
5055 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5056 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5057 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5058 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5059 ])
5060 });
5061 assert_eq!(
5062 editor.display_text(cx),
5063 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5064 );
5065
5066 editor.move_line_up(&MoveLineUp, window, cx);
5067 assert_eq!(
5068 editor.display_text(cx),
5069 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5070 );
5071 assert_eq!(
5072 editor.selections.display_ranges(cx),
5073 vec![
5074 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5075 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5076 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5077 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5078 ]
5079 );
5080 });
5081
5082 _ = editor.update(cx, |editor, window, cx| {
5083 editor.move_line_down(&MoveLineDown, window, cx);
5084 assert_eq!(
5085 editor.display_text(cx),
5086 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5087 );
5088 assert_eq!(
5089 editor.selections.display_ranges(cx),
5090 vec![
5091 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5092 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5093 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5094 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5095 ]
5096 );
5097 });
5098
5099 _ = editor.update(cx, |editor, window, cx| {
5100 editor.move_line_down(&MoveLineDown, window, cx);
5101 assert_eq!(
5102 editor.display_text(cx),
5103 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5104 );
5105 assert_eq!(
5106 editor.selections.display_ranges(cx),
5107 vec![
5108 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5109 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5110 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5111 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5112 ]
5113 );
5114 });
5115
5116 _ = editor.update(cx, |editor, window, cx| {
5117 editor.move_line_up(&MoveLineUp, window, cx);
5118 assert_eq!(
5119 editor.display_text(cx),
5120 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5121 );
5122 assert_eq!(
5123 editor.selections.display_ranges(cx),
5124 vec![
5125 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5126 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5127 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5128 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5129 ]
5130 );
5131 });
5132}
5133
5134#[gpui::test]
5135fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5136 init_test(cx, |_| {});
5137 let editor = cx.add_window(|window, cx| {
5138 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5139 build_editor(buffer, window, cx)
5140 });
5141 _ = editor.update(cx, |editor, window, cx| {
5142 editor.fold_creases(
5143 vec![Crease::simple(
5144 Point::new(6, 4)..Point::new(7, 4),
5145 FoldPlaceholder::test(),
5146 )],
5147 true,
5148 window,
5149 cx,
5150 );
5151 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5152 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5153 });
5154 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5155 editor.move_line_up(&MoveLineUp, window, cx);
5156 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5157 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5158 });
5159}
5160
5161#[gpui::test]
5162fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5163 init_test(cx, |_| {});
5164
5165 let editor = cx.add_window(|window, cx| {
5166 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5167 build_editor(buffer, window, cx)
5168 });
5169 _ = editor.update(cx, |editor, window, cx| {
5170 let snapshot = editor.buffer.read(cx).snapshot(cx);
5171 editor.insert_blocks(
5172 [BlockProperties {
5173 style: BlockStyle::Fixed,
5174 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5175 height: Some(1),
5176 render: Arc::new(|_| div().into_any()),
5177 priority: 0,
5178 }],
5179 Some(Autoscroll::fit()),
5180 cx,
5181 );
5182 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5183 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5184 });
5185 editor.move_line_down(&MoveLineDown, window, cx);
5186 });
5187}
5188
5189#[gpui::test]
5190async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5191 init_test(cx, |_| {});
5192
5193 let mut cx = EditorTestContext::new(cx).await;
5194 cx.set_state(
5195 &"
5196 ˇzero
5197 one
5198 two
5199 three
5200 four
5201 five
5202 "
5203 .unindent(),
5204 );
5205
5206 // Create a four-line block that replaces three lines of text.
5207 cx.update_editor(|editor, window, cx| {
5208 let snapshot = editor.snapshot(window, cx);
5209 let snapshot = &snapshot.buffer_snapshot;
5210 let placement = BlockPlacement::Replace(
5211 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5212 );
5213 editor.insert_blocks(
5214 [BlockProperties {
5215 placement,
5216 height: Some(4),
5217 style: BlockStyle::Sticky,
5218 render: Arc::new(|_| gpui::div().into_any_element()),
5219 priority: 0,
5220 }],
5221 None,
5222 cx,
5223 );
5224 });
5225
5226 // Move down so that the cursor touches the block.
5227 cx.update_editor(|editor, window, cx| {
5228 editor.move_down(&Default::default(), window, cx);
5229 });
5230 cx.assert_editor_state(
5231 &"
5232 zero
5233 «one
5234 two
5235 threeˇ»
5236 four
5237 five
5238 "
5239 .unindent(),
5240 );
5241
5242 // Move down past the block.
5243 cx.update_editor(|editor, window, cx| {
5244 editor.move_down(&Default::default(), window, cx);
5245 });
5246 cx.assert_editor_state(
5247 &"
5248 zero
5249 one
5250 two
5251 three
5252 ˇfour
5253 five
5254 "
5255 .unindent(),
5256 );
5257}
5258
5259#[gpui::test]
5260fn test_transpose(cx: &mut TestAppContext) {
5261 init_test(cx, |_| {});
5262
5263 _ = cx.add_window(|window, cx| {
5264 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5265 editor.set_style(EditorStyle::default(), window, cx);
5266 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5267 s.select_ranges([1..1])
5268 });
5269 editor.transpose(&Default::default(), window, cx);
5270 assert_eq!(editor.text(cx), "bac");
5271 assert_eq!(editor.selections.ranges(cx), [2..2]);
5272
5273 editor.transpose(&Default::default(), window, cx);
5274 assert_eq!(editor.text(cx), "bca");
5275 assert_eq!(editor.selections.ranges(cx), [3..3]);
5276
5277 editor.transpose(&Default::default(), window, cx);
5278 assert_eq!(editor.text(cx), "bac");
5279 assert_eq!(editor.selections.ranges(cx), [3..3]);
5280
5281 editor
5282 });
5283
5284 _ = cx.add_window(|window, cx| {
5285 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5286 editor.set_style(EditorStyle::default(), window, cx);
5287 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5288 s.select_ranges([3..3])
5289 });
5290 editor.transpose(&Default::default(), window, cx);
5291 assert_eq!(editor.text(cx), "acb\nde");
5292 assert_eq!(editor.selections.ranges(cx), [3..3]);
5293
5294 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5295 s.select_ranges([4..4])
5296 });
5297 editor.transpose(&Default::default(), window, cx);
5298 assert_eq!(editor.text(cx), "acbd\ne");
5299 assert_eq!(editor.selections.ranges(cx), [5..5]);
5300
5301 editor.transpose(&Default::default(), window, cx);
5302 assert_eq!(editor.text(cx), "acbde\n");
5303 assert_eq!(editor.selections.ranges(cx), [6..6]);
5304
5305 editor.transpose(&Default::default(), window, cx);
5306 assert_eq!(editor.text(cx), "acbd\ne");
5307 assert_eq!(editor.selections.ranges(cx), [6..6]);
5308
5309 editor
5310 });
5311
5312 _ = cx.add_window(|window, cx| {
5313 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5314 editor.set_style(EditorStyle::default(), window, cx);
5315 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5316 s.select_ranges([1..1, 2..2, 4..4])
5317 });
5318 editor.transpose(&Default::default(), window, cx);
5319 assert_eq!(editor.text(cx), "bacd\ne");
5320 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5321
5322 editor.transpose(&Default::default(), window, cx);
5323 assert_eq!(editor.text(cx), "bcade\n");
5324 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5325
5326 editor.transpose(&Default::default(), window, cx);
5327 assert_eq!(editor.text(cx), "bcda\ne");
5328 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5329
5330 editor.transpose(&Default::default(), window, cx);
5331 assert_eq!(editor.text(cx), "bcade\n");
5332 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5333
5334 editor.transpose(&Default::default(), window, cx);
5335 assert_eq!(editor.text(cx), "bcaed\n");
5336 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5337
5338 editor
5339 });
5340
5341 _ = cx.add_window(|window, cx| {
5342 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5343 editor.set_style(EditorStyle::default(), window, cx);
5344 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5345 s.select_ranges([4..4])
5346 });
5347 editor.transpose(&Default::default(), window, cx);
5348 assert_eq!(editor.text(cx), "🏀🍐✋");
5349 assert_eq!(editor.selections.ranges(cx), [8..8]);
5350
5351 editor.transpose(&Default::default(), window, cx);
5352 assert_eq!(editor.text(cx), "🏀✋🍐");
5353 assert_eq!(editor.selections.ranges(cx), [11..11]);
5354
5355 editor.transpose(&Default::default(), window, cx);
5356 assert_eq!(editor.text(cx), "🏀🍐✋");
5357 assert_eq!(editor.selections.ranges(cx), [11..11]);
5358
5359 editor
5360 });
5361}
5362
5363#[gpui::test]
5364async fn test_rewrap(cx: &mut TestAppContext) {
5365 init_test(cx, |settings| {
5366 settings.languages.0.extend([
5367 (
5368 "Markdown".into(),
5369 LanguageSettingsContent {
5370 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5371 preferred_line_length: Some(40),
5372 ..Default::default()
5373 },
5374 ),
5375 (
5376 "Plain Text".into(),
5377 LanguageSettingsContent {
5378 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5379 preferred_line_length: Some(40),
5380 ..Default::default()
5381 },
5382 ),
5383 (
5384 "C++".into(),
5385 LanguageSettingsContent {
5386 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5387 preferred_line_length: Some(40),
5388 ..Default::default()
5389 },
5390 ),
5391 (
5392 "Python".into(),
5393 LanguageSettingsContent {
5394 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5395 preferred_line_length: Some(40),
5396 ..Default::default()
5397 },
5398 ),
5399 (
5400 "Rust".into(),
5401 LanguageSettingsContent {
5402 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5403 preferred_line_length: Some(40),
5404 ..Default::default()
5405 },
5406 ),
5407 ])
5408 });
5409
5410 let mut cx = EditorTestContext::new(cx).await;
5411
5412 let cpp_language = Arc::new(Language::new(
5413 LanguageConfig {
5414 name: "C++".into(),
5415 line_comments: vec!["// ".into()],
5416 ..LanguageConfig::default()
5417 },
5418 None,
5419 ));
5420 let python_language = Arc::new(Language::new(
5421 LanguageConfig {
5422 name: "Python".into(),
5423 line_comments: vec!["# ".into()],
5424 ..LanguageConfig::default()
5425 },
5426 None,
5427 ));
5428 let markdown_language = Arc::new(Language::new(
5429 LanguageConfig {
5430 name: "Markdown".into(),
5431 rewrap_prefixes: vec![
5432 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5433 regex::Regex::new("[-*+]\\s+").unwrap(),
5434 ],
5435 ..LanguageConfig::default()
5436 },
5437 None,
5438 ));
5439 let rust_language = Arc::new(Language::new(
5440 LanguageConfig {
5441 name: "Rust".into(),
5442 line_comments: vec!["// ".into(), "/// ".into()],
5443 ..LanguageConfig::default()
5444 },
5445 Some(tree_sitter_rust::LANGUAGE.into()),
5446 ));
5447
5448 let plaintext_language = Arc::new(Language::new(
5449 LanguageConfig {
5450 name: "Plain Text".into(),
5451 ..LanguageConfig::default()
5452 },
5453 None,
5454 ));
5455
5456 // Test basic rewrapping of a long line with a cursor
5457 assert_rewrap(
5458 indoc! {"
5459 // ˇThis is a long comment that needs to be wrapped.
5460 "},
5461 indoc! {"
5462 // ˇThis is a long comment that needs to
5463 // be wrapped.
5464 "},
5465 cpp_language.clone(),
5466 &mut cx,
5467 );
5468
5469 // Test rewrapping a full selection
5470 assert_rewrap(
5471 indoc! {"
5472 «// This selected long comment needs to be wrapped.ˇ»"
5473 },
5474 indoc! {"
5475 «// This selected long comment needs to
5476 // be wrapped.ˇ»"
5477 },
5478 cpp_language.clone(),
5479 &mut cx,
5480 );
5481
5482 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5483 assert_rewrap(
5484 indoc! {"
5485 // ˇThis is the first line.
5486 // Thisˇ is the second line.
5487 // This is the thirdˇ line, all part of one paragraph.
5488 "},
5489 indoc! {"
5490 // ˇThis is the first line. Thisˇ is the
5491 // second line. This is the thirdˇ line,
5492 // all part of one paragraph.
5493 "},
5494 cpp_language.clone(),
5495 &mut cx,
5496 );
5497
5498 // Test multiple cursors in different paragraphs trigger separate rewraps
5499 assert_rewrap(
5500 indoc! {"
5501 // ˇThis is the first paragraph, first line.
5502 // ˇThis is the first paragraph, second line.
5503
5504 // ˇThis is the second paragraph, first line.
5505 // ˇThis is the second paragraph, second line.
5506 "},
5507 indoc! {"
5508 // ˇThis is the first paragraph, first
5509 // line. ˇThis is the first paragraph,
5510 // second line.
5511
5512 // ˇThis is the second paragraph, first
5513 // line. ˇThis is the second paragraph,
5514 // second line.
5515 "},
5516 cpp_language.clone(),
5517 &mut cx,
5518 );
5519
5520 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5521 assert_rewrap(
5522 indoc! {"
5523 «// A regular long long comment to be wrapped.
5524 /// A documentation long comment to be wrapped.ˇ»
5525 "},
5526 indoc! {"
5527 «// A regular long long comment to be
5528 // wrapped.
5529 /// A documentation long comment to be
5530 /// wrapped.ˇ»
5531 "},
5532 rust_language.clone(),
5533 &mut cx,
5534 );
5535
5536 // Test that change in indentation level trigger seperate rewraps
5537 assert_rewrap(
5538 indoc! {"
5539 fn foo() {
5540 «// This is a long comment at the base indent.
5541 // This is a long comment at the next indent.ˇ»
5542 }
5543 "},
5544 indoc! {"
5545 fn foo() {
5546 «// This is a long comment at the
5547 // base indent.
5548 // This is a long comment at the
5549 // next indent.ˇ»
5550 }
5551 "},
5552 rust_language.clone(),
5553 &mut cx,
5554 );
5555
5556 // Test that different comment prefix characters (e.g., '#') are handled correctly
5557 assert_rewrap(
5558 indoc! {"
5559 # ˇThis is a long comment using a pound sign.
5560 "},
5561 indoc! {"
5562 # ˇThis is a long comment using a pound
5563 # sign.
5564 "},
5565 python_language.clone(),
5566 &mut cx,
5567 );
5568
5569 // Test rewrapping only affects comments, not code even when selected
5570 assert_rewrap(
5571 indoc! {"
5572 «/// This doc comment is long and should be wrapped.
5573 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5574 "},
5575 indoc! {"
5576 «/// This doc comment is long and should
5577 /// be wrapped.
5578 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5579 "},
5580 rust_language.clone(),
5581 &mut cx,
5582 );
5583
5584 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
5585 assert_rewrap(
5586 indoc! {"
5587 # Header
5588
5589 A long long long line of markdown text to wrap.ˇ
5590 "},
5591 indoc! {"
5592 # Header
5593
5594 A long long long line of markdown text
5595 to wrap.ˇ
5596 "},
5597 markdown_language.clone(),
5598 &mut cx,
5599 );
5600
5601 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
5602 assert_rewrap(
5603 indoc! {"
5604 «1. This is a numbered list item that is very long and needs to be wrapped properly.
5605 2. This is a numbered list item that is very long and needs to be wrapped properly.
5606 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
5607 "},
5608 indoc! {"
5609 «1. This is a numbered list item that is
5610 very long and needs to be wrapped
5611 properly.
5612 2. This is a numbered list item that is
5613 very long and needs to be wrapped
5614 properly.
5615 - This is an unordered list item that is
5616 also very long and should not merge
5617 with the numbered item.ˇ»
5618 "},
5619 markdown_language.clone(),
5620 &mut cx,
5621 );
5622
5623 // Test that rewrapping add indents for rewrapping boundary if not exists already.
5624 assert_rewrap(
5625 indoc! {"
5626 «1. This is a numbered list item that is
5627 very long and needs to be wrapped
5628 properly.
5629 2. This is a numbered list item that is
5630 very long and needs to be wrapped
5631 properly.
5632 - This is an unordered list item that is
5633 also very long and should not merge with
5634 the numbered item.ˇ»
5635 "},
5636 indoc! {"
5637 «1. This is a numbered list item that is
5638 very long and needs to be wrapped
5639 properly.
5640 2. This is a numbered list item that is
5641 very long and needs to be wrapped
5642 properly.
5643 - This is an unordered list item that is
5644 also very long and should not merge
5645 with the numbered item.ˇ»
5646 "},
5647 markdown_language.clone(),
5648 &mut cx,
5649 );
5650
5651 // Test that rewrapping maintain indents even when they already exists.
5652 assert_rewrap(
5653 indoc! {"
5654 «1. This is a numbered list
5655 item that is very long and needs to be wrapped properly.
5656 2. This is a numbered list
5657 item that is very long and needs to be wrapped properly.
5658 - This is an unordered list item that is also very long and
5659 should not merge with the numbered item.ˇ»
5660 "},
5661 indoc! {"
5662 «1. This is a numbered list item that is
5663 very long and needs to be wrapped
5664 properly.
5665 2. This is a numbered list item that is
5666 very long and needs to be wrapped
5667 properly.
5668 - This is an unordered list item that is
5669 also very long and should not merge
5670 with the numbered item.ˇ»
5671 "},
5672 markdown_language.clone(),
5673 &mut cx,
5674 );
5675
5676 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
5677 assert_rewrap(
5678 indoc! {"
5679 ˇThis is a very long line of plain text that will be wrapped.
5680 "},
5681 indoc! {"
5682 ˇThis is a very long line of plain text
5683 that will be wrapped.
5684 "},
5685 plaintext_language.clone(),
5686 &mut cx,
5687 );
5688
5689 // Test that non-commented code acts as a paragraph boundary within a selection
5690 assert_rewrap(
5691 indoc! {"
5692 «// This is the first long comment block to be wrapped.
5693 fn my_func(a: u32);
5694 // This is the second long comment block to be wrapped.ˇ»
5695 "},
5696 indoc! {"
5697 «// This is the first long comment block
5698 // to be wrapped.
5699 fn my_func(a: u32);
5700 // This is the second long comment block
5701 // to be wrapped.ˇ»
5702 "},
5703 rust_language.clone(),
5704 &mut cx,
5705 );
5706
5707 // Test rewrapping multiple selections, including ones with blank lines or tabs
5708 assert_rewrap(
5709 indoc! {"
5710 «ˇThis is a very long line that will be wrapped.
5711
5712 This is another paragraph in the same selection.»
5713
5714 «\tThis is a very long indented line that will be wrapped.ˇ»
5715 "},
5716 indoc! {"
5717 «ˇThis is a very long line that will be
5718 wrapped.
5719
5720 This is another paragraph in the same
5721 selection.»
5722
5723 «\tThis is a very long indented line
5724 \tthat will be wrapped.ˇ»
5725 "},
5726 plaintext_language.clone(),
5727 &mut cx,
5728 );
5729
5730 // Test that an empty comment line acts as a paragraph boundary
5731 assert_rewrap(
5732 indoc! {"
5733 // ˇThis is a long comment that will be wrapped.
5734 //
5735 // And this is another long comment that will also be wrapped.ˇ
5736 "},
5737 indoc! {"
5738 // ˇThis is a long comment that will be
5739 // wrapped.
5740 //
5741 // And this is another long comment that
5742 // will also be wrapped.ˇ
5743 "},
5744 cpp_language,
5745 &mut cx,
5746 );
5747
5748 #[track_caller]
5749 fn assert_rewrap(
5750 unwrapped_text: &str,
5751 wrapped_text: &str,
5752 language: Arc<Language>,
5753 cx: &mut EditorTestContext,
5754 ) {
5755 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5756 cx.set_state(unwrapped_text);
5757 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5758 cx.assert_editor_state(wrapped_text);
5759 }
5760}
5761
5762#[gpui::test]
5763async fn test_hard_wrap(cx: &mut TestAppContext) {
5764 init_test(cx, |_| {});
5765 let mut cx = EditorTestContext::new(cx).await;
5766
5767 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5768 cx.update_editor(|editor, _, cx| {
5769 editor.set_hard_wrap(Some(14), cx);
5770 });
5771
5772 cx.set_state(indoc!(
5773 "
5774 one two three ˇ
5775 "
5776 ));
5777 cx.simulate_input("four");
5778 cx.run_until_parked();
5779
5780 cx.assert_editor_state(indoc!(
5781 "
5782 one two three
5783 fourˇ
5784 "
5785 ));
5786
5787 cx.update_editor(|editor, window, cx| {
5788 editor.newline(&Default::default(), window, cx);
5789 });
5790 cx.run_until_parked();
5791 cx.assert_editor_state(indoc!(
5792 "
5793 one two three
5794 four
5795 ˇ
5796 "
5797 ));
5798
5799 cx.simulate_input("five");
5800 cx.run_until_parked();
5801 cx.assert_editor_state(indoc!(
5802 "
5803 one two three
5804 four
5805 fiveˇ
5806 "
5807 ));
5808
5809 cx.update_editor(|editor, window, cx| {
5810 editor.newline(&Default::default(), window, cx);
5811 });
5812 cx.run_until_parked();
5813 cx.simulate_input("# ");
5814 cx.run_until_parked();
5815 cx.assert_editor_state(indoc!(
5816 "
5817 one two three
5818 four
5819 five
5820 # ˇ
5821 "
5822 ));
5823
5824 cx.update_editor(|editor, window, cx| {
5825 editor.newline(&Default::default(), window, cx);
5826 });
5827 cx.run_until_parked();
5828 cx.assert_editor_state(indoc!(
5829 "
5830 one two three
5831 four
5832 five
5833 #\x20
5834 #ˇ
5835 "
5836 ));
5837
5838 cx.simulate_input(" 6");
5839 cx.run_until_parked();
5840 cx.assert_editor_state(indoc!(
5841 "
5842 one two three
5843 four
5844 five
5845 #
5846 # 6ˇ
5847 "
5848 ));
5849}
5850
5851#[gpui::test]
5852async fn test_clipboard(cx: &mut TestAppContext) {
5853 init_test(cx, |_| {});
5854
5855 let mut cx = EditorTestContext::new(cx).await;
5856
5857 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5858 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5859 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5860
5861 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5862 cx.set_state("two ˇfour ˇsix ˇ");
5863 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5864 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5865
5866 // Paste again but with only two cursors. Since the number of cursors doesn't
5867 // match the number of slices in the clipboard, the entire clipboard text
5868 // is pasted at each cursor.
5869 cx.set_state("ˇtwo one✅ four three six five ˇ");
5870 cx.update_editor(|e, window, cx| {
5871 e.handle_input("( ", window, cx);
5872 e.paste(&Paste, window, cx);
5873 e.handle_input(") ", window, cx);
5874 });
5875 cx.assert_editor_state(
5876 &([
5877 "( one✅ ",
5878 "three ",
5879 "five ) ˇtwo one✅ four three six five ( one✅ ",
5880 "three ",
5881 "five ) ˇ",
5882 ]
5883 .join("\n")),
5884 );
5885
5886 // Cut with three selections, one of which is full-line.
5887 cx.set_state(indoc! {"
5888 1«2ˇ»3
5889 4ˇ567
5890 «8ˇ»9"});
5891 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5892 cx.assert_editor_state(indoc! {"
5893 1ˇ3
5894 ˇ9"});
5895
5896 // Paste with three selections, noticing how the copied selection that was full-line
5897 // gets inserted before the second cursor.
5898 cx.set_state(indoc! {"
5899 1ˇ3
5900 9ˇ
5901 «oˇ»ne"});
5902 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5903 cx.assert_editor_state(indoc! {"
5904 12ˇ3
5905 4567
5906 9ˇ
5907 8ˇne"});
5908
5909 // Copy with a single cursor only, which writes the whole line into the clipboard.
5910 cx.set_state(indoc! {"
5911 The quick brown
5912 fox juˇmps over
5913 the lazy dog"});
5914 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5915 assert_eq!(
5916 cx.read_from_clipboard()
5917 .and_then(|item| item.text().as_deref().map(str::to_string)),
5918 Some("fox jumps over\n".to_string())
5919 );
5920
5921 // Paste with three selections, noticing how the copied full-line selection is inserted
5922 // before the empty selections but replaces the selection that is non-empty.
5923 cx.set_state(indoc! {"
5924 Tˇhe quick brown
5925 «foˇ»x jumps over
5926 tˇhe lazy dog"});
5927 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5928 cx.assert_editor_state(indoc! {"
5929 fox jumps over
5930 Tˇhe quick brown
5931 fox jumps over
5932 ˇx jumps over
5933 fox jumps over
5934 tˇhe lazy dog"});
5935}
5936
5937#[gpui::test]
5938async fn test_copy_trim(cx: &mut TestAppContext) {
5939 init_test(cx, |_| {});
5940
5941 let mut cx = EditorTestContext::new(cx).await;
5942 cx.set_state(
5943 r#" «for selection in selections.iter() {
5944 let mut start = selection.start;
5945 let mut end = selection.end;
5946 let is_entire_line = selection.is_empty();
5947 if is_entire_line {
5948 start = Point::new(start.row, 0);ˇ»
5949 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5950 }
5951 "#,
5952 );
5953 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5954 assert_eq!(
5955 cx.read_from_clipboard()
5956 .and_then(|item| item.text().as_deref().map(str::to_string)),
5957 Some(
5958 "for selection in selections.iter() {
5959 let mut start = selection.start;
5960 let mut end = selection.end;
5961 let is_entire_line = selection.is_empty();
5962 if is_entire_line {
5963 start = Point::new(start.row, 0);"
5964 .to_string()
5965 ),
5966 "Regular copying preserves all indentation selected",
5967 );
5968 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5969 assert_eq!(
5970 cx.read_from_clipboard()
5971 .and_then(|item| item.text().as_deref().map(str::to_string)),
5972 Some(
5973 "for selection in selections.iter() {
5974let mut start = selection.start;
5975let mut end = selection.end;
5976let is_entire_line = selection.is_empty();
5977if is_entire_line {
5978 start = Point::new(start.row, 0);"
5979 .to_string()
5980 ),
5981 "Copying with stripping should strip all leading whitespaces"
5982 );
5983
5984 cx.set_state(
5985 r#" « for selection in selections.iter() {
5986 let mut start = selection.start;
5987 let mut end = selection.end;
5988 let is_entire_line = selection.is_empty();
5989 if is_entire_line {
5990 start = Point::new(start.row, 0);ˇ»
5991 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5992 }
5993 "#,
5994 );
5995 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5996 assert_eq!(
5997 cx.read_from_clipboard()
5998 .and_then(|item| item.text().as_deref().map(str::to_string)),
5999 Some(
6000 " for selection in selections.iter() {
6001 let mut start = selection.start;
6002 let mut end = selection.end;
6003 let is_entire_line = selection.is_empty();
6004 if is_entire_line {
6005 start = Point::new(start.row, 0);"
6006 .to_string()
6007 ),
6008 "Regular copying preserves all indentation selected",
6009 );
6010 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6011 assert_eq!(
6012 cx.read_from_clipboard()
6013 .and_then(|item| item.text().as_deref().map(str::to_string)),
6014 Some(
6015 "for selection in selections.iter() {
6016let mut start = selection.start;
6017let mut end = selection.end;
6018let is_entire_line = selection.is_empty();
6019if is_entire_line {
6020 start = Point::new(start.row, 0);"
6021 .to_string()
6022 ),
6023 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
6024 );
6025
6026 cx.set_state(
6027 r#" «ˇ for selection in selections.iter() {
6028 let mut start = selection.start;
6029 let mut end = selection.end;
6030 let is_entire_line = selection.is_empty();
6031 if is_entire_line {
6032 start = Point::new(start.row, 0);»
6033 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6034 }
6035 "#,
6036 );
6037 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6038 assert_eq!(
6039 cx.read_from_clipboard()
6040 .and_then(|item| item.text().as_deref().map(str::to_string)),
6041 Some(
6042 " for selection in selections.iter() {
6043 let mut start = selection.start;
6044 let mut end = selection.end;
6045 let is_entire_line = selection.is_empty();
6046 if is_entire_line {
6047 start = Point::new(start.row, 0);"
6048 .to_string()
6049 ),
6050 "Regular copying for reverse selection works the same",
6051 );
6052 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6053 assert_eq!(
6054 cx.read_from_clipboard()
6055 .and_then(|item| item.text().as_deref().map(str::to_string)),
6056 Some(
6057 "for selection in selections.iter() {
6058let mut start = selection.start;
6059let mut end = selection.end;
6060let is_entire_line = selection.is_empty();
6061if is_entire_line {
6062 start = Point::new(start.row, 0);"
6063 .to_string()
6064 ),
6065 "Copying with stripping for reverse selection works the same"
6066 );
6067
6068 cx.set_state(
6069 r#" for selection «in selections.iter() {
6070 let mut start = selection.start;
6071 let mut end = selection.end;
6072 let is_entire_line = selection.is_empty();
6073 if is_entire_line {
6074 start = Point::new(start.row, 0);ˇ»
6075 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6076 }
6077 "#,
6078 );
6079 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6080 assert_eq!(
6081 cx.read_from_clipboard()
6082 .and_then(|item| item.text().as_deref().map(str::to_string)),
6083 Some(
6084 "in selections.iter() {
6085 let mut start = selection.start;
6086 let mut end = selection.end;
6087 let is_entire_line = selection.is_empty();
6088 if is_entire_line {
6089 start = Point::new(start.row, 0);"
6090 .to_string()
6091 ),
6092 "When selecting past the indent, the copying works as usual",
6093 );
6094 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6095 assert_eq!(
6096 cx.read_from_clipboard()
6097 .and_then(|item| item.text().as_deref().map(str::to_string)),
6098 Some(
6099 "in selections.iter() {
6100 let mut start = selection.start;
6101 let mut end = selection.end;
6102 let is_entire_line = selection.is_empty();
6103 if is_entire_line {
6104 start = Point::new(start.row, 0);"
6105 .to_string()
6106 ),
6107 "When selecting past the indent, nothing is trimmed"
6108 );
6109
6110 cx.set_state(
6111 r#" «for selection in selections.iter() {
6112 let mut start = selection.start;
6113
6114 let mut end = selection.end;
6115 let is_entire_line = selection.is_empty();
6116 if is_entire_line {
6117 start = Point::new(start.row, 0);
6118ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
6119 }
6120 "#,
6121 );
6122 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6123 assert_eq!(
6124 cx.read_from_clipboard()
6125 .and_then(|item| item.text().as_deref().map(str::to_string)),
6126 Some(
6127 "for selection in selections.iter() {
6128let mut start = selection.start;
6129
6130let mut end = selection.end;
6131let is_entire_line = selection.is_empty();
6132if is_entire_line {
6133 start = Point::new(start.row, 0);
6134"
6135 .to_string()
6136 ),
6137 "Copying with stripping should ignore empty lines"
6138 );
6139}
6140
6141#[gpui::test]
6142async fn test_paste_multiline(cx: &mut TestAppContext) {
6143 init_test(cx, |_| {});
6144
6145 let mut cx = EditorTestContext::new(cx).await;
6146 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6147
6148 // Cut an indented block, without the leading whitespace.
6149 cx.set_state(indoc! {"
6150 const a: B = (
6151 c(),
6152 «d(
6153 e,
6154 f
6155 )ˇ»
6156 );
6157 "});
6158 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6159 cx.assert_editor_state(indoc! {"
6160 const a: B = (
6161 c(),
6162 ˇ
6163 );
6164 "});
6165
6166 // Paste it at the same position.
6167 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6168 cx.assert_editor_state(indoc! {"
6169 const a: B = (
6170 c(),
6171 d(
6172 e,
6173 f
6174 )ˇ
6175 );
6176 "});
6177
6178 // Paste it at a line with a lower indent level.
6179 cx.set_state(indoc! {"
6180 ˇ
6181 const a: B = (
6182 c(),
6183 );
6184 "});
6185 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6186 cx.assert_editor_state(indoc! {"
6187 d(
6188 e,
6189 f
6190 )ˇ
6191 const a: B = (
6192 c(),
6193 );
6194 "});
6195
6196 // Cut an indented block, with the leading whitespace.
6197 cx.set_state(indoc! {"
6198 const a: B = (
6199 c(),
6200 « d(
6201 e,
6202 f
6203 )
6204 ˇ»);
6205 "});
6206 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6207 cx.assert_editor_state(indoc! {"
6208 const a: B = (
6209 c(),
6210 ˇ);
6211 "});
6212
6213 // Paste it at the same position.
6214 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6215 cx.assert_editor_state(indoc! {"
6216 const a: B = (
6217 c(),
6218 d(
6219 e,
6220 f
6221 )
6222 ˇ);
6223 "});
6224
6225 // Paste it at a line with a higher indent level.
6226 cx.set_state(indoc! {"
6227 const a: B = (
6228 c(),
6229 d(
6230 e,
6231 fˇ
6232 )
6233 );
6234 "});
6235 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6236 cx.assert_editor_state(indoc! {"
6237 const a: B = (
6238 c(),
6239 d(
6240 e,
6241 f d(
6242 e,
6243 f
6244 )
6245 ˇ
6246 )
6247 );
6248 "});
6249
6250 // Copy an indented block, starting mid-line
6251 cx.set_state(indoc! {"
6252 const a: B = (
6253 c(),
6254 somethin«g(
6255 e,
6256 f
6257 )ˇ»
6258 );
6259 "});
6260 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6261
6262 // Paste it on a line with a lower indent level
6263 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
6264 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6265 cx.assert_editor_state(indoc! {"
6266 const a: B = (
6267 c(),
6268 something(
6269 e,
6270 f
6271 )
6272 );
6273 g(
6274 e,
6275 f
6276 )ˇ"});
6277}
6278
6279#[gpui::test]
6280async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
6281 init_test(cx, |_| {});
6282
6283 cx.write_to_clipboard(ClipboardItem::new_string(
6284 " d(\n e\n );\n".into(),
6285 ));
6286
6287 let mut cx = EditorTestContext::new(cx).await;
6288 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6289
6290 cx.set_state(indoc! {"
6291 fn a() {
6292 b();
6293 if c() {
6294 ˇ
6295 }
6296 }
6297 "});
6298
6299 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6300 cx.assert_editor_state(indoc! {"
6301 fn a() {
6302 b();
6303 if c() {
6304 d(
6305 e
6306 );
6307 ˇ
6308 }
6309 }
6310 "});
6311
6312 cx.set_state(indoc! {"
6313 fn a() {
6314 b();
6315 ˇ
6316 }
6317 "});
6318
6319 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6320 cx.assert_editor_state(indoc! {"
6321 fn a() {
6322 b();
6323 d(
6324 e
6325 );
6326 ˇ
6327 }
6328 "});
6329}
6330
6331#[gpui::test]
6332fn test_select_all(cx: &mut TestAppContext) {
6333 init_test(cx, |_| {});
6334
6335 let editor = cx.add_window(|window, cx| {
6336 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6337 build_editor(buffer, window, cx)
6338 });
6339 _ = editor.update(cx, |editor, window, cx| {
6340 editor.select_all(&SelectAll, window, cx);
6341 assert_eq!(
6342 editor.selections.display_ranges(cx),
6343 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6344 );
6345 });
6346}
6347
6348#[gpui::test]
6349fn test_select_line(cx: &mut TestAppContext) {
6350 init_test(cx, |_| {});
6351
6352 let editor = cx.add_window(|window, cx| {
6353 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6354 build_editor(buffer, window, cx)
6355 });
6356 _ = editor.update(cx, |editor, window, cx| {
6357 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6358 s.select_display_ranges([
6359 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6360 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6361 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6362 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6363 ])
6364 });
6365 editor.select_line(&SelectLine, window, cx);
6366 assert_eq!(
6367 editor.selections.display_ranges(cx),
6368 vec![
6369 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6370 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6371 ]
6372 );
6373 });
6374
6375 _ = editor.update(cx, |editor, window, cx| {
6376 editor.select_line(&SelectLine, window, cx);
6377 assert_eq!(
6378 editor.selections.display_ranges(cx),
6379 vec![
6380 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6381 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6382 ]
6383 );
6384 });
6385
6386 _ = editor.update(cx, |editor, window, cx| {
6387 editor.select_line(&SelectLine, window, cx);
6388 assert_eq!(
6389 editor.selections.display_ranges(cx),
6390 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6391 );
6392 });
6393}
6394
6395#[gpui::test]
6396async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6397 init_test(cx, |_| {});
6398 let mut cx = EditorTestContext::new(cx).await;
6399
6400 #[track_caller]
6401 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6402 cx.set_state(initial_state);
6403 cx.update_editor(|e, window, cx| {
6404 e.split_selection_into_lines(&Default::default(), window, cx)
6405 });
6406 cx.assert_editor_state(expected_state);
6407 }
6408
6409 // Selection starts and ends at the middle of lines, left-to-right
6410 test(
6411 &mut cx,
6412 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6413 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6414 );
6415 // Same thing, right-to-left
6416 test(
6417 &mut cx,
6418 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6419 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6420 );
6421
6422 // Whole buffer, left-to-right, last line *doesn't* end with newline
6423 test(
6424 &mut cx,
6425 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6426 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6427 );
6428 // Same thing, right-to-left
6429 test(
6430 &mut cx,
6431 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6432 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6433 );
6434
6435 // Whole buffer, left-to-right, last line ends with newline
6436 test(
6437 &mut cx,
6438 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6439 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6440 );
6441 // Same thing, right-to-left
6442 test(
6443 &mut cx,
6444 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6445 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6446 );
6447
6448 // Starts at the end of a line, ends at the start of another
6449 test(
6450 &mut cx,
6451 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6452 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6453 );
6454}
6455
6456#[gpui::test]
6457async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6458 init_test(cx, |_| {});
6459
6460 let editor = cx.add_window(|window, cx| {
6461 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6462 build_editor(buffer, window, cx)
6463 });
6464
6465 // setup
6466 _ = editor.update(cx, |editor, window, cx| {
6467 editor.fold_creases(
6468 vec![
6469 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6470 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6471 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6472 ],
6473 true,
6474 window,
6475 cx,
6476 );
6477 assert_eq!(
6478 editor.display_text(cx),
6479 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6480 );
6481 });
6482
6483 _ = editor.update(cx, |editor, window, cx| {
6484 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6485 s.select_display_ranges([
6486 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6487 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6488 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6489 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6490 ])
6491 });
6492 editor.split_selection_into_lines(&Default::default(), window, cx);
6493 assert_eq!(
6494 editor.display_text(cx),
6495 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6496 );
6497 });
6498 EditorTestContext::for_editor(editor, cx)
6499 .await
6500 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6501
6502 _ = editor.update(cx, |editor, window, cx| {
6503 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6504 s.select_display_ranges([
6505 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6506 ])
6507 });
6508 editor.split_selection_into_lines(&Default::default(), window, cx);
6509 assert_eq!(
6510 editor.display_text(cx),
6511 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6512 );
6513 assert_eq!(
6514 editor.selections.display_ranges(cx),
6515 [
6516 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6517 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6518 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6519 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6520 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6521 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6522 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6523 ]
6524 );
6525 });
6526 EditorTestContext::for_editor(editor, cx)
6527 .await
6528 .assert_editor_state(
6529 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6530 );
6531}
6532
6533#[gpui::test]
6534async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6535 init_test(cx, |_| {});
6536
6537 let mut cx = EditorTestContext::new(cx).await;
6538
6539 cx.set_state(indoc!(
6540 r#"abc
6541 defˇghi
6542
6543 jk
6544 nlmo
6545 "#
6546 ));
6547
6548 cx.update_editor(|editor, window, cx| {
6549 editor.add_selection_above(&Default::default(), window, cx);
6550 });
6551
6552 cx.assert_editor_state(indoc!(
6553 r#"abcˇ
6554 defˇghi
6555
6556 jk
6557 nlmo
6558 "#
6559 ));
6560
6561 cx.update_editor(|editor, window, cx| {
6562 editor.add_selection_above(&Default::default(), window, cx);
6563 });
6564
6565 cx.assert_editor_state(indoc!(
6566 r#"abcˇ
6567 defˇghi
6568
6569 jk
6570 nlmo
6571 "#
6572 ));
6573
6574 cx.update_editor(|editor, window, cx| {
6575 editor.add_selection_below(&Default::default(), window, cx);
6576 });
6577
6578 cx.assert_editor_state(indoc!(
6579 r#"abc
6580 defˇghi
6581
6582 jk
6583 nlmo
6584 "#
6585 ));
6586
6587 cx.update_editor(|editor, window, cx| {
6588 editor.undo_selection(&Default::default(), window, cx);
6589 });
6590
6591 cx.assert_editor_state(indoc!(
6592 r#"abcˇ
6593 defˇghi
6594
6595 jk
6596 nlmo
6597 "#
6598 ));
6599
6600 cx.update_editor(|editor, window, cx| {
6601 editor.redo_selection(&Default::default(), window, cx);
6602 });
6603
6604 cx.assert_editor_state(indoc!(
6605 r#"abc
6606 defˇghi
6607
6608 jk
6609 nlmo
6610 "#
6611 ));
6612
6613 cx.update_editor(|editor, window, cx| {
6614 editor.add_selection_below(&Default::default(), window, cx);
6615 });
6616
6617 cx.assert_editor_state(indoc!(
6618 r#"abc
6619 defˇghi
6620 ˇ
6621 jk
6622 nlmo
6623 "#
6624 ));
6625
6626 cx.update_editor(|editor, window, cx| {
6627 editor.add_selection_below(&Default::default(), window, cx);
6628 });
6629
6630 cx.assert_editor_state(indoc!(
6631 r#"abc
6632 defˇghi
6633 ˇ
6634 jkˇ
6635 nlmo
6636 "#
6637 ));
6638
6639 cx.update_editor(|editor, window, cx| {
6640 editor.add_selection_below(&Default::default(), window, cx);
6641 });
6642
6643 cx.assert_editor_state(indoc!(
6644 r#"abc
6645 defˇghi
6646 ˇ
6647 jkˇ
6648 nlmˇo
6649 "#
6650 ));
6651
6652 cx.update_editor(|editor, window, cx| {
6653 editor.add_selection_below(&Default::default(), window, cx);
6654 });
6655
6656 cx.assert_editor_state(indoc!(
6657 r#"abc
6658 defˇghi
6659 ˇ
6660 jkˇ
6661 nlmˇo
6662 ˇ"#
6663 ));
6664
6665 // change selections
6666 cx.set_state(indoc!(
6667 r#"abc
6668 def«ˇg»hi
6669
6670 jk
6671 nlmo
6672 "#
6673 ));
6674
6675 cx.update_editor(|editor, window, cx| {
6676 editor.add_selection_below(&Default::default(), window, cx);
6677 });
6678
6679 cx.assert_editor_state(indoc!(
6680 r#"abc
6681 def«ˇg»hi
6682
6683 jk
6684 nlm«ˇo»
6685 "#
6686 ));
6687
6688 cx.update_editor(|editor, window, cx| {
6689 editor.add_selection_below(&Default::default(), window, cx);
6690 });
6691
6692 cx.assert_editor_state(indoc!(
6693 r#"abc
6694 def«ˇg»hi
6695
6696 jk
6697 nlm«ˇo»
6698 "#
6699 ));
6700
6701 cx.update_editor(|editor, window, cx| {
6702 editor.add_selection_above(&Default::default(), window, cx);
6703 });
6704
6705 cx.assert_editor_state(indoc!(
6706 r#"abc
6707 def«ˇg»hi
6708
6709 jk
6710 nlmo
6711 "#
6712 ));
6713
6714 cx.update_editor(|editor, window, cx| {
6715 editor.add_selection_above(&Default::default(), window, cx);
6716 });
6717
6718 cx.assert_editor_state(indoc!(
6719 r#"abc
6720 def«ˇg»hi
6721
6722 jk
6723 nlmo
6724 "#
6725 ));
6726
6727 // Change selections again
6728 cx.set_state(indoc!(
6729 r#"a«bc
6730 defgˇ»hi
6731
6732 jk
6733 nlmo
6734 "#
6735 ));
6736
6737 cx.update_editor(|editor, window, cx| {
6738 editor.add_selection_below(&Default::default(), window, cx);
6739 });
6740
6741 cx.assert_editor_state(indoc!(
6742 r#"a«bcˇ»
6743 d«efgˇ»hi
6744
6745 j«kˇ»
6746 nlmo
6747 "#
6748 ));
6749
6750 cx.update_editor(|editor, window, cx| {
6751 editor.add_selection_below(&Default::default(), window, cx);
6752 });
6753 cx.assert_editor_state(indoc!(
6754 r#"a«bcˇ»
6755 d«efgˇ»hi
6756
6757 j«kˇ»
6758 n«lmoˇ»
6759 "#
6760 ));
6761 cx.update_editor(|editor, window, cx| {
6762 editor.add_selection_above(&Default::default(), window, cx);
6763 });
6764
6765 cx.assert_editor_state(indoc!(
6766 r#"a«bcˇ»
6767 d«efgˇ»hi
6768
6769 j«kˇ»
6770 nlmo
6771 "#
6772 ));
6773
6774 // Change selections again
6775 cx.set_state(indoc!(
6776 r#"abc
6777 d«ˇefghi
6778
6779 jk
6780 nlm»o
6781 "#
6782 ));
6783
6784 cx.update_editor(|editor, window, cx| {
6785 editor.add_selection_above(&Default::default(), window, cx);
6786 });
6787
6788 cx.assert_editor_state(indoc!(
6789 r#"a«ˇbc»
6790 d«ˇef»ghi
6791
6792 j«ˇk»
6793 n«ˇlm»o
6794 "#
6795 ));
6796
6797 cx.update_editor(|editor, window, cx| {
6798 editor.add_selection_below(&Default::default(), window, cx);
6799 });
6800
6801 cx.assert_editor_state(indoc!(
6802 r#"abc
6803 d«ˇef»ghi
6804
6805 j«ˇk»
6806 n«ˇlm»o
6807 "#
6808 ));
6809}
6810
6811#[gpui::test]
6812async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6813 init_test(cx, |_| {});
6814 let mut cx = EditorTestContext::new(cx).await;
6815
6816 cx.set_state(indoc!(
6817 r#"line onˇe
6818 liˇne two
6819 line three
6820 line four"#
6821 ));
6822
6823 cx.update_editor(|editor, window, cx| {
6824 editor.add_selection_below(&Default::default(), window, cx);
6825 });
6826
6827 // test multiple cursors expand in the same direction
6828 cx.assert_editor_state(indoc!(
6829 r#"line onˇe
6830 liˇne twˇo
6831 liˇne three
6832 line four"#
6833 ));
6834
6835 cx.update_editor(|editor, window, cx| {
6836 editor.add_selection_below(&Default::default(), window, cx);
6837 });
6838
6839 cx.update_editor(|editor, window, cx| {
6840 editor.add_selection_below(&Default::default(), window, cx);
6841 });
6842
6843 // test multiple cursors expand below overflow
6844 cx.assert_editor_state(indoc!(
6845 r#"line onˇe
6846 liˇne twˇo
6847 liˇne thˇree
6848 liˇne foˇur"#
6849 ));
6850
6851 cx.update_editor(|editor, window, cx| {
6852 editor.add_selection_above(&Default::default(), window, cx);
6853 });
6854
6855 // test multiple cursors retrieves back correctly
6856 cx.assert_editor_state(indoc!(
6857 r#"line onˇe
6858 liˇne twˇo
6859 liˇne thˇree
6860 line four"#
6861 ));
6862
6863 cx.update_editor(|editor, window, cx| {
6864 editor.add_selection_above(&Default::default(), window, cx);
6865 });
6866
6867 cx.update_editor(|editor, window, cx| {
6868 editor.add_selection_above(&Default::default(), window, cx);
6869 });
6870
6871 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6872 cx.assert_editor_state(indoc!(
6873 r#"liˇne onˇe
6874 liˇne two
6875 line three
6876 line four"#
6877 ));
6878
6879 cx.update_editor(|editor, window, cx| {
6880 editor.undo_selection(&Default::default(), window, cx);
6881 });
6882
6883 // test undo
6884 cx.assert_editor_state(indoc!(
6885 r#"line onˇe
6886 liˇne twˇo
6887 line three
6888 line four"#
6889 ));
6890
6891 cx.update_editor(|editor, window, cx| {
6892 editor.redo_selection(&Default::default(), window, cx);
6893 });
6894
6895 // test redo
6896 cx.assert_editor_state(indoc!(
6897 r#"liˇne onˇe
6898 liˇne two
6899 line three
6900 line four"#
6901 ));
6902
6903 cx.set_state(indoc!(
6904 r#"abcd
6905 ef«ghˇ»
6906 ijkl
6907 «mˇ»nop"#
6908 ));
6909
6910 cx.update_editor(|editor, window, cx| {
6911 editor.add_selection_above(&Default::default(), window, cx);
6912 });
6913
6914 // test multiple selections expand in the same direction
6915 cx.assert_editor_state(indoc!(
6916 r#"ab«cdˇ»
6917 ef«ghˇ»
6918 «iˇ»jkl
6919 «mˇ»nop"#
6920 ));
6921
6922 cx.update_editor(|editor, window, cx| {
6923 editor.add_selection_above(&Default::default(), window, cx);
6924 });
6925
6926 // test multiple selection upward overflow
6927 cx.assert_editor_state(indoc!(
6928 r#"ab«cdˇ»
6929 «eˇ»f«ghˇ»
6930 «iˇ»jkl
6931 «mˇ»nop"#
6932 ));
6933
6934 cx.update_editor(|editor, window, cx| {
6935 editor.add_selection_below(&Default::default(), window, cx);
6936 });
6937
6938 // test multiple selection retrieves back correctly
6939 cx.assert_editor_state(indoc!(
6940 r#"abcd
6941 ef«ghˇ»
6942 «iˇ»jkl
6943 «mˇ»nop"#
6944 ));
6945
6946 cx.update_editor(|editor, window, cx| {
6947 editor.add_selection_below(&Default::default(), window, cx);
6948 });
6949
6950 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6951 cx.assert_editor_state(indoc!(
6952 r#"abcd
6953 ef«ghˇ»
6954 ij«klˇ»
6955 «mˇ»nop"#
6956 ));
6957
6958 cx.update_editor(|editor, window, cx| {
6959 editor.undo_selection(&Default::default(), window, cx);
6960 });
6961
6962 // test undo
6963 cx.assert_editor_state(indoc!(
6964 r#"abcd
6965 ef«ghˇ»
6966 «iˇ»jkl
6967 «mˇ»nop"#
6968 ));
6969
6970 cx.update_editor(|editor, window, cx| {
6971 editor.redo_selection(&Default::default(), window, cx);
6972 });
6973
6974 // test redo
6975 cx.assert_editor_state(indoc!(
6976 r#"abcd
6977 ef«ghˇ»
6978 ij«klˇ»
6979 «mˇ»nop"#
6980 ));
6981}
6982
6983#[gpui::test]
6984async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6985 init_test(cx, |_| {});
6986 let mut cx = EditorTestContext::new(cx).await;
6987
6988 cx.set_state(indoc!(
6989 r#"line onˇe
6990 liˇne two
6991 line three
6992 line four"#
6993 ));
6994
6995 cx.update_editor(|editor, window, cx| {
6996 editor.add_selection_below(&Default::default(), window, cx);
6997 editor.add_selection_below(&Default::default(), window, cx);
6998 editor.add_selection_below(&Default::default(), window, cx);
6999 });
7000
7001 // initial state with two multi cursor groups
7002 cx.assert_editor_state(indoc!(
7003 r#"line onˇe
7004 liˇne twˇo
7005 liˇne thˇree
7006 liˇne foˇur"#
7007 ));
7008
7009 // add single cursor in middle - simulate opt click
7010 cx.update_editor(|editor, window, cx| {
7011 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
7012 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7013 editor.end_selection(window, cx);
7014 });
7015
7016 cx.assert_editor_state(indoc!(
7017 r#"line onˇe
7018 liˇne twˇo
7019 liˇneˇ thˇree
7020 liˇne foˇur"#
7021 ));
7022
7023 cx.update_editor(|editor, window, cx| {
7024 editor.add_selection_above(&Default::default(), window, cx);
7025 });
7026
7027 // test new added selection expands above and existing selection shrinks
7028 cx.assert_editor_state(indoc!(
7029 r#"line onˇe
7030 liˇneˇ twˇo
7031 liˇneˇ thˇree
7032 line four"#
7033 ));
7034
7035 cx.update_editor(|editor, window, cx| {
7036 editor.add_selection_above(&Default::default(), window, cx);
7037 });
7038
7039 // test new added selection expands above and existing selection shrinks
7040 cx.assert_editor_state(indoc!(
7041 r#"lineˇ onˇe
7042 liˇneˇ twˇo
7043 lineˇ three
7044 line four"#
7045 ));
7046
7047 // intial state with two selection groups
7048 cx.set_state(indoc!(
7049 r#"abcd
7050 ef«ghˇ»
7051 ijkl
7052 «mˇ»nop"#
7053 ));
7054
7055 cx.update_editor(|editor, window, cx| {
7056 editor.add_selection_above(&Default::default(), window, cx);
7057 editor.add_selection_above(&Default::default(), window, cx);
7058 });
7059
7060 cx.assert_editor_state(indoc!(
7061 r#"ab«cdˇ»
7062 «eˇ»f«ghˇ»
7063 «iˇ»jkl
7064 «mˇ»nop"#
7065 ));
7066
7067 // add single selection in middle - simulate opt drag
7068 cx.update_editor(|editor, window, cx| {
7069 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
7070 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7071 editor.update_selection(
7072 DisplayPoint::new(DisplayRow(2), 4),
7073 0,
7074 gpui::Point::<f32>::default(),
7075 window,
7076 cx,
7077 );
7078 editor.end_selection(window, cx);
7079 });
7080
7081 cx.assert_editor_state(indoc!(
7082 r#"ab«cdˇ»
7083 «eˇ»f«ghˇ»
7084 «iˇ»jk«lˇ»
7085 «mˇ»nop"#
7086 ));
7087
7088 cx.update_editor(|editor, window, cx| {
7089 editor.add_selection_below(&Default::default(), window, cx);
7090 });
7091
7092 // test new added selection expands below, others shrinks from above
7093 cx.assert_editor_state(indoc!(
7094 r#"abcd
7095 ef«ghˇ»
7096 «iˇ»jk«lˇ»
7097 «mˇ»no«pˇ»"#
7098 ));
7099}
7100
7101#[gpui::test]
7102async fn test_select_next(cx: &mut TestAppContext) {
7103 init_test(cx, |_| {});
7104
7105 let mut cx = EditorTestContext::new(cx).await;
7106 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7107
7108 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7109 .unwrap();
7110 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7111
7112 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7113 .unwrap();
7114 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7115
7116 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7117 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7118
7119 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7120 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7121
7122 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7123 .unwrap();
7124 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7125
7126 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7127 .unwrap();
7128 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7129
7130 // Test selection direction should be preserved
7131 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7132
7133 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7134 .unwrap();
7135 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
7136}
7137
7138#[gpui::test]
7139async fn test_select_all_matches(cx: &mut TestAppContext) {
7140 init_test(cx, |_| {});
7141
7142 let mut cx = EditorTestContext::new(cx).await;
7143
7144 // Test caret-only selections
7145 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7146 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7147 .unwrap();
7148 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7149
7150 // Test left-to-right selections
7151 cx.set_state("abc\n«abcˇ»\nabc");
7152 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7153 .unwrap();
7154 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
7155
7156 // Test right-to-left selections
7157 cx.set_state("abc\n«ˇabc»\nabc");
7158 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7159 .unwrap();
7160 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
7161
7162 // Test selecting whitespace with caret selection
7163 cx.set_state("abc\nˇ abc\nabc");
7164 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7165 .unwrap();
7166 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7167
7168 // Test selecting whitespace with left-to-right selection
7169 cx.set_state("abc\n«ˇ »abc\nabc");
7170 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7171 .unwrap();
7172 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
7173
7174 // Test no matches with right-to-left selection
7175 cx.set_state("abc\n« ˇ»abc\nabc");
7176 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7177 .unwrap();
7178 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7179
7180 // Test with a single word and clip_at_line_ends=true (#29823)
7181 cx.set_state("aˇbc");
7182 cx.update_editor(|e, window, cx| {
7183 e.set_clip_at_line_ends(true, cx);
7184 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
7185 e.set_clip_at_line_ends(false, cx);
7186 });
7187 cx.assert_editor_state("«abcˇ»");
7188}
7189
7190#[gpui::test]
7191async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
7192 init_test(cx, |_| {});
7193
7194 let mut cx = EditorTestContext::new(cx).await;
7195
7196 let large_body_1 = "\nd".repeat(200);
7197 let large_body_2 = "\ne".repeat(200);
7198
7199 cx.set_state(&format!(
7200 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
7201 ));
7202 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
7203 let scroll_position = editor.scroll_position(cx);
7204 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
7205 scroll_position
7206 });
7207
7208 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7209 .unwrap();
7210 cx.assert_editor_state(&format!(
7211 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
7212 ));
7213 let scroll_position_after_selection =
7214 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
7215 assert_eq!(
7216 initial_scroll_position, scroll_position_after_selection,
7217 "Scroll position should not change after selecting all matches"
7218 );
7219}
7220
7221#[gpui::test]
7222async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
7223 init_test(cx, |_| {});
7224
7225 let mut cx = EditorLspTestContext::new_rust(
7226 lsp::ServerCapabilities {
7227 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7228 ..Default::default()
7229 },
7230 cx,
7231 )
7232 .await;
7233
7234 cx.set_state(indoc! {"
7235 line 1
7236 line 2
7237 linˇe 3
7238 line 4
7239 line 5
7240 "});
7241
7242 // Make an edit
7243 cx.update_editor(|editor, window, cx| {
7244 editor.handle_input("X", window, cx);
7245 });
7246
7247 // Move cursor to a different position
7248 cx.update_editor(|editor, window, cx| {
7249 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7250 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
7251 });
7252 });
7253
7254 cx.assert_editor_state(indoc! {"
7255 line 1
7256 line 2
7257 linXe 3
7258 line 4
7259 liˇne 5
7260 "});
7261
7262 cx.lsp
7263 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7264 Ok(Some(vec![lsp::TextEdit::new(
7265 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
7266 "PREFIX ".to_string(),
7267 )]))
7268 });
7269
7270 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
7271 .unwrap()
7272 .await
7273 .unwrap();
7274
7275 cx.assert_editor_state(indoc! {"
7276 PREFIX line 1
7277 line 2
7278 linXe 3
7279 line 4
7280 liˇne 5
7281 "});
7282
7283 // Undo formatting
7284 cx.update_editor(|editor, window, cx| {
7285 editor.undo(&Default::default(), window, cx);
7286 });
7287
7288 // Verify cursor moved back to position after edit
7289 cx.assert_editor_state(indoc! {"
7290 line 1
7291 line 2
7292 linXˇe 3
7293 line 4
7294 line 5
7295 "});
7296}
7297
7298#[gpui::test]
7299async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7300 init_test(cx, |_| {});
7301
7302 let mut cx = EditorTestContext::new(cx).await;
7303
7304 let provider = cx.new(|_| FakeEditPredictionProvider::default());
7305 cx.update_editor(|editor, window, cx| {
7306 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7307 });
7308
7309 cx.set_state(indoc! {"
7310 line 1
7311 line 2
7312 linˇe 3
7313 line 4
7314 line 5
7315 line 6
7316 line 7
7317 line 8
7318 line 9
7319 line 10
7320 "});
7321
7322 let snapshot = cx.buffer_snapshot();
7323 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7324
7325 cx.update(|_, cx| {
7326 provider.update(cx, |provider, _| {
7327 provider.set_edit_prediction(Some(edit_prediction::EditPrediction {
7328 id: None,
7329 edits: vec![(edit_position..edit_position, "X".into())],
7330 edit_preview: None,
7331 }))
7332 })
7333 });
7334
7335 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
7336 cx.update_editor(|editor, window, cx| {
7337 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7338 });
7339
7340 cx.assert_editor_state(indoc! {"
7341 line 1
7342 line 2
7343 lineXˇ 3
7344 line 4
7345 line 5
7346 line 6
7347 line 7
7348 line 8
7349 line 9
7350 line 10
7351 "});
7352
7353 cx.update_editor(|editor, window, cx| {
7354 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7355 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7356 });
7357 });
7358
7359 cx.assert_editor_state(indoc! {"
7360 line 1
7361 line 2
7362 lineX 3
7363 line 4
7364 line 5
7365 line 6
7366 line 7
7367 line 8
7368 line 9
7369 liˇne 10
7370 "});
7371
7372 cx.update_editor(|editor, window, cx| {
7373 editor.undo(&Default::default(), window, cx);
7374 });
7375
7376 cx.assert_editor_state(indoc! {"
7377 line 1
7378 line 2
7379 lineˇ 3
7380 line 4
7381 line 5
7382 line 6
7383 line 7
7384 line 8
7385 line 9
7386 line 10
7387 "});
7388}
7389
7390#[gpui::test]
7391async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7392 init_test(cx, |_| {});
7393
7394 let mut cx = EditorTestContext::new(cx).await;
7395 cx.set_state(
7396 r#"let foo = 2;
7397lˇet foo = 2;
7398let fooˇ = 2;
7399let foo = 2;
7400let foo = ˇ2;"#,
7401 );
7402
7403 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7404 .unwrap();
7405 cx.assert_editor_state(
7406 r#"let foo = 2;
7407«letˇ» foo = 2;
7408let «fooˇ» = 2;
7409let foo = 2;
7410let foo = «2ˇ»;"#,
7411 );
7412
7413 // noop for multiple selections with different contents
7414 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7415 .unwrap();
7416 cx.assert_editor_state(
7417 r#"let foo = 2;
7418«letˇ» foo = 2;
7419let «fooˇ» = 2;
7420let foo = 2;
7421let foo = «2ˇ»;"#,
7422 );
7423
7424 // Test last selection direction should be preserved
7425 cx.set_state(
7426 r#"let foo = 2;
7427let foo = 2;
7428let «fooˇ» = 2;
7429let «ˇfoo» = 2;
7430let foo = 2;"#,
7431 );
7432
7433 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7434 .unwrap();
7435 cx.assert_editor_state(
7436 r#"let foo = 2;
7437let foo = 2;
7438let «fooˇ» = 2;
7439let «ˇfoo» = 2;
7440let «ˇfoo» = 2;"#,
7441 );
7442}
7443
7444#[gpui::test]
7445async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7446 init_test(cx, |_| {});
7447
7448 let mut cx =
7449 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7450
7451 cx.assert_editor_state(indoc! {"
7452 ˇbbb
7453 ccc
7454
7455 bbb
7456 ccc
7457 "});
7458 cx.dispatch_action(SelectPrevious::default());
7459 cx.assert_editor_state(indoc! {"
7460 «bbbˇ»
7461 ccc
7462
7463 bbb
7464 ccc
7465 "});
7466 cx.dispatch_action(SelectPrevious::default());
7467 cx.assert_editor_state(indoc! {"
7468 «bbbˇ»
7469 ccc
7470
7471 «bbbˇ»
7472 ccc
7473 "});
7474}
7475
7476#[gpui::test]
7477async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7478 init_test(cx, |_| {});
7479
7480 let mut cx = EditorTestContext::new(cx).await;
7481 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7482
7483 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7484 .unwrap();
7485 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7486
7487 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7488 .unwrap();
7489 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7490
7491 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7492 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7493
7494 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7495 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7496
7497 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7498 .unwrap();
7499 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7500
7501 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7502 .unwrap();
7503 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7504}
7505
7506#[gpui::test]
7507async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7508 init_test(cx, |_| {});
7509
7510 let mut cx = EditorTestContext::new(cx).await;
7511 cx.set_state("aˇ");
7512
7513 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7514 .unwrap();
7515 cx.assert_editor_state("«aˇ»");
7516 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7517 .unwrap();
7518 cx.assert_editor_state("«aˇ»");
7519}
7520
7521#[gpui::test]
7522async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7523 init_test(cx, |_| {});
7524
7525 let mut cx = EditorTestContext::new(cx).await;
7526 cx.set_state(
7527 r#"let foo = 2;
7528lˇet foo = 2;
7529let fooˇ = 2;
7530let foo = 2;
7531let foo = ˇ2;"#,
7532 );
7533
7534 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7535 .unwrap();
7536 cx.assert_editor_state(
7537 r#"let foo = 2;
7538«letˇ» foo = 2;
7539let «fooˇ» = 2;
7540let foo = 2;
7541let foo = «2ˇ»;"#,
7542 );
7543
7544 // noop for multiple selections with different contents
7545 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7546 .unwrap();
7547 cx.assert_editor_state(
7548 r#"let foo = 2;
7549«letˇ» foo = 2;
7550let «fooˇ» = 2;
7551let foo = 2;
7552let foo = «2ˇ»;"#,
7553 );
7554}
7555
7556#[gpui::test]
7557async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7558 init_test(cx, |_| {});
7559
7560 let mut cx = EditorTestContext::new(cx).await;
7561 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7562
7563 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7564 .unwrap();
7565 // selection direction is preserved
7566 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7567
7568 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7569 .unwrap();
7570 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7571
7572 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7573 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7574
7575 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7576 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7577
7578 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7579 .unwrap();
7580 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7581
7582 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7583 .unwrap();
7584 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7585}
7586
7587#[gpui::test]
7588async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7589 init_test(cx, |_| {});
7590
7591 let language = Arc::new(Language::new(
7592 LanguageConfig::default(),
7593 Some(tree_sitter_rust::LANGUAGE.into()),
7594 ));
7595
7596 let text = r#"
7597 use mod1::mod2::{mod3, mod4};
7598
7599 fn fn_1(param1: bool, param2: &str) {
7600 let var1 = "text";
7601 }
7602 "#
7603 .unindent();
7604
7605 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7606 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7607 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7608
7609 editor
7610 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7611 .await;
7612
7613 editor.update_in(cx, |editor, window, cx| {
7614 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7615 s.select_display_ranges([
7616 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7617 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7618 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7619 ]);
7620 });
7621 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7622 });
7623 editor.update(cx, |editor, cx| {
7624 assert_text_with_selections(
7625 editor,
7626 indoc! {r#"
7627 use mod1::mod2::{mod3, «mod4ˇ»};
7628
7629 fn fn_1«ˇ(param1: bool, param2: &str)» {
7630 let var1 = "«ˇtext»";
7631 }
7632 "#},
7633 cx,
7634 );
7635 });
7636
7637 editor.update_in(cx, |editor, window, cx| {
7638 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7639 });
7640 editor.update(cx, |editor, cx| {
7641 assert_text_with_selections(
7642 editor,
7643 indoc! {r#"
7644 use mod1::mod2::«{mod3, mod4}ˇ»;
7645
7646 «ˇfn fn_1(param1: bool, param2: &str) {
7647 let var1 = "text";
7648 }»
7649 "#},
7650 cx,
7651 );
7652 });
7653
7654 editor.update_in(cx, |editor, window, cx| {
7655 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7656 });
7657 assert_eq!(
7658 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7659 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7660 );
7661
7662 // Trying to expand the selected syntax node one more time has no effect.
7663 editor.update_in(cx, |editor, window, cx| {
7664 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7665 });
7666 assert_eq!(
7667 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7668 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7669 );
7670
7671 editor.update_in(cx, |editor, window, cx| {
7672 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7673 });
7674 editor.update(cx, |editor, cx| {
7675 assert_text_with_selections(
7676 editor,
7677 indoc! {r#"
7678 use mod1::mod2::«{mod3, mod4}ˇ»;
7679
7680 «ˇfn fn_1(param1: bool, param2: &str) {
7681 let var1 = "text";
7682 }»
7683 "#},
7684 cx,
7685 );
7686 });
7687
7688 editor.update_in(cx, |editor, window, cx| {
7689 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7690 });
7691 editor.update(cx, |editor, cx| {
7692 assert_text_with_selections(
7693 editor,
7694 indoc! {r#"
7695 use mod1::mod2::{mod3, «mod4ˇ»};
7696
7697 fn fn_1«ˇ(param1: bool, param2: &str)» {
7698 let var1 = "«ˇtext»";
7699 }
7700 "#},
7701 cx,
7702 );
7703 });
7704
7705 editor.update_in(cx, |editor, window, cx| {
7706 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7707 });
7708 editor.update(cx, |editor, cx| {
7709 assert_text_with_selections(
7710 editor,
7711 indoc! {r#"
7712 use mod1::mod2::{mod3, mo«ˇ»d4};
7713
7714 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7715 let var1 = "te«ˇ»xt";
7716 }
7717 "#},
7718 cx,
7719 );
7720 });
7721
7722 // Trying to shrink the selected syntax node one more time has no effect.
7723 editor.update_in(cx, |editor, window, cx| {
7724 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7725 });
7726 editor.update_in(cx, |editor, _, cx| {
7727 assert_text_with_selections(
7728 editor,
7729 indoc! {r#"
7730 use mod1::mod2::{mod3, mo«ˇ»d4};
7731
7732 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7733 let var1 = "te«ˇ»xt";
7734 }
7735 "#},
7736 cx,
7737 );
7738 });
7739
7740 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7741 // a fold.
7742 editor.update_in(cx, |editor, window, cx| {
7743 editor.fold_creases(
7744 vec![
7745 Crease::simple(
7746 Point::new(0, 21)..Point::new(0, 24),
7747 FoldPlaceholder::test(),
7748 ),
7749 Crease::simple(
7750 Point::new(3, 20)..Point::new(3, 22),
7751 FoldPlaceholder::test(),
7752 ),
7753 ],
7754 true,
7755 window,
7756 cx,
7757 );
7758 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7759 });
7760 editor.update(cx, |editor, cx| {
7761 assert_text_with_selections(
7762 editor,
7763 indoc! {r#"
7764 use mod1::mod2::«{mod3, mod4}ˇ»;
7765
7766 fn fn_1«ˇ(param1: bool, param2: &str)» {
7767 let var1 = "«ˇtext»";
7768 }
7769 "#},
7770 cx,
7771 );
7772 });
7773}
7774
7775#[gpui::test]
7776async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7777 init_test(cx, |_| {});
7778
7779 let language = Arc::new(Language::new(
7780 LanguageConfig::default(),
7781 Some(tree_sitter_rust::LANGUAGE.into()),
7782 ));
7783
7784 let text = "let a = 2;";
7785
7786 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7787 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7788 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7789
7790 editor
7791 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7792 .await;
7793
7794 // Test case 1: Cursor at end of word
7795 editor.update_in(cx, |editor, window, cx| {
7796 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7797 s.select_display_ranges([
7798 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7799 ]);
7800 });
7801 });
7802 editor.update(cx, |editor, cx| {
7803 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7804 });
7805 editor.update_in(cx, |editor, window, cx| {
7806 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7807 });
7808 editor.update(cx, |editor, cx| {
7809 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7810 });
7811 editor.update_in(cx, |editor, window, cx| {
7812 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7813 });
7814 editor.update(cx, |editor, cx| {
7815 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7816 });
7817
7818 // Test case 2: Cursor at end of statement
7819 editor.update_in(cx, |editor, window, cx| {
7820 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7821 s.select_display_ranges([
7822 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7823 ]);
7824 });
7825 });
7826 editor.update(cx, |editor, cx| {
7827 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7828 });
7829 editor.update_in(cx, |editor, window, cx| {
7830 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7831 });
7832 editor.update(cx, |editor, cx| {
7833 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7834 });
7835}
7836
7837#[gpui::test]
7838async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7839 init_test(cx, |_| {});
7840
7841 let language = Arc::new(Language::new(
7842 LanguageConfig::default(),
7843 Some(tree_sitter_rust::LANGUAGE.into()),
7844 ));
7845
7846 let text = r#"
7847 use mod1::mod2::{mod3, mod4};
7848
7849 fn fn_1(param1: bool, param2: &str) {
7850 let var1 = "hello world";
7851 }
7852 "#
7853 .unindent();
7854
7855 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7856 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7857 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7858
7859 editor
7860 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7861 .await;
7862
7863 // Test 1: Cursor on a letter of a string word
7864 editor.update_in(cx, |editor, window, cx| {
7865 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7866 s.select_display_ranges([
7867 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7868 ]);
7869 });
7870 });
7871 editor.update_in(cx, |editor, window, cx| {
7872 assert_text_with_selections(
7873 editor,
7874 indoc! {r#"
7875 use mod1::mod2::{mod3, mod4};
7876
7877 fn fn_1(param1: bool, param2: &str) {
7878 let var1 = "hˇello world";
7879 }
7880 "#},
7881 cx,
7882 );
7883 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7884 assert_text_with_selections(
7885 editor,
7886 indoc! {r#"
7887 use mod1::mod2::{mod3, mod4};
7888
7889 fn fn_1(param1: bool, param2: &str) {
7890 let var1 = "«ˇhello» world";
7891 }
7892 "#},
7893 cx,
7894 );
7895 });
7896
7897 // Test 2: Partial selection within a word
7898 editor.update_in(cx, |editor, window, cx| {
7899 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7900 s.select_display_ranges([
7901 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7902 ]);
7903 });
7904 });
7905 editor.update_in(cx, |editor, window, cx| {
7906 assert_text_with_selections(
7907 editor,
7908 indoc! {r#"
7909 use mod1::mod2::{mod3, mod4};
7910
7911 fn fn_1(param1: bool, param2: &str) {
7912 let var1 = "h«elˇ»lo world";
7913 }
7914 "#},
7915 cx,
7916 );
7917 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7918 assert_text_with_selections(
7919 editor,
7920 indoc! {r#"
7921 use mod1::mod2::{mod3, mod4};
7922
7923 fn fn_1(param1: bool, param2: &str) {
7924 let var1 = "«ˇhello» world";
7925 }
7926 "#},
7927 cx,
7928 );
7929 });
7930
7931 // Test 3: Complete word already selected
7932 editor.update_in(cx, |editor, window, cx| {
7933 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7934 s.select_display_ranges([
7935 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7936 ]);
7937 });
7938 });
7939 editor.update_in(cx, |editor, window, cx| {
7940 assert_text_with_selections(
7941 editor,
7942 indoc! {r#"
7943 use mod1::mod2::{mod3, mod4};
7944
7945 fn fn_1(param1: bool, param2: &str) {
7946 let var1 = "«helloˇ» world";
7947 }
7948 "#},
7949 cx,
7950 );
7951 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7952 assert_text_with_selections(
7953 editor,
7954 indoc! {r#"
7955 use mod1::mod2::{mod3, mod4};
7956
7957 fn fn_1(param1: bool, param2: &str) {
7958 let var1 = "«hello worldˇ»";
7959 }
7960 "#},
7961 cx,
7962 );
7963 });
7964
7965 // Test 4: Selection spanning across words
7966 editor.update_in(cx, |editor, window, cx| {
7967 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7968 s.select_display_ranges([
7969 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7970 ]);
7971 });
7972 });
7973 editor.update_in(cx, |editor, window, cx| {
7974 assert_text_with_selections(
7975 editor,
7976 indoc! {r#"
7977 use mod1::mod2::{mod3, mod4};
7978
7979 fn fn_1(param1: bool, param2: &str) {
7980 let var1 = "hel«lo woˇ»rld";
7981 }
7982 "#},
7983 cx,
7984 );
7985 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7986 assert_text_with_selections(
7987 editor,
7988 indoc! {r#"
7989 use mod1::mod2::{mod3, mod4};
7990
7991 fn fn_1(param1: bool, param2: &str) {
7992 let var1 = "«ˇhello world»";
7993 }
7994 "#},
7995 cx,
7996 );
7997 });
7998
7999 // Test 5: Expansion beyond string
8000 editor.update_in(cx, |editor, window, cx| {
8001 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8002 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8003 assert_text_with_selections(
8004 editor,
8005 indoc! {r#"
8006 use mod1::mod2::{mod3, mod4};
8007
8008 fn fn_1(param1: bool, param2: &str) {
8009 «ˇlet var1 = "hello world";»
8010 }
8011 "#},
8012 cx,
8013 );
8014 });
8015}
8016
8017#[gpui::test]
8018async fn test_unwrap_syntax_node(cx: &mut gpui::TestAppContext) {
8019 init_test(cx, |_| {});
8020
8021 let mut cx = EditorTestContext::new(cx).await;
8022
8023 let language = Arc::new(Language::new(
8024 LanguageConfig::default(),
8025 Some(tree_sitter_rust::LANGUAGE.into()),
8026 ));
8027
8028 cx.update_buffer(|buffer, cx| {
8029 buffer.set_language(Some(language), cx);
8030 });
8031
8032 cx.set_state(
8033 &r#"
8034 use mod1::mod2::{«mod3ˇ», mod4};
8035 "#
8036 .unindent(),
8037 );
8038 cx.update_editor(|editor, window, cx| {
8039 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
8040 });
8041 cx.assert_editor_state(
8042 &r#"
8043 use mod1::mod2::«mod3ˇ»;
8044 "#
8045 .unindent(),
8046 );
8047}
8048
8049#[gpui::test]
8050async fn test_fold_function_bodies(cx: &mut TestAppContext) {
8051 init_test(cx, |_| {});
8052
8053 let base_text = r#"
8054 impl A {
8055 // this is an uncommitted comment
8056
8057 fn b() {
8058 c();
8059 }
8060
8061 // this is another uncommitted comment
8062
8063 fn d() {
8064 // e
8065 // f
8066 }
8067 }
8068
8069 fn g() {
8070 // h
8071 }
8072 "#
8073 .unindent();
8074
8075 let text = r#"
8076 ˇimpl A {
8077
8078 fn b() {
8079 c();
8080 }
8081
8082 fn d() {
8083 // e
8084 // f
8085 }
8086 }
8087
8088 fn g() {
8089 // h
8090 }
8091 "#
8092 .unindent();
8093
8094 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8095 cx.set_state(&text);
8096 cx.set_head_text(&base_text);
8097 cx.update_editor(|editor, window, cx| {
8098 editor.expand_all_diff_hunks(&Default::default(), window, cx);
8099 });
8100
8101 cx.assert_state_with_diff(
8102 "
8103 ˇimpl A {
8104 - // this is an uncommitted comment
8105
8106 fn b() {
8107 c();
8108 }
8109
8110 - // this is another uncommitted comment
8111 -
8112 fn d() {
8113 // e
8114 // f
8115 }
8116 }
8117
8118 fn g() {
8119 // h
8120 }
8121 "
8122 .unindent(),
8123 );
8124
8125 let expected_display_text = "
8126 impl A {
8127 // this is an uncommitted comment
8128
8129 fn b() {
8130 ⋯
8131 }
8132
8133 // this is another uncommitted comment
8134
8135 fn d() {
8136 ⋯
8137 }
8138 }
8139
8140 fn g() {
8141 ⋯
8142 }
8143 "
8144 .unindent();
8145
8146 cx.update_editor(|editor, window, cx| {
8147 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
8148 assert_eq!(editor.display_text(cx), expected_display_text);
8149 });
8150}
8151
8152#[gpui::test]
8153async fn test_autoindent(cx: &mut TestAppContext) {
8154 init_test(cx, |_| {});
8155
8156 let language = Arc::new(
8157 Language::new(
8158 LanguageConfig {
8159 brackets: BracketPairConfig {
8160 pairs: vec![
8161 BracketPair {
8162 start: "{".to_string(),
8163 end: "}".to_string(),
8164 close: false,
8165 surround: false,
8166 newline: true,
8167 },
8168 BracketPair {
8169 start: "(".to_string(),
8170 end: ")".to_string(),
8171 close: false,
8172 surround: false,
8173 newline: true,
8174 },
8175 ],
8176 ..Default::default()
8177 },
8178 ..Default::default()
8179 },
8180 Some(tree_sitter_rust::LANGUAGE.into()),
8181 )
8182 .with_indents_query(
8183 r#"
8184 (_ "(" ")" @end) @indent
8185 (_ "{" "}" @end) @indent
8186 "#,
8187 )
8188 .unwrap(),
8189 );
8190
8191 let text = "fn a() {}";
8192
8193 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8194 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8195 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8196 editor
8197 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8198 .await;
8199
8200 editor.update_in(cx, |editor, window, cx| {
8201 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8202 s.select_ranges([5..5, 8..8, 9..9])
8203 });
8204 editor.newline(&Newline, window, cx);
8205 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
8206 assert_eq!(
8207 editor.selections.ranges(cx),
8208 &[
8209 Point::new(1, 4)..Point::new(1, 4),
8210 Point::new(3, 4)..Point::new(3, 4),
8211 Point::new(5, 0)..Point::new(5, 0)
8212 ]
8213 );
8214 });
8215}
8216
8217#[gpui::test]
8218async fn test_autoindent_disabled(cx: &mut TestAppContext) {
8219 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
8220
8221 let language = Arc::new(
8222 Language::new(
8223 LanguageConfig {
8224 brackets: BracketPairConfig {
8225 pairs: vec![
8226 BracketPair {
8227 start: "{".to_string(),
8228 end: "}".to_string(),
8229 close: false,
8230 surround: false,
8231 newline: true,
8232 },
8233 BracketPair {
8234 start: "(".to_string(),
8235 end: ")".to_string(),
8236 close: false,
8237 surround: false,
8238 newline: true,
8239 },
8240 ],
8241 ..Default::default()
8242 },
8243 ..Default::default()
8244 },
8245 Some(tree_sitter_rust::LANGUAGE.into()),
8246 )
8247 .with_indents_query(
8248 r#"
8249 (_ "(" ")" @end) @indent
8250 (_ "{" "}" @end) @indent
8251 "#,
8252 )
8253 .unwrap(),
8254 );
8255
8256 let text = "fn a() {}";
8257
8258 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8259 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8260 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8261 editor
8262 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8263 .await;
8264
8265 editor.update_in(cx, |editor, window, cx| {
8266 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8267 s.select_ranges([5..5, 8..8, 9..9])
8268 });
8269 editor.newline(&Newline, window, cx);
8270 assert_eq!(
8271 editor.text(cx),
8272 indoc!(
8273 "
8274 fn a(
8275
8276 ) {
8277
8278 }
8279 "
8280 )
8281 );
8282 assert_eq!(
8283 editor.selections.ranges(cx),
8284 &[
8285 Point::new(1, 0)..Point::new(1, 0),
8286 Point::new(3, 0)..Point::new(3, 0),
8287 Point::new(5, 0)..Point::new(5, 0)
8288 ]
8289 );
8290 });
8291}
8292
8293#[gpui::test]
8294async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
8295 init_test(cx, |settings| {
8296 settings.defaults.auto_indent = Some(true);
8297 settings.languages.0.insert(
8298 "python".into(),
8299 LanguageSettingsContent {
8300 auto_indent: Some(false),
8301 ..Default::default()
8302 },
8303 );
8304 });
8305
8306 let mut cx = EditorTestContext::new(cx).await;
8307
8308 let injected_language = Arc::new(
8309 Language::new(
8310 LanguageConfig {
8311 brackets: BracketPairConfig {
8312 pairs: vec![
8313 BracketPair {
8314 start: "{".to_string(),
8315 end: "}".to_string(),
8316 close: false,
8317 surround: false,
8318 newline: true,
8319 },
8320 BracketPair {
8321 start: "(".to_string(),
8322 end: ")".to_string(),
8323 close: true,
8324 surround: false,
8325 newline: true,
8326 },
8327 ],
8328 ..Default::default()
8329 },
8330 name: "python".into(),
8331 ..Default::default()
8332 },
8333 Some(tree_sitter_python::LANGUAGE.into()),
8334 )
8335 .with_indents_query(
8336 r#"
8337 (_ "(" ")" @end) @indent
8338 (_ "{" "}" @end) @indent
8339 "#,
8340 )
8341 .unwrap(),
8342 );
8343
8344 let language = Arc::new(
8345 Language::new(
8346 LanguageConfig {
8347 brackets: BracketPairConfig {
8348 pairs: vec![
8349 BracketPair {
8350 start: "{".to_string(),
8351 end: "}".to_string(),
8352 close: false,
8353 surround: false,
8354 newline: true,
8355 },
8356 BracketPair {
8357 start: "(".to_string(),
8358 end: ")".to_string(),
8359 close: true,
8360 surround: false,
8361 newline: true,
8362 },
8363 ],
8364 ..Default::default()
8365 },
8366 name: LanguageName::new("rust"),
8367 ..Default::default()
8368 },
8369 Some(tree_sitter_rust::LANGUAGE.into()),
8370 )
8371 .with_indents_query(
8372 r#"
8373 (_ "(" ")" @end) @indent
8374 (_ "{" "}" @end) @indent
8375 "#,
8376 )
8377 .unwrap()
8378 .with_injection_query(
8379 r#"
8380 (macro_invocation
8381 macro: (identifier) @_macro_name
8382 (token_tree) @injection.content
8383 (#set! injection.language "python"))
8384 "#,
8385 )
8386 .unwrap(),
8387 );
8388
8389 cx.language_registry().add(injected_language);
8390 cx.language_registry().add(language.clone());
8391
8392 cx.update_buffer(|buffer, cx| {
8393 buffer.set_language(Some(language), cx);
8394 });
8395
8396 cx.set_state(&r#"struct A {ˇ}"#);
8397
8398 cx.update_editor(|editor, window, cx| {
8399 editor.newline(&Default::default(), window, cx);
8400 });
8401
8402 cx.assert_editor_state(indoc!(
8403 "struct A {
8404 ˇ
8405 }"
8406 ));
8407
8408 cx.set_state(&r#"select_biased!(ˇ)"#);
8409
8410 cx.update_editor(|editor, window, cx| {
8411 editor.newline(&Default::default(), window, cx);
8412 editor.handle_input("def ", window, cx);
8413 editor.handle_input("(", window, cx);
8414 editor.newline(&Default::default(), window, cx);
8415 editor.handle_input("a", window, cx);
8416 });
8417
8418 cx.assert_editor_state(indoc!(
8419 "select_biased!(
8420 def (
8421 aˇ
8422 )
8423 )"
8424 ));
8425}
8426
8427#[gpui::test]
8428async fn test_autoindent_selections(cx: &mut TestAppContext) {
8429 init_test(cx, |_| {});
8430
8431 {
8432 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8433 cx.set_state(indoc! {"
8434 impl A {
8435
8436 fn b() {}
8437
8438 «fn c() {
8439
8440 }ˇ»
8441 }
8442 "});
8443
8444 cx.update_editor(|editor, window, cx| {
8445 editor.autoindent(&Default::default(), window, cx);
8446 });
8447
8448 cx.assert_editor_state(indoc! {"
8449 impl A {
8450
8451 fn b() {}
8452
8453 «fn c() {
8454
8455 }ˇ»
8456 }
8457 "});
8458 }
8459
8460 {
8461 let mut cx = EditorTestContext::new_multibuffer(
8462 cx,
8463 [indoc! { "
8464 impl A {
8465 «
8466 // a
8467 fn b(){}
8468 »
8469 «
8470 }
8471 fn c(){}
8472 »
8473 "}],
8474 );
8475
8476 let buffer = cx.update_editor(|editor, _, cx| {
8477 let buffer = editor.buffer().update(cx, |buffer, _| {
8478 buffer.all_buffers().iter().next().unwrap().clone()
8479 });
8480 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8481 buffer
8482 });
8483
8484 cx.run_until_parked();
8485 cx.update_editor(|editor, window, cx| {
8486 editor.select_all(&Default::default(), window, cx);
8487 editor.autoindent(&Default::default(), window, cx)
8488 });
8489 cx.run_until_parked();
8490
8491 cx.update(|_, cx| {
8492 assert_eq!(
8493 buffer.read(cx).text(),
8494 indoc! { "
8495 impl A {
8496
8497 // a
8498 fn b(){}
8499
8500
8501 }
8502 fn c(){}
8503
8504 " }
8505 )
8506 });
8507 }
8508}
8509
8510#[gpui::test]
8511async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8512 init_test(cx, |_| {});
8513
8514 let mut cx = EditorTestContext::new(cx).await;
8515
8516 let language = Arc::new(Language::new(
8517 LanguageConfig {
8518 brackets: BracketPairConfig {
8519 pairs: vec![
8520 BracketPair {
8521 start: "{".to_string(),
8522 end: "}".to_string(),
8523 close: true,
8524 surround: true,
8525 newline: true,
8526 },
8527 BracketPair {
8528 start: "(".to_string(),
8529 end: ")".to_string(),
8530 close: true,
8531 surround: true,
8532 newline: true,
8533 },
8534 BracketPair {
8535 start: "/*".to_string(),
8536 end: " */".to_string(),
8537 close: true,
8538 surround: true,
8539 newline: true,
8540 },
8541 BracketPair {
8542 start: "[".to_string(),
8543 end: "]".to_string(),
8544 close: false,
8545 surround: false,
8546 newline: true,
8547 },
8548 BracketPair {
8549 start: "\"".to_string(),
8550 end: "\"".to_string(),
8551 close: true,
8552 surround: true,
8553 newline: false,
8554 },
8555 BracketPair {
8556 start: "<".to_string(),
8557 end: ">".to_string(),
8558 close: false,
8559 surround: true,
8560 newline: true,
8561 },
8562 ],
8563 ..Default::default()
8564 },
8565 autoclose_before: "})]".to_string(),
8566 ..Default::default()
8567 },
8568 Some(tree_sitter_rust::LANGUAGE.into()),
8569 ));
8570
8571 cx.language_registry().add(language.clone());
8572 cx.update_buffer(|buffer, cx| {
8573 buffer.set_language(Some(language), cx);
8574 });
8575
8576 cx.set_state(
8577 &r#"
8578 🏀ˇ
8579 εˇ
8580 ❤️ˇ
8581 "#
8582 .unindent(),
8583 );
8584
8585 // autoclose multiple nested brackets at multiple cursors
8586 cx.update_editor(|editor, window, cx| {
8587 editor.handle_input("{", window, cx);
8588 editor.handle_input("{", window, cx);
8589 editor.handle_input("{", window, cx);
8590 });
8591 cx.assert_editor_state(
8592 &"
8593 🏀{{{ˇ}}}
8594 ε{{{ˇ}}}
8595 ❤️{{{ˇ}}}
8596 "
8597 .unindent(),
8598 );
8599
8600 // insert a different closing bracket
8601 cx.update_editor(|editor, window, cx| {
8602 editor.handle_input(")", window, cx);
8603 });
8604 cx.assert_editor_state(
8605 &"
8606 🏀{{{)ˇ}}}
8607 ε{{{)ˇ}}}
8608 ❤️{{{)ˇ}}}
8609 "
8610 .unindent(),
8611 );
8612
8613 // skip over the auto-closed brackets when typing a closing bracket
8614 cx.update_editor(|editor, window, cx| {
8615 editor.move_right(&MoveRight, window, cx);
8616 editor.handle_input("}", window, cx);
8617 editor.handle_input("}", window, cx);
8618 editor.handle_input("}", window, cx);
8619 });
8620 cx.assert_editor_state(
8621 &"
8622 🏀{{{)}}}}ˇ
8623 ε{{{)}}}}ˇ
8624 ❤️{{{)}}}}ˇ
8625 "
8626 .unindent(),
8627 );
8628
8629 // autoclose multi-character pairs
8630 cx.set_state(
8631 &"
8632 ˇ
8633 ˇ
8634 "
8635 .unindent(),
8636 );
8637 cx.update_editor(|editor, window, cx| {
8638 editor.handle_input("/", window, cx);
8639 editor.handle_input("*", window, cx);
8640 });
8641 cx.assert_editor_state(
8642 &"
8643 /*ˇ */
8644 /*ˇ */
8645 "
8646 .unindent(),
8647 );
8648
8649 // one cursor autocloses a multi-character pair, one cursor
8650 // does not autoclose.
8651 cx.set_state(
8652 &"
8653 /ˇ
8654 ˇ
8655 "
8656 .unindent(),
8657 );
8658 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8659 cx.assert_editor_state(
8660 &"
8661 /*ˇ */
8662 *ˇ
8663 "
8664 .unindent(),
8665 );
8666
8667 // Don't autoclose if the next character isn't whitespace and isn't
8668 // listed in the language's "autoclose_before" section.
8669 cx.set_state("ˇa b");
8670 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8671 cx.assert_editor_state("{ˇa b");
8672
8673 // Don't autoclose if `close` is false for the bracket pair
8674 cx.set_state("ˇ");
8675 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8676 cx.assert_editor_state("[ˇ");
8677
8678 // Surround with brackets if text is selected
8679 cx.set_state("«aˇ» b");
8680 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8681 cx.assert_editor_state("{«aˇ»} b");
8682
8683 // Autoclose when not immediately after a word character
8684 cx.set_state("a ˇ");
8685 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8686 cx.assert_editor_state("a \"ˇ\"");
8687
8688 // Autoclose pair where the start and end characters are the same
8689 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8690 cx.assert_editor_state("a \"\"ˇ");
8691
8692 // Don't autoclose when immediately after a word character
8693 cx.set_state("aˇ");
8694 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8695 cx.assert_editor_state("a\"ˇ");
8696
8697 // Do autoclose when after a non-word character
8698 cx.set_state("{ˇ");
8699 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8700 cx.assert_editor_state("{\"ˇ\"");
8701
8702 // Non identical pairs autoclose regardless of preceding character
8703 cx.set_state("aˇ");
8704 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8705 cx.assert_editor_state("a{ˇ}");
8706
8707 // Don't autoclose pair if autoclose is disabled
8708 cx.set_state("ˇ");
8709 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8710 cx.assert_editor_state("<ˇ");
8711
8712 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8713 cx.set_state("«aˇ» b");
8714 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8715 cx.assert_editor_state("<«aˇ»> b");
8716}
8717
8718#[gpui::test]
8719async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8720 init_test(cx, |settings| {
8721 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8722 });
8723
8724 let mut cx = EditorTestContext::new(cx).await;
8725
8726 let language = Arc::new(Language::new(
8727 LanguageConfig {
8728 brackets: BracketPairConfig {
8729 pairs: vec![
8730 BracketPair {
8731 start: "{".to_string(),
8732 end: "}".to_string(),
8733 close: true,
8734 surround: true,
8735 newline: true,
8736 },
8737 BracketPair {
8738 start: "(".to_string(),
8739 end: ")".to_string(),
8740 close: true,
8741 surround: true,
8742 newline: true,
8743 },
8744 BracketPair {
8745 start: "[".to_string(),
8746 end: "]".to_string(),
8747 close: false,
8748 surround: false,
8749 newline: true,
8750 },
8751 ],
8752 ..Default::default()
8753 },
8754 autoclose_before: "})]".to_string(),
8755 ..Default::default()
8756 },
8757 Some(tree_sitter_rust::LANGUAGE.into()),
8758 ));
8759
8760 cx.language_registry().add(language.clone());
8761 cx.update_buffer(|buffer, cx| {
8762 buffer.set_language(Some(language), cx);
8763 });
8764
8765 cx.set_state(
8766 &"
8767 ˇ
8768 ˇ
8769 ˇ
8770 "
8771 .unindent(),
8772 );
8773
8774 // ensure only matching closing brackets are skipped over
8775 cx.update_editor(|editor, window, cx| {
8776 editor.handle_input("}", window, cx);
8777 editor.move_left(&MoveLeft, window, cx);
8778 editor.handle_input(")", window, cx);
8779 editor.move_left(&MoveLeft, window, cx);
8780 });
8781 cx.assert_editor_state(
8782 &"
8783 ˇ)}
8784 ˇ)}
8785 ˇ)}
8786 "
8787 .unindent(),
8788 );
8789
8790 // skip-over closing brackets at multiple cursors
8791 cx.update_editor(|editor, window, cx| {
8792 editor.handle_input(")", window, cx);
8793 editor.handle_input("}", window, cx);
8794 });
8795 cx.assert_editor_state(
8796 &"
8797 )}ˇ
8798 )}ˇ
8799 )}ˇ
8800 "
8801 .unindent(),
8802 );
8803
8804 // ignore non-close brackets
8805 cx.update_editor(|editor, window, cx| {
8806 editor.handle_input("]", window, cx);
8807 editor.move_left(&MoveLeft, window, cx);
8808 editor.handle_input("]", window, cx);
8809 });
8810 cx.assert_editor_state(
8811 &"
8812 )}]ˇ]
8813 )}]ˇ]
8814 )}]ˇ]
8815 "
8816 .unindent(),
8817 );
8818}
8819
8820#[gpui::test]
8821async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8822 init_test(cx, |_| {});
8823
8824 let mut cx = EditorTestContext::new(cx).await;
8825
8826 let html_language = Arc::new(
8827 Language::new(
8828 LanguageConfig {
8829 name: "HTML".into(),
8830 brackets: BracketPairConfig {
8831 pairs: vec![
8832 BracketPair {
8833 start: "<".into(),
8834 end: ">".into(),
8835 close: true,
8836 ..Default::default()
8837 },
8838 BracketPair {
8839 start: "{".into(),
8840 end: "}".into(),
8841 close: true,
8842 ..Default::default()
8843 },
8844 BracketPair {
8845 start: "(".into(),
8846 end: ")".into(),
8847 close: true,
8848 ..Default::default()
8849 },
8850 ],
8851 ..Default::default()
8852 },
8853 autoclose_before: "})]>".into(),
8854 ..Default::default()
8855 },
8856 Some(tree_sitter_html::LANGUAGE.into()),
8857 )
8858 .with_injection_query(
8859 r#"
8860 (script_element
8861 (raw_text) @injection.content
8862 (#set! injection.language "javascript"))
8863 "#,
8864 )
8865 .unwrap(),
8866 );
8867
8868 let javascript_language = Arc::new(Language::new(
8869 LanguageConfig {
8870 name: "JavaScript".into(),
8871 brackets: BracketPairConfig {
8872 pairs: vec![
8873 BracketPair {
8874 start: "/*".into(),
8875 end: " */".into(),
8876 close: true,
8877 ..Default::default()
8878 },
8879 BracketPair {
8880 start: "{".into(),
8881 end: "}".into(),
8882 close: true,
8883 ..Default::default()
8884 },
8885 BracketPair {
8886 start: "(".into(),
8887 end: ")".into(),
8888 close: true,
8889 ..Default::default()
8890 },
8891 ],
8892 ..Default::default()
8893 },
8894 autoclose_before: "})]>".into(),
8895 ..Default::default()
8896 },
8897 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8898 ));
8899
8900 cx.language_registry().add(html_language.clone());
8901 cx.language_registry().add(javascript_language.clone());
8902 cx.executor().run_until_parked();
8903
8904 cx.update_buffer(|buffer, cx| {
8905 buffer.set_language(Some(html_language), cx);
8906 });
8907
8908 cx.set_state(
8909 &r#"
8910 <body>ˇ
8911 <script>
8912 var x = 1;ˇ
8913 </script>
8914 </body>ˇ
8915 "#
8916 .unindent(),
8917 );
8918
8919 // Precondition: different languages are active at different locations.
8920 cx.update_editor(|editor, window, cx| {
8921 let snapshot = editor.snapshot(window, cx);
8922 let cursors = editor.selections.ranges::<usize>(cx);
8923 let languages = cursors
8924 .iter()
8925 .map(|c| snapshot.language_at(c.start).unwrap().name())
8926 .collect::<Vec<_>>();
8927 assert_eq!(
8928 languages,
8929 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8930 );
8931 });
8932
8933 // Angle brackets autoclose in HTML, but not JavaScript.
8934 cx.update_editor(|editor, window, cx| {
8935 editor.handle_input("<", window, cx);
8936 editor.handle_input("a", window, cx);
8937 });
8938 cx.assert_editor_state(
8939 &r#"
8940 <body><aˇ>
8941 <script>
8942 var x = 1;<aˇ
8943 </script>
8944 </body><aˇ>
8945 "#
8946 .unindent(),
8947 );
8948
8949 // Curly braces and parens autoclose in both HTML and JavaScript.
8950 cx.update_editor(|editor, window, cx| {
8951 editor.handle_input(" b=", window, cx);
8952 editor.handle_input("{", window, cx);
8953 editor.handle_input("c", window, cx);
8954 editor.handle_input("(", window, cx);
8955 });
8956 cx.assert_editor_state(
8957 &r#"
8958 <body><a b={c(ˇ)}>
8959 <script>
8960 var x = 1;<a b={c(ˇ)}
8961 </script>
8962 </body><a b={c(ˇ)}>
8963 "#
8964 .unindent(),
8965 );
8966
8967 // Brackets that were already autoclosed are skipped.
8968 cx.update_editor(|editor, window, cx| {
8969 editor.handle_input(")", window, cx);
8970 editor.handle_input("d", window, cx);
8971 editor.handle_input("}", window, cx);
8972 });
8973 cx.assert_editor_state(
8974 &r#"
8975 <body><a b={c()d}ˇ>
8976 <script>
8977 var x = 1;<a b={c()d}ˇ
8978 </script>
8979 </body><a b={c()d}ˇ>
8980 "#
8981 .unindent(),
8982 );
8983 cx.update_editor(|editor, window, cx| {
8984 editor.handle_input(">", window, cx);
8985 });
8986 cx.assert_editor_state(
8987 &r#"
8988 <body><a b={c()d}>ˇ
8989 <script>
8990 var x = 1;<a b={c()d}>ˇ
8991 </script>
8992 </body><a b={c()d}>ˇ
8993 "#
8994 .unindent(),
8995 );
8996
8997 // Reset
8998 cx.set_state(
8999 &r#"
9000 <body>ˇ
9001 <script>
9002 var x = 1;ˇ
9003 </script>
9004 </body>ˇ
9005 "#
9006 .unindent(),
9007 );
9008
9009 cx.update_editor(|editor, window, cx| {
9010 editor.handle_input("<", window, cx);
9011 });
9012 cx.assert_editor_state(
9013 &r#"
9014 <body><ˇ>
9015 <script>
9016 var x = 1;<ˇ
9017 </script>
9018 </body><ˇ>
9019 "#
9020 .unindent(),
9021 );
9022
9023 // When backspacing, the closing angle brackets are removed.
9024 cx.update_editor(|editor, window, cx| {
9025 editor.backspace(&Backspace, window, cx);
9026 });
9027 cx.assert_editor_state(
9028 &r#"
9029 <body>ˇ
9030 <script>
9031 var x = 1;ˇ
9032 </script>
9033 </body>ˇ
9034 "#
9035 .unindent(),
9036 );
9037
9038 // Block comments autoclose in JavaScript, but not HTML.
9039 cx.update_editor(|editor, window, cx| {
9040 editor.handle_input("/", window, cx);
9041 editor.handle_input("*", window, cx);
9042 });
9043 cx.assert_editor_state(
9044 &r#"
9045 <body>/*ˇ
9046 <script>
9047 var x = 1;/*ˇ */
9048 </script>
9049 </body>/*ˇ
9050 "#
9051 .unindent(),
9052 );
9053}
9054
9055#[gpui::test]
9056async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
9057 init_test(cx, |_| {});
9058
9059 let mut cx = EditorTestContext::new(cx).await;
9060
9061 let rust_language = Arc::new(
9062 Language::new(
9063 LanguageConfig {
9064 name: "Rust".into(),
9065 brackets: serde_json::from_value(json!([
9066 { "start": "{", "end": "}", "close": true, "newline": true },
9067 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
9068 ]))
9069 .unwrap(),
9070 autoclose_before: "})]>".into(),
9071 ..Default::default()
9072 },
9073 Some(tree_sitter_rust::LANGUAGE.into()),
9074 )
9075 .with_override_query("(string_literal) @string")
9076 .unwrap(),
9077 );
9078
9079 cx.language_registry().add(rust_language.clone());
9080 cx.update_buffer(|buffer, cx| {
9081 buffer.set_language(Some(rust_language), cx);
9082 });
9083
9084 cx.set_state(
9085 &r#"
9086 let x = ˇ
9087 "#
9088 .unindent(),
9089 );
9090
9091 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
9092 cx.update_editor(|editor, window, cx| {
9093 editor.handle_input("\"", window, cx);
9094 });
9095 cx.assert_editor_state(
9096 &r#"
9097 let x = "ˇ"
9098 "#
9099 .unindent(),
9100 );
9101
9102 // Inserting another quotation mark. The cursor moves across the existing
9103 // automatically-inserted quotation mark.
9104 cx.update_editor(|editor, window, cx| {
9105 editor.handle_input("\"", window, cx);
9106 });
9107 cx.assert_editor_state(
9108 &r#"
9109 let x = ""ˇ
9110 "#
9111 .unindent(),
9112 );
9113
9114 // Reset
9115 cx.set_state(
9116 &r#"
9117 let x = ˇ
9118 "#
9119 .unindent(),
9120 );
9121
9122 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
9123 cx.update_editor(|editor, window, cx| {
9124 editor.handle_input("\"", window, cx);
9125 editor.handle_input(" ", window, cx);
9126 editor.move_left(&Default::default(), window, cx);
9127 editor.handle_input("\\", window, cx);
9128 editor.handle_input("\"", window, cx);
9129 });
9130 cx.assert_editor_state(
9131 &r#"
9132 let x = "\"ˇ "
9133 "#
9134 .unindent(),
9135 );
9136
9137 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
9138 // mark. Nothing is inserted.
9139 cx.update_editor(|editor, window, cx| {
9140 editor.move_right(&Default::default(), window, cx);
9141 editor.handle_input("\"", window, cx);
9142 });
9143 cx.assert_editor_state(
9144 &r#"
9145 let x = "\" "ˇ
9146 "#
9147 .unindent(),
9148 );
9149}
9150
9151#[gpui::test]
9152async fn test_surround_with_pair(cx: &mut TestAppContext) {
9153 init_test(cx, |_| {});
9154
9155 let language = Arc::new(Language::new(
9156 LanguageConfig {
9157 brackets: BracketPairConfig {
9158 pairs: vec![
9159 BracketPair {
9160 start: "{".to_string(),
9161 end: "}".to_string(),
9162 close: true,
9163 surround: true,
9164 newline: true,
9165 },
9166 BracketPair {
9167 start: "/* ".to_string(),
9168 end: "*/".to_string(),
9169 close: true,
9170 surround: true,
9171 ..Default::default()
9172 },
9173 ],
9174 ..Default::default()
9175 },
9176 ..Default::default()
9177 },
9178 Some(tree_sitter_rust::LANGUAGE.into()),
9179 ));
9180
9181 let text = r#"
9182 a
9183 b
9184 c
9185 "#
9186 .unindent();
9187
9188 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9189 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9190 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9191 editor
9192 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9193 .await;
9194
9195 editor.update_in(cx, |editor, window, cx| {
9196 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9197 s.select_display_ranges([
9198 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9199 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
9200 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
9201 ])
9202 });
9203
9204 editor.handle_input("{", window, cx);
9205 editor.handle_input("{", window, cx);
9206 editor.handle_input("{", window, cx);
9207 assert_eq!(
9208 editor.text(cx),
9209 "
9210 {{{a}}}
9211 {{{b}}}
9212 {{{c}}}
9213 "
9214 .unindent()
9215 );
9216 assert_eq!(
9217 editor.selections.display_ranges(cx),
9218 [
9219 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
9220 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
9221 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
9222 ]
9223 );
9224
9225 editor.undo(&Undo, window, cx);
9226 editor.undo(&Undo, window, cx);
9227 editor.undo(&Undo, window, cx);
9228 assert_eq!(
9229 editor.text(cx),
9230 "
9231 a
9232 b
9233 c
9234 "
9235 .unindent()
9236 );
9237 assert_eq!(
9238 editor.selections.display_ranges(cx),
9239 [
9240 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9241 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
9242 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
9243 ]
9244 );
9245
9246 // Ensure inserting the first character of a multi-byte bracket pair
9247 // doesn't surround the selections with the bracket.
9248 editor.handle_input("/", window, cx);
9249 assert_eq!(
9250 editor.text(cx),
9251 "
9252 /
9253 /
9254 /
9255 "
9256 .unindent()
9257 );
9258 assert_eq!(
9259 editor.selections.display_ranges(cx),
9260 [
9261 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
9262 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
9263 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
9264 ]
9265 );
9266
9267 editor.undo(&Undo, window, cx);
9268 assert_eq!(
9269 editor.text(cx),
9270 "
9271 a
9272 b
9273 c
9274 "
9275 .unindent()
9276 );
9277 assert_eq!(
9278 editor.selections.display_ranges(cx),
9279 [
9280 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9281 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
9282 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
9283 ]
9284 );
9285
9286 // Ensure inserting the last character of a multi-byte bracket pair
9287 // doesn't surround the selections with the bracket.
9288 editor.handle_input("*", window, cx);
9289 assert_eq!(
9290 editor.text(cx),
9291 "
9292 *
9293 *
9294 *
9295 "
9296 .unindent()
9297 );
9298 assert_eq!(
9299 editor.selections.display_ranges(cx),
9300 [
9301 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
9302 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
9303 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
9304 ]
9305 );
9306 });
9307}
9308
9309#[gpui::test]
9310async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
9311 init_test(cx, |_| {});
9312
9313 let language = Arc::new(Language::new(
9314 LanguageConfig {
9315 brackets: BracketPairConfig {
9316 pairs: vec![BracketPair {
9317 start: "{".to_string(),
9318 end: "}".to_string(),
9319 close: true,
9320 surround: true,
9321 newline: true,
9322 }],
9323 ..Default::default()
9324 },
9325 autoclose_before: "}".to_string(),
9326 ..Default::default()
9327 },
9328 Some(tree_sitter_rust::LANGUAGE.into()),
9329 ));
9330
9331 let text = r#"
9332 a
9333 b
9334 c
9335 "#
9336 .unindent();
9337
9338 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9339 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9340 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9341 editor
9342 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9343 .await;
9344
9345 editor.update_in(cx, |editor, window, cx| {
9346 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9347 s.select_ranges([
9348 Point::new(0, 1)..Point::new(0, 1),
9349 Point::new(1, 1)..Point::new(1, 1),
9350 Point::new(2, 1)..Point::new(2, 1),
9351 ])
9352 });
9353
9354 editor.handle_input("{", window, cx);
9355 editor.handle_input("{", window, cx);
9356 editor.handle_input("_", window, cx);
9357 assert_eq!(
9358 editor.text(cx),
9359 "
9360 a{{_}}
9361 b{{_}}
9362 c{{_}}
9363 "
9364 .unindent()
9365 );
9366 assert_eq!(
9367 editor.selections.ranges::<Point>(cx),
9368 [
9369 Point::new(0, 4)..Point::new(0, 4),
9370 Point::new(1, 4)..Point::new(1, 4),
9371 Point::new(2, 4)..Point::new(2, 4)
9372 ]
9373 );
9374
9375 editor.backspace(&Default::default(), window, cx);
9376 editor.backspace(&Default::default(), window, cx);
9377 assert_eq!(
9378 editor.text(cx),
9379 "
9380 a{}
9381 b{}
9382 c{}
9383 "
9384 .unindent()
9385 );
9386 assert_eq!(
9387 editor.selections.ranges::<Point>(cx),
9388 [
9389 Point::new(0, 2)..Point::new(0, 2),
9390 Point::new(1, 2)..Point::new(1, 2),
9391 Point::new(2, 2)..Point::new(2, 2)
9392 ]
9393 );
9394
9395 editor.delete_to_previous_word_start(&Default::default(), window, cx);
9396 assert_eq!(
9397 editor.text(cx),
9398 "
9399 a
9400 b
9401 c
9402 "
9403 .unindent()
9404 );
9405 assert_eq!(
9406 editor.selections.ranges::<Point>(cx),
9407 [
9408 Point::new(0, 1)..Point::new(0, 1),
9409 Point::new(1, 1)..Point::new(1, 1),
9410 Point::new(2, 1)..Point::new(2, 1)
9411 ]
9412 );
9413 });
9414}
9415
9416#[gpui::test]
9417async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
9418 init_test(cx, |settings| {
9419 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9420 });
9421
9422 let mut cx = EditorTestContext::new(cx).await;
9423
9424 let language = Arc::new(Language::new(
9425 LanguageConfig {
9426 brackets: BracketPairConfig {
9427 pairs: vec![
9428 BracketPair {
9429 start: "{".to_string(),
9430 end: "}".to_string(),
9431 close: true,
9432 surround: true,
9433 newline: true,
9434 },
9435 BracketPair {
9436 start: "(".to_string(),
9437 end: ")".to_string(),
9438 close: true,
9439 surround: true,
9440 newline: true,
9441 },
9442 BracketPair {
9443 start: "[".to_string(),
9444 end: "]".to_string(),
9445 close: false,
9446 surround: true,
9447 newline: true,
9448 },
9449 ],
9450 ..Default::default()
9451 },
9452 autoclose_before: "})]".to_string(),
9453 ..Default::default()
9454 },
9455 Some(tree_sitter_rust::LANGUAGE.into()),
9456 ));
9457
9458 cx.language_registry().add(language.clone());
9459 cx.update_buffer(|buffer, cx| {
9460 buffer.set_language(Some(language), cx);
9461 });
9462
9463 cx.set_state(
9464 &"
9465 {(ˇ)}
9466 [[ˇ]]
9467 {(ˇ)}
9468 "
9469 .unindent(),
9470 );
9471
9472 cx.update_editor(|editor, window, cx| {
9473 editor.backspace(&Default::default(), window, cx);
9474 editor.backspace(&Default::default(), window, cx);
9475 });
9476
9477 cx.assert_editor_state(
9478 &"
9479 ˇ
9480 ˇ]]
9481 ˇ
9482 "
9483 .unindent(),
9484 );
9485
9486 cx.update_editor(|editor, window, cx| {
9487 editor.handle_input("{", window, cx);
9488 editor.handle_input("{", window, cx);
9489 editor.move_right(&MoveRight, window, cx);
9490 editor.move_right(&MoveRight, window, cx);
9491 editor.move_left(&MoveLeft, window, cx);
9492 editor.move_left(&MoveLeft, window, cx);
9493 editor.backspace(&Default::default(), window, cx);
9494 });
9495
9496 cx.assert_editor_state(
9497 &"
9498 {ˇ}
9499 {ˇ}]]
9500 {ˇ}
9501 "
9502 .unindent(),
9503 );
9504
9505 cx.update_editor(|editor, window, cx| {
9506 editor.backspace(&Default::default(), window, cx);
9507 });
9508
9509 cx.assert_editor_state(
9510 &"
9511 ˇ
9512 ˇ]]
9513 ˇ
9514 "
9515 .unindent(),
9516 );
9517}
9518
9519#[gpui::test]
9520async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9521 init_test(cx, |_| {});
9522
9523 let language = Arc::new(Language::new(
9524 LanguageConfig::default(),
9525 Some(tree_sitter_rust::LANGUAGE.into()),
9526 ));
9527
9528 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9529 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9530 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9531 editor
9532 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9533 .await;
9534
9535 editor.update_in(cx, |editor, window, cx| {
9536 editor.set_auto_replace_emoji_shortcode(true);
9537
9538 editor.handle_input("Hello ", window, cx);
9539 editor.handle_input(":wave", window, cx);
9540 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9541
9542 editor.handle_input(":", window, cx);
9543 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9544
9545 editor.handle_input(" :smile", window, cx);
9546 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9547
9548 editor.handle_input(":", window, cx);
9549 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9550
9551 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9552 editor.handle_input(":wave", window, cx);
9553 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9554
9555 editor.handle_input(":", window, cx);
9556 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9557
9558 editor.handle_input(":1", window, cx);
9559 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9560
9561 editor.handle_input(":", window, cx);
9562 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9563
9564 // Ensure shortcode does not get replaced when it is part of a word
9565 editor.handle_input(" Test:wave", window, cx);
9566 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9567
9568 editor.handle_input(":", window, cx);
9569 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9570
9571 editor.set_auto_replace_emoji_shortcode(false);
9572
9573 // Ensure shortcode does not get replaced when auto replace is off
9574 editor.handle_input(" :wave", window, cx);
9575 assert_eq!(
9576 editor.text(cx),
9577 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9578 );
9579
9580 editor.handle_input(":", window, cx);
9581 assert_eq!(
9582 editor.text(cx),
9583 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9584 );
9585 });
9586}
9587
9588#[gpui::test]
9589async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9590 init_test(cx, |_| {});
9591
9592 let (text, insertion_ranges) = marked_text_ranges(
9593 indoc! {"
9594 ˇ
9595 "},
9596 false,
9597 );
9598
9599 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9600 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9601
9602 _ = editor.update_in(cx, |editor, window, cx| {
9603 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9604
9605 editor
9606 .insert_snippet(&insertion_ranges, snippet, window, cx)
9607 .unwrap();
9608
9609 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9610 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9611 assert_eq!(editor.text(cx), expected_text);
9612 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9613 }
9614
9615 assert(
9616 editor,
9617 cx,
9618 indoc! {"
9619 type «» =•
9620 "},
9621 );
9622
9623 assert!(editor.context_menu_visible(), "There should be a matches");
9624 });
9625}
9626
9627#[gpui::test]
9628async fn test_snippets(cx: &mut TestAppContext) {
9629 init_test(cx, |_| {});
9630
9631 let mut cx = EditorTestContext::new(cx).await;
9632
9633 cx.set_state(indoc! {"
9634 a.ˇ b
9635 a.ˇ b
9636 a.ˇ b
9637 "});
9638
9639 cx.update_editor(|editor, window, cx| {
9640 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9641 let insertion_ranges = editor
9642 .selections
9643 .all(cx)
9644 .iter()
9645 .map(|s| s.range().clone())
9646 .collect::<Vec<_>>();
9647 editor
9648 .insert_snippet(&insertion_ranges, snippet, window, cx)
9649 .unwrap();
9650 });
9651
9652 cx.assert_editor_state(indoc! {"
9653 a.f(«oneˇ», two, «threeˇ») b
9654 a.f(«oneˇ», two, «threeˇ») b
9655 a.f(«oneˇ», two, «threeˇ») b
9656 "});
9657
9658 // Can't move earlier than the first tab stop
9659 cx.update_editor(|editor, window, cx| {
9660 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9661 });
9662 cx.assert_editor_state(indoc! {"
9663 a.f(«oneˇ», two, «threeˇ») b
9664 a.f(«oneˇ», two, «threeˇ») b
9665 a.f(«oneˇ», two, «threeˇ») b
9666 "});
9667
9668 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9669 cx.assert_editor_state(indoc! {"
9670 a.f(one, «twoˇ», three) b
9671 a.f(one, «twoˇ», three) b
9672 a.f(one, «twoˇ», three) b
9673 "});
9674
9675 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9676 cx.assert_editor_state(indoc! {"
9677 a.f(«oneˇ», two, «threeˇ») b
9678 a.f(«oneˇ», two, «threeˇ») b
9679 a.f(«oneˇ», two, «threeˇ») b
9680 "});
9681
9682 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9683 cx.assert_editor_state(indoc! {"
9684 a.f(one, «twoˇ», three) b
9685 a.f(one, «twoˇ», three) b
9686 a.f(one, «twoˇ», three) b
9687 "});
9688 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9689 cx.assert_editor_state(indoc! {"
9690 a.f(one, two, three)ˇ b
9691 a.f(one, two, three)ˇ b
9692 a.f(one, two, three)ˇ b
9693 "});
9694
9695 // As soon as the last tab stop is reached, snippet state is gone
9696 cx.update_editor(|editor, window, cx| {
9697 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9698 });
9699 cx.assert_editor_state(indoc! {"
9700 a.f(one, two, three)ˇ b
9701 a.f(one, two, three)ˇ b
9702 a.f(one, two, three)ˇ b
9703 "});
9704}
9705
9706#[gpui::test]
9707async fn test_snippet_indentation(cx: &mut TestAppContext) {
9708 init_test(cx, |_| {});
9709
9710 let mut cx = EditorTestContext::new(cx).await;
9711
9712 cx.update_editor(|editor, window, cx| {
9713 let snippet = Snippet::parse(indoc! {"
9714 /*
9715 * Multiline comment with leading indentation
9716 *
9717 * $1
9718 */
9719 $0"})
9720 .unwrap();
9721 let insertion_ranges = editor
9722 .selections
9723 .all(cx)
9724 .iter()
9725 .map(|s| s.range().clone())
9726 .collect::<Vec<_>>();
9727 editor
9728 .insert_snippet(&insertion_ranges, snippet, window, cx)
9729 .unwrap();
9730 });
9731
9732 cx.assert_editor_state(indoc! {"
9733 /*
9734 * Multiline comment with leading indentation
9735 *
9736 * ˇ
9737 */
9738 "});
9739
9740 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9741 cx.assert_editor_state(indoc! {"
9742 /*
9743 * Multiline comment with leading indentation
9744 *
9745 *•
9746 */
9747 ˇ"});
9748}
9749
9750#[gpui::test]
9751async fn test_document_format_during_save(cx: &mut TestAppContext) {
9752 init_test(cx, |_| {});
9753
9754 let fs = FakeFs::new(cx.executor());
9755 fs.insert_file(path!("/file.rs"), Default::default()).await;
9756
9757 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9758
9759 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9760 language_registry.add(rust_lang());
9761 let mut fake_servers = language_registry.register_fake_lsp(
9762 "Rust",
9763 FakeLspAdapter {
9764 capabilities: lsp::ServerCapabilities {
9765 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9766 ..Default::default()
9767 },
9768 ..Default::default()
9769 },
9770 );
9771
9772 let buffer = project
9773 .update(cx, |project, cx| {
9774 project.open_local_buffer(path!("/file.rs"), cx)
9775 })
9776 .await
9777 .unwrap();
9778
9779 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9780 let (editor, cx) = cx.add_window_view(|window, cx| {
9781 build_editor_with_project(project.clone(), buffer, window, cx)
9782 });
9783 editor.update_in(cx, |editor, window, cx| {
9784 editor.set_text("one\ntwo\nthree\n", window, cx)
9785 });
9786 assert!(cx.read(|cx| editor.is_dirty(cx)));
9787
9788 cx.executor().start_waiting();
9789 let fake_server = fake_servers.next().await.unwrap();
9790
9791 {
9792 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9793 move |params, _| async move {
9794 assert_eq!(
9795 params.text_document.uri,
9796 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9797 );
9798 assert_eq!(params.options.tab_size, 4);
9799 Ok(Some(vec![lsp::TextEdit::new(
9800 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9801 ", ".to_string(),
9802 )]))
9803 },
9804 );
9805 let save = editor
9806 .update_in(cx, |editor, window, cx| {
9807 editor.save(
9808 SaveOptions {
9809 format: true,
9810 autosave: false,
9811 },
9812 project.clone(),
9813 window,
9814 cx,
9815 )
9816 })
9817 .unwrap();
9818 cx.executor().start_waiting();
9819 save.await;
9820
9821 assert_eq!(
9822 editor.update(cx, |editor, cx| editor.text(cx)),
9823 "one, two\nthree\n"
9824 );
9825 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9826 }
9827
9828 {
9829 editor.update_in(cx, |editor, window, cx| {
9830 editor.set_text("one\ntwo\nthree\n", window, cx)
9831 });
9832 assert!(cx.read(|cx| editor.is_dirty(cx)));
9833
9834 // Ensure we can still save even if formatting hangs.
9835 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9836 move |params, _| async move {
9837 assert_eq!(
9838 params.text_document.uri,
9839 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9840 );
9841 futures::future::pending::<()>().await;
9842 unreachable!()
9843 },
9844 );
9845 let save = editor
9846 .update_in(cx, |editor, window, cx| {
9847 editor.save(
9848 SaveOptions {
9849 format: true,
9850 autosave: false,
9851 },
9852 project.clone(),
9853 window,
9854 cx,
9855 )
9856 })
9857 .unwrap();
9858 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9859 cx.executor().start_waiting();
9860 save.await;
9861 assert_eq!(
9862 editor.update(cx, |editor, cx| editor.text(cx)),
9863 "one\ntwo\nthree\n"
9864 );
9865 }
9866
9867 // Set rust language override and assert overridden tabsize is sent to language server
9868 update_test_language_settings(cx, |settings| {
9869 settings.languages.0.insert(
9870 "Rust".into(),
9871 LanguageSettingsContent {
9872 tab_size: NonZeroU32::new(8),
9873 ..Default::default()
9874 },
9875 );
9876 });
9877
9878 {
9879 editor.update_in(cx, |editor, window, cx| {
9880 editor.set_text("somehting_new\n", window, cx)
9881 });
9882 assert!(cx.read(|cx| editor.is_dirty(cx)));
9883 let _formatting_request_signal = fake_server
9884 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9885 assert_eq!(
9886 params.text_document.uri,
9887 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9888 );
9889 assert_eq!(params.options.tab_size, 8);
9890 Ok(Some(vec![]))
9891 });
9892 let save = editor
9893 .update_in(cx, |editor, window, cx| {
9894 editor.save(
9895 SaveOptions {
9896 format: true,
9897 autosave: false,
9898 },
9899 project.clone(),
9900 window,
9901 cx,
9902 )
9903 })
9904 .unwrap();
9905 cx.executor().start_waiting();
9906 save.await;
9907 }
9908}
9909
9910#[gpui::test]
9911async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
9912 init_test(cx, |settings| {
9913 settings.defaults.ensure_final_newline_on_save = Some(false);
9914 });
9915
9916 let fs = FakeFs::new(cx.executor());
9917 fs.insert_file(path!("/file.txt"), "foo".into()).await;
9918
9919 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
9920
9921 let buffer = project
9922 .update(cx, |project, cx| {
9923 project.open_local_buffer(path!("/file.txt"), cx)
9924 })
9925 .await
9926 .unwrap();
9927
9928 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9929 let (editor, cx) = cx.add_window_view(|window, cx| {
9930 build_editor_with_project(project.clone(), buffer, window, cx)
9931 });
9932 editor.update_in(cx, |editor, window, cx| {
9933 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9934 s.select_ranges([0..0])
9935 });
9936 });
9937 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9938
9939 editor.update_in(cx, |editor, window, cx| {
9940 editor.handle_input("\n", window, cx)
9941 });
9942 cx.run_until_parked();
9943 save(&editor, &project, cx).await;
9944 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9945
9946 editor.update_in(cx, |editor, window, cx| {
9947 editor.undo(&Default::default(), window, cx);
9948 });
9949 save(&editor, &project, cx).await;
9950 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9951
9952 editor.update_in(cx, |editor, window, cx| {
9953 editor.redo(&Default::default(), window, cx);
9954 });
9955 cx.run_until_parked();
9956 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9957
9958 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
9959 let save = editor
9960 .update_in(cx, |editor, window, cx| {
9961 editor.save(
9962 SaveOptions {
9963 format: true,
9964 autosave: false,
9965 },
9966 project.clone(),
9967 window,
9968 cx,
9969 )
9970 })
9971 .unwrap();
9972 cx.executor().start_waiting();
9973 save.await;
9974 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9975 }
9976}
9977
9978#[gpui::test]
9979async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9980 init_test(cx, |_| {});
9981
9982 let cols = 4;
9983 let rows = 10;
9984 let sample_text_1 = sample_text(rows, cols, 'a');
9985 assert_eq!(
9986 sample_text_1,
9987 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9988 );
9989 let sample_text_2 = sample_text(rows, cols, 'l');
9990 assert_eq!(
9991 sample_text_2,
9992 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9993 );
9994 let sample_text_3 = sample_text(rows, cols, 'v');
9995 assert_eq!(
9996 sample_text_3,
9997 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9998 );
9999
10000 let fs = FakeFs::new(cx.executor());
10001 fs.insert_tree(
10002 path!("/a"),
10003 json!({
10004 "main.rs": sample_text_1,
10005 "other.rs": sample_text_2,
10006 "lib.rs": sample_text_3,
10007 }),
10008 )
10009 .await;
10010
10011 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10012 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10013 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10014
10015 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10016 language_registry.add(rust_lang());
10017 let mut fake_servers = language_registry.register_fake_lsp(
10018 "Rust",
10019 FakeLspAdapter {
10020 capabilities: lsp::ServerCapabilities {
10021 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10022 ..Default::default()
10023 },
10024 ..Default::default()
10025 },
10026 );
10027
10028 let worktree = project.update(cx, |project, cx| {
10029 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
10030 assert_eq!(worktrees.len(), 1);
10031 worktrees.pop().unwrap()
10032 });
10033 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
10034
10035 let buffer_1 = project
10036 .update(cx, |project, cx| {
10037 project.open_buffer((worktree_id, "main.rs"), cx)
10038 })
10039 .await
10040 .unwrap();
10041 let buffer_2 = project
10042 .update(cx, |project, cx| {
10043 project.open_buffer((worktree_id, "other.rs"), cx)
10044 })
10045 .await
10046 .unwrap();
10047 let buffer_3 = project
10048 .update(cx, |project, cx| {
10049 project.open_buffer((worktree_id, "lib.rs"), cx)
10050 })
10051 .await
10052 .unwrap();
10053
10054 let multi_buffer = cx.new(|cx| {
10055 let mut multi_buffer = MultiBuffer::new(ReadWrite);
10056 multi_buffer.push_excerpts(
10057 buffer_1.clone(),
10058 [
10059 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10060 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10061 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10062 ],
10063 cx,
10064 );
10065 multi_buffer.push_excerpts(
10066 buffer_2.clone(),
10067 [
10068 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10069 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10070 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10071 ],
10072 cx,
10073 );
10074 multi_buffer.push_excerpts(
10075 buffer_3.clone(),
10076 [
10077 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10078 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10079 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10080 ],
10081 cx,
10082 );
10083 multi_buffer
10084 });
10085 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
10086 Editor::new(
10087 EditorMode::full(),
10088 multi_buffer,
10089 Some(project.clone()),
10090 window,
10091 cx,
10092 )
10093 });
10094
10095 multi_buffer_editor.update_in(cx, |editor, window, cx| {
10096 editor.change_selections(
10097 SelectionEffects::scroll(Autoscroll::Next),
10098 window,
10099 cx,
10100 |s| s.select_ranges(Some(1..2)),
10101 );
10102 editor.insert("|one|two|three|", window, cx);
10103 });
10104 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
10105 multi_buffer_editor.update_in(cx, |editor, window, cx| {
10106 editor.change_selections(
10107 SelectionEffects::scroll(Autoscroll::Next),
10108 window,
10109 cx,
10110 |s| s.select_ranges(Some(60..70)),
10111 );
10112 editor.insert("|four|five|six|", window, cx);
10113 });
10114 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
10115
10116 // First two buffers should be edited, but not the third one.
10117 assert_eq!(
10118 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
10119 "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}",
10120 );
10121 buffer_1.update(cx, |buffer, _| {
10122 assert!(buffer.is_dirty());
10123 assert_eq!(
10124 buffer.text(),
10125 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
10126 )
10127 });
10128 buffer_2.update(cx, |buffer, _| {
10129 assert!(buffer.is_dirty());
10130 assert_eq!(
10131 buffer.text(),
10132 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
10133 )
10134 });
10135 buffer_3.update(cx, |buffer, _| {
10136 assert!(!buffer.is_dirty());
10137 assert_eq!(buffer.text(), sample_text_3,)
10138 });
10139 cx.executor().run_until_parked();
10140
10141 cx.executor().start_waiting();
10142 let save = multi_buffer_editor
10143 .update_in(cx, |editor, window, cx| {
10144 editor.save(
10145 SaveOptions {
10146 format: true,
10147 autosave: false,
10148 },
10149 project.clone(),
10150 window,
10151 cx,
10152 )
10153 })
10154 .unwrap();
10155
10156 let fake_server = fake_servers.next().await.unwrap();
10157 fake_server
10158 .server
10159 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
10160 Ok(Some(vec![lsp::TextEdit::new(
10161 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10162 format!("[{} formatted]", params.text_document.uri),
10163 )]))
10164 })
10165 .detach();
10166 save.await;
10167
10168 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
10169 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
10170 assert_eq!(
10171 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
10172 uri!(
10173 "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}"
10174 ),
10175 );
10176 buffer_1.update(cx, |buffer, _| {
10177 assert!(!buffer.is_dirty());
10178 assert_eq!(
10179 buffer.text(),
10180 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
10181 )
10182 });
10183 buffer_2.update(cx, |buffer, _| {
10184 assert!(!buffer.is_dirty());
10185 assert_eq!(
10186 buffer.text(),
10187 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
10188 )
10189 });
10190 buffer_3.update(cx, |buffer, _| {
10191 assert!(!buffer.is_dirty());
10192 assert_eq!(buffer.text(), sample_text_3,)
10193 });
10194}
10195
10196#[gpui::test]
10197async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
10198 init_test(cx, |_| {});
10199
10200 let fs = FakeFs::new(cx.executor());
10201 fs.insert_tree(
10202 path!("/dir"),
10203 json!({
10204 "file1.rs": "fn main() { println!(\"hello\"); }",
10205 "file2.rs": "fn test() { println!(\"test\"); }",
10206 "file3.rs": "fn other() { println!(\"other\"); }\n",
10207 }),
10208 )
10209 .await;
10210
10211 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
10212 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10213 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10214
10215 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10216 language_registry.add(rust_lang());
10217
10218 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
10219 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
10220
10221 // Open three buffers
10222 let buffer_1 = project
10223 .update(cx, |project, cx| {
10224 project.open_buffer((worktree_id, "file1.rs"), cx)
10225 })
10226 .await
10227 .unwrap();
10228 let buffer_2 = project
10229 .update(cx, |project, cx| {
10230 project.open_buffer((worktree_id, "file2.rs"), cx)
10231 })
10232 .await
10233 .unwrap();
10234 let buffer_3 = project
10235 .update(cx, |project, cx| {
10236 project.open_buffer((worktree_id, "file3.rs"), cx)
10237 })
10238 .await
10239 .unwrap();
10240
10241 // Create a multi-buffer with all three buffers
10242 let multi_buffer = cx.new(|cx| {
10243 let mut multi_buffer = MultiBuffer::new(ReadWrite);
10244 multi_buffer.push_excerpts(
10245 buffer_1.clone(),
10246 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10247 cx,
10248 );
10249 multi_buffer.push_excerpts(
10250 buffer_2.clone(),
10251 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10252 cx,
10253 );
10254 multi_buffer.push_excerpts(
10255 buffer_3.clone(),
10256 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10257 cx,
10258 );
10259 multi_buffer
10260 });
10261
10262 let editor = cx.new_window_entity(|window, cx| {
10263 Editor::new(
10264 EditorMode::full(),
10265 multi_buffer,
10266 Some(project.clone()),
10267 window,
10268 cx,
10269 )
10270 });
10271
10272 // Edit only the first buffer
10273 editor.update_in(cx, |editor, window, cx| {
10274 editor.change_selections(
10275 SelectionEffects::scroll(Autoscroll::Next),
10276 window,
10277 cx,
10278 |s| s.select_ranges(Some(10..10)),
10279 );
10280 editor.insert("// edited", window, cx);
10281 });
10282
10283 // Verify that only buffer 1 is dirty
10284 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
10285 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10286 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10287
10288 // Get write counts after file creation (files were created with initial content)
10289 // We expect each file to have been written once during creation
10290 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
10291 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
10292 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
10293
10294 // Perform autosave
10295 let save_task = editor.update_in(cx, |editor, window, cx| {
10296 editor.save(
10297 SaveOptions {
10298 format: true,
10299 autosave: true,
10300 },
10301 project.clone(),
10302 window,
10303 cx,
10304 )
10305 });
10306 save_task.await.unwrap();
10307
10308 // Only the dirty buffer should have been saved
10309 assert_eq!(
10310 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10311 1,
10312 "Buffer 1 was dirty, so it should have been written once during autosave"
10313 );
10314 assert_eq!(
10315 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10316 0,
10317 "Buffer 2 was clean, so it should not have been written during autosave"
10318 );
10319 assert_eq!(
10320 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10321 0,
10322 "Buffer 3 was clean, so it should not have been written during autosave"
10323 );
10324
10325 // Verify buffer states after autosave
10326 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10327 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10328 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10329
10330 // Now perform a manual save (format = true)
10331 let save_task = editor.update_in(cx, |editor, window, cx| {
10332 editor.save(
10333 SaveOptions {
10334 format: true,
10335 autosave: false,
10336 },
10337 project.clone(),
10338 window,
10339 cx,
10340 )
10341 });
10342 save_task.await.unwrap();
10343
10344 // During manual save, clean buffers don't get written to disk
10345 // They just get did_save called for language server notifications
10346 assert_eq!(
10347 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10348 1,
10349 "Buffer 1 should only have been written once total (during autosave, not manual save)"
10350 );
10351 assert_eq!(
10352 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10353 0,
10354 "Buffer 2 should not have been written at all"
10355 );
10356 assert_eq!(
10357 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10358 0,
10359 "Buffer 3 should not have been written at all"
10360 );
10361}
10362
10363async fn setup_range_format_test(
10364 cx: &mut TestAppContext,
10365) -> (
10366 Entity<Project>,
10367 Entity<Editor>,
10368 &mut gpui::VisualTestContext,
10369 lsp::FakeLanguageServer,
10370) {
10371 init_test(cx, |_| {});
10372
10373 let fs = FakeFs::new(cx.executor());
10374 fs.insert_file(path!("/file.rs"), Default::default()).await;
10375
10376 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10377
10378 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10379 language_registry.add(rust_lang());
10380 let mut fake_servers = language_registry.register_fake_lsp(
10381 "Rust",
10382 FakeLspAdapter {
10383 capabilities: lsp::ServerCapabilities {
10384 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
10385 ..lsp::ServerCapabilities::default()
10386 },
10387 ..FakeLspAdapter::default()
10388 },
10389 );
10390
10391 let buffer = project
10392 .update(cx, |project, cx| {
10393 project.open_local_buffer(path!("/file.rs"), cx)
10394 })
10395 .await
10396 .unwrap();
10397
10398 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10399 let (editor, cx) = cx.add_window_view(|window, cx| {
10400 build_editor_with_project(project.clone(), buffer, window, cx)
10401 });
10402
10403 cx.executor().start_waiting();
10404 let fake_server = fake_servers.next().await.unwrap();
10405
10406 (project, editor, cx, fake_server)
10407}
10408
10409#[gpui::test]
10410async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
10411 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10412
10413 editor.update_in(cx, |editor, window, cx| {
10414 editor.set_text("one\ntwo\nthree\n", window, cx)
10415 });
10416 assert!(cx.read(|cx| editor.is_dirty(cx)));
10417
10418 let save = editor
10419 .update_in(cx, |editor, window, cx| {
10420 editor.save(
10421 SaveOptions {
10422 format: true,
10423 autosave: false,
10424 },
10425 project.clone(),
10426 window,
10427 cx,
10428 )
10429 })
10430 .unwrap();
10431 fake_server
10432 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10433 assert_eq!(
10434 params.text_document.uri,
10435 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10436 );
10437 assert_eq!(params.options.tab_size, 4);
10438 Ok(Some(vec![lsp::TextEdit::new(
10439 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10440 ", ".to_string(),
10441 )]))
10442 })
10443 .next()
10444 .await;
10445 cx.executor().start_waiting();
10446 save.await;
10447 assert_eq!(
10448 editor.update(cx, |editor, cx| editor.text(cx)),
10449 "one, two\nthree\n"
10450 );
10451 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10452}
10453
10454#[gpui::test]
10455async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
10456 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10457
10458 editor.update_in(cx, |editor, window, cx| {
10459 editor.set_text("one\ntwo\nthree\n", window, cx)
10460 });
10461 assert!(cx.read(|cx| editor.is_dirty(cx)));
10462
10463 // Test that save still works when formatting hangs
10464 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
10465 move |params, _| async move {
10466 assert_eq!(
10467 params.text_document.uri,
10468 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10469 );
10470 futures::future::pending::<()>().await;
10471 unreachable!()
10472 },
10473 );
10474 let save = editor
10475 .update_in(cx, |editor, window, cx| {
10476 editor.save(
10477 SaveOptions {
10478 format: true,
10479 autosave: false,
10480 },
10481 project.clone(),
10482 window,
10483 cx,
10484 )
10485 })
10486 .unwrap();
10487 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10488 cx.executor().start_waiting();
10489 save.await;
10490 assert_eq!(
10491 editor.update(cx, |editor, cx| editor.text(cx)),
10492 "one\ntwo\nthree\n"
10493 );
10494 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10495}
10496
10497#[gpui::test]
10498async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
10499 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10500
10501 // Buffer starts clean, no formatting should be requested
10502 let save = editor
10503 .update_in(cx, |editor, window, cx| {
10504 editor.save(
10505 SaveOptions {
10506 format: false,
10507 autosave: false,
10508 },
10509 project.clone(),
10510 window,
10511 cx,
10512 )
10513 })
10514 .unwrap();
10515 let _pending_format_request = fake_server
10516 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
10517 panic!("Should not be invoked");
10518 })
10519 .next();
10520 cx.executor().start_waiting();
10521 save.await;
10522 cx.run_until_parked();
10523}
10524
10525#[gpui::test]
10526async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
10527 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10528
10529 // Set Rust language override and assert overridden tabsize is sent to language server
10530 update_test_language_settings(cx, |settings| {
10531 settings.languages.0.insert(
10532 "Rust".into(),
10533 LanguageSettingsContent {
10534 tab_size: NonZeroU32::new(8),
10535 ..Default::default()
10536 },
10537 );
10538 });
10539
10540 editor.update_in(cx, |editor, window, cx| {
10541 editor.set_text("something_new\n", window, cx)
10542 });
10543 assert!(cx.read(|cx| editor.is_dirty(cx)));
10544 let save = editor
10545 .update_in(cx, |editor, window, cx| {
10546 editor.save(
10547 SaveOptions {
10548 format: true,
10549 autosave: false,
10550 },
10551 project.clone(),
10552 window,
10553 cx,
10554 )
10555 })
10556 .unwrap();
10557 fake_server
10558 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10559 assert_eq!(
10560 params.text_document.uri,
10561 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10562 );
10563 assert_eq!(params.options.tab_size, 8);
10564 Ok(Some(Vec::new()))
10565 })
10566 .next()
10567 .await;
10568 save.await;
10569}
10570
10571#[gpui::test]
10572async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
10573 init_test(cx, |settings| {
10574 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
10575 Formatter::LanguageServer { name: None },
10576 )))
10577 });
10578
10579 let fs = FakeFs::new(cx.executor());
10580 fs.insert_file(path!("/file.rs"), Default::default()).await;
10581
10582 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10583
10584 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10585 language_registry.add(Arc::new(Language::new(
10586 LanguageConfig {
10587 name: "Rust".into(),
10588 matcher: LanguageMatcher {
10589 path_suffixes: vec!["rs".to_string()],
10590 ..Default::default()
10591 },
10592 ..LanguageConfig::default()
10593 },
10594 Some(tree_sitter_rust::LANGUAGE.into()),
10595 )));
10596 update_test_language_settings(cx, |settings| {
10597 // Enable Prettier formatting for the same buffer, and ensure
10598 // LSP is called instead of Prettier.
10599 settings.defaults.prettier = Some(PrettierSettings {
10600 allowed: true,
10601 ..PrettierSettings::default()
10602 });
10603 });
10604 let mut fake_servers = language_registry.register_fake_lsp(
10605 "Rust",
10606 FakeLspAdapter {
10607 capabilities: lsp::ServerCapabilities {
10608 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10609 ..Default::default()
10610 },
10611 ..Default::default()
10612 },
10613 );
10614
10615 let buffer = project
10616 .update(cx, |project, cx| {
10617 project.open_local_buffer(path!("/file.rs"), cx)
10618 })
10619 .await
10620 .unwrap();
10621
10622 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10623 let (editor, cx) = cx.add_window_view(|window, cx| {
10624 build_editor_with_project(project.clone(), buffer, window, cx)
10625 });
10626 editor.update_in(cx, |editor, window, cx| {
10627 editor.set_text("one\ntwo\nthree\n", window, cx)
10628 });
10629
10630 cx.executor().start_waiting();
10631 let fake_server = fake_servers.next().await.unwrap();
10632
10633 let format = editor
10634 .update_in(cx, |editor, window, cx| {
10635 editor.perform_format(
10636 project.clone(),
10637 FormatTrigger::Manual,
10638 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10639 window,
10640 cx,
10641 )
10642 })
10643 .unwrap();
10644 fake_server
10645 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10646 assert_eq!(
10647 params.text_document.uri,
10648 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10649 );
10650 assert_eq!(params.options.tab_size, 4);
10651 Ok(Some(vec![lsp::TextEdit::new(
10652 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10653 ", ".to_string(),
10654 )]))
10655 })
10656 .next()
10657 .await;
10658 cx.executor().start_waiting();
10659 format.await;
10660 assert_eq!(
10661 editor.update(cx, |editor, cx| editor.text(cx)),
10662 "one, two\nthree\n"
10663 );
10664
10665 editor.update_in(cx, |editor, window, cx| {
10666 editor.set_text("one\ntwo\nthree\n", window, cx)
10667 });
10668 // Ensure we don't lock if formatting hangs.
10669 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10670 move |params, _| async move {
10671 assert_eq!(
10672 params.text_document.uri,
10673 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10674 );
10675 futures::future::pending::<()>().await;
10676 unreachable!()
10677 },
10678 );
10679 let format = editor
10680 .update_in(cx, |editor, window, cx| {
10681 editor.perform_format(
10682 project,
10683 FormatTrigger::Manual,
10684 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10685 window,
10686 cx,
10687 )
10688 })
10689 .unwrap();
10690 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10691 cx.executor().start_waiting();
10692 format.await;
10693 assert_eq!(
10694 editor.update(cx, |editor, cx| editor.text(cx)),
10695 "one\ntwo\nthree\n"
10696 );
10697}
10698
10699#[gpui::test]
10700async fn test_multiple_formatters(cx: &mut TestAppContext) {
10701 init_test(cx, |settings| {
10702 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10703 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10704 Formatter::LanguageServer { name: None },
10705 Formatter::CodeActions(
10706 [
10707 ("code-action-1".into(), true),
10708 ("code-action-2".into(), true),
10709 ]
10710 .into_iter()
10711 .collect(),
10712 ),
10713 ])))
10714 });
10715
10716 let fs = FakeFs::new(cx.executor());
10717 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10718 .await;
10719
10720 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10721 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10722 language_registry.add(rust_lang());
10723
10724 let mut fake_servers = language_registry.register_fake_lsp(
10725 "Rust",
10726 FakeLspAdapter {
10727 capabilities: lsp::ServerCapabilities {
10728 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10729 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10730 commands: vec!["the-command-for-code-action-1".into()],
10731 ..Default::default()
10732 }),
10733 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10734 ..Default::default()
10735 },
10736 ..Default::default()
10737 },
10738 );
10739
10740 let buffer = project
10741 .update(cx, |project, cx| {
10742 project.open_local_buffer(path!("/file.rs"), cx)
10743 })
10744 .await
10745 .unwrap();
10746
10747 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10748 let (editor, cx) = cx.add_window_view(|window, cx| {
10749 build_editor_with_project(project.clone(), buffer, window, cx)
10750 });
10751
10752 cx.executor().start_waiting();
10753
10754 let fake_server = fake_servers.next().await.unwrap();
10755 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10756 move |_params, _| async move {
10757 Ok(Some(vec![lsp::TextEdit::new(
10758 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10759 "applied-formatting\n".to_string(),
10760 )]))
10761 },
10762 );
10763 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10764 move |params, _| async move {
10765 assert_eq!(
10766 params.context.only,
10767 Some(vec!["code-action-1".into(), "code-action-2".into()])
10768 );
10769 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10770 Ok(Some(vec![
10771 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10772 kind: Some("code-action-1".into()),
10773 edit: Some(lsp::WorkspaceEdit::new(
10774 [(
10775 uri.clone(),
10776 vec![lsp::TextEdit::new(
10777 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10778 "applied-code-action-1-edit\n".to_string(),
10779 )],
10780 )]
10781 .into_iter()
10782 .collect(),
10783 )),
10784 command: Some(lsp::Command {
10785 command: "the-command-for-code-action-1".into(),
10786 ..Default::default()
10787 }),
10788 ..Default::default()
10789 }),
10790 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10791 kind: Some("code-action-2".into()),
10792 edit: Some(lsp::WorkspaceEdit::new(
10793 [(
10794 uri.clone(),
10795 vec![lsp::TextEdit::new(
10796 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10797 "applied-code-action-2-edit\n".to_string(),
10798 )],
10799 )]
10800 .into_iter()
10801 .collect(),
10802 )),
10803 ..Default::default()
10804 }),
10805 ]))
10806 },
10807 );
10808
10809 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10810 move |params, _| async move { Ok(params) }
10811 });
10812
10813 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10814 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10815 let fake = fake_server.clone();
10816 let lock = command_lock.clone();
10817 move |params, _| {
10818 assert_eq!(params.command, "the-command-for-code-action-1");
10819 let fake = fake.clone();
10820 let lock = lock.clone();
10821 async move {
10822 lock.lock().await;
10823 fake.server
10824 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10825 label: None,
10826 edit: lsp::WorkspaceEdit {
10827 changes: Some(
10828 [(
10829 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10830 vec![lsp::TextEdit {
10831 range: lsp::Range::new(
10832 lsp::Position::new(0, 0),
10833 lsp::Position::new(0, 0),
10834 ),
10835 new_text: "applied-code-action-1-command\n".into(),
10836 }],
10837 )]
10838 .into_iter()
10839 .collect(),
10840 ),
10841 ..Default::default()
10842 },
10843 })
10844 .await
10845 .into_response()
10846 .unwrap();
10847 Ok(Some(json!(null)))
10848 }
10849 }
10850 });
10851
10852 cx.executor().start_waiting();
10853 editor
10854 .update_in(cx, |editor, window, cx| {
10855 editor.perform_format(
10856 project.clone(),
10857 FormatTrigger::Manual,
10858 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10859 window,
10860 cx,
10861 )
10862 })
10863 .unwrap()
10864 .await;
10865 editor.update(cx, |editor, cx| {
10866 assert_eq!(
10867 editor.text(cx),
10868 r#"
10869 applied-code-action-2-edit
10870 applied-code-action-1-command
10871 applied-code-action-1-edit
10872 applied-formatting
10873 one
10874 two
10875 three
10876 "#
10877 .unindent()
10878 );
10879 });
10880
10881 editor.update_in(cx, |editor, window, cx| {
10882 editor.undo(&Default::default(), window, cx);
10883 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10884 });
10885
10886 // Perform a manual edit while waiting for an LSP command
10887 // that's being run as part of a formatting code action.
10888 let lock_guard = command_lock.lock().await;
10889 let format = editor
10890 .update_in(cx, |editor, window, cx| {
10891 editor.perform_format(
10892 project.clone(),
10893 FormatTrigger::Manual,
10894 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10895 window,
10896 cx,
10897 )
10898 })
10899 .unwrap();
10900 cx.run_until_parked();
10901 editor.update(cx, |editor, cx| {
10902 assert_eq!(
10903 editor.text(cx),
10904 r#"
10905 applied-code-action-1-edit
10906 applied-formatting
10907 one
10908 two
10909 three
10910 "#
10911 .unindent()
10912 );
10913
10914 editor.buffer.update(cx, |buffer, cx| {
10915 let ix = buffer.len(cx);
10916 buffer.edit([(ix..ix, "edited\n")], None, cx);
10917 });
10918 });
10919
10920 // Allow the LSP command to proceed. Because the buffer was edited,
10921 // the second code action will not be run.
10922 drop(lock_guard);
10923 format.await;
10924 editor.update_in(cx, |editor, window, cx| {
10925 assert_eq!(
10926 editor.text(cx),
10927 r#"
10928 applied-code-action-1-command
10929 applied-code-action-1-edit
10930 applied-formatting
10931 one
10932 two
10933 three
10934 edited
10935 "#
10936 .unindent()
10937 );
10938
10939 // The manual edit is undone first, because it is the last thing the user did
10940 // (even though the command completed afterwards).
10941 editor.undo(&Default::default(), window, cx);
10942 assert_eq!(
10943 editor.text(cx),
10944 r#"
10945 applied-code-action-1-command
10946 applied-code-action-1-edit
10947 applied-formatting
10948 one
10949 two
10950 three
10951 "#
10952 .unindent()
10953 );
10954
10955 // All the formatting (including the command, which completed after the manual edit)
10956 // is undone together.
10957 editor.undo(&Default::default(), window, cx);
10958 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10959 });
10960}
10961
10962#[gpui::test]
10963async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10964 init_test(cx, |settings| {
10965 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10966 Formatter::LanguageServer { name: None },
10967 ])))
10968 });
10969
10970 let fs = FakeFs::new(cx.executor());
10971 fs.insert_file(path!("/file.ts"), Default::default()).await;
10972
10973 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10974
10975 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10976 language_registry.add(Arc::new(Language::new(
10977 LanguageConfig {
10978 name: "TypeScript".into(),
10979 matcher: LanguageMatcher {
10980 path_suffixes: vec!["ts".to_string()],
10981 ..Default::default()
10982 },
10983 ..LanguageConfig::default()
10984 },
10985 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10986 )));
10987 update_test_language_settings(cx, |settings| {
10988 settings.defaults.prettier = Some(PrettierSettings {
10989 allowed: true,
10990 ..PrettierSettings::default()
10991 });
10992 });
10993 let mut fake_servers = language_registry.register_fake_lsp(
10994 "TypeScript",
10995 FakeLspAdapter {
10996 capabilities: lsp::ServerCapabilities {
10997 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10998 ..Default::default()
10999 },
11000 ..Default::default()
11001 },
11002 );
11003
11004 let buffer = project
11005 .update(cx, |project, cx| {
11006 project.open_local_buffer(path!("/file.ts"), cx)
11007 })
11008 .await
11009 .unwrap();
11010
11011 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11012 let (editor, cx) = cx.add_window_view(|window, cx| {
11013 build_editor_with_project(project.clone(), buffer, window, cx)
11014 });
11015 editor.update_in(cx, |editor, window, cx| {
11016 editor.set_text(
11017 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
11018 window,
11019 cx,
11020 )
11021 });
11022
11023 cx.executor().start_waiting();
11024 let fake_server = fake_servers.next().await.unwrap();
11025
11026 let format = editor
11027 .update_in(cx, |editor, window, cx| {
11028 editor.perform_code_action_kind(
11029 project.clone(),
11030 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
11031 window,
11032 cx,
11033 )
11034 })
11035 .unwrap();
11036 fake_server
11037 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
11038 assert_eq!(
11039 params.text_document.uri,
11040 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
11041 );
11042 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
11043 lsp::CodeAction {
11044 title: "Organize Imports".to_string(),
11045 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
11046 edit: Some(lsp::WorkspaceEdit {
11047 changes: Some(
11048 [(
11049 params.text_document.uri.clone(),
11050 vec![lsp::TextEdit::new(
11051 lsp::Range::new(
11052 lsp::Position::new(1, 0),
11053 lsp::Position::new(2, 0),
11054 ),
11055 "".to_string(),
11056 )],
11057 )]
11058 .into_iter()
11059 .collect(),
11060 ),
11061 ..Default::default()
11062 }),
11063 ..Default::default()
11064 },
11065 )]))
11066 })
11067 .next()
11068 .await;
11069 cx.executor().start_waiting();
11070 format.await;
11071 assert_eq!(
11072 editor.update(cx, |editor, cx| editor.text(cx)),
11073 "import { a } from 'module';\n\nconst x = a;\n"
11074 );
11075
11076 editor.update_in(cx, |editor, window, cx| {
11077 editor.set_text(
11078 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
11079 window,
11080 cx,
11081 )
11082 });
11083 // Ensure we don't lock if code action hangs.
11084 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11085 move |params, _| async move {
11086 assert_eq!(
11087 params.text_document.uri,
11088 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
11089 );
11090 futures::future::pending::<()>().await;
11091 unreachable!()
11092 },
11093 );
11094 let format = editor
11095 .update_in(cx, |editor, window, cx| {
11096 editor.perform_code_action_kind(
11097 project,
11098 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
11099 window,
11100 cx,
11101 )
11102 })
11103 .unwrap();
11104 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
11105 cx.executor().start_waiting();
11106 format.await;
11107 assert_eq!(
11108 editor.update(cx, |editor, cx| editor.text(cx)),
11109 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
11110 );
11111}
11112
11113#[gpui::test]
11114async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
11115 init_test(cx, |_| {});
11116
11117 let mut cx = EditorLspTestContext::new_rust(
11118 lsp::ServerCapabilities {
11119 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11120 ..Default::default()
11121 },
11122 cx,
11123 )
11124 .await;
11125
11126 cx.set_state(indoc! {"
11127 one.twoˇ
11128 "});
11129
11130 // The format request takes a long time. When it completes, it inserts
11131 // a newline and an indent before the `.`
11132 cx.lsp
11133 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
11134 let executor = cx.background_executor().clone();
11135 async move {
11136 executor.timer(Duration::from_millis(100)).await;
11137 Ok(Some(vec![lsp::TextEdit {
11138 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
11139 new_text: "\n ".into(),
11140 }]))
11141 }
11142 });
11143
11144 // Submit a format request.
11145 let format_1 = cx
11146 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
11147 .unwrap();
11148 cx.executor().run_until_parked();
11149
11150 // Submit a second format request.
11151 let format_2 = cx
11152 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
11153 .unwrap();
11154 cx.executor().run_until_parked();
11155
11156 // Wait for both format requests to complete
11157 cx.executor().advance_clock(Duration::from_millis(200));
11158 cx.executor().start_waiting();
11159 format_1.await.unwrap();
11160 cx.executor().start_waiting();
11161 format_2.await.unwrap();
11162
11163 // The formatting edits only happens once.
11164 cx.assert_editor_state(indoc! {"
11165 one
11166 .twoˇ
11167 "});
11168}
11169
11170#[gpui::test]
11171async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
11172 init_test(cx, |settings| {
11173 settings.defaults.formatter = Some(SelectedFormatter::Auto)
11174 });
11175
11176 let mut cx = EditorLspTestContext::new_rust(
11177 lsp::ServerCapabilities {
11178 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11179 ..Default::default()
11180 },
11181 cx,
11182 )
11183 .await;
11184
11185 // Set up a buffer white some trailing whitespace and no trailing newline.
11186 cx.set_state(
11187 &[
11188 "one ", //
11189 "twoˇ", //
11190 "three ", //
11191 "four", //
11192 ]
11193 .join("\n"),
11194 );
11195
11196 // Submit a format request.
11197 let format = cx
11198 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
11199 .unwrap();
11200
11201 // Record which buffer changes have been sent to the language server
11202 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
11203 cx.lsp
11204 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
11205 let buffer_changes = buffer_changes.clone();
11206 move |params, _| {
11207 buffer_changes.lock().extend(
11208 params
11209 .content_changes
11210 .into_iter()
11211 .map(|e| (e.range.unwrap(), e.text)),
11212 );
11213 }
11214 });
11215
11216 // Handle formatting requests to the language server.
11217 cx.lsp
11218 .set_request_handler::<lsp::request::Formatting, _, _>({
11219 let buffer_changes = buffer_changes.clone();
11220 move |_, _| {
11221 // When formatting is requested, trailing whitespace has already been stripped,
11222 // and the trailing newline has already been added.
11223 assert_eq!(
11224 &buffer_changes.lock()[1..],
11225 &[
11226 (
11227 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
11228 "".into()
11229 ),
11230 (
11231 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
11232 "".into()
11233 ),
11234 (
11235 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
11236 "\n".into()
11237 ),
11238 ]
11239 );
11240
11241 // Insert blank lines between each line of the buffer.
11242 async move {
11243 Ok(Some(vec![
11244 lsp::TextEdit {
11245 range: lsp::Range::new(
11246 lsp::Position::new(1, 0),
11247 lsp::Position::new(1, 0),
11248 ),
11249 new_text: "\n".into(),
11250 },
11251 lsp::TextEdit {
11252 range: lsp::Range::new(
11253 lsp::Position::new(2, 0),
11254 lsp::Position::new(2, 0),
11255 ),
11256 new_text: "\n".into(),
11257 },
11258 ]))
11259 }
11260 }
11261 });
11262
11263 // After formatting the buffer, the trailing whitespace is stripped,
11264 // a newline is appended, and the edits provided by the language server
11265 // have been applied.
11266 format.await.unwrap();
11267 cx.assert_editor_state(
11268 &[
11269 "one", //
11270 "", //
11271 "twoˇ", //
11272 "", //
11273 "three", //
11274 "four", //
11275 "", //
11276 ]
11277 .join("\n"),
11278 );
11279
11280 // Undoing the formatting undoes the trailing whitespace removal, the
11281 // trailing newline, and the LSP edits.
11282 cx.update_buffer(|buffer, cx| buffer.undo(cx));
11283 cx.assert_editor_state(
11284 &[
11285 "one ", //
11286 "twoˇ", //
11287 "three ", //
11288 "four", //
11289 ]
11290 .join("\n"),
11291 );
11292}
11293
11294#[gpui::test]
11295async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
11296 cx: &mut TestAppContext,
11297) {
11298 init_test(cx, |_| {});
11299
11300 cx.update(|cx| {
11301 cx.update_global::<SettingsStore, _>(|settings, cx| {
11302 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11303 settings.auto_signature_help = Some(true);
11304 });
11305 });
11306 });
11307
11308 let mut cx = EditorLspTestContext::new_rust(
11309 lsp::ServerCapabilities {
11310 signature_help_provider: Some(lsp::SignatureHelpOptions {
11311 ..Default::default()
11312 }),
11313 ..Default::default()
11314 },
11315 cx,
11316 )
11317 .await;
11318
11319 let language = Language::new(
11320 LanguageConfig {
11321 name: "Rust".into(),
11322 brackets: BracketPairConfig {
11323 pairs: vec![
11324 BracketPair {
11325 start: "{".to_string(),
11326 end: "}".to_string(),
11327 close: true,
11328 surround: true,
11329 newline: true,
11330 },
11331 BracketPair {
11332 start: "(".to_string(),
11333 end: ")".to_string(),
11334 close: true,
11335 surround: true,
11336 newline: true,
11337 },
11338 BracketPair {
11339 start: "/*".to_string(),
11340 end: " */".to_string(),
11341 close: true,
11342 surround: true,
11343 newline: true,
11344 },
11345 BracketPair {
11346 start: "[".to_string(),
11347 end: "]".to_string(),
11348 close: false,
11349 surround: false,
11350 newline: true,
11351 },
11352 BracketPair {
11353 start: "\"".to_string(),
11354 end: "\"".to_string(),
11355 close: true,
11356 surround: true,
11357 newline: false,
11358 },
11359 BracketPair {
11360 start: "<".to_string(),
11361 end: ">".to_string(),
11362 close: false,
11363 surround: true,
11364 newline: true,
11365 },
11366 ],
11367 ..Default::default()
11368 },
11369 autoclose_before: "})]".to_string(),
11370 ..Default::default()
11371 },
11372 Some(tree_sitter_rust::LANGUAGE.into()),
11373 );
11374 let language = Arc::new(language);
11375
11376 cx.language_registry().add(language.clone());
11377 cx.update_buffer(|buffer, cx| {
11378 buffer.set_language(Some(language), cx);
11379 });
11380
11381 cx.set_state(
11382 &r#"
11383 fn main() {
11384 sampleˇ
11385 }
11386 "#
11387 .unindent(),
11388 );
11389
11390 cx.update_editor(|editor, window, cx| {
11391 editor.handle_input("(", window, cx);
11392 });
11393 cx.assert_editor_state(
11394 &"
11395 fn main() {
11396 sample(ˇ)
11397 }
11398 "
11399 .unindent(),
11400 );
11401
11402 let mocked_response = lsp::SignatureHelp {
11403 signatures: vec![lsp::SignatureInformation {
11404 label: "fn sample(param1: u8, param2: u8)".to_string(),
11405 documentation: None,
11406 parameters: Some(vec![
11407 lsp::ParameterInformation {
11408 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11409 documentation: None,
11410 },
11411 lsp::ParameterInformation {
11412 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11413 documentation: None,
11414 },
11415 ]),
11416 active_parameter: None,
11417 }],
11418 active_signature: Some(0),
11419 active_parameter: Some(0),
11420 };
11421 handle_signature_help_request(&mut cx, mocked_response).await;
11422
11423 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11424 .await;
11425
11426 cx.editor(|editor, _, _| {
11427 let signature_help_state = editor.signature_help_state.popover().cloned();
11428 let signature = signature_help_state.unwrap();
11429 assert_eq!(
11430 signature.signatures[signature.current_signature].label,
11431 "fn sample(param1: u8, param2: u8)"
11432 );
11433 });
11434}
11435
11436#[gpui::test]
11437async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
11438 init_test(cx, |_| {});
11439
11440 cx.update(|cx| {
11441 cx.update_global::<SettingsStore, _>(|settings, cx| {
11442 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11443 settings.auto_signature_help = Some(false);
11444 settings.show_signature_help_after_edits = Some(false);
11445 });
11446 });
11447 });
11448
11449 let mut cx = EditorLspTestContext::new_rust(
11450 lsp::ServerCapabilities {
11451 signature_help_provider: Some(lsp::SignatureHelpOptions {
11452 ..Default::default()
11453 }),
11454 ..Default::default()
11455 },
11456 cx,
11457 )
11458 .await;
11459
11460 let language = Language::new(
11461 LanguageConfig {
11462 name: "Rust".into(),
11463 brackets: BracketPairConfig {
11464 pairs: vec![
11465 BracketPair {
11466 start: "{".to_string(),
11467 end: "}".to_string(),
11468 close: true,
11469 surround: true,
11470 newline: true,
11471 },
11472 BracketPair {
11473 start: "(".to_string(),
11474 end: ")".to_string(),
11475 close: true,
11476 surround: true,
11477 newline: true,
11478 },
11479 BracketPair {
11480 start: "/*".to_string(),
11481 end: " */".to_string(),
11482 close: true,
11483 surround: true,
11484 newline: true,
11485 },
11486 BracketPair {
11487 start: "[".to_string(),
11488 end: "]".to_string(),
11489 close: false,
11490 surround: false,
11491 newline: true,
11492 },
11493 BracketPair {
11494 start: "\"".to_string(),
11495 end: "\"".to_string(),
11496 close: true,
11497 surround: true,
11498 newline: false,
11499 },
11500 BracketPair {
11501 start: "<".to_string(),
11502 end: ">".to_string(),
11503 close: false,
11504 surround: true,
11505 newline: true,
11506 },
11507 ],
11508 ..Default::default()
11509 },
11510 autoclose_before: "})]".to_string(),
11511 ..Default::default()
11512 },
11513 Some(tree_sitter_rust::LANGUAGE.into()),
11514 );
11515 let language = Arc::new(language);
11516
11517 cx.language_registry().add(language.clone());
11518 cx.update_buffer(|buffer, cx| {
11519 buffer.set_language(Some(language), cx);
11520 });
11521
11522 // Ensure that signature_help is not called when no signature help is enabled.
11523 cx.set_state(
11524 &r#"
11525 fn main() {
11526 sampleˇ
11527 }
11528 "#
11529 .unindent(),
11530 );
11531 cx.update_editor(|editor, window, cx| {
11532 editor.handle_input("(", window, cx);
11533 });
11534 cx.assert_editor_state(
11535 &"
11536 fn main() {
11537 sample(ˇ)
11538 }
11539 "
11540 .unindent(),
11541 );
11542 cx.editor(|editor, _, _| {
11543 assert!(editor.signature_help_state.task().is_none());
11544 });
11545
11546 let mocked_response = lsp::SignatureHelp {
11547 signatures: vec![lsp::SignatureInformation {
11548 label: "fn sample(param1: u8, param2: u8)".to_string(),
11549 documentation: None,
11550 parameters: Some(vec![
11551 lsp::ParameterInformation {
11552 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11553 documentation: None,
11554 },
11555 lsp::ParameterInformation {
11556 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11557 documentation: None,
11558 },
11559 ]),
11560 active_parameter: None,
11561 }],
11562 active_signature: Some(0),
11563 active_parameter: Some(0),
11564 };
11565
11566 // Ensure that signature_help is called when enabled afte edits
11567 cx.update(|_, cx| {
11568 cx.update_global::<SettingsStore, _>(|settings, cx| {
11569 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11570 settings.auto_signature_help = Some(false);
11571 settings.show_signature_help_after_edits = Some(true);
11572 });
11573 });
11574 });
11575 cx.set_state(
11576 &r#"
11577 fn main() {
11578 sampleˇ
11579 }
11580 "#
11581 .unindent(),
11582 );
11583 cx.update_editor(|editor, window, cx| {
11584 editor.handle_input("(", window, cx);
11585 });
11586 cx.assert_editor_state(
11587 &"
11588 fn main() {
11589 sample(ˇ)
11590 }
11591 "
11592 .unindent(),
11593 );
11594 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11595 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11596 .await;
11597 cx.update_editor(|editor, _, _| {
11598 let signature_help_state = editor.signature_help_state.popover().cloned();
11599 assert!(signature_help_state.is_some());
11600 let signature = signature_help_state.unwrap();
11601 assert_eq!(
11602 signature.signatures[signature.current_signature].label,
11603 "fn sample(param1: u8, param2: u8)"
11604 );
11605 editor.signature_help_state = SignatureHelpState::default();
11606 });
11607
11608 // Ensure that signature_help is called when auto signature help override is enabled
11609 cx.update(|_, cx| {
11610 cx.update_global::<SettingsStore, _>(|settings, cx| {
11611 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11612 settings.auto_signature_help = Some(true);
11613 settings.show_signature_help_after_edits = Some(false);
11614 });
11615 });
11616 });
11617 cx.set_state(
11618 &r#"
11619 fn main() {
11620 sampleˇ
11621 }
11622 "#
11623 .unindent(),
11624 );
11625 cx.update_editor(|editor, window, cx| {
11626 editor.handle_input("(", window, cx);
11627 });
11628 cx.assert_editor_state(
11629 &"
11630 fn main() {
11631 sample(ˇ)
11632 }
11633 "
11634 .unindent(),
11635 );
11636 handle_signature_help_request(&mut cx, mocked_response).await;
11637 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11638 .await;
11639 cx.editor(|editor, _, _| {
11640 let signature_help_state = editor.signature_help_state.popover().cloned();
11641 assert!(signature_help_state.is_some());
11642 let signature = signature_help_state.unwrap();
11643 assert_eq!(
11644 signature.signatures[signature.current_signature].label,
11645 "fn sample(param1: u8, param2: u8)"
11646 );
11647 });
11648}
11649
11650#[gpui::test]
11651async fn test_signature_help(cx: &mut TestAppContext) {
11652 init_test(cx, |_| {});
11653 cx.update(|cx| {
11654 cx.update_global::<SettingsStore, _>(|settings, cx| {
11655 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11656 settings.auto_signature_help = Some(true);
11657 });
11658 });
11659 });
11660
11661 let mut cx = EditorLspTestContext::new_rust(
11662 lsp::ServerCapabilities {
11663 signature_help_provider: Some(lsp::SignatureHelpOptions {
11664 ..Default::default()
11665 }),
11666 ..Default::default()
11667 },
11668 cx,
11669 )
11670 .await;
11671
11672 // A test that directly calls `show_signature_help`
11673 cx.update_editor(|editor, window, cx| {
11674 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11675 });
11676
11677 let mocked_response = lsp::SignatureHelp {
11678 signatures: vec![lsp::SignatureInformation {
11679 label: "fn sample(param1: u8, param2: u8)".to_string(),
11680 documentation: None,
11681 parameters: Some(vec![
11682 lsp::ParameterInformation {
11683 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11684 documentation: None,
11685 },
11686 lsp::ParameterInformation {
11687 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11688 documentation: None,
11689 },
11690 ]),
11691 active_parameter: None,
11692 }],
11693 active_signature: Some(0),
11694 active_parameter: Some(0),
11695 };
11696 handle_signature_help_request(&mut cx, mocked_response).await;
11697
11698 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11699 .await;
11700
11701 cx.editor(|editor, _, _| {
11702 let signature_help_state = editor.signature_help_state.popover().cloned();
11703 assert!(signature_help_state.is_some());
11704 let signature = signature_help_state.unwrap();
11705 assert_eq!(
11706 signature.signatures[signature.current_signature].label,
11707 "fn sample(param1: u8, param2: u8)"
11708 );
11709 });
11710
11711 // When exiting outside from inside the brackets, `signature_help` is closed.
11712 cx.set_state(indoc! {"
11713 fn main() {
11714 sample(ˇ);
11715 }
11716
11717 fn sample(param1: u8, param2: u8) {}
11718 "});
11719
11720 cx.update_editor(|editor, window, cx| {
11721 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11722 s.select_ranges([0..0])
11723 });
11724 });
11725
11726 let mocked_response = lsp::SignatureHelp {
11727 signatures: Vec::new(),
11728 active_signature: None,
11729 active_parameter: None,
11730 };
11731 handle_signature_help_request(&mut cx, mocked_response).await;
11732
11733 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11734 .await;
11735
11736 cx.editor(|editor, _, _| {
11737 assert!(!editor.signature_help_state.is_shown());
11738 });
11739
11740 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11741 cx.set_state(indoc! {"
11742 fn main() {
11743 sample(ˇ);
11744 }
11745
11746 fn sample(param1: u8, param2: u8) {}
11747 "});
11748
11749 let mocked_response = lsp::SignatureHelp {
11750 signatures: vec![lsp::SignatureInformation {
11751 label: "fn sample(param1: u8, param2: u8)".to_string(),
11752 documentation: None,
11753 parameters: Some(vec![
11754 lsp::ParameterInformation {
11755 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11756 documentation: None,
11757 },
11758 lsp::ParameterInformation {
11759 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11760 documentation: None,
11761 },
11762 ]),
11763 active_parameter: None,
11764 }],
11765 active_signature: Some(0),
11766 active_parameter: Some(0),
11767 };
11768 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11769 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11770 .await;
11771 cx.editor(|editor, _, _| {
11772 assert!(editor.signature_help_state.is_shown());
11773 });
11774
11775 // Restore the popover with more parameter input
11776 cx.set_state(indoc! {"
11777 fn main() {
11778 sample(param1, param2ˇ);
11779 }
11780
11781 fn sample(param1: u8, param2: u8) {}
11782 "});
11783
11784 let mocked_response = lsp::SignatureHelp {
11785 signatures: vec![lsp::SignatureInformation {
11786 label: "fn sample(param1: u8, param2: u8)".to_string(),
11787 documentation: None,
11788 parameters: Some(vec![
11789 lsp::ParameterInformation {
11790 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11791 documentation: None,
11792 },
11793 lsp::ParameterInformation {
11794 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11795 documentation: None,
11796 },
11797 ]),
11798 active_parameter: None,
11799 }],
11800 active_signature: Some(0),
11801 active_parameter: Some(1),
11802 };
11803 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11804 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11805 .await;
11806
11807 // When selecting a range, the popover is gone.
11808 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11809 cx.update_editor(|editor, window, cx| {
11810 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11811 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11812 })
11813 });
11814 cx.assert_editor_state(indoc! {"
11815 fn main() {
11816 sample(param1, «ˇparam2»);
11817 }
11818
11819 fn sample(param1: u8, param2: u8) {}
11820 "});
11821 cx.editor(|editor, _, _| {
11822 assert!(!editor.signature_help_state.is_shown());
11823 });
11824
11825 // When unselecting again, the popover is back if within the brackets.
11826 cx.update_editor(|editor, window, cx| {
11827 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11828 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11829 })
11830 });
11831 cx.assert_editor_state(indoc! {"
11832 fn main() {
11833 sample(param1, ˇparam2);
11834 }
11835
11836 fn sample(param1: u8, param2: u8) {}
11837 "});
11838 handle_signature_help_request(&mut cx, mocked_response).await;
11839 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11840 .await;
11841 cx.editor(|editor, _, _| {
11842 assert!(editor.signature_help_state.is_shown());
11843 });
11844
11845 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11846 cx.update_editor(|editor, window, cx| {
11847 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11848 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11849 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11850 })
11851 });
11852 cx.assert_editor_state(indoc! {"
11853 fn main() {
11854 sample(param1, ˇparam2);
11855 }
11856
11857 fn sample(param1: u8, param2: u8) {}
11858 "});
11859
11860 let mocked_response = lsp::SignatureHelp {
11861 signatures: vec![lsp::SignatureInformation {
11862 label: "fn sample(param1: u8, param2: u8)".to_string(),
11863 documentation: None,
11864 parameters: Some(vec![
11865 lsp::ParameterInformation {
11866 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11867 documentation: None,
11868 },
11869 lsp::ParameterInformation {
11870 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11871 documentation: None,
11872 },
11873 ]),
11874 active_parameter: None,
11875 }],
11876 active_signature: Some(0),
11877 active_parameter: Some(1),
11878 };
11879 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11880 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11881 .await;
11882 cx.update_editor(|editor, _, cx| {
11883 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11884 });
11885 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11886 .await;
11887 cx.update_editor(|editor, window, cx| {
11888 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11889 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11890 })
11891 });
11892 cx.assert_editor_state(indoc! {"
11893 fn main() {
11894 sample(param1, «ˇparam2»);
11895 }
11896
11897 fn sample(param1: u8, param2: u8) {}
11898 "});
11899 cx.update_editor(|editor, window, cx| {
11900 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11901 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11902 })
11903 });
11904 cx.assert_editor_state(indoc! {"
11905 fn main() {
11906 sample(param1, ˇparam2);
11907 }
11908
11909 fn sample(param1: u8, param2: u8) {}
11910 "});
11911 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11912 .await;
11913}
11914
11915#[gpui::test]
11916async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
11917 init_test(cx, |_| {});
11918
11919 let mut cx = EditorLspTestContext::new_rust(
11920 lsp::ServerCapabilities {
11921 signature_help_provider: Some(lsp::SignatureHelpOptions {
11922 ..Default::default()
11923 }),
11924 ..Default::default()
11925 },
11926 cx,
11927 )
11928 .await;
11929
11930 cx.set_state(indoc! {"
11931 fn main() {
11932 overloadedˇ
11933 }
11934 "});
11935
11936 cx.update_editor(|editor, window, cx| {
11937 editor.handle_input("(", window, cx);
11938 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11939 });
11940
11941 // Mock response with 3 signatures
11942 let mocked_response = lsp::SignatureHelp {
11943 signatures: vec![
11944 lsp::SignatureInformation {
11945 label: "fn overloaded(x: i32)".to_string(),
11946 documentation: None,
11947 parameters: Some(vec![lsp::ParameterInformation {
11948 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11949 documentation: None,
11950 }]),
11951 active_parameter: None,
11952 },
11953 lsp::SignatureInformation {
11954 label: "fn overloaded(x: i32, y: i32)".to_string(),
11955 documentation: None,
11956 parameters: Some(vec![
11957 lsp::ParameterInformation {
11958 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11959 documentation: None,
11960 },
11961 lsp::ParameterInformation {
11962 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11963 documentation: None,
11964 },
11965 ]),
11966 active_parameter: None,
11967 },
11968 lsp::SignatureInformation {
11969 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
11970 documentation: None,
11971 parameters: Some(vec![
11972 lsp::ParameterInformation {
11973 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11974 documentation: None,
11975 },
11976 lsp::ParameterInformation {
11977 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11978 documentation: None,
11979 },
11980 lsp::ParameterInformation {
11981 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
11982 documentation: None,
11983 },
11984 ]),
11985 active_parameter: None,
11986 },
11987 ],
11988 active_signature: Some(1),
11989 active_parameter: Some(0),
11990 };
11991 handle_signature_help_request(&mut cx, mocked_response).await;
11992
11993 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11994 .await;
11995
11996 // Verify we have multiple signatures and the right one is selected
11997 cx.editor(|editor, _, _| {
11998 let popover = editor.signature_help_state.popover().cloned().unwrap();
11999 assert_eq!(popover.signatures.len(), 3);
12000 // active_signature was 1, so that should be the current
12001 assert_eq!(popover.current_signature, 1);
12002 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
12003 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
12004 assert_eq!(
12005 popover.signatures[2].label,
12006 "fn overloaded(x: i32, y: i32, z: i32)"
12007 );
12008 });
12009
12010 // Test navigation functionality
12011 cx.update_editor(|editor, window, cx| {
12012 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
12013 });
12014
12015 cx.editor(|editor, _, _| {
12016 let popover = editor.signature_help_state.popover().cloned().unwrap();
12017 assert_eq!(popover.current_signature, 2);
12018 });
12019
12020 // Test wrap around
12021 cx.update_editor(|editor, window, cx| {
12022 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
12023 });
12024
12025 cx.editor(|editor, _, _| {
12026 let popover = editor.signature_help_state.popover().cloned().unwrap();
12027 assert_eq!(popover.current_signature, 0);
12028 });
12029
12030 // Test previous navigation
12031 cx.update_editor(|editor, window, cx| {
12032 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
12033 });
12034
12035 cx.editor(|editor, _, _| {
12036 let popover = editor.signature_help_state.popover().cloned().unwrap();
12037 assert_eq!(popover.current_signature, 2);
12038 });
12039}
12040
12041#[gpui::test]
12042async fn test_completion_mode(cx: &mut TestAppContext) {
12043 init_test(cx, |_| {});
12044 let mut cx = EditorLspTestContext::new_rust(
12045 lsp::ServerCapabilities {
12046 completion_provider: Some(lsp::CompletionOptions {
12047 resolve_provider: Some(true),
12048 ..Default::default()
12049 }),
12050 ..Default::default()
12051 },
12052 cx,
12053 )
12054 .await;
12055
12056 struct Run {
12057 run_description: &'static str,
12058 initial_state: String,
12059 buffer_marked_text: String,
12060 completion_label: &'static str,
12061 completion_text: &'static str,
12062 expected_with_insert_mode: String,
12063 expected_with_replace_mode: String,
12064 expected_with_replace_subsequence_mode: String,
12065 expected_with_replace_suffix_mode: String,
12066 }
12067
12068 let runs = [
12069 Run {
12070 run_description: "Start of word matches completion text",
12071 initial_state: "before ediˇ after".into(),
12072 buffer_marked_text: "before <edi|> after".into(),
12073 completion_label: "editor",
12074 completion_text: "editor",
12075 expected_with_insert_mode: "before editorˇ after".into(),
12076 expected_with_replace_mode: "before editorˇ after".into(),
12077 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12078 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12079 },
12080 Run {
12081 run_description: "Accept same text at the middle of the word",
12082 initial_state: "before ediˇtor after".into(),
12083 buffer_marked_text: "before <edi|tor> after".into(),
12084 completion_label: "editor",
12085 completion_text: "editor",
12086 expected_with_insert_mode: "before editorˇtor after".into(),
12087 expected_with_replace_mode: "before editorˇ after".into(),
12088 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12089 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12090 },
12091 Run {
12092 run_description: "End of word matches completion text -- cursor at end",
12093 initial_state: "before torˇ after".into(),
12094 buffer_marked_text: "before <tor|> after".into(),
12095 completion_label: "editor",
12096 completion_text: "editor",
12097 expected_with_insert_mode: "before editorˇ after".into(),
12098 expected_with_replace_mode: "before editorˇ after".into(),
12099 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12100 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12101 },
12102 Run {
12103 run_description: "End of word matches completion text -- cursor at start",
12104 initial_state: "before ˇtor after".into(),
12105 buffer_marked_text: "before <|tor> after".into(),
12106 completion_label: "editor",
12107 completion_text: "editor",
12108 expected_with_insert_mode: "before editorˇtor after".into(),
12109 expected_with_replace_mode: "before editorˇ after".into(),
12110 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12111 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12112 },
12113 Run {
12114 run_description: "Prepend text containing whitespace",
12115 initial_state: "pˇfield: bool".into(),
12116 buffer_marked_text: "<p|field>: bool".into(),
12117 completion_label: "pub ",
12118 completion_text: "pub ",
12119 expected_with_insert_mode: "pub ˇfield: bool".into(),
12120 expected_with_replace_mode: "pub ˇ: bool".into(),
12121 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
12122 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
12123 },
12124 Run {
12125 run_description: "Add element to start of list",
12126 initial_state: "[element_ˇelement_2]".into(),
12127 buffer_marked_text: "[<element_|element_2>]".into(),
12128 completion_label: "element_1",
12129 completion_text: "element_1",
12130 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
12131 expected_with_replace_mode: "[element_1ˇ]".into(),
12132 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
12133 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
12134 },
12135 Run {
12136 run_description: "Add element to start of list -- first and second elements are equal",
12137 initial_state: "[elˇelement]".into(),
12138 buffer_marked_text: "[<el|element>]".into(),
12139 completion_label: "element",
12140 completion_text: "element",
12141 expected_with_insert_mode: "[elementˇelement]".into(),
12142 expected_with_replace_mode: "[elementˇ]".into(),
12143 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
12144 expected_with_replace_suffix_mode: "[elementˇ]".into(),
12145 },
12146 Run {
12147 run_description: "Ends with matching suffix",
12148 initial_state: "SubˇError".into(),
12149 buffer_marked_text: "<Sub|Error>".into(),
12150 completion_label: "SubscriptionError",
12151 completion_text: "SubscriptionError",
12152 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
12153 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
12154 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
12155 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
12156 },
12157 Run {
12158 run_description: "Suffix is a subsequence -- contiguous",
12159 initial_state: "SubˇErr".into(),
12160 buffer_marked_text: "<Sub|Err>".into(),
12161 completion_label: "SubscriptionError",
12162 completion_text: "SubscriptionError",
12163 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
12164 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
12165 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
12166 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
12167 },
12168 Run {
12169 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
12170 initial_state: "Suˇscrirr".into(),
12171 buffer_marked_text: "<Su|scrirr>".into(),
12172 completion_label: "SubscriptionError",
12173 completion_text: "SubscriptionError",
12174 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
12175 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
12176 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
12177 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
12178 },
12179 Run {
12180 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
12181 initial_state: "foo(indˇix)".into(),
12182 buffer_marked_text: "foo(<ind|ix>)".into(),
12183 completion_label: "node_index",
12184 completion_text: "node_index",
12185 expected_with_insert_mode: "foo(node_indexˇix)".into(),
12186 expected_with_replace_mode: "foo(node_indexˇ)".into(),
12187 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
12188 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
12189 },
12190 Run {
12191 run_description: "Replace range ends before cursor - should extend to cursor",
12192 initial_state: "before editˇo after".into(),
12193 buffer_marked_text: "before <{ed}>it|o after".into(),
12194 completion_label: "editor",
12195 completion_text: "editor",
12196 expected_with_insert_mode: "before editorˇo after".into(),
12197 expected_with_replace_mode: "before editorˇo after".into(),
12198 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
12199 expected_with_replace_suffix_mode: "before editorˇo after".into(),
12200 },
12201 Run {
12202 run_description: "Uses label for suffix matching",
12203 initial_state: "before ediˇtor after".into(),
12204 buffer_marked_text: "before <edi|tor> after".into(),
12205 completion_label: "editor",
12206 completion_text: "editor()",
12207 expected_with_insert_mode: "before editor()ˇtor after".into(),
12208 expected_with_replace_mode: "before editor()ˇ after".into(),
12209 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
12210 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
12211 },
12212 Run {
12213 run_description: "Case insensitive subsequence and suffix matching",
12214 initial_state: "before EDiˇtoR after".into(),
12215 buffer_marked_text: "before <EDi|toR> after".into(),
12216 completion_label: "editor",
12217 completion_text: "editor",
12218 expected_with_insert_mode: "before editorˇtoR after".into(),
12219 expected_with_replace_mode: "before editorˇ after".into(),
12220 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12221 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12222 },
12223 ];
12224
12225 for run in runs {
12226 let run_variations = [
12227 (LspInsertMode::Insert, run.expected_with_insert_mode),
12228 (LspInsertMode::Replace, run.expected_with_replace_mode),
12229 (
12230 LspInsertMode::ReplaceSubsequence,
12231 run.expected_with_replace_subsequence_mode,
12232 ),
12233 (
12234 LspInsertMode::ReplaceSuffix,
12235 run.expected_with_replace_suffix_mode,
12236 ),
12237 ];
12238
12239 for (lsp_insert_mode, expected_text) in run_variations {
12240 eprintln!(
12241 "run = {:?}, mode = {lsp_insert_mode:.?}",
12242 run.run_description,
12243 );
12244
12245 update_test_language_settings(&mut cx, |settings| {
12246 settings.defaults.completions = Some(CompletionSettings {
12247 lsp_insert_mode,
12248 words: WordsCompletionMode::Disabled,
12249 lsp: true,
12250 lsp_fetch_timeout_ms: 0,
12251 });
12252 });
12253
12254 cx.set_state(&run.initial_state);
12255 cx.update_editor(|editor, window, cx| {
12256 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12257 });
12258
12259 let counter = Arc::new(AtomicUsize::new(0));
12260 handle_completion_request_with_insert_and_replace(
12261 &mut cx,
12262 &run.buffer_marked_text,
12263 vec![(run.completion_label, run.completion_text)],
12264 counter.clone(),
12265 )
12266 .await;
12267 cx.condition(|editor, _| editor.context_menu_visible())
12268 .await;
12269 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12270
12271 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12272 editor
12273 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12274 .unwrap()
12275 });
12276 cx.assert_editor_state(&expected_text);
12277 handle_resolve_completion_request(&mut cx, None).await;
12278 apply_additional_edits.await.unwrap();
12279 }
12280 }
12281}
12282
12283#[gpui::test]
12284async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
12285 init_test(cx, |_| {});
12286 let mut cx = EditorLspTestContext::new_rust(
12287 lsp::ServerCapabilities {
12288 completion_provider: Some(lsp::CompletionOptions {
12289 resolve_provider: Some(true),
12290 ..Default::default()
12291 }),
12292 ..Default::default()
12293 },
12294 cx,
12295 )
12296 .await;
12297
12298 let initial_state = "SubˇError";
12299 let buffer_marked_text = "<Sub|Error>";
12300 let completion_text = "SubscriptionError";
12301 let expected_with_insert_mode = "SubscriptionErrorˇError";
12302 let expected_with_replace_mode = "SubscriptionErrorˇ";
12303
12304 update_test_language_settings(&mut cx, |settings| {
12305 settings.defaults.completions = Some(CompletionSettings {
12306 words: WordsCompletionMode::Disabled,
12307 // set the opposite here to ensure that the action is overriding the default behavior
12308 lsp_insert_mode: LspInsertMode::Insert,
12309 lsp: true,
12310 lsp_fetch_timeout_ms: 0,
12311 });
12312 });
12313
12314 cx.set_state(initial_state);
12315 cx.update_editor(|editor, window, cx| {
12316 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12317 });
12318
12319 let counter = Arc::new(AtomicUsize::new(0));
12320 handle_completion_request_with_insert_and_replace(
12321 &mut cx,
12322 &buffer_marked_text,
12323 vec![(completion_text, completion_text)],
12324 counter.clone(),
12325 )
12326 .await;
12327 cx.condition(|editor, _| editor.context_menu_visible())
12328 .await;
12329 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12330
12331 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12332 editor
12333 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12334 .unwrap()
12335 });
12336 cx.assert_editor_state(&expected_with_replace_mode);
12337 handle_resolve_completion_request(&mut cx, None).await;
12338 apply_additional_edits.await.unwrap();
12339
12340 update_test_language_settings(&mut cx, |settings| {
12341 settings.defaults.completions = Some(CompletionSettings {
12342 words: WordsCompletionMode::Disabled,
12343 // set the opposite here to ensure that the action is overriding the default behavior
12344 lsp_insert_mode: LspInsertMode::Replace,
12345 lsp: true,
12346 lsp_fetch_timeout_ms: 0,
12347 });
12348 });
12349
12350 cx.set_state(initial_state);
12351 cx.update_editor(|editor, window, cx| {
12352 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12353 });
12354 handle_completion_request_with_insert_and_replace(
12355 &mut cx,
12356 &buffer_marked_text,
12357 vec![(completion_text, completion_text)],
12358 counter.clone(),
12359 )
12360 .await;
12361 cx.condition(|editor, _| editor.context_menu_visible())
12362 .await;
12363 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12364
12365 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12366 editor
12367 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
12368 .unwrap()
12369 });
12370 cx.assert_editor_state(&expected_with_insert_mode);
12371 handle_resolve_completion_request(&mut cx, None).await;
12372 apply_additional_edits.await.unwrap();
12373}
12374
12375#[gpui::test]
12376async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
12377 init_test(cx, |_| {});
12378 let mut cx = EditorLspTestContext::new_rust(
12379 lsp::ServerCapabilities {
12380 completion_provider: Some(lsp::CompletionOptions {
12381 resolve_provider: Some(true),
12382 ..Default::default()
12383 }),
12384 ..Default::default()
12385 },
12386 cx,
12387 )
12388 .await;
12389
12390 // scenario: surrounding text matches completion text
12391 let completion_text = "to_offset";
12392 let initial_state = indoc! {"
12393 1. buf.to_offˇsuffix
12394 2. buf.to_offˇsuf
12395 3. buf.to_offˇfix
12396 4. buf.to_offˇ
12397 5. into_offˇensive
12398 6. ˇsuffix
12399 7. let ˇ //
12400 8. aaˇzz
12401 9. buf.to_off«zzzzzˇ»suffix
12402 10. buf.«ˇzzzzz»suffix
12403 11. to_off«ˇzzzzz»
12404
12405 buf.to_offˇsuffix // newest cursor
12406 "};
12407 let completion_marked_buffer = indoc! {"
12408 1. buf.to_offsuffix
12409 2. buf.to_offsuf
12410 3. buf.to_offfix
12411 4. buf.to_off
12412 5. into_offensive
12413 6. suffix
12414 7. let //
12415 8. aazz
12416 9. buf.to_offzzzzzsuffix
12417 10. buf.zzzzzsuffix
12418 11. to_offzzzzz
12419
12420 buf.<to_off|suffix> // newest cursor
12421 "};
12422 let expected = indoc! {"
12423 1. buf.to_offsetˇ
12424 2. buf.to_offsetˇsuf
12425 3. buf.to_offsetˇfix
12426 4. buf.to_offsetˇ
12427 5. into_offsetˇensive
12428 6. to_offsetˇsuffix
12429 7. let to_offsetˇ //
12430 8. aato_offsetˇzz
12431 9. buf.to_offsetˇ
12432 10. buf.to_offsetˇsuffix
12433 11. to_offsetˇ
12434
12435 buf.to_offsetˇ // newest cursor
12436 "};
12437 cx.set_state(initial_state);
12438 cx.update_editor(|editor, window, cx| {
12439 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12440 });
12441 handle_completion_request_with_insert_and_replace(
12442 &mut cx,
12443 completion_marked_buffer,
12444 vec![(completion_text, completion_text)],
12445 Arc::new(AtomicUsize::new(0)),
12446 )
12447 .await;
12448 cx.condition(|editor, _| editor.context_menu_visible())
12449 .await;
12450 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12451 editor
12452 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12453 .unwrap()
12454 });
12455 cx.assert_editor_state(expected);
12456 handle_resolve_completion_request(&mut cx, None).await;
12457 apply_additional_edits.await.unwrap();
12458
12459 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
12460 let completion_text = "foo_and_bar";
12461 let initial_state = indoc! {"
12462 1. ooanbˇ
12463 2. zooanbˇ
12464 3. ooanbˇz
12465 4. zooanbˇz
12466 5. ooanˇ
12467 6. oanbˇ
12468
12469 ooanbˇ
12470 "};
12471 let completion_marked_buffer = indoc! {"
12472 1. ooanb
12473 2. zooanb
12474 3. ooanbz
12475 4. zooanbz
12476 5. ooan
12477 6. oanb
12478
12479 <ooanb|>
12480 "};
12481 let expected = indoc! {"
12482 1. foo_and_barˇ
12483 2. zfoo_and_barˇ
12484 3. foo_and_barˇz
12485 4. zfoo_and_barˇz
12486 5. ooanfoo_and_barˇ
12487 6. oanbfoo_and_barˇ
12488
12489 foo_and_barˇ
12490 "};
12491 cx.set_state(initial_state);
12492 cx.update_editor(|editor, window, cx| {
12493 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12494 });
12495 handle_completion_request_with_insert_and_replace(
12496 &mut cx,
12497 completion_marked_buffer,
12498 vec![(completion_text, completion_text)],
12499 Arc::new(AtomicUsize::new(0)),
12500 )
12501 .await;
12502 cx.condition(|editor, _| editor.context_menu_visible())
12503 .await;
12504 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12505 editor
12506 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12507 .unwrap()
12508 });
12509 cx.assert_editor_state(expected);
12510 handle_resolve_completion_request(&mut cx, None).await;
12511 apply_additional_edits.await.unwrap();
12512
12513 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
12514 // (expects the same as if it was inserted at the end)
12515 let completion_text = "foo_and_bar";
12516 let initial_state = indoc! {"
12517 1. ooˇanb
12518 2. zooˇanb
12519 3. ooˇanbz
12520 4. zooˇanbz
12521
12522 ooˇanb
12523 "};
12524 let completion_marked_buffer = indoc! {"
12525 1. ooanb
12526 2. zooanb
12527 3. ooanbz
12528 4. zooanbz
12529
12530 <oo|anb>
12531 "};
12532 let expected = indoc! {"
12533 1. foo_and_barˇ
12534 2. zfoo_and_barˇ
12535 3. foo_and_barˇz
12536 4. zfoo_and_barˇz
12537
12538 foo_and_barˇ
12539 "};
12540 cx.set_state(initial_state);
12541 cx.update_editor(|editor, window, cx| {
12542 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12543 });
12544 handle_completion_request_with_insert_and_replace(
12545 &mut cx,
12546 completion_marked_buffer,
12547 vec![(completion_text, completion_text)],
12548 Arc::new(AtomicUsize::new(0)),
12549 )
12550 .await;
12551 cx.condition(|editor, _| editor.context_menu_visible())
12552 .await;
12553 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12554 editor
12555 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12556 .unwrap()
12557 });
12558 cx.assert_editor_state(expected);
12559 handle_resolve_completion_request(&mut cx, None).await;
12560 apply_additional_edits.await.unwrap();
12561}
12562
12563// This used to crash
12564#[gpui::test]
12565async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
12566 init_test(cx, |_| {});
12567
12568 let buffer_text = indoc! {"
12569 fn main() {
12570 10.satu;
12571
12572 //
12573 // separate cursors so they open in different excerpts (manually reproducible)
12574 //
12575
12576 10.satu20;
12577 }
12578 "};
12579 let multibuffer_text_with_selections = indoc! {"
12580 fn main() {
12581 10.satuˇ;
12582
12583 //
12584
12585 //
12586
12587 10.satuˇ20;
12588 }
12589 "};
12590 let expected_multibuffer = indoc! {"
12591 fn main() {
12592 10.saturating_sub()ˇ;
12593
12594 //
12595
12596 //
12597
12598 10.saturating_sub()ˇ;
12599 }
12600 "};
12601
12602 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
12603 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
12604
12605 let fs = FakeFs::new(cx.executor());
12606 fs.insert_tree(
12607 path!("/a"),
12608 json!({
12609 "main.rs": buffer_text,
12610 }),
12611 )
12612 .await;
12613
12614 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12615 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12616 language_registry.add(rust_lang());
12617 let mut fake_servers = language_registry.register_fake_lsp(
12618 "Rust",
12619 FakeLspAdapter {
12620 capabilities: lsp::ServerCapabilities {
12621 completion_provider: Some(lsp::CompletionOptions {
12622 resolve_provider: None,
12623 ..lsp::CompletionOptions::default()
12624 }),
12625 ..lsp::ServerCapabilities::default()
12626 },
12627 ..FakeLspAdapter::default()
12628 },
12629 );
12630 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12631 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12632 let buffer = project
12633 .update(cx, |project, cx| {
12634 project.open_local_buffer(path!("/a/main.rs"), cx)
12635 })
12636 .await
12637 .unwrap();
12638
12639 let multi_buffer = cx.new(|cx| {
12640 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
12641 multi_buffer.push_excerpts(
12642 buffer.clone(),
12643 [ExcerptRange::new(0..first_excerpt_end)],
12644 cx,
12645 );
12646 multi_buffer.push_excerpts(
12647 buffer.clone(),
12648 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
12649 cx,
12650 );
12651 multi_buffer
12652 });
12653
12654 let editor = workspace
12655 .update(cx, |_, window, cx| {
12656 cx.new(|cx| {
12657 Editor::new(
12658 EditorMode::Full {
12659 scale_ui_elements_with_buffer_font_size: false,
12660 show_active_line_background: false,
12661 sized_by_content: false,
12662 },
12663 multi_buffer.clone(),
12664 Some(project.clone()),
12665 window,
12666 cx,
12667 )
12668 })
12669 })
12670 .unwrap();
12671
12672 let pane = workspace
12673 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12674 .unwrap();
12675 pane.update_in(cx, |pane, window, cx| {
12676 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
12677 });
12678
12679 let fake_server = fake_servers.next().await.unwrap();
12680
12681 editor.update_in(cx, |editor, window, cx| {
12682 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12683 s.select_ranges([
12684 Point::new(1, 11)..Point::new(1, 11),
12685 Point::new(7, 11)..Point::new(7, 11),
12686 ])
12687 });
12688
12689 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
12690 });
12691
12692 editor.update_in(cx, |editor, window, cx| {
12693 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12694 });
12695
12696 fake_server
12697 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12698 let completion_item = lsp::CompletionItem {
12699 label: "saturating_sub()".into(),
12700 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12701 lsp::InsertReplaceEdit {
12702 new_text: "saturating_sub()".to_owned(),
12703 insert: lsp::Range::new(
12704 lsp::Position::new(7, 7),
12705 lsp::Position::new(7, 11),
12706 ),
12707 replace: lsp::Range::new(
12708 lsp::Position::new(7, 7),
12709 lsp::Position::new(7, 13),
12710 ),
12711 },
12712 )),
12713 ..lsp::CompletionItem::default()
12714 };
12715
12716 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
12717 })
12718 .next()
12719 .await
12720 .unwrap();
12721
12722 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12723 .await;
12724
12725 editor
12726 .update_in(cx, |editor, window, cx| {
12727 editor
12728 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12729 .unwrap()
12730 })
12731 .await
12732 .unwrap();
12733
12734 editor.update(cx, |editor, cx| {
12735 assert_text_with_selections(editor, expected_multibuffer, cx);
12736 })
12737}
12738
12739#[gpui::test]
12740async fn test_completion(cx: &mut TestAppContext) {
12741 init_test(cx, |_| {});
12742
12743 let mut cx = EditorLspTestContext::new_rust(
12744 lsp::ServerCapabilities {
12745 completion_provider: Some(lsp::CompletionOptions {
12746 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12747 resolve_provider: Some(true),
12748 ..Default::default()
12749 }),
12750 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12751 ..Default::default()
12752 },
12753 cx,
12754 )
12755 .await;
12756 let counter = Arc::new(AtomicUsize::new(0));
12757
12758 cx.set_state(indoc! {"
12759 oneˇ
12760 two
12761 three
12762 "});
12763 cx.simulate_keystroke(".");
12764 handle_completion_request(
12765 indoc! {"
12766 one.|<>
12767 two
12768 three
12769 "},
12770 vec!["first_completion", "second_completion"],
12771 true,
12772 counter.clone(),
12773 &mut cx,
12774 )
12775 .await;
12776 cx.condition(|editor, _| editor.context_menu_visible())
12777 .await;
12778 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12779
12780 let _handler = handle_signature_help_request(
12781 &mut cx,
12782 lsp::SignatureHelp {
12783 signatures: vec![lsp::SignatureInformation {
12784 label: "test signature".to_string(),
12785 documentation: None,
12786 parameters: Some(vec![lsp::ParameterInformation {
12787 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12788 documentation: None,
12789 }]),
12790 active_parameter: None,
12791 }],
12792 active_signature: None,
12793 active_parameter: None,
12794 },
12795 );
12796 cx.update_editor(|editor, window, cx| {
12797 assert!(
12798 !editor.signature_help_state.is_shown(),
12799 "No signature help was called for"
12800 );
12801 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12802 });
12803 cx.run_until_parked();
12804 cx.update_editor(|editor, _, _| {
12805 assert!(
12806 !editor.signature_help_state.is_shown(),
12807 "No signature help should be shown when completions menu is open"
12808 );
12809 });
12810
12811 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12812 editor.context_menu_next(&Default::default(), window, cx);
12813 editor
12814 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12815 .unwrap()
12816 });
12817 cx.assert_editor_state(indoc! {"
12818 one.second_completionˇ
12819 two
12820 three
12821 "});
12822
12823 handle_resolve_completion_request(
12824 &mut cx,
12825 Some(vec![
12826 (
12827 //This overlaps with the primary completion edit which is
12828 //misbehavior from the LSP spec, test that we filter it out
12829 indoc! {"
12830 one.second_ˇcompletion
12831 two
12832 threeˇ
12833 "},
12834 "overlapping additional edit",
12835 ),
12836 (
12837 indoc! {"
12838 one.second_completion
12839 two
12840 threeˇ
12841 "},
12842 "\nadditional edit",
12843 ),
12844 ]),
12845 )
12846 .await;
12847 apply_additional_edits.await.unwrap();
12848 cx.assert_editor_state(indoc! {"
12849 one.second_completionˇ
12850 two
12851 three
12852 additional edit
12853 "});
12854
12855 cx.set_state(indoc! {"
12856 one.second_completion
12857 twoˇ
12858 threeˇ
12859 additional edit
12860 "});
12861 cx.simulate_keystroke(" ");
12862 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12863 cx.simulate_keystroke("s");
12864 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12865
12866 cx.assert_editor_state(indoc! {"
12867 one.second_completion
12868 two sˇ
12869 three sˇ
12870 additional edit
12871 "});
12872 handle_completion_request(
12873 indoc! {"
12874 one.second_completion
12875 two s
12876 three <s|>
12877 additional edit
12878 "},
12879 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12880 true,
12881 counter.clone(),
12882 &mut cx,
12883 )
12884 .await;
12885 cx.condition(|editor, _| editor.context_menu_visible())
12886 .await;
12887 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12888
12889 cx.simulate_keystroke("i");
12890
12891 handle_completion_request(
12892 indoc! {"
12893 one.second_completion
12894 two si
12895 three <si|>
12896 additional edit
12897 "},
12898 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12899 true,
12900 counter.clone(),
12901 &mut cx,
12902 )
12903 .await;
12904 cx.condition(|editor, _| editor.context_menu_visible())
12905 .await;
12906 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12907
12908 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12909 editor
12910 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12911 .unwrap()
12912 });
12913 cx.assert_editor_state(indoc! {"
12914 one.second_completion
12915 two sixth_completionˇ
12916 three sixth_completionˇ
12917 additional edit
12918 "});
12919
12920 apply_additional_edits.await.unwrap();
12921
12922 update_test_language_settings(&mut cx, |settings| {
12923 settings.defaults.show_completions_on_input = Some(false);
12924 });
12925 cx.set_state("editorˇ");
12926 cx.simulate_keystroke(".");
12927 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12928 cx.simulate_keystrokes("c l o");
12929 cx.assert_editor_state("editor.cloˇ");
12930 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12931 cx.update_editor(|editor, window, cx| {
12932 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12933 });
12934 handle_completion_request(
12935 "editor.<clo|>",
12936 vec!["close", "clobber"],
12937 true,
12938 counter.clone(),
12939 &mut cx,
12940 )
12941 .await;
12942 cx.condition(|editor, _| editor.context_menu_visible())
12943 .await;
12944 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12945
12946 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12947 editor
12948 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12949 .unwrap()
12950 });
12951 cx.assert_editor_state("editor.clobberˇ");
12952 handle_resolve_completion_request(&mut cx, None).await;
12953 apply_additional_edits.await.unwrap();
12954}
12955
12956#[gpui::test]
12957async fn test_completion_reuse(cx: &mut TestAppContext) {
12958 init_test(cx, |_| {});
12959
12960 let mut cx = EditorLspTestContext::new_rust(
12961 lsp::ServerCapabilities {
12962 completion_provider: Some(lsp::CompletionOptions {
12963 trigger_characters: Some(vec![".".to_string()]),
12964 ..Default::default()
12965 }),
12966 ..Default::default()
12967 },
12968 cx,
12969 )
12970 .await;
12971
12972 let counter = Arc::new(AtomicUsize::new(0));
12973 cx.set_state("objˇ");
12974 cx.simulate_keystroke(".");
12975
12976 // Initial completion request returns complete results
12977 let is_incomplete = false;
12978 handle_completion_request(
12979 "obj.|<>",
12980 vec!["a", "ab", "abc"],
12981 is_incomplete,
12982 counter.clone(),
12983 &mut cx,
12984 )
12985 .await;
12986 cx.run_until_parked();
12987 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12988 cx.assert_editor_state("obj.ˇ");
12989 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12990
12991 // Type "a" - filters existing completions
12992 cx.simulate_keystroke("a");
12993 cx.run_until_parked();
12994 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12995 cx.assert_editor_state("obj.aˇ");
12996 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12997
12998 // Type "b" - filters existing completions
12999 cx.simulate_keystroke("b");
13000 cx.run_until_parked();
13001 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13002 cx.assert_editor_state("obj.abˇ");
13003 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13004
13005 // Type "c" - filters existing completions
13006 cx.simulate_keystroke("c");
13007 cx.run_until_parked();
13008 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13009 cx.assert_editor_state("obj.abcˇ");
13010 check_displayed_completions(vec!["abc"], &mut cx);
13011
13012 // Backspace to delete "c" - filters existing completions
13013 cx.update_editor(|editor, window, cx| {
13014 editor.backspace(&Backspace, window, cx);
13015 });
13016 cx.run_until_parked();
13017 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13018 cx.assert_editor_state("obj.abˇ");
13019 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13020
13021 // Moving cursor to the left dismisses menu.
13022 cx.update_editor(|editor, window, cx| {
13023 editor.move_left(&MoveLeft, window, cx);
13024 });
13025 cx.run_until_parked();
13026 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13027 cx.assert_editor_state("obj.aˇb");
13028 cx.update_editor(|editor, _, _| {
13029 assert_eq!(editor.context_menu_visible(), false);
13030 });
13031
13032 // Type "b" - new request
13033 cx.simulate_keystroke("b");
13034 let is_incomplete = false;
13035 handle_completion_request(
13036 "obj.<ab|>a",
13037 vec!["ab", "abc"],
13038 is_incomplete,
13039 counter.clone(),
13040 &mut cx,
13041 )
13042 .await;
13043 cx.run_until_parked();
13044 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13045 cx.assert_editor_state("obj.abˇb");
13046 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13047
13048 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
13049 cx.update_editor(|editor, window, cx| {
13050 editor.backspace(&Backspace, window, cx);
13051 });
13052 let is_incomplete = false;
13053 handle_completion_request(
13054 "obj.<a|>b",
13055 vec!["a", "ab", "abc"],
13056 is_incomplete,
13057 counter.clone(),
13058 &mut cx,
13059 )
13060 .await;
13061 cx.run_until_parked();
13062 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13063 cx.assert_editor_state("obj.aˇb");
13064 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
13065
13066 // Backspace to delete "a" - dismisses menu.
13067 cx.update_editor(|editor, window, cx| {
13068 editor.backspace(&Backspace, window, cx);
13069 });
13070 cx.run_until_parked();
13071 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13072 cx.assert_editor_state("obj.ˇb");
13073 cx.update_editor(|editor, _, _| {
13074 assert_eq!(editor.context_menu_visible(), false);
13075 });
13076}
13077
13078#[gpui::test]
13079async fn test_word_completion(cx: &mut TestAppContext) {
13080 let lsp_fetch_timeout_ms = 10;
13081 init_test(cx, |language_settings| {
13082 language_settings.defaults.completions = Some(CompletionSettings {
13083 words: WordsCompletionMode::Fallback,
13084 lsp: true,
13085 lsp_fetch_timeout_ms: 10,
13086 lsp_insert_mode: LspInsertMode::Insert,
13087 });
13088 });
13089
13090 let mut cx = EditorLspTestContext::new_rust(
13091 lsp::ServerCapabilities {
13092 completion_provider: Some(lsp::CompletionOptions {
13093 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13094 ..lsp::CompletionOptions::default()
13095 }),
13096 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13097 ..lsp::ServerCapabilities::default()
13098 },
13099 cx,
13100 )
13101 .await;
13102
13103 let throttle_completions = Arc::new(AtomicBool::new(false));
13104
13105 let lsp_throttle_completions = throttle_completions.clone();
13106 let _completion_requests_handler =
13107 cx.lsp
13108 .server
13109 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
13110 let lsp_throttle_completions = lsp_throttle_completions.clone();
13111 let cx = cx.clone();
13112 async move {
13113 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
13114 cx.background_executor()
13115 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
13116 .await;
13117 }
13118 Ok(Some(lsp::CompletionResponse::Array(vec![
13119 lsp::CompletionItem {
13120 label: "first".into(),
13121 ..lsp::CompletionItem::default()
13122 },
13123 lsp::CompletionItem {
13124 label: "last".into(),
13125 ..lsp::CompletionItem::default()
13126 },
13127 ])))
13128 }
13129 });
13130
13131 cx.set_state(indoc! {"
13132 oneˇ
13133 two
13134 three
13135 "});
13136 cx.simulate_keystroke(".");
13137 cx.executor().run_until_parked();
13138 cx.condition(|editor, _| editor.context_menu_visible())
13139 .await;
13140 cx.update_editor(|editor, window, cx| {
13141 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13142 {
13143 assert_eq!(
13144 completion_menu_entries(&menu),
13145 &["first", "last"],
13146 "When LSP server is fast to reply, no fallback word completions are used"
13147 );
13148 } else {
13149 panic!("expected completion menu to be open");
13150 }
13151 editor.cancel(&Cancel, window, cx);
13152 });
13153 cx.executor().run_until_parked();
13154 cx.condition(|editor, _| !editor.context_menu_visible())
13155 .await;
13156
13157 throttle_completions.store(true, atomic::Ordering::Release);
13158 cx.simulate_keystroke(".");
13159 cx.executor()
13160 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
13161 cx.executor().run_until_parked();
13162 cx.condition(|editor, _| editor.context_menu_visible())
13163 .await;
13164 cx.update_editor(|editor, _, _| {
13165 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13166 {
13167 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
13168 "When LSP server is slow, document words can be shown instead, if configured accordingly");
13169 } else {
13170 panic!("expected completion menu to be open");
13171 }
13172 });
13173}
13174
13175#[gpui::test]
13176async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
13177 init_test(cx, |language_settings| {
13178 language_settings.defaults.completions = Some(CompletionSettings {
13179 words: WordsCompletionMode::Enabled,
13180 lsp: true,
13181 lsp_fetch_timeout_ms: 0,
13182 lsp_insert_mode: LspInsertMode::Insert,
13183 });
13184 });
13185
13186 let mut cx = EditorLspTestContext::new_rust(
13187 lsp::ServerCapabilities {
13188 completion_provider: Some(lsp::CompletionOptions {
13189 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13190 ..lsp::CompletionOptions::default()
13191 }),
13192 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13193 ..lsp::ServerCapabilities::default()
13194 },
13195 cx,
13196 )
13197 .await;
13198
13199 let _completion_requests_handler =
13200 cx.lsp
13201 .server
13202 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
13203 Ok(Some(lsp::CompletionResponse::Array(vec![
13204 lsp::CompletionItem {
13205 label: "first".into(),
13206 ..lsp::CompletionItem::default()
13207 },
13208 lsp::CompletionItem {
13209 label: "last".into(),
13210 ..lsp::CompletionItem::default()
13211 },
13212 ])))
13213 });
13214
13215 cx.set_state(indoc! {"ˇ
13216 first
13217 last
13218 second
13219 "});
13220 cx.simulate_keystroke(".");
13221 cx.executor().run_until_parked();
13222 cx.condition(|editor, _| editor.context_menu_visible())
13223 .await;
13224 cx.update_editor(|editor, _, _| {
13225 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13226 {
13227 assert_eq!(
13228 completion_menu_entries(&menu),
13229 &["first", "last", "second"],
13230 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
13231 );
13232 } else {
13233 panic!("expected completion menu to be open");
13234 }
13235 });
13236}
13237
13238#[gpui::test]
13239async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
13240 init_test(cx, |language_settings| {
13241 language_settings.defaults.completions = Some(CompletionSettings {
13242 words: WordsCompletionMode::Disabled,
13243 lsp: true,
13244 lsp_fetch_timeout_ms: 0,
13245 lsp_insert_mode: LspInsertMode::Insert,
13246 });
13247 });
13248
13249 let mut cx = EditorLspTestContext::new_rust(
13250 lsp::ServerCapabilities {
13251 completion_provider: Some(lsp::CompletionOptions {
13252 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13253 ..lsp::CompletionOptions::default()
13254 }),
13255 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13256 ..lsp::ServerCapabilities::default()
13257 },
13258 cx,
13259 )
13260 .await;
13261
13262 let _completion_requests_handler =
13263 cx.lsp
13264 .server
13265 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
13266 panic!("LSP completions should not be queried when dealing with word completions")
13267 });
13268
13269 cx.set_state(indoc! {"ˇ
13270 first
13271 last
13272 second
13273 "});
13274 cx.update_editor(|editor, window, cx| {
13275 editor.show_word_completions(&ShowWordCompletions, window, cx);
13276 });
13277 cx.executor().run_until_parked();
13278 cx.condition(|editor, _| editor.context_menu_visible())
13279 .await;
13280 cx.update_editor(|editor, _, _| {
13281 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13282 {
13283 assert_eq!(
13284 completion_menu_entries(&menu),
13285 &["first", "last", "second"],
13286 "`ShowWordCompletions` action should show word completions"
13287 );
13288 } else {
13289 panic!("expected completion menu to be open");
13290 }
13291 });
13292
13293 cx.simulate_keystroke("l");
13294 cx.executor().run_until_parked();
13295 cx.condition(|editor, _| editor.context_menu_visible())
13296 .await;
13297 cx.update_editor(|editor, _, _| {
13298 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13299 {
13300 assert_eq!(
13301 completion_menu_entries(&menu),
13302 &["last"],
13303 "After showing word completions, further editing should filter them and not query the LSP"
13304 );
13305 } else {
13306 panic!("expected completion menu to be open");
13307 }
13308 });
13309}
13310
13311#[gpui::test]
13312async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
13313 init_test(cx, |language_settings| {
13314 language_settings.defaults.completions = Some(CompletionSettings {
13315 words: WordsCompletionMode::Fallback,
13316 lsp: false,
13317 lsp_fetch_timeout_ms: 0,
13318 lsp_insert_mode: LspInsertMode::Insert,
13319 });
13320 });
13321
13322 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13323
13324 cx.set_state(indoc! {"ˇ
13325 0_usize
13326 let
13327 33
13328 4.5f32
13329 "});
13330 cx.update_editor(|editor, window, cx| {
13331 editor.show_completions(&ShowCompletions::default(), window, cx);
13332 });
13333 cx.executor().run_until_parked();
13334 cx.condition(|editor, _| editor.context_menu_visible())
13335 .await;
13336 cx.update_editor(|editor, window, cx| {
13337 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13338 {
13339 assert_eq!(
13340 completion_menu_entries(&menu),
13341 &["let"],
13342 "With no digits in the completion query, no digits should be in the word completions"
13343 );
13344 } else {
13345 panic!("expected completion menu to be open");
13346 }
13347 editor.cancel(&Cancel, window, cx);
13348 });
13349
13350 cx.set_state(indoc! {"3ˇ
13351 0_usize
13352 let
13353 3
13354 33.35f32
13355 "});
13356 cx.update_editor(|editor, window, cx| {
13357 editor.show_completions(&ShowCompletions::default(), window, cx);
13358 });
13359 cx.executor().run_until_parked();
13360 cx.condition(|editor, _| editor.context_menu_visible())
13361 .await;
13362 cx.update_editor(|editor, _, _| {
13363 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13364 {
13365 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
13366 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
13367 } else {
13368 panic!("expected completion menu to be open");
13369 }
13370 });
13371}
13372
13373fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
13374 let position = || lsp::Position {
13375 line: params.text_document_position.position.line,
13376 character: params.text_document_position.position.character,
13377 };
13378 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13379 range: lsp::Range {
13380 start: position(),
13381 end: position(),
13382 },
13383 new_text: text.to_string(),
13384 }))
13385}
13386
13387#[gpui::test]
13388async fn test_multiline_completion(cx: &mut TestAppContext) {
13389 init_test(cx, |_| {});
13390
13391 let fs = FakeFs::new(cx.executor());
13392 fs.insert_tree(
13393 path!("/a"),
13394 json!({
13395 "main.ts": "a",
13396 }),
13397 )
13398 .await;
13399
13400 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13401 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13402 let typescript_language = Arc::new(Language::new(
13403 LanguageConfig {
13404 name: "TypeScript".into(),
13405 matcher: LanguageMatcher {
13406 path_suffixes: vec!["ts".to_string()],
13407 ..LanguageMatcher::default()
13408 },
13409 line_comments: vec!["// ".into()],
13410 ..LanguageConfig::default()
13411 },
13412 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13413 ));
13414 language_registry.add(typescript_language.clone());
13415 let mut fake_servers = language_registry.register_fake_lsp(
13416 "TypeScript",
13417 FakeLspAdapter {
13418 capabilities: lsp::ServerCapabilities {
13419 completion_provider: Some(lsp::CompletionOptions {
13420 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13421 ..lsp::CompletionOptions::default()
13422 }),
13423 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13424 ..lsp::ServerCapabilities::default()
13425 },
13426 // Emulate vtsls label generation
13427 label_for_completion: Some(Box::new(|item, _| {
13428 let text = if let Some(description) = item
13429 .label_details
13430 .as_ref()
13431 .and_then(|label_details| label_details.description.as_ref())
13432 {
13433 format!("{} {}", item.label, description)
13434 } else if let Some(detail) = &item.detail {
13435 format!("{} {}", item.label, detail)
13436 } else {
13437 item.label.clone()
13438 };
13439 let len = text.len();
13440 Some(language::CodeLabel {
13441 text,
13442 runs: Vec::new(),
13443 filter_range: 0..len,
13444 })
13445 })),
13446 ..FakeLspAdapter::default()
13447 },
13448 );
13449 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13450 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13451 let worktree_id = workspace
13452 .update(cx, |workspace, _window, cx| {
13453 workspace.project().update(cx, |project, cx| {
13454 project.worktrees(cx).next().unwrap().read(cx).id()
13455 })
13456 })
13457 .unwrap();
13458 let _buffer = project
13459 .update(cx, |project, cx| {
13460 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
13461 })
13462 .await
13463 .unwrap();
13464 let editor = workspace
13465 .update(cx, |workspace, window, cx| {
13466 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
13467 })
13468 .unwrap()
13469 .await
13470 .unwrap()
13471 .downcast::<Editor>()
13472 .unwrap();
13473 let fake_server = fake_servers.next().await.unwrap();
13474
13475 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
13476 let multiline_label_2 = "a\nb\nc\n";
13477 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
13478 let multiline_description = "d\ne\nf\n";
13479 let multiline_detail_2 = "g\nh\ni\n";
13480
13481 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
13482 move |params, _| async move {
13483 Ok(Some(lsp::CompletionResponse::Array(vec![
13484 lsp::CompletionItem {
13485 label: multiline_label.to_string(),
13486 text_edit: gen_text_edit(¶ms, "new_text_1"),
13487 ..lsp::CompletionItem::default()
13488 },
13489 lsp::CompletionItem {
13490 label: "single line label 1".to_string(),
13491 detail: Some(multiline_detail.to_string()),
13492 text_edit: gen_text_edit(¶ms, "new_text_2"),
13493 ..lsp::CompletionItem::default()
13494 },
13495 lsp::CompletionItem {
13496 label: "single line label 2".to_string(),
13497 label_details: Some(lsp::CompletionItemLabelDetails {
13498 description: Some(multiline_description.to_string()),
13499 detail: None,
13500 }),
13501 text_edit: gen_text_edit(¶ms, "new_text_2"),
13502 ..lsp::CompletionItem::default()
13503 },
13504 lsp::CompletionItem {
13505 label: multiline_label_2.to_string(),
13506 detail: Some(multiline_detail_2.to_string()),
13507 text_edit: gen_text_edit(¶ms, "new_text_3"),
13508 ..lsp::CompletionItem::default()
13509 },
13510 lsp::CompletionItem {
13511 label: "Label with many spaces and \t but without newlines".to_string(),
13512 detail: Some(
13513 "Details with many spaces and \t but without newlines".to_string(),
13514 ),
13515 text_edit: gen_text_edit(¶ms, "new_text_4"),
13516 ..lsp::CompletionItem::default()
13517 },
13518 ])))
13519 },
13520 );
13521
13522 editor.update_in(cx, |editor, window, cx| {
13523 cx.focus_self(window);
13524 editor.move_to_end(&MoveToEnd, window, cx);
13525 editor.handle_input(".", window, cx);
13526 });
13527 cx.run_until_parked();
13528 completion_handle.next().await.unwrap();
13529
13530 editor.update(cx, |editor, _| {
13531 assert!(editor.context_menu_visible());
13532 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13533 {
13534 let completion_labels = menu
13535 .completions
13536 .borrow()
13537 .iter()
13538 .map(|c| c.label.text.clone())
13539 .collect::<Vec<_>>();
13540 assert_eq!(
13541 completion_labels,
13542 &[
13543 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
13544 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
13545 "single line label 2 d e f ",
13546 "a b c g h i ",
13547 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
13548 ],
13549 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
13550 );
13551
13552 for completion in menu
13553 .completions
13554 .borrow()
13555 .iter() {
13556 assert_eq!(
13557 completion.label.filter_range,
13558 0..completion.label.text.len(),
13559 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
13560 );
13561 }
13562 } else {
13563 panic!("expected completion menu to be open");
13564 }
13565 });
13566}
13567
13568#[gpui::test]
13569async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
13570 init_test(cx, |_| {});
13571 let mut cx = EditorLspTestContext::new_rust(
13572 lsp::ServerCapabilities {
13573 completion_provider: Some(lsp::CompletionOptions {
13574 trigger_characters: Some(vec![".".to_string()]),
13575 ..Default::default()
13576 }),
13577 ..Default::default()
13578 },
13579 cx,
13580 )
13581 .await;
13582 cx.lsp
13583 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13584 Ok(Some(lsp::CompletionResponse::Array(vec![
13585 lsp::CompletionItem {
13586 label: "first".into(),
13587 ..Default::default()
13588 },
13589 lsp::CompletionItem {
13590 label: "last".into(),
13591 ..Default::default()
13592 },
13593 ])))
13594 });
13595 cx.set_state("variableˇ");
13596 cx.simulate_keystroke(".");
13597 cx.executor().run_until_parked();
13598
13599 cx.update_editor(|editor, _, _| {
13600 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13601 {
13602 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
13603 } else {
13604 panic!("expected completion menu to be open");
13605 }
13606 });
13607
13608 cx.update_editor(|editor, window, cx| {
13609 editor.move_page_down(&MovePageDown::default(), window, cx);
13610 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13611 {
13612 assert!(
13613 menu.selected_item == 1,
13614 "expected PageDown to select the last item from the context menu"
13615 );
13616 } else {
13617 panic!("expected completion menu to stay open after PageDown");
13618 }
13619 });
13620
13621 cx.update_editor(|editor, window, cx| {
13622 editor.move_page_up(&MovePageUp::default(), window, cx);
13623 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13624 {
13625 assert!(
13626 menu.selected_item == 0,
13627 "expected PageUp to select the first item from the context menu"
13628 );
13629 } else {
13630 panic!("expected completion menu to stay open after PageUp");
13631 }
13632 });
13633}
13634
13635#[gpui::test]
13636async fn test_as_is_completions(cx: &mut TestAppContext) {
13637 init_test(cx, |_| {});
13638 let mut cx = EditorLspTestContext::new_rust(
13639 lsp::ServerCapabilities {
13640 completion_provider: Some(lsp::CompletionOptions {
13641 ..Default::default()
13642 }),
13643 ..Default::default()
13644 },
13645 cx,
13646 )
13647 .await;
13648 cx.lsp
13649 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13650 Ok(Some(lsp::CompletionResponse::Array(vec![
13651 lsp::CompletionItem {
13652 label: "unsafe".into(),
13653 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13654 range: lsp::Range {
13655 start: lsp::Position {
13656 line: 1,
13657 character: 2,
13658 },
13659 end: lsp::Position {
13660 line: 1,
13661 character: 3,
13662 },
13663 },
13664 new_text: "unsafe".to_string(),
13665 })),
13666 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
13667 ..Default::default()
13668 },
13669 ])))
13670 });
13671 cx.set_state("fn a() {}\n nˇ");
13672 cx.executor().run_until_parked();
13673 cx.update_editor(|editor, window, cx| {
13674 editor.show_completions(
13675 &ShowCompletions {
13676 trigger: Some("\n".into()),
13677 },
13678 window,
13679 cx,
13680 );
13681 });
13682 cx.executor().run_until_parked();
13683
13684 cx.update_editor(|editor, window, cx| {
13685 editor.confirm_completion(&Default::default(), window, cx)
13686 });
13687 cx.executor().run_until_parked();
13688 cx.assert_editor_state("fn a() {}\n unsafeˇ");
13689}
13690
13691#[gpui::test]
13692async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
13693 init_test(cx, |_| {});
13694 let language =
13695 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
13696 let mut cx = EditorLspTestContext::new(
13697 language,
13698 lsp::ServerCapabilities {
13699 completion_provider: Some(lsp::CompletionOptions {
13700 ..lsp::CompletionOptions::default()
13701 }),
13702 ..lsp::ServerCapabilities::default()
13703 },
13704 cx,
13705 )
13706 .await;
13707
13708 cx.set_state(
13709 "#ifndef BAR_H
13710#define BAR_H
13711
13712#include <stdbool.h>
13713
13714int fn_branch(bool do_branch1, bool do_branch2);
13715
13716#endif // BAR_H
13717ˇ",
13718 );
13719 cx.executor().run_until_parked();
13720 cx.update_editor(|editor, window, cx| {
13721 editor.handle_input("#", window, cx);
13722 });
13723 cx.executor().run_until_parked();
13724 cx.update_editor(|editor, window, cx| {
13725 editor.handle_input("i", window, cx);
13726 });
13727 cx.executor().run_until_parked();
13728 cx.update_editor(|editor, window, cx| {
13729 editor.handle_input("n", window, cx);
13730 });
13731 cx.executor().run_until_parked();
13732 cx.assert_editor_state(
13733 "#ifndef BAR_H
13734#define BAR_H
13735
13736#include <stdbool.h>
13737
13738int fn_branch(bool do_branch1, bool do_branch2);
13739
13740#endif // BAR_H
13741#inˇ",
13742 );
13743
13744 cx.lsp
13745 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13746 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13747 is_incomplete: false,
13748 item_defaults: None,
13749 items: vec![lsp::CompletionItem {
13750 kind: Some(lsp::CompletionItemKind::SNIPPET),
13751 label_details: Some(lsp::CompletionItemLabelDetails {
13752 detail: Some("header".to_string()),
13753 description: None,
13754 }),
13755 label: " include".to_string(),
13756 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13757 range: lsp::Range {
13758 start: lsp::Position {
13759 line: 8,
13760 character: 1,
13761 },
13762 end: lsp::Position {
13763 line: 8,
13764 character: 1,
13765 },
13766 },
13767 new_text: "include \"$0\"".to_string(),
13768 })),
13769 sort_text: Some("40b67681include".to_string()),
13770 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13771 filter_text: Some("include".to_string()),
13772 insert_text: Some("include \"$0\"".to_string()),
13773 ..lsp::CompletionItem::default()
13774 }],
13775 })))
13776 });
13777 cx.update_editor(|editor, window, cx| {
13778 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13779 });
13780 cx.executor().run_until_parked();
13781 cx.update_editor(|editor, window, cx| {
13782 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
13783 });
13784 cx.executor().run_until_parked();
13785 cx.assert_editor_state(
13786 "#ifndef BAR_H
13787#define BAR_H
13788
13789#include <stdbool.h>
13790
13791int fn_branch(bool do_branch1, bool do_branch2);
13792
13793#endif // BAR_H
13794#include \"ˇ\"",
13795 );
13796
13797 cx.lsp
13798 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13799 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13800 is_incomplete: true,
13801 item_defaults: None,
13802 items: vec![lsp::CompletionItem {
13803 kind: Some(lsp::CompletionItemKind::FILE),
13804 label: "AGL/".to_string(),
13805 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13806 range: lsp::Range {
13807 start: lsp::Position {
13808 line: 8,
13809 character: 10,
13810 },
13811 end: lsp::Position {
13812 line: 8,
13813 character: 11,
13814 },
13815 },
13816 new_text: "AGL/".to_string(),
13817 })),
13818 sort_text: Some("40b67681AGL/".to_string()),
13819 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13820 filter_text: Some("AGL/".to_string()),
13821 insert_text: Some("AGL/".to_string()),
13822 ..lsp::CompletionItem::default()
13823 }],
13824 })))
13825 });
13826 cx.update_editor(|editor, window, cx| {
13827 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13828 });
13829 cx.executor().run_until_parked();
13830 cx.update_editor(|editor, window, cx| {
13831 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
13832 });
13833 cx.executor().run_until_parked();
13834 cx.assert_editor_state(
13835 r##"#ifndef BAR_H
13836#define BAR_H
13837
13838#include <stdbool.h>
13839
13840int fn_branch(bool do_branch1, bool do_branch2);
13841
13842#endif // BAR_H
13843#include "AGL/ˇ"##,
13844 );
13845
13846 cx.update_editor(|editor, window, cx| {
13847 editor.handle_input("\"", window, cx);
13848 });
13849 cx.executor().run_until_parked();
13850 cx.assert_editor_state(
13851 r##"#ifndef BAR_H
13852#define BAR_H
13853
13854#include <stdbool.h>
13855
13856int fn_branch(bool do_branch1, bool do_branch2);
13857
13858#endif // BAR_H
13859#include "AGL/"ˇ"##,
13860 );
13861}
13862
13863#[gpui::test]
13864async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
13865 init_test(cx, |_| {});
13866
13867 let mut cx = EditorLspTestContext::new_rust(
13868 lsp::ServerCapabilities {
13869 completion_provider: Some(lsp::CompletionOptions {
13870 trigger_characters: Some(vec![".".to_string()]),
13871 resolve_provider: Some(true),
13872 ..Default::default()
13873 }),
13874 ..Default::default()
13875 },
13876 cx,
13877 )
13878 .await;
13879
13880 cx.set_state("fn main() { let a = 2ˇ; }");
13881 cx.simulate_keystroke(".");
13882 let completion_item = lsp::CompletionItem {
13883 label: "Some".into(),
13884 kind: Some(lsp::CompletionItemKind::SNIPPET),
13885 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13886 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13887 kind: lsp::MarkupKind::Markdown,
13888 value: "```rust\nSome(2)\n```".to_string(),
13889 })),
13890 deprecated: Some(false),
13891 sort_text: Some("Some".to_string()),
13892 filter_text: Some("Some".to_string()),
13893 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13894 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13895 range: lsp::Range {
13896 start: lsp::Position {
13897 line: 0,
13898 character: 22,
13899 },
13900 end: lsp::Position {
13901 line: 0,
13902 character: 22,
13903 },
13904 },
13905 new_text: "Some(2)".to_string(),
13906 })),
13907 additional_text_edits: Some(vec![lsp::TextEdit {
13908 range: lsp::Range {
13909 start: lsp::Position {
13910 line: 0,
13911 character: 20,
13912 },
13913 end: lsp::Position {
13914 line: 0,
13915 character: 22,
13916 },
13917 },
13918 new_text: "".to_string(),
13919 }]),
13920 ..Default::default()
13921 };
13922
13923 let closure_completion_item = completion_item.clone();
13924 let counter = Arc::new(AtomicUsize::new(0));
13925 let counter_clone = counter.clone();
13926 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13927 let task_completion_item = closure_completion_item.clone();
13928 counter_clone.fetch_add(1, atomic::Ordering::Release);
13929 async move {
13930 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13931 is_incomplete: true,
13932 item_defaults: None,
13933 items: vec![task_completion_item],
13934 })))
13935 }
13936 });
13937
13938 cx.condition(|editor, _| editor.context_menu_visible())
13939 .await;
13940 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13941 assert!(request.next().await.is_some());
13942 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13943
13944 cx.simulate_keystrokes("S o m");
13945 cx.condition(|editor, _| editor.context_menu_visible())
13946 .await;
13947 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13948 assert!(request.next().await.is_some());
13949 assert!(request.next().await.is_some());
13950 assert!(request.next().await.is_some());
13951 request.close();
13952 assert!(request.next().await.is_none());
13953 assert_eq!(
13954 counter.load(atomic::Ordering::Acquire),
13955 4,
13956 "With the completions menu open, only one LSP request should happen per input"
13957 );
13958}
13959
13960#[gpui::test]
13961async fn test_toggle_comment(cx: &mut TestAppContext) {
13962 init_test(cx, |_| {});
13963 let mut cx = EditorTestContext::new(cx).await;
13964 let language = Arc::new(Language::new(
13965 LanguageConfig {
13966 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13967 ..Default::default()
13968 },
13969 Some(tree_sitter_rust::LANGUAGE.into()),
13970 ));
13971 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13972
13973 // If multiple selections intersect a line, the line is only toggled once.
13974 cx.set_state(indoc! {"
13975 fn a() {
13976 «//b();
13977 ˇ»// «c();
13978 //ˇ» d();
13979 }
13980 "});
13981
13982 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13983
13984 cx.assert_editor_state(indoc! {"
13985 fn a() {
13986 «b();
13987 c();
13988 ˇ» d();
13989 }
13990 "});
13991
13992 // The comment prefix is inserted at the same column for every line in a
13993 // selection.
13994 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13995
13996 cx.assert_editor_state(indoc! {"
13997 fn a() {
13998 // «b();
13999 // c();
14000 ˇ»// d();
14001 }
14002 "});
14003
14004 // If a selection ends at the beginning of a line, that line is not toggled.
14005 cx.set_selections_state(indoc! {"
14006 fn a() {
14007 // b();
14008 «// c();
14009 ˇ» // d();
14010 }
14011 "});
14012
14013 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14014
14015 cx.assert_editor_state(indoc! {"
14016 fn a() {
14017 // b();
14018 «c();
14019 ˇ» // d();
14020 }
14021 "});
14022
14023 // If a selection span a single line and is empty, the line is toggled.
14024 cx.set_state(indoc! {"
14025 fn a() {
14026 a();
14027 b();
14028 ˇ
14029 }
14030 "});
14031
14032 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14033
14034 cx.assert_editor_state(indoc! {"
14035 fn a() {
14036 a();
14037 b();
14038 //•ˇ
14039 }
14040 "});
14041
14042 // If a selection span multiple lines, empty lines are not toggled.
14043 cx.set_state(indoc! {"
14044 fn a() {
14045 «a();
14046
14047 c();ˇ»
14048 }
14049 "});
14050
14051 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14052
14053 cx.assert_editor_state(indoc! {"
14054 fn a() {
14055 // «a();
14056
14057 // c();ˇ»
14058 }
14059 "});
14060
14061 // If a selection includes multiple comment prefixes, all lines are uncommented.
14062 cx.set_state(indoc! {"
14063 fn a() {
14064 «// a();
14065 /// b();
14066 //! c();ˇ»
14067 }
14068 "});
14069
14070 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14071
14072 cx.assert_editor_state(indoc! {"
14073 fn a() {
14074 «a();
14075 b();
14076 c();ˇ»
14077 }
14078 "});
14079}
14080
14081#[gpui::test]
14082async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
14083 init_test(cx, |_| {});
14084 let mut cx = EditorTestContext::new(cx).await;
14085 let language = Arc::new(Language::new(
14086 LanguageConfig {
14087 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
14088 ..Default::default()
14089 },
14090 Some(tree_sitter_rust::LANGUAGE.into()),
14091 ));
14092 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
14093
14094 let toggle_comments = &ToggleComments {
14095 advance_downwards: false,
14096 ignore_indent: true,
14097 };
14098
14099 // If multiple selections intersect a line, the line is only toggled once.
14100 cx.set_state(indoc! {"
14101 fn a() {
14102 // «b();
14103 // c();
14104 // ˇ» d();
14105 }
14106 "});
14107
14108 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14109
14110 cx.assert_editor_state(indoc! {"
14111 fn a() {
14112 «b();
14113 c();
14114 ˇ» d();
14115 }
14116 "});
14117
14118 // The comment prefix is inserted at the beginning of each line
14119 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14120
14121 cx.assert_editor_state(indoc! {"
14122 fn a() {
14123 // «b();
14124 // c();
14125 // ˇ» d();
14126 }
14127 "});
14128
14129 // If a selection ends at the beginning of a line, that line is not toggled.
14130 cx.set_selections_state(indoc! {"
14131 fn a() {
14132 // b();
14133 // «c();
14134 ˇ»// d();
14135 }
14136 "});
14137
14138 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14139
14140 cx.assert_editor_state(indoc! {"
14141 fn a() {
14142 // b();
14143 «c();
14144 ˇ»// d();
14145 }
14146 "});
14147
14148 // If a selection span a single line and is empty, the line is toggled.
14149 cx.set_state(indoc! {"
14150 fn a() {
14151 a();
14152 b();
14153 ˇ
14154 }
14155 "});
14156
14157 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14158
14159 cx.assert_editor_state(indoc! {"
14160 fn a() {
14161 a();
14162 b();
14163 //ˇ
14164 }
14165 "});
14166
14167 // If a selection span multiple lines, empty lines are not toggled.
14168 cx.set_state(indoc! {"
14169 fn a() {
14170 «a();
14171
14172 c();ˇ»
14173 }
14174 "});
14175
14176 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14177
14178 cx.assert_editor_state(indoc! {"
14179 fn a() {
14180 // «a();
14181
14182 // c();ˇ»
14183 }
14184 "});
14185
14186 // If a selection includes multiple comment prefixes, all lines are uncommented.
14187 cx.set_state(indoc! {"
14188 fn a() {
14189 // «a();
14190 /// b();
14191 //! c();ˇ»
14192 }
14193 "});
14194
14195 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14196
14197 cx.assert_editor_state(indoc! {"
14198 fn a() {
14199 «a();
14200 b();
14201 c();ˇ»
14202 }
14203 "});
14204}
14205
14206#[gpui::test]
14207async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
14208 init_test(cx, |_| {});
14209
14210 let language = Arc::new(Language::new(
14211 LanguageConfig {
14212 line_comments: vec!["// ".into()],
14213 ..Default::default()
14214 },
14215 Some(tree_sitter_rust::LANGUAGE.into()),
14216 ));
14217
14218 let mut cx = EditorTestContext::new(cx).await;
14219
14220 cx.language_registry().add(language.clone());
14221 cx.update_buffer(|buffer, cx| {
14222 buffer.set_language(Some(language), cx);
14223 });
14224
14225 let toggle_comments = &ToggleComments {
14226 advance_downwards: true,
14227 ignore_indent: false,
14228 };
14229
14230 // Single cursor on one line -> advance
14231 // Cursor moves horizontally 3 characters as well on non-blank line
14232 cx.set_state(indoc!(
14233 "fn a() {
14234 ˇdog();
14235 cat();
14236 }"
14237 ));
14238 cx.update_editor(|editor, window, cx| {
14239 editor.toggle_comments(toggle_comments, window, cx);
14240 });
14241 cx.assert_editor_state(indoc!(
14242 "fn a() {
14243 // dog();
14244 catˇ();
14245 }"
14246 ));
14247
14248 // Single selection on one line -> don't advance
14249 cx.set_state(indoc!(
14250 "fn a() {
14251 «dog()ˇ»;
14252 cat();
14253 }"
14254 ));
14255 cx.update_editor(|editor, window, cx| {
14256 editor.toggle_comments(toggle_comments, window, cx);
14257 });
14258 cx.assert_editor_state(indoc!(
14259 "fn a() {
14260 // «dog()ˇ»;
14261 cat();
14262 }"
14263 ));
14264
14265 // Multiple cursors on one line -> advance
14266 cx.set_state(indoc!(
14267 "fn a() {
14268 ˇdˇog();
14269 cat();
14270 }"
14271 ));
14272 cx.update_editor(|editor, window, cx| {
14273 editor.toggle_comments(toggle_comments, window, cx);
14274 });
14275 cx.assert_editor_state(indoc!(
14276 "fn a() {
14277 // dog();
14278 catˇ(ˇ);
14279 }"
14280 ));
14281
14282 // Multiple cursors on one line, with selection -> don't advance
14283 cx.set_state(indoc!(
14284 "fn a() {
14285 ˇdˇog«()ˇ»;
14286 cat();
14287 }"
14288 ));
14289 cx.update_editor(|editor, window, cx| {
14290 editor.toggle_comments(toggle_comments, window, cx);
14291 });
14292 cx.assert_editor_state(indoc!(
14293 "fn a() {
14294 // ˇdˇog«()ˇ»;
14295 cat();
14296 }"
14297 ));
14298
14299 // Single cursor on one line -> advance
14300 // Cursor moves to column 0 on blank line
14301 cx.set_state(indoc!(
14302 "fn a() {
14303 ˇdog();
14304
14305 cat();
14306 }"
14307 ));
14308 cx.update_editor(|editor, window, cx| {
14309 editor.toggle_comments(toggle_comments, window, cx);
14310 });
14311 cx.assert_editor_state(indoc!(
14312 "fn a() {
14313 // dog();
14314 ˇ
14315 cat();
14316 }"
14317 ));
14318
14319 // Single cursor on one line -> advance
14320 // Cursor starts and ends at column 0
14321 cx.set_state(indoc!(
14322 "fn a() {
14323 ˇ dog();
14324 cat();
14325 }"
14326 ));
14327 cx.update_editor(|editor, window, cx| {
14328 editor.toggle_comments(toggle_comments, window, cx);
14329 });
14330 cx.assert_editor_state(indoc!(
14331 "fn a() {
14332 // dog();
14333 ˇ cat();
14334 }"
14335 ));
14336}
14337
14338#[gpui::test]
14339async fn test_toggle_block_comment(cx: &mut TestAppContext) {
14340 init_test(cx, |_| {});
14341
14342 let mut cx = EditorTestContext::new(cx).await;
14343
14344 let html_language = Arc::new(
14345 Language::new(
14346 LanguageConfig {
14347 name: "HTML".into(),
14348 block_comment: Some(BlockCommentConfig {
14349 start: "<!-- ".into(),
14350 prefix: "".into(),
14351 end: " -->".into(),
14352 tab_size: 0,
14353 }),
14354 ..Default::default()
14355 },
14356 Some(tree_sitter_html::LANGUAGE.into()),
14357 )
14358 .with_injection_query(
14359 r#"
14360 (script_element
14361 (raw_text) @injection.content
14362 (#set! injection.language "javascript"))
14363 "#,
14364 )
14365 .unwrap(),
14366 );
14367
14368 let javascript_language = Arc::new(Language::new(
14369 LanguageConfig {
14370 name: "JavaScript".into(),
14371 line_comments: vec!["// ".into()],
14372 ..Default::default()
14373 },
14374 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14375 ));
14376
14377 cx.language_registry().add(html_language.clone());
14378 cx.language_registry().add(javascript_language.clone());
14379 cx.update_buffer(|buffer, cx| {
14380 buffer.set_language(Some(html_language), cx);
14381 });
14382
14383 // Toggle comments for empty selections
14384 cx.set_state(
14385 &r#"
14386 <p>A</p>ˇ
14387 <p>B</p>ˇ
14388 <p>C</p>ˇ
14389 "#
14390 .unindent(),
14391 );
14392 cx.update_editor(|editor, window, cx| {
14393 editor.toggle_comments(&ToggleComments::default(), window, cx)
14394 });
14395 cx.assert_editor_state(
14396 &r#"
14397 <!-- <p>A</p>ˇ -->
14398 <!-- <p>B</p>ˇ -->
14399 <!-- <p>C</p>ˇ -->
14400 "#
14401 .unindent(),
14402 );
14403 cx.update_editor(|editor, window, cx| {
14404 editor.toggle_comments(&ToggleComments::default(), window, cx)
14405 });
14406 cx.assert_editor_state(
14407 &r#"
14408 <p>A</p>ˇ
14409 <p>B</p>ˇ
14410 <p>C</p>ˇ
14411 "#
14412 .unindent(),
14413 );
14414
14415 // Toggle comments for mixture of empty and non-empty selections, where
14416 // multiple selections occupy a given line.
14417 cx.set_state(
14418 &r#"
14419 <p>A«</p>
14420 <p>ˇ»B</p>ˇ
14421 <p>C«</p>
14422 <p>ˇ»D</p>ˇ
14423 "#
14424 .unindent(),
14425 );
14426
14427 cx.update_editor(|editor, window, cx| {
14428 editor.toggle_comments(&ToggleComments::default(), window, cx)
14429 });
14430 cx.assert_editor_state(
14431 &r#"
14432 <!-- <p>A«</p>
14433 <p>ˇ»B</p>ˇ -->
14434 <!-- <p>C«</p>
14435 <p>ˇ»D</p>ˇ -->
14436 "#
14437 .unindent(),
14438 );
14439 cx.update_editor(|editor, window, cx| {
14440 editor.toggle_comments(&ToggleComments::default(), window, cx)
14441 });
14442 cx.assert_editor_state(
14443 &r#"
14444 <p>A«</p>
14445 <p>ˇ»B</p>ˇ
14446 <p>C«</p>
14447 <p>ˇ»D</p>ˇ
14448 "#
14449 .unindent(),
14450 );
14451
14452 // Toggle comments when different languages are active for different
14453 // selections.
14454 cx.set_state(
14455 &r#"
14456 ˇ<script>
14457 ˇvar x = new Y();
14458 ˇ</script>
14459 "#
14460 .unindent(),
14461 );
14462 cx.executor().run_until_parked();
14463 cx.update_editor(|editor, window, cx| {
14464 editor.toggle_comments(&ToggleComments::default(), window, cx)
14465 });
14466 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
14467 // Uncommenting and commenting from this position brings in even more wrong artifacts.
14468 cx.assert_editor_state(
14469 &r#"
14470 <!-- ˇ<script> -->
14471 // ˇvar x = new Y();
14472 <!-- ˇ</script> -->
14473 "#
14474 .unindent(),
14475 );
14476}
14477
14478#[gpui::test]
14479fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
14480 init_test(cx, |_| {});
14481
14482 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14483 let multibuffer = cx.new(|cx| {
14484 let mut multibuffer = MultiBuffer::new(ReadWrite);
14485 multibuffer.push_excerpts(
14486 buffer.clone(),
14487 [
14488 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
14489 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
14490 ],
14491 cx,
14492 );
14493 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
14494 multibuffer
14495 });
14496
14497 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14498 editor.update_in(cx, |editor, window, cx| {
14499 assert_eq!(editor.text(cx), "aaaa\nbbbb");
14500 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14501 s.select_ranges([
14502 Point::new(0, 0)..Point::new(0, 0),
14503 Point::new(1, 0)..Point::new(1, 0),
14504 ])
14505 });
14506
14507 editor.handle_input("X", window, cx);
14508 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
14509 assert_eq!(
14510 editor.selections.ranges(cx),
14511 [
14512 Point::new(0, 1)..Point::new(0, 1),
14513 Point::new(1, 1)..Point::new(1, 1),
14514 ]
14515 );
14516
14517 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
14518 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14519 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
14520 });
14521 editor.backspace(&Default::default(), window, cx);
14522 assert_eq!(editor.text(cx), "Xa\nbbb");
14523 assert_eq!(
14524 editor.selections.ranges(cx),
14525 [Point::new(1, 0)..Point::new(1, 0)]
14526 );
14527
14528 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14529 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
14530 });
14531 editor.backspace(&Default::default(), window, cx);
14532 assert_eq!(editor.text(cx), "X\nbb");
14533 assert_eq!(
14534 editor.selections.ranges(cx),
14535 [Point::new(0, 1)..Point::new(0, 1)]
14536 );
14537 });
14538}
14539
14540#[gpui::test]
14541fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
14542 init_test(cx, |_| {});
14543
14544 let markers = vec![('[', ']').into(), ('(', ')').into()];
14545 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
14546 indoc! {"
14547 [aaaa
14548 (bbbb]
14549 cccc)",
14550 },
14551 markers.clone(),
14552 );
14553 let excerpt_ranges = markers.into_iter().map(|marker| {
14554 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
14555 ExcerptRange::new(context.clone())
14556 });
14557 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
14558 let multibuffer = cx.new(|cx| {
14559 let mut multibuffer = MultiBuffer::new(ReadWrite);
14560 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
14561 multibuffer
14562 });
14563
14564 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14565 editor.update_in(cx, |editor, window, cx| {
14566 let (expected_text, selection_ranges) = marked_text_ranges(
14567 indoc! {"
14568 aaaa
14569 bˇbbb
14570 bˇbbˇb
14571 cccc"
14572 },
14573 true,
14574 );
14575 assert_eq!(editor.text(cx), expected_text);
14576 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14577 s.select_ranges(selection_ranges)
14578 });
14579
14580 editor.handle_input("X", window, cx);
14581
14582 let (expected_text, expected_selections) = marked_text_ranges(
14583 indoc! {"
14584 aaaa
14585 bXˇbbXb
14586 bXˇbbXˇb
14587 cccc"
14588 },
14589 false,
14590 );
14591 assert_eq!(editor.text(cx), expected_text);
14592 assert_eq!(editor.selections.ranges(cx), expected_selections);
14593
14594 editor.newline(&Newline, window, cx);
14595 let (expected_text, expected_selections) = marked_text_ranges(
14596 indoc! {"
14597 aaaa
14598 bX
14599 ˇbbX
14600 b
14601 bX
14602 ˇbbX
14603 ˇb
14604 cccc"
14605 },
14606 false,
14607 );
14608 assert_eq!(editor.text(cx), expected_text);
14609 assert_eq!(editor.selections.ranges(cx), expected_selections);
14610 });
14611}
14612
14613#[gpui::test]
14614fn test_refresh_selections(cx: &mut TestAppContext) {
14615 init_test(cx, |_| {});
14616
14617 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14618 let mut excerpt1_id = None;
14619 let multibuffer = cx.new(|cx| {
14620 let mut multibuffer = MultiBuffer::new(ReadWrite);
14621 excerpt1_id = multibuffer
14622 .push_excerpts(
14623 buffer.clone(),
14624 [
14625 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14626 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14627 ],
14628 cx,
14629 )
14630 .into_iter()
14631 .next();
14632 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14633 multibuffer
14634 });
14635
14636 let editor = cx.add_window(|window, cx| {
14637 let mut editor = build_editor(multibuffer.clone(), window, cx);
14638 let snapshot = editor.snapshot(window, cx);
14639 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14640 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
14641 });
14642 editor.begin_selection(
14643 Point::new(2, 1).to_display_point(&snapshot),
14644 true,
14645 1,
14646 window,
14647 cx,
14648 );
14649 assert_eq!(
14650 editor.selections.ranges(cx),
14651 [
14652 Point::new(1, 3)..Point::new(1, 3),
14653 Point::new(2, 1)..Point::new(2, 1),
14654 ]
14655 );
14656 editor
14657 });
14658
14659 // Refreshing selections is a no-op when excerpts haven't changed.
14660 _ = editor.update(cx, |editor, window, cx| {
14661 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14662 assert_eq!(
14663 editor.selections.ranges(cx),
14664 [
14665 Point::new(1, 3)..Point::new(1, 3),
14666 Point::new(2, 1)..Point::new(2, 1),
14667 ]
14668 );
14669 });
14670
14671 multibuffer.update(cx, |multibuffer, cx| {
14672 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14673 });
14674 _ = editor.update(cx, |editor, window, cx| {
14675 // Removing an excerpt causes the first selection to become degenerate.
14676 assert_eq!(
14677 editor.selections.ranges(cx),
14678 [
14679 Point::new(0, 0)..Point::new(0, 0),
14680 Point::new(0, 1)..Point::new(0, 1)
14681 ]
14682 );
14683
14684 // Refreshing selections will relocate the first selection to the original buffer
14685 // location.
14686 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14687 assert_eq!(
14688 editor.selections.ranges(cx),
14689 [
14690 Point::new(0, 1)..Point::new(0, 1),
14691 Point::new(0, 3)..Point::new(0, 3)
14692 ]
14693 );
14694 assert!(editor.selections.pending_anchor().is_some());
14695 });
14696}
14697
14698#[gpui::test]
14699fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
14700 init_test(cx, |_| {});
14701
14702 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14703 let mut excerpt1_id = None;
14704 let multibuffer = cx.new(|cx| {
14705 let mut multibuffer = MultiBuffer::new(ReadWrite);
14706 excerpt1_id = multibuffer
14707 .push_excerpts(
14708 buffer.clone(),
14709 [
14710 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14711 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14712 ],
14713 cx,
14714 )
14715 .into_iter()
14716 .next();
14717 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14718 multibuffer
14719 });
14720
14721 let editor = cx.add_window(|window, cx| {
14722 let mut editor = build_editor(multibuffer.clone(), window, cx);
14723 let snapshot = editor.snapshot(window, cx);
14724 editor.begin_selection(
14725 Point::new(1, 3).to_display_point(&snapshot),
14726 false,
14727 1,
14728 window,
14729 cx,
14730 );
14731 assert_eq!(
14732 editor.selections.ranges(cx),
14733 [Point::new(1, 3)..Point::new(1, 3)]
14734 );
14735 editor
14736 });
14737
14738 multibuffer.update(cx, |multibuffer, cx| {
14739 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14740 });
14741 _ = editor.update(cx, |editor, window, cx| {
14742 assert_eq!(
14743 editor.selections.ranges(cx),
14744 [Point::new(0, 0)..Point::new(0, 0)]
14745 );
14746
14747 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
14748 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14749 assert_eq!(
14750 editor.selections.ranges(cx),
14751 [Point::new(0, 3)..Point::new(0, 3)]
14752 );
14753 assert!(editor.selections.pending_anchor().is_some());
14754 });
14755}
14756
14757#[gpui::test]
14758async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
14759 init_test(cx, |_| {});
14760
14761 let language = Arc::new(
14762 Language::new(
14763 LanguageConfig {
14764 brackets: BracketPairConfig {
14765 pairs: vec![
14766 BracketPair {
14767 start: "{".to_string(),
14768 end: "}".to_string(),
14769 close: true,
14770 surround: true,
14771 newline: true,
14772 },
14773 BracketPair {
14774 start: "/* ".to_string(),
14775 end: " */".to_string(),
14776 close: true,
14777 surround: true,
14778 newline: true,
14779 },
14780 ],
14781 ..Default::default()
14782 },
14783 ..Default::default()
14784 },
14785 Some(tree_sitter_rust::LANGUAGE.into()),
14786 )
14787 .with_indents_query("")
14788 .unwrap(),
14789 );
14790
14791 let text = concat!(
14792 "{ }\n", //
14793 " x\n", //
14794 " /* */\n", //
14795 "x\n", //
14796 "{{} }\n", //
14797 );
14798
14799 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14800 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14801 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14802 editor
14803 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
14804 .await;
14805
14806 editor.update_in(cx, |editor, window, cx| {
14807 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14808 s.select_display_ranges([
14809 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
14810 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
14811 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
14812 ])
14813 });
14814 editor.newline(&Newline, window, cx);
14815
14816 assert_eq!(
14817 editor.buffer().read(cx).read(cx).text(),
14818 concat!(
14819 "{ \n", // Suppress rustfmt
14820 "\n", //
14821 "}\n", //
14822 " x\n", //
14823 " /* \n", //
14824 " \n", //
14825 " */\n", //
14826 "x\n", //
14827 "{{} \n", //
14828 "}\n", //
14829 )
14830 );
14831 });
14832}
14833
14834#[gpui::test]
14835fn test_highlighted_ranges(cx: &mut TestAppContext) {
14836 init_test(cx, |_| {});
14837
14838 let editor = cx.add_window(|window, cx| {
14839 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
14840 build_editor(buffer.clone(), window, cx)
14841 });
14842
14843 _ = editor.update(cx, |editor, window, cx| {
14844 struct Type1;
14845 struct Type2;
14846
14847 let buffer = editor.buffer.read(cx).snapshot(cx);
14848
14849 let anchor_range =
14850 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
14851
14852 editor.highlight_background::<Type1>(
14853 &[
14854 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
14855 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
14856 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
14857 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
14858 ],
14859 |_| Hsla::red(),
14860 cx,
14861 );
14862 editor.highlight_background::<Type2>(
14863 &[
14864 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
14865 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
14866 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
14867 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
14868 ],
14869 |_| Hsla::green(),
14870 cx,
14871 );
14872
14873 let snapshot = editor.snapshot(window, cx);
14874 let mut highlighted_ranges = editor.background_highlights_in_range(
14875 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
14876 &snapshot,
14877 cx.theme(),
14878 );
14879 // Enforce a consistent ordering based on color without relying on the ordering of the
14880 // highlight's `TypeId` which is non-executor.
14881 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
14882 assert_eq!(
14883 highlighted_ranges,
14884 &[
14885 (
14886 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
14887 Hsla::red(),
14888 ),
14889 (
14890 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14891 Hsla::red(),
14892 ),
14893 (
14894 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
14895 Hsla::green(),
14896 ),
14897 (
14898 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
14899 Hsla::green(),
14900 ),
14901 ]
14902 );
14903 assert_eq!(
14904 editor.background_highlights_in_range(
14905 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
14906 &snapshot,
14907 cx.theme(),
14908 ),
14909 &[(
14910 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14911 Hsla::red(),
14912 )]
14913 );
14914 });
14915}
14916
14917#[gpui::test]
14918async fn test_following(cx: &mut TestAppContext) {
14919 init_test(cx, |_| {});
14920
14921 let fs = FakeFs::new(cx.executor());
14922 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14923
14924 let buffer = project.update(cx, |project, cx| {
14925 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
14926 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
14927 });
14928 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
14929 let follower = cx.update(|cx| {
14930 cx.open_window(
14931 WindowOptions {
14932 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
14933 gpui::Point::new(px(0.), px(0.)),
14934 gpui::Point::new(px(10.), px(80.)),
14935 ))),
14936 ..Default::default()
14937 },
14938 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
14939 )
14940 .unwrap()
14941 });
14942
14943 let is_still_following = Rc::new(RefCell::new(true));
14944 let follower_edit_event_count = Rc::new(RefCell::new(0));
14945 let pending_update = Rc::new(RefCell::new(None));
14946 let leader_entity = leader.root(cx).unwrap();
14947 let follower_entity = follower.root(cx).unwrap();
14948 _ = follower.update(cx, {
14949 let update = pending_update.clone();
14950 let is_still_following = is_still_following.clone();
14951 let follower_edit_event_count = follower_edit_event_count.clone();
14952 |_, window, cx| {
14953 cx.subscribe_in(
14954 &leader_entity,
14955 window,
14956 move |_, leader, event, window, cx| {
14957 leader.read(cx).add_event_to_update_proto(
14958 event,
14959 &mut update.borrow_mut(),
14960 window,
14961 cx,
14962 );
14963 },
14964 )
14965 .detach();
14966
14967 cx.subscribe_in(
14968 &follower_entity,
14969 window,
14970 move |_, _, event: &EditorEvent, _window, _cx| {
14971 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14972 *is_still_following.borrow_mut() = false;
14973 }
14974
14975 if let EditorEvent::BufferEdited = event {
14976 *follower_edit_event_count.borrow_mut() += 1;
14977 }
14978 },
14979 )
14980 .detach();
14981 }
14982 });
14983
14984 // Update the selections only
14985 _ = leader.update(cx, |leader, window, cx| {
14986 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14987 s.select_ranges([1..1])
14988 });
14989 });
14990 follower
14991 .update(cx, |follower, window, cx| {
14992 follower.apply_update_proto(
14993 &project,
14994 pending_update.borrow_mut().take().unwrap(),
14995 window,
14996 cx,
14997 )
14998 })
14999 .unwrap()
15000 .await
15001 .unwrap();
15002 _ = follower.update(cx, |follower, _, cx| {
15003 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
15004 });
15005 assert!(*is_still_following.borrow());
15006 assert_eq!(*follower_edit_event_count.borrow(), 0);
15007
15008 // Update the scroll position only
15009 _ = leader.update(cx, |leader, window, cx| {
15010 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
15011 });
15012 follower
15013 .update(cx, |follower, window, cx| {
15014 follower.apply_update_proto(
15015 &project,
15016 pending_update.borrow_mut().take().unwrap(),
15017 window,
15018 cx,
15019 )
15020 })
15021 .unwrap()
15022 .await
15023 .unwrap();
15024 assert_eq!(
15025 follower
15026 .update(cx, |follower, _, cx| follower.scroll_position(cx))
15027 .unwrap(),
15028 gpui::Point::new(1.5, 3.5)
15029 );
15030 assert!(*is_still_following.borrow());
15031 assert_eq!(*follower_edit_event_count.borrow(), 0);
15032
15033 // Update the selections and scroll position. The follower's scroll position is updated
15034 // via autoscroll, not via the leader's exact scroll position.
15035 _ = leader.update(cx, |leader, window, cx| {
15036 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15037 s.select_ranges([0..0])
15038 });
15039 leader.request_autoscroll(Autoscroll::newest(), cx);
15040 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
15041 });
15042 follower
15043 .update(cx, |follower, window, cx| {
15044 follower.apply_update_proto(
15045 &project,
15046 pending_update.borrow_mut().take().unwrap(),
15047 window,
15048 cx,
15049 )
15050 })
15051 .unwrap()
15052 .await
15053 .unwrap();
15054 _ = follower.update(cx, |follower, _, cx| {
15055 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
15056 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
15057 });
15058 assert!(*is_still_following.borrow());
15059
15060 // Creating a pending selection that precedes another selection
15061 _ = leader.update(cx, |leader, window, cx| {
15062 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15063 s.select_ranges([1..1])
15064 });
15065 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
15066 });
15067 follower
15068 .update(cx, |follower, window, cx| {
15069 follower.apply_update_proto(
15070 &project,
15071 pending_update.borrow_mut().take().unwrap(),
15072 window,
15073 cx,
15074 )
15075 })
15076 .unwrap()
15077 .await
15078 .unwrap();
15079 _ = follower.update(cx, |follower, _, cx| {
15080 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
15081 });
15082 assert!(*is_still_following.borrow());
15083
15084 // Extend the pending selection so that it surrounds another selection
15085 _ = leader.update(cx, |leader, window, cx| {
15086 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
15087 });
15088 follower
15089 .update(cx, |follower, window, cx| {
15090 follower.apply_update_proto(
15091 &project,
15092 pending_update.borrow_mut().take().unwrap(),
15093 window,
15094 cx,
15095 )
15096 })
15097 .unwrap()
15098 .await
15099 .unwrap();
15100 _ = follower.update(cx, |follower, _, cx| {
15101 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
15102 });
15103
15104 // Scrolling locally breaks the follow
15105 _ = follower.update(cx, |follower, window, cx| {
15106 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
15107 follower.set_scroll_anchor(
15108 ScrollAnchor {
15109 anchor: top_anchor,
15110 offset: gpui::Point::new(0.0, 0.5),
15111 },
15112 window,
15113 cx,
15114 );
15115 });
15116 assert!(!(*is_still_following.borrow()));
15117}
15118
15119#[gpui::test]
15120async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
15121 init_test(cx, |_| {});
15122
15123 let fs = FakeFs::new(cx.executor());
15124 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
15125 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15126 let pane = workspace
15127 .update(cx, |workspace, _, _| workspace.active_pane().clone())
15128 .unwrap();
15129
15130 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15131
15132 let leader = pane.update_in(cx, |_, window, cx| {
15133 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
15134 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
15135 });
15136
15137 // Start following the editor when it has no excerpts.
15138 let mut state_message =
15139 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
15140 let workspace_entity = workspace.root(cx).unwrap();
15141 let follower_1 = cx
15142 .update_window(*workspace.deref(), |_, window, cx| {
15143 Editor::from_state_proto(
15144 workspace_entity,
15145 ViewId {
15146 creator: CollaboratorId::PeerId(PeerId::default()),
15147 id: 0,
15148 },
15149 &mut state_message,
15150 window,
15151 cx,
15152 )
15153 })
15154 .unwrap()
15155 .unwrap()
15156 .await
15157 .unwrap();
15158
15159 let update_message = Rc::new(RefCell::new(None));
15160 follower_1.update_in(cx, {
15161 let update = update_message.clone();
15162 |_, window, cx| {
15163 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
15164 leader.read(cx).add_event_to_update_proto(
15165 event,
15166 &mut update.borrow_mut(),
15167 window,
15168 cx,
15169 );
15170 })
15171 .detach();
15172 }
15173 });
15174
15175 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
15176 (
15177 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
15178 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
15179 )
15180 });
15181
15182 // Insert some excerpts.
15183 leader.update(cx, |leader, cx| {
15184 leader.buffer.update(cx, |multibuffer, cx| {
15185 multibuffer.set_excerpts_for_path(
15186 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
15187 buffer_1.clone(),
15188 vec![
15189 Point::row_range(0..3),
15190 Point::row_range(1..6),
15191 Point::row_range(12..15),
15192 ],
15193 0,
15194 cx,
15195 );
15196 multibuffer.set_excerpts_for_path(
15197 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
15198 buffer_2.clone(),
15199 vec![Point::row_range(0..6), Point::row_range(8..12)],
15200 0,
15201 cx,
15202 );
15203 });
15204 });
15205
15206 // Apply the update of adding the excerpts.
15207 follower_1
15208 .update_in(cx, |follower, window, cx| {
15209 follower.apply_update_proto(
15210 &project,
15211 update_message.borrow().clone().unwrap(),
15212 window,
15213 cx,
15214 )
15215 })
15216 .await
15217 .unwrap();
15218 assert_eq!(
15219 follower_1.update(cx, |editor, cx| editor.text(cx)),
15220 leader.update(cx, |editor, cx| editor.text(cx))
15221 );
15222 update_message.borrow_mut().take();
15223
15224 // Start following separately after it already has excerpts.
15225 let mut state_message =
15226 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
15227 let workspace_entity = workspace.root(cx).unwrap();
15228 let follower_2 = cx
15229 .update_window(*workspace.deref(), |_, window, cx| {
15230 Editor::from_state_proto(
15231 workspace_entity,
15232 ViewId {
15233 creator: CollaboratorId::PeerId(PeerId::default()),
15234 id: 0,
15235 },
15236 &mut state_message,
15237 window,
15238 cx,
15239 )
15240 })
15241 .unwrap()
15242 .unwrap()
15243 .await
15244 .unwrap();
15245 assert_eq!(
15246 follower_2.update(cx, |editor, cx| editor.text(cx)),
15247 leader.update(cx, |editor, cx| editor.text(cx))
15248 );
15249
15250 // Remove some excerpts.
15251 leader.update(cx, |leader, cx| {
15252 leader.buffer.update(cx, |multibuffer, cx| {
15253 let excerpt_ids = multibuffer.excerpt_ids();
15254 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
15255 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
15256 });
15257 });
15258
15259 // Apply the update of removing the excerpts.
15260 follower_1
15261 .update_in(cx, |follower, window, cx| {
15262 follower.apply_update_proto(
15263 &project,
15264 update_message.borrow().clone().unwrap(),
15265 window,
15266 cx,
15267 )
15268 })
15269 .await
15270 .unwrap();
15271 follower_2
15272 .update_in(cx, |follower, window, cx| {
15273 follower.apply_update_proto(
15274 &project,
15275 update_message.borrow().clone().unwrap(),
15276 window,
15277 cx,
15278 )
15279 })
15280 .await
15281 .unwrap();
15282 update_message.borrow_mut().take();
15283 assert_eq!(
15284 follower_1.update(cx, |editor, cx| editor.text(cx)),
15285 leader.update(cx, |editor, cx| editor.text(cx))
15286 );
15287}
15288
15289#[gpui::test]
15290async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15291 init_test(cx, |_| {});
15292
15293 let mut cx = EditorTestContext::new(cx).await;
15294 let lsp_store =
15295 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
15296
15297 cx.set_state(indoc! {"
15298 ˇfn func(abc def: i32) -> u32 {
15299 }
15300 "});
15301
15302 cx.update(|_, cx| {
15303 lsp_store.update(cx, |lsp_store, cx| {
15304 lsp_store
15305 .update_diagnostics(
15306 LanguageServerId(0),
15307 lsp::PublishDiagnosticsParams {
15308 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
15309 version: None,
15310 diagnostics: vec![
15311 lsp::Diagnostic {
15312 range: lsp::Range::new(
15313 lsp::Position::new(0, 11),
15314 lsp::Position::new(0, 12),
15315 ),
15316 severity: Some(lsp::DiagnosticSeverity::ERROR),
15317 ..Default::default()
15318 },
15319 lsp::Diagnostic {
15320 range: lsp::Range::new(
15321 lsp::Position::new(0, 12),
15322 lsp::Position::new(0, 15),
15323 ),
15324 severity: Some(lsp::DiagnosticSeverity::ERROR),
15325 ..Default::default()
15326 },
15327 lsp::Diagnostic {
15328 range: lsp::Range::new(
15329 lsp::Position::new(0, 25),
15330 lsp::Position::new(0, 28),
15331 ),
15332 severity: Some(lsp::DiagnosticSeverity::ERROR),
15333 ..Default::default()
15334 },
15335 ],
15336 },
15337 None,
15338 DiagnosticSourceKind::Pushed,
15339 &[],
15340 cx,
15341 )
15342 .unwrap()
15343 });
15344 });
15345
15346 executor.run_until_parked();
15347
15348 cx.update_editor(|editor, window, cx| {
15349 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15350 });
15351
15352 cx.assert_editor_state(indoc! {"
15353 fn func(abc def: i32) -> ˇu32 {
15354 }
15355 "});
15356
15357 cx.update_editor(|editor, window, cx| {
15358 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15359 });
15360
15361 cx.assert_editor_state(indoc! {"
15362 fn func(abc ˇdef: i32) -> u32 {
15363 }
15364 "});
15365
15366 cx.update_editor(|editor, window, cx| {
15367 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15368 });
15369
15370 cx.assert_editor_state(indoc! {"
15371 fn func(abcˇ def: i32) -> u32 {
15372 }
15373 "});
15374
15375 cx.update_editor(|editor, window, cx| {
15376 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15377 });
15378
15379 cx.assert_editor_state(indoc! {"
15380 fn func(abc def: i32) -> ˇu32 {
15381 }
15382 "});
15383}
15384
15385#[gpui::test]
15386async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15387 init_test(cx, |_| {});
15388
15389 let mut cx = EditorTestContext::new(cx).await;
15390
15391 let diff_base = r#"
15392 use some::mod;
15393
15394 const A: u32 = 42;
15395
15396 fn main() {
15397 println!("hello");
15398
15399 println!("world");
15400 }
15401 "#
15402 .unindent();
15403
15404 // Edits are modified, removed, modified, added
15405 cx.set_state(
15406 &r#"
15407 use some::modified;
15408
15409 ˇ
15410 fn main() {
15411 println!("hello there");
15412
15413 println!("around the");
15414 println!("world");
15415 }
15416 "#
15417 .unindent(),
15418 );
15419
15420 cx.set_head_text(&diff_base);
15421 executor.run_until_parked();
15422
15423 cx.update_editor(|editor, window, cx| {
15424 //Wrap around the bottom of the buffer
15425 for _ in 0..3 {
15426 editor.go_to_next_hunk(&GoToHunk, window, cx);
15427 }
15428 });
15429
15430 cx.assert_editor_state(
15431 &r#"
15432 ˇuse some::modified;
15433
15434
15435 fn main() {
15436 println!("hello there");
15437
15438 println!("around the");
15439 println!("world");
15440 }
15441 "#
15442 .unindent(),
15443 );
15444
15445 cx.update_editor(|editor, window, cx| {
15446 //Wrap around the top of the buffer
15447 for _ in 0..2 {
15448 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15449 }
15450 });
15451
15452 cx.assert_editor_state(
15453 &r#"
15454 use some::modified;
15455
15456
15457 fn main() {
15458 ˇ println!("hello there");
15459
15460 println!("around the");
15461 println!("world");
15462 }
15463 "#
15464 .unindent(),
15465 );
15466
15467 cx.update_editor(|editor, window, cx| {
15468 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15469 });
15470
15471 cx.assert_editor_state(
15472 &r#"
15473 use some::modified;
15474
15475 ˇ
15476 fn main() {
15477 println!("hello there");
15478
15479 println!("around the");
15480 println!("world");
15481 }
15482 "#
15483 .unindent(),
15484 );
15485
15486 cx.update_editor(|editor, window, cx| {
15487 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15488 });
15489
15490 cx.assert_editor_state(
15491 &r#"
15492 ˇuse some::modified;
15493
15494
15495 fn main() {
15496 println!("hello there");
15497
15498 println!("around the");
15499 println!("world");
15500 }
15501 "#
15502 .unindent(),
15503 );
15504
15505 cx.update_editor(|editor, window, cx| {
15506 for _ in 0..2 {
15507 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15508 }
15509 });
15510
15511 cx.assert_editor_state(
15512 &r#"
15513 use some::modified;
15514
15515
15516 fn main() {
15517 ˇ println!("hello there");
15518
15519 println!("around the");
15520 println!("world");
15521 }
15522 "#
15523 .unindent(),
15524 );
15525
15526 cx.update_editor(|editor, window, cx| {
15527 editor.fold(&Fold, window, cx);
15528 });
15529
15530 cx.update_editor(|editor, window, cx| {
15531 editor.go_to_next_hunk(&GoToHunk, window, cx);
15532 });
15533
15534 cx.assert_editor_state(
15535 &r#"
15536 ˇuse some::modified;
15537
15538
15539 fn main() {
15540 println!("hello there");
15541
15542 println!("around the");
15543 println!("world");
15544 }
15545 "#
15546 .unindent(),
15547 );
15548}
15549
15550#[test]
15551fn test_split_words() {
15552 fn split(text: &str) -> Vec<&str> {
15553 split_words(text).collect()
15554 }
15555
15556 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
15557 assert_eq!(split("hello_world"), &["hello_", "world"]);
15558 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
15559 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
15560 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
15561 assert_eq!(split("helloworld"), &["helloworld"]);
15562
15563 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
15564}
15565
15566#[gpui::test]
15567async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
15568 init_test(cx, |_| {});
15569
15570 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
15571 let mut assert = |before, after| {
15572 let _state_context = cx.set_state(before);
15573 cx.run_until_parked();
15574 cx.update_editor(|editor, window, cx| {
15575 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
15576 });
15577 cx.run_until_parked();
15578 cx.assert_editor_state(after);
15579 };
15580
15581 // Outside bracket jumps to outside of matching bracket
15582 assert("console.logˇ(var);", "console.log(var)ˇ;");
15583 assert("console.log(var)ˇ;", "console.logˇ(var);");
15584
15585 // Inside bracket jumps to inside of matching bracket
15586 assert("console.log(ˇvar);", "console.log(varˇ);");
15587 assert("console.log(varˇ);", "console.log(ˇvar);");
15588
15589 // When outside a bracket and inside, favor jumping to the inside bracket
15590 assert(
15591 "console.log('foo', [1, 2, 3]ˇ);",
15592 "console.log(ˇ'foo', [1, 2, 3]);",
15593 );
15594 assert(
15595 "console.log(ˇ'foo', [1, 2, 3]);",
15596 "console.log('foo', [1, 2, 3]ˇ);",
15597 );
15598
15599 // Bias forward if two options are equally likely
15600 assert(
15601 "let result = curried_fun()ˇ();",
15602 "let result = curried_fun()()ˇ;",
15603 );
15604
15605 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
15606 assert(
15607 indoc! {"
15608 function test() {
15609 console.log('test')ˇ
15610 }"},
15611 indoc! {"
15612 function test() {
15613 console.logˇ('test')
15614 }"},
15615 );
15616}
15617
15618#[gpui::test]
15619async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
15620 init_test(cx, |_| {});
15621
15622 let fs = FakeFs::new(cx.executor());
15623 fs.insert_tree(
15624 path!("/a"),
15625 json!({
15626 "main.rs": "fn main() { let a = 5; }",
15627 "other.rs": "// Test file",
15628 }),
15629 )
15630 .await;
15631 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15632
15633 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15634 language_registry.add(Arc::new(Language::new(
15635 LanguageConfig {
15636 name: "Rust".into(),
15637 matcher: LanguageMatcher {
15638 path_suffixes: vec!["rs".to_string()],
15639 ..Default::default()
15640 },
15641 brackets: BracketPairConfig {
15642 pairs: vec![BracketPair {
15643 start: "{".to_string(),
15644 end: "}".to_string(),
15645 close: true,
15646 surround: true,
15647 newline: true,
15648 }],
15649 disabled_scopes_by_bracket_ix: Vec::new(),
15650 },
15651 ..Default::default()
15652 },
15653 Some(tree_sitter_rust::LANGUAGE.into()),
15654 )));
15655 let mut fake_servers = language_registry.register_fake_lsp(
15656 "Rust",
15657 FakeLspAdapter {
15658 capabilities: lsp::ServerCapabilities {
15659 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15660 first_trigger_character: "{".to_string(),
15661 more_trigger_character: None,
15662 }),
15663 ..Default::default()
15664 },
15665 ..Default::default()
15666 },
15667 );
15668
15669 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15670
15671 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15672
15673 let worktree_id = workspace
15674 .update(cx, |workspace, _, cx| {
15675 workspace.project().update(cx, |project, cx| {
15676 project.worktrees(cx).next().unwrap().read(cx).id()
15677 })
15678 })
15679 .unwrap();
15680
15681 let buffer = project
15682 .update(cx, |project, cx| {
15683 project.open_local_buffer(path!("/a/main.rs"), cx)
15684 })
15685 .await
15686 .unwrap();
15687 let editor_handle = workspace
15688 .update(cx, |workspace, window, cx| {
15689 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
15690 })
15691 .unwrap()
15692 .await
15693 .unwrap()
15694 .downcast::<Editor>()
15695 .unwrap();
15696
15697 cx.executor().start_waiting();
15698 let fake_server = fake_servers.next().await.unwrap();
15699
15700 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
15701 |params, _| async move {
15702 assert_eq!(
15703 params.text_document_position.text_document.uri,
15704 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
15705 );
15706 assert_eq!(
15707 params.text_document_position.position,
15708 lsp::Position::new(0, 21),
15709 );
15710
15711 Ok(Some(vec![lsp::TextEdit {
15712 new_text: "]".to_string(),
15713 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15714 }]))
15715 },
15716 );
15717
15718 editor_handle.update_in(cx, |editor, window, cx| {
15719 window.focus(&editor.focus_handle(cx));
15720 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15721 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
15722 });
15723 editor.handle_input("{", window, cx);
15724 });
15725
15726 cx.executor().run_until_parked();
15727
15728 buffer.update(cx, |buffer, _| {
15729 assert_eq!(
15730 buffer.text(),
15731 "fn main() { let a = {5}; }",
15732 "No extra braces from on type formatting should appear in the buffer"
15733 )
15734 });
15735}
15736
15737#[gpui::test(iterations = 20, seeds(31))]
15738async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
15739 init_test(cx, |_| {});
15740
15741 let mut cx = EditorLspTestContext::new_rust(
15742 lsp::ServerCapabilities {
15743 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15744 first_trigger_character: ".".to_string(),
15745 more_trigger_character: None,
15746 }),
15747 ..Default::default()
15748 },
15749 cx,
15750 )
15751 .await;
15752
15753 cx.update_buffer(|buffer, _| {
15754 // This causes autoindent to be async.
15755 buffer.set_sync_parse_timeout(Duration::ZERO)
15756 });
15757
15758 cx.set_state("fn c() {\n d()ˇ\n}\n");
15759 cx.simulate_keystroke("\n");
15760 cx.run_until_parked();
15761
15762 let buffer_cloned =
15763 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
15764 let mut request =
15765 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
15766 let buffer_cloned = buffer_cloned.clone();
15767 async move {
15768 buffer_cloned.update(&mut cx, |buffer, _| {
15769 assert_eq!(
15770 buffer.text(),
15771 "fn c() {\n d()\n .\n}\n",
15772 "OnTypeFormatting should triggered after autoindent applied"
15773 )
15774 })?;
15775
15776 Ok(Some(vec![]))
15777 }
15778 });
15779
15780 cx.simulate_keystroke(".");
15781 cx.run_until_parked();
15782
15783 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
15784 assert!(request.next().await.is_some());
15785 request.close();
15786 assert!(request.next().await.is_none());
15787}
15788
15789#[gpui::test]
15790async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
15791 init_test(cx, |_| {});
15792
15793 let fs = FakeFs::new(cx.executor());
15794 fs.insert_tree(
15795 path!("/a"),
15796 json!({
15797 "main.rs": "fn main() { let a = 5; }",
15798 "other.rs": "// Test file",
15799 }),
15800 )
15801 .await;
15802
15803 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15804
15805 let server_restarts = Arc::new(AtomicUsize::new(0));
15806 let closure_restarts = Arc::clone(&server_restarts);
15807 let language_server_name = "test language server";
15808 let language_name: LanguageName = "Rust".into();
15809
15810 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15811 language_registry.add(Arc::new(Language::new(
15812 LanguageConfig {
15813 name: language_name.clone(),
15814 matcher: LanguageMatcher {
15815 path_suffixes: vec!["rs".to_string()],
15816 ..Default::default()
15817 },
15818 ..Default::default()
15819 },
15820 Some(tree_sitter_rust::LANGUAGE.into()),
15821 )));
15822 let mut fake_servers = language_registry.register_fake_lsp(
15823 "Rust",
15824 FakeLspAdapter {
15825 name: language_server_name,
15826 initialization_options: Some(json!({
15827 "testOptionValue": true
15828 })),
15829 initializer: Some(Box::new(move |fake_server| {
15830 let task_restarts = Arc::clone(&closure_restarts);
15831 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
15832 task_restarts.fetch_add(1, atomic::Ordering::Release);
15833 futures::future::ready(Ok(()))
15834 });
15835 })),
15836 ..Default::default()
15837 },
15838 );
15839
15840 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15841 let _buffer = project
15842 .update(cx, |project, cx| {
15843 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
15844 })
15845 .await
15846 .unwrap();
15847 let _fake_server = fake_servers.next().await.unwrap();
15848 update_test_language_settings(cx, |language_settings| {
15849 language_settings.languages.0.insert(
15850 language_name.clone(),
15851 LanguageSettingsContent {
15852 tab_size: NonZeroU32::new(8),
15853 ..Default::default()
15854 },
15855 );
15856 });
15857 cx.executor().run_until_parked();
15858 assert_eq!(
15859 server_restarts.load(atomic::Ordering::Acquire),
15860 0,
15861 "Should not restart LSP server on an unrelated change"
15862 );
15863
15864 update_test_project_settings(cx, |project_settings| {
15865 project_settings.lsp.insert(
15866 "Some other server name".into(),
15867 LspSettings {
15868 binary: None,
15869 settings: None,
15870 initialization_options: Some(json!({
15871 "some other init value": false
15872 })),
15873 enable_lsp_tasks: false,
15874 },
15875 );
15876 });
15877 cx.executor().run_until_parked();
15878 assert_eq!(
15879 server_restarts.load(atomic::Ordering::Acquire),
15880 0,
15881 "Should not restart LSP server on an unrelated LSP settings change"
15882 );
15883
15884 update_test_project_settings(cx, |project_settings| {
15885 project_settings.lsp.insert(
15886 language_server_name.into(),
15887 LspSettings {
15888 binary: None,
15889 settings: None,
15890 initialization_options: Some(json!({
15891 "anotherInitValue": false
15892 })),
15893 enable_lsp_tasks: false,
15894 },
15895 );
15896 });
15897 cx.executor().run_until_parked();
15898 assert_eq!(
15899 server_restarts.load(atomic::Ordering::Acquire),
15900 1,
15901 "Should restart LSP server on a related LSP settings change"
15902 );
15903
15904 update_test_project_settings(cx, |project_settings| {
15905 project_settings.lsp.insert(
15906 language_server_name.into(),
15907 LspSettings {
15908 binary: None,
15909 settings: None,
15910 initialization_options: Some(json!({
15911 "anotherInitValue": false
15912 })),
15913 enable_lsp_tasks: false,
15914 },
15915 );
15916 });
15917 cx.executor().run_until_parked();
15918 assert_eq!(
15919 server_restarts.load(atomic::Ordering::Acquire),
15920 1,
15921 "Should not restart LSP server on a related LSP settings change that is the same"
15922 );
15923
15924 update_test_project_settings(cx, |project_settings| {
15925 project_settings.lsp.insert(
15926 language_server_name.into(),
15927 LspSettings {
15928 binary: None,
15929 settings: None,
15930 initialization_options: None,
15931 enable_lsp_tasks: false,
15932 },
15933 );
15934 });
15935 cx.executor().run_until_parked();
15936 assert_eq!(
15937 server_restarts.load(atomic::Ordering::Acquire),
15938 2,
15939 "Should restart LSP server on another related LSP settings change"
15940 );
15941}
15942
15943#[gpui::test]
15944async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15945 init_test(cx, |_| {});
15946
15947 let mut cx = EditorLspTestContext::new_rust(
15948 lsp::ServerCapabilities {
15949 completion_provider: Some(lsp::CompletionOptions {
15950 trigger_characters: Some(vec![".".to_string()]),
15951 resolve_provider: Some(true),
15952 ..Default::default()
15953 }),
15954 ..Default::default()
15955 },
15956 cx,
15957 )
15958 .await;
15959
15960 cx.set_state("fn main() { let a = 2ˇ; }");
15961 cx.simulate_keystroke(".");
15962 let completion_item = lsp::CompletionItem {
15963 label: "some".into(),
15964 kind: Some(lsp::CompletionItemKind::SNIPPET),
15965 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15966 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15967 kind: lsp::MarkupKind::Markdown,
15968 value: "```rust\nSome(2)\n```".to_string(),
15969 })),
15970 deprecated: Some(false),
15971 sort_text: Some("fffffff2".to_string()),
15972 filter_text: Some("some".to_string()),
15973 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15974 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15975 range: lsp::Range {
15976 start: lsp::Position {
15977 line: 0,
15978 character: 22,
15979 },
15980 end: lsp::Position {
15981 line: 0,
15982 character: 22,
15983 },
15984 },
15985 new_text: "Some(2)".to_string(),
15986 })),
15987 additional_text_edits: Some(vec![lsp::TextEdit {
15988 range: lsp::Range {
15989 start: lsp::Position {
15990 line: 0,
15991 character: 20,
15992 },
15993 end: lsp::Position {
15994 line: 0,
15995 character: 22,
15996 },
15997 },
15998 new_text: "".to_string(),
15999 }]),
16000 ..Default::default()
16001 };
16002
16003 let closure_completion_item = completion_item.clone();
16004 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16005 let task_completion_item = closure_completion_item.clone();
16006 async move {
16007 Ok(Some(lsp::CompletionResponse::Array(vec![
16008 task_completion_item,
16009 ])))
16010 }
16011 });
16012
16013 request.next().await;
16014
16015 cx.condition(|editor, _| editor.context_menu_visible())
16016 .await;
16017 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16018 editor
16019 .confirm_completion(&ConfirmCompletion::default(), window, cx)
16020 .unwrap()
16021 });
16022 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
16023
16024 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
16025 let task_completion_item = completion_item.clone();
16026 async move { Ok(task_completion_item) }
16027 })
16028 .next()
16029 .await
16030 .unwrap();
16031 apply_additional_edits.await.unwrap();
16032 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
16033}
16034
16035#[gpui::test]
16036async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
16037 init_test(cx, |_| {});
16038
16039 let mut cx = EditorLspTestContext::new_rust(
16040 lsp::ServerCapabilities {
16041 completion_provider: Some(lsp::CompletionOptions {
16042 trigger_characters: Some(vec![".".to_string()]),
16043 resolve_provider: Some(true),
16044 ..Default::default()
16045 }),
16046 ..Default::default()
16047 },
16048 cx,
16049 )
16050 .await;
16051
16052 cx.set_state("fn main() { let a = 2ˇ; }");
16053 cx.simulate_keystroke(".");
16054
16055 let item1 = lsp::CompletionItem {
16056 label: "method id()".to_string(),
16057 filter_text: Some("id".to_string()),
16058 detail: None,
16059 documentation: None,
16060 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16061 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16062 new_text: ".id".to_string(),
16063 })),
16064 ..lsp::CompletionItem::default()
16065 };
16066
16067 let item2 = lsp::CompletionItem {
16068 label: "other".to_string(),
16069 filter_text: Some("other".to_string()),
16070 detail: None,
16071 documentation: None,
16072 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16073 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16074 new_text: ".other".to_string(),
16075 })),
16076 ..lsp::CompletionItem::default()
16077 };
16078
16079 let item1 = item1.clone();
16080 cx.set_request_handler::<lsp::request::Completion, _, _>({
16081 let item1 = item1.clone();
16082 move |_, _, _| {
16083 let item1 = item1.clone();
16084 let item2 = item2.clone();
16085 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
16086 }
16087 })
16088 .next()
16089 .await;
16090
16091 cx.condition(|editor, _| editor.context_menu_visible())
16092 .await;
16093 cx.update_editor(|editor, _, _| {
16094 let context_menu = editor.context_menu.borrow_mut();
16095 let context_menu = context_menu
16096 .as_ref()
16097 .expect("Should have the context menu deployed");
16098 match context_menu {
16099 CodeContextMenu::Completions(completions_menu) => {
16100 let completions = completions_menu.completions.borrow_mut();
16101 assert_eq!(
16102 completions
16103 .iter()
16104 .map(|completion| &completion.label.text)
16105 .collect::<Vec<_>>(),
16106 vec!["method id()", "other"]
16107 )
16108 }
16109 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
16110 }
16111 });
16112
16113 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
16114 let item1 = item1.clone();
16115 move |_, item_to_resolve, _| {
16116 let item1 = item1.clone();
16117 async move {
16118 if item1 == item_to_resolve {
16119 Ok(lsp::CompletionItem {
16120 label: "method id()".to_string(),
16121 filter_text: Some("id".to_string()),
16122 detail: Some("Now resolved!".to_string()),
16123 documentation: Some(lsp::Documentation::String("Docs".to_string())),
16124 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16125 range: lsp::Range::new(
16126 lsp::Position::new(0, 22),
16127 lsp::Position::new(0, 22),
16128 ),
16129 new_text: ".id".to_string(),
16130 })),
16131 ..lsp::CompletionItem::default()
16132 })
16133 } else {
16134 Ok(item_to_resolve)
16135 }
16136 }
16137 }
16138 })
16139 .next()
16140 .await
16141 .unwrap();
16142 cx.run_until_parked();
16143
16144 cx.update_editor(|editor, window, cx| {
16145 editor.context_menu_next(&Default::default(), window, cx);
16146 });
16147
16148 cx.update_editor(|editor, _, _| {
16149 let context_menu = editor.context_menu.borrow_mut();
16150 let context_menu = context_menu
16151 .as_ref()
16152 .expect("Should have the context menu deployed");
16153 match context_menu {
16154 CodeContextMenu::Completions(completions_menu) => {
16155 let completions = completions_menu.completions.borrow_mut();
16156 assert_eq!(
16157 completions
16158 .iter()
16159 .map(|completion| &completion.label.text)
16160 .collect::<Vec<_>>(),
16161 vec!["method id() Now resolved!", "other"],
16162 "Should update first completion label, but not second as the filter text did not match."
16163 );
16164 }
16165 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
16166 }
16167 });
16168}
16169
16170#[gpui::test]
16171async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
16172 init_test(cx, |_| {});
16173 let mut cx = EditorLspTestContext::new_rust(
16174 lsp::ServerCapabilities {
16175 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
16176 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
16177 completion_provider: Some(lsp::CompletionOptions {
16178 resolve_provider: Some(true),
16179 ..Default::default()
16180 }),
16181 ..Default::default()
16182 },
16183 cx,
16184 )
16185 .await;
16186 cx.set_state(indoc! {"
16187 struct TestStruct {
16188 field: i32
16189 }
16190
16191 fn mainˇ() {
16192 let unused_var = 42;
16193 let test_struct = TestStruct { field: 42 };
16194 }
16195 "});
16196 let symbol_range = cx.lsp_range(indoc! {"
16197 struct TestStruct {
16198 field: i32
16199 }
16200
16201 «fn main»() {
16202 let unused_var = 42;
16203 let test_struct = TestStruct { field: 42 };
16204 }
16205 "});
16206 let mut hover_requests =
16207 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
16208 Ok(Some(lsp::Hover {
16209 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
16210 kind: lsp::MarkupKind::Markdown,
16211 value: "Function documentation".to_string(),
16212 }),
16213 range: Some(symbol_range),
16214 }))
16215 });
16216
16217 // Case 1: Test that code action menu hide hover popover
16218 cx.dispatch_action(Hover);
16219 hover_requests.next().await;
16220 cx.condition(|editor, _| editor.hover_state.visible()).await;
16221 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
16222 move |_, _, _| async move {
16223 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
16224 lsp::CodeAction {
16225 title: "Remove unused variable".to_string(),
16226 kind: Some(CodeActionKind::QUICKFIX),
16227 edit: Some(lsp::WorkspaceEdit {
16228 changes: Some(
16229 [(
16230 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
16231 vec![lsp::TextEdit {
16232 range: lsp::Range::new(
16233 lsp::Position::new(5, 4),
16234 lsp::Position::new(5, 27),
16235 ),
16236 new_text: "".to_string(),
16237 }],
16238 )]
16239 .into_iter()
16240 .collect(),
16241 ),
16242 ..Default::default()
16243 }),
16244 ..Default::default()
16245 },
16246 )]))
16247 },
16248 );
16249 cx.update_editor(|editor, window, cx| {
16250 editor.toggle_code_actions(
16251 &ToggleCodeActions {
16252 deployed_from: None,
16253 quick_launch: false,
16254 },
16255 window,
16256 cx,
16257 );
16258 });
16259 code_action_requests.next().await;
16260 cx.run_until_parked();
16261 cx.condition(|editor, _| editor.context_menu_visible())
16262 .await;
16263 cx.update_editor(|editor, _, _| {
16264 assert!(
16265 !editor.hover_state.visible(),
16266 "Hover popover should be hidden when code action menu is shown"
16267 );
16268 // Hide code actions
16269 editor.context_menu.take();
16270 });
16271
16272 // Case 2: Test that code completions hide hover popover
16273 cx.dispatch_action(Hover);
16274 hover_requests.next().await;
16275 cx.condition(|editor, _| editor.hover_state.visible()).await;
16276 let counter = Arc::new(AtomicUsize::new(0));
16277 let mut completion_requests =
16278 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16279 let counter = counter.clone();
16280 async move {
16281 counter.fetch_add(1, atomic::Ordering::Release);
16282 Ok(Some(lsp::CompletionResponse::Array(vec![
16283 lsp::CompletionItem {
16284 label: "main".into(),
16285 kind: Some(lsp::CompletionItemKind::FUNCTION),
16286 detail: Some("() -> ()".to_string()),
16287 ..Default::default()
16288 },
16289 lsp::CompletionItem {
16290 label: "TestStruct".into(),
16291 kind: Some(lsp::CompletionItemKind::STRUCT),
16292 detail: Some("struct TestStruct".to_string()),
16293 ..Default::default()
16294 },
16295 ])))
16296 }
16297 });
16298 cx.update_editor(|editor, window, cx| {
16299 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
16300 });
16301 completion_requests.next().await;
16302 cx.condition(|editor, _| editor.context_menu_visible())
16303 .await;
16304 cx.update_editor(|editor, _, _| {
16305 assert!(
16306 !editor.hover_state.visible(),
16307 "Hover popover should be hidden when completion menu is shown"
16308 );
16309 });
16310}
16311
16312#[gpui::test]
16313async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
16314 init_test(cx, |_| {});
16315
16316 let mut cx = EditorLspTestContext::new_rust(
16317 lsp::ServerCapabilities {
16318 completion_provider: Some(lsp::CompletionOptions {
16319 trigger_characters: Some(vec![".".to_string()]),
16320 resolve_provider: Some(true),
16321 ..Default::default()
16322 }),
16323 ..Default::default()
16324 },
16325 cx,
16326 )
16327 .await;
16328
16329 cx.set_state("fn main() { let a = 2ˇ; }");
16330 cx.simulate_keystroke(".");
16331
16332 let unresolved_item_1 = lsp::CompletionItem {
16333 label: "id".to_string(),
16334 filter_text: Some("id".to_string()),
16335 detail: None,
16336 documentation: None,
16337 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16338 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16339 new_text: ".id".to_string(),
16340 })),
16341 ..lsp::CompletionItem::default()
16342 };
16343 let resolved_item_1 = lsp::CompletionItem {
16344 additional_text_edits: Some(vec![lsp::TextEdit {
16345 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
16346 new_text: "!!".to_string(),
16347 }]),
16348 ..unresolved_item_1.clone()
16349 };
16350 let unresolved_item_2 = lsp::CompletionItem {
16351 label: "other".to_string(),
16352 filter_text: Some("other".to_string()),
16353 detail: None,
16354 documentation: None,
16355 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16356 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16357 new_text: ".other".to_string(),
16358 })),
16359 ..lsp::CompletionItem::default()
16360 };
16361 let resolved_item_2 = lsp::CompletionItem {
16362 additional_text_edits: Some(vec![lsp::TextEdit {
16363 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
16364 new_text: "??".to_string(),
16365 }]),
16366 ..unresolved_item_2.clone()
16367 };
16368
16369 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
16370 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
16371 cx.lsp
16372 .server
16373 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16374 let unresolved_item_1 = unresolved_item_1.clone();
16375 let resolved_item_1 = resolved_item_1.clone();
16376 let unresolved_item_2 = unresolved_item_2.clone();
16377 let resolved_item_2 = resolved_item_2.clone();
16378 let resolve_requests_1 = resolve_requests_1.clone();
16379 let resolve_requests_2 = resolve_requests_2.clone();
16380 move |unresolved_request, _| {
16381 let unresolved_item_1 = unresolved_item_1.clone();
16382 let resolved_item_1 = resolved_item_1.clone();
16383 let unresolved_item_2 = unresolved_item_2.clone();
16384 let resolved_item_2 = resolved_item_2.clone();
16385 let resolve_requests_1 = resolve_requests_1.clone();
16386 let resolve_requests_2 = resolve_requests_2.clone();
16387 async move {
16388 if unresolved_request == unresolved_item_1 {
16389 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
16390 Ok(resolved_item_1.clone())
16391 } else if unresolved_request == unresolved_item_2 {
16392 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
16393 Ok(resolved_item_2.clone())
16394 } else {
16395 panic!("Unexpected completion item {unresolved_request:?}")
16396 }
16397 }
16398 }
16399 })
16400 .detach();
16401
16402 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16403 let unresolved_item_1 = unresolved_item_1.clone();
16404 let unresolved_item_2 = unresolved_item_2.clone();
16405 async move {
16406 Ok(Some(lsp::CompletionResponse::Array(vec![
16407 unresolved_item_1,
16408 unresolved_item_2,
16409 ])))
16410 }
16411 })
16412 .next()
16413 .await;
16414
16415 cx.condition(|editor, _| editor.context_menu_visible())
16416 .await;
16417 cx.update_editor(|editor, _, _| {
16418 let context_menu = editor.context_menu.borrow_mut();
16419 let context_menu = context_menu
16420 .as_ref()
16421 .expect("Should have the context menu deployed");
16422 match context_menu {
16423 CodeContextMenu::Completions(completions_menu) => {
16424 let completions = completions_menu.completions.borrow_mut();
16425 assert_eq!(
16426 completions
16427 .iter()
16428 .map(|completion| &completion.label.text)
16429 .collect::<Vec<_>>(),
16430 vec!["id", "other"]
16431 )
16432 }
16433 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
16434 }
16435 });
16436 cx.run_until_parked();
16437
16438 cx.update_editor(|editor, window, cx| {
16439 editor.context_menu_next(&ContextMenuNext, window, cx);
16440 });
16441 cx.run_until_parked();
16442 cx.update_editor(|editor, window, cx| {
16443 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16444 });
16445 cx.run_until_parked();
16446 cx.update_editor(|editor, window, cx| {
16447 editor.context_menu_next(&ContextMenuNext, window, cx);
16448 });
16449 cx.run_until_parked();
16450 cx.update_editor(|editor, window, cx| {
16451 editor
16452 .compose_completion(&ComposeCompletion::default(), window, cx)
16453 .expect("No task returned")
16454 })
16455 .await
16456 .expect("Completion failed");
16457 cx.run_until_parked();
16458
16459 cx.update_editor(|editor, _, cx| {
16460 assert_eq!(
16461 resolve_requests_1.load(atomic::Ordering::Acquire),
16462 1,
16463 "Should always resolve once despite multiple selections"
16464 );
16465 assert_eq!(
16466 resolve_requests_2.load(atomic::Ordering::Acquire),
16467 1,
16468 "Should always resolve once after multiple selections and applying the completion"
16469 );
16470 assert_eq!(
16471 editor.text(cx),
16472 "fn main() { let a = ??.other; }",
16473 "Should use resolved data when applying the completion"
16474 );
16475 });
16476}
16477
16478#[gpui::test]
16479async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
16480 init_test(cx, |_| {});
16481
16482 let item_0 = lsp::CompletionItem {
16483 label: "abs".into(),
16484 insert_text: Some("abs".into()),
16485 data: Some(json!({ "very": "special"})),
16486 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
16487 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
16488 lsp::InsertReplaceEdit {
16489 new_text: "abs".to_string(),
16490 insert: lsp::Range::default(),
16491 replace: lsp::Range::default(),
16492 },
16493 )),
16494 ..lsp::CompletionItem::default()
16495 };
16496 let items = iter::once(item_0.clone())
16497 .chain((11..51).map(|i| lsp::CompletionItem {
16498 label: format!("item_{}", i),
16499 insert_text: Some(format!("item_{}", i)),
16500 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16501 ..lsp::CompletionItem::default()
16502 }))
16503 .collect::<Vec<_>>();
16504
16505 let default_commit_characters = vec!["?".to_string()];
16506 let default_data = json!({ "default": "data"});
16507 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
16508 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
16509 let default_edit_range = lsp::Range {
16510 start: lsp::Position {
16511 line: 0,
16512 character: 5,
16513 },
16514 end: lsp::Position {
16515 line: 0,
16516 character: 5,
16517 },
16518 };
16519
16520 let mut cx = EditorLspTestContext::new_rust(
16521 lsp::ServerCapabilities {
16522 completion_provider: Some(lsp::CompletionOptions {
16523 trigger_characters: Some(vec![".".to_string()]),
16524 resolve_provider: Some(true),
16525 ..Default::default()
16526 }),
16527 ..Default::default()
16528 },
16529 cx,
16530 )
16531 .await;
16532
16533 cx.set_state("fn main() { let a = 2ˇ; }");
16534 cx.simulate_keystroke(".");
16535
16536 let completion_data = default_data.clone();
16537 let completion_characters = default_commit_characters.clone();
16538 let completion_items = items.clone();
16539 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16540 let default_data = completion_data.clone();
16541 let default_commit_characters = completion_characters.clone();
16542 let items = completion_items.clone();
16543 async move {
16544 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16545 items,
16546 item_defaults: Some(lsp::CompletionListItemDefaults {
16547 data: Some(default_data.clone()),
16548 commit_characters: Some(default_commit_characters.clone()),
16549 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
16550 default_edit_range,
16551 )),
16552 insert_text_format: Some(default_insert_text_format),
16553 insert_text_mode: Some(default_insert_text_mode),
16554 }),
16555 ..lsp::CompletionList::default()
16556 })))
16557 }
16558 })
16559 .next()
16560 .await;
16561
16562 let resolved_items = Arc::new(Mutex::new(Vec::new()));
16563 cx.lsp
16564 .server
16565 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16566 let closure_resolved_items = resolved_items.clone();
16567 move |item_to_resolve, _| {
16568 let closure_resolved_items = closure_resolved_items.clone();
16569 async move {
16570 closure_resolved_items.lock().push(item_to_resolve.clone());
16571 Ok(item_to_resolve)
16572 }
16573 }
16574 })
16575 .detach();
16576
16577 cx.condition(|editor, _| editor.context_menu_visible())
16578 .await;
16579 cx.run_until_parked();
16580 cx.update_editor(|editor, _, _| {
16581 let menu = editor.context_menu.borrow_mut();
16582 match menu.as_ref().expect("should have the completions menu") {
16583 CodeContextMenu::Completions(completions_menu) => {
16584 assert_eq!(
16585 completions_menu
16586 .entries
16587 .borrow()
16588 .iter()
16589 .map(|mat| mat.string.clone())
16590 .collect::<Vec<String>>(),
16591 items
16592 .iter()
16593 .map(|completion| completion.label.clone())
16594 .collect::<Vec<String>>()
16595 );
16596 }
16597 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
16598 }
16599 });
16600 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
16601 // with 4 from the end.
16602 assert_eq!(
16603 *resolved_items.lock(),
16604 [&items[0..16], &items[items.len() - 4..items.len()]]
16605 .concat()
16606 .iter()
16607 .cloned()
16608 .map(|mut item| {
16609 if item.data.is_none() {
16610 item.data = Some(default_data.clone());
16611 }
16612 item
16613 })
16614 .collect::<Vec<lsp::CompletionItem>>(),
16615 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
16616 );
16617 resolved_items.lock().clear();
16618
16619 cx.update_editor(|editor, window, cx| {
16620 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16621 });
16622 cx.run_until_parked();
16623 // Completions that have already been resolved are skipped.
16624 assert_eq!(
16625 *resolved_items.lock(),
16626 items[items.len() - 17..items.len() - 4]
16627 .iter()
16628 .cloned()
16629 .map(|mut item| {
16630 if item.data.is_none() {
16631 item.data = Some(default_data.clone());
16632 }
16633 item
16634 })
16635 .collect::<Vec<lsp::CompletionItem>>()
16636 );
16637 resolved_items.lock().clear();
16638}
16639
16640#[gpui::test]
16641async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
16642 init_test(cx, |_| {});
16643
16644 let mut cx = EditorLspTestContext::new(
16645 Language::new(
16646 LanguageConfig {
16647 matcher: LanguageMatcher {
16648 path_suffixes: vec!["jsx".into()],
16649 ..Default::default()
16650 },
16651 overrides: [(
16652 "element".into(),
16653 LanguageConfigOverride {
16654 completion_query_characters: Override::Set(['-'].into_iter().collect()),
16655 ..Default::default()
16656 },
16657 )]
16658 .into_iter()
16659 .collect(),
16660 ..Default::default()
16661 },
16662 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16663 )
16664 .with_override_query("(jsx_self_closing_element) @element")
16665 .unwrap(),
16666 lsp::ServerCapabilities {
16667 completion_provider: Some(lsp::CompletionOptions {
16668 trigger_characters: Some(vec![":".to_string()]),
16669 ..Default::default()
16670 }),
16671 ..Default::default()
16672 },
16673 cx,
16674 )
16675 .await;
16676
16677 cx.lsp
16678 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16679 Ok(Some(lsp::CompletionResponse::Array(vec![
16680 lsp::CompletionItem {
16681 label: "bg-blue".into(),
16682 ..Default::default()
16683 },
16684 lsp::CompletionItem {
16685 label: "bg-red".into(),
16686 ..Default::default()
16687 },
16688 lsp::CompletionItem {
16689 label: "bg-yellow".into(),
16690 ..Default::default()
16691 },
16692 ])))
16693 });
16694
16695 cx.set_state(r#"<p class="bgˇ" />"#);
16696
16697 // Trigger completion when typing a dash, because the dash is an extra
16698 // word character in the 'element' scope, which contains the cursor.
16699 cx.simulate_keystroke("-");
16700 cx.executor().run_until_parked();
16701 cx.update_editor(|editor, _, _| {
16702 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16703 {
16704 assert_eq!(
16705 completion_menu_entries(&menu),
16706 &["bg-blue", "bg-red", "bg-yellow"]
16707 );
16708 } else {
16709 panic!("expected completion menu to be open");
16710 }
16711 });
16712
16713 cx.simulate_keystroke("l");
16714 cx.executor().run_until_parked();
16715 cx.update_editor(|editor, _, _| {
16716 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16717 {
16718 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
16719 } else {
16720 panic!("expected completion menu to be open");
16721 }
16722 });
16723
16724 // When filtering completions, consider the character after the '-' to
16725 // be the start of a subword.
16726 cx.set_state(r#"<p class="yelˇ" />"#);
16727 cx.simulate_keystroke("l");
16728 cx.executor().run_until_parked();
16729 cx.update_editor(|editor, _, _| {
16730 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16731 {
16732 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
16733 } else {
16734 panic!("expected completion menu to be open");
16735 }
16736 });
16737}
16738
16739fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
16740 let entries = menu.entries.borrow();
16741 entries.iter().map(|mat| mat.string.clone()).collect()
16742}
16743
16744#[gpui::test]
16745async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
16746 init_test(cx, |settings| {
16747 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
16748 Formatter::Prettier,
16749 )))
16750 });
16751
16752 let fs = FakeFs::new(cx.executor());
16753 fs.insert_file(path!("/file.ts"), Default::default()).await;
16754
16755 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
16756 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16757
16758 language_registry.add(Arc::new(Language::new(
16759 LanguageConfig {
16760 name: "TypeScript".into(),
16761 matcher: LanguageMatcher {
16762 path_suffixes: vec!["ts".to_string()],
16763 ..Default::default()
16764 },
16765 ..Default::default()
16766 },
16767 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16768 )));
16769 update_test_language_settings(cx, |settings| {
16770 settings.defaults.prettier = Some(PrettierSettings {
16771 allowed: true,
16772 ..PrettierSettings::default()
16773 });
16774 });
16775
16776 let test_plugin = "test_plugin";
16777 let _ = language_registry.register_fake_lsp(
16778 "TypeScript",
16779 FakeLspAdapter {
16780 prettier_plugins: vec![test_plugin],
16781 ..Default::default()
16782 },
16783 );
16784
16785 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
16786 let buffer = project
16787 .update(cx, |project, cx| {
16788 project.open_local_buffer(path!("/file.ts"), cx)
16789 })
16790 .await
16791 .unwrap();
16792
16793 let buffer_text = "one\ntwo\nthree\n";
16794 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16795 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16796 editor.update_in(cx, |editor, window, cx| {
16797 editor.set_text(buffer_text, window, cx)
16798 });
16799
16800 editor
16801 .update_in(cx, |editor, window, cx| {
16802 editor.perform_format(
16803 project.clone(),
16804 FormatTrigger::Manual,
16805 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16806 window,
16807 cx,
16808 )
16809 })
16810 .unwrap()
16811 .await;
16812 assert_eq!(
16813 editor.update(cx, |editor, cx| editor.text(cx)),
16814 buffer_text.to_string() + prettier_format_suffix,
16815 "Test prettier formatting was not applied to the original buffer text",
16816 );
16817
16818 update_test_language_settings(cx, |settings| {
16819 settings.defaults.formatter = Some(SelectedFormatter::Auto)
16820 });
16821 let format = editor.update_in(cx, |editor, window, cx| {
16822 editor.perform_format(
16823 project.clone(),
16824 FormatTrigger::Manual,
16825 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16826 window,
16827 cx,
16828 )
16829 });
16830 format.await.unwrap();
16831 assert_eq!(
16832 editor.update(cx, |editor, cx| editor.text(cx)),
16833 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
16834 "Autoformatting (via test prettier) was not applied to the original buffer text",
16835 );
16836}
16837
16838#[gpui::test]
16839async fn test_addition_reverts(cx: &mut TestAppContext) {
16840 init_test(cx, |_| {});
16841 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16842 let base_text = indoc! {r#"
16843 struct Row;
16844 struct Row1;
16845 struct Row2;
16846
16847 struct Row4;
16848 struct Row5;
16849 struct Row6;
16850
16851 struct Row8;
16852 struct Row9;
16853 struct Row10;"#};
16854
16855 // When addition hunks are not adjacent to carets, no hunk revert is performed
16856 assert_hunk_revert(
16857 indoc! {r#"struct Row;
16858 struct Row1;
16859 struct Row1.1;
16860 struct Row1.2;
16861 struct Row2;ˇ
16862
16863 struct Row4;
16864 struct Row5;
16865 struct Row6;
16866
16867 struct Row8;
16868 ˇstruct Row9;
16869 struct Row9.1;
16870 struct Row9.2;
16871 struct Row9.3;
16872 struct Row10;"#},
16873 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16874 indoc! {r#"struct Row;
16875 struct Row1;
16876 struct Row1.1;
16877 struct Row1.2;
16878 struct Row2;ˇ
16879
16880 struct Row4;
16881 struct Row5;
16882 struct Row6;
16883
16884 struct Row8;
16885 ˇstruct Row9;
16886 struct Row9.1;
16887 struct Row9.2;
16888 struct Row9.3;
16889 struct Row10;"#},
16890 base_text,
16891 &mut cx,
16892 );
16893 // Same for selections
16894 assert_hunk_revert(
16895 indoc! {r#"struct Row;
16896 struct Row1;
16897 struct Row2;
16898 struct Row2.1;
16899 struct Row2.2;
16900 «ˇ
16901 struct Row4;
16902 struct» Row5;
16903 «struct Row6;
16904 ˇ»
16905 struct Row9.1;
16906 struct Row9.2;
16907 struct Row9.3;
16908 struct Row8;
16909 struct Row9;
16910 struct Row10;"#},
16911 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16912 indoc! {r#"struct Row;
16913 struct Row1;
16914 struct Row2;
16915 struct Row2.1;
16916 struct Row2.2;
16917 «ˇ
16918 struct Row4;
16919 struct» Row5;
16920 «struct Row6;
16921 ˇ»
16922 struct Row9.1;
16923 struct Row9.2;
16924 struct Row9.3;
16925 struct Row8;
16926 struct Row9;
16927 struct Row10;"#},
16928 base_text,
16929 &mut cx,
16930 );
16931
16932 // When carets and selections intersect the addition hunks, those are reverted.
16933 // Adjacent carets got merged.
16934 assert_hunk_revert(
16935 indoc! {r#"struct Row;
16936 ˇ// something on the top
16937 struct Row1;
16938 struct Row2;
16939 struct Roˇw3.1;
16940 struct Row2.2;
16941 struct Row2.3;ˇ
16942
16943 struct Row4;
16944 struct ˇRow5.1;
16945 struct Row5.2;
16946 struct «Rowˇ»5.3;
16947 struct Row5;
16948 struct Row6;
16949 ˇ
16950 struct Row9.1;
16951 struct «Rowˇ»9.2;
16952 struct «ˇRow»9.3;
16953 struct Row8;
16954 struct Row9;
16955 «ˇ// something on bottom»
16956 struct Row10;"#},
16957 vec![
16958 DiffHunkStatusKind::Added,
16959 DiffHunkStatusKind::Added,
16960 DiffHunkStatusKind::Added,
16961 DiffHunkStatusKind::Added,
16962 DiffHunkStatusKind::Added,
16963 ],
16964 indoc! {r#"struct Row;
16965 ˇstruct Row1;
16966 struct Row2;
16967 ˇ
16968 struct Row4;
16969 ˇstruct Row5;
16970 struct Row6;
16971 ˇ
16972 ˇstruct Row8;
16973 struct Row9;
16974 ˇstruct Row10;"#},
16975 base_text,
16976 &mut cx,
16977 );
16978}
16979
16980#[gpui::test]
16981async fn test_modification_reverts(cx: &mut TestAppContext) {
16982 init_test(cx, |_| {});
16983 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16984 let base_text = indoc! {r#"
16985 struct Row;
16986 struct Row1;
16987 struct Row2;
16988
16989 struct Row4;
16990 struct Row5;
16991 struct Row6;
16992
16993 struct Row8;
16994 struct Row9;
16995 struct Row10;"#};
16996
16997 // Modification hunks behave the same as the addition ones.
16998 assert_hunk_revert(
16999 indoc! {r#"struct Row;
17000 struct Row1;
17001 struct Row33;
17002 ˇ
17003 struct Row4;
17004 struct Row5;
17005 struct Row6;
17006 ˇ
17007 struct Row99;
17008 struct Row9;
17009 struct Row10;"#},
17010 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
17011 indoc! {r#"struct Row;
17012 struct Row1;
17013 struct Row33;
17014 ˇ
17015 struct Row4;
17016 struct Row5;
17017 struct Row6;
17018 ˇ
17019 struct Row99;
17020 struct Row9;
17021 struct Row10;"#},
17022 base_text,
17023 &mut cx,
17024 );
17025 assert_hunk_revert(
17026 indoc! {r#"struct Row;
17027 struct Row1;
17028 struct Row33;
17029 «ˇ
17030 struct Row4;
17031 struct» Row5;
17032 «struct Row6;
17033 ˇ»
17034 struct Row99;
17035 struct Row9;
17036 struct Row10;"#},
17037 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
17038 indoc! {r#"struct Row;
17039 struct Row1;
17040 struct Row33;
17041 «ˇ
17042 struct Row4;
17043 struct» Row5;
17044 «struct Row6;
17045 ˇ»
17046 struct Row99;
17047 struct Row9;
17048 struct Row10;"#},
17049 base_text,
17050 &mut cx,
17051 );
17052
17053 assert_hunk_revert(
17054 indoc! {r#"ˇstruct Row1.1;
17055 struct Row1;
17056 «ˇstr»uct Row22;
17057
17058 struct ˇRow44;
17059 struct Row5;
17060 struct «Rˇ»ow66;ˇ
17061
17062 «struˇ»ct Row88;
17063 struct Row9;
17064 struct Row1011;ˇ"#},
17065 vec![
17066 DiffHunkStatusKind::Modified,
17067 DiffHunkStatusKind::Modified,
17068 DiffHunkStatusKind::Modified,
17069 DiffHunkStatusKind::Modified,
17070 DiffHunkStatusKind::Modified,
17071 DiffHunkStatusKind::Modified,
17072 ],
17073 indoc! {r#"struct Row;
17074 ˇstruct Row1;
17075 struct Row2;
17076 ˇ
17077 struct Row4;
17078 ˇstruct Row5;
17079 struct Row6;
17080 ˇ
17081 struct Row8;
17082 ˇstruct Row9;
17083 struct Row10;ˇ"#},
17084 base_text,
17085 &mut cx,
17086 );
17087}
17088
17089#[gpui::test]
17090async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
17091 init_test(cx, |_| {});
17092 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17093 let base_text = indoc! {r#"
17094 one
17095
17096 two
17097 three
17098 "#};
17099
17100 cx.set_head_text(base_text);
17101 cx.set_state("\nˇ\n");
17102 cx.executor().run_until_parked();
17103 cx.update_editor(|editor, _window, cx| {
17104 editor.expand_selected_diff_hunks(cx);
17105 });
17106 cx.executor().run_until_parked();
17107 cx.update_editor(|editor, window, cx| {
17108 editor.backspace(&Default::default(), window, cx);
17109 });
17110 cx.run_until_parked();
17111 cx.assert_state_with_diff(
17112 indoc! {r#"
17113
17114 - two
17115 - threeˇ
17116 +
17117 "#}
17118 .to_string(),
17119 );
17120}
17121
17122#[gpui::test]
17123async fn test_deletion_reverts(cx: &mut TestAppContext) {
17124 init_test(cx, |_| {});
17125 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17126 let base_text = indoc! {r#"struct Row;
17127struct Row1;
17128struct Row2;
17129
17130struct Row4;
17131struct Row5;
17132struct Row6;
17133
17134struct Row8;
17135struct Row9;
17136struct Row10;"#};
17137
17138 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
17139 assert_hunk_revert(
17140 indoc! {r#"struct Row;
17141 struct Row2;
17142
17143 ˇstruct Row4;
17144 struct Row5;
17145 struct Row6;
17146 ˇ
17147 struct Row8;
17148 struct Row10;"#},
17149 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
17150 indoc! {r#"struct Row;
17151 struct Row2;
17152
17153 ˇstruct Row4;
17154 struct Row5;
17155 struct Row6;
17156 ˇ
17157 struct Row8;
17158 struct Row10;"#},
17159 base_text,
17160 &mut cx,
17161 );
17162 assert_hunk_revert(
17163 indoc! {r#"struct Row;
17164 struct Row2;
17165
17166 «ˇstruct Row4;
17167 struct» Row5;
17168 «struct Row6;
17169 ˇ»
17170 struct Row8;
17171 struct Row10;"#},
17172 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
17173 indoc! {r#"struct Row;
17174 struct Row2;
17175
17176 «ˇstruct Row4;
17177 struct» Row5;
17178 «struct Row6;
17179 ˇ»
17180 struct Row8;
17181 struct Row10;"#},
17182 base_text,
17183 &mut cx,
17184 );
17185
17186 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
17187 assert_hunk_revert(
17188 indoc! {r#"struct Row;
17189 ˇstruct Row2;
17190
17191 struct Row4;
17192 struct Row5;
17193 struct Row6;
17194
17195 struct Row8;ˇ
17196 struct Row10;"#},
17197 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
17198 indoc! {r#"struct Row;
17199 struct Row1;
17200 ˇstruct Row2;
17201
17202 struct Row4;
17203 struct Row5;
17204 struct Row6;
17205
17206 struct Row8;ˇ
17207 struct Row9;
17208 struct Row10;"#},
17209 base_text,
17210 &mut cx,
17211 );
17212 assert_hunk_revert(
17213 indoc! {r#"struct Row;
17214 struct Row2«ˇ;
17215 struct Row4;
17216 struct» Row5;
17217 «struct Row6;
17218
17219 struct Row8;ˇ»
17220 struct Row10;"#},
17221 vec![
17222 DiffHunkStatusKind::Deleted,
17223 DiffHunkStatusKind::Deleted,
17224 DiffHunkStatusKind::Deleted,
17225 ],
17226 indoc! {r#"struct Row;
17227 struct Row1;
17228 struct Row2«ˇ;
17229
17230 struct Row4;
17231 struct» Row5;
17232 «struct Row6;
17233
17234 struct Row8;ˇ»
17235 struct Row9;
17236 struct Row10;"#},
17237 base_text,
17238 &mut cx,
17239 );
17240}
17241
17242#[gpui::test]
17243async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
17244 init_test(cx, |_| {});
17245
17246 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
17247 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
17248 let base_text_3 =
17249 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
17250
17251 let text_1 = edit_first_char_of_every_line(base_text_1);
17252 let text_2 = edit_first_char_of_every_line(base_text_2);
17253 let text_3 = edit_first_char_of_every_line(base_text_3);
17254
17255 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
17256 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
17257 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
17258
17259 let multibuffer = cx.new(|cx| {
17260 let mut multibuffer = MultiBuffer::new(ReadWrite);
17261 multibuffer.push_excerpts(
17262 buffer_1.clone(),
17263 [
17264 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17265 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17266 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17267 ],
17268 cx,
17269 );
17270 multibuffer.push_excerpts(
17271 buffer_2.clone(),
17272 [
17273 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17274 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17275 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17276 ],
17277 cx,
17278 );
17279 multibuffer.push_excerpts(
17280 buffer_3.clone(),
17281 [
17282 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17283 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17284 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17285 ],
17286 cx,
17287 );
17288 multibuffer
17289 });
17290
17291 let fs = FakeFs::new(cx.executor());
17292 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
17293 let (editor, cx) = cx
17294 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
17295 editor.update_in(cx, |editor, _window, cx| {
17296 for (buffer, diff_base) in [
17297 (buffer_1.clone(), base_text_1),
17298 (buffer_2.clone(), base_text_2),
17299 (buffer_3.clone(), base_text_3),
17300 ] {
17301 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17302 editor
17303 .buffer
17304 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17305 }
17306 });
17307 cx.executor().run_until_parked();
17308
17309 editor.update_in(cx, |editor, window, cx| {
17310 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}");
17311 editor.select_all(&SelectAll, window, cx);
17312 editor.git_restore(&Default::default(), window, cx);
17313 });
17314 cx.executor().run_until_parked();
17315
17316 // When all ranges are selected, all buffer hunks are reverted.
17317 editor.update(cx, |editor, cx| {
17318 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");
17319 });
17320 buffer_1.update(cx, |buffer, _| {
17321 assert_eq!(buffer.text(), base_text_1);
17322 });
17323 buffer_2.update(cx, |buffer, _| {
17324 assert_eq!(buffer.text(), base_text_2);
17325 });
17326 buffer_3.update(cx, |buffer, _| {
17327 assert_eq!(buffer.text(), base_text_3);
17328 });
17329
17330 editor.update_in(cx, |editor, window, cx| {
17331 editor.undo(&Default::default(), window, cx);
17332 });
17333
17334 editor.update_in(cx, |editor, window, cx| {
17335 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17336 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
17337 });
17338 editor.git_restore(&Default::default(), window, cx);
17339 });
17340
17341 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
17342 // but not affect buffer_2 and its related excerpts.
17343 editor.update(cx, |editor, cx| {
17344 assert_eq!(
17345 editor.text(cx),
17346 "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}"
17347 );
17348 });
17349 buffer_1.update(cx, |buffer, _| {
17350 assert_eq!(buffer.text(), base_text_1);
17351 });
17352 buffer_2.update(cx, |buffer, _| {
17353 assert_eq!(
17354 buffer.text(),
17355 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
17356 );
17357 });
17358 buffer_3.update(cx, |buffer, _| {
17359 assert_eq!(
17360 buffer.text(),
17361 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
17362 );
17363 });
17364
17365 fn edit_first_char_of_every_line(text: &str) -> String {
17366 text.split('\n')
17367 .map(|line| format!("X{}", &line[1..]))
17368 .collect::<Vec<_>>()
17369 .join("\n")
17370 }
17371}
17372
17373#[gpui::test]
17374async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
17375 init_test(cx, |_| {});
17376
17377 let cols = 4;
17378 let rows = 10;
17379 let sample_text_1 = sample_text(rows, cols, 'a');
17380 assert_eq!(
17381 sample_text_1,
17382 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
17383 );
17384 let sample_text_2 = sample_text(rows, cols, 'l');
17385 assert_eq!(
17386 sample_text_2,
17387 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
17388 );
17389 let sample_text_3 = sample_text(rows, cols, 'v');
17390 assert_eq!(
17391 sample_text_3,
17392 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
17393 );
17394
17395 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
17396 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
17397 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
17398
17399 let multi_buffer = cx.new(|cx| {
17400 let mut multibuffer = MultiBuffer::new(ReadWrite);
17401 multibuffer.push_excerpts(
17402 buffer_1.clone(),
17403 [
17404 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17405 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17406 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17407 ],
17408 cx,
17409 );
17410 multibuffer.push_excerpts(
17411 buffer_2.clone(),
17412 [
17413 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17414 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17415 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17416 ],
17417 cx,
17418 );
17419 multibuffer.push_excerpts(
17420 buffer_3.clone(),
17421 [
17422 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17423 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17424 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17425 ],
17426 cx,
17427 );
17428 multibuffer
17429 });
17430
17431 let fs = FakeFs::new(cx.executor());
17432 fs.insert_tree(
17433 "/a",
17434 json!({
17435 "main.rs": sample_text_1,
17436 "other.rs": sample_text_2,
17437 "lib.rs": sample_text_3,
17438 }),
17439 )
17440 .await;
17441 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17442 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17443 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17444 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17445 Editor::new(
17446 EditorMode::full(),
17447 multi_buffer,
17448 Some(project.clone()),
17449 window,
17450 cx,
17451 )
17452 });
17453 let multibuffer_item_id = workspace
17454 .update(cx, |workspace, window, cx| {
17455 assert!(
17456 workspace.active_item(cx).is_none(),
17457 "active item should be None before the first item is added"
17458 );
17459 workspace.add_item_to_active_pane(
17460 Box::new(multi_buffer_editor.clone()),
17461 None,
17462 true,
17463 window,
17464 cx,
17465 );
17466 let active_item = workspace
17467 .active_item(cx)
17468 .expect("should have an active item after adding the multi buffer");
17469 assert!(
17470 !active_item.is_singleton(cx),
17471 "A multi buffer was expected to active after adding"
17472 );
17473 active_item.item_id()
17474 })
17475 .unwrap();
17476 cx.executor().run_until_parked();
17477
17478 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17479 editor.change_selections(
17480 SelectionEffects::scroll(Autoscroll::Next),
17481 window,
17482 cx,
17483 |s| s.select_ranges(Some(1..2)),
17484 );
17485 editor.open_excerpts(&OpenExcerpts, window, cx);
17486 });
17487 cx.executor().run_until_parked();
17488 let first_item_id = workspace
17489 .update(cx, |workspace, window, cx| {
17490 let active_item = workspace
17491 .active_item(cx)
17492 .expect("should have an active item after navigating into the 1st buffer");
17493 let first_item_id = active_item.item_id();
17494 assert_ne!(
17495 first_item_id, multibuffer_item_id,
17496 "Should navigate into the 1st buffer and activate it"
17497 );
17498 assert!(
17499 active_item.is_singleton(cx),
17500 "New active item should be a singleton buffer"
17501 );
17502 assert_eq!(
17503 active_item
17504 .act_as::<Editor>(cx)
17505 .expect("should have navigated into an editor for the 1st buffer")
17506 .read(cx)
17507 .text(cx),
17508 sample_text_1
17509 );
17510
17511 workspace
17512 .go_back(workspace.active_pane().downgrade(), window, cx)
17513 .detach_and_log_err(cx);
17514
17515 first_item_id
17516 })
17517 .unwrap();
17518 cx.executor().run_until_parked();
17519 workspace
17520 .update(cx, |workspace, _, cx| {
17521 let active_item = workspace
17522 .active_item(cx)
17523 .expect("should have an active item after navigating back");
17524 assert_eq!(
17525 active_item.item_id(),
17526 multibuffer_item_id,
17527 "Should navigate back to the multi buffer"
17528 );
17529 assert!(!active_item.is_singleton(cx));
17530 })
17531 .unwrap();
17532
17533 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17534 editor.change_selections(
17535 SelectionEffects::scroll(Autoscroll::Next),
17536 window,
17537 cx,
17538 |s| s.select_ranges(Some(39..40)),
17539 );
17540 editor.open_excerpts(&OpenExcerpts, window, cx);
17541 });
17542 cx.executor().run_until_parked();
17543 let second_item_id = workspace
17544 .update(cx, |workspace, window, cx| {
17545 let active_item = workspace
17546 .active_item(cx)
17547 .expect("should have an active item after navigating into the 2nd buffer");
17548 let second_item_id = active_item.item_id();
17549 assert_ne!(
17550 second_item_id, multibuffer_item_id,
17551 "Should navigate away from the multibuffer"
17552 );
17553 assert_ne!(
17554 second_item_id, first_item_id,
17555 "Should navigate into the 2nd buffer and activate it"
17556 );
17557 assert!(
17558 active_item.is_singleton(cx),
17559 "New active item should be a singleton buffer"
17560 );
17561 assert_eq!(
17562 active_item
17563 .act_as::<Editor>(cx)
17564 .expect("should have navigated into an editor")
17565 .read(cx)
17566 .text(cx),
17567 sample_text_2
17568 );
17569
17570 workspace
17571 .go_back(workspace.active_pane().downgrade(), window, cx)
17572 .detach_and_log_err(cx);
17573
17574 second_item_id
17575 })
17576 .unwrap();
17577 cx.executor().run_until_parked();
17578 workspace
17579 .update(cx, |workspace, _, cx| {
17580 let active_item = workspace
17581 .active_item(cx)
17582 .expect("should have an active item after navigating back from the 2nd buffer");
17583 assert_eq!(
17584 active_item.item_id(),
17585 multibuffer_item_id,
17586 "Should navigate back from the 2nd buffer to the multi buffer"
17587 );
17588 assert!(!active_item.is_singleton(cx));
17589 })
17590 .unwrap();
17591
17592 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17593 editor.change_selections(
17594 SelectionEffects::scroll(Autoscroll::Next),
17595 window,
17596 cx,
17597 |s| s.select_ranges(Some(70..70)),
17598 );
17599 editor.open_excerpts(&OpenExcerpts, window, cx);
17600 });
17601 cx.executor().run_until_parked();
17602 workspace
17603 .update(cx, |workspace, window, cx| {
17604 let active_item = workspace
17605 .active_item(cx)
17606 .expect("should have an active item after navigating into the 3rd buffer");
17607 let third_item_id = active_item.item_id();
17608 assert_ne!(
17609 third_item_id, multibuffer_item_id,
17610 "Should navigate into the 3rd buffer and activate it"
17611 );
17612 assert_ne!(third_item_id, first_item_id);
17613 assert_ne!(third_item_id, second_item_id);
17614 assert!(
17615 active_item.is_singleton(cx),
17616 "New active item should be a singleton buffer"
17617 );
17618 assert_eq!(
17619 active_item
17620 .act_as::<Editor>(cx)
17621 .expect("should have navigated into an editor")
17622 .read(cx)
17623 .text(cx),
17624 sample_text_3
17625 );
17626
17627 workspace
17628 .go_back(workspace.active_pane().downgrade(), window, cx)
17629 .detach_and_log_err(cx);
17630 })
17631 .unwrap();
17632 cx.executor().run_until_parked();
17633 workspace
17634 .update(cx, |workspace, _, cx| {
17635 let active_item = workspace
17636 .active_item(cx)
17637 .expect("should have an active item after navigating back from the 3rd buffer");
17638 assert_eq!(
17639 active_item.item_id(),
17640 multibuffer_item_id,
17641 "Should navigate back from the 3rd buffer to the multi buffer"
17642 );
17643 assert!(!active_item.is_singleton(cx));
17644 })
17645 .unwrap();
17646}
17647
17648#[gpui::test]
17649async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17650 init_test(cx, |_| {});
17651
17652 let mut cx = EditorTestContext::new(cx).await;
17653
17654 let diff_base = r#"
17655 use some::mod;
17656
17657 const A: u32 = 42;
17658
17659 fn main() {
17660 println!("hello");
17661
17662 println!("world");
17663 }
17664 "#
17665 .unindent();
17666
17667 cx.set_state(
17668 &r#"
17669 use some::modified;
17670
17671 ˇ
17672 fn main() {
17673 println!("hello there");
17674
17675 println!("around the");
17676 println!("world");
17677 }
17678 "#
17679 .unindent(),
17680 );
17681
17682 cx.set_head_text(&diff_base);
17683 executor.run_until_parked();
17684
17685 cx.update_editor(|editor, window, cx| {
17686 editor.go_to_next_hunk(&GoToHunk, window, cx);
17687 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17688 });
17689 executor.run_until_parked();
17690 cx.assert_state_with_diff(
17691 r#"
17692 use some::modified;
17693
17694
17695 fn main() {
17696 - println!("hello");
17697 + ˇ println!("hello there");
17698
17699 println!("around the");
17700 println!("world");
17701 }
17702 "#
17703 .unindent(),
17704 );
17705
17706 cx.update_editor(|editor, window, cx| {
17707 for _ in 0..2 {
17708 editor.go_to_next_hunk(&GoToHunk, window, cx);
17709 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17710 }
17711 });
17712 executor.run_until_parked();
17713 cx.assert_state_with_diff(
17714 r#"
17715 - use some::mod;
17716 + ˇuse some::modified;
17717
17718
17719 fn main() {
17720 - println!("hello");
17721 + println!("hello there");
17722
17723 + println!("around the");
17724 println!("world");
17725 }
17726 "#
17727 .unindent(),
17728 );
17729
17730 cx.update_editor(|editor, window, cx| {
17731 editor.go_to_next_hunk(&GoToHunk, window, cx);
17732 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17733 });
17734 executor.run_until_parked();
17735 cx.assert_state_with_diff(
17736 r#"
17737 - use some::mod;
17738 + use some::modified;
17739
17740 - const A: u32 = 42;
17741 ˇ
17742 fn main() {
17743 - println!("hello");
17744 + println!("hello there");
17745
17746 + println!("around the");
17747 println!("world");
17748 }
17749 "#
17750 .unindent(),
17751 );
17752
17753 cx.update_editor(|editor, window, cx| {
17754 editor.cancel(&Cancel, window, cx);
17755 });
17756
17757 cx.assert_state_with_diff(
17758 r#"
17759 use some::modified;
17760
17761 ˇ
17762 fn main() {
17763 println!("hello there");
17764
17765 println!("around the");
17766 println!("world");
17767 }
17768 "#
17769 .unindent(),
17770 );
17771}
17772
17773#[gpui::test]
17774async fn test_diff_base_change_with_expanded_diff_hunks(
17775 executor: BackgroundExecutor,
17776 cx: &mut TestAppContext,
17777) {
17778 init_test(cx, |_| {});
17779
17780 let mut cx = EditorTestContext::new(cx).await;
17781
17782 let diff_base = r#"
17783 use some::mod1;
17784 use some::mod2;
17785
17786 const A: u32 = 42;
17787 const B: u32 = 42;
17788 const C: u32 = 42;
17789
17790 fn main() {
17791 println!("hello");
17792
17793 println!("world");
17794 }
17795 "#
17796 .unindent();
17797
17798 cx.set_state(
17799 &r#"
17800 use some::mod2;
17801
17802 const A: u32 = 42;
17803 const C: u32 = 42;
17804
17805 fn main(ˇ) {
17806 //println!("hello");
17807
17808 println!("world");
17809 //
17810 //
17811 }
17812 "#
17813 .unindent(),
17814 );
17815
17816 cx.set_head_text(&diff_base);
17817 executor.run_until_parked();
17818
17819 cx.update_editor(|editor, window, cx| {
17820 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17821 });
17822 executor.run_until_parked();
17823 cx.assert_state_with_diff(
17824 r#"
17825 - use some::mod1;
17826 use some::mod2;
17827
17828 const A: u32 = 42;
17829 - const B: u32 = 42;
17830 const C: u32 = 42;
17831
17832 fn main(ˇ) {
17833 - println!("hello");
17834 + //println!("hello");
17835
17836 println!("world");
17837 + //
17838 + //
17839 }
17840 "#
17841 .unindent(),
17842 );
17843
17844 cx.set_head_text("new diff base!");
17845 executor.run_until_parked();
17846 cx.assert_state_with_diff(
17847 r#"
17848 - new diff base!
17849 + use some::mod2;
17850 +
17851 + const A: u32 = 42;
17852 + const C: u32 = 42;
17853 +
17854 + fn main(ˇ) {
17855 + //println!("hello");
17856 +
17857 + println!("world");
17858 + //
17859 + //
17860 + }
17861 "#
17862 .unindent(),
17863 );
17864}
17865
17866#[gpui::test]
17867async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
17868 init_test(cx, |_| {});
17869
17870 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17871 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17872 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17873 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17874 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
17875 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
17876
17877 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
17878 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
17879 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
17880
17881 let multi_buffer = cx.new(|cx| {
17882 let mut multibuffer = MultiBuffer::new(ReadWrite);
17883 multibuffer.push_excerpts(
17884 buffer_1.clone(),
17885 [
17886 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17887 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17888 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17889 ],
17890 cx,
17891 );
17892 multibuffer.push_excerpts(
17893 buffer_2.clone(),
17894 [
17895 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17896 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17897 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17898 ],
17899 cx,
17900 );
17901 multibuffer.push_excerpts(
17902 buffer_3.clone(),
17903 [
17904 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17905 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17906 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17907 ],
17908 cx,
17909 );
17910 multibuffer
17911 });
17912
17913 let editor =
17914 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17915 editor
17916 .update(cx, |editor, _window, cx| {
17917 for (buffer, diff_base) in [
17918 (buffer_1.clone(), file_1_old),
17919 (buffer_2.clone(), file_2_old),
17920 (buffer_3.clone(), file_3_old),
17921 ] {
17922 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17923 editor
17924 .buffer
17925 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17926 }
17927 })
17928 .unwrap();
17929
17930 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17931 cx.run_until_parked();
17932
17933 cx.assert_editor_state(
17934 &"
17935 ˇaaa
17936 ccc
17937 ddd
17938
17939 ggg
17940 hhh
17941
17942
17943 lll
17944 mmm
17945 NNN
17946
17947 qqq
17948 rrr
17949
17950 uuu
17951 111
17952 222
17953 333
17954
17955 666
17956 777
17957
17958 000
17959 !!!"
17960 .unindent(),
17961 );
17962
17963 cx.update_editor(|editor, window, cx| {
17964 editor.select_all(&SelectAll, window, cx);
17965 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17966 });
17967 cx.executor().run_until_parked();
17968
17969 cx.assert_state_with_diff(
17970 "
17971 «aaa
17972 - bbb
17973 ccc
17974 ddd
17975
17976 ggg
17977 hhh
17978
17979
17980 lll
17981 mmm
17982 - nnn
17983 + NNN
17984
17985 qqq
17986 rrr
17987
17988 uuu
17989 111
17990 222
17991 333
17992
17993 + 666
17994 777
17995
17996 000
17997 !!!ˇ»"
17998 .unindent(),
17999 );
18000}
18001
18002#[gpui::test]
18003async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
18004 init_test(cx, |_| {});
18005
18006 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
18007 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
18008
18009 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
18010 let multi_buffer = cx.new(|cx| {
18011 let mut multibuffer = MultiBuffer::new(ReadWrite);
18012 multibuffer.push_excerpts(
18013 buffer.clone(),
18014 [
18015 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
18016 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
18017 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
18018 ],
18019 cx,
18020 );
18021 multibuffer
18022 });
18023
18024 let editor =
18025 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
18026 editor
18027 .update(cx, |editor, _window, cx| {
18028 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
18029 editor
18030 .buffer
18031 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
18032 })
18033 .unwrap();
18034
18035 let mut cx = EditorTestContext::for_editor(editor, cx).await;
18036 cx.run_until_parked();
18037
18038 cx.update_editor(|editor, window, cx| {
18039 editor.expand_all_diff_hunks(&Default::default(), window, cx)
18040 });
18041 cx.executor().run_until_parked();
18042
18043 // When the start of a hunk coincides with the start of its excerpt,
18044 // the hunk is expanded. When the start of a a hunk is earlier than
18045 // the start of its excerpt, the hunk is not expanded.
18046 cx.assert_state_with_diff(
18047 "
18048 ˇaaa
18049 - bbb
18050 + BBB
18051
18052 - ddd
18053 - eee
18054 + DDD
18055 + EEE
18056 fff
18057
18058 iii
18059 "
18060 .unindent(),
18061 );
18062}
18063
18064#[gpui::test]
18065async fn test_edits_around_expanded_insertion_hunks(
18066 executor: BackgroundExecutor,
18067 cx: &mut TestAppContext,
18068) {
18069 init_test(cx, |_| {});
18070
18071 let mut cx = EditorTestContext::new(cx).await;
18072
18073 let diff_base = r#"
18074 use some::mod1;
18075 use some::mod2;
18076
18077 const A: u32 = 42;
18078
18079 fn main() {
18080 println!("hello");
18081
18082 println!("world");
18083 }
18084 "#
18085 .unindent();
18086 executor.run_until_parked();
18087 cx.set_state(
18088 &r#"
18089 use some::mod1;
18090 use some::mod2;
18091
18092 const A: u32 = 42;
18093 const B: u32 = 42;
18094 const C: u32 = 42;
18095 ˇ
18096
18097 fn main() {
18098 println!("hello");
18099
18100 println!("world");
18101 }
18102 "#
18103 .unindent(),
18104 );
18105
18106 cx.set_head_text(&diff_base);
18107 executor.run_until_parked();
18108
18109 cx.update_editor(|editor, window, cx| {
18110 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18111 });
18112 executor.run_until_parked();
18113
18114 cx.assert_state_with_diff(
18115 r#"
18116 use some::mod1;
18117 use some::mod2;
18118
18119 const A: u32 = 42;
18120 + const B: u32 = 42;
18121 + const C: u32 = 42;
18122 + ˇ
18123
18124 fn main() {
18125 println!("hello");
18126
18127 println!("world");
18128 }
18129 "#
18130 .unindent(),
18131 );
18132
18133 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
18134 executor.run_until_parked();
18135
18136 cx.assert_state_with_diff(
18137 r#"
18138 use some::mod1;
18139 use some::mod2;
18140
18141 const A: u32 = 42;
18142 + const B: u32 = 42;
18143 + const C: u32 = 42;
18144 + const D: u32 = 42;
18145 + ˇ
18146
18147 fn main() {
18148 println!("hello");
18149
18150 println!("world");
18151 }
18152 "#
18153 .unindent(),
18154 );
18155
18156 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
18157 executor.run_until_parked();
18158
18159 cx.assert_state_with_diff(
18160 r#"
18161 use some::mod1;
18162 use some::mod2;
18163
18164 const A: u32 = 42;
18165 + const B: u32 = 42;
18166 + const C: u32 = 42;
18167 + const D: u32 = 42;
18168 + const E: u32 = 42;
18169 + ˇ
18170
18171 fn main() {
18172 println!("hello");
18173
18174 println!("world");
18175 }
18176 "#
18177 .unindent(),
18178 );
18179
18180 cx.update_editor(|editor, window, cx| {
18181 editor.delete_line(&DeleteLine, window, cx);
18182 });
18183 executor.run_until_parked();
18184
18185 cx.assert_state_with_diff(
18186 r#"
18187 use some::mod1;
18188 use some::mod2;
18189
18190 const A: u32 = 42;
18191 + const B: u32 = 42;
18192 + const C: u32 = 42;
18193 + const D: u32 = 42;
18194 + const E: u32 = 42;
18195 ˇ
18196 fn main() {
18197 println!("hello");
18198
18199 println!("world");
18200 }
18201 "#
18202 .unindent(),
18203 );
18204
18205 cx.update_editor(|editor, window, cx| {
18206 editor.move_up(&MoveUp, window, cx);
18207 editor.delete_line(&DeleteLine, window, cx);
18208 editor.move_up(&MoveUp, window, cx);
18209 editor.delete_line(&DeleteLine, window, cx);
18210 editor.move_up(&MoveUp, window, cx);
18211 editor.delete_line(&DeleteLine, window, cx);
18212 });
18213 executor.run_until_parked();
18214 cx.assert_state_with_diff(
18215 r#"
18216 use some::mod1;
18217 use some::mod2;
18218
18219 const A: u32 = 42;
18220 + const B: u32 = 42;
18221 ˇ
18222 fn main() {
18223 println!("hello");
18224
18225 println!("world");
18226 }
18227 "#
18228 .unindent(),
18229 );
18230
18231 cx.update_editor(|editor, window, cx| {
18232 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
18233 editor.delete_line(&DeleteLine, window, cx);
18234 });
18235 executor.run_until_parked();
18236 cx.assert_state_with_diff(
18237 r#"
18238 ˇ
18239 fn main() {
18240 println!("hello");
18241
18242 println!("world");
18243 }
18244 "#
18245 .unindent(),
18246 );
18247}
18248
18249#[gpui::test]
18250async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
18251 init_test(cx, |_| {});
18252
18253 let mut cx = EditorTestContext::new(cx).await;
18254 cx.set_head_text(indoc! { "
18255 one
18256 two
18257 three
18258 four
18259 five
18260 "
18261 });
18262 cx.set_state(indoc! { "
18263 one
18264 ˇthree
18265 five
18266 "});
18267 cx.run_until_parked();
18268 cx.update_editor(|editor, window, cx| {
18269 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18270 });
18271 cx.assert_state_with_diff(
18272 indoc! { "
18273 one
18274 - two
18275 ˇthree
18276 - four
18277 five
18278 "}
18279 .to_string(),
18280 );
18281 cx.update_editor(|editor, window, cx| {
18282 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18283 });
18284
18285 cx.assert_state_with_diff(
18286 indoc! { "
18287 one
18288 ˇthree
18289 five
18290 "}
18291 .to_string(),
18292 );
18293
18294 cx.set_state(indoc! { "
18295 one
18296 ˇTWO
18297 three
18298 four
18299 five
18300 "});
18301 cx.run_until_parked();
18302 cx.update_editor(|editor, window, cx| {
18303 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18304 });
18305
18306 cx.assert_state_with_diff(
18307 indoc! { "
18308 one
18309 - two
18310 + ˇTWO
18311 three
18312 four
18313 five
18314 "}
18315 .to_string(),
18316 );
18317 cx.update_editor(|editor, window, cx| {
18318 editor.move_up(&Default::default(), window, cx);
18319 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18320 });
18321 cx.assert_state_with_diff(
18322 indoc! { "
18323 one
18324 ˇTWO
18325 three
18326 four
18327 five
18328 "}
18329 .to_string(),
18330 );
18331}
18332
18333#[gpui::test]
18334async fn test_edits_around_expanded_deletion_hunks(
18335 executor: BackgroundExecutor,
18336 cx: &mut TestAppContext,
18337) {
18338 init_test(cx, |_| {});
18339
18340 let mut cx = EditorTestContext::new(cx).await;
18341
18342 let diff_base = r#"
18343 use some::mod1;
18344 use some::mod2;
18345
18346 const A: u32 = 42;
18347 const B: u32 = 42;
18348 const C: u32 = 42;
18349
18350
18351 fn main() {
18352 println!("hello");
18353
18354 println!("world");
18355 }
18356 "#
18357 .unindent();
18358 executor.run_until_parked();
18359 cx.set_state(
18360 &r#"
18361 use some::mod1;
18362 use some::mod2;
18363
18364 ˇconst B: u32 = 42;
18365 const C: u32 = 42;
18366
18367
18368 fn main() {
18369 println!("hello");
18370
18371 println!("world");
18372 }
18373 "#
18374 .unindent(),
18375 );
18376
18377 cx.set_head_text(&diff_base);
18378 executor.run_until_parked();
18379
18380 cx.update_editor(|editor, window, cx| {
18381 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18382 });
18383 executor.run_until_parked();
18384
18385 cx.assert_state_with_diff(
18386 r#"
18387 use some::mod1;
18388 use some::mod2;
18389
18390 - const A: u32 = 42;
18391 ˇconst B: u32 = 42;
18392 const C: u32 = 42;
18393
18394
18395 fn main() {
18396 println!("hello");
18397
18398 println!("world");
18399 }
18400 "#
18401 .unindent(),
18402 );
18403
18404 cx.update_editor(|editor, window, cx| {
18405 editor.delete_line(&DeleteLine, window, cx);
18406 });
18407 executor.run_until_parked();
18408 cx.assert_state_with_diff(
18409 r#"
18410 use some::mod1;
18411 use some::mod2;
18412
18413 - const A: u32 = 42;
18414 - const B: u32 = 42;
18415 ˇconst C: u32 = 42;
18416
18417
18418 fn main() {
18419 println!("hello");
18420
18421 println!("world");
18422 }
18423 "#
18424 .unindent(),
18425 );
18426
18427 cx.update_editor(|editor, window, cx| {
18428 editor.delete_line(&DeleteLine, window, cx);
18429 });
18430 executor.run_until_parked();
18431 cx.assert_state_with_diff(
18432 r#"
18433 use some::mod1;
18434 use some::mod2;
18435
18436 - const A: u32 = 42;
18437 - const B: u32 = 42;
18438 - const C: u32 = 42;
18439 ˇ
18440
18441 fn main() {
18442 println!("hello");
18443
18444 println!("world");
18445 }
18446 "#
18447 .unindent(),
18448 );
18449
18450 cx.update_editor(|editor, window, cx| {
18451 editor.handle_input("replacement", window, cx);
18452 });
18453 executor.run_until_parked();
18454 cx.assert_state_with_diff(
18455 r#"
18456 use some::mod1;
18457 use some::mod2;
18458
18459 - const A: u32 = 42;
18460 - const B: u32 = 42;
18461 - const C: u32 = 42;
18462 -
18463 + replacementˇ
18464
18465 fn main() {
18466 println!("hello");
18467
18468 println!("world");
18469 }
18470 "#
18471 .unindent(),
18472 );
18473}
18474
18475#[gpui::test]
18476async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18477 init_test(cx, |_| {});
18478
18479 let mut cx = EditorTestContext::new(cx).await;
18480
18481 let base_text = r#"
18482 one
18483 two
18484 three
18485 four
18486 five
18487 "#
18488 .unindent();
18489 executor.run_until_parked();
18490 cx.set_state(
18491 &r#"
18492 one
18493 two
18494 fˇour
18495 five
18496 "#
18497 .unindent(),
18498 );
18499
18500 cx.set_head_text(&base_text);
18501 executor.run_until_parked();
18502
18503 cx.update_editor(|editor, window, cx| {
18504 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18505 });
18506 executor.run_until_parked();
18507
18508 cx.assert_state_with_diff(
18509 r#"
18510 one
18511 two
18512 - three
18513 fˇour
18514 five
18515 "#
18516 .unindent(),
18517 );
18518
18519 cx.update_editor(|editor, window, cx| {
18520 editor.backspace(&Backspace, window, cx);
18521 editor.backspace(&Backspace, window, cx);
18522 });
18523 executor.run_until_parked();
18524 cx.assert_state_with_diff(
18525 r#"
18526 one
18527 two
18528 - threeˇ
18529 - four
18530 + our
18531 five
18532 "#
18533 .unindent(),
18534 );
18535}
18536
18537#[gpui::test]
18538async fn test_edit_after_expanded_modification_hunk(
18539 executor: BackgroundExecutor,
18540 cx: &mut TestAppContext,
18541) {
18542 init_test(cx, |_| {});
18543
18544 let mut cx = EditorTestContext::new(cx).await;
18545
18546 let diff_base = r#"
18547 use some::mod1;
18548 use some::mod2;
18549
18550 const A: u32 = 42;
18551 const B: u32 = 42;
18552 const C: u32 = 42;
18553 const D: u32 = 42;
18554
18555
18556 fn main() {
18557 println!("hello");
18558
18559 println!("world");
18560 }"#
18561 .unindent();
18562
18563 cx.set_state(
18564 &r#"
18565 use some::mod1;
18566 use some::mod2;
18567
18568 const A: u32 = 42;
18569 const B: u32 = 42;
18570 const C: u32 = 43ˇ
18571 const D: u32 = 42;
18572
18573
18574 fn main() {
18575 println!("hello");
18576
18577 println!("world");
18578 }"#
18579 .unindent(),
18580 );
18581
18582 cx.set_head_text(&diff_base);
18583 executor.run_until_parked();
18584 cx.update_editor(|editor, window, cx| {
18585 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18586 });
18587 executor.run_until_parked();
18588
18589 cx.assert_state_with_diff(
18590 r#"
18591 use some::mod1;
18592 use some::mod2;
18593
18594 const A: u32 = 42;
18595 const B: u32 = 42;
18596 - const C: u32 = 42;
18597 + const C: u32 = 43ˇ
18598 const D: u32 = 42;
18599
18600
18601 fn main() {
18602 println!("hello");
18603
18604 println!("world");
18605 }"#
18606 .unindent(),
18607 );
18608
18609 cx.update_editor(|editor, window, cx| {
18610 editor.handle_input("\nnew_line\n", window, cx);
18611 });
18612 executor.run_until_parked();
18613
18614 cx.assert_state_with_diff(
18615 r#"
18616 use some::mod1;
18617 use some::mod2;
18618
18619 const A: u32 = 42;
18620 const B: u32 = 42;
18621 - const C: u32 = 42;
18622 + const C: u32 = 43
18623 + new_line
18624 + ˇ
18625 const D: u32 = 42;
18626
18627
18628 fn main() {
18629 println!("hello");
18630
18631 println!("world");
18632 }"#
18633 .unindent(),
18634 );
18635}
18636
18637#[gpui::test]
18638async fn test_stage_and_unstage_added_file_hunk(
18639 executor: BackgroundExecutor,
18640 cx: &mut TestAppContext,
18641) {
18642 init_test(cx, |_| {});
18643
18644 let mut cx = EditorTestContext::new(cx).await;
18645 cx.update_editor(|editor, _, cx| {
18646 editor.set_expand_all_diff_hunks(cx);
18647 });
18648
18649 let working_copy = r#"
18650 ˇfn main() {
18651 println!("hello, world!");
18652 }
18653 "#
18654 .unindent();
18655
18656 cx.set_state(&working_copy);
18657 executor.run_until_parked();
18658
18659 cx.assert_state_with_diff(
18660 r#"
18661 + ˇfn main() {
18662 + println!("hello, world!");
18663 + }
18664 "#
18665 .unindent(),
18666 );
18667 cx.assert_index_text(None);
18668
18669 cx.update_editor(|editor, window, cx| {
18670 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18671 });
18672 executor.run_until_parked();
18673 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
18674 cx.assert_state_with_diff(
18675 r#"
18676 + ˇfn main() {
18677 + println!("hello, world!");
18678 + }
18679 "#
18680 .unindent(),
18681 );
18682
18683 cx.update_editor(|editor, window, cx| {
18684 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18685 });
18686 executor.run_until_parked();
18687 cx.assert_index_text(None);
18688}
18689
18690async fn setup_indent_guides_editor(
18691 text: &str,
18692 cx: &mut TestAppContext,
18693) -> (BufferId, EditorTestContext) {
18694 init_test(cx, |_| {});
18695
18696 let mut cx = EditorTestContext::new(cx).await;
18697
18698 let buffer_id = cx.update_editor(|editor, window, cx| {
18699 editor.set_text(text, window, cx);
18700 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
18701
18702 buffer_ids[0]
18703 });
18704
18705 (buffer_id, cx)
18706}
18707
18708fn assert_indent_guides(
18709 range: Range<u32>,
18710 expected: Vec<IndentGuide>,
18711 active_indices: Option<Vec<usize>>,
18712 cx: &mut EditorTestContext,
18713) {
18714 let indent_guides = cx.update_editor(|editor, window, cx| {
18715 let snapshot = editor.snapshot(window, cx).display_snapshot;
18716 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
18717 editor,
18718 MultiBufferRow(range.start)..MultiBufferRow(range.end),
18719 true,
18720 &snapshot,
18721 cx,
18722 );
18723
18724 indent_guides.sort_by(|a, b| {
18725 a.depth.cmp(&b.depth).then(
18726 a.start_row
18727 .cmp(&b.start_row)
18728 .then(a.end_row.cmp(&b.end_row)),
18729 )
18730 });
18731 indent_guides
18732 });
18733
18734 if let Some(expected) = active_indices {
18735 let active_indices = cx.update_editor(|editor, window, cx| {
18736 let snapshot = editor.snapshot(window, cx).display_snapshot;
18737 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
18738 });
18739
18740 assert_eq!(
18741 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
18742 expected,
18743 "Active indent guide indices do not match"
18744 );
18745 }
18746
18747 assert_eq!(indent_guides, expected, "Indent guides do not match");
18748}
18749
18750fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
18751 IndentGuide {
18752 buffer_id,
18753 start_row: MultiBufferRow(start_row),
18754 end_row: MultiBufferRow(end_row),
18755 depth,
18756 tab_size: 4,
18757 settings: IndentGuideSettings {
18758 enabled: true,
18759 line_width: 1,
18760 active_line_width: 1,
18761 ..Default::default()
18762 },
18763 }
18764}
18765
18766#[gpui::test]
18767async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
18768 let (buffer_id, mut cx) = setup_indent_guides_editor(
18769 &"
18770 fn main() {
18771 let a = 1;
18772 }"
18773 .unindent(),
18774 cx,
18775 )
18776 .await;
18777
18778 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18779}
18780
18781#[gpui::test]
18782async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
18783 let (buffer_id, mut cx) = setup_indent_guides_editor(
18784 &"
18785 fn main() {
18786 let a = 1;
18787 let b = 2;
18788 }"
18789 .unindent(),
18790 cx,
18791 )
18792 .await;
18793
18794 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
18795}
18796
18797#[gpui::test]
18798async fn test_indent_guide_nested(cx: &mut TestAppContext) {
18799 let (buffer_id, mut cx) = setup_indent_guides_editor(
18800 &"
18801 fn main() {
18802 let a = 1;
18803 if a == 3 {
18804 let b = 2;
18805 } else {
18806 let c = 3;
18807 }
18808 }"
18809 .unindent(),
18810 cx,
18811 )
18812 .await;
18813
18814 assert_indent_guides(
18815 0..8,
18816 vec![
18817 indent_guide(buffer_id, 1, 6, 0),
18818 indent_guide(buffer_id, 3, 3, 1),
18819 indent_guide(buffer_id, 5, 5, 1),
18820 ],
18821 None,
18822 &mut cx,
18823 );
18824}
18825
18826#[gpui::test]
18827async fn test_indent_guide_tab(cx: &mut TestAppContext) {
18828 let (buffer_id, mut cx) = setup_indent_guides_editor(
18829 &"
18830 fn main() {
18831 let a = 1;
18832 let b = 2;
18833 let c = 3;
18834 }"
18835 .unindent(),
18836 cx,
18837 )
18838 .await;
18839
18840 assert_indent_guides(
18841 0..5,
18842 vec![
18843 indent_guide(buffer_id, 1, 3, 0),
18844 indent_guide(buffer_id, 2, 2, 1),
18845 ],
18846 None,
18847 &mut cx,
18848 );
18849}
18850
18851#[gpui::test]
18852async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
18853 let (buffer_id, mut cx) = setup_indent_guides_editor(
18854 &"
18855 fn main() {
18856 let a = 1;
18857
18858 let c = 3;
18859 }"
18860 .unindent(),
18861 cx,
18862 )
18863 .await;
18864
18865 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
18866}
18867
18868#[gpui::test]
18869async fn test_indent_guide_complex(cx: &mut TestAppContext) {
18870 let (buffer_id, mut cx) = setup_indent_guides_editor(
18871 &"
18872 fn main() {
18873 let a = 1;
18874
18875 let c = 3;
18876
18877 if a == 3 {
18878 let b = 2;
18879 } else {
18880 let c = 3;
18881 }
18882 }"
18883 .unindent(),
18884 cx,
18885 )
18886 .await;
18887
18888 assert_indent_guides(
18889 0..11,
18890 vec![
18891 indent_guide(buffer_id, 1, 9, 0),
18892 indent_guide(buffer_id, 6, 6, 1),
18893 indent_guide(buffer_id, 8, 8, 1),
18894 ],
18895 None,
18896 &mut cx,
18897 );
18898}
18899
18900#[gpui::test]
18901async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
18902 let (buffer_id, mut cx) = setup_indent_guides_editor(
18903 &"
18904 fn main() {
18905 let a = 1;
18906
18907 let c = 3;
18908
18909 if a == 3 {
18910 let b = 2;
18911 } else {
18912 let c = 3;
18913 }
18914 }"
18915 .unindent(),
18916 cx,
18917 )
18918 .await;
18919
18920 assert_indent_guides(
18921 1..11,
18922 vec![
18923 indent_guide(buffer_id, 1, 9, 0),
18924 indent_guide(buffer_id, 6, 6, 1),
18925 indent_guide(buffer_id, 8, 8, 1),
18926 ],
18927 None,
18928 &mut cx,
18929 );
18930}
18931
18932#[gpui::test]
18933async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
18934 let (buffer_id, mut cx) = setup_indent_guides_editor(
18935 &"
18936 fn main() {
18937 let a = 1;
18938
18939 let c = 3;
18940
18941 if a == 3 {
18942 let b = 2;
18943 } else {
18944 let c = 3;
18945 }
18946 }"
18947 .unindent(),
18948 cx,
18949 )
18950 .await;
18951
18952 assert_indent_guides(
18953 1..10,
18954 vec![
18955 indent_guide(buffer_id, 1, 9, 0),
18956 indent_guide(buffer_id, 6, 6, 1),
18957 indent_guide(buffer_id, 8, 8, 1),
18958 ],
18959 None,
18960 &mut cx,
18961 );
18962}
18963
18964#[gpui::test]
18965async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18966 let (buffer_id, mut cx) = setup_indent_guides_editor(
18967 &"
18968 fn main() {
18969 if a {
18970 b(
18971 c,
18972 d,
18973 )
18974 } else {
18975 e(
18976 f
18977 )
18978 }
18979 }"
18980 .unindent(),
18981 cx,
18982 )
18983 .await;
18984
18985 assert_indent_guides(
18986 0..11,
18987 vec![
18988 indent_guide(buffer_id, 1, 10, 0),
18989 indent_guide(buffer_id, 2, 5, 1),
18990 indent_guide(buffer_id, 7, 9, 1),
18991 indent_guide(buffer_id, 3, 4, 2),
18992 indent_guide(buffer_id, 8, 8, 2),
18993 ],
18994 None,
18995 &mut cx,
18996 );
18997
18998 cx.update_editor(|editor, window, cx| {
18999 editor.fold_at(MultiBufferRow(2), window, cx);
19000 assert_eq!(
19001 editor.display_text(cx),
19002 "
19003 fn main() {
19004 if a {
19005 b(⋯
19006 )
19007 } else {
19008 e(
19009 f
19010 )
19011 }
19012 }"
19013 .unindent()
19014 );
19015 });
19016
19017 assert_indent_guides(
19018 0..11,
19019 vec![
19020 indent_guide(buffer_id, 1, 10, 0),
19021 indent_guide(buffer_id, 2, 5, 1),
19022 indent_guide(buffer_id, 7, 9, 1),
19023 indent_guide(buffer_id, 8, 8, 2),
19024 ],
19025 None,
19026 &mut cx,
19027 );
19028}
19029
19030#[gpui::test]
19031async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
19032 let (buffer_id, mut cx) = setup_indent_guides_editor(
19033 &"
19034 block1
19035 block2
19036 block3
19037 block4
19038 block2
19039 block1
19040 block1"
19041 .unindent(),
19042 cx,
19043 )
19044 .await;
19045
19046 assert_indent_guides(
19047 1..10,
19048 vec![
19049 indent_guide(buffer_id, 1, 4, 0),
19050 indent_guide(buffer_id, 2, 3, 1),
19051 indent_guide(buffer_id, 3, 3, 2),
19052 ],
19053 None,
19054 &mut cx,
19055 );
19056}
19057
19058#[gpui::test]
19059async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
19060 let (buffer_id, mut cx) = setup_indent_guides_editor(
19061 &"
19062 block1
19063 block2
19064 block3
19065
19066 block1
19067 block1"
19068 .unindent(),
19069 cx,
19070 )
19071 .await;
19072
19073 assert_indent_guides(
19074 0..6,
19075 vec![
19076 indent_guide(buffer_id, 1, 2, 0),
19077 indent_guide(buffer_id, 2, 2, 1),
19078 ],
19079 None,
19080 &mut cx,
19081 );
19082}
19083
19084#[gpui::test]
19085async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
19086 let (buffer_id, mut cx) = setup_indent_guides_editor(
19087 &"
19088 function component() {
19089 \treturn (
19090 \t\t\t
19091 \t\t<div>
19092 \t\t\t<abc></abc>
19093 \t\t</div>
19094 \t)
19095 }"
19096 .unindent(),
19097 cx,
19098 )
19099 .await;
19100
19101 assert_indent_guides(
19102 0..8,
19103 vec![
19104 indent_guide(buffer_id, 1, 6, 0),
19105 indent_guide(buffer_id, 2, 5, 1),
19106 indent_guide(buffer_id, 4, 4, 2),
19107 ],
19108 None,
19109 &mut cx,
19110 );
19111}
19112
19113#[gpui::test]
19114async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
19115 let (buffer_id, mut cx) = setup_indent_guides_editor(
19116 &"
19117 function component() {
19118 \treturn (
19119 \t
19120 \t\t<div>
19121 \t\t\t<abc></abc>
19122 \t\t</div>
19123 \t)
19124 }"
19125 .unindent(),
19126 cx,
19127 )
19128 .await;
19129
19130 assert_indent_guides(
19131 0..8,
19132 vec![
19133 indent_guide(buffer_id, 1, 6, 0),
19134 indent_guide(buffer_id, 2, 5, 1),
19135 indent_guide(buffer_id, 4, 4, 2),
19136 ],
19137 None,
19138 &mut cx,
19139 );
19140}
19141
19142#[gpui::test]
19143async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
19144 let (buffer_id, mut cx) = setup_indent_guides_editor(
19145 &"
19146 block1
19147
19148
19149
19150 block2
19151 "
19152 .unindent(),
19153 cx,
19154 )
19155 .await;
19156
19157 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
19158}
19159
19160#[gpui::test]
19161async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
19162 let (buffer_id, mut cx) = setup_indent_guides_editor(
19163 &"
19164 def a:
19165 \tb = 3
19166 \tif True:
19167 \t\tc = 4
19168 \t\td = 5
19169 \tprint(b)
19170 "
19171 .unindent(),
19172 cx,
19173 )
19174 .await;
19175
19176 assert_indent_guides(
19177 0..6,
19178 vec![
19179 indent_guide(buffer_id, 1, 5, 0),
19180 indent_guide(buffer_id, 3, 4, 1),
19181 ],
19182 None,
19183 &mut cx,
19184 );
19185}
19186
19187#[gpui::test]
19188async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
19189 let (buffer_id, mut cx) = setup_indent_guides_editor(
19190 &"
19191 fn main() {
19192 let a = 1;
19193 }"
19194 .unindent(),
19195 cx,
19196 )
19197 .await;
19198
19199 cx.update_editor(|editor, window, cx| {
19200 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19201 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
19202 });
19203 });
19204
19205 assert_indent_guides(
19206 0..3,
19207 vec![indent_guide(buffer_id, 1, 1, 0)],
19208 Some(vec![0]),
19209 &mut cx,
19210 );
19211}
19212
19213#[gpui::test]
19214async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
19215 let (buffer_id, mut cx) = setup_indent_guides_editor(
19216 &"
19217 fn main() {
19218 if 1 == 2 {
19219 let a = 1;
19220 }
19221 }"
19222 .unindent(),
19223 cx,
19224 )
19225 .await;
19226
19227 cx.update_editor(|editor, window, cx| {
19228 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19229 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
19230 });
19231 });
19232
19233 assert_indent_guides(
19234 0..4,
19235 vec![
19236 indent_guide(buffer_id, 1, 3, 0),
19237 indent_guide(buffer_id, 2, 2, 1),
19238 ],
19239 Some(vec![1]),
19240 &mut cx,
19241 );
19242
19243 cx.update_editor(|editor, window, cx| {
19244 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19245 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
19246 });
19247 });
19248
19249 assert_indent_guides(
19250 0..4,
19251 vec![
19252 indent_guide(buffer_id, 1, 3, 0),
19253 indent_guide(buffer_id, 2, 2, 1),
19254 ],
19255 Some(vec![1]),
19256 &mut cx,
19257 );
19258
19259 cx.update_editor(|editor, window, cx| {
19260 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19261 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
19262 });
19263 });
19264
19265 assert_indent_guides(
19266 0..4,
19267 vec![
19268 indent_guide(buffer_id, 1, 3, 0),
19269 indent_guide(buffer_id, 2, 2, 1),
19270 ],
19271 Some(vec![0]),
19272 &mut cx,
19273 );
19274}
19275
19276#[gpui::test]
19277async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
19278 let (buffer_id, mut cx) = setup_indent_guides_editor(
19279 &"
19280 fn main() {
19281 let a = 1;
19282
19283 let b = 2;
19284 }"
19285 .unindent(),
19286 cx,
19287 )
19288 .await;
19289
19290 cx.update_editor(|editor, window, cx| {
19291 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19292 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
19293 });
19294 });
19295
19296 assert_indent_guides(
19297 0..5,
19298 vec![indent_guide(buffer_id, 1, 3, 0)],
19299 Some(vec![0]),
19300 &mut cx,
19301 );
19302}
19303
19304#[gpui::test]
19305async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
19306 let (buffer_id, mut cx) = setup_indent_guides_editor(
19307 &"
19308 def m:
19309 a = 1
19310 pass"
19311 .unindent(),
19312 cx,
19313 )
19314 .await;
19315
19316 cx.update_editor(|editor, window, cx| {
19317 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19318 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
19319 });
19320 });
19321
19322 assert_indent_guides(
19323 0..3,
19324 vec![indent_guide(buffer_id, 1, 2, 0)],
19325 Some(vec![0]),
19326 &mut cx,
19327 );
19328}
19329
19330#[gpui::test]
19331async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
19332 init_test(cx, |_| {});
19333 let mut cx = EditorTestContext::new(cx).await;
19334 let text = indoc! {
19335 "
19336 impl A {
19337 fn b() {
19338 0;
19339 3;
19340 5;
19341 6;
19342 7;
19343 }
19344 }
19345 "
19346 };
19347 let base_text = indoc! {
19348 "
19349 impl A {
19350 fn b() {
19351 0;
19352 1;
19353 2;
19354 3;
19355 4;
19356 }
19357 fn c() {
19358 5;
19359 6;
19360 7;
19361 }
19362 }
19363 "
19364 };
19365
19366 cx.update_editor(|editor, window, cx| {
19367 editor.set_text(text, window, cx);
19368
19369 editor.buffer().update(cx, |multibuffer, cx| {
19370 let buffer = multibuffer.as_singleton().unwrap();
19371 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
19372
19373 multibuffer.set_all_diff_hunks_expanded(cx);
19374 multibuffer.add_diff(diff, cx);
19375
19376 buffer.read(cx).remote_id()
19377 })
19378 });
19379 cx.run_until_parked();
19380
19381 cx.assert_state_with_diff(
19382 indoc! { "
19383 impl A {
19384 fn b() {
19385 0;
19386 - 1;
19387 - 2;
19388 3;
19389 - 4;
19390 - }
19391 - fn c() {
19392 5;
19393 6;
19394 7;
19395 }
19396 }
19397 ˇ"
19398 }
19399 .to_string(),
19400 );
19401
19402 let mut actual_guides = cx.update_editor(|editor, window, cx| {
19403 editor
19404 .snapshot(window, cx)
19405 .buffer_snapshot
19406 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
19407 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
19408 .collect::<Vec<_>>()
19409 });
19410 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
19411 assert_eq!(
19412 actual_guides,
19413 vec![
19414 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
19415 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
19416 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
19417 ]
19418 );
19419}
19420
19421#[gpui::test]
19422async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19423 init_test(cx, |_| {});
19424 let mut cx = EditorTestContext::new(cx).await;
19425
19426 let diff_base = r#"
19427 a
19428 b
19429 c
19430 "#
19431 .unindent();
19432
19433 cx.set_state(
19434 &r#"
19435 ˇA
19436 b
19437 C
19438 "#
19439 .unindent(),
19440 );
19441 cx.set_head_text(&diff_base);
19442 cx.update_editor(|editor, window, cx| {
19443 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19444 });
19445 executor.run_until_parked();
19446
19447 let both_hunks_expanded = r#"
19448 - a
19449 + ˇA
19450 b
19451 - c
19452 + C
19453 "#
19454 .unindent();
19455
19456 cx.assert_state_with_diff(both_hunks_expanded.clone());
19457
19458 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19459 let snapshot = editor.snapshot(window, cx);
19460 let hunks = editor
19461 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19462 .collect::<Vec<_>>();
19463 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19464 let buffer_id = hunks[0].buffer_id;
19465 hunks
19466 .into_iter()
19467 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19468 .collect::<Vec<_>>()
19469 });
19470 assert_eq!(hunk_ranges.len(), 2);
19471
19472 cx.update_editor(|editor, _, cx| {
19473 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19474 });
19475 executor.run_until_parked();
19476
19477 let second_hunk_expanded = r#"
19478 ˇA
19479 b
19480 - c
19481 + C
19482 "#
19483 .unindent();
19484
19485 cx.assert_state_with_diff(second_hunk_expanded);
19486
19487 cx.update_editor(|editor, _, cx| {
19488 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19489 });
19490 executor.run_until_parked();
19491
19492 cx.assert_state_with_diff(both_hunks_expanded.clone());
19493
19494 cx.update_editor(|editor, _, cx| {
19495 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19496 });
19497 executor.run_until_parked();
19498
19499 let first_hunk_expanded = r#"
19500 - a
19501 + ˇA
19502 b
19503 C
19504 "#
19505 .unindent();
19506
19507 cx.assert_state_with_diff(first_hunk_expanded);
19508
19509 cx.update_editor(|editor, _, cx| {
19510 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19511 });
19512 executor.run_until_parked();
19513
19514 cx.assert_state_with_diff(both_hunks_expanded);
19515
19516 cx.set_state(
19517 &r#"
19518 ˇA
19519 b
19520 "#
19521 .unindent(),
19522 );
19523 cx.run_until_parked();
19524
19525 // TODO this cursor position seems bad
19526 cx.assert_state_with_diff(
19527 r#"
19528 - ˇa
19529 + A
19530 b
19531 "#
19532 .unindent(),
19533 );
19534
19535 cx.update_editor(|editor, window, cx| {
19536 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19537 });
19538
19539 cx.assert_state_with_diff(
19540 r#"
19541 - ˇa
19542 + A
19543 b
19544 - c
19545 "#
19546 .unindent(),
19547 );
19548
19549 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19550 let snapshot = editor.snapshot(window, cx);
19551 let hunks = editor
19552 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19553 .collect::<Vec<_>>();
19554 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19555 let buffer_id = hunks[0].buffer_id;
19556 hunks
19557 .into_iter()
19558 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19559 .collect::<Vec<_>>()
19560 });
19561 assert_eq!(hunk_ranges.len(), 2);
19562
19563 cx.update_editor(|editor, _, cx| {
19564 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19565 });
19566 executor.run_until_parked();
19567
19568 cx.assert_state_with_diff(
19569 r#"
19570 - ˇa
19571 + A
19572 b
19573 "#
19574 .unindent(),
19575 );
19576}
19577
19578#[gpui::test]
19579async fn test_toggle_deletion_hunk_at_start_of_file(
19580 executor: BackgroundExecutor,
19581 cx: &mut TestAppContext,
19582) {
19583 init_test(cx, |_| {});
19584 let mut cx = EditorTestContext::new(cx).await;
19585
19586 let diff_base = r#"
19587 a
19588 b
19589 c
19590 "#
19591 .unindent();
19592
19593 cx.set_state(
19594 &r#"
19595 ˇb
19596 c
19597 "#
19598 .unindent(),
19599 );
19600 cx.set_head_text(&diff_base);
19601 cx.update_editor(|editor, window, cx| {
19602 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19603 });
19604 executor.run_until_parked();
19605
19606 let hunk_expanded = r#"
19607 - a
19608 ˇb
19609 c
19610 "#
19611 .unindent();
19612
19613 cx.assert_state_with_diff(hunk_expanded.clone());
19614
19615 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19616 let snapshot = editor.snapshot(window, cx);
19617 let hunks = editor
19618 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19619 .collect::<Vec<_>>();
19620 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19621 let buffer_id = hunks[0].buffer_id;
19622 hunks
19623 .into_iter()
19624 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19625 .collect::<Vec<_>>()
19626 });
19627 assert_eq!(hunk_ranges.len(), 1);
19628
19629 cx.update_editor(|editor, _, cx| {
19630 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19631 });
19632 executor.run_until_parked();
19633
19634 let hunk_collapsed = r#"
19635 ˇb
19636 c
19637 "#
19638 .unindent();
19639
19640 cx.assert_state_with_diff(hunk_collapsed);
19641
19642 cx.update_editor(|editor, _, cx| {
19643 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19644 });
19645 executor.run_until_parked();
19646
19647 cx.assert_state_with_diff(hunk_expanded.clone());
19648}
19649
19650#[gpui::test]
19651async fn test_display_diff_hunks(cx: &mut TestAppContext) {
19652 init_test(cx, |_| {});
19653
19654 let fs = FakeFs::new(cx.executor());
19655 fs.insert_tree(
19656 path!("/test"),
19657 json!({
19658 ".git": {},
19659 "file-1": "ONE\n",
19660 "file-2": "TWO\n",
19661 "file-3": "THREE\n",
19662 }),
19663 )
19664 .await;
19665
19666 fs.set_head_for_repo(
19667 path!("/test/.git").as_ref(),
19668 &[
19669 ("file-1".into(), "one\n".into()),
19670 ("file-2".into(), "two\n".into()),
19671 ("file-3".into(), "three\n".into()),
19672 ],
19673 "deadbeef",
19674 );
19675
19676 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
19677 let mut buffers = vec![];
19678 for i in 1..=3 {
19679 let buffer = project
19680 .update(cx, |project, cx| {
19681 let path = format!(path!("/test/file-{}"), i);
19682 project.open_local_buffer(path, cx)
19683 })
19684 .await
19685 .unwrap();
19686 buffers.push(buffer);
19687 }
19688
19689 let multibuffer = cx.new(|cx| {
19690 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
19691 multibuffer.set_all_diff_hunks_expanded(cx);
19692 for buffer in &buffers {
19693 let snapshot = buffer.read(cx).snapshot();
19694 multibuffer.set_excerpts_for_path(
19695 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
19696 buffer.clone(),
19697 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
19698 DEFAULT_MULTIBUFFER_CONTEXT,
19699 cx,
19700 );
19701 }
19702 multibuffer
19703 });
19704
19705 let editor = cx.add_window(|window, cx| {
19706 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
19707 });
19708 cx.run_until_parked();
19709
19710 let snapshot = editor
19711 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19712 .unwrap();
19713 let hunks = snapshot
19714 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
19715 .map(|hunk| match hunk {
19716 DisplayDiffHunk::Unfolded {
19717 display_row_range, ..
19718 } => display_row_range,
19719 DisplayDiffHunk::Folded { .. } => unreachable!(),
19720 })
19721 .collect::<Vec<_>>();
19722 assert_eq!(
19723 hunks,
19724 [
19725 DisplayRow(2)..DisplayRow(4),
19726 DisplayRow(7)..DisplayRow(9),
19727 DisplayRow(12)..DisplayRow(14),
19728 ]
19729 );
19730}
19731
19732#[gpui::test]
19733async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
19734 init_test(cx, |_| {});
19735
19736 let mut cx = EditorTestContext::new(cx).await;
19737 cx.set_head_text(indoc! { "
19738 one
19739 two
19740 three
19741 four
19742 five
19743 "
19744 });
19745 cx.set_index_text(indoc! { "
19746 one
19747 two
19748 three
19749 four
19750 five
19751 "
19752 });
19753 cx.set_state(indoc! {"
19754 one
19755 TWO
19756 ˇTHREE
19757 FOUR
19758 five
19759 "});
19760 cx.run_until_parked();
19761 cx.update_editor(|editor, window, cx| {
19762 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19763 });
19764 cx.run_until_parked();
19765 cx.assert_index_text(Some(indoc! {"
19766 one
19767 TWO
19768 THREE
19769 FOUR
19770 five
19771 "}));
19772 cx.set_state(indoc! { "
19773 one
19774 TWO
19775 ˇTHREE-HUNDRED
19776 FOUR
19777 five
19778 "});
19779 cx.run_until_parked();
19780 cx.update_editor(|editor, window, cx| {
19781 let snapshot = editor.snapshot(window, cx);
19782 let hunks = editor
19783 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19784 .collect::<Vec<_>>();
19785 assert_eq!(hunks.len(), 1);
19786 assert_eq!(
19787 hunks[0].status(),
19788 DiffHunkStatus {
19789 kind: DiffHunkStatusKind::Modified,
19790 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
19791 }
19792 );
19793
19794 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19795 });
19796 cx.run_until_parked();
19797 cx.assert_index_text(Some(indoc! {"
19798 one
19799 TWO
19800 THREE-HUNDRED
19801 FOUR
19802 five
19803 "}));
19804}
19805
19806#[gpui::test]
19807fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
19808 init_test(cx, |_| {});
19809
19810 let editor = cx.add_window(|window, cx| {
19811 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
19812 build_editor(buffer, window, cx)
19813 });
19814
19815 let render_args = Arc::new(Mutex::new(None));
19816 let snapshot = editor
19817 .update(cx, |editor, window, cx| {
19818 let snapshot = editor.buffer().read(cx).snapshot(cx);
19819 let range =
19820 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
19821
19822 struct RenderArgs {
19823 row: MultiBufferRow,
19824 folded: bool,
19825 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
19826 }
19827
19828 let crease = Crease::inline(
19829 range,
19830 FoldPlaceholder::test(),
19831 {
19832 let toggle_callback = render_args.clone();
19833 move |row, folded, callback, _window, _cx| {
19834 *toggle_callback.lock() = Some(RenderArgs {
19835 row,
19836 folded,
19837 callback,
19838 });
19839 div()
19840 }
19841 },
19842 |_row, _folded, _window, _cx| div(),
19843 );
19844
19845 editor.insert_creases(Some(crease), cx);
19846 let snapshot = editor.snapshot(window, cx);
19847 let _div =
19848 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
19849 snapshot
19850 })
19851 .unwrap();
19852
19853 let render_args = render_args.lock().take().unwrap();
19854 assert_eq!(render_args.row, MultiBufferRow(1));
19855 assert!(!render_args.folded);
19856 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19857
19858 cx.update_window(*editor, |_, window, cx| {
19859 (render_args.callback)(true, window, cx)
19860 })
19861 .unwrap();
19862 let snapshot = editor
19863 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19864 .unwrap();
19865 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
19866
19867 cx.update_window(*editor, |_, window, cx| {
19868 (render_args.callback)(false, window, cx)
19869 })
19870 .unwrap();
19871 let snapshot = editor
19872 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19873 .unwrap();
19874 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19875}
19876
19877#[gpui::test]
19878async fn test_input_text(cx: &mut TestAppContext) {
19879 init_test(cx, |_| {});
19880 let mut cx = EditorTestContext::new(cx).await;
19881
19882 cx.set_state(
19883 &r#"ˇone
19884 two
19885
19886 three
19887 fourˇ
19888 five
19889
19890 siˇx"#
19891 .unindent(),
19892 );
19893
19894 cx.dispatch_action(HandleInput(String::new()));
19895 cx.assert_editor_state(
19896 &r#"ˇone
19897 two
19898
19899 three
19900 fourˇ
19901 five
19902
19903 siˇx"#
19904 .unindent(),
19905 );
19906
19907 cx.dispatch_action(HandleInput("AAAA".to_string()));
19908 cx.assert_editor_state(
19909 &r#"AAAAˇone
19910 two
19911
19912 three
19913 fourAAAAˇ
19914 five
19915
19916 siAAAAˇx"#
19917 .unindent(),
19918 );
19919}
19920
19921#[gpui::test]
19922async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
19923 init_test(cx, |_| {});
19924
19925 let mut cx = EditorTestContext::new(cx).await;
19926 cx.set_state(
19927 r#"let foo = 1;
19928let foo = 2;
19929let foo = 3;
19930let fooˇ = 4;
19931let foo = 5;
19932let foo = 6;
19933let foo = 7;
19934let foo = 8;
19935let foo = 9;
19936let foo = 10;
19937let foo = 11;
19938let foo = 12;
19939let foo = 13;
19940let foo = 14;
19941let foo = 15;"#,
19942 );
19943
19944 cx.update_editor(|e, window, cx| {
19945 assert_eq!(
19946 e.next_scroll_position,
19947 NextScrollCursorCenterTopBottom::Center,
19948 "Default next scroll direction is center",
19949 );
19950
19951 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19952 assert_eq!(
19953 e.next_scroll_position,
19954 NextScrollCursorCenterTopBottom::Top,
19955 "After center, next scroll direction should be top",
19956 );
19957
19958 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19959 assert_eq!(
19960 e.next_scroll_position,
19961 NextScrollCursorCenterTopBottom::Bottom,
19962 "After top, next scroll direction should be bottom",
19963 );
19964
19965 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19966 assert_eq!(
19967 e.next_scroll_position,
19968 NextScrollCursorCenterTopBottom::Center,
19969 "After bottom, scrolling should start over",
19970 );
19971
19972 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19973 assert_eq!(
19974 e.next_scroll_position,
19975 NextScrollCursorCenterTopBottom::Top,
19976 "Scrolling continues if retriggered fast enough"
19977 );
19978 });
19979
19980 cx.executor()
19981 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19982 cx.executor().run_until_parked();
19983 cx.update_editor(|e, _, _| {
19984 assert_eq!(
19985 e.next_scroll_position,
19986 NextScrollCursorCenterTopBottom::Center,
19987 "If scrolling is not triggered fast enough, it should reset"
19988 );
19989 });
19990}
19991
19992#[gpui::test]
19993async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19994 init_test(cx, |_| {});
19995 let mut cx = EditorLspTestContext::new_rust(
19996 lsp::ServerCapabilities {
19997 definition_provider: Some(lsp::OneOf::Left(true)),
19998 references_provider: Some(lsp::OneOf::Left(true)),
19999 ..lsp::ServerCapabilities::default()
20000 },
20001 cx,
20002 )
20003 .await;
20004
20005 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
20006 let go_to_definition = cx
20007 .lsp
20008 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
20009 move |params, _| async move {
20010 if empty_go_to_definition {
20011 Ok(None)
20012 } else {
20013 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
20014 uri: params.text_document_position_params.text_document.uri,
20015 range: lsp::Range::new(
20016 lsp::Position::new(4, 3),
20017 lsp::Position::new(4, 6),
20018 ),
20019 })))
20020 }
20021 },
20022 );
20023 let references = cx
20024 .lsp
20025 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
20026 Ok(Some(vec![lsp::Location {
20027 uri: params.text_document_position.text_document.uri,
20028 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
20029 }]))
20030 });
20031 (go_to_definition, references)
20032 };
20033
20034 cx.set_state(
20035 &r#"fn one() {
20036 let mut a = ˇtwo();
20037 }
20038
20039 fn two() {}"#
20040 .unindent(),
20041 );
20042 set_up_lsp_handlers(false, &mut cx);
20043 let navigated = cx
20044 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
20045 .await
20046 .expect("Failed to navigate to definition");
20047 assert_eq!(
20048 navigated,
20049 Navigated::Yes,
20050 "Should have navigated to definition from the GetDefinition response"
20051 );
20052 cx.assert_editor_state(
20053 &r#"fn one() {
20054 let mut a = two();
20055 }
20056
20057 fn «twoˇ»() {}"#
20058 .unindent(),
20059 );
20060
20061 let editors = cx.update_workspace(|workspace, _, cx| {
20062 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
20063 });
20064 cx.update_editor(|_, _, test_editor_cx| {
20065 assert_eq!(
20066 editors.len(),
20067 1,
20068 "Initially, only one, test, editor should be open in the workspace"
20069 );
20070 assert_eq!(
20071 test_editor_cx.entity(),
20072 editors.last().expect("Asserted len is 1").clone()
20073 );
20074 });
20075
20076 set_up_lsp_handlers(true, &mut cx);
20077 let navigated = cx
20078 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
20079 .await
20080 .expect("Failed to navigate to lookup references");
20081 assert_eq!(
20082 navigated,
20083 Navigated::Yes,
20084 "Should have navigated to references as a fallback after empty GoToDefinition response"
20085 );
20086 // We should not change the selections in the existing file,
20087 // if opening another milti buffer with the references
20088 cx.assert_editor_state(
20089 &r#"fn one() {
20090 let mut a = two();
20091 }
20092
20093 fn «twoˇ»() {}"#
20094 .unindent(),
20095 );
20096 let editors = cx.update_workspace(|workspace, _, cx| {
20097 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
20098 });
20099 cx.update_editor(|_, _, test_editor_cx| {
20100 assert_eq!(
20101 editors.len(),
20102 2,
20103 "After falling back to references search, we open a new editor with the results"
20104 );
20105 let references_fallback_text = editors
20106 .into_iter()
20107 .find(|new_editor| *new_editor != test_editor_cx.entity())
20108 .expect("Should have one non-test editor now")
20109 .read(test_editor_cx)
20110 .text(test_editor_cx);
20111 assert_eq!(
20112 references_fallback_text, "fn one() {\n let mut a = two();\n}",
20113 "Should use the range from the references response and not the GoToDefinition one"
20114 );
20115 });
20116}
20117
20118#[gpui::test]
20119async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
20120 init_test(cx, |_| {});
20121 cx.update(|cx| {
20122 let mut editor_settings = EditorSettings::get_global(cx).clone();
20123 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
20124 EditorSettings::override_global(editor_settings, cx);
20125 });
20126 let mut cx = EditorLspTestContext::new_rust(
20127 lsp::ServerCapabilities {
20128 definition_provider: Some(lsp::OneOf::Left(true)),
20129 references_provider: Some(lsp::OneOf::Left(true)),
20130 ..lsp::ServerCapabilities::default()
20131 },
20132 cx,
20133 )
20134 .await;
20135 let original_state = r#"fn one() {
20136 let mut a = ˇtwo();
20137 }
20138
20139 fn two() {}"#
20140 .unindent();
20141 cx.set_state(&original_state);
20142
20143 let mut go_to_definition = cx
20144 .lsp
20145 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
20146 move |_, _| async move { Ok(None) },
20147 );
20148 let _references = cx
20149 .lsp
20150 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
20151 panic!("Should not call for references with no go to definition fallback")
20152 });
20153
20154 let navigated = cx
20155 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
20156 .await
20157 .expect("Failed to navigate to lookup references");
20158 go_to_definition
20159 .next()
20160 .await
20161 .expect("Should have called the go_to_definition handler");
20162
20163 assert_eq!(
20164 navigated,
20165 Navigated::No,
20166 "Should have navigated to references as a fallback after empty GoToDefinition response"
20167 );
20168 cx.assert_editor_state(&original_state);
20169 let editors = cx.update_workspace(|workspace, _, cx| {
20170 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
20171 });
20172 cx.update_editor(|_, _, _| {
20173 assert_eq!(
20174 editors.len(),
20175 1,
20176 "After unsuccessful fallback, no other editor should have been opened"
20177 );
20178 });
20179}
20180
20181#[gpui::test]
20182async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
20183 init_test(cx, |_| {});
20184
20185 let language = Arc::new(Language::new(
20186 LanguageConfig::default(),
20187 Some(tree_sitter_rust::LANGUAGE.into()),
20188 ));
20189
20190 let text = r#"
20191 #[cfg(test)]
20192 mod tests() {
20193 #[test]
20194 fn runnable_1() {
20195 let a = 1;
20196 }
20197
20198 #[test]
20199 fn runnable_2() {
20200 let a = 1;
20201 let b = 2;
20202 }
20203 }
20204 "#
20205 .unindent();
20206
20207 let fs = FakeFs::new(cx.executor());
20208 fs.insert_file("/file.rs", Default::default()).await;
20209
20210 let project = Project::test(fs, ["/a".as_ref()], cx).await;
20211 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20212 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20213 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
20214 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
20215
20216 let editor = cx.new_window_entity(|window, cx| {
20217 Editor::new(
20218 EditorMode::full(),
20219 multi_buffer,
20220 Some(project.clone()),
20221 window,
20222 cx,
20223 )
20224 });
20225
20226 editor.update_in(cx, |editor, window, cx| {
20227 let snapshot = editor.buffer().read(cx).snapshot(cx);
20228 editor.tasks.insert(
20229 (buffer.read(cx).remote_id(), 3),
20230 RunnableTasks {
20231 templates: vec![],
20232 offset: snapshot.anchor_before(43),
20233 column: 0,
20234 extra_variables: HashMap::default(),
20235 context_range: BufferOffset(43)..BufferOffset(85),
20236 },
20237 );
20238 editor.tasks.insert(
20239 (buffer.read(cx).remote_id(), 8),
20240 RunnableTasks {
20241 templates: vec![],
20242 offset: snapshot.anchor_before(86),
20243 column: 0,
20244 extra_variables: HashMap::default(),
20245 context_range: BufferOffset(86)..BufferOffset(191),
20246 },
20247 );
20248
20249 // Test finding task when cursor is inside function body
20250 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20251 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
20252 });
20253 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
20254 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
20255
20256 // Test finding task when cursor is on function name
20257 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20258 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
20259 });
20260 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
20261 assert_eq!(row, 8, "Should find task when cursor is on function name");
20262 });
20263}
20264
20265#[gpui::test]
20266async fn test_folding_buffers(cx: &mut TestAppContext) {
20267 init_test(cx, |_| {});
20268
20269 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20270 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
20271 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
20272
20273 let fs = FakeFs::new(cx.executor());
20274 fs.insert_tree(
20275 path!("/a"),
20276 json!({
20277 "first.rs": sample_text_1,
20278 "second.rs": sample_text_2,
20279 "third.rs": sample_text_3,
20280 }),
20281 )
20282 .await;
20283 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20284 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20285 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20286 let worktree = project.update(cx, |project, cx| {
20287 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20288 assert_eq!(worktrees.len(), 1);
20289 worktrees.pop().unwrap()
20290 });
20291 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20292
20293 let buffer_1 = project
20294 .update(cx, |project, cx| {
20295 project.open_buffer((worktree_id, "first.rs"), cx)
20296 })
20297 .await
20298 .unwrap();
20299 let buffer_2 = project
20300 .update(cx, |project, cx| {
20301 project.open_buffer((worktree_id, "second.rs"), cx)
20302 })
20303 .await
20304 .unwrap();
20305 let buffer_3 = project
20306 .update(cx, |project, cx| {
20307 project.open_buffer((worktree_id, "third.rs"), cx)
20308 })
20309 .await
20310 .unwrap();
20311
20312 let multi_buffer = cx.new(|cx| {
20313 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20314 multi_buffer.push_excerpts(
20315 buffer_1.clone(),
20316 [
20317 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20318 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20319 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20320 ],
20321 cx,
20322 );
20323 multi_buffer.push_excerpts(
20324 buffer_2.clone(),
20325 [
20326 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20327 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20328 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20329 ],
20330 cx,
20331 );
20332 multi_buffer.push_excerpts(
20333 buffer_3.clone(),
20334 [
20335 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20336 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20337 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20338 ],
20339 cx,
20340 );
20341 multi_buffer
20342 });
20343 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20344 Editor::new(
20345 EditorMode::full(),
20346 multi_buffer.clone(),
20347 Some(project.clone()),
20348 window,
20349 cx,
20350 )
20351 });
20352
20353 assert_eq!(
20354 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20355 "\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",
20356 );
20357
20358 multi_buffer_editor.update(cx, |editor, cx| {
20359 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20360 });
20361 assert_eq!(
20362 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20363 "\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",
20364 "After folding the first buffer, its text should not be displayed"
20365 );
20366
20367 multi_buffer_editor.update(cx, |editor, cx| {
20368 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20369 });
20370 assert_eq!(
20371 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20372 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
20373 "After folding the second buffer, its text should not be displayed"
20374 );
20375
20376 multi_buffer_editor.update(cx, |editor, cx| {
20377 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20378 });
20379 assert_eq!(
20380 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20381 "\n\n\n\n\n",
20382 "After folding the third buffer, its text should not be displayed"
20383 );
20384
20385 // Emulate selection inside the fold logic, that should work
20386 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20387 editor
20388 .snapshot(window, cx)
20389 .next_line_boundary(Point::new(0, 4));
20390 });
20391
20392 multi_buffer_editor.update(cx, |editor, cx| {
20393 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20394 });
20395 assert_eq!(
20396 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20397 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
20398 "After unfolding the second buffer, its text should be displayed"
20399 );
20400
20401 // Typing inside of buffer 1 causes that buffer to be unfolded.
20402 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20403 assert_eq!(
20404 multi_buffer
20405 .read(cx)
20406 .snapshot(cx)
20407 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
20408 .collect::<String>(),
20409 "bbbb"
20410 );
20411 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
20412 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
20413 });
20414 editor.handle_input("B", window, cx);
20415 });
20416
20417 assert_eq!(
20418 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20419 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
20420 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
20421 );
20422
20423 multi_buffer_editor.update(cx, |editor, cx| {
20424 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20425 });
20426 assert_eq!(
20427 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20428 "\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",
20429 "After unfolding the all buffers, all original text should be displayed"
20430 );
20431}
20432
20433#[gpui::test]
20434async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
20435 init_test(cx, |_| {});
20436
20437 let sample_text_1 = "1111\n2222\n3333".to_string();
20438 let sample_text_2 = "4444\n5555\n6666".to_string();
20439 let sample_text_3 = "7777\n8888\n9999".to_string();
20440
20441 let fs = FakeFs::new(cx.executor());
20442 fs.insert_tree(
20443 path!("/a"),
20444 json!({
20445 "first.rs": sample_text_1,
20446 "second.rs": sample_text_2,
20447 "third.rs": sample_text_3,
20448 }),
20449 )
20450 .await;
20451 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20452 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20453 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20454 let worktree = project.update(cx, |project, cx| {
20455 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20456 assert_eq!(worktrees.len(), 1);
20457 worktrees.pop().unwrap()
20458 });
20459 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20460
20461 let buffer_1 = project
20462 .update(cx, |project, cx| {
20463 project.open_buffer((worktree_id, "first.rs"), cx)
20464 })
20465 .await
20466 .unwrap();
20467 let buffer_2 = project
20468 .update(cx, |project, cx| {
20469 project.open_buffer((worktree_id, "second.rs"), cx)
20470 })
20471 .await
20472 .unwrap();
20473 let buffer_3 = project
20474 .update(cx, |project, cx| {
20475 project.open_buffer((worktree_id, "third.rs"), cx)
20476 })
20477 .await
20478 .unwrap();
20479
20480 let multi_buffer = cx.new(|cx| {
20481 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20482 multi_buffer.push_excerpts(
20483 buffer_1.clone(),
20484 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20485 cx,
20486 );
20487 multi_buffer.push_excerpts(
20488 buffer_2.clone(),
20489 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20490 cx,
20491 );
20492 multi_buffer.push_excerpts(
20493 buffer_3.clone(),
20494 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20495 cx,
20496 );
20497 multi_buffer
20498 });
20499
20500 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20501 Editor::new(
20502 EditorMode::full(),
20503 multi_buffer,
20504 Some(project.clone()),
20505 window,
20506 cx,
20507 )
20508 });
20509
20510 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
20511 assert_eq!(
20512 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20513 full_text,
20514 );
20515
20516 multi_buffer_editor.update(cx, |editor, cx| {
20517 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20518 });
20519 assert_eq!(
20520 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20521 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
20522 "After folding the first buffer, its text should not be displayed"
20523 );
20524
20525 multi_buffer_editor.update(cx, |editor, cx| {
20526 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20527 });
20528
20529 assert_eq!(
20530 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20531 "\n\n\n\n\n\n7777\n8888\n9999",
20532 "After folding the second buffer, its text should not be displayed"
20533 );
20534
20535 multi_buffer_editor.update(cx, |editor, cx| {
20536 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20537 });
20538 assert_eq!(
20539 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20540 "\n\n\n\n\n",
20541 "After folding the third buffer, its text should not be displayed"
20542 );
20543
20544 multi_buffer_editor.update(cx, |editor, cx| {
20545 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20546 });
20547 assert_eq!(
20548 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20549 "\n\n\n\n4444\n5555\n6666\n\n",
20550 "After unfolding the second buffer, its text should be displayed"
20551 );
20552
20553 multi_buffer_editor.update(cx, |editor, cx| {
20554 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
20555 });
20556 assert_eq!(
20557 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20558 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
20559 "After unfolding the first buffer, its text should be displayed"
20560 );
20561
20562 multi_buffer_editor.update(cx, |editor, cx| {
20563 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20564 });
20565 assert_eq!(
20566 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20567 full_text,
20568 "After unfolding all buffers, all original text should be displayed"
20569 );
20570}
20571
20572#[gpui::test]
20573async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
20574 init_test(cx, |_| {});
20575
20576 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20577
20578 let fs = FakeFs::new(cx.executor());
20579 fs.insert_tree(
20580 path!("/a"),
20581 json!({
20582 "main.rs": sample_text,
20583 }),
20584 )
20585 .await;
20586 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20587 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20588 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20589 let worktree = project.update(cx, |project, cx| {
20590 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20591 assert_eq!(worktrees.len(), 1);
20592 worktrees.pop().unwrap()
20593 });
20594 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20595
20596 let buffer_1 = project
20597 .update(cx, |project, cx| {
20598 project.open_buffer((worktree_id, "main.rs"), cx)
20599 })
20600 .await
20601 .unwrap();
20602
20603 let multi_buffer = cx.new(|cx| {
20604 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20605 multi_buffer.push_excerpts(
20606 buffer_1.clone(),
20607 [ExcerptRange::new(
20608 Point::new(0, 0)
20609 ..Point::new(
20610 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
20611 0,
20612 ),
20613 )],
20614 cx,
20615 );
20616 multi_buffer
20617 });
20618 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20619 Editor::new(
20620 EditorMode::full(),
20621 multi_buffer,
20622 Some(project.clone()),
20623 window,
20624 cx,
20625 )
20626 });
20627
20628 let selection_range = Point::new(1, 0)..Point::new(2, 0);
20629 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20630 enum TestHighlight {}
20631 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
20632 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
20633 editor.highlight_text::<TestHighlight>(
20634 vec![highlight_range.clone()],
20635 HighlightStyle::color(Hsla::green()),
20636 cx,
20637 );
20638 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20639 s.select_ranges(Some(highlight_range))
20640 });
20641 });
20642
20643 let full_text = format!("\n\n{sample_text}");
20644 assert_eq!(
20645 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20646 full_text,
20647 );
20648}
20649
20650#[gpui::test]
20651async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
20652 init_test(cx, |_| {});
20653 cx.update(|cx| {
20654 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
20655 "keymaps/default-linux.json",
20656 cx,
20657 )
20658 .unwrap();
20659 cx.bind_keys(default_key_bindings);
20660 });
20661
20662 let (editor, cx) = cx.add_window_view(|window, cx| {
20663 let multi_buffer = MultiBuffer::build_multi(
20664 [
20665 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
20666 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
20667 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
20668 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
20669 ],
20670 cx,
20671 );
20672 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
20673
20674 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
20675 // fold all but the second buffer, so that we test navigating between two
20676 // adjacent folded buffers, as well as folded buffers at the start and
20677 // end the multibuffer
20678 editor.fold_buffer(buffer_ids[0], cx);
20679 editor.fold_buffer(buffer_ids[2], cx);
20680 editor.fold_buffer(buffer_ids[3], cx);
20681
20682 editor
20683 });
20684 cx.simulate_resize(size(px(1000.), px(1000.)));
20685
20686 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
20687 cx.assert_excerpts_with_selections(indoc! {"
20688 [EXCERPT]
20689 ˇ[FOLDED]
20690 [EXCERPT]
20691 a1
20692 b1
20693 [EXCERPT]
20694 [FOLDED]
20695 [EXCERPT]
20696 [FOLDED]
20697 "
20698 });
20699 cx.simulate_keystroke("down");
20700 cx.assert_excerpts_with_selections(indoc! {"
20701 [EXCERPT]
20702 [FOLDED]
20703 [EXCERPT]
20704 ˇa1
20705 b1
20706 [EXCERPT]
20707 [FOLDED]
20708 [EXCERPT]
20709 [FOLDED]
20710 "
20711 });
20712 cx.simulate_keystroke("down");
20713 cx.assert_excerpts_with_selections(indoc! {"
20714 [EXCERPT]
20715 [FOLDED]
20716 [EXCERPT]
20717 a1
20718 ˇb1
20719 [EXCERPT]
20720 [FOLDED]
20721 [EXCERPT]
20722 [FOLDED]
20723 "
20724 });
20725 cx.simulate_keystroke("down");
20726 cx.assert_excerpts_with_selections(indoc! {"
20727 [EXCERPT]
20728 [FOLDED]
20729 [EXCERPT]
20730 a1
20731 b1
20732 ˇ[EXCERPT]
20733 [FOLDED]
20734 [EXCERPT]
20735 [FOLDED]
20736 "
20737 });
20738 cx.simulate_keystroke("down");
20739 cx.assert_excerpts_with_selections(indoc! {"
20740 [EXCERPT]
20741 [FOLDED]
20742 [EXCERPT]
20743 a1
20744 b1
20745 [EXCERPT]
20746 ˇ[FOLDED]
20747 [EXCERPT]
20748 [FOLDED]
20749 "
20750 });
20751 for _ in 0..5 {
20752 cx.simulate_keystroke("down");
20753 cx.assert_excerpts_with_selections(indoc! {"
20754 [EXCERPT]
20755 [FOLDED]
20756 [EXCERPT]
20757 a1
20758 b1
20759 [EXCERPT]
20760 [FOLDED]
20761 [EXCERPT]
20762 ˇ[FOLDED]
20763 "
20764 });
20765 }
20766
20767 cx.simulate_keystroke("up");
20768 cx.assert_excerpts_with_selections(indoc! {"
20769 [EXCERPT]
20770 [FOLDED]
20771 [EXCERPT]
20772 a1
20773 b1
20774 [EXCERPT]
20775 ˇ[FOLDED]
20776 [EXCERPT]
20777 [FOLDED]
20778 "
20779 });
20780 cx.simulate_keystroke("up");
20781 cx.assert_excerpts_with_selections(indoc! {"
20782 [EXCERPT]
20783 [FOLDED]
20784 [EXCERPT]
20785 a1
20786 b1
20787 ˇ[EXCERPT]
20788 [FOLDED]
20789 [EXCERPT]
20790 [FOLDED]
20791 "
20792 });
20793 cx.simulate_keystroke("up");
20794 cx.assert_excerpts_with_selections(indoc! {"
20795 [EXCERPT]
20796 [FOLDED]
20797 [EXCERPT]
20798 a1
20799 ˇb1
20800 [EXCERPT]
20801 [FOLDED]
20802 [EXCERPT]
20803 [FOLDED]
20804 "
20805 });
20806 cx.simulate_keystroke("up");
20807 cx.assert_excerpts_with_selections(indoc! {"
20808 [EXCERPT]
20809 [FOLDED]
20810 [EXCERPT]
20811 ˇa1
20812 b1
20813 [EXCERPT]
20814 [FOLDED]
20815 [EXCERPT]
20816 [FOLDED]
20817 "
20818 });
20819 for _ in 0..5 {
20820 cx.simulate_keystroke("up");
20821 cx.assert_excerpts_with_selections(indoc! {"
20822 [EXCERPT]
20823 ˇ[FOLDED]
20824 [EXCERPT]
20825 a1
20826 b1
20827 [EXCERPT]
20828 [FOLDED]
20829 [EXCERPT]
20830 [FOLDED]
20831 "
20832 });
20833 }
20834}
20835
20836#[gpui::test]
20837async fn test_edit_prediction_text(cx: &mut TestAppContext) {
20838 init_test(cx, |_| {});
20839
20840 // Simple insertion
20841 assert_highlighted_edits(
20842 "Hello, world!",
20843 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
20844 true,
20845 cx,
20846 |highlighted_edits, cx| {
20847 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
20848 assert_eq!(highlighted_edits.highlights.len(), 1);
20849 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
20850 assert_eq!(
20851 highlighted_edits.highlights[0].1.background_color,
20852 Some(cx.theme().status().created_background)
20853 );
20854 },
20855 )
20856 .await;
20857
20858 // Replacement
20859 assert_highlighted_edits(
20860 "This is a test.",
20861 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
20862 false,
20863 cx,
20864 |highlighted_edits, cx| {
20865 assert_eq!(highlighted_edits.text, "That is a test.");
20866 assert_eq!(highlighted_edits.highlights.len(), 1);
20867 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
20868 assert_eq!(
20869 highlighted_edits.highlights[0].1.background_color,
20870 Some(cx.theme().status().created_background)
20871 );
20872 },
20873 )
20874 .await;
20875
20876 // Multiple edits
20877 assert_highlighted_edits(
20878 "Hello, world!",
20879 vec![
20880 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
20881 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
20882 ],
20883 false,
20884 cx,
20885 |highlighted_edits, cx| {
20886 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
20887 assert_eq!(highlighted_edits.highlights.len(), 2);
20888 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
20889 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
20890 assert_eq!(
20891 highlighted_edits.highlights[0].1.background_color,
20892 Some(cx.theme().status().created_background)
20893 );
20894 assert_eq!(
20895 highlighted_edits.highlights[1].1.background_color,
20896 Some(cx.theme().status().created_background)
20897 );
20898 },
20899 )
20900 .await;
20901
20902 // Multiple lines with edits
20903 assert_highlighted_edits(
20904 "First line\nSecond line\nThird line\nFourth line",
20905 vec![
20906 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
20907 (
20908 Point::new(2, 0)..Point::new(2, 10),
20909 "New third line".to_string(),
20910 ),
20911 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
20912 ],
20913 false,
20914 cx,
20915 |highlighted_edits, cx| {
20916 assert_eq!(
20917 highlighted_edits.text,
20918 "Second modified\nNew third line\nFourth updated line"
20919 );
20920 assert_eq!(highlighted_edits.highlights.len(), 3);
20921 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
20922 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
20923 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
20924 for highlight in &highlighted_edits.highlights {
20925 assert_eq!(
20926 highlight.1.background_color,
20927 Some(cx.theme().status().created_background)
20928 );
20929 }
20930 },
20931 )
20932 .await;
20933}
20934
20935#[gpui::test]
20936async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
20937 init_test(cx, |_| {});
20938
20939 // Deletion
20940 assert_highlighted_edits(
20941 "Hello, world!",
20942 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20943 true,
20944 cx,
20945 |highlighted_edits, cx| {
20946 assert_eq!(highlighted_edits.text, "Hello, world!");
20947 assert_eq!(highlighted_edits.highlights.len(), 1);
20948 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20949 assert_eq!(
20950 highlighted_edits.highlights[0].1.background_color,
20951 Some(cx.theme().status().deleted_background)
20952 );
20953 },
20954 )
20955 .await;
20956
20957 // Insertion
20958 assert_highlighted_edits(
20959 "Hello, world!",
20960 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
20961 true,
20962 cx,
20963 |highlighted_edits, cx| {
20964 assert_eq!(highlighted_edits.highlights.len(), 1);
20965 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20966 assert_eq!(
20967 highlighted_edits.highlights[0].1.background_color,
20968 Some(cx.theme().status().created_background)
20969 );
20970 },
20971 )
20972 .await;
20973}
20974
20975async fn assert_highlighted_edits(
20976 text: &str,
20977 edits: Vec<(Range<Point>, String)>,
20978 include_deletions: bool,
20979 cx: &mut TestAppContext,
20980 assertion_fn: impl Fn(HighlightedText, &App),
20981) {
20982 let window = cx.add_window(|window, cx| {
20983 let buffer = MultiBuffer::build_simple(text, cx);
20984 Editor::new(EditorMode::full(), buffer, None, window, cx)
20985 });
20986 let cx = &mut VisualTestContext::from_window(*window, cx);
20987
20988 let (buffer, snapshot) = window
20989 .update(cx, |editor, _window, cx| {
20990 (
20991 editor.buffer().clone(),
20992 editor.buffer().read(cx).snapshot(cx),
20993 )
20994 })
20995 .unwrap();
20996
20997 let edits = edits
20998 .into_iter()
20999 .map(|(range, edit)| {
21000 (
21001 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
21002 edit,
21003 )
21004 })
21005 .collect::<Vec<_>>();
21006
21007 let text_anchor_edits = edits
21008 .clone()
21009 .into_iter()
21010 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
21011 .collect::<Vec<_>>();
21012
21013 let edit_preview = window
21014 .update(cx, |_, _window, cx| {
21015 buffer
21016 .read(cx)
21017 .as_singleton()
21018 .unwrap()
21019 .read(cx)
21020 .preview_edits(text_anchor_edits.into(), cx)
21021 })
21022 .unwrap()
21023 .await;
21024
21025 cx.update(|_window, cx| {
21026 let highlighted_edits = edit_prediction_edit_text(
21027 &snapshot.as_singleton().unwrap().2,
21028 &edits,
21029 &edit_preview,
21030 include_deletions,
21031 cx,
21032 );
21033 assertion_fn(highlighted_edits, cx)
21034 });
21035}
21036
21037#[track_caller]
21038fn assert_breakpoint(
21039 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
21040 path: &Arc<Path>,
21041 expected: Vec<(u32, Breakpoint)>,
21042) {
21043 if expected.len() == 0usize {
21044 assert!(!breakpoints.contains_key(path), "{}", path.display());
21045 } else {
21046 let mut breakpoint = breakpoints
21047 .get(path)
21048 .unwrap()
21049 .into_iter()
21050 .map(|breakpoint| {
21051 (
21052 breakpoint.row,
21053 Breakpoint {
21054 message: breakpoint.message.clone(),
21055 state: breakpoint.state,
21056 condition: breakpoint.condition.clone(),
21057 hit_condition: breakpoint.hit_condition.clone(),
21058 },
21059 )
21060 })
21061 .collect::<Vec<_>>();
21062
21063 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
21064
21065 assert_eq!(expected, breakpoint);
21066 }
21067}
21068
21069fn add_log_breakpoint_at_cursor(
21070 editor: &mut Editor,
21071 log_message: &str,
21072 window: &mut Window,
21073 cx: &mut Context<Editor>,
21074) {
21075 let (anchor, bp) = editor
21076 .breakpoints_at_cursors(window, cx)
21077 .first()
21078 .and_then(|(anchor, bp)| {
21079 if let Some(bp) = bp {
21080 Some((*anchor, bp.clone()))
21081 } else {
21082 None
21083 }
21084 })
21085 .unwrap_or_else(|| {
21086 let cursor_position: Point = editor.selections.newest(cx).head();
21087
21088 let breakpoint_position = editor
21089 .snapshot(window, cx)
21090 .display_snapshot
21091 .buffer_snapshot
21092 .anchor_before(Point::new(cursor_position.row, 0));
21093
21094 (breakpoint_position, Breakpoint::new_log(&log_message))
21095 });
21096
21097 editor.edit_breakpoint_at_anchor(
21098 anchor,
21099 bp,
21100 BreakpointEditAction::EditLogMessage(log_message.into()),
21101 cx,
21102 );
21103}
21104
21105#[gpui::test]
21106async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
21107 init_test(cx, |_| {});
21108
21109 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
21110 let fs = FakeFs::new(cx.executor());
21111 fs.insert_tree(
21112 path!("/a"),
21113 json!({
21114 "main.rs": sample_text,
21115 }),
21116 )
21117 .await;
21118 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21119 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21120 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21121
21122 let fs = FakeFs::new(cx.executor());
21123 fs.insert_tree(
21124 path!("/a"),
21125 json!({
21126 "main.rs": sample_text,
21127 }),
21128 )
21129 .await;
21130 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21131 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21132 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21133 let worktree_id = workspace
21134 .update(cx, |workspace, _window, cx| {
21135 workspace.project().update(cx, |project, cx| {
21136 project.worktrees(cx).next().unwrap().read(cx).id()
21137 })
21138 })
21139 .unwrap();
21140
21141 let buffer = project
21142 .update(cx, |project, cx| {
21143 project.open_buffer((worktree_id, "main.rs"), cx)
21144 })
21145 .await
21146 .unwrap();
21147
21148 let (editor, cx) = cx.add_window_view(|window, cx| {
21149 Editor::new(
21150 EditorMode::full(),
21151 MultiBuffer::build_from_buffer(buffer, cx),
21152 Some(project.clone()),
21153 window,
21154 cx,
21155 )
21156 });
21157
21158 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21159 let abs_path = project.read_with(cx, |project, cx| {
21160 project
21161 .absolute_path(&project_path, cx)
21162 .map(|path_buf| Arc::from(path_buf.to_owned()))
21163 .unwrap()
21164 });
21165
21166 // assert we can add breakpoint on the first line
21167 editor.update_in(cx, |editor, window, cx| {
21168 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21169 editor.move_to_end(&MoveToEnd, window, cx);
21170 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21171 });
21172
21173 let breakpoints = editor.update(cx, |editor, cx| {
21174 editor
21175 .breakpoint_store()
21176 .as_ref()
21177 .unwrap()
21178 .read(cx)
21179 .all_source_breakpoints(cx)
21180 .clone()
21181 });
21182
21183 assert_eq!(1, breakpoints.len());
21184 assert_breakpoint(
21185 &breakpoints,
21186 &abs_path,
21187 vec![
21188 (0, Breakpoint::new_standard()),
21189 (3, Breakpoint::new_standard()),
21190 ],
21191 );
21192
21193 editor.update_in(cx, |editor, window, cx| {
21194 editor.move_to_beginning(&MoveToBeginning, window, cx);
21195 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21196 });
21197
21198 let breakpoints = editor.update(cx, |editor, cx| {
21199 editor
21200 .breakpoint_store()
21201 .as_ref()
21202 .unwrap()
21203 .read(cx)
21204 .all_source_breakpoints(cx)
21205 .clone()
21206 });
21207
21208 assert_eq!(1, breakpoints.len());
21209 assert_breakpoint(
21210 &breakpoints,
21211 &abs_path,
21212 vec![(3, Breakpoint::new_standard())],
21213 );
21214
21215 editor.update_in(cx, |editor, window, cx| {
21216 editor.move_to_end(&MoveToEnd, window, cx);
21217 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21218 });
21219
21220 let breakpoints = editor.update(cx, |editor, cx| {
21221 editor
21222 .breakpoint_store()
21223 .as_ref()
21224 .unwrap()
21225 .read(cx)
21226 .all_source_breakpoints(cx)
21227 .clone()
21228 });
21229
21230 assert_eq!(0, breakpoints.len());
21231 assert_breakpoint(&breakpoints, &abs_path, vec![]);
21232}
21233
21234#[gpui::test]
21235async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
21236 init_test(cx, |_| {});
21237
21238 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
21239
21240 let fs = FakeFs::new(cx.executor());
21241 fs.insert_tree(
21242 path!("/a"),
21243 json!({
21244 "main.rs": sample_text,
21245 }),
21246 )
21247 .await;
21248 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21249 let (workspace, cx) =
21250 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21251
21252 let worktree_id = workspace.update(cx, |workspace, cx| {
21253 workspace.project().update(cx, |project, cx| {
21254 project.worktrees(cx).next().unwrap().read(cx).id()
21255 })
21256 });
21257
21258 let buffer = project
21259 .update(cx, |project, cx| {
21260 project.open_buffer((worktree_id, "main.rs"), cx)
21261 })
21262 .await
21263 .unwrap();
21264
21265 let (editor, cx) = cx.add_window_view(|window, cx| {
21266 Editor::new(
21267 EditorMode::full(),
21268 MultiBuffer::build_from_buffer(buffer, cx),
21269 Some(project.clone()),
21270 window,
21271 cx,
21272 )
21273 });
21274
21275 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21276 let abs_path = project.read_with(cx, |project, cx| {
21277 project
21278 .absolute_path(&project_path, cx)
21279 .map(|path_buf| Arc::from(path_buf.to_owned()))
21280 .unwrap()
21281 });
21282
21283 editor.update_in(cx, |editor, window, cx| {
21284 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
21285 });
21286
21287 let breakpoints = editor.update(cx, |editor, cx| {
21288 editor
21289 .breakpoint_store()
21290 .as_ref()
21291 .unwrap()
21292 .read(cx)
21293 .all_source_breakpoints(cx)
21294 .clone()
21295 });
21296
21297 assert_breakpoint(
21298 &breakpoints,
21299 &abs_path,
21300 vec![(0, Breakpoint::new_log("hello world"))],
21301 );
21302
21303 // Removing a log message from a log breakpoint should remove it
21304 editor.update_in(cx, |editor, window, cx| {
21305 add_log_breakpoint_at_cursor(editor, "", window, cx);
21306 });
21307
21308 let breakpoints = editor.update(cx, |editor, cx| {
21309 editor
21310 .breakpoint_store()
21311 .as_ref()
21312 .unwrap()
21313 .read(cx)
21314 .all_source_breakpoints(cx)
21315 .clone()
21316 });
21317
21318 assert_breakpoint(&breakpoints, &abs_path, vec![]);
21319
21320 editor.update_in(cx, |editor, window, cx| {
21321 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21322 editor.move_to_end(&MoveToEnd, window, cx);
21323 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21324 // Not adding a log message to a standard breakpoint shouldn't remove it
21325 add_log_breakpoint_at_cursor(editor, "", window, cx);
21326 });
21327
21328 let breakpoints = editor.update(cx, |editor, cx| {
21329 editor
21330 .breakpoint_store()
21331 .as_ref()
21332 .unwrap()
21333 .read(cx)
21334 .all_source_breakpoints(cx)
21335 .clone()
21336 });
21337
21338 assert_breakpoint(
21339 &breakpoints,
21340 &abs_path,
21341 vec![
21342 (0, Breakpoint::new_standard()),
21343 (3, Breakpoint::new_standard()),
21344 ],
21345 );
21346
21347 editor.update_in(cx, |editor, window, cx| {
21348 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
21349 });
21350
21351 let breakpoints = editor.update(cx, |editor, cx| {
21352 editor
21353 .breakpoint_store()
21354 .as_ref()
21355 .unwrap()
21356 .read(cx)
21357 .all_source_breakpoints(cx)
21358 .clone()
21359 });
21360
21361 assert_breakpoint(
21362 &breakpoints,
21363 &abs_path,
21364 vec![
21365 (0, Breakpoint::new_standard()),
21366 (3, Breakpoint::new_log("hello world")),
21367 ],
21368 );
21369
21370 editor.update_in(cx, |editor, window, cx| {
21371 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
21372 });
21373
21374 let breakpoints = editor.update(cx, |editor, cx| {
21375 editor
21376 .breakpoint_store()
21377 .as_ref()
21378 .unwrap()
21379 .read(cx)
21380 .all_source_breakpoints(cx)
21381 .clone()
21382 });
21383
21384 assert_breakpoint(
21385 &breakpoints,
21386 &abs_path,
21387 vec![
21388 (0, Breakpoint::new_standard()),
21389 (3, Breakpoint::new_log("hello Earth!!")),
21390 ],
21391 );
21392}
21393
21394/// This also tests that Editor::breakpoint_at_cursor_head is working properly
21395/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
21396/// or when breakpoints were placed out of order. This tests for a regression too
21397#[gpui::test]
21398async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
21399 init_test(cx, |_| {});
21400
21401 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
21402 let fs = FakeFs::new(cx.executor());
21403 fs.insert_tree(
21404 path!("/a"),
21405 json!({
21406 "main.rs": sample_text,
21407 }),
21408 )
21409 .await;
21410 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21411 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21412 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21413
21414 let fs = FakeFs::new(cx.executor());
21415 fs.insert_tree(
21416 path!("/a"),
21417 json!({
21418 "main.rs": sample_text,
21419 }),
21420 )
21421 .await;
21422 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21423 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21424 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21425 let worktree_id = workspace
21426 .update(cx, |workspace, _window, cx| {
21427 workspace.project().update(cx, |project, cx| {
21428 project.worktrees(cx).next().unwrap().read(cx).id()
21429 })
21430 })
21431 .unwrap();
21432
21433 let buffer = project
21434 .update(cx, |project, cx| {
21435 project.open_buffer((worktree_id, "main.rs"), cx)
21436 })
21437 .await
21438 .unwrap();
21439
21440 let (editor, cx) = cx.add_window_view(|window, cx| {
21441 Editor::new(
21442 EditorMode::full(),
21443 MultiBuffer::build_from_buffer(buffer, cx),
21444 Some(project.clone()),
21445 window,
21446 cx,
21447 )
21448 });
21449
21450 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21451 let abs_path = project.read_with(cx, |project, cx| {
21452 project
21453 .absolute_path(&project_path, cx)
21454 .map(|path_buf| Arc::from(path_buf.to_owned()))
21455 .unwrap()
21456 });
21457
21458 // assert we can add breakpoint on the first line
21459 editor.update_in(cx, |editor, window, cx| {
21460 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21461 editor.move_to_end(&MoveToEnd, window, cx);
21462 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21463 editor.move_up(&MoveUp, window, cx);
21464 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21465 });
21466
21467 let breakpoints = editor.update(cx, |editor, cx| {
21468 editor
21469 .breakpoint_store()
21470 .as_ref()
21471 .unwrap()
21472 .read(cx)
21473 .all_source_breakpoints(cx)
21474 .clone()
21475 });
21476
21477 assert_eq!(1, breakpoints.len());
21478 assert_breakpoint(
21479 &breakpoints,
21480 &abs_path,
21481 vec![
21482 (0, Breakpoint::new_standard()),
21483 (2, Breakpoint::new_standard()),
21484 (3, Breakpoint::new_standard()),
21485 ],
21486 );
21487
21488 editor.update_in(cx, |editor, window, cx| {
21489 editor.move_to_beginning(&MoveToBeginning, window, cx);
21490 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21491 editor.move_to_end(&MoveToEnd, window, cx);
21492 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21493 // Disabling a breakpoint that doesn't exist should do nothing
21494 editor.move_up(&MoveUp, window, cx);
21495 editor.move_up(&MoveUp, window, cx);
21496 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21497 });
21498
21499 let breakpoints = editor.update(cx, |editor, cx| {
21500 editor
21501 .breakpoint_store()
21502 .as_ref()
21503 .unwrap()
21504 .read(cx)
21505 .all_source_breakpoints(cx)
21506 .clone()
21507 });
21508
21509 let disable_breakpoint = {
21510 let mut bp = Breakpoint::new_standard();
21511 bp.state = BreakpointState::Disabled;
21512 bp
21513 };
21514
21515 assert_eq!(1, breakpoints.len());
21516 assert_breakpoint(
21517 &breakpoints,
21518 &abs_path,
21519 vec![
21520 (0, disable_breakpoint.clone()),
21521 (2, Breakpoint::new_standard()),
21522 (3, disable_breakpoint.clone()),
21523 ],
21524 );
21525
21526 editor.update_in(cx, |editor, window, cx| {
21527 editor.move_to_beginning(&MoveToBeginning, window, cx);
21528 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21529 editor.move_to_end(&MoveToEnd, window, cx);
21530 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21531 editor.move_up(&MoveUp, window, cx);
21532 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21533 });
21534
21535 let breakpoints = editor.update(cx, |editor, cx| {
21536 editor
21537 .breakpoint_store()
21538 .as_ref()
21539 .unwrap()
21540 .read(cx)
21541 .all_source_breakpoints(cx)
21542 .clone()
21543 });
21544
21545 assert_eq!(1, breakpoints.len());
21546 assert_breakpoint(
21547 &breakpoints,
21548 &abs_path,
21549 vec![
21550 (0, Breakpoint::new_standard()),
21551 (2, disable_breakpoint),
21552 (3, Breakpoint::new_standard()),
21553 ],
21554 );
21555}
21556
21557#[gpui::test]
21558async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
21559 init_test(cx, |_| {});
21560 let capabilities = lsp::ServerCapabilities {
21561 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
21562 prepare_provider: Some(true),
21563 work_done_progress_options: Default::default(),
21564 })),
21565 ..Default::default()
21566 };
21567 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21568
21569 cx.set_state(indoc! {"
21570 struct Fˇoo {}
21571 "});
21572
21573 cx.update_editor(|editor, _, cx| {
21574 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21575 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21576 editor.highlight_background::<DocumentHighlightRead>(
21577 &[highlight_range],
21578 |theme| theme.colors().editor_document_highlight_read_background,
21579 cx,
21580 );
21581 });
21582
21583 let mut prepare_rename_handler = cx
21584 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
21585 move |_, _, _| async move {
21586 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
21587 start: lsp::Position {
21588 line: 0,
21589 character: 7,
21590 },
21591 end: lsp::Position {
21592 line: 0,
21593 character: 10,
21594 },
21595 })))
21596 },
21597 );
21598 let prepare_rename_task = cx
21599 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21600 .expect("Prepare rename was not started");
21601 prepare_rename_handler.next().await.unwrap();
21602 prepare_rename_task.await.expect("Prepare rename failed");
21603
21604 let mut rename_handler =
21605 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21606 let edit = lsp::TextEdit {
21607 range: lsp::Range {
21608 start: lsp::Position {
21609 line: 0,
21610 character: 7,
21611 },
21612 end: lsp::Position {
21613 line: 0,
21614 character: 10,
21615 },
21616 },
21617 new_text: "FooRenamed".to_string(),
21618 };
21619 Ok(Some(lsp::WorkspaceEdit::new(
21620 // Specify the same edit twice
21621 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
21622 )))
21623 });
21624 let rename_task = cx
21625 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21626 .expect("Confirm rename was not started");
21627 rename_handler.next().await.unwrap();
21628 rename_task.await.expect("Confirm rename failed");
21629 cx.run_until_parked();
21630
21631 // Despite two edits, only one is actually applied as those are identical
21632 cx.assert_editor_state(indoc! {"
21633 struct FooRenamedˇ {}
21634 "});
21635}
21636
21637#[gpui::test]
21638async fn test_rename_without_prepare(cx: &mut TestAppContext) {
21639 init_test(cx, |_| {});
21640 // These capabilities indicate that the server does not support prepare rename.
21641 let capabilities = lsp::ServerCapabilities {
21642 rename_provider: Some(lsp::OneOf::Left(true)),
21643 ..Default::default()
21644 };
21645 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21646
21647 cx.set_state(indoc! {"
21648 struct Fˇoo {}
21649 "});
21650
21651 cx.update_editor(|editor, _window, cx| {
21652 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21653 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21654 editor.highlight_background::<DocumentHighlightRead>(
21655 &[highlight_range],
21656 |theme| theme.colors().editor_document_highlight_read_background,
21657 cx,
21658 );
21659 });
21660
21661 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21662 .expect("Prepare rename was not started")
21663 .await
21664 .expect("Prepare rename failed");
21665
21666 let mut rename_handler =
21667 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21668 let edit = lsp::TextEdit {
21669 range: lsp::Range {
21670 start: lsp::Position {
21671 line: 0,
21672 character: 7,
21673 },
21674 end: lsp::Position {
21675 line: 0,
21676 character: 10,
21677 },
21678 },
21679 new_text: "FooRenamed".to_string(),
21680 };
21681 Ok(Some(lsp::WorkspaceEdit::new(
21682 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
21683 )))
21684 });
21685 let rename_task = cx
21686 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21687 .expect("Confirm rename was not started");
21688 rename_handler.next().await.unwrap();
21689 rename_task.await.expect("Confirm rename failed");
21690 cx.run_until_parked();
21691
21692 // Correct range is renamed, as `surrounding_word` is used to find it.
21693 cx.assert_editor_state(indoc! {"
21694 struct FooRenamedˇ {}
21695 "});
21696}
21697
21698#[gpui::test]
21699async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
21700 init_test(cx, |_| {});
21701 let mut cx = EditorTestContext::new(cx).await;
21702
21703 let language = Arc::new(
21704 Language::new(
21705 LanguageConfig::default(),
21706 Some(tree_sitter_html::LANGUAGE.into()),
21707 )
21708 .with_brackets_query(
21709 r#"
21710 ("<" @open "/>" @close)
21711 ("</" @open ">" @close)
21712 ("<" @open ">" @close)
21713 ("\"" @open "\"" @close)
21714 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
21715 "#,
21716 )
21717 .unwrap(),
21718 );
21719 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21720
21721 cx.set_state(indoc! {"
21722 <span>ˇ</span>
21723 "});
21724 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21725 cx.assert_editor_state(indoc! {"
21726 <span>
21727 ˇ
21728 </span>
21729 "});
21730
21731 cx.set_state(indoc! {"
21732 <span><span></span>ˇ</span>
21733 "});
21734 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21735 cx.assert_editor_state(indoc! {"
21736 <span><span></span>
21737 ˇ</span>
21738 "});
21739
21740 cx.set_state(indoc! {"
21741 <span>ˇ
21742 </span>
21743 "});
21744 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21745 cx.assert_editor_state(indoc! {"
21746 <span>
21747 ˇ
21748 </span>
21749 "});
21750}
21751
21752#[gpui::test(iterations = 10)]
21753async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
21754 init_test(cx, |_| {});
21755
21756 let fs = FakeFs::new(cx.executor());
21757 fs.insert_tree(
21758 path!("/dir"),
21759 json!({
21760 "a.ts": "a",
21761 }),
21762 )
21763 .await;
21764
21765 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
21766 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21767 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21768
21769 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21770 language_registry.add(Arc::new(Language::new(
21771 LanguageConfig {
21772 name: "TypeScript".into(),
21773 matcher: LanguageMatcher {
21774 path_suffixes: vec!["ts".to_string()],
21775 ..Default::default()
21776 },
21777 ..Default::default()
21778 },
21779 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21780 )));
21781 let mut fake_language_servers = language_registry.register_fake_lsp(
21782 "TypeScript",
21783 FakeLspAdapter {
21784 capabilities: lsp::ServerCapabilities {
21785 code_lens_provider: Some(lsp::CodeLensOptions {
21786 resolve_provider: Some(true),
21787 }),
21788 execute_command_provider: Some(lsp::ExecuteCommandOptions {
21789 commands: vec!["_the/command".to_string()],
21790 ..lsp::ExecuteCommandOptions::default()
21791 }),
21792 ..lsp::ServerCapabilities::default()
21793 },
21794 ..FakeLspAdapter::default()
21795 },
21796 );
21797
21798 let editor = workspace
21799 .update(cx, |workspace, window, cx| {
21800 workspace.open_abs_path(
21801 PathBuf::from(path!("/dir/a.ts")),
21802 OpenOptions::default(),
21803 window,
21804 cx,
21805 )
21806 })
21807 .unwrap()
21808 .await
21809 .unwrap()
21810 .downcast::<Editor>()
21811 .unwrap();
21812 cx.executor().run_until_parked();
21813
21814 let fake_server = fake_language_servers.next().await.unwrap();
21815
21816 let buffer = editor.update(cx, |editor, cx| {
21817 editor
21818 .buffer()
21819 .read(cx)
21820 .as_singleton()
21821 .expect("have opened a single file by path")
21822 });
21823
21824 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
21825 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
21826 drop(buffer_snapshot);
21827 let actions = cx
21828 .update_window(*workspace, |_, window, cx| {
21829 project.code_actions(&buffer, anchor..anchor, window, cx)
21830 })
21831 .unwrap();
21832
21833 fake_server
21834 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21835 Ok(Some(vec![
21836 lsp::CodeLens {
21837 range: lsp::Range::default(),
21838 command: Some(lsp::Command {
21839 title: "Code lens command".to_owned(),
21840 command: "_the/command".to_owned(),
21841 arguments: None,
21842 }),
21843 data: None,
21844 },
21845 lsp::CodeLens {
21846 range: lsp::Range::default(),
21847 command: Some(lsp::Command {
21848 title: "Command not in capabilities".to_owned(),
21849 command: "not in capabilities".to_owned(),
21850 arguments: None,
21851 }),
21852 data: None,
21853 },
21854 lsp::CodeLens {
21855 range: lsp::Range {
21856 start: lsp::Position {
21857 line: 1,
21858 character: 1,
21859 },
21860 end: lsp::Position {
21861 line: 1,
21862 character: 1,
21863 },
21864 },
21865 command: Some(lsp::Command {
21866 title: "Command not in range".to_owned(),
21867 command: "_the/command".to_owned(),
21868 arguments: None,
21869 }),
21870 data: None,
21871 },
21872 ]))
21873 })
21874 .next()
21875 .await;
21876
21877 let actions = actions.await.unwrap();
21878 assert_eq!(
21879 actions.len(),
21880 1,
21881 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
21882 );
21883 let action = actions[0].clone();
21884 let apply = project.update(cx, |project, cx| {
21885 project.apply_code_action(buffer.clone(), action, true, cx)
21886 });
21887
21888 // Resolving the code action does not populate its edits. In absence of
21889 // edits, we must execute the given command.
21890 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
21891 |mut lens, _| async move {
21892 let lens_command = lens.command.as_mut().expect("should have a command");
21893 assert_eq!(lens_command.title, "Code lens command");
21894 lens_command.arguments = Some(vec![json!("the-argument")]);
21895 Ok(lens)
21896 },
21897 );
21898
21899 // While executing the command, the language server sends the editor
21900 // a `workspaceEdit` request.
21901 fake_server
21902 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
21903 let fake = fake_server.clone();
21904 move |params, _| {
21905 assert_eq!(params.command, "_the/command");
21906 let fake = fake.clone();
21907 async move {
21908 fake.server
21909 .request::<lsp::request::ApplyWorkspaceEdit>(
21910 lsp::ApplyWorkspaceEditParams {
21911 label: None,
21912 edit: lsp::WorkspaceEdit {
21913 changes: Some(
21914 [(
21915 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
21916 vec![lsp::TextEdit {
21917 range: lsp::Range::new(
21918 lsp::Position::new(0, 0),
21919 lsp::Position::new(0, 0),
21920 ),
21921 new_text: "X".into(),
21922 }],
21923 )]
21924 .into_iter()
21925 .collect(),
21926 ),
21927 ..lsp::WorkspaceEdit::default()
21928 },
21929 },
21930 )
21931 .await
21932 .into_response()
21933 .unwrap();
21934 Ok(Some(json!(null)))
21935 }
21936 }
21937 })
21938 .next()
21939 .await;
21940
21941 // Applying the code lens command returns a project transaction containing the edits
21942 // sent by the language server in its `workspaceEdit` request.
21943 let transaction = apply.await.unwrap();
21944 assert!(transaction.0.contains_key(&buffer));
21945 buffer.update(cx, |buffer, cx| {
21946 assert_eq!(buffer.text(), "Xa");
21947 buffer.undo(cx);
21948 assert_eq!(buffer.text(), "a");
21949 });
21950
21951 let actions_after_edits = cx
21952 .update_window(*workspace, |_, window, cx| {
21953 project.code_actions(&buffer, anchor..anchor, window, cx)
21954 })
21955 .unwrap()
21956 .await
21957 .unwrap();
21958 assert_eq!(
21959 actions, actions_after_edits,
21960 "For the same selection, same code lens actions should be returned"
21961 );
21962
21963 let _responses =
21964 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21965 panic!("No more code lens requests are expected");
21966 });
21967 editor.update_in(cx, |editor, window, cx| {
21968 editor.select_all(&SelectAll, window, cx);
21969 });
21970 cx.executor().run_until_parked();
21971 let new_actions = cx
21972 .update_window(*workspace, |_, window, cx| {
21973 project.code_actions(&buffer, anchor..anchor, window, cx)
21974 })
21975 .unwrap()
21976 .await
21977 .unwrap();
21978 assert_eq!(
21979 actions, new_actions,
21980 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
21981 );
21982}
21983
21984#[gpui::test]
21985async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
21986 init_test(cx, |_| {});
21987
21988 let fs = FakeFs::new(cx.executor());
21989 let main_text = r#"fn main() {
21990println!("1");
21991println!("2");
21992println!("3");
21993println!("4");
21994println!("5");
21995}"#;
21996 let lib_text = "mod foo {}";
21997 fs.insert_tree(
21998 path!("/a"),
21999 json!({
22000 "lib.rs": lib_text,
22001 "main.rs": main_text,
22002 }),
22003 )
22004 .await;
22005
22006 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22007 let (workspace, cx) =
22008 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22009 let worktree_id = workspace.update(cx, |workspace, cx| {
22010 workspace.project().update(cx, |project, cx| {
22011 project.worktrees(cx).next().unwrap().read(cx).id()
22012 })
22013 });
22014
22015 let expected_ranges = vec![
22016 Point::new(0, 0)..Point::new(0, 0),
22017 Point::new(1, 0)..Point::new(1, 1),
22018 Point::new(2, 0)..Point::new(2, 2),
22019 Point::new(3, 0)..Point::new(3, 3),
22020 ];
22021
22022 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22023 let editor_1 = workspace
22024 .update_in(cx, |workspace, window, cx| {
22025 workspace.open_path(
22026 (worktree_id, "main.rs"),
22027 Some(pane_1.downgrade()),
22028 true,
22029 window,
22030 cx,
22031 )
22032 })
22033 .unwrap()
22034 .await
22035 .downcast::<Editor>()
22036 .unwrap();
22037 pane_1.update(cx, |pane, cx| {
22038 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22039 open_editor.update(cx, |editor, cx| {
22040 assert_eq!(
22041 editor.display_text(cx),
22042 main_text,
22043 "Original main.rs text on initial open",
22044 );
22045 assert_eq!(
22046 editor
22047 .selections
22048 .all::<Point>(cx)
22049 .into_iter()
22050 .map(|s| s.range())
22051 .collect::<Vec<_>>(),
22052 vec![Point::zero()..Point::zero()],
22053 "Default selections on initial open",
22054 );
22055 })
22056 });
22057 editor_1.update_in(cx, |editor, window, cx| {
22058 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22059 s.select_ranges(expected_ranges.clone());
22060 });
22061 });
22062
22063 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
22064 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
22065 });
22066 let editor_2 = workspace
22067 .update_in(cx, |workspace, window, cx| {
22068 workspace.open_path(
22069 (worktree_id, "main.rs"),
22070 Some(pane_2.downgrade()),
22071 true,
22072 window,
22073 cx,
22074 )
22075 })
22076 .unwrap()
22077 .await
22078 .downcast::<Editor>()
22079 .unwrap();
22080 pane_2.update(cx, |pane, cx| {
22081 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22082 open_editor.update(cx, |editor, cx| {
22083 assert_eq!(
22084 editor.display_text(cx),
22085 main_text,
22086 "Original main.rs text on initial open in another panel",
22087 );
22088 assert_eq!(
22089 editor
22090 .selections
22091 .all::<Point>(cx)
22092 .into_iter()
22093 .map(|s| s.range())
22094 .collect::<Vec<_>>(),
22095 vec![Point::zero()..Point::zero()],
22096 "Default selections on initial open in another panel",
22097 );
22098 })
22099 });
22100
22101 editor_2.update_in(cx, |editor, window, cx| {
22102 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
22103 });
22104
22105 let _other_editor_1 = workspace
22106 .update_in(cx, |workspace, window, cx| {
22107 workspace.open_path(
22108 (worktree_id, "lib.rs"),
22109 Some(pane_1.downgrade()),
22110 true,
22111 window,
22112 cx,
22113 )
22114 })
22115 .unwrap()
22116 .await
22117 .downcast::<Editor>()
22118 .unwrap();
22119 pane_1
22120 .update_in(cx, |pane, window, cx| {
22121 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
22122 })
22123 .await
22124 .unwrap();
22125 drop(editor_1);
22126 pane_1.update(cx, |pane, cx| {
22127 pane.active_item()
22128 .unwrap()
22129 .downcast::<Editor>()
22130 .unwrap()
22131 .update(cx, |editor, cx| {
22132 assert_eq!(
22133 editor.display_text(cx),
22134 lib_text,
22135 "Other file should be open and active",
22136 );
22137 });
22138 assert_eq!(pane.items().count(), 1, "No other editors should be open");
22139 });
22140
22141 let _other_editor_2 = workspace
22142 .update_in(cx, |workspace, window, cx| {
22143 workspace.open_path(
22144 (worktree_id, "lib.rs"),
22145 Some(pane_2.downgrade()),
22146 true,
22147 window,
22148 cx,
22149 )
22150 })
22151 .unwrap()
22152 .await
22153 .downcast::<Editor>()
22154 .unwrap();
22155 pane_2
22156 .update_in(cx, |pane, window, cx| {
22157 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
22158 })
22159 .await
22160 .unwrap();
22161 drop(editor_2);
22162 pane_2.update(cx, |pane, cx| {
22163 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22164 open_editor.update(cx, |editor, cx| {
22165 assert_eq!(
22166 editor.display_text(cx),
22167 lib_text,
22168 "Other file should be open and active in another panel too",
22169 );
22170 });
22171 assert_eq!(
22172 pane.items().count(),
22173 1,
22174 "No other editors should be open in another pane",
22175 );
22176 });
22177
22178 let _editor_1_reopened = workspace
22179 .update_in(cx, |workspace, window, cx| {
22180 workspace.open_path(
22181 (worktree_id, "main.rs"),
22182 Some(pane_1.downgrade()),
22183 true,
22184 window,
22185 cx,
22186 )
22187 })
22188 .unwrap()
22189 .await
22190 .downcast::<Editor>()
22191 .unwrap();
22192 let _editor_2_reopened = workspace
22193 .update_in(cx, |workspace, window, cx| {
22194 workspace.open_path(
22195 (worktree_id, "main.rs"),
22196 Some(pane_2.downgrade()),
22197 true,
22198 window,
22199 cx,
22200 )
22201 })
22202 .unwrap()
22203 .await
22204 .downcast::<Editor>()
22205 .unwrap();
22206 pane_1.update(cx, |pane, cx| {
22207 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22208 open_editor.update(cx, |editor, cx| {
22209 assert_eq!(
22210 editor.display_text(cx),
22211 main_text,
22212 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
22213 );
22214 assert_eq!(
22215 editor
22216 .selections
22217 .all::<Point>(cx)
22218 .into_iter()
22219 .map(|s| s.range())
22220 .collect::<Vec<_>>(),
22221 expected_ranges,
22222 "Previous editor in the 1st panel had selections and should get them restored on reopen",
22223 );
22224 })
22225 });
22226 pane_2.update(cx, |pane, cx| {
22227 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22228 open_editor.update(cx, |editor, cx| {
22229 assert_eq!(
22230 editor.display_text(cx),
22231 r#"fn main() {
22232⋯rintln!("1");
22233⋯intln!("2");
22234⋯ntln!("3");
22235println!("4");
22236println!("5");
22237}"#,
22238 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
22239 );
22240 assert_eq!(
22241 editor
22242 .selections
22243 .all::<Point>(cx)
22244 .into_iter()
22245 .map(|s| s.range())
22246 .collect::<Vec<_>>(),
22247 vec![Point::zero()..Point::zero()],
22248 "Previous editor in the 2nd pane had no selections changed hence should restore none",
22249 );
22250 })
22251 });
22252}
22253
22254#[gpui::test]
22255async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
22256 init_test(cx, |_| {});
22257
22258 let fs = FakeFs::new(cx.executor());
22259 let main_text = r#"fn main() {
22260println!("1");
22261println!("2");
22262println!("3");
22263println!("4");
22264println!("5");
22265}"#;
22266 let lib_text = "mod foo {}";
22267 fs.insert_tree(
22268 path!("/a"),
22269 json!({
22270 "lib.rs": lib_text,
22271 "main.rs": main_text,
22272 }),
22273 )
22274 .await;
22275
22276 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22277 let (workspace, cx) =
22278 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22279 let worktree_id = workspace.update(cx, |workspace, cx| {
22280 workspace.project().update(cx, |project, cx| {
22281 project.worktrees(cx).next().unwrap().read(cx).id()
22282 })
22283 });
22284
22285 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22286 let editor = workspace
22287 .update_in(cx, |workspace, window, cx| {
22288 workspace.open_path(
22289 (worktree_id, "main.rs"),
22290 Some(pane.downgrade()),
22291 true,
22292 window,
22293 cx,
22294 )
22295 })
22296 .unwrap()
22297 .await
22298 .downcast::<Editor>()
22299 .unwrap();
22300 pane.update(cx, |pane, cx| {
22301 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22302 open_editor.update(cx, |editor, cx| {
22303 assert_eq!(
22304 editor.display_text(cx),
22305 main_text,
22306 "Original main.rs text on initial open",
22307 );
22308 })
22309 });
22310 editor.update_in(cx, |editor, window, cx| {
22311 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
22312 });
22313
22314 cx.update_global(|store: &mut SettingsStore, cx| {
22315 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
22316 s.restore_on_file_reopen = Some(false);
22317 });
22318 });
22319 editor.update_in(cx, |editor, window, cx| {
22320 editor.fold_ranges(
22321 vec![
22322 Point::new(1, 0)..Point::new(1, 1),
22323 Point::new(2, 0)..Point::new(2, 2),
22324 Point::new(3, 0)..Point::new(3, 3),
22325 ],
22326 false,
22327 window,
22328 cx,
22329 );
22330 });
22331 pane.update_in(cx, |pane, window, cx| {
22332 pane.close_all_items(&CloseAllItems::default(), window, cx)
22333 })
22334 .await
22335 .unwrap();
22336 pane.update(cx, |pane, _| {
22337 assert!(pane.active_item().is_none());
22338 });
22339 cx.update_global(|store: &mut SettingsStore, cx| {
22340 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
22341 s.restore_on_file_reopen = Some(true);
22342 });
22343 });
22344
22345 let _editor_reopened = workspace
22346 .update_in(cx, |workspace, window, cx| {
22347 workspace.open_path(
22348 (worktree_id, "main.rs"),
22349 Some(pane.downgrade()),
22350 true,
22351 window,
22352 cx,
22353 )
22354 })
22355 .unwrap()
22356 .await
22357 .downcast::<Editor>()
22358 .unwrap();
22359 pane.update(cx, |pane, cx| {
22360 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22361 open_editor.update(cx, |editor, cx| {
22362 assert_eq!(
22363 editor.display_text(cx),
22364 main_text,
22365 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
22366 );
22367 })
22368 });
22369}
22370
22371#[gpui::test]
22372async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
22373 struct EmptyModalView {
22374 focus_handle: gpui::FocusHandle,
22375 }
22376 impl EventEmitter<DismissEvent> for EmptyModalView {}
22377 impl Render for EmptyModalView {
22378 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
22379 div()
22380 }
22381 }
22382 impl Focusable for EmptyModalView {
22383 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
22384 self.focus_handle.clone()
22385 }
22386 }
22387 impl workspace::ModalView for EmptyModalView {}
22388 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
22389 EmptyModalView {
22390 focus_handle: cx.focus_handle(),
22391 }
22392 }
22393
22394 init_test(cx, |_| {});
22395
22396 let fs = FakeFs::new(cx.executor());
22397 let project = Project::test(fs, [], cx).await;
22398 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22399 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
22400 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22401 let editor = cx.new_window_entity(|window, cx| {
22402 Editor::new(
22403 EditorMode::full(),
22404 buffer,
22405 Some(project.clone()),
22406 window,
22407 cx,
22408 )
22409 });
22410 workspace
22411 .update(cx, |workspace, window, cx| {
22412 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
22413 })
22414 .unwrap();
22415 editor.update_in(cx, |editor, window, cx| {
22416 editor.open_context_menu(&OpenContextMenu, window, cx);
22417 assert!(editor.mouse_context_menu.is_some());
22418 });
22419 workspace
22420 .update(cx, |workspace, window, cx| {
22421 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
22422 })
22423 .unwrap();
22424 cx.read(|cx| {
22425 assert!(editor.read(cx).mouse_context_menu.is_none());
22426 });
22427}
22428
22429#[gpui::test]
22430async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
22431 init_test(cx, |_| {});
22432
22433 let fs = FakeFs::new(cx.executor());
22434 fs.insert_file(path!("/file.html"), Default::default())
22435 .await;
22436
22437 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
22438
22439 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22440 let html_language = Arc::new(Language::new(
22441 LanguageConfig {
22442 name: "HTML".into(),
22443 matcher: LanguageMatcher {
22444 path_suffixes: vec!["html".to_string()],
22445 ..LanguageMatcher::default()
22446 },
22447 brackets: BracketPairConfig {
22448 pairs: vec![BracketPair {
22449 start: "<".into(),
22450 end: ">".into(),
22451 close: true,
22452 ..Default::default()
22453 }],
22454 ..Default::default()
22455 },
22456 ..Default::default()
22457 },
22458 Some(tree_sitter_html::LANGUAGE.into()),
22459 ));
22460 language_registry.add(html_language);
22461 let mut fake_servers = language_registry.register_fake_lsp(
22462 "HTML",
22463 FakeLspAdapter {
22464 capabilities: lsp::ServerCapabilities {
22465 completion_provider: Some(lsp::CompletionOptions {
22466 resolve_provider: Some(true),
22467 ..Default::default()
22468 }),
22469 ..Default::default()
22470 },
22471 ..Default::default()
22472 },
22473 );
22474
22475 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22476 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22477
22478 let worktree_id = workspace
22479 .update(cx, |workspace, _window, cx| {
22480 workspace.project().update(cx, |project, cx| {
22481 project.worktrees(cx).next().unwrap().read(cx).id()
22482 })
22483 })
22484 .unwrap();
22485 project
22486 .update(cx, |project, cx| {
22487 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
22488 })
22489 .await
22490 .unwrap();
22491 let editor = workspace
22492 .update(cx, |workspace, window, cx| {
22493 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
22494 })
22495 .unwrap()
22496 .await
22497 .unwrap()
22498 .downcast::<Editor>()
22499 .unwrap();
22500
22501 let fake_server = fake_servers.next().await.unwrap();
22502 editor.update_in(cx, |editor, window, cx| {
22503 editor.set_text("<ad></ad>", window, cx);
22504 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22505 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
22506 });
22507 let Some((buffer, _)) = editor
22508 .buffer
22509 .read(cx)
22510 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
22511 else {
22512 panic!("Failed to get buffer for selection position");
22513 };
22514 let buffer = buffer.read(cx);
22515 let buffer_id = buffer.remote_id();
22516 let opening_range =
22517 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
22518 let closing_range =
22519 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
22520 let mut linked_ranges = HashMap::default();
22521 linked_ranges.insert(
22522 buffer_id,
22523 vec![(opening_range.clone(), vec![closing_range.clone()])],
22524 );
22525 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
22526 });
22527 let mut completion_handle =
22528 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
22529 Ok(Some(lsp::CompletionResponse::Array(vec![
22530 lsp::CompletionItem {
22531 label: "head".to_string(),
22532 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22533 lsp::InsertReplaceEdit {
22534 new_text: "head".to_string(),
22535 insert: lsp::Range::new(
22536 lsp::Position::new(0, 1),
22537 lsp::Position::new(0, 3),
22538 ),
22539 replace: lsp::Range::new(
22540 lsp::Position::new(0, 1),
22541 lsp::Position::new(0, 3),
22542 ),
22543 },
22544 )),
22545 ..Default::default()
22546 },
22547 ])))
22548 });
22549 editor.update_in(cx, |editor, window, cx| {
22550 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
22551 });
22552 cx.run_until_parked();
22553 completion_handle.next().await.unwrap();
22554 editor.update(cx, |editor, _| {
22555 assert!(
22556 editor.context_menu_visible(),
22557 "Completion menu should be visible"
22558 );
22559 });
22560 editor.update_in(cx, |editor, window, cx| {
22561 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
22562 });
22563 cx.executor().run_until_parked();
22564 editor.update(cx, |editor, cx| {
22565 assert_eq!(editor.text(cx), "<head></head>");
22566 });
22567}
22568
22569#[gpui::test]
22570async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
22571 init_test(cx, |_| {});
22572
22573 let fs = FakeFs::new(cx.executor());
22574 fs.insert_tree(
22575 path!("/root"),
22576 json!({
22577 "a": {
22578 "main.rs": "fn main() {}",
22579 },
22580 "foo": {
22581 "bar": {
22582 "external_file.rs": "pub mod external {}",
22583 }
22584 }
22585 }),
22586 )
22587 .await;
22588
22589 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
22590 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22591 language_registry.add(rust_lang());
22592 let _fake_servers = language_registry.register_fake_lsp(
22593 "Rust",
22594 FakeLspAdapter {
22595 ..FakeLspAdapter::default()
22596 },
22597 );
22598 let (workspace, cx) =
22599 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22600 let worktree_id = workspace.update(cx, |workspace, cx| {
22601 workspace.project().update(cx, |project, cx| {
22602 project.worktrees(cx).next().unwrap().read(cx).id()
22603 })
22604 });
22605
22606 let assert_language_servers_count =
22607 |expected: usize, context: &str, cx: &mut VisualTestContext| {
22608 project.update(cx, |project, cx| {
22609 let current = project
22610 .lsp_store()
22611 .read(cx)
22612 .as_local()
22613 .unwrap()
22614 .language_servers
22615 .len();
22616 assert_eq!(expected, current, "{context}");
22617 });
22618 };
22619
22620 assert_language_servers_count(
22621 0,
22622 "No servers should be running before any file is open",
22623 cx,
22624 );
22625 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22626 let main_editor = workspace
22627 .update_in(cx, |workspace, window, cx| {
22628 workspace.open_path(
22629 (worktree_id, "main.rs"),
22630 Some(pane.downgrade()),
22631 true,
22632 window,
22633 cx,
22634 )
22635 })
22636 .unwrap()
22637 .await
22638 .downcast::<Editor>()
22639 .unwrap();
22640 pane.update(cx, |pane, cx| {
22641 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22642 open_editor.update(cx, |editor, cx| {
22643 assert_eq!(
22644 editor.display_text(cx),
22645 "fn main() {}",
22646 "Original main.rs text on initial open",
22647 );
22648 });
22649 assert_eq!(open_editor, main_editor);
22650 });
22651 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
22652
22653 let external_editor = workspace
22654 .update_in(cx, |workspace, window, cx| {
22655 workspace.open_abs_path(
22656 PathBuf::from("/root/foo/bar/external_file.rs"),
22657 OpenOptions::default(),
22658 window,
22659 cx,
22660 )
22661 })
22662 .await
22663 .expect("opening external file")
22664 .downcast::<Editor>()
22665 .expect("downcasted external file's open element to editor");
22666 pane.update(cx, |pane, cx| {
22667 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22668 open_editor.update(cx, |editor, cx| {
22669 assert_eq!(
22670 editor.display_text(cx),
22671 "pub mod external {}",
22672 "External file is open now",
22673 );
22674 });
22675 assert_eq!(open_editor, external_editor);
22676 });
22677 assert_language_servers_count(
22678 1,
22679 "Second, external, *.rs file should join the existing server",
22680 cx,
22681 );
22682
22683 pane.update_in(cx, |pane, window, cx| {
22684 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22685 })
22686 .await
22687 .unwrap();
22688 pane.update_in(cx, |pane, window, cx| {
22689 pane.navigate_backward(window, cx);
22690 });
22691 cx.run_until_parked();
22692 pane.update(cx, |pane, cx| {
22693 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22694 open_editor.update(cx, |editor, cx| {
22695 assert_eq!(
22696 editor.display_text(cx),
22697 "pub mod external {}",
22698 "External file is open now",
22699 );
22700 });
22701 });
22702 assert_language_servers_count(
22703 1,
22704 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
22705 cx,
22706 );
22707
22708 cx.update(|_, cx| {
22709 workspace::reload(cx);
22710 });
22711 assert_language_servers_count(
22712 1,
22713 "After reloading the worktree with local and external files opened, only one project should be started",
22714 cx,
22715 );
22716}
22717
22718#[gpui::test]
22719async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
22720 init_test(cx, |_| {});
22721
22722 let mut cx = EditorTestContext::new(cx).await;
22723 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22724 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22725
22726 // test cursor move to start of each line on tab
22727 // for `if`, `elif`, `else`, `while`, `with` and `for`
22728 cx.set_state(indoc! {"
22729 def main():
22730 ˇ for item in items:
22731 ˇ while item.active:
22732 ˇ if item.value > 10:
22733 ˇ continue
22734 ˇ elif item.value < 0:
22735 ˇ break
22736 ˇ else:
22737 ˇ with item.context() as ctx:
22738 ˇ yield count
22739 ˇ else:
22740 ˇ log('while else')
22741 ˇ else:
22742 ˇ log('for else')
22743 "});
22744 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22745 cx.assert_editor_state(indoc! {"
22746 def main():
22747 ˇfor item in items:
22748 ˇwhile item.active:
22749 ˇif item.value > 10:
22750 ˇcontinue
22751 ˇelif item.value < 0:
22752 ˇbreak
22753 ˇelse:
22754 ˇwith item.context() as ctx:
22755 ˇyield count
22756 ˇelse:
22757 ˇlog('while else')
22758 ˇelse:
22759 ˇlog('for else')
22760 "});
22761 // test relative indent is preserved when tab
22762 // for `if`, `elif`, `else`, `while`, `with` and `for`
22763 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22764 cx.assert_editor_state(indoc! {"
22765 def main():
22766 ˇfor item in items:
22767 ˇwhile item.active:
22768 ˇif item.value > 10:
22769 ˇcontinue
22770 ˇelif item.value < 0:
22771 ˇbreak
22772 ˇelse:
22773 ˇwith item.context() as ctx:
22774 ˇyield count
22775 ˇelse:
22776 ˇlog('while else')
22777 ˇelse:
22778 ˇlog('for else')
22779 "});
22780
22781 // test cursor move to start of each line on tab
22782 // for `try`, `except`, `else`, `finally`, `match` and `def`
22783 cx.set_state(indoc! {"
22784 def main():
22785 ˇ try:
22786 ˇ fetch()
22787 ˇ except ValueError:
22788 ˇ handle_error()
22789 ˇ else:
22790 ˇ match value:
22791 ˇ case _:
22792 ˇ finally:
22793 ˇ def status():
22794 ˇ return 0
22795 "});
22796 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22797 cx.assert_editor_state(indoc! {"
22798 def main():
22799 ˇtry:
22800 ˇfetch()
22801 ˇexcept ValueError:
22802 ˇhandle_error()
22803 ˇelse:
22804 ˇmatch value:
22805 ˇcase _:
22806 ˇfinally:
22807 ˇdef status():
22808 ˇreturn 0
22809 "});
22810 // test relative indent is preserved when tab
22811 // for `try`, `except`, `else`, `finally`, `match` and `def`
22812 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22813 cx.assert_editor_state(indoc! {"
22814 def main():
22815 ˇtry:
22816 ˇfetch()
22817 ˇexcept ValueError:
22818 ˇhandle_error()
22819 ˇelse:
22820 ˇmatch value:
22821 ˇcase _:
22822 ˇfinally:
22823 ˇdef status():
22824 ˇreturn 0
22825 "});
22826}
22827
22828#[gpui::test]
22829async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
22830 init_test(cx, |_| {});
22831
22832 let mut cx = EditorTestContext::new(cx).await;
22833 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22834 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22835
22836 // test `else` auto outdents when typed inside `if` block
22837 cx.set_state(indoc! {"
22838 def main():
22839 if i == 2:
22840 return
22841 ˇ
22842 "});
22843 cx.update_editor(|editor, window, cx| {
22844 editor.handle_input("else:", window, cx);
22845 });
22846 cx.assert_editor_state(indoc! {"
22847 def main():
22848 if i == 2:
22849 return
22850 else:ˇ
22851 "});
22852
22853 // test `except` auto outdents when typed inside `try` block
22854 cx.set_state(indoc! {"
22855 def main():
22856 try:
22857 i = 2
22858 ˇ
22859 "});
22860 cx.update_editor(|editor, window, cx| {
22861 editor.handle_input("except:", window, cx);
22862 });
22863 cx.assert_editor_state(indoc! {"
22864 def main():
22865 try:
22866 i = 2
22867 except:ˇ
22868 "});
22869
22870 // test `else` auto outdents when typed inside `except` block
22871 cx.set_state(indoc! {"
22872 def main():
22873 try:
22874 i = 2
22875 except:
22876 j = 2
22877 ˇ
22878 "});
22879 cx.update_editor(|editor, window, cx| {
22880 editor.handle_input("else:", window, cx);
22881 });
22882 cx.assert_editor_state(indoc! {"
22883 def main():
22884 try:
22885 i = 2
22886 except:
22887 j = 2
22888 else:ˇ
22889 "});
22890
22891 // test `finally` auto outdents when typed inside `else` block
22892 cx.set_state(indoc! {"
22893 def main():
22894 try:
22895 i = 2
22896 except:
22897 j = 2
22898 else:
22899 k = 2
22900 ˇ
22901 "});
22902 cx.update_editor(|editor, window, cx| {
22903 editor.handle_input("finally:", window, cx);
22904 });
22905 cx.assert_editor_state(indoc! {"
22906 def main():
22907 try:
22908 i = 2
22909 except:
22910 j = 2
22911 else:
22912 k = 2
22913 finally:ˇ
22914 "});
22915
22916 // test `else` does not outdents when typed inside `except` block right after for block
22917 cx.set_state(indoc! {"
22918 def main():
22919 try:
22920 i = 2
22921 except:
22922 for i in range(n):
22923 pass
22924 ˇ
22925 "});
22926 cx.update_editor(|editor, window, cx| {
22927 editor.handle_input("else:", window, cx);
22928 });
22929 cx.assert_editor_state(indoc! {"
22930 def main():
22931 try:
22932 i = 2
22933 except:
22934 for i in range(n):
22935 pass
22936 else:ˇ
22937 "});
22938
22939 // test `finally` auto outdents when typed inside `else` block right after for block
22940 cx.set_state(indoc! {"
22941 def main():
22942 try:
22943 i = 2
22944 except:
22945 j = 2
22946 else:
22947 for i in range(n):
22948 pass
22949 ˇ
22950 "});
22951 cx.update_editor(|editor, window, cx| {
22952 editor.handle_input("finally:", window, cx);
22953 });
22954 cx.assert_editor_state(indoc! {"
22955 def main():
22956 try:
22957 i = 2
22958 except:
22959 j = 2
22960 else:
22961 for i in range(n):
22962 pass
22963 finally:ˇ
22964 "});
22965
22966 // test `except` outdents to inner "try" block
22967 cx.set_state(indoc! {"
22968 def main():
22969 try:
22970 i = 2
22971 if i == 2:
22972 try:
22973 i = 3
22974 ˇ
22975 "});
22976 cx.update_editor(|editor, window, cx| {
22977 editor.handle_input("except:", window, cx);
22978 });
22979 cx.assert_editor_state(indoc! {"
22980 def main():
22981 try:
22982 i = 2
22983 if i == 2:
22984 try:
22985 i = 3
22986 except:ˇ
22987 "});
22988
22989 // test `except` outdents to outer "try" block
22990 cx.set_state(indoc! {"
22991 def main():
22992 try:
22993 i = 2
22994 if i == 2:
22995 try:
22996 i = 3
22997 ˇ
22998 "});
22999 cx.update_editor(|editor, window, cx| {
23000 editor.handle_input("except:", window, cx);
23001 });
23002 cx.assert_editor_state(indoc! {"
23003 def main():
23004 try:
23005 i = 2
23006 if i == 2:
23007 try:
23008 i = 3
23009 except:ˇ
23010 "});
23011
23012 // test `else` stays at correct indent when typed after `for` block
23013 cx.set_state(indoc! {"
23014 def main():
23015 for i in range(10):
23016 if i == 3:
23017 break
23018 ˇ
23019 "});
23020 cx.update_editor(|editor, window, cx| {
23021 editor.handle_input("else:", window, cx);
23022 });
23023 cx.assert_editor_state(indoc! {"
23024 def main():
23025 for i in range(10):
23026 if i == 3:
23027 break
23028 else:ˇ
23029 "});
23030
23031 // test does not outdent on typing after line with square brackets
23032 cx.set_state(indoc! {"
23033 def f() -> list[str]:
23034 ˇ
23035 "});
23036 cx.update_editor(|editor, window, cx| {
23037 editor.handle_input("a", window, cx);
23038 });
23039 cx.assert_editor_state(indoc! {"
23040 def f() -> list[str]:
23041 aˇ
23042 "});
23043
23044 // test does not outdent on typing : after case keyword
23045 cx.set_state(indoc! {"
23046 match 1:
23047 caseˇ
23048 "});
23049 cx.update_editor(|editor, window, cx| {
23050 editor.handle_input(":", window, cx);
23051 });
23052 cx.assert_editor_state(indoc! {"
23053 match 1:
23054 case:ˇ
23055 "});
23056}
23057
23058#[gpui::test]
23059async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
23060 init_test(cx, |_| {});
23061 update_test_language_settings(cx, |settings| {
23062 settings.defaults.extend_comment_on_newline = Some(false);
23063 });
23064 let mut cx = EditorTestContext::new(cx).await;
23065 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
23066 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23067
23068 // test correct indent after newline on comment
23069 cx.set_state(indoc! {"
23070 # COMMENT:ˇ
23071 "});
23072 cx.update_editor(|editor, window, cx| {
23073 editor.newline(&Newline, window, cx);
23074 });
23075 cx.assert_editor_state(indoc! {"
23076 # COMMENT:
23077 ˇ
23078 "});
23079
23080 // test correct indent after newline in brackets
23081 cx.set_state(indoc! {"
23082 {ˇ}
23083 "});
23084 cx.update_editor(|editor, window, cx| {
23085 editor.newline(&Newline, window, cx);
23086 });
23087 cx.run_until_parked();
23088 cx.assert_editor_state(indoc! {"
23089 {
23090 ˇ
23091 }
23092 "});
23093
23094 cx.set_state(indoc! {"
23095 (ˇ)
23096 "});
23097 cx.update_editor(|editor, window, cx| {
23098 editor.newline(&Newline, window, cx);
23099 });
23100 cx.run_until_parked();
23101 cx.assert_editor_state(indoc! {"
23102 (
23103 ˇ
23104 )
23105 "});
23106
23107 // do not indent after empty lists or dictionaries
23108 cx.set_state(indoc! {"
23109 a = []ˇ
23110 "});
23111 cx.update_editor(|editor, window, cx| {
23112 editor.newline(&Newline, window, cx);
23113 });
23114 cx.run_until_parked();
23115 cx.assert_editor_state(indoc! {"
23116 a = []
23117 ˇ
23118 "});
23119}
23120
23121#[gpui::test]
23122async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
23123 init_test(cx, |_| {});
23124
23125 let mut cx = EditorTestContext::new(cx).await;
23126 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23127 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23128
23129 // test cursor move to start of each line on tab
23130 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
23131 cx.set_state(indoc! {"
23132 function main() {
23133 ˇ for item in $items; do
23134 ˇ while [ -n \"$item\" ]; do
23135 ˇ if [ \"$value\" -gt 10 ]; then
23136 ˇ continue
23137 ˇ elif [ \"$value\" -lt 0 ]; then
23138 ˇ break
23139 ˇ else
23140 ˇ echo \"$item\"
23141 ˇ fi
23142 ˇ done
23143 ˇ done
23144 ˇ}
23145 "});
23146 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23147 cx.assert_editor_state(indoc! {"
23148 function main() {
23149 ˇfor item in $items; do
23150 ˇwhile [ -n \"$item\" ]; do
23151 ˇif [ \"$value\" -gt 10 ]; then
23152 ˇcontinue
23153 ˇelif [ \"$value\" -lt 0 ]; then
23154 ˇbreak
23155 ˇelse
23156 ˇecho \"$item\"
23157 ˇfi
23158 ˇdone
23159 ˇdone
23160 ˇ}
23161 "});
23162 // test relative indent is preserved when tab
23163 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23164 cx.assert_editor_state(indoc! {"
23165 function main() {
23166 ˇfor item in $items; do
23167 ˇwhile [ -n \"$item\" ]; do
23168 ˇif [ \"$value\" -gt 10 ]; then
23169 ˇcontinue
23170 ˇelif [ \"$value\" -lt 0 ]; then
23171 ˇbreak
23172 ˇelse
23173 ˇecho \"$item\"
23174 ˇfi
23175 ˇdone
23176 ˇdone
23177 ˇ}
23178 "});
23179
23180 // test cursor move to start of each line on tab
23181 // for `case` statement with patterns
23182 cx.set_state(indoc! {"
23183 function handle() {
23184 ˇ case \"$1\" in
23185 ˇ start)
23186 ˇ echo \"a\"
23187 ˇ ;;
23188 ˇ stop)
23189 ˇ echo \"b\"
23190 ˇ ;;
23191 ˇ *)
23192 ˇ echo \"c\"
23193 ˇ ;;
23194 ˇ esac
23195 ˇ}
23196 "});
23197 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23198 cx.assert_editor_state(indoc! {"
23199 function handle() {
23200 ˇcase \"$1\" in
23201 ˇstart)
23202 ˇecho \"a\"
23203 ˇ;;
23204 ˇstop)
23205 ˇecho \"b\"
23206 ˇ;;
23207 ˇ*)
23208 ˇecho \"c\"
23209 ˇ;;
23210 ˇesac
23211 ˇ}
23212 "});
23213}
23214
23215#[gpui::test]
23216async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
23217 init_test(cx, |_| {});
23218
23219 let mut cx = EditorTestContext::new(cx).await;
23220 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23221 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23222
23223 // test indents on comment insert
23224 cx.set_state(indoc! {"
23225 function main() {
23226 ˇ for item in $items; do
23227 ˇ while [ -n \"$item\" ]; do
23228 ˇ if [ \"$value\" -gt 10 ]; then
23229 ˇ continue
23230 ˇ elif [ \"$value\" -lt 0 ]; then
23231 ˇ break
23232 ˇ else
23233 ˇ echo \"$item\"
23234 ˇ fi
23235 ˇ done
23236 ˇ done
23237 ˇ}
23238 "});
23239 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
23240 cx.assert_editor_state(indoc! {"
23241 function main() {
23242 #ˇ for item in $items; do
23243 #ˇ while [ -n \"$item\" ]; do
23244 #ˇ if [ \"$value\" -gt 10 ]; then
23245 #ˇ continue
23246 #ˇ elif [ \"$value\" -lt 0 ]; then
23247 #ˇ break
23248 #ˇ else
23249 #ˇ echo \"$item\"
23250 #ˇ fi
23251 #ˇ done
23252 #ˇ done
23253 #ˇ}
23254 "});
23255}
23256
23257#[gpui::test]
23258async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
23259 init_test(cx, |_| {});
23260
23261 let mut cx = EditorTestContext::new(cx).await;
23262 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23263 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23264
23265 // test `else` auto outdents when typed inside `if` block
23266 cx.set_state(indoc! {"
23267 if [ \"$1\" = \"test\" ]; then
23268 echo \"foo bar\"
23269 ˇ
23270 "});
23271 cx.update_editor(|editor, window, cx| {
23272 editor.handle_input("else", window, cx);
23273 });
23274 cx.assert_editor_state(indoc! {"
23275 if [ \"$1\" = \"test\" ]; then
23276 echo \"foo bar\"
23277 elseˇ
23278 "});
23279
23280 // test `elif` auto outdents when typed inside `if` block
23281 cx.set_state(indoc! {"
23282 if [ \"$1\" = \"test\" ]; then
23283 echo \"foo bar\"
23284 ˇ
23285 "});
23286 cx.update_editor(|editor, window, cx| {
23287 editor.handle_input("elif", window, cx);
23288 });
23289 cx.assert_editor_state(indoc! {"
23290 if [ \"$1\" = \"test\" ]; then
23291 echo \"foo bar\"
23292 elifˇ
23293 "});
23294
23295 // test `fi` auto outdents when typed inside `else` block
23296 cx.set_state(indoc! {"
23297 if [ \"$1\" = \"test\" ]; then
23298 echo \"foo bar\"
23299 else
23300 echo \"bar baz\"
23301 ˇ
23302 "});
23303 cx.update_editor(|editor, window, cx| {
23304 editor.handle_input("fi", window, cx);
23305 });
23306 cx.assert_editor_state(indoc! {"
23307 if [ \"$1\" = \"test\" ]; then
23308 echo \"foo bar\"
23309 else
23310 echo \"bar baz\"
23311 fiˇ
23312 "});
23313
23314 // test `done` auto outdents when typed inside `while` block
23315 cx.set_state(indoc! {"
23316 while read line; do
23317 echo \"$line\"
23318 ˇ
23319 "});
23320 cx.update_editor(|editor, window, cx| {
23321 editor.handle_input("done", window, cx);
23322 });
23323 cx.assert_editor_state(indoc! {"
23324 while read line; do
23325 echo \"$line\"
23326 doneˇ
23327 "});
23328
23329 // test `done` auto outdents when typed inside `for` block
23330 cx.set_state(indoc! {"
23331 for file in *.txt; do
23332 cat \"$file\"
23333 ˇ
23334 "});
23335 cx.update_editor(|editor, window, cx| {
23336 editor.handle_input("done", window, cx);
23337 });
23338 cx.assert_editor_state(indoc! {"
23339 for file in *.txt; do
23340 cat \"$file\"
23341 doneˇ
23342 "});
23343
23344 // test `esac` auto outdents when typed inside `case` block
23345 cx.set_state(indoc! {"
23346 case \"$1\" in
23347 start)
23348 echo \"foo bar\"
23349 ;;
23350 stop)
23351 echo \"bar baz\"
23352 ;;
23353 ˇ
23354 "});
23355 cx.update_editor(|editor, window, cx| {
23356 editor.handle_input("esac", window, cx);
23357 });
23358 cx.assert_editor_state(indoc! {"
23359 case \"$1\" in
23360 start)
23361 echo \"foo bar\"
23362 ;;
23363 stop)
23364 echo \"bar baz\"
23365 ;;
23366 esacˇ
23367 "});
23368
23369 // test `*)` auto outdents when typed inside `case` block
23370 cx.set_state(indoc! {"
23371 case \"$1\" in
23372 start)
23373 echo \"foo bar\"
23374 ;;
23375 ˇ
23376 "});
23377 cx.update_editor(|editor, window, cx| {
23378 editor.handle_input("*)", window, cx);
23379 });
23380 cx.assert_editor_state(indoc! {"
23381 case \"$1\" in
23382 start)
23383 echo \"foo bar\"
23384 ;;
23385 *)ˇ
23386 "});
23387
23388 // test `fi` outdents to correct level with nested if blocks
23389 cx.set_state(indoc! {"
23390 if [ \"$1\" = \"test\" ]; then
23391 echo \"outer if\"
23392 if [ \"$2\" = \"debug\" ]; then
23393 echo \"inner if\"
23394 ˇ
23395 "});
23396 cx.update_editor(|editor, window, cx| {
23397 editor.handle_input("fi", window, cx);
23398 });
23399 cx.assert_editor_state(indoc! {"
23400 if [ \"$1\" = \"test\" ]; then
23401 echo \"outer if\"
23402 if [ \"$2\" = \"debug\" ]; then
23403 echo \"inner if\"
23404 fiˇ
23405 "});
23406}
23407
23408#[gpui::test]
23409async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
23410 init_test(cx, |_| {});
23411 update_test_language_settings(cx, |settings| {
23412 settings.defaults.extend_comment_on_newline = Some(false);
23413 });
23414 let mut cx = EditorTestContext::new(cx).await;
23415 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23416 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23417
23418 // test correct indent after newline on comment
23419 cx.set_state(indoc! {"
23420 # COMMENT:ˇ
23421 "});
23422 cx.update_editor(|editor, window, cx| {
23423 editor.newline(&Newline, window, cx);
23424 });
23425 cx.assert_editor_state(indoc! {"
23426 # COMMENT:
23427 ˇ
23428 "});
23429
23430 // test correct indent after newline after `then`
23431 cx.set_state(indoc! {"
23432
23433 if [ \"$1\" = \"test\" ]; thenˇ
23434 "});
23435 cx.update_editor(|editor, window, cx| {
23436 editor.newline(&Newline, window, cx);
23437 });
23438 cx.run_until_parked();
23439 cx.assert_editor_state(indoc! {"
23440
23441 if [ \"$1\" = \"test\" ]; then
23442 ˇ
23443 "});
23444
23445 // test correct indent after newline after `else`
23446 cx.set_state(indoc! {"
23447 if [ \"$1\" = \"test\" ]; then
23448 elseˇ
23449 "});
23450 cx.update_editor(|editor, window, cx| {
23451 editor.newline(&Newline, window, cx);
23452 });
23453 cx.run_until_parked();
23454 cx.assert_editor_state(indoc! {"
23455 if [ \"$1\" = \"test\" ]; then
23456 else
23457 ˇ
23458 "});
23459
23460 // test correct indent after newline after `elif`
23461 cx.set_state(indoc! {"
23462 if [ \"$1\" = \"test\" ]; then
23463 elifˇ
23464 "});
23465 cx.update_editor(|editor, window, cx| {
23466 editor.newline(&Newline, window, cx);
23467 });
23468 cx.run_until_parked();
23469 cx.assert_editor_state(indoc! {"
23470 if [ \"$1\" = \"test\" ]; then
23471 elif
23472 ˇ
23473 "});
23474
23475 // test correct indent after newline after `do`
23476 cx.set_state(indoc! {"
23477 for file in *.txt; doˇ
23478 "});
23479 cx.update_editor(|editor, window, cx| {
23480 editor.newline(&Newline, window, cx);
23481 });
23482 cx.run_until_parked();
23483 cx.assert_editor_state(indoc! {"
23484 for file in *.txt; do
23485 ˇ
23486 "});
23487
23488 // test correct indent after newline after case pattern
23489 cx.set_state(indoc! {"
23490 case \"$1\" in
23491 start)ˇ
23492 "});
23493 cx.update_editor(|editor, window, cx| {
23494 editor.newline(&Newline, window, cx);
23495 });
23496 cx.run_until_parked();
23497 cx.assert_editor_state(indoc! {"
23498 case \"$1\" in
23499 start)
23500 ˇ
23501 "});
23502
23503 // test correct indent after newline after case pattern
23504 cx.set_state(indoc! {"
23505 case \"$1\" in
23506 start)
23507 ;;
23508 *)ˇ
23509 "});
23510 cx.update_editor(|editor, window, cx| {
23511 editor.newline(&Newline, window, cx);
23512 });
23513 cx.run_until_parked();
23514 cx.assert_editor_state(indoc! {"
23515 case \"$1\" in
23516 start)
23517 ;;
23518 *)
23519 ˇ
23520 "});
23521
23522 // test correct indent after newline after function opening brace
23523 cx.set_state(indoc! {"
23524 function test() {ˇ}
23525 "});
23526 cx.update_editor(|editor, window, cx| {
23527 editor.newline(&Newline, window, cx);
23528 });
23529 cx.run_until_parked();
23530 cx.assert_editor_state(indoc! {"
23531 function test() {
23532 ˇ
23533 }
23534 "});
23535
23536 // test no extra indent after semicolon on same line
23537 cx.set_state(indoc! {"
23538 echo \"test\";ˇ
23539 "});
23540 cx.update_editor(|editor, window, cx| {
23541 editor.newline(&Newline, window, cx);
23542 });
23543 cx.run_until_parked();
23544 cx.assert_editor_state(indoc! {"
23545 echo \"test\";
23546 ˇ
23547 "});
23548}
23549
23550fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
23551 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
23552 point..point
23553}
23554
23555#[track_caller]
23556fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
23557 let (text, ranges) = marked_text_ranges(marked_text, true);
23558 assert_eq!(editor.text(cx), text);
23559 assert_eq!(
23560 editor.selections.ranges(cx),
23561 ranges,
23562 "Assert selections are {}",
23563 marked_text
23564 );
23565}
23566
23567pub fn handle_signature_help_request(
23568 cx: &mut EditorLspTestContext,
23569 mocked_response: lsp::SignatureHelp,
23570) -> impl Future<Output = ()> + use<> {
23571 let mut request =
23572 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
23573 let mocked_response = mocked_response.clone();
23574 async move { Ok(Some(mocked_response)) }
23575 });
23576
23577 async move {
23578 request.next().await;
23579 }
23580}
23581
23582#[track_caller]
23583pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
23584 cx.update_editor(|editor, _, _| {
23585 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
23586 let entries = menu.entries.borrow();
23587 let entries = entries
23588 .iter()
23589 .map(|entry| entry.string.as_str())
23590 .collect::<Vec<_>>();
23591 assert_eq!(entries, expected);
23592 } else {
23593 panic!("Expected completions menu");
23594 }
23595 });
23596}
23597
23598/// Handle completion request passing a marked string specifying where the completion
23599/// should be triggered from using '|' character, what range should be replaced, and what completions
23600/// should be returned using '<' and '>' to delimit the range.
23601///
23602/// Also see `handle_completion_request_with_insert_and_replace`.
23603#[track_caller]
23604pub fn handle_completion_request(
23605 marked_string: &str,
23606 completions: Vec<&'static str>,
23607 is_incomplete: bool,
23608 counter: Arc<AtomicUsize>,
23609 cx: &mut EditorLspTestContext,
23610) -> impl Future<Output = ()> {
23611 let complete_from_marker: TextRangeMarker = '|'.into();
23612 let replace_range_marker: TextRangeMarker = ('<', '>').into();
23613 let (_, mut marked_ranges) = marked_text_ranges_by(
23614 marked_string,
23615 vec![complete_from_marker.clone(), replace_range_marker.clone()],
23616 );
23617
23618 let complete_from_position =
23619 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
23620 let replace_range =
23621 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
23622
23623 let mut request =
23624 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
23625 let completions = completions.clone();
23626 counter.fetch_add(1, atomic::Ordering::Release);
23627 async move {
23628 assert_eq!(params.text_document_position.text_document.uri, url.clone());
23629 assert_eq!(
23630 params.text_document_position.position,
23631 complete_from_position
23632 );
23633 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
23634 is_incomplete: is_incomplete,
23635 item_defaults: None,
23636 items: completions
23637 .iter()
23638 .map(|completion_text| lsp::CompletionItem {
23639 label: completion_text.to_string(),
23640 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
23641 range: replace_range,
23642 new_text: completion_text.to_string(),
23643 })),
23644 ..Default::default()
23645 })
23646 .collect(),
23647 })))
23648 }
23649 });
23650
23651 async move {
23652 request.next().await;
23653 }
23654}
23655
23656/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
23657/// given instead, which also contains an `insert` range.
23658///
23659/// This function uses markers to define ranges:
23660/// - `|` marks the cursor position
23661/// - `<>` marks the replace range
23662/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
23663pub fn handle_completion_request_with_insert_and_replace(
23664 cx: &mut EditorLspTestContext,
23665 marked_string: &str,
23666 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
23667 counter: Arc<AtomicUsize>,
23668) -> impl Future<Output = ()> {
23669 let complete_from_marker: TextRangeMarker = '|'.into();
23670 let replace_range_marker: TextRangeMarker = ('<', '>').into();
23671 let insert_range_marker: TextRangeMarker = ('{', '}').into();
23672
23673 let (_, mut marked_ranges) = marked_text_ranges_by(
23674 marked_string,
23675 vec![
23676 complete_from_marker.clone(),
23677 replace_range_marker.clone(),
23678 insert_range_marker.clone(),
23679 ],
23680 );
23681
23682 let complete_from_position =
23683 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
23684 let replace_range =
23685 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
23686
23687 let insert_range = match marked_ranges.remove(&insert_range_marker) {
23688 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
23689 _ => lsp::Range {
23690 start: replace_range.start,
23691 end: complete_from_position,
23692 },
23693 };
23694
23695 let mut request =
23696 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
23697 let completions = completions.clone();
23698 counter.fetch_add(1, atomic::Ordering::Release);
23699 async move {
23700 assert_eq!(params.text_document_position.text_document.uri, url.clone());
23701 assert_eq!(
23702 params.text_document_position.position, complete_from_position,
23703 "marker `|` position doesn't match",
23704 );
23705 Ok(Some(lsp::CompletionResponse::Array(
23706 completions
23707 .iter()
23708 .map(|(label, new_text)| lsp::CompletionItem {
23709 label: label.to_string(),
23710 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23711 lsp::InsertReplaceEdit {
23712 insert: insert_range,
23713 replace: replace_range,
23714 new_text: new_text.to_string(),
23715 },
23716 )),
23717 ..Default::default()
23718 })
23719 .collect(),
23720 )))
23721 }
23722 });
23723
23724 async move {
23725 request.next().await;
23726 }
23727}
23728
23729fn handle_resolve_completion_request(
23730 cx: &mut EditorLspTestContext,
23731 edits: Option<Vec<(&'static str, &'static str)>>,
23732) -> impl Future<Output = ()> {
23733 let edits = edits.map(|edits| {
23734 edits
23735 .iter()
23736 .map(|(marked_string, new_text)| {
23737 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
23738 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
23739 lsp::TextEdit::new(replace_range, new_text.to_string())
23740 })
23741 .collect::<Vec<_>>()
23742 });
23743
23744 let mut request =
23745 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
23746 let edits = edits.clone();
23747 async move {
23748 Ok(lsp::CompletionItem {
23749 additional_text_edits: edits,
23750 ..Default::default()
23751 })
23752 }
23753 });
23754
23755 async move {
23756 request.next().await;
23757 }
23758}
23759
23760pub(crate) fn update_test_language_settings(
23761 cx: &mut TestAppContext,
23762 f: impl Fn(&mut AllLanguageSettingsContent),
23763) {
23764 cx.update(|cx| {
23765 SettingsStore::update_global(cx, |store, cx| {
23766 store.update_user_settings::<AllLanguageSettings>(cx, f);
23767 });
23768 });
23769}
23770
23771pub(crate) fn update_test_project_settings(
23772 cx: &mut TestAppContext,
23773 f: impl Fn(&mut ProjectSettings),
23774) {
23775 cx.update(|cx| {
23776 SettingsStore::update_global(cx, |store, cx| {
23777 store.update_user_settings::<ProjectSettings>(cx, f);
23778 });
23779 });
23780}
23781
23782pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
23783 cx.update(|cx| {
23784 assets::Assets.load_test_fonts(cx);
23785 let store = SettingsStore::test(cx);
23786 cx.set_global(store);
23787 theme::init(theme::LoadThemes::JustBase, cx);
23788 release_channel::init(SemanticVersion::default(), cx);
23789 client::init_settings(cx);
23790 language::init(cx);
23791 Project::init_settings(cx);
23792 workspace::init_settings(cx);
23793 crate::init(cx);
23794 });
23795 zlog::init_test();
23796 update_test_language_settings(cx, f);
23797}
23798
23799#[track_caller]
23800fn assert_hunk_revert(
23801 not_reverted_text_with_selections: &str,
23802 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
23803 expected_reverted_text_with_selections: &str,
23804 base_text: &str,
23805 cx: &mut EditorLspTestContext,
23806) {
23807 cx.set_state(not_reverted_text_with_selections);
23808 cx.set_head_text(base_text);
23809 cx.executor().run_until_parked();
23810
23811 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
23812 let snapshot = editor.snapshot(window, cx);
23813 let reverted_hunk_statuses = snapshot
23814 .buffer_snapshot
23815 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
23816 .map(|hunk| hunk.status().kind)
23817 .collect::<Vec<_>>();
23818
23819 editor.git_restore(&Default::default(), window, cx);
23820 reverted_hunk_statuses
23821 });
23822 cx.executor().run_until_parked();
23823 cx.assert_editor_state(expected_reverted_text_with_selections);
23824 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
23825}
23826
23827#[gpui::test(iterations = 10)]
23828async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
23829 init_test(cx, |_| {});
23830
23831 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
23832 let counter = diagnostic_requests.clone();
23833
23834 let fs = FakeFs::new(cx.executor());
23835 fs.insert_tree(
23836 path!("/a"),
23837 json!({
23838 "first.rs": "fn main() { let a = 5; }",
23839 "second.rs": "// Test file",
23840 }),
23841 )
23842 .await;
23843
23844 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23845 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23846 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23847
23848 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23849 language_registry.add(rust_lang());
23850 let mut fake_servers = language_registry.register_fake_lsp(
23851 "Rust",
23852 FakeLspAdapter {
23853 capabilities: lsp::ServerCapabilities {
23854 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
23855 lsp::DiagnosticOptions {
23856 identifier: None,
23857 inter_file_dependencies: true,
23858 workspace_diagnostics: true,
23859 work_done_progress_options: Default::default(),
23860 },
23861 )),
23862 ..Default::default()
23863 },
23864 ..Default::default()
23865 },
23866 );
23867
23868 let editor = workspace
23869 .update(cx, |workspace, window, cx| {
23870 workspace.open_abs_path(
23871 PathBuf::from(path!("/a/first.rs")),
23872 OpenOptions::default(),
23873 window,
23874 cx,
23875 )
23876 })
23877 .unwrap()
23878 .await
23879 .unwrap()
23880 .downcast::<Editor>()
23881 .unwrap();
23882 let fake_server = fake_servers.next().await.unwrap();
23883 let server_id = fake_server.server.server_id();
23884 let mut first_request = fake_server
23885 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
23886 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
23887 let result_id = Some(new_result_id.to_string());
23888 assert_eq!(
23889 params.text_document.uri,
23890 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23891 );
23892 async move {
23893 Ok(lsp::DocumentDiagnosticReportResult::Report(
23894 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
23895 related_documents: None,
23896 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
23897 items: Vec::new(),
23898 result_id,
23899 },
23900 }),
23901 ))
23902 }
23903 });
23904
23905 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
23906 project.update(cx, |project, cx| {
23907 let buffer_id = editor
23908 .read(cx)
23909 .buffer()
23910 .read(cx)
23911 .as_singleton()
23912 .expect("created a singleton buffer")
23913 .read(cx)
23914 .remote_id();
23915 let buffer_result_id = project
23916 .lsp_store()
23917 .read(cx)
23918 .result_id(server_id, buffer_id, cx);
23919 assert_eq!(expected, buffer_result_id);
23920 });
23921 };
23922
23923 ensure_result_id(None, cx);
23924 cx.executor().advance_clock(Duration::from_millis(60));
23925 cx.executor().run_until_parked();
23926 assert_eq!(
23927 diagnostic_requests.load(atomic::Ordering::Acquire),
23928 1,
23929 "Opening file should trigger diagnostic request"
23930 );
23931 first_request
23932 .next()
23933 .await
23934 .expect("should have sent the first diagnostics pull request");
23935 ensure_result_id(Some("1".to_string()), cx);
23936
23937 // Editing should trigger diagnostics
23938 editor.update_in(cx, |editor, window, cx| {
23939 editor.handle_input("2", window, cx)
23940 });
23941 cx.executor().advance_clock(Duration::from_millis(60));
23942 cx.executor().run_until_parked();
23943 assert_eq!(
23944 diagnostic_requests.load(atomic::Ordering::Acquire),
23945 2,
23946 "Editing should trigger diagnostic request"
23947 );
23948 ensure_result_id(Some("2".to_string()), cx);
23949
23950 // Moving cursor should not trigger diagnostic request
23951 editor.update_in(cx, |editor, window, cx| {
23952 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23953 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
23954 });
23955 });
23956 cx.executor().advance_clock(Duration::from_millis(60));
23957 cx.executor().run_until_parked();
23958 assert_eq!(
23959 diagnostic_requests.load(atomic::Ordering::Acquire),
23960 2,
23961 "Cursor movement should not trigger diagnostic request"
23962 );
23963 ensure_result_id(Some("2".to_string()), cx);
23964 // Multiple rapid edits should be debounced
23965 for _ in 0..5 {
23966 editor.update_in(cx, |editor, window, cx| {
23967 editor.handle_input("x", window, cx)
23968 });
23969 }
23970 cx.executor().advance_clock(Duration::from_millis(60));
23971 cx.executor().run_until_parked();
23972
23973 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
23974 assert!(
23975 final_requests <= 4,
23976 "Multiple rapid edits should be debounced (got {final_requests} requests)",
23977 );
23978 ensure_result_id(Some(final_requests.to_string()), cx);
23979}
23980
23981#[gpui::test]
23982async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
23983 // Regression test for issue #11671
23984 // Previously, adding a cursor after moving multiple cursors would reset
23985 // the cursor count instead of adding to the existing cursors.
23986 init_test(cx, |_| {});
23987 let mut cx = EditorTestContext::new(cx).await;
23988
23989 // Create a simple buffer with cursor at start
23990 cx.set_state(indoc! {"
23991 ˇaaaa
23992 bbbb
23993 cccc
23994 dddd
23995 eeee
23996 ffff
23997 gggg
23998 hhhh"});
23999
24000 // Add 2 cursors below (so we have 3 total)
24001 cx.update_editor(|editor, window, cx| {
24002 editor.add_selection_below(&Default::default(), window, cx);
24003 editor.add_selection_below(&Default::default(), window, cx);
24004 });
24005
24006 // Verify we have 3 cursors
24007 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
24008 assert_eq!(
24009 initial_count, 3,
24010 "Should have 3 cursors after adding 2 below"
24011 );
24012
24013 // Move down one line
24014 cx.update_editor(|editor, window, cx| {
24015 editor.move_down(&MoveDown, window, cx);
24016 });
24017
24018 // Add another cursor below
24019 cx.update_editor(|editor, window, cx| {
24020 editor.add_selection_below(&Default::default(), window, cx);
24021 });
24022
24023 // Should now have 4 cursors (3 original + 1 new)
24024 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
24025 assert_eq!(
24026 final_count, 4,
24027 "Should have 4 cursors after moving and adding another"
24028 );
24029}
24030
24031#[gpui::test(iterations = 10)]
24032async fn test_document_colors(cx: &mut TestAppContext) {
24033 let expected_color = Rgba {
24034 r: 0.33,
24035 g: 0.33,
24036 b: 0.33,
24037 a: 0.33,
24038 };
24039
24040 init_test(cx, |_| {});
24041
24042 let fs = FakeFs::new(cx.executor());
24043 fs.insert_tree(
24044 path!("/a"),
24045 json!({
24046 "first.rs": "fn main() { let a = 5; }",
24047 }),
24048 )
24049 .await;
24050
24051 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24052 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24053 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24054
24055 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24056 language_registry.add(rust_lang());
24057 let mut fake_servers = language_registry.register_fake_lsp(
24058 "Rust",
24059 FakeLspAdapter {
24060 capabilities: lsp::ServerCapabilities {
24061 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
24062 ..lsp::ServerCapabilities::default()
24063 },
24064 name: "rust-analyzer",
24065 ..FakeLspAdapter::default()
24066 },
24067 );
24068 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
24069 "Rust",
24070 FakeLspAdapter {
24071 capabilities: lsp::ServerCapabilities {
24072 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
24073 ..lsp::ServerCapabilities::default()
24074 },
24075 name: "not-rust-analyzer",
24076 ..FakeLspAdapter::default()
24077 },
24078 );
24079
24080 let editor = workspace
24081 .update(cx, |workspace, window, cx| {
24082 workspace.open_abs_path(
24083 PathBuf::from(path!("/a/first.rs")),
24084 OpenOptions::default(),
24085 window,
24086 cx,
24087 )
24088 })
24089 .unwrap()
24090 .await
24091 .unwrap()
24092 .downcast::<Editor>()
24093 .unwrap();
24094 let fake_language_server = fake_servers.next().await.unwrap();
24095 let fake_language_server_without_capabilities =
24096 fake_servers_without_capabilities.next().await.unwrap();
24097 let requests_made = Arc::new(AtomicUsize::new(0));
24098 let closure_requests_made = Arc::clone(&requests_made);
24099 let mut color_request_handle = fake_language_server
24100 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
24101 let requests_made = Arc::clone(&closure_requests_made);
24102 async move {
24103 assert_eq!(
24104 params.text_document.uri,
24105 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
24106 );
24107 requests_made.fetch_add(1, atomic::Ordering::Release);
24108 Ok(vec![
24109 lsp::ColorInformation {
24110 range: lsp::Range {
24111 start: lsp::Position {
24112 line: 0,
24113 character: 0,
24114 },
24115 end: lsp::Position {
24116 line: 0,
24117 character: 1,
24118 },
24119 },
24120 color: lsp::Color {
24121 red: 0.33,
24122 green: 0.33,
24123 blue: 0.33,
24124 alpha: 0.33,
24125 },
24126 },
24127 lsp::ColorInformation {
24128 range: lsp::Range {
24129 start: lsp::Position {
24130 line: 0,
24131 character: 0,
24132 },
24133 end: lsp::Position {
24134 line: 0,
24135 character: 1,
24136 },
24137 },
24138 color: lsp::Color {
24139 red: 0.33,
24140 green: 0.33,
24141 blue: 0.33,
24142 alpha: 0.33,
24143 },
24144 },
24145 ])
24146 }
24147 });
24148
24149 let _handle = fake_language_server_without_capabilities
24150 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
24151 panic!("Should not be called");
24152 });
24153 cx.executor().advance_clock(Duration::from_millis(100));
24154 color_request_handle.next().await.unwrap();
24155 cx.run_until_parked();
24156 assert_eq!(
24157 1,
24158 requests_made.load(atomic::Ordering::Acquire),
24159 "Should query for colors once per editor open"
24160 );
24161 editor.update_in(cx, |editor, _, cx| {
24162 assert_eq!(
24163 vec![expected_color],
24164 extract_color_inlays(editor, cx),
24165 "Should have an initial inlay"
24166 );
24167 });
24168
24169 // opening another file in a split should not influence the LSP query counter
24170 workspace
24171 .update(cx, |workspace, window, cx| {
24172 assert_eq!(
24173 workspace.panes().len(),
24174 1,
24175 "Should have one pane with one editor"
24176 );
24177 workspace.move_item_to_pane_in_direction(
24178 &MoveItemToPaneInDirection {
24179 direction: SplitDirection::Right,
24180 focus: false,
24181 clone: true,
24182 },
24183 window,
24184 cx,
24185 );
24186 })
24187 .unwrap();
24188 cx.run_until_parked();
24189 workspace
24190 .update(cx, |workspace, _, cx| {
24191 let panes = workspace.panes();
24192 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
24193 for pane in panes {
24194 let editor = pane
24195 .read(cx)
24196 .active_item()
24197 .and_then(|item| item.downcast::<Editor>())
24198 .expect("Should have opened an editor in each split");
24199 let editor_file = editor
24200 .read(cx)
24201 .buffer()
24202 .read(cx)
24203 .as_singleton()
24204 .expect("test deals with singleton buffers")
24205 .read(cx)
24206 .file()
24207 .expect("test buffese should have a file")
24208 .path();
24209 assert_eq!(
24210 editor_file.as_ref(),
24211 Path::new("first.rs"),
24212 "Both editors should be opened for the same file"
24213 )
24214 }
24215 })
24216 .unwrap();
24217
24218 cx.executor().advance_clock(Duration::from_millis(500));
24219 let save = editor.update_in(cx, |editor, window, cx| {
24220 editor.move_to_end(&MoveToEnd, window, cx);
24221 editor.handle_input("dirty", window, cx);
24222 editor.save(
24223 SaveOptions {
24224 format: true,
24225 autosave: true,
24226 },
24227 project.clone(),
24228 window,
24229 cx,
24230 )
24231 });
24232 save.await.unwrap();
24233
24234 color_request_handle.next().await.unwrap();
24235 cx.run_until_parked();
24236 assert_eq!(
24237 3,
24238 requests_made.load(atomic::Ordering::Acquire),
24239 "Should query for colors once per save and once per formatting after save"
24240 );
24241
24242 drop(editor);
24243 let close = workspace
24244 .update(cx, |workspace, window, cx| {
24245 workspace.active_pane().update(cx, |pane, cx| {
24246 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24247 })
24248 })
24249 .unwrap();
24250 close.await.unwrap();
24251 let close = workspace
24252 .update(cx, |workspace, window, cx| {
24253 workspace.active_pane().update(cx, |pane, cx| {
24254 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24255 })
24256 })
24257 .unwrap();
24258 close.await.unwrap();
24259 assert_eq!(
24260 3,
24261 requests_made.load(atomic::Ordering::Acquire),
24262 "After saving and closing all editors, no extra requests should be made"
24263 );
24264 workspace
24265 .update(cx, |workspace, _, cx| {
24266 assert!(
24267 workspace.active_item(cx).is_none(),
24268 "Should close all editors"
24269 )
24270 })
24271 .unwrap();
24272
24273 workspace
24274 .update(cx, |workspace, window, cx| {
24275 workspace.active_pane().update(cx, |pane, cx| {
24276 pane.navigate_backward(window, cx);
24277 })
24278 })
24279 .unwrap();
24280 cx.executor().advance_clock(Duration::from_millis(100));
24281 cx.run_until_parked();
24282 let editor = workspace
24283 .update(cx, |workspace, _, cx| {
24284 workspace
24285 .active_item(cx)
24286 .expect("Should have reopened the editor again after navigating back")
24287 .downcast::<Editor>()
24288 .expect("Should be an editor")
24289 })
24290 .unwrap();
24291 color_request_handle.next().await.unwrap();
24292 assert_eq!(
24293 3,
24294 requests_made.load(atomic::Ordering::Acquire),
24295 "Cache should be reused on buffer close and reopen"
24296 );
24297 editor.update(cx, |editor, cx| {
24298 assert_eq!(
24299 vec![expected_color],
24300 extract_color_inlays(editor, cx),
24301 "Should have an initial inlay"
24302 );
24303 });
24304}
24305
24306#[gpui::test]
24307async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
24308 init_test(cx, |_| {});
24309 let (editor, cx) = cx.add_window_view(Editor::single_line);
24310 editor.update_in(cx, |editor, window, cx| {
24311 editor.set_text("oops\n\nwow\n", window, cx)
24312 });
24313 cx.run_until_parked();
24314 editor.update(cx, |editor, cx| {
24315 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
24316 });
24317 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
24318 cx.run_until_parked();
24319 editor.update(cx, |editor, cx| {
24320 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
24321 });
24322}
24323
24324#[track_caller]
24325fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
24326 editor
24327 .all_inlays(cx)
24328 .into_iter()
24329 .filter_map(|inlay| inlay.get_color())
24330 .map(Rgba::from)
24331 .collect()
24332}